Skip to content

fix(kimi-code): flush wire journals before print-mode exit - #3531

Merged
7Sageer merged 15 commits into
mainfrom
fix/print-wire-flush
Sep 4, 2026
Merged

fix(kimi-code): flush wire journals before print-mode exit#3531
7Sageer merged 15 commits into
mainfrom
fix/print-wire-flush

Conversation

@7Sageer

@7Sageer 7Sageer commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No tracking issue — regression reported against e2e fixtures after #3498.

Problem

Print mode (kimi -p) never waited for the wire journal's async persist queue on exit. A turn's tail records (step.end / turn.ended / prompt.completed) are dispatched fire-and-forget and reach the journal only through the wire service's async persist queue (a microtask-scheduled append-log flush plus a threadpool fs write). Before #3498, print mode always attached the telemetry appender, and shutting it down (a network flush) kept the event loop busy long enough for that queue to drain. #3498 made print mode honor KIMI_DISABLE_TELEMETRY=1; with no appender attached, cleanup returns in microseconds and the error path's process.exit(1) cuts off the pending flush — the failed turn's closing records never land on disk, so a resumed session / transcript is missing the turn's error closing. Reproduced on the same build: KIMI_DISABLE_TELEMETRY=1 → 0/6 runs persist step.end(finishReason=error); telemetry re-enabled → 6/6.

What changed

The print cleanup path now explicitly flushes every session agent's event dispatcher (which awaits the wire persist queue and the append-log store) before disposing the app:

  • the main agent is flushed via its already-resolved handle, plus every agent in the session roster, so even a closing agent's already-dispatched records land;
  • the flush is bounded by CLI_SHUTDOWN_TIMEOUT_MS via the existing raceWithTimeout, and best-effort (errors swallowed) so a persist failure never masks the turn's own outcome — it was already reported where the append failed;
  • it runs on success, failure, and the signal-termination path (all share this cleanup), so record persistence no longer depends on how long telemetry shutdown happens to take.

Tests: three new cases in v2-run-print.test.ts — flush happens before app.dispose() on success, still happens when the turn fails, and a flush failure does not mask the turn error.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: the issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

A print-mode turn's tail records (step.end / turn.ended / prompt.completed)
are dispatched fire-and-forget and reach the journal only through the wire
service's async persist queue. The print cleanup path never awaited that
queue: with telemetry disabled (KIMI_DISABLE_TELEMETRY=1) cleanup returns
in microseconds, and process.exit on the error path cut off the pending
append-log flush, dropping the failed turn's closing records. Flush every
session agent's dispatcher (which awaits the wire persist queue and the
append-log store) before disposing the app, bounded by the shutdown
timeout and best-effort so a persist failure never masks the turn outcome.
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ab7c823

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: 3511b89a48

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +897 to +899
await Promise.all(
[...handles].map((handle) => handle.accessor.get(IEventDispatcher).flush()),
);

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 Keep flushing agents after one journal fails

When multiple agents are present and any dispatcher's flush() rejects before another agent finishes flushing, Promise.all rejects immediately; the caller swallows that rejection and proceeds to app.dispose(), so the remaining flushes are no longer awaited and the subsequent process exit can still truncate otherwise healthy journals. Settle every per-agent flush independently (for example with Promise.allSettled) before returning the best-effort cleanup.

Useful? React with 👍 / 👎.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@ab7c823
npx https://pkg.pr.new/@moonshot-ai/kimi-code@ab7c823

commit: ab7c823

Promise.all rejects as soon as one agent's flush fails; the best-effort
caller then proceeds to app.dispose() and process.exit while the remaining
agents' flushes are still in flight, re-exposing their journals to the
truncation this cleanup is meant to prevent. Await every flush with
Promise.allSettled instead (matches the drain helper's convention), and
cover it with a two-agent regression test.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: 463967f226

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// that returns fast (e.g. telemetry disabled) lets process.exit cut off
// that queue before the records land on disk. Best-effort: a persist
// failure was already reported where the append failed.
await raceWithTimeout(flushWires(), CLI_SHUTDOWN_TIMEOUT_MS).catch(() => {});

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 Keep sequential shutdown within the cleanup deadline

When the wire flush uses its 3-second allowance and both telemetry pipelines are also slow, cleanup can now take up to 9 seconds (flushWires, telemetryService.shutdown, and shutdownTelemetry each receive CLI_SHUTDOWN_TIMEOUT_MS) while the outer cleanup still stops waiting after 8 seconds. On a failed run or signal, the caller then invokes process.exit immediately, truncating the final telemetry shutdown and potentially preventing app.dispose() from running. Use a shared deadline, run independent shutdowns concurrently, or increase the outer bound to cover all sequential phases.

Useful? React with 👍 / 👎.

The wire flush, v2 telemetry shutdown, and v1 telemetry shutdown each hold
a 3s allowance; run sequentially they can take 9s, past the 8s outer
cleanup bound — the caller's process.exit would then cut off the tail
(app.dispose included). The phases are independent, so await them
concurrently (worst case one allowance) while keeping the v2 shutdown
failure propagation and the best-effort semantics of the other two.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: 84fe69b924

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// process.exit cut off that queue before the records land on disk.
// Best-effort: a persist failure was already reported where the
// append failed.
raceWithTimeout(flushWires(), CLI_SHUTDOWN_TIMEOUT_MS).catch(() => {}),

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 Quiesce active turns before flushing their journals

When SIGINT, SIGTERM, or SIGHUP arrives during an active turn, installPromptTerminationCleanup invokes this cleanup without first cancelling or awaiting that turn. This flush only drains records queued at that instant, so the still-running turn can append its cancellation and closing records afterward; app.dispose() starts asynchronous teardown without awaiting it, and the signal handler then calls process.exit, recreating the journal truncation this change is intended to prevent. Cancel and settle active agents before the flush, or await teardown and perform the final flush afterward.

Useful? React with 👍 / 👎.

A termination signal (SIGINT/SIGTERM/SIGHUP) can arrive mid-turn: the
cleanup ran the wire flush immediately, while the still-running turn only
produced its cancellation and closing records from dispose()'s
fire-and-forget teardown — after the flush, and after the signal handler's
process.exit. Cancel every session agent's queued and active turns and
await loop idleness before flushing (best-effort, bounded by the shutdown
timeout); idle loops make this a no-op on the normal exit paths. Torn-down
agent scopes are skipped defensively, mirroring the flush's per-agent
settlement.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: f8fb8c8c87

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

for (const turnId of loop.status().pendingTurnIds) loop.cancel(turnId);
loop.cancel();
}
await Promise.allSettled(loops.map((loop) => loop.settled()));

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 Await prompt completion before flushing

When a termination signal arrives during an active prompt, IAgentLoopService.settled() can resolve from AgentLoopService.releaseActiveTurn() before the turn-result callback in AgentPromptService.settle() dispatches prompt.completed; cleanup can therefore start and finish the wire flush before that final record is queued, after which the signal handler immediately calls process.exit. Fresh evidence beyond the earlier comment is this ordering between releaseActiveTurn(), job.result.resolve, and AgentPromptService.settle(), so quiescing must also await prompt completion (and drain pending prompts) before flushing.

Useful? React with 👍 / 👎.

Comment on lines +992 to +996
if (suppressions.length > 0) await Promise.allSettled(suppressions);
if (activeCount === 0 || batch.length === 0) break;
await Promise.all(batch);
await Promise.allSettled(batch);
}
if (allWaiters.length > 0) await Promise.all(allWaiters);
if (allWaiters.length > 0) await Promise.allSettled(allWaiters);

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 Propagate background-drain persistence failures

When print background mode is drain and either suppressTerminalNotification() or wait() rejects because task state cannot be persisted, these allSettled calls discard the error and let the CLI report success even though task state or notification suppression was not saved. This behavior was unintentionally changed alongside the per-agent wire-flush handling; only independent wire flushes should be best-effort, while background draining should retain its previous rejection propagation.

Useful? React with 👍 / 👎.

…iesce

Two follow-ups from review:

- The loop settles (releaseActiveTurn) before the prompt-settle chain
  dispatches prompt.completed, so awaiting loop idleness alone let the
  wire flush race the final record. Quiesce now also drains each agent's
  prompt queue and awaits the tracked prompt completions (registered at
  enqueue time) before the flush runs.

- Restore drainBackgroundTasks to Promise.all: a previous edit
  unintentionally switched it to allSettled alongside the per-agent wire
  flush, silently discarding persistence failures from
  suppressTerminalNotification()/wait(). Only the independent wire
  flushes are meant to be best-effort.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: e566535306

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +282 to +284
quiesceAgents = async () => {
await quiesceSessionAgents(resolved.session, resolved.agent);
await Promise.allSettled(promptCompletions);

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 Await subagent prompt completion before flushing

When termination occurs while a subagent or background-generated prompt is active, quiesceSessionAgents() cancels its loop, but promptCompletions contains only handles passed through the CLI's runNativeTurn. Since IAgentLoopService.settled() can resolve from releaseActiveTurn() before that agent's AgentPromptService.settle() dispatches prompt.completed, this wait does not close the ordering gap for other agents; their final record can therefore be queued after the wire flush and lost on process.exit. Fresh evidence beyond the prior comment is that the final code registers only the main run's completion rather than completions from every drained agent.

Useful? React with 👍 / 👎.

Tracking only the main run's prompt completion left subagent and
background prompts racing the wire flush the same way: the loop reports
idle before that agent's prompt-settle chain dispatches prompt.completed.
Replace the per-handle completion tracking with a uniform wait — after
draining and cancelling, poll every agent's prompt queue snapshot until
none reports an active or pending prompt. settle() clears the active
prompt and dispatches the record in one synchronous block, so an empty
snapshot proves the record is already queued for the flush.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: 61e34b089f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +963 to +964
const snapshot = service.list();
return snapshot.active !== undefined || snapshot.pending.length > 0;

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 Track prompts while startNext() is launching them

When a termination signal arrives after AgentPromptService.startNext() removes a prompt from pending but before it assigns active—for example while materializeDaemonRefs() or the pre-submit hook is awaiting—drain() cannot cancel that prompt and this snapshot reports an empty queue. The loop can also still be idle, so cleanup flushes the journal and calls process.exit while startNext() remains able to launch the turn and dispatch records afterward. Include the service's launching phase in quiescence or await/cancel the in-flight prompt launch before treating an empty snapshot as settled.

Useful? React with 👍 / 👎.

… quiesce

A termination signal can also arrive while startNext() is mid-launch: the
prompt has left pending and is not active yet, so drain() cannot cancel it
and an active/pending snapshot reads empty. Quiesce would then flush and
exit while the launch still dispatches records afterward.

Expose the service's launching phase on PromptQueueSnapshot and make the
print cleanup's quiesce a repeat-until-idle loop: every pass drains,
cancels, and awaits the loops, then re-checks launching/active/pending, so
a prompt surfacing from the launch window is cancelled on the next pass
instead of escaping the flush.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: a755e738b8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return false;
}
});
if (!busy) 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 Stop live producers before declaring agents quiescent

When print background mode is exit, or a cron becomes due during shutdown, task and cron services remain live after this empty snapshot. AgentTaskService.notifyAgentTask() and cron deliverFire() enqueue directly into IAgentLoopService (taskService.ts:1113 and cronService.ts:185), so a completion or tick after loop.settled() and this check can start another turn without making PromptQueueSnapshot busy. The concurrent wire flush may already have finished while telemetry shutdown keeps the process alive, after which the signal handler exits and loses the newly emitted records. Stop these producers before quiescing, or prevent new loop enqueues and recheck loop quiescence before flushing.

Useful? React with 👍 / 👎.

An empty prompt queue is only a point-in-time observation: background
task completions and cron fires enqueue straight into the loop, bypassing
the prompt queue, so a late producer could still start a turn — and new
records — after the journals were flushed. Quiesce now leaves a
quiescence guard held on every loop once queues read empty; the caller
holds the release across the wire flush and app disposal, so late
submissions queue behind the guard and are rejected by disposal instead
of racing process.exit. Loops that refuse the guard (still busy) send the
quiesce into another drain/cancel/settle pass.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: 0614a7abb9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +922 to +924
for (const agent of agentManager.list()) {
const handle = agentManager.handleOf(agent.agentId);
if (handle !== undefined) handles.add(handle);

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 Include closing subagents in the wire flush

When a non-main agent has entered AgentLifecycleService.remove() during shutdown, both list() and handleOf() exclude it because its managed.closing flag is already set, so this collector omits that agent from both quiescence and flushSessionWires(). Its removal releases the append-log lease with a fire-and-forget retirement flush, allowing the signal handler's subsequent process.exit() to truncate that subagent's closing records. Fresh evidence beyond the earlier all-agent comments is the explicit closing-agent filter in both lifecycle lookup methods; only the main agent is preserved separately here.

Useful? React with 👍 / 👎.

AgentLifecycleService.remove() quiesced the agent but never flushed its
wire journal: the records reached the append log only through the
fire-and-forget retirement flush, so a process exiting right after a
subagent's removal (e.g. print mode on a termination signal) could
truncate the closing records. Flush the agent's event dispatcher after
the quiesce and before disposal; a persist failure is reported without
blocking the removal.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: dda1e054eb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

compaction.abortController.abort(reason);
}
await Promise.all([loop.settled(), compactionSettled, prompt.drain(reason)]);
await handle.accessor.get(IEventDispatcher).flush().catch(onUnexpectedError);

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 Wait for prompt settlement before flushing removed agents

When remove() cancels an agent with an active prompt, loop.settled() can resolve in releaseActiveTurn() before the turn-result callback invokes AgentPromptService.settle() and dispatches prompt.aborted/prompt.completed; prompt.drain() also returns without awaiting that completion. Fresh evidence in the final revision is that this new flush can therefore snapshot the persistence queue before the prompt's final event is appended, after which scope disposal may truncate it. Await the active prompt completion or verify that the prompt queue is fully idle before flushing.

Useful? React with 👍 / 👎.

…ing a removed agent

The remove() flush could still snapshot the persistence queue before the
cancelled prompt's final record was appended: loop.settled() resolves in
releaseActiveTurn() before the prompt-settle chain dispatches
prompt.aborted/prompt.completed, and prompt.drain() does not await that
settlement. Wait (bounded) for the prompt queue to report no launching,
active, or pending prompts before flushing; an unreadable snapshot counts
as idle so a wedged service cannot stall the removal.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: 5c739e50c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +384 to +390
const snapshot = prompt.list();
idle =
!snapshot.launching && snapshot.active === undefined && snapshot.pending.length === 0;
} catch {
idle = true;
}
if (idle || Date.now() >= promptIdleDeadline) break;

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 Re-cancel prompts that finish launching during removal

When remove() runs while startNext() is awaiting daemon materialization or a pre-submit hook, the initial prompt.drain() cannot see the prompt because it has already left pending but is not yet active, and the loop has no turn to cancel. This polling loop only observes that prompt afterward; it never drains or cancels it again, so once it becomes active it can run until the three-second deadline, after which the journal is flushed before scope disposal generates its cancellation records. Repeat the drain/cancel operation while waiting, or freeze admissions and cancel the newly launched prompt before flushing.

Useful? React with 👍 / 👎.

…moval

A prompt mid-launch (startNext awaiting daemon materialization or a
pre-submit hook) is invisible to drain() and has no turn to cancel yet;
the passive idle wait would then let it run to the deadline and flush
before its closing records existed. Repeat drain/cancel/settle on every
pass until the queue reads idle, and once idle hold a quiescence guard
across the flush and disposal so a late producer cannot start new work
in between.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: f4a76a4433

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +243 to +245
// Quiescence guards stay held across the flush and disposal so a
// late background-task completion or cron fire cannot start a new
// turn (and new records) after the journals were drained.

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 Stop task event producers before flushing journals

In print background exit mode, or when a signal arrives while a detached task is active, the quiescence guard only blocks loop admission: AgentTaskService.recordTaskTerminated() dispatches TaskTerminated directly, and app.dispose() can itself trigger that path by cancelling the task after this flush has completed. The signal handler may then call process.exit before the new dispatch is persisted, so task records can still be lost. Fresh evidence beyond the earlier producer comment is the direct dispatcher call at taskService.ts:1085-1088, which bypasses the guarded loop entirely; stop/settle task producers before flushing or perform a final flush after their disposal.

Useful? React with 👍 / 👎.

Task termination bypasses both the prompt queue and the loop quiescence
guard: AgentTaskService dispatches TaskTerminated straight to the wire,
and disposal would force-stop still-running tasks after the journals were
already flushed. Stop every session agent's tasks up front in the quiesce
phase (mirroring AgentLifecycleService.remove()), so each task's
termination record is dispatched before the flush instead of racing
process.exit. keepAliveOnExit tasks stay exempt, matching remove().
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@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: 36eb475935

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +972 to +975
await Promise.allSettled(
handles.flatMap((handle) => {
try {
return [handle.accessor.get(IAgentTaskService).stopAllOnExit('Session closed')];

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 Stop every task producer before flushing journals

When print mode exits with [task].keepAliveOnExit=true, or when suppressing a detached task's terminal notification fails, stopAllOnExit() is not a shutdown barrier: taskService.ts:800-808 either returns immediately or rejects before calling stopAll(). Because Promise.allSettled discards that result, cleanup proceeds to freeze the loops and flush while the task remains active; its direct TaskTerminated dispatch can then occur after the flush and be lost on process exit. Fresh evidence beyond the earlier producer comment is that the newly added shutdown path specifically delegates to this conditional, failure-short-circuiting helper; use an unconditional best-effort stop/settlement barrier here before flushing.

Useful? React with 👍 / 👎.

…AllOnExit

A detached task's terminal-notification suppression failing (e.g. the
persist write rejects) used to reject the whole stopAllOnExit before
stopAll() ran, so every task stayed active — and callers settling the
rejection (print-mode cleanup, agent removal) proceeded as if the tasks
were stopped, losing their termination records at exit. Settle each
suppression independently, log the failure, and always stop the tasks.
keepAliveOnExit tasks remain exempt by design.
@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: ed9fe386e3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@7Hanrui

7Hanrui commented Sep 4, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: ab7c823025

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@7Sageer
7Sageer merged commit 51bd52a into main Sep 4, 2026
15 checks passed
@7Sageer
7Sageer deleted the fix/print-wire-flush branch September 4, 2026 10:31
@github-actions github-actions Bot mentioned this pull request Sep 4, 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.

2 participants