Skip to content

fix(runtime): repair missing terminal runtime ledgers - #410

Merged
jackwener merged 37 commits into
mainfrom
codex/runtime-terminal-invariant
Jul 2, 2026
Merged

fix(runtime): repair missing terminal runtime ledgers#410
jackwener merged 37 commits into
mainfrom
codex/runtime-terminal-invariant

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

  • Ensure AiSdkFlow always closes exhausted assistant streams with a terminal runtime event.
  • Repair persisted session ledgers before model-context reads when a terminal run is missing its terminal RuntimeEvent fact.
  • Add regression coverage for new stream exhaustion, AgentRun finalization, and legacy session repair paths.

Why

Fixes #397

A backend stream could end after non-terminal output or an error without producing a terminal runtime event. The run could then be finalized as terminal, but replaying the session later failed because the runtime event ledger had no terminal fact. This fixes the invariant at write time and repairs already-persisted broken ledgers before they are read.

Scope

Changed:

  • AiSdkFlow synthesizes failed terminal events when a stream exhausts without one, while preserving the original error event when there is one.
  • AgentRun no longer defaults an unclassified finalization to completed; missing terminal evidence becomes missing_terminal_event, and stopped runs stay aborted.
  • SessionManager repairs missing terminal runtime facts from durable legacy events when available, or records a failed missing_terminal_event terminal when there is no trustworthy legacy terminal.
  • Tests now cover the prevention path, empty-ledger backfill, non-empty ledger repair, and the no-evidence fallback.

Not included:

  • UI changes, schema migrations, or benchmark harness changes.

Verification

  • npm run typecheck
  • npm run -w @maka/runtime test
  • git diff --check

User-facing impact

No UI changes. Previously broken sessions with missing terminal runtime facts should become readable and retryable again, and future runs should close as failed instead of becoming terminal without terminal evidence.

Reviewer notes

This is intentionally one PR: prevention and repair are the same runtime invariant. Splitting them would either leave old ledgers broken or allow new broken ledgers to keep being written.

Astro-Han added 30 commits July 3, 2026 00:09
Astro-Han and others added 7 commits July 3, 2026 00:10
…ck repair path

Rebase integration onto main: the heavy-task self-check gate added a second
runRuntimeAttempt() call site (repair path) after this PR made
requireTerminalRuntimeEventWrite a required field. Pass it there too
(matching the primary call site) and import isTerminalRuntimeEvent for the
isNonTerminalErrorRuntimeEvent helper.
@jackwener
jackwener force-pushed the codex/runtime-terminal-invariant branch from a79bcb7 to f613f1a Compare July 2, 2026 16:13
@jackwener
jackwener merged commit fe3f408 into main Jul 2, 2026
@jackwener
jackwener deleted the codex/runtime-terminal-invariant branch July 2, 2026 16:13
@jackwener

Copy link
Copy Markdown
Member

Merged! 🎉 Thorough, well-tested fix — the terminal-fact invariant is enforced consistently across AgentRun, SessionManager, and the read models, and the regression coverage (prevention, empty-ledger backfill, non-empty repair, and no-evidence fallback) is excellent.

Two notes from review:

  • Rebase integration: main's heavy-task self-check gate added a second runRuntimeAttempt() call site (the repair path) after you made requireTerminalRuntimeEventWrite required. I wired it through there (Boolean(runtimeEventStore), matching the primary site) and imported isTerminalRuntimeEvent into task-agent-controller.ts. Full suite green afterward (core 671 / storage 139 / runtime 803 / headless 673).
  • I considered whether repairMissingTerminalAsFailed downgrading a header-completed run with no corroborating turn_state to failed was too aggressive, but concluded it's the correct conservative call given Assistant runs can silently finalize as 'completed' without a terminal RuntimeEvent, permanently breaking the session (stuck 'streaming' UI + RuntimeReadModelError) #397 — trusting run.status alone would reintroduce the exact 'terminal without terminal evidence' state this PR removes. Left as-is.

jackwener added a commit that referenced this pull request Jul 3, 2026
… timestamps; localize bypass label (#452)

Three fixes from a real-app screenshot report (main window, 2026-07-03):

1. Blocked display semantics. The #410 terminal-ledger invariant marks
   legacy sessions missing a terminal fact as blocked/unknown. The
   sidebar groups by raw status, so intact, resumable conversations
   piled up under an 已阻塞 group with shield icons and the chat header
   badge read 已阻塞 — for sessions where the model had answered and
   nothing needed the user's attention. Session-level blocked is now a
   display-layer distinction: only actionable reasons
   (NO_REAL_CONNECTION / auth / permission_required) keep the blocked
   presentation; tool_failed / unknown normalize to active at the
   renderer state boundary (commitSessions / upsertSessionSummary), so
   grouping, row icon, and header badge agree. Runtime status writes
   are untouched. Regression: session-status-display-normalization.test.ts.

2. Sidebar timestamps. Sessions older than 7 days rendered the full
   medium date + time (2026年6月20日 16:33, ~150px) which crushed the
   title next to it to ~2 characters. New formatCompactTimestamp in
   @maka/core: relative inside 7 days, date-only beyond (6月20日 within
   the year, 2025年6月20日 across years). Sidebar-only; wide surfaces
   keep formatRelativeTimestamp.

3. i18n consistency. The composer permission-mode pill showed English
   'Bypass permissions' between two Chinese modes; now 跳过确认 in the
   composer, command palette, session settings actions, and the
   account settings copy.

Desktop suite: 1685/1685; typecheck clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants