Skip to content

fix(space): defer terminal node-error handling until the delivery job dead-letters [#944] - #2488

Closed
lsm wants to merge 28 commits into
devfrom
space/space-defer-terminal-node-error-handling-until-the-delivery
Closed

lsm wants to merge 28 commits into
devfrom
space/space-defer-terminal-node-error-handling-until-the-delivery

Conversation

@lsm

@lsm lsm commented Aug 14, 2026

Copy link
Copy Markdown
Owner

A recoverable provider error during a delivery-driven node-agent turn no longer blocks the workflow node: registerCompletionCallback now treats a recoverable session.error (details.recoverable === true, delivery v2) as non-terminal, suppressing the post-error idle so the message_delivery retry can complete the node. Only the dead-letter settlement or a genuinely non-recoverable error blocks it. Follow-up to #2471 (Codex review r3771285317).

… dead-letters [#944]

A recoverable provider error during a delivery-driven node-agent turn fires
session.error before the message_delivery retry is scheduled. TaskAgentManager's
registerCompletionCallback treated any session.error as terminal (marking the
node execution blocked and tearing down listeners), so even when the delivery
layer's automatic retry (PR #2471) succeeded, the workflow could no longer
complete the node — one transient 5xx permanently blocked it.

Now a recoverable session.error (details.recoverable === true, with delivery v2
enabled) is non-terminal: it arms a deliveryRetryPending flag, and the idle that
follows it (the turn produced no result; a retry is pending) is suppressed rather
than treated as completion. Only the dead-letter settlement (session.error with
no details) or a genuinely non-recoverable error blocks the node. Gated on
delivery v2 so the legacy inline path keeps its first-error-blocks behavior.

Follow-up to PR #2471, which introduced the retry machinery this defers for.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59e9cf7cdd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1[1m] (z.ai)

Model: glm-5.1[1m] | Client: HyperNeo | Provider: z.ai

Verdict: REQUEST_CHANGES (1× P1, 4× P2, 6× P3). Posted as COMMENT because this is my own PR — the marker line below carries the verdict.

Recommendation: REQUEST_CHANGES

Premise & goal (verified against task #944 and #2471 thread r3771285317)

The ask: a recoverable error during the delivery-retry window should be invisible to Space; Space sees exactly one failure signal — the message_delivery dead-letter. The diff implements the decided direction (defer terminal handling until the dead-letter settlement), gated on delivery v2 so legacy keeps first-error-blocks, with a focused 7-test file. Scope is exactly the two files the task named — no scope creep. The core event contract checks out: exactly two session.error publishers exist (app.ts:1064 dead-letter with no details; error-manager.ts:515 with details = StructuredError, boolean recoverable set at error-manager.ts:115), the strict === true read degrades conservatively, and the error-before-idle ordering the flag relies on holds (the publishAsync microtask is queued during the awaited handleError before setIdle). The happy path — transient 5xx, retry succeeds, node completes on the retry's idle — is correct and pinned by tests.

The problem: the change treats "a recoverable session.error occurred" as equivalent to "the durable delivery layer will retry and will eventually idle again or dead-letter." That equivalence holds only for the narrow case this was written for. Every deviation either swallows a genuine completion (P1) or defeats the dead-letter's terminal signal (the first two P2s). The four inline threads carry the traces.

Findings

P1 — False-positive suppression eats the genuine completion idle; node hangs in_progress, invisible to every runtime sweep. A recoverable session.error can fire mid-turn from an unrelated subsystem while the turn still succeeds — the exact case the delivery layer itself guards against (agent-session.ts:2231-2239: "it came from an unrelated subsystem; this avoids a false-positive retry"; reachable via query-runner.ts:870-893, where a MESSAGE-category error is broadcast and the stream loop continues to a terminal result). The gate at task-agent-manager.ts:3724 arms deliveryRetryPending; the successful turn's completion idle is then suppressed at :3676; driveDeliveryTurn returns completed so no retry or dead-letter ever repays the flag. fired stays false and handleSubSessionComplete never runs. Recovery audit: handleAliveStuckExecutions (space-runtime.ts:7660) skips executions whose last SDK message classifies terminal (space-runtime.ts:7746-7749) — a successful turn's result row is exactly that — and the idle-status sweeps filter on status === 'idle' while this node sits in_progress. The run stalls until a daemon restart. Fix direction: before suppressing an idle, confirm a retry is actually pending for this session (an active delivery job for the session AND the turn produced no terminal result), or have the delivery layer publish an explicit retry-pending/turn-completed signal keyed to the messageUuid.

P2 — Error throttling breaks the deferral on the 4th+ identical recoverable error inside 10s. shouldThrottleError (error-manager.ts:357-393) drops the broadcast after 3 identical session:category:code errors per 10s. A throttled attempt does not arm the flag, but its terminal idle still publishes (query-runner.ts:1439-1441) — the completion path fires, marks the execution idle, and tears down both listeners. A later dead-letter's no-details session.error then hits if (fired) return and the node is never blocked; it sits idle with a failed kickoff row. Job backoff (1s/2s/4s) places fast-fail loops inside the window. Deriving retry-pendingness from job state rather than the broadcast (see the P1 fix) also fixes this.

P2 — Dead-letter repayment only exists for origin === 'space_inject' && role === 'turn'. The settlement publishes the terminal session.error solely for that shape (message-delivery-dead-letter.ts:51). A stranded kickoff re-enqueued by reconcileStrandedDeliveries carries origin: 'recovery' (message-delivery.ts:327); if that recovered job's retries exhaust after recoverable errors armed the flag, no terminal signal ever arrives — the flag is stranded and the node stalls until the ~15-min sweep. Pre-change, the first recoverable error would have blocked the node; this strictly widens that case.

P2 — The heuristic reuses a taxonomy the delivery layer has already publicly diverged from. details.recoverable === true is ErrorManager's classification, but delivery's own terminal classification (isTerminalTurnError + TERMINAL_TURN_ERROR_CATEGORIES, message-delivery.ts:169-184) treats auth as terminal despite recoverable === true. Space's correctness in the auth case is incidental — it works only because the immediate dead-letter fires a second, details-less session.error before any idle completes. Nothing in the code acknowledges that divergence, and the comment at :3705-3716 presents recoverable as a stable contract. The #944 design direction ("key off the dead-letter settlement") anticipated deriving this from delivery-layer state; consulting jobQueue.activeDeliveryMessageUuids(sessionId) (or an explicit event field) at suppression time would close the P1 and both preceding P2s with one mechanism.

P2 — Missing tests for the two failure modes above. (a) The P1 case: recoverable error mid-turn while the turn still succeeds — the sibling layer's own false-positive guard; nothing pins what Space does. (b) The sdkCount === 0 early return (:3661-3662) precedes the flag check (:3676) — startup-retry idles never consume the flag, so it survives to eat the first real idle of the eventual successful turn; the fixture hardcodes getSDKMessageCount: () => 5, so this ordering is never exercised.

P3 (×6):

  1. Import re-sort churn — ~106 of 169 changed lines are an ad-hoc alphabetical re-sort (partial: still two blocks with an interface interleaved), violating the surgical-change rule and polluting blame on a 5,800-line file.
  2. Stale deliveryRetryPending across session re-activation — the closure is reused via the sessionListeners.has early-return (:3635) on the :1574 re-register path; an unconsumed flag suppresses the next activation's first genuine idle. Narrow window.
  3. Kill-switch blast radius — the only rollback is HYPERNEO_MESSAGE_DELIVERY_V2=0, which disables all of delivery v2, far broader than the node-blocking behavior changed here.
  4. Docs — docs/features/message-delivery-v2.md has zero mentions of session.error; the now-load-bearing convention (no details = terminal for Space nodes) lives only in code comments. The event map (internal-event-bus.ts:395) deserves the comment too.
  5. Comment nits — "The QueryRunner broadcasts" (:167): the publisher is ErrorManager.broadcastError; and the bare-filename message-delivery-dead-letter.ts reference (:3645) sits two directories away.
  6. Test hygiene — pin HYPERNEO_MESSAGE_DELIVERY_V2 in beforeEach for the deferral tests (ambient =0 would silently flip them); assert listener/callback cleanup on the terminal path (it skips completionCallbacks.delete, unlike the idle path — pre-existing but now longer-lived); one composition case wiring the real settleMessageDeliveryDeadLetter through the bus into the manager would pin the no-details contract end-to-end.

Verified sound (no action)

  • Restart mid-retry-window self-heals: rehydrateSubSession re-registers the callback; the re-claimed job's idle completes the node.
  • Long-horizon agents and the Task Agent parent do not flow through registerCompletionCallback; LHA deliveries use origin: 'long_term_agent' — unaffected.
  • Recoverable errors still surface to users (web session-store toast) and state-projection-service — only node-blocking is deferred.
  • Sequential recoverable errors re-arm correctly; the dead-letter-after-deferral and v2-off legacy paths are pinned by tests.
  • Security: no new attack surface; recoverable is set by a fixed table, never provider-controlled; the deferral window is bounded (retry budget 8, exponential backoff, consumption timeouts); nothing new logged.
  • Rebase is faithful: callback area and test file are byte-identical to the reviewed commit; PR-head deltas are dev-side features only.

— Reviewer (glm-5.1[1m] via HyperNeo), head inspected: f0c0331

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
…sion.error [#944]

Review follow-up (PR #2488, REQUEST_CHANGES): the flag-based deferral armed
`deliveryRetryPending` from the `session.error` broadcast, which is both
throttled (ErrorManager drops the 4th identical error per 10s — a fast-fail
retry loop could then leak the post-error idle as a completion) and ambiguous
(an unrelated recoverable error mid-turn suppressed a successful turn's
completion idle with no retry/dead-letter ever coming, hanging the node).

The deferral now derives retry-pendingness from the durable job row:

- An idle while `activeDeliveryMessageUuids(sessionId)` is non-empty is not a
  completion — covers both sides of the turn-end race (failed turn idles
  before the job's throw→backoff; successful turn idles just before the job
  row completes).
- New processor `onComplete` lane hook publishes `session.delivery_settled`
  (real `processing`→`completed` only; parks excluded), which completes the
  node when nothing else is in flight. This repays the suppressed idle after a
  successful retry and completes a successful turn that carried an unrelated
  recoverable error.
- New `session.delivery_failed` (published from `onDead` for EVERY dead
  delivery, unlike the settlement's `space_inject`-origin-gated session.error)
  blocks the node when `role === 'turn'` — recovery-origin re-enqueued
  kickoffs that dead-letter with no session.error no longer hang in_progress.
  Steers never block.
- The recoverable `session.error` classification is now advisory: a
  misclassification (ErrorManager's taxonomy diverges from delivery's on auth)
  only delays the block until the immediate dead-letter; `delivery_failed`
  guarantees the terminal outcome.

Also documents the contract in docs/features/message-delivery-v2.md §18 and
adds tests for the throttled-error, unrelated-error, sdkCount===0, steer-vs-
turn dead-letter, and real-settlement composition cases.
@lsm

lsm commented Aug 15, 2026

Copy link
Copy Markdown
Owner Author

Review response — 89ff285 (rebased onto the dev merge head; all inline threads answered in place).

The flag-based mechanism is removed entirely. Per your suggested single mechanism, retry-pendingness now derives from delivery-layer state: an idle completes only when jobQueue.activeDeliveryMessageUuids(sessionId) is empty, and the job's own settlement decides terminality — a new processor onComplete lane hook publishes session.delivery_settled (real processing→completed only; parks excluded) to complete the node, and onDead publishes session.delivery_failed for every dead delivery (role-gated: turn blocks, steer doesn't), closing the recovery-origin gap. This closes the P1 and all three P2s; the ErrorManager taxonomy is now advisory only — a misclassification delays the block until the immediate dead-letter, it can't flip the outcome.

Requested tests added: mid-turn error + successful turn, sdkCount===0 ordering on both completion paths, steer-vs-turn dead-letter, and a composition case wiring the real settleMessageDeliveryDeadLetter + lane events through the bus.

P3 dispositions:

  • Flag-on-activation, import churn — resolved by the redesign (no flag, one new import).
  • message-delivery-v2.md — added §18 documenting the full node-completion contract.
  • Kill-switch blast radius — pushing back: with v2=0 the retry machinery this defers for doesn't exist, so first-error-blocks is the correct legacy semantic, not a regression.
  • Comment nits / test hygiene — addressed in the rewrite; happy to take specific ones if anything still reads wrong.

Verified: 12 deferral tests + 146 related (delivery-v2, dead-letter, processor, runtime completion/terminal-error) pass; tsc/oxlint/biome/knip clean via pre-commit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89ff285a5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/app.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
… lost settles [#944]

Codex follow-up on the job-row deferral (PR #2488 round 2):

- P1: a steer settles at mid-turn CONSUMPTION while the agent is still
  working; completing the node on it tore down the error listeners mid-work,
  so a later turn failure could no longer block the node.
  `session.delivery_settled` now carries `role`, and only a TURN settle
  completes the node — the owning turn's settle always follows (its job
  completes at turn end).
- P2: a dead-lettered STEER that was the last active job left a suppressed
  terminal idle unrepaid (the turn's settle was ignored while the steer was
  in flight). The delivery_failed handler now repays the suppressed
  completion for steers when nothing else is in flight, instead of stranding
  the node; steers still never block.
- P2: a daemon crash between the job row's completion and the settle
  publication loses the signal permanently (the idle was already suppressed,
  the completed job is never re-claimed, and restart rehydration publishes no
  fresh idle). A one-shot unref'd reconciliation (default 30s, operator-
  tunable via HYPERNEO_DELIVERY_SETTLE_RECONCILE_MS) completes an idle session
  with history and no active delivery jobs — activation enqueues its job
  within milliseconds, so a still-jobless idle session this late is genuinely
  stranded.

Docs updated (message-delivery-v2.md §18); tests added for all three cases
(16 deferral tests, stable across repeated runs for timer races).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20886d412a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/app.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
…turns [#944]

Codex P2 (round 3): the processor's onComplete hook published
session.delivery_settled for every completed message_delivery job, but the
handler also completes jobs with non-success outcomes — a reclaimed ACP turn
whose message is still `submitted` returns { outcome: 'skipped' }, plus
aborted / no_content / archived / stale_attempt. Settling those let
TaskAgentManager complete a workflow node whose prompt was never accepted or
processed.

New isCompletedTurnResult(result) predicate (outcome === 'completed', which
also admits the completed-with-turn_terminated marker case — the turn
genuinely ended on a prior attempt) gates the publication in app.ts. Parks
never reach it (a requeued row makes the auto-complete a no-op, so onComplete
does not fire). Unit-pinned across all outcome shapes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7bf5544fc3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/app.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
…tion [#944]

Codex P2 (round 3): the delayed reconciliation could not distinguish a lost
settlement from a kickoff that was never enqueued. On a reused node-agent
session the callback registers before the next kickoff's job is enqueued; a
daemon exit in that window rehydrates an idle session with a historical
transcript and no active job, and the timer would falsely complete the new
execution whose turn never ran.

The reconcile now requires SDK output postdating the execution's start (new
SDKMessageRepository.hasMessagesSince) — a historical transcript is not proof
the current activation ran. Consistent with the active-job check: an enqueued
kickoff has a job row and is suppressed there, so "no job + output since
start" is exactly the lost-settle shape. The timer body is also fully
defensive now (best-effort by nature; a DB error must not surface as an
unhandled timer exception).

Doc §18 updated; tests: positive reconcile, reused-session negative, and
timer-disarm hygiene between tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59ae548e20

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
…ers [#944]

Codex P1s (round 4):

- The recoverable session.error deferral now requires an ACTIVE delivery job:
  errors from non-delivery work (rehydration's direct streaming start and
  tool-continuation replays have no job) cannot be retried and no
  delivery_failed will repay them — without the gate their idle completed the
  node despite failed work.
- A SETTLED steer repays a suppressed terminal idle: in the ACP shape the
  owning turn settles while its steer is parked awaiting acceptance, so when
  the accepted steer later completes ('consumed'/'already_consumed') as the
  last active job, its settlement is the only repayment. New
  isSettledSteerResult publishes those outcomes (role-aware filter in the
  onComplete hook); the manager completes on a steer settle only when the
  session is live-idle and nothing else is in flight — never mid-turn. The
  dead-steer repayment now carries the same idle guard.

Doc §18 updated; tests: settled-steer repayment (ACP), steer-settle while
processing, recoverable error with no active job blocks.
@lsm
lsm force-pushed the space/space-defer-terminal-node-error-handling-until-the-delivery branch from 93a04f5 to 5da7282 Compare August 15, 2026 02:34
…944]

Codex P2 (round 5): the delayed reconciliation used transcript evidence (any
SDK message postdating the execution start), so a turn whose job row went
dead before a crash — losing the fire-and-forget session.delivery_failed
publication — would RECONCILE AS COMPLETE on restart (dead rows are absent
from the active-job set and the failed turn's partial output satisfies the
transcript check), marking a failed node idle instead of blocked.

New JobQueueRepository.deliveryTurnOutcomeSince(sessionId, sinceMs) reads the
durable terminal outcome of the session's turn jobs settled in the current
activation: 'dead' (any dead-lettered turn → the reconcile BLOCKS, repaying
the lost publication), 'completed' (a genuinely successful turn result →
completes), else null (declines — covers the reused-session /
kickoff-never-enqueued shape and non-success outcomes that
isCompletedTurnResult already filters). Transcript-based evidence
(hasMessagesSince) is removed — the job row is strictly stronger.

Tests: lost-settle completes, lost-dead-letter blocks, never-enqueued
declines, active/processing declines. Doc §18 updated.
Comment thread packages/daemon/src/app.ts
Comment thread packages/daemon/src/app.ts
Comment thread packages/daemon/src/app.ts
Comment thread packages/daemon/src/lib/internal-event-bus.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/app.ts Outdated

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1[1m] (z.ai)

Model: glm-5.1[1m] | Client: HyperNeo | Provider: z.ai

Re-reviewed at head 9195e2d33 (rounds 1–5: 89ff285 → 20886d4 → 7bf5544 → 59ae548 → 5da7282 → 9195e2d). The redesign landed exactly on the mechanism my first round asked for: retry-pendingness derives from the durable job row, and both terminal outcomes flow through the job lifecycle (delivery_settled completes / delivery_failed + settlement session.error blocks). Every P1/P2 from all five rounds — throttled 4th error, unrelated recoverable error during a successful turn, recovery-origin dead kickoffs, steer mid-work completion, settled-steer repayment, non-delivery-work errors, crash-lost publications — is fixed and pinned by tests. The crash reconciliation now derives from the durable job row (deliveryTurnOutcomeSince), which is strictly stronger than the transcript evidence it replaced. 22 deferral tests exercise the real internalEventBus, real repos, and the real settlement composition. No correctness, security, performance, or compatibility findings remain. Remaining items are P3 polish only.

Verified fixed (all prior rounds)

  • Throttle immunity / job-row keying — suppression reads activeDeliveryMessageUuids, not the error broadcast; ErrorManager's 4th-identical-error silence can no longer leak a completion or drop a block. Pinned by the throttled-4th test.
  • Steer lifecycle — steer settles are repayment-only (live-idle + nothing-in-flight), dead steers repay but never block, dead turns block regardless of origin, and the ACP awaiting-acceptance park budget exhausts into DeadLetterImmediatelyError → delivery_failed, so the steer branches all terminate.
  • Non-delivery work — recoverable-error deferral now requires an active job; rehydration streaming / tool-continuation replays keep first-error-blocks.
  • Crash windows — deliveryTurnOutcomeSince (durable row, scoped to the activation via startedAt) repays both a lost settle and a lost dead-letter; the 7-day job retention vs 30s reconcile window leaves no interference.
  • Processor contract — onComplete fires only on a real claim-fenced processing→completed transition; parks/requeues return null and cannot fire it; other lanes are untouched.
  • Security posture — role is DB-arbitrated (not caller-choosable), the subscription is session-scoped, parent resolution dies on unknown sessions, and the blocked path writes the constant DEAD_LETTER_SESSION_ERROR instead of provider-derived text (strictly better than the pre-PR event.error).

P3 findings (non-blocking polish)

  1. Event payload types are looser than the source unions (internal-event-bus.ts:411,427): role?: string; origin?: string while MessageDeliveryRole = 'turn' | 'steer' and MessageDeliveryOrigin are closed unions exported from message-delivery.ts:49-57. Typing them as the unions would give compile-time protection on event.role !== 'turn' consumers and match the precision of the rest of the SessionEvents map.
  2. Stale comments after the listener expansion (task-agent-manager.ts:3864 "self-unsubscribes both listeners" — there are now four plus the reconcile timer; task-agent-manager.ts:31 file-header completion description covers only the idle path).
  3. Import re-sort churn still present (app.ts:6-12 region, ~145 of 204 changed lines in app.ts are the alphabetical re-sort; same in task-agent-manager.ts): no repo tooling enforces import order (no biome assist/organizeImports, oxlint has three rules, CI runs formatter only). Not functional — but it inflates the diff on the two most-touched files.
  4. Test origin value not from the real union (task-agent-delivery-retry-deferral.test.ts:413): the recovery-origin case pins origin: 'system'; the real re-enqueue path writes origin: 'recovery' (message-delivery.ts:343). Any non-space_inject value exercises the gate identically today, but the real value would keep the test honest if origin-specific logic ever lands.

P3 push-back accepted

  • v2=0 kill-switch semantics: agreed — with the retry machinery absent, first-error-blocks is the correct legacy semantic, not a regression. The docs and code comments now scope the deferral correctly to v2-on with an active job.

Recommendation: APPROVE

- Event payload fields typed with the closed MessageDeliveryRole /
  MessageDeliveryOrigin unions instead of string (compile-time protection for
  the role-gated branches).
- Stale "both listeners" comment and the file-header completion-detection
  description updated for the four-listener + reconcile-timer reality.
- Test fixture origin 'system' → 'recovery' (matches
  reconcileStrandedDeliveries' re-enqueue origin).
- Import re-sort churn reverted: app.ts and task-agent-manager.ts import
  blocks restored to dev's order (my local biome version re-sorted them
  differently from the version that formatted dev); diffs vs dev are now the
  functional changes only (app.ts 55+/1-).

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1[1m] (z.ai)

Model: glm-5.1[1m] | Client: HyperNeo | Provider: z.ai

Re-review of the P3 polish commit baf2c8b01 (head advanced past the approved 9195e2d33).

Delta verification (approved → new head, mechanically checked):

  • app.ts — body byte-identical between the two commits; the change is a pure import-block reorder. Diff vs dev is now exactly the approved functional change (55+/1-: the session.delivery_failed publish on every dead-letter + the role-gated onComplete session.delivery_settled publish).
  • task-agent-manager.ts — import reorder + the two comment updates (four-listener + reconcile-timer reality). The one non-reorder nuance (NodeExecutionRepository type-only → value import) exactly restores dev's own form at dev:120.
  • internal-event-bus.ts — type-only: role/origin narrowed from string to the closed MessageDeliveryRole / MessageDeliveryOrigin unions. Both events' only publishers (app.ts:1055, app.ts:1118) pass already-union-typed payload fields, so no other call site is affected.
  • test fixture — origin: 'system' → 'recovery': not merely cosmetic, it was required by the new typing ('system' is not a member of MessageDeliveryOrigin), and it correctly matches reconcileStrandedDeliveries' re-enqueue origin (message-delivery.ts:343).

All four P3 items are addressed as claimed. All 24 review threads resolved. CI green on the completed shards of the new head (remaining shards pending; the delta cannot change behavior).

Recommendation: APPROVE

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: baf2c8b012

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/storage/repositories/job-queue-repository.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
lsm added 2 commits August 14, 2026 23:27
…off [#944]

Codex P2 (round 6): createSubSession's fire-and-forget pending-message flush
can deliver a PEER handoff as a role:'turn' job before the node's actual
kickoff is enqueued. The crash reconciliation accepted any turn settled since
execution.startedAt, so a daemon exit in that window treated the peer turn as
the activation's successful kickoff and marked the unstarted node idle.

spawnWorkflowNodeAgentForExecution now stamps the kickoff's message UUID onto
the owning node execution (data.kickoffMessageUuid, persisted via the
execution row — no schema change), and deliveryTurnOutcomeSince takes an
optional messageUuid so the reconciliation only accepts THAT delivery's
settlement. Without a stamped kickoff (older rows, or the crash-before-enqueue
window itself) the reconciliation declines — conservative: a kickoff that
lost the turn arbiter to a flushed peer settles as a steer and correctly does
not qualify.

Tests: flushed-peer-turn negative (no stamp → declines), plus the existing
lost-settle / lost-dead-letter / never-enrolled cases now keyed on the uuid.
…urn [#944]

Codex P2s (round 7):

- The crash reconciliation no longer checks the CURRENT v2 flag: its decision
  reads durable rows written while v2 WAS enabled, so an operator restarting
  with the rollback switch after a crash still gets the repair — the settled
  job row is never re-claimed and rehydrating an idle session publishes no
  fresh idle, so the node would otherwise strand exactly when rolling back.
- The recoverable-error deferral now requires a delivery job currently being
  DRIVEN (claimed `processing`, via new
  JobQueueRepository.hasProcessingDeliveryForSession), not any active job: a
  merely-queued job cannot retry a rehydration tool-continuation replay that
  errors while it waits, and deferring such an error let a later unrelated
  settlement complete the node instead of blocking it.

Doc §18 updated; tests: queued-only continuation-replay error blocks, and the
reconcile repairs a stranded settle with v2 switched off.
@lsm
lsm force-pushed the space/space-defer-terminal-node-error-handling-until-the-delivery branch from 557d0b1 to 485be21 Compare August 15, 2026 03:33
lsm added 3 commits August 15, 2026 00:06
…concile [#944]

Codex findings (round 11):

- P2: the kickoff stamp was written AFTER the inject, but the inject awaits
  SDK consumption — a crash in that window stranded an unstamped execution
  with delivery underway (both the live settle and the reconcile would reject
  it forever). The uuid is now generated and stamped BEFORE the inject and
  passed as the injection's explicit message id.
- P1: a kickoff that lost the arbiter to a flush settles as a consumed steer;
  the owning peer turn's settle carries a uuid ≠ the stamp and was rejected,
  and a turn longer than the 30s reconcile delay left the one-shot timer
  already spent — the node stranded. The settle's TURN branch now consults
  the durable correlated outcome on mismatch (kickoff-consumed-steer + this
  turn terminal → complete; dead → block), and the reconcile RE-ARMS while a
  delivery is in flight or the session is not idle.
- P2: the recoverable-error deferral counted any processing row; a claimed
  STEER (mid-turn feed) must not enable it — the predicate is now
  role='turn'-filtered, so only the driving turn owns the retry.

Tests: steer-only processing blocks; owning-turn settle completes and
no-durable-settlement declines; reconcile re-arm across a long turn.
…r repayment [#944]

Codex findings (round 12):

- P2: an older stranded message re-enqueued by the startup reconciler as an
  origin:'recovery' turn job on a reused session dead-lettering must not fail
  the CURRENT activation — the delivery_failed turn branch now correlates
  event.messageUuid with the execution's stamped kickoff (an unstamped
  execution declines; the kickoff keeps its uuid across a recovery re-enqueue,
  so a genuine kickoff dead-letter still blocks).
- P1: steer repayment (settle and dead-letter paths) now requires
  current-activation evidence — the kickoff's own durable settlement is
  'completed'. A restored old steer settling 'already_consumed' in the
  pre-kickoff window (callback registered, kickoff not injected) on a reused
  session no longer completes the new execution off the historical
  transcript.

Shared kickoffDeliveryOutcome helper carries the correlation (no-execution vs
unstamped vs stamped+outcome).

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1[1m] (Z.ai)

Model: glm-5.1[1m] | Client: HyperNeo | Provider: Z.ai

Re-review at head e4b117ebe (rounds 6–12 since the earlier APPROVE at 9195e2d33). The head moved three times while I read (c05004abc → 8caf83493 → d2b34196 → e4b117ebe); this verdict is against what I read at e4b117ebe.

Verified fixes

Every finding I independently confirmed on earlier heads is properly closed at this one:

  • Pre-stamped kickoff — UUID generated → execution stamped → inject with it as the explicit id. The unstamped-crash window is gone. (d5MR)
  • Re-armable reconcile — re-arms while the session is processing or delivery is in flight; the long-peer-turn strand is repaired after the turn ends. (d5MU)
  • Owning-turn settle with durable correlation — a uuid-mismatched turn settle consults deliveryTurnOutcomeSince correlated mode: kickoff-as-consumed-steer + owning turn terminal → complete, dead → block, else decline. (d5MU)
  • Turn-only deferral predicate — role='turn' filter in hasProcessingDeliveryForSession; a claimed steer can no longer suppress a continuation-replay error. (d5MX)
  • Dead-turn correlation — delivery_failed turn events must match the stamped kickoff; unrelated recovery re-enqueues no longer false-block, unstamped executions decline, and the genuine kickoff keeps its uuid across a recovery re-enqueue. (d8oJ)
  • Activation-evidenced steer repayment — both steer repayment paths now require the kickoff's own durable completed settlement via kickoffDeliveryOutcome; the pre-kickoff restored-steer window can no longer complete off the historical transcript. (d8oM)

The new kickoffDeliveryOutcome helper centralizes the correlation the settle / dead-letter / repayment decisions share — good factoring, tested (35 manager tests + repo-level outcome tests), §18 doc in sync. All review threads resolved.

Nit (P3, non-blocking)

One stale doc comment: /** Returns a sub-session … */ at task-agent-manager.ts:3109 is orphaned — round 7's hasProcessingDeliveryJob insertion detached it from getSubSession (now at :3149, undocumented), and round 12's helper landed directly under it. Drop the dangling line and re-attach it to getSubSession (or fold into the helper's doc).

Verdict

Approve. Rounds 6–12 converge on a single coherent invariant — only the activation's own kickoff settlement (as turn, or consumed steer under a terminated owning turn) decides the node — and every failure mode raised across six review rounds has a landed fix with a regression test. CI is still running on this head at review time; please let it finish green before merge.

Recommendation: APPROVE

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e4b117ebe5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/storage/repositories/job-queue-repository.ts Outdated
lsm added 2 commits August 15, 2026 00:23
…lost kickoffs [#944]

Codex P2s (rounds 13-14):

- The dead-letter settlement's uuid-less fallback session.error (published
  for space_inject turns) bypassed the uuid correlation and blocked the node
  for an unrelated flushed peer turn's dead-letter. The error handler now
  attributes it via the durable rows (new hasDeadTurnExcept): it blocks only
  when the kickoff itself did not already settle and no dead non-kickoff turn
  exists.
- The consumed-kickoff-steer recursion aggregated every turn since
  activation, so a later unrelated promoted-steer dead-letter blocked an
  activation whose kickoff was consumed successfully. The owner is now
  scoped: the earliest turn terminal at/after the steer's consumption.
- A stamped kickoff whose delivery row never landed (crash between the
  execution update and the enqueue) left the reconcile declining with nothing
  left to re-arm. It now BLOCKS with an explicit lost-kickoff reason so the
  runtime's blocked-execution re-spawn machinery re-activates the node.

Tests: fallback-error attribution (declines for non-kickoff dead, blocks kept
for genuine kickoff failures), owner-scoped shapes, lost-kickoff re-spawn.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c7ec2503d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/storage/repositories/job-queue-repository.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
lsm added 2 commits August 15, 2026 00:38
#944]

Codex P1 (round 15): the owner-scoped query keyed on the steer row's
completed_at, but an accepted ACP steer stays parked and completes
('already_consumed') on a later claim — after its owning turn already
finished — so the owner's completed_at preceded the steer's and the match
missed (or picked a later unrelated turn), leading the reconciliation to
block and re-spawn a node whose kickoff was consumed successfully.

The owner is now matched by claim window: the turn with started_at <= the
steer job's CREATION <= completed_at (the turn active when the steer entered
delivery), falling back to the earliest terminal turn at/after creation for
retried owners whose last claim postdates the steer. Repo test pins the ACP
shape.
…ow [#944]

Round-16 review fixes (three P2s):

- Stamp the kickoff uuid on the node execution BEFORE the awaited
  goal/memory construction, not between it and the inject. A daemon exit
  during those awaits previously rehydrated an unstamped in_progress
  execution that neither the live settle path nor the reconciliation
  could ever resolve; the recovery blocked forever.
- A stamped kickoff whose delivery row is missing is no longer
  unconditionally "lost pre-enqueue": consult the kickoff's persisted
  sdk_messages row (it survives job-queue cleanup). send_status
  'consumed'/'failed' → complete (the job row aged out of the 7-day
  retention window, or the activation used the legacy inline path with
  no job row at all — finished work must not be re-spawned); still
  'enqueued' or a gone row → block for re-spawn as before.
- Tests: lost pre-enqueue still blocks (cleared processing stand-ins so
  the reconcile path is exercised); expired settlement (consumed row,
  aged-out job row) completes; legacy v2-off activation with no job row
  completes via the message row.

Doc §18 updated for the pre-await stamp and the send_status
classification.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4103b0643

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (Z.ai)

Model: glm-5.1 | Client: HyperNeo | Provider: Z.ai

Reviewing head f4103b064 (rounds 15–16 landed while I was reviewing 1c7ec2503; findings re-anchored and re-verified against the current head — the round-16 pre-stamp-before-awaits change and the persisted-message-row classification are incorporated below).

Verdict summary

The contract itself is sound and now well-hardened: stamp-before-await closes the unstamped-crash window, the claim-window steer-owner match (round 15) fixes the ACP already_consumed mis-attribution, and classifying a missing kickoff job row via the persisted sdk_messages row (round 16) correctly rescues aged-out and legacy-inline activations. The fired mutual exclusion, four-listener teardown, single-flight re-arm, and claim-fenced onComplete gating are all correct as written; all new SQL is parameterized; no new security surface. One P1 remains plus two P2s and two P3s.

P1 — lost-kickoff BLOCK false-positives on deferred kickoffs (rate-limit / parent-limited)

task-agent-manager.ts:3941-3963: the reconcile's terminal heuristic — stamped kickoff + deliveryTurnOutcomeSince → null + nothing active + idle — cannot distinguish "crash between stamp and enqueue" from "kickoff legitimately persisted as deferred". injectMessageIntoSession (~line 5048) defers even an immediate kickoff when inRateLimitCooldown || parentLimited: it persists the sdk_messages row carrying the stamp uuid with sendStatus: 'deferred' and enqueues no message_delivery job. Round 16's classification only completes on 'consumed' | 'failed' — a 'deferred' row still falls through to fireTerminalError('…lost before enqueue…').

Reachable two ways: (a) live: re-activation on a reused session whose parent task is rate/usage-limited — parentLimited keys off task status while the session itself sits idle, so nothing else holds the reconcile off; (b) after restart: the code's own comment (~line 5040) notes rehydration flips a persisted rate_limit_cooldown session to idle while the parent-task pause is restored only by a later sweep. Either way the 30s shot blocks a healthy-but-paused node, the re-spawn machinery issues a fresh kickoff, and when the deferred row later replays the node gets a duplicate kickoff.

Fix: a sendStatus === 'deferred' row should re-arm (or decline), not block — the durable evidence to disambiguate is the same lookup round 16 already added (getDeliveryContent(sessionId, stampUuid)). Inline comment attached.

P2 — re-activation window reads the previous activation's stamp

task-agent-manager.ts:1319 and :1617 both register listeners before spawnWorkflowNodeAgentForExecution overwrites data.kickoffMessageUuid (~line 1328). In that window (spanning the awaited goal/memory construction) the execution still carries activation N−1's stamp, whose durable row is already completed. A session.delivery_settled for a flush-driven peer turn then takes the mismatch branch (~line 3851), deliveryTurnOutcomeSince(…, oldStampUuid) returns 'completed' — correlated mode ignores sinceMs, so nothing bounds it to this activation — and completeFromDeliveryState fires with no active jobs and sdkCount > 0 on the reused session: the new activation is marked complete before its kickoff runs. Narrow (needs a peer turn settling inside a seconds-wide window) but it is exactly the race class this PR closes. Clearing data.kickoffMessageUuid at the start of re-activation closes it.

P2 — the reconcile's catch surrenders permanently

task-agent-manager.ts:3967-3972: a transient throw at the single shot (e.g. SQLITE_BUSY on the claim-contended job_queue table) logs at debug and never re-arms — yet the design premise is that no future event will re-publish the suppressed idle, so the crash-repair this timer exists to provide is lost forever. Every decline path re-arms; the error path should too, bounded the same way.

P3 — dead second check in the delivery_failed turn path

task-agent-manager.ts:4020-4023: when kickoff.kickoffMessageUuid === null, the preceding event.messageUuid !== kickoff.kickoffMessageUuid (string vs null) already returned, so the unstamped-decline check is unreachable-for-effect. Harmless; misleading to read.

P3 — session-scoped branch of deliveryTurnOutcomeSince is production-dead

job-queue-repository.ts:585-603: every production caller passes messageUuid (or declines before calling); the no-uuid mode runs only in its own test. Dead query paths on job_queue (unindexed json_extract scan) invite drift from the correlated mode that actually runs — drop it or wire a real caller.

Tests / coverage note (no severity)

The 37 manager tests + 10 repo-level deliveryTurnOutcomeSince shapes (including the round-15 ACP claim-window pin) are genuinely strong. Gap: hasDeadTurnExcept and hasProcessingDeliveryForSession have no repo-level tests — the manager tests exercise them only as stand-ins, so a SQL regression (e.g. wrong completed_at bound) would pass the whole suite. Worth two seeded rows each in job-queue-repository.test.ts.

Verified clean (spot-checks)

SQL fully parameterized (the conditional exceptUuid fragment is a constant-string idiom); kickoff uuid server-generated and confined to bound params; no new external surface (events stay on the in-process bus, sole subscriber TaskAgentManager); dead rows always carry completed_at (fail/markDead); restoreFromDatabase indeed broadcasts nothing (reconcile premise holds); recovery re-enqueue preserves the message uuid (correlation claim holds); complete() null-on-park correctly gates onComplete; 2 ** retryCount is semantics-preserving.

Recommendation: REQUEST_CHANGES

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/storage/repositories/job-queue-repository.ts Outdated
…ation [#944]

Round-17 review fixes (P1 deferred kickoffs, 2 P2s, 2 P3s) plus the
follow-up Codex P2 on the round-16 classification:

- P1: a kickoff deferred by a rate-limit cooldown / parent-task limit is
  persisted 'deferred' with no delivery job. The rowless classification
  treated it as lost-before-enqueue and blocked a healthy-but-paused
  node (then re-spawned a duplicate when the cooldown re-enqueued).
  'deferred' now declines and re-arms.
- Codex P2: 'failed' send_status means the delivery terminalized as
  failed — it now BLOCKS for re-spawn instead of completing. 'consumed'
  alone proves only SDK acknowledgment (the flip precedes the turn), so
  it completes only when hasTerminalResultAfter finds a success terminal
  result after the kickoff's consumption. A turn that crashed mid-run
  (consumed, no result) blocks for re-spawn.
- P2: clear data.kickoffMessageUuid synchronously before a re-activation's
  first await. The fresh completion callback registers inside
  createSubSession's session-reuse branch before the new stamp lands, so
  a peer-turn settle in that window could correlate the PREVIOUS
  activation's stamp and false-complete the new activation (and a
  kickoff:false re-activation never restamps at all).
- P2: a thrown reconcile shot re-arms once (bounded: two consecutive
  throws give up; a successful shot resets the budget) so a transient
  SQLITE_BUSY cannot surrender the crash repair permanently.
- P3: drop the unreachable unstamped-decline check in the
  delivery_failed turn branch (the uuid-mismatch return already covers
  the null case).
- P3: drop the production-dead session-scoped mode of
  deliveryTurnOutcomeSince along with its now-unused sinceMs parameter —
  every caller correlates by the stamped kickoff uuid, which IS the
  activation scope now that re-activations clear it. Add repo-level
  tests for hasDeadTurnExcept and hasProcessingDeliveryForSession
  (manager tests used stand-ins only).

Tests: deferred declines, failed blocks, consumed-without-result
blocks, transient-throw re-arm recovers, persistent throw is bounded.
Doc §18 updated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: efcdfdd0cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
… retry-safe [#944]

Round-18 review fixes (four Codex P2s):

- Stamp the kickoff BEFORE the activation's first await: the clear+stamp
  is now one write issued before the createSubSession call (whose
  session-reuse branch registers the fresh completion callback and whose
  setup awaits), instead of after it and the attachment check. A daemon
  exit during session reuse, MCP reinjection, or the attachment check
  now rehydrates a stamped execution whose reconcile can classify it.
- Guard the missing-row classification with an in-memory pre-inject
  marker (preInjectKickoffExecutions, released in the spawn finally):
  setup + goal/memory construction can outlast the reconcile delay (a
  cold embedder load), and the reconcile must not fire the lost-kickoff
  block while the inject is still pending — that ran work under a
  blocked execution. A daemon death empties the marker so the restart
  shot still blocks for re-spawn.
- Reset the catch-side retry budget after EVERY non-throwing reconcile
  shot, declines included (the shot body moved into runReconcileShot so
  the reset covers the early returns): previously one transient failure
  permanently consumed the allowance even after many healthy checks.
- Retain the terminal decision until it persists: fireTerminalError no
  longer tears down the listeners before handleSubSessionError resolves
  — a transient SQLite failure on the blocked-status update un-consumes
  the decision (fired=false), keeps the listeners, and re-arms the
  reconcile to retry from the durable dead row.

Tests: alternating-throw chain keeps repairing (budget reset on
declines), mid-construction activation declines then classifies after
the guard releases, failed terminal persist retries the block.
Doc §18 updated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7abe5d0568

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/storage/repositories/job-queue-repository.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
…e drive [#944]

Round-19 review fixes (two P1s, one P2):

- P1 (steer consumption attribution): a steer can sit unclaimed past its
  creation-time turn and be consumed by a LATER turn (feedDeliverySteer
  runs inside whatever turn is live at claim). deliveryTurnOutcomeSince
  matched the owner by the steer's creation timestamp, so an earlier
  successful turn masked the consuming turn's dead-letter (false
  complete) and vice versa. The owner is now the LATEST-STARTED turn
  whose claim window overlaps the steer's active interval
  [created_at, completed_at] — consumption happens inside the consuming
  turn's claim, so it always overlaps — with the
  earliest-terminal-after-creation fallback kept for retried owners
  whose consuming claim was overwritten. The ACP shape still matches
  its original owner (consumed during its claim; the later completion
  is administrative).
- P1 (rollout): executions already in_progress at deploy time carry no
  kickoffMessageUuid (the parent revision never wrote one). The settle
  handler declined them as "pre-kickoff window" and the dead-letter
  turn branch ignored them, stranding the node in_progress regardless
  of outcome. An activation started before this daemon's boot
  (isPreUpgradeActivation) now keeps the pre-#944 unconditional
  behavior: the reclaimed delivery settles the node live, and its
  dead-letter blocks.
- P2 (drive attribution): the recoverable-error deferral keyed on a
  session-wide claimed-turn row. During rehydration the restored session
  is published to the runtime caches before the direct streaming /
  continuation-replay awaits, and the processor can claim an unrelated
  restored turn in that window — its row satisfied the gate while being
  unable to retry the replay's error. AgentSession now exposes
  isDeliveryTurnDriving (an in-flight driveDeliveryTurn counter) and
  the deferral requires an actual drive, not merely a claimed row.

Tests: late-consumed steer both directions (repo); pre-upgrade settle
completes / dead blocks; claimed-but-not-driving recoverable error
blocks. Doc §18 updated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f07fcfd350

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/storage/repositories/job-queue-repository.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
lsm added 2 commits August 16, 2026 00:13
…fter dev merge [#944]

Merge origin/dev (brought queue-preview batch flush #2493, delivery
observability #2492, orphan-process cleanup #2491, online shard tooling
#2497/#2501/#2513). Git auto-merged driveDeliveryTurn's new batchUuids
parameter into the wrapper signature without passing it through to
driveDeliveryTurnCore — dev's batch-narrowing tests (agent-session
"narrows the batch payload" / "refuses to feed a reduced batch") failed
on the merge ref. Thread the parameter through; all shards green
(1-core 4315, runtime-a 691, runtime-b 1075, 4-space-storage 2165).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c24e8f6e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
Comment thread packages/daemon/src/storage/repositories/job-queue-repository.ts
…e; batch-aware correlation [#944]

Round-20 review fixes (one P1, three P2s):

- P1 (steer consuming turn): the interval-overlap approximation could
  pick an unrelated later turn when an ACP park outlives its consumer —
  both turns overlap the steer's [created, completed] interval. The
  owner is now matched by the claim window CONTAINING the kickoff's
  persisted CONSUMPTION timestamp (getDeliveryConsumedAt; the consumed
  flip aligns the row timestamp atomically), with the overlap
  approximation kept only for legacy rows without one. Repo tests cover
  both ACP-park directions.
- P2 (rowless evidence bound): hasTerminalResultAfter accepted ANY
  later success, so a reused session's later peer result could
  classify a kickoff whose own turn failed. The rowless
  classification now requires the FIRST terminal result after the
  kickoff's consumption to be a success
  (getFirstTerminalResultSubtypeAfter); an error subtype blocks with
  its reason, and no result keeps blocking.
- P2 (drive marker): deliveryTurnDriveCount is now incremented only
  on the DRIVING path — after the session-lock critical section — not
  at driveDeliveryTurn entry, so a recoverable error from
  rehydration's direct replay during the lock wait is not suppressed
  by a job that cannot retry it. The wrapper/core split from round 19
  is gone (single method again).
- P2 (batch correlation): deliveryTurnOutcomeSince matches the head
  uuid OR any batchUuids member (queue-flush batches key their row on
  the head), and a batch-head dead-letter blocks live via the durable
  outcome consult on uuid mismatch.

Tests: ACP-park consumption steering both directions, batch member
completed/dead, first-terminal-error blocks, batch-head dead-letter
blocks. Doc §18 updated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24bfc23c1b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2082 to +2083
.get(sessionId, uuid) as { timestamp: number } | undefined;
return row?.timestamp ?? null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse the persisted consumption timestamp before correlation

Fresh evidence beyond the earlier consuming-turn thread is that sdk_messages.timestamp is stored as ISO text, but this method casts and returns it as a number without conversion. durableKickoffOutcome() passes that string to the epoch-millisecond comparisons in deliveryTurnOutcomeSince(), so SQLite cannot match the turn whose claim window contains the consumption instant and falls back to creation-time inference. When a kickoff steer is created during turn A but consumed by turn B, this can report A's outcome—completing the execution when B dead-lettered, or blocking it when B succeeded. Convert the persisted value with new Date(...).getTime() before returning it.

Useful? React with 👍 / 👎.

Comment on lines +2057 to +2058
ORDER BY r.consumed_seq ASC
LIMIT 1`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the successful retry outcome after cleanup

Fresh evidence beyond the earlier rowless-success thread is that delivery retries reuse the same consumed kickoff UUID and can persist multiple terminal results: for example, error_during_execution on the first attempt followed by success on a retry. After the completed job row ages out, rowless reconciliation calls this method, whose ascending order returns the stale first error and blocks/re-spawns an activation that ultimately succeeded. Persist the delivery's final outcome outside the cleanup-managed job row, or otherwise bound the selected result to the final retry attempt without accepting results from later peer turns.

Useful? React with 👍 / 👎.

Comment on lines +4012 to +4015
const kickoffMessageUuid = (
execution?.data as { kickoffMessageUuid?: unknown } | null | undefined
)?.kickoffMessageUuid;
if (typeof kickoffMessageUuid !== 'string') return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile terminal rows for unstamped rollout executions

Fresh evidence beyond the earlier rollout-compatibility thread is that the compatibility fallback exists only in the live settlement handlers; this durable reconciliation still returns immediately for every unstamped execution. If a pre-upgrade activation's delivery becomes completed or dead and the upgraded daemon exits after committing that row but before its live event is handled, the next boot restores an idle unstamped execution, completed/dead jobs are never reclaimed, and this return prevents the only crash-window repair, leaving the execution in_progress indefinitely. Add a durable compatibility reconciliation path for activations identified by isPreUpgradeActivation() rather than relying exclusively on the one-time live publication.

Useful? React with 👍 / 👎.

@lsm

lsm commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Closing: owning task was cancelled and nothing is driving this PR. Branch kept for reference — reopen via a fresh task if this work is resumed.

@lsm lsm closed this Aug 18, 2026
@lsm

lsm commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Reopening: the owning task was mass-cancelled by the 2026-08-16 space-stop incident, not deliberately abandoned — the earlier closure mistook incident quiescence for dead work. Task restoration pending the update_task status capability.

@lsm

lsm commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

Closing: sibling #2471 landed, this half did not, and the branch is 1,200+ commits behind. Re-cut from dev if recoverable node errors still block workflow nodes; no tracking issue exists yet. Branch retained.

@lsm lsm closed this Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant