fix(runtime): isolate an unclaimed control fact from the session view - #1618

Merged
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event
Jul 29, 2026
Merged

fix(runtime): isolate an unclaimed control fact from the session view#1618
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

One RuntimeEvent the projection does not claim made an entire session unreadable, even when every message in it was intact. The projection emitted a hard unsupported_event for any unrecognised shape, RuntimeReadModel.buildView throws on any hard diagnostic, and the whole projection was discarded — failing every getSessionView caller, not just the transcript: branching, revising, and every turn-scoped action went with it. #1607 was one instance; #1609 claimed those two shapes but left the amplification in place.

The hard failure is deliberate — it exists so messages are never silently dropped — so this splits it rather than relaxing it wholesale:

  • unclaimed and content-bearing → a reader may be missing a row, the view is not faithful → stays unsupported_event, hard.
  • unclaimed and control-only → there is no row to lose, while discarding the projection costs every intact message beside it → new unclaimed_control_fact, soft.

Severity now lives in one table keyed by code, so a new diagnostic cannot exist without deciding which side it falls on, and buildView asks the projection instead of restating the codes.

What makes the soft side safe is claim coverage, not the absence of content. Actions do own user-visible rows — permissionDecision, tokenUsage, and the terminal fact all produce one, and runtime-event-backfill.ts already writes a content-free event that projects to a visible permission_decision. Nothing with a row reaches the degrading branch only because every action field a reader can meet is claimed, so the projection-coverage contract now has to prove exactly that.

The contract previously enumerated BackendSessionEvent['type'] alone, which left every RuntimeEvent produced outside mapSessionEventToRuntimeEvent uncovered — tool-runtime, terminal-run-commit, and runtime-event-backfill all write actions directly. It now also enumerates every field of RuntimeEventActions, keyed so a new field cannot compile without a sample and cannot pass without being claimed.

Writing that contract found three action fields the projection had never claimed: artifactDelta, transferToAgent, and runtimeProtocol. The first two have no emitter yet. runtimeProtocol doesruntime-runner.ts writes it, and RecoveryResolver reads it; it only ever avoided breaking a session because it has so far always ridden on an already-claimed carrier. All three are now claimed as control facts.

#1609 declined this downgrade because it "would hide a future projection gap". That is answered by keeping the two questions separate: severity decides whether a session opens, the coverage contract decides whether coverage is missing. The contract asserts on the union of both unclaimed codes, so softening a severity cannot soften the contract.

Closes#1613. Refs #1607, #1609.

Verification

  • @maka/runtime full suite: 2795 tests, 2786 pass, 0 fail, 9 skipped. @maka/headless 1428 pass, CLI 668 pass, 0 fail.
  • End-to-end reproduction: a backend emits a SessionEvent variant the mapping was never taught, the turn completes through a real sendMessage, and the session reads back intact with the unclaimed event reported as one unclaimed_control_fact. Reverting the fallback to always-hard makes it fail with the original RuntimeReadModelError.
  • Its counterpart pins the caller: appending a content-bearing unclaimed event to a completed run's ledger must make getSessionView throw. Verified to bite by making the fallback unconditionally soft.
  • The coverage contract was checked in both directions: removing the runtimeProtocol claim fails it at runtime; removing its table entry fails compilation with TS2741.
  • The unclaimed predicate is now asserted through itself, so dropping unclaimed_control_fact from it can no longer silently narrow both contracts to unsupported_event.
  • Claim strictness is untouched: fix(runtime): reload completed sessions after sandbox boundary decisions #1609's eight malformed-boundary counter-examples still assert the event stays unclaimed; only its severity moved.
  • Test-merged with fix/sandbox-boundary-pending-restart (fix: let a pending sandbox boundary request survive a host restart #1612), which touches the same file: auto-merges clean, combined suite green.
  • npm run format, npm run lint, npm run typecheck --workspaces clean. Not run: desktop E2E — this change does not reach renderer or main.

Review focus

A malformed control fact — one that half-matches a known shape — is soft alongside a genuinely unknown one. The read model is not the enforcement authority (boundary enforcement has its own durable revisions) and an unopenable session is the worst answer available, but if ledger corruption should outrank forward compatibility that argues for a third severity tier, which this PR does not add.

stateDelta is an open record, so the contract can only cover the field's existence, not new keys inside the delta. That limit is recorded in the table rather than papered over.

artifactDelta and transferToAgent are claimed as silent control facts before either has an emitter. If a hand-off should eventually render as a visible row, that claim is where it has to change.

RuntimeReadModel decided which projection diagnostics are fatal by restating
their codes, so the projection declared the diagnostics and its caller declared
what they mean. Move that decision next to the codes as a table keyed by
`RuntimeEventReadModelDiagnosticCode`: a new diagnostic cannot compile without
saying whether it means a user-visible row may be missing. The caller and the
persisted-compat test now ask that authority instead of listing codes.
No behavior change — the table restates today's hard set exactly.
One RuntimeEvent the projection did not claim made an entire session
unreadable. The catch-all emitted a hard `unsupported_event`, RuntimeReadModel
threw on it, and the whole projection went with it — so getMessages, listTurns,
branching, revising and every turn-scoped action failed over a fact that owns no
chat row. #1607 was one instance; #1609 claimed those two shapes but left the
amplification in place.
Split the catch-all on the RuntimeEvent's own structure: `content` is its
message payload, `actions` its control intent. Every row this projection emits
from an unclaimed shape would have come from content, so a content-bearing
event stays hard — "a message is never silently dropped" is the invariant the
hard failure exists for. A control-only fact has nothing to lose, so it becomes
`unclaimed_control_fact` and degrades the view instead of discarding it. A
projector that tried to build a row and failed still reports its own hard
diagnostic, so this softens nothing that attempted a message.
A future gap is still caught before a user meets it. The projection-coverage
contract now asserts on the unclaimed codes at either severity rather than the
hard one alone, so a new SessionEvent variant with no claim still fails CI, and
AiSdkFlow's exhaustiveness guard is what a variant becomes: a content-free
control fact that lands on the degradable side by construction.
Fixes#1613
…meet
The soft path rests on a premise that was not machine-checked: an unclaimed
content-free event degrades the view instead of withholding it, which is only
safe while no unclaimed action can owe a row. `content === undefined` does not
prove that on its own — permissionDecision, tokenUsage and the terminal fact all
produce rows, and runtime-event-backfill already writes a content-free event
that becomes a visible `permission_decision`. What actually holds the rule up is
claim coverage, so make coverage the thing that is proven.
The SessionEvent contract only covers events built by
`mapSessionEventToRuntimeEvent`; tool-runtime, terminal-run-commit and the
backfill write RuntimeEvents directly, so a new action field on those paths was
invisible to it. A second contract keyed on `RuntimeEventActions` gives every
field a reachable sample typed to its own key: a new field cannot compile
without one and cannot pass without being claimed.
Writing it found three fields the projection never claimed — `artifactDelta`,
`transferToAgent` and `runtimeProtocol`, the last of which real emitters already
write. All three are control-only, so claim them, and say in the fallback what
the rule actually depends on.
Two regressions the suite could not see. The unmapped-SessionEvent test compared
the raw code string, so dropping `unclaimed_control_fact` from
`isUnclaimedRuntimeEventDiagnostic` would have quietly narrowed the coverage
contract to `unsupported_event` with every test still green; it now filters
through the predicate itself. And the hard side was only asserted inside the
projector, so a caller that stopped enforcing the policy went unnoticed: append
a content-bearing unclaimed event to a completed run's ledger and getSessionView
must still refuse the view — the counterpart of the soft reproduction beside it.
@Astro-Han
Astro-Han merged commit 1f43ea7 into mainJul 29, 2026
3 checks passed
Sign up for freeto 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.

fix: isolate an unclaimed RuntimeEvent instead of rejecting the whole session view

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(runtime): isolate an unclaimed control fact from the session view - #1618

Merged
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event
Jul 29, 2026
Merged

fix(runtime): isolate an unclaimed control fact from the session view#1618
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

One RuntimeEvent the projection does not claim made an entire session unreadable, even when every message in it was intact. The projection emitted a hard unsupported_event for any unrecognised shape, RuntimeReadModel.buildView throws on any hard diagnostic, and the whole projection was discarded — failing every getSessionView caller, not just the transcript: branching, revising, and every turn-scoped action went with it. #1607 was one instance; #1609 claimed those two shapes but left the amplification in place.

The hard failure is deliberate — it exists so messages are never silently dropped — so this splits it rather than relaxing it wholesale:

  • unclaimed and content-bearing → a reader may be missing a row, the view is not faithful → stays unsupported_event, hard.
  • unclaimed and control-only → there is no row to lose, while discarding the projection costs every intact message beside it → new unclaimed_control_fact, soft.

Severity now lives in one table keyed by code, so a new diagnostic cannot exist without deciding which side it falls on, and buildView asks the projection instead of restating the codes.

What makes the soft side safe is claim coverage, not the absence of content. Actions do own user-visible rows — permissionDecision, tokenUsage, and the terminal fact all produce one, and runtime-event-backfill.ts already writes a content-free event that projects to a visible permission_decision. Nothing with a row reaches the degrading branch only because every action field a reader can meet is claimed, so the projection-coverage contract now has to prove exactly that.

The contract previously enumerated BackendSessionEvent['type'] alone, which left every RuntimeEvent produced outside mapSessionEventToRuntimeEvent uncovered — tool-runtime, terminal-run-commit, and runtime-event-backfill all write actions directly. It now also enumerates every field of RuntimeEventActions, keyed so a new field cannot compile without a sample and cannot pass without being claimed.

Writing that contract found three action fields the projection had never claimed: artifactDelta, transferToAgent, and runtimeProtocol. The first two have no emitter yet. runtimeProtocol doesruntime-runner.ts writes it, and RecoveryResolver reads it; it only ever avoided breaking a session because it has so far always ridden on an already-claimed carrier. All three are now claimed as control facts.

#1609 declined this downgrade because it "would hide a future projection gap". That is answered by keeping the two questions separate: severity decides whether a session opens, the coverage contract decides whether coverage is missing. The contract asserts on the union of both unclaimed codes, so softening a severity cannot soften the contract.

Closes#1613. Refs #1607, #1609.

Verification

  • @maka/runtime full suite: 2795 tests, 2786 pass, 0 fail, 9 skipped. @maka/headless 1428 pass, CLI 668 pass, 0 fail.
  • End-to-end reproduction: a backend emits a SessionEvent variant the mapping was never taught, the turn completes through a real sendMessage, and the session reads back intact with the unclaimed event reported as one unclaimed_control_fact. Reverting the fallback to always-hard makes it fail with the original RuntimeReadModelError.
  • Its counterpart pins the caller: appending a content-bearing unclaimed event to a completed run's ledger must make getSessionView throw. Verified to bite by making the fallback unconditionally soft.
  • The coverage contract was checked in both directions: removing the runtimeProtocol claim fails it at runtime; removing its table entry fails compilation with TS2741.
  • The unclaimed predicate is now asserted through itself, so dropping unclaimed_control_fact from it can no longer silently narrow both contracts to unsupported_event.
  • Claim strictness is untouched: fix(runtime): reload completed sessions after sandbox boundary decisions #1609's eight malformed-boundary counter-examples still assert the event stays unclaimed; only its severity moved.
  • Test-merged with fix/sandbox-boundary-pending-restart (fix: let a pending sandbox boundary request survive a host restart #1612), which touches the same file: auto-merges clean, combined suite green.
  • npm run format, npm run lint, npm run typecheck --workspaces clean. Not run: desktop E2E — this change does not reach renderer or main.

Review focus

A malformed control fact — one that half-matches a known shape — is soft alongside a genuinely unknown one. The read model is not the enforcement authority (boundary enforcement has its own durable revisions) and an unopenable session is the worst answer available, but if ledger corruption should outrank forward compatibility that argues for a third severity tier, which this PR does not add.

stateDelta is an open record, so the contract can only cover the field's existence, not new keys inside the delta. That limit is recorded in the table rather than papered over.

artifactDelta and transferToAgent are claimed as silent control facts before either has an emitter. If a hand-off should eventually render as a visible row, that claim is where it has to change.

RuntimeReadModel decided which projection diagnostics are fatal by restating
their codes, so the projection declared the diagnostics and its caller declared
what they mean. Move that decision next to the codes as a table keyed by
`RuntimeEventReadModelDiagnosticCode`: a new diagnostic cannot compile without
saying whether it means a user-visible row may be missing. The caller and the
persisted-compat test now ask that authority instead of listing codes.
No behavior change — the table restates today's hard set exactly.
One RuntimeEvent the projection did not claim made an entire session
unreadable. The catch-all emitted a hard `unsupported_event`, RuntimeReadModel
threw on it, and the whole projection went with it — so getMessages, listTurns,
branching, revising and every turn-scoped action failed over a fact that owns no
chat row. #1607 was one instance; #1609 claimed those two shapes but left the
amplification in place.
Split the catch-all on the RuntimeEvent's own structure: `content` is its
message payload, `actions` its control intent. Every row this projection emits
from an unclaimed shape would have come from content, so a content-bearing
event stays hard — "a message is never silently dropped" is the invariant the
hard failure exists for. A control-only fact has nothing to lose, so it becomes
`unclaimed_control_fact` and degrades the view instead of discarding it. A
projector that tried to build a row and failed still reports its own hard
diagnostic, so this softens nothing that attempted a message.
A future gap is still caught before a user meets it. The projection-coverage
contract now asserts on the unclaimed codes at either severity rather than the
hard one alone, so a new SessionEvent variant with no claim still fails CI, and
AiSdkFlow's exhaustiveness guard is what a variant becomes: a content-free
control fact that lands on the degradable side by construction.
Fixes#1613
…meet
The soft path rests on a premise that was not machine-checked: an unclaimed
content-free event degrades the view instead of withholding it, which is only
safe while no unclaimed action can owe a row. `content === undefined` does not
prove that on its own — permissionDecision, tokenUsage and the terminal fact all
produce rows, and runtime-event-backfill already writes a content-free event
that becomes a visible `permission_decision`. What actually holds the rule up is
claim coverage, so make coverage the thing that is proven.
The SessionEvent contract only covers events built by
`mapSessionEventToRuntimeEvent`; tool-runtime, terminal-run-commit and the
backfill write RuntimeEvents directly, so a new action field on those paths was
invisible to it. A second contract keyed on `RuntimeEventActions` gives every
field a reachable sample typed to its own key: a new field cannot compile
without one and cannot pass without being claimed.
Writing it found three fields the projection never claimed — `artifactDelta`,
`transferToAgent` and `runtimeProtocol`, the last of which real emitters already
write. All three are control-only, so claim them, and say in the fallback what
the rule actually depends on.
Two regressions the suite could not see. The unmapped-SessionEvent test compared
the raw code string, so dropping `unclaimed_control_fact` from
`isUnclaimedRuntimeEventDiagnostic` would have quietly narrowed the coverage
contract to `unsupported_event` with every test still green; it now filters
through the predicate itself. And the hard side was only asserted inside the
projector, so a caller that stopped enforcing the policy went unnoticed: append
a content-bearing unclaimed event to a completed run's ledger and getSessionView
must still refuse the view — the counterpart of the soft reproduction beside it.
@Astro-Han
Astro-Han merged commit 1f43ea7 into mainJul 29, 2026
3 checks passed
Sign up for freeto 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.

fix: isolate an unclaimed RuntimeEvent instead of rejecting the whole session view

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(runtime): isolate an unclaimed control fact from the session view - #1618

Merged
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event
Jul 29, 2026
Merged

fix(runtime): isolate an unclaimed control fact from the session view#1618
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

One RuntimeEvent the projection does not claim made an entire session unreadable, even when every message in it was intact. The projection emitted a hard unsupported_event for any unrecognised shape, RuntimeReadModel.buildView throws on any hard diagnostic, and the whole projection was discarded — failing every getSessionView caller, not just the transcript: branching, revising, and every turn-scoped action went with it. #1607 was one instance; #1609 claimed those two shapes but left the amplification in place.

The hard failure is deliberate — it exists so messages are never silently dropped — so this splits it rather than relaxing it wholesale:

  • unclaimed and content-bearing → a reader may be missing a row, the view is not faithful → stays unsupported_event, hard.
  • unclaimed and control-only → there is no row to lose, while discarding the projection costs every intact message beside it → new unclaimed_control_fact, soft.

Severity now lives in one table keyed by code, so a new diagnostic cannot exist without deciding which side it falls on, and buildView asks the projection instead of restating the codes.

What makes the soft side safe is claim coverage, not the absence of content. Actions do own user-visible rows — permissionDecision, tokenUsage, and the terminal fact all produce one, and runtime-event-backfill.ts already writes a content-free event that projects to a visible permission_decision. Nothing with a row reaches the degrading branch only because every action field a reader can meet is claimed, so the projection-coverage contract now has to prove exactly that.

The contract previously enumerated BackendSessionEvent['type'] alone, which left every RuntimeEvent produced outside mapSessionEventToRuntimeEvent uncovered — tool-runtime, terminal-run-commit, and runtime-event-backfill all write actions directly. It now also enumerates every field of RuntimeEventActions, keyed so a new field cannot compile without a sample and cannot pass without being claimed.

Writing that contract found three action fields the projection had never claimed: artifactDelta, transferToAgent, and runtimeProtocol. The first two have no emitter yet. runtimeProtocol doesruntime-runner.ts writes it, and RecoveryResolver reads it; it only ever avoided breaking a session because it has so far always ridden on an already-claimed carrier. All three are now claimed as control facts.

#1609 declined this downgrade because it "would hide a future projection gap". That is answered by keeping the two questions separate: severity decides whether a session opens, the coverage contract decides whether coverage is missing. The contract asserts on the union of both unclaimed codes, so softening a severity cannot soften the contract.

Closes#1613. Refs #1607, #1609.

Verification

  • @maka/runtime full suite: 2795 tests, 2786 pass, 0 fail, 9 skipped. @maka/headless 1428 pass, CLI 668 pass, 0 fail.
  • End-to-end reproduction: a backend emits a SessionEvent variant the mapping was never taught, the turn completes through a real sendMessage, and the session reads back intact with the unclaimed event reported as one unclaimed_control_fact. Reverting the fallback to always-hard makes it fail with the original RuntimeReadModelError.
  • Its counterpart pins the caller: appending a content-bearing unclaimed event to a completed run's ledger must make getSessionView throw. Verified to bite by making the fallback unconditionally soft.
  • The coverage contract was checked in both directions: removing the runtimeProtocol claim fails it at runtime; removing its table entry fails compilation with TS2741.
  • The unclaimed predicate is now asserted through itself, so dropping unclaimed_control_fact from it can no longer silently narrow both contracts to unsupported_event.
  • Claim strictness is untouched: fix(runtime): reload completed sessions after sandbox boundary decisions #1609's eight malformed-boundary counter-examples still assert the event stays unclaimed; only its severity moved.
  • Test-merged with fix/sandbox-boundary-pending-restart (fix: let a pending sandbox boundary request survive a host restart #1612), which touches the same file: auto-merges clean, combined suite green.
  • npm run format, npm run lint, npm run typecheck --workspaces clean. Not run: desktop E2E — this change does not reach renderer or main.

Review focus

A malformed control fact — one that half-matches a known shape — is soft alongside a genuinely unknown one. The read model is not the enforcement authority (boundary enforcement has its own durable revisions) and an unopenable session is the worst answer available, but if ledger corruption should outrank forward compatibility that argues for a third severity tier, which this PR does not add.

stateDelta is an open record, so the contract can only cover the field's existence, not new keys inside the delta. That limit is recorded in the table rather than papered over.

artifactDelta and transferToAgent are claimed as silent control facts before either has an emitter. If a hand-off should eventually render as a visible row, that claim is where it has to change.

RuntimeReadModel decided which projection diagnostics are fatal by restating
their codes, so the projection declared the diagnostics and its caller declared
what they mean. Move that decision next to the codes as a table keyed by
`RuntimeEventReadModelDiagnosticCode`: a new diagnostic cannot compile without
saying whether it means a user-visible row may be missing. The caller and the
persisted-compat test now ask that authority instead of listing codes.
No behavior change — the table restates today's hard set exactly.
One RuntimeEvent the projection did not claim made an entire session
unreadable. The catch-all emitted a hard `unsupported_event`, RuntimeReadModel
threw on it, and the whole projection went with it — so getMessages, listTurns,
branching, revising and every turn-scoped action failed over a fact that owns no
chat row. #1607 was one instance; #1609 claimed those two shapes but left the
amplification in place.
Split the catch-all on the RuntimeEvent's own structure: `content` is its
message payload, `actions` its control intent. Every row this projection emits
from an unclaimed shape would have come from content, so a content-bearing
event stays hard — "a message is never silently dropped" is the invariant the
hard failure exists for. A control-only fact has nothing to lose, so it becomes
`unclaimed_control_fact` and degrades the view instead of discarding it. A
projector that tried to build a row and failed still reports its own hard
diagnostic, so this softens nothing that attempted a message.
A future gap is still caught before a user meets it. The projection-coverage
contract now asserts on the unclaimed codes at either severity rather than the
hard one alone, so a new SessionEvent variant with no claim still fails CI, and
AiSdkFlow's exhaustiveness guard is what a variant becomes: a content-free
control fact that lands on the degradable side by construction.
Fixes#1613
…meet
The soft path rests on a premise that was not machine-checked: an unclaimed
content-free event degrades the view instead of withholding it, which is only
safe while no unclaimed action can owe a row. `content === undefined` does not
prove that on its own — permissionDecision, tokenUsage and the terminal fact all
produce rows, and runtime-event-backfill already writes a content-free event
that becomes a visible `permission_decision`. What actually holds the rule up is
claim coverage, so make coverage the thing that is proven.
The SessionEvent contract only covers events built by
`mapSessionEventToRuntimeEvent`; tool-runtime, terminal-run-commit and the
backfill write RuntimeEvents directly, so a new action field on those paths was
invisible to it. A second contract keyed on `RuntimeEventActions` gives every
field a reachable sample typed to its own key: a new field cannot compile
without one and cannot pass without being claimed.
Writing it found three fields the projection never claimed — `artifactDelta`,
`transferToAgent` and `runtimeProtocol`, the last of which real emitters already
write. All three are control-only, so claim them, and say in the fallback what
the rule actually depends on.
Two regressions the suite could not see. The unmapped-SessionEvent test compared
the raw code string, so dropping `unclaimed_control_fact` from
`isUnclaimedRuntimeEventDiagnostic` would have quietly narrowed the coverage
contract to `unsupported_event` with every test still green; it now filters
through the predicate itself. And the hard side was only asserted inside the
projector, so a caller that stopped enforcing the policy went unnoticed: append
a content-bearing unclaimed event to a completed run's ledger and getSessionView
must still refuse the view — the counterpart of the soft reproduction beside it.
@Astro-Han
Astro-Han merged commit 1f43ea7 into mainJul 29, 2026
3 checks passed
Sign up for freeto 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.

fix: isolate an unclaimed RuntimeEvent instead of rejecting the whole session view

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(runtime): isolate an unclaimed control fact from the session view - #1618

Merged
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event
Jul 29, 2026
Merged

fix(runtime): isolate an unclaimed control fact from the session view#1618
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

One RuntimeEvent the projection does not claim made an entire session unreadable, even when every message in it was intact. The projection emitted a hard unsupported_event for any unrecognised shape, RuntimeReadModel.buildView throws on any hard diagnostic, and the whole projection was discarded — failing every getSessionView caller, not just the transcript: branching, revising, and every turn-scoped action went with it. #1607 was one instance; #1609 claimed those two shapes but left the amplification in place.

The hard failure is deliberate — it exists so messages are never silently dropped — so this splits it rather than relaxing it wholesale:

  • unclaimed and content-bearing → a reader may be missing a row, the view is not faithful → stays unsupported_event, hard.
  • unclaimed and control-only → there is no row to lose, while discarding the projection costs every intact message beside it → new unclaimed_control_fact, soft.

Severity now lives in one table keyed by code, so a new diagnostic cannot exist without deciding which side it falls on, and buildView asks the projection instead of restating the codes.

What makes the soft side safe is claim coverage, not the absence of content. Actions do own user-visible rows — permissionDecision, tokenUsage, and the terminal fact all produce one, and runtime-event-backfill.ts already writes a content-free event that projects to a visible permission_decision. Nothing with a row reaches the degrading branch only because every action field a reader can meet is claimed, so the projection-coverage contract now has to prove exactly that.

The contract previously enumerated BackendSessionEvent['type'] alone, which left every RuntimeEvent produced outside mapSessionEventToRuntimeEvent uncovered — tool-runtime, terminal-run-commit, and runtime-event-backfill all write actions directly. It now also enumerates every field of RuntimeEventActions, keyed so a new field cannot compile without a sample and cannot pass without being claimed.

Writing that contract found three action fields the projection had never claimed: artifactDelta, transferToAgent, and runtimeProtocol. The first two have no emitter yet. runtimeProtocol doesruntime-runner.ts writes it, and RecoveryResolver reads it; it only ever avoided breaking a session because it has so far always ridden on an already-claimed carrier. All three are now claimed as control facts.

#1609 declined this downgrade because it "would hide a future projection gap". That is answered by keeping the two questions separate: severity decides whether a session opens, the coverage contract decides whether coverage is missing. The contract asserts on the union of both unclaimed codes, so softening a severity cannot soften the contract.

Closes#1613. Refs #1607, #1609.

Verification

  • @maka/runtime full suite: 2795 tests, 2786 pass, 0 fail, 9 skipped. @maka/headless 1428 pass, CLI 668 pass, 0 fail.
  • End-to-end reproduction: a backend emits a SessionEvent variant the mapping was never taught, the turn completes through a real sendMessage, and the session reads back intact with the unclaimed event reported as one unclaimed_control_fact. Reverting the fallback to always-hard makes it fail with the original RuntimeReadModelError.
  • Its counterpart pins the caller: appending a content-bearing unclaimed event to a completed run's ledger must make getSessionView throw. Verified to bite by making the fallback unconditionally soft.
  • The coverage contract was checked in both directions: removing the runtimeProtocol claim fails it at runtime; removing its table entry fails compilation with TS2741.
  • The unclaimed predicate is now asserted through itself, so dropping unclaimed_control_fact from it can no longer silently narrow both contracts to unsupported_event.
  • Claim strictness is untouched: fix(runtime): reload completed sessions after sandbox boundary decisions #1609's eight malformed-boundary counter-examples still assert the event stays unclaimed; only its severity moved.
  • Test-merged with fix/sandbox-boundary-pending-restart (fix: let a pending sandbox boundary request survive a host restart #1612), which touches the same file: auto-merges clean, combined suite green.
  • npm run format, npm run lint, npm run typecheck --workspaces clean. Not run: desktop E2E — this change does not reach renderer or main.

Review focus

A malformed control fact — one that half-matches a known shape — is soft alongside a genuinely unknown one. The read model is not the enforcement authority (boundary enforcement has its own durable revisions) and an unopenable session is the worst answer available, but if ledger corruption should outrank forward compatibility that argues for a third severity tier, which this PR does not add.

stateDelta is an open record, so the contract can only cover the field's existence, not new keys inside the delta. That limit is recorded in the table rather than papered over.

artifactDelta and transferToAgent are claimed as silent control facts before either has an emitter. If a hand-off should eventually render as a visible row, that claim is where it has to change.

RuntimeReadModel decided which projection diagnostics are fatal by restating
their codes, so the projection declared the diagnostics and its caller declared
what they mean. Move that decision next to the codes as a table keyed by
`RuntimeEventReadModelDiagnosticCode`: a new diagnostic cannot compile without
saying whether it means a user-visible row may be missing. The caller and the
persisted-compat test now ask that authority instead of listing codes.
No behavior change — the table restates today's hard set exactly.
One RuntimeEvent the projection did not claim made an entire session
unreadable. The catch-all emitted a hard `unsupported_event`, RuntimeReadModel
threw on it, and the whole projection went with it — so getMessages, listTurns,
branching, revising and every turn-scoped action failed over a fact that owns no
chat row. #1607 was one instance; #1609 claimed those two shapes but left the
amplification in place.
Split the catch-all on the RuntimeEvent's own structure: `content` is its
message payload, `actions` its control intent. Every row this projection emits
from an unclaimed shape would have come from content, so a content-bearing
event stays hard — "a message is never silently dropped" is the invariant the
hard failure exists for. A control-only fact has nothing to lose, so it becomes
`unclaimed_control_fact` and degrades the view instead of discarding it. A
projector that tried to build a row and failed still reports its own hard
diagnostic, so this softens nothing that attempted a message.
A future gap is still caught before a user meets it. The projection-coverage
contract now asserts on the unclaimed codes at either severity rather than the
hard one alone, so a new SessionEvent variant with no claim still fails CI, and
AiSdkFlow's exhaustiveness guard is what a variant becomes: a content-free
control fact that lands on the degradable side by construction.
Fixes#1613
…meet
The soft path rests on a premise that was not machine-checked: an unclaimed
content-free event degrades the view instead of withholding it, which is only
safe while no unclaimed action can owe a row. `content === undefined` does not
prove that on its own — permissionDecision, tokenUsage and the terminal fact all
produce rows, and runtime-event-backfill already writes a content-free event
that becomes a visible `permission_decision`. What actually holds the rule up is
claim coverage, so make coverage the thing that is proven.
The SessionEvent contract only covers events built by
`mapSessionEventToRuntimeEvent`; tool-runtime, terminal-run-commit and the
backfill write RuntimeEvents directly, so a new action field on those paths was
invisible to it. A second contract keyed on `RuntimeEventActions` gives every
field a reachable sample typed to its own key: a new field cannot compile
without one and cannot pass without being claimed.
Writing it found three fields the projection never claimed — `artifactDelta`,
`transferToAgent` and `runtimeProtocol`, the last of which real emitters already
write. All three are control-only, so claim them, and say in the fallback what
the rule actually depends on.
Two regressions the suite could not see. The unmapped-SessionEvent test compared
the raw code string, so dropping `unclaimed_control_fact` from
`isUnclaimedRuntimeEventDiagnostic` would have quietly narrowed the coverage
contract to `unsupported_event` with every test still green; it now filters
through the predicate itself. And the hard side was only asserted inside the
projector, so a caller that stopped enforcing the policy went unnoticed: append
a content-bearing unclaimed event to a completed run's ledger and getSessionView
must still refuse the view — the counterpart of the soft reproduction beside it.
@Astro-Han
Astro-Han merged commit 1f43ea7 into mainJul 29, 2026
3 checks passed
Sign up for freeto 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.

fix: isolate an unclaimed RuntimeEvent instead of rejecting the whole session view

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(runtime): isolate an unclaimed control fact from the session view - #1618

Merged
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event
Jul 29, 2026
Merged

fix(runtime): isolate an unclaimed control fact from the session view#1618
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

One RuntimeEvent the projection does not claim made an entire session unreadable, even when every message in it was intact. The projection emitted a hard unsupported_event for any unrecognised shape, RuntimeReadModel.buildView throws on any hard diagnostic, and the whole projection was discarded — failing every getSessionView caller, not just the transcript: branching, revising, and every turn-scoped action went with it. #1607 was one instance; #1609 claimed those two shapes but left the amplification in place.

The hard failure is deliberate — it exists so messages are never silently dropped — so this splits it rather than relaxing it wholesale:

  • unclaimed and content-bearing → a reader may be missing a row, the view is not faithful → stays unsupported_event, hard.
  • unclaimed and control-only → there is no row to lose, while discarding the projection costs every intact message beside it → new unclaimed_control_fact, soft.

Severity now lives in one table keyed by code, so a new diagnostic cannot exist without deciding which side it falls on, and buildView asks the projection instead of restating the codes.

What makes the soft side safe is claim coverage, not the absence of content. Actions do own user-visible rows — permissionDecision, tokenUsage, and the terminal fact all produce one, and runtime-event-backfill.ts already writes a content-free event that projects to a visible permission_decision. Nothing with a row reaches the degrading branch only because every action field a reader can meet is claimed, so the projection-coverage contract now has to prove exactly that.

The contract previously enumerated BackendSessionEvent['type'] alone, which left every RuntimeEvent produced outside mapSessionEventToRuntimeEvent uncovered — tool-runtime, terminal-run-commit, and runtime-event-backfill all write actions directly. It now also enumerates every field of RuntimeEventActions, keyed so a new field cannot compile without a sample and cannot pass without being claimed.

Writing that contract found three action fields the projection had never claimed: artifactDelta, transferToAgent, and runtimeProtocol. The first two have no emitter yet. runtimeProtocol doesruntime-runner.ts writes it, and RecoveryResolver reads it; it only ever avoided breaking a session because it has so far always ridden on an already-claimed carrier. All three are now claimed as control facts.

#1609 declined this downgrade because it "would hide a future projection gap". That is answered by keeping the two questions separate: severity decides whether a session opens, the coverage contract decides whether coverage is missing. The contract asserts on the union of both unclaimed codes, so softening a severity cannot soften the contract.

Closes#1613. Refs #1607, #1609.

Verification

  • @maka/runtime full suite: 2795 tests, 2786 pass, 0 fail, 9 skipped. @maka/headless 1428 pass, CLI 668 pass, 0 fail.
  • End-to-end reproduction: a backend emits a SessionEvent variant the mapping was never taught, the turn completes through a real sendMessage, and the session reads back intact with the unclaimed event reported as one unclaimed_control_fact. Reverting the fallback to always-hard makes it fail with the original RuntimeReadModelError.
  • Its counterpart pins the caller: appending a content-bearing unclaimed event to a completed run's ledger must make getSessionView throw. Verified to bite by making the fallback unconditionally soft.
  • The coverage contract was checked in both directions: removing the runtimeProtocol claim fails it at runtime; removing its table entry fails compilation with TS2741.
  • The unclaimed predicate is now asserted through itself, so dropping unclaimed_control_fact from it can no longer silently narrow both contracts to unsupported_event.
  • Claim strictness is untouched: fix(runtime): reload completed sessions after sandbox boundary decisions #1609's eight malformed-boundary counter-examples still assert the event stays unclaimed; only its severity moved.
  • Test-merged with fix/sandbox-boundary-pending-restart (fix: let a pending sandbox boundary request survive a host restart #1612), which touches the same file: auto-merges clean, combined suite green.
  • npm run format, npm run lint, npm run typecheck --workspaces clean. Not run: desktop E2E — this change does not reach renderer or main.

Review focus

A malformed control fact — one that half-matches a known shape — is soft alongside a genuinely unknown one. The read model is not the enforcement authority (boundary enforcement has its own durable revisions) and an unopenable session is the worst answer available, but if ledger corruption should outrank forward compatibility that argues for a third severity tier, which this PR does not add.

stateDelta is an open record, so the contract can only cover the field's existence, not new keys inside the delta. That limit is recorded in the table rather than papered over.

artifactDelta and transferToAgent are claimed as silent control facts before either has an emitter. If a hand-off should eventually render as a visible row, that claim is where it has to change.

RuntimeReadModel decided which projection diagnostics are fatal by restating
their codes, so the projection declared the diagnostics and its caller declared
what they mean. Move that decision next to the codes as a table keyed by
`RuntimeEventReadModelDiagnosticCode`: a new diagnostic cannot compile without
saying whether it means a user-visible row may be missing. The caller and the
persisted-compat test now ask that authority instead of listing codes.
No behavior change — the table restates today's hard set exactly.
One RuntimeEvent the projection did not claim made an entire session
unreadable. The catch-all emitted a hard `unsupported_event`, RuntimeReadModel
threw on it, and the whole projection went with it — so getMessages, listTurns,
branching, revising and every turn-scoped action failed over a fact that owns no
chat row. #1607 was one instance; #1609 claimed those two shapes but left the
amplification in place.
Split the catch-all on the RuntimeEvent's own structure: `content` is its
message payload, `actions` its control intent. Every row this projection emits
from an unclaimed shape would have come from content, so a content-bearing
event stays hard — "a message is never silently dropped" is the invariant the
hard failure exists for. A control-only fact has nothing to lose, so it becomes
`unclaimed_control_fact` and degrades the view instead of discarding it. A
projector that tried to build a row and failed still reports its own hard
diagnostic, so this softens nothing that attempted a message.
A future gap is still caught before a user meets it. The projection-coverage
contract now asserts on the unclaimed codes at either severity rather than the
hard one alone, so a new SessionEvent variant with no claim still fails CI, and
AiSdkFlow's exhaustiveness guard is what a variant becomes: a content-free
control fact that lands on the degradable side by construction.
Fixes#1613
…meet
The soft path rests on a premise that was not machine-checked: an unclaimed
content-free event degrades the view instead of withholding it, which is only
safe while no unclaimed action can owe a row. `content === undefined` does not
prove that on its own — permissionDecision, tokenUsage and the terminal fact all
produce rows, and runtime-event-backfill already writes a content-free event
that becomes a visible `permission_decision`. What actually holds the rule up is
claim coverage, so make coverage the thing that is proven.
The SessionEvent contract only covers events built by
`mapSessionEventToRuntimeEvent`; tool-runtime, terminal-run-commit and the
backfill write RuntimeEvents directly, so a new action field on those paths was
invisible to it. A second contract keyed on `RuntimeEventActions` gives every
field a reachable sample typed to its own key: a new field cannot compile
without one and cannot pass without being claimed.
Writing it found three fields the projection never claimed — `artifactDelta`,
`transferToAgent` and `runtimeProtocol`, the last of which real emitters already
write. All three are control-only, so claim them, and say in the fallback what
the rule actually depends on.
Two regressions the suite could not see. The unmapped-SessionEvent test compared
the raw code string, so dropping `unclaimed_control_fact` from
`isUnclaimedRuntimeEventDiagnostic` would have quietly narrowed the coverage
contract to `unsupported_event` with every test still green; it now filters
through the predicate itself. And the hard side was only asserted inside the
projector, so a caller that stopped enforcing the policy went unnoticed: append
a content-bearing unclaimed event to a completed run's ledger and getSessionView
must still refuse the view — the counterpart of the soft reproduction beside it.
@Astro-Han
Astro-Han merged commit 1f43ea7 into mainJul 29, 2026
3 checks passed
Sign up for freeto 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.

fix: isolate an unclaimed RuntimeEvent instead of rejecting the whole session view

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(runtime): isolate an unclaimed control fact from the session view - #1618

Merged
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event
Jul 29, 2026
Merged

fix(runtime): isolate an unclaimed control fact from the session view#1618
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

One RuntimeEvent the projection does not claim made an entire session unreadable, even when every message in it was intact. The projection emitted a hard unsupported_event for any unrecognised shape, RuntimeReadModel.buildView throws on any hard diagnostic, and the whole projection was discarded — failing every getSessionView caller, not just the transcript: branching, revising, and every turn-scoped action went with it. #1607 was one instance; #1609 claimed those two shapes but left the amplification in place.

The hard failure is deliberate — it exists so messages are never silently dropped — so this splits it rather than relaxing it wholesale:

  • unclaimed and content-bearing → a reader may be missing a row, the view is not faithful → stays unsupported_event, hard.
  • unclaimed and control-only → there is no row to lose, while discarding the projection costs every intact message beside it → new unclaimed_control_fact, soft.

Severity now lives in one table keyed by code, so a new diagnostic cannot exist without deciding which side it falls on, and buildView asks the projection instead of restating the codes.

What makes the soft side safe is claim coverage, not the absence of content. Actions do own user-visible rows — permissionDecision, tokenUsage, and the terminal fact all produce one, and runtime-event-backfill.ts already writes a content-free event that projects to a visible permission_decision. Nothing with a row reaches the degrading branch only because every action field a reader can meet is claimed, so the projection-coverage contract now has to prove exactly that.

The contract previously enumerated BackendSessionEvent['type'] alone, which left every RuntimeEvent produced outside mapSessionEventToRuntimeEvent uncovered — tool-runtime, terminal-run-commit, and runtime-event-backfill all write actions directly. It now also enumerates every field of RuntimeEventActions, keyed so a new field cannot compile without a sample and cannot pass without being claimed.

Writing that contract found three action fields the projection had never claimed: artifactDelta, transferToAgent, and runtimeProtocol. The first two have no emitter yet. runtimeProtocol doesruntime-runner.ts writes it, and RecoveryResolver reads it; it only ever avoided breaking a session because it has so far always ridden on an already-claimed carrier. All three are now claimed as control facts.

#1609 declined this downgrade because it "would hide a future projection gap". That is answered by keeping the two questions separate: severity decides whether a session opens, the coverage contract decides whether coverage is missing. The contract asserts on the union of both unclaimed codes, so softening a severity cannot soften the contract.

Closes#1613. Refs #1607, #1609.

Verification

  • @maka/runtime full suite: 2795 tests, 2786 pass, 0 fail, 9 skipped. @maka/headless 1428 pass, CLI 668 pass, 0 fail.
  • End-to-end reproduction: a backend emits a SessionEvent variant the mapping was never taught, the turn completes through a real sendMessage, and the session reads back intact with the unclaimed event reported as one unclaimed_control_fact. Reverting the fallback to always-hard makes it fail with the original RuntimeReadModelError.
  • Its counterpart pins the caller: appending a content-bearing unclaimed event to a completed run's ledger must make getSessionView throw. Verified to bite by making the fallback unconditionally soft.
  • The coverage contract was checked in both directions: removing the runtimeProtocol claim fails it at runtime; removing its table entry fails compilation with TS2741.
  • The unclaimed predicate is now asserted through itself, so dropping unclaimed_control_fact from it can no longer silently narrow both contracts to unsupported_event.
  • Claim strictness is untouched: fix(runtime): reload completed sessions after sandbox boundary decisions #1609's eight malformed-boundary counter-examples still assert the event stays unclaimed; only its severity moved.
  • Test-merged with fix/sandbox-boundary-pending-restart (fix: let a pending sandbox boundary request survive a host restart #1612), which touches the same file: auto-merges clean, combined suite green.
  • npm run format, npm run lint, npm run typecheck --workspaces clean. Not run: desktop E2E — this change does not reach renderer or main.

Review focus

A malformed control fact — one that half-matches a known shape — is soft alongside a genuinely unknown one. The read model is not the enforcement authority (boundary enforcement has its own durable revisions) and an unopenable session is the worst answer available, but if ledger corruption should outrank forward compatibility that argues for a third severity tier, which this PR does not add.

stateDelta is an open record, so the contract can only cover the field's existence, not new keys inside the delta. That limit is recorded in the table rather than papered over.

artifactDelta and transferToAgent are claimed as silent control facts before either has an emitter. If a hand-off should eventually render as a visible row, that claim is where it has to change.

RuntimeReadModel decided which projection diagnostics are fatal by restating
their codes, so the projection declared the diagnostics and its caller declared
what they mean. Move that decision next to the codes as a table keyed by
`RuntimeEventReadModelDiagnosticCode`: a new diagnostic cannot compile without
saying whether it means a user-visible row may be missing. The caller and the
persisted-compat test now ask that authority instead of listing codes.
No behavior change — the table restates today's hard set exactly.
One RuntimeEvent the projection did not claim made an entire session
unreadable. The catch-all emitted a hard `unsupported_event`, RuntimeReadModel
threw on it, and the whole projection went with it — so getMessages, listTurns,
branching, revising and every turn-scoped action failed over a fact that owns no
chat row. #1607 was one instance; #1609 claimed those two shapes but left the
amplification in place.
Split the catch-all on the RuntimeEvent's own structure: `content` is its
message payload, `actions` its control intent. Every row this projection emits
from an unclaimed shape would have come from content, so a content-bearing
event stays hard — "a message is never silently dropped" is the invariant the
hard failure exists for. A control-only fact has nothing to lose, so it becomes
`unclaimed_control_fact` and degrades the view instead of discarding it. A
projector that tried to build a row and failed still reports its own hard
diagnostic, so this softens nothing that attempted a message.
A future gap is still caught before a user meets it. The projection-coverage
contract now asserts on the unclaimed codes at either severity rather than the
hard one alone, so a new SessionEvent variant with no claim still fails CI, and
AiSdkFlow's exhaustiveness guard is what a variant becomes: a content-free
control fact that lands on the degradable side by construction.
Fixes#1613
…meet
The soft path rests on a premise that was not machine-checked: an unclaimed
content-free event degrades the view instead of withholding it, which is only
safe while no unclaimed action can owe a row. `content === undefined` does not
prove that on its own — permissionDecision, tokenUsage and the terminal fact all
produce rows, and runtime-event-backfill already writes a content-free event
that becomes a visible `permission_decision`. What actually holds the rule up is
claim coverage, so make coverage the thing that is proven.
The SessionEvent contract only covers events built by
`mapSessionEventToRuntimeEvent`; tool-runtime, terminal-run-commit and the
backfill write RuntimeEvents directly, so a new action field on those paths was
invisible to it. A second contract keyed on `RuntimeEventActions` gives every
field a reachable sample typed to its own key: a new field cannot compile
without one and cannot pass without being claimed.
Writing it found three fields the projection never claimed — `artifactDelta`,
`transferToAgent` and `runtimeProtocol`, the last of which real emitters already
write. All three are control-only, so claim them, and say in the fallback what
the rule actually depends on.
Two regressions the suite could not see. The unmapped-SessionEvent test compared
the raw code string, so dropping `unclaimed_control_fact` from
`isUnclaimedRuntimeEventDiagnostic` would have quietly narrowed the coverage
contract to `unsupported_event` with every test still green; it now filters
through the predicate itself. And the hard side was only asserted inside the
projector, so a caller that stopped enforcing the policy went unnoticed: append
a content-bearing unclaimed event to a completed run's ledger and getSessionView
must still refuse the view — the counterpart of the soft reproduction beside it.
@Astro-Han
Astro-Han merged commit 1f43ea7 into mainJul 29, 2026
3 checks passed
Sign up for freeto 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.

fix: isolate an unclaimed RuntimeEvent instead of rejecting the whole session view

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(runtime): isolate an unclaimed control fact from the session view - #1618

Merged
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event
Jul 29, 2026
Merged

fix(runtime): isolate an unclaimed control fact from the session view#1618
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

One RuntimeEvent the projection does not claim made an entire session unreadable, even when every message in it was intact. The projection emitted a hard unsupported_event for any unrecognised shape, RuntimeReadModel.buildView throws on any hard diagnostic, and the whole projection was discarded — failing every getSessionView caller, not just the transcript: branching, revising, and every turn-scoped action went with it. #1607 was one instance; #1609 claimed those two shapes but left the amplification in place.

The hard failure is deliberate — it exists so messages are never silently dropped — so this splits it rather than relaxing it wholesale:

  • unclaimed and content-bearing → a reader may be missing a row, the view is not faithful → stays unsupported_event, hard.
  • unclaimed and control-only → there is no row to lose, while discarding the projection costs every intact message beside it → new unclaimed_control_fact, soft.

Severity now lives in one table keyed by code, so a new diagnostic cannot exist without deciding which side it falls on, and buildView asks the projection instead of restating the codes.

What makes the soft side safe is claim coverage, not the absence of content. Actions do own user-visible rows — permissionDecision, tokenUsage, and the terminal fact all produce one, and runtime-event-backfill.ts already writes a content-free event that projects to a visible permission_decision. Nothing with a row reaches the degrading branch only because every action field a reader can meet is claimed, so the projection-coverage contract now has to prove exactly that.

The contract previously enumerated BackendSessionEvent['type'] alone, which left every RuntimeEvent produced outside mapSessionEventToRuntimeEvent uncovered — tool-runtime, terminal-run-commit, and runtime-event-backfill all write actions directly. It now also enumerates every field of RuntimeEventActions, keyed so a new field cannot compile without a sample and cannot pass without being claimed.

Writing that contract found three action fields the projection had never claimed: artifactDelta, transferToAgent, and runtimeProtocol. The first two have no emitter yet. runtimeProtocol doesruntime-runner.ts writes it, and RecoveryResolver reads it; it only ever avoided breaking a session because it has so far always ridden on an already-claimed carrier. All three are now claimed as control facts.

#1609 declined this downgrade because it "would hide a future projection gap". That is answered by keeping the two questions separate: severity decides whether a session opens, the coverage contract decides whether coverage is missing. The contract asserts on the union of both unclaimed codes, so softening a severity cannot soften the contract.

Closes#1613. Refs #1607, #1609.

Verification

  • @maka/runtime full suite: 2795 tests, 2786 pass, 0 fail, 9 skipped. @maka/headless 1428 pass, CLI 668 pass, 0 fail.
  • End-to-end reproduction: a backend emits a SessionEvent variant the mapping was never taught, the turn completes through a real sendMessage, and the session reads back intact with the unclaimed event reported as one unclaimed_control_fact. Reverting the fallback to always-hard makes it fail with the original RuntimeReadModelError.
  • Its counterpart pins the caller: appending a content-bearing unclaimed event to a completed run's ledger must make getSessionView throw. Verified to bite by making the fallback unconditionally soft.
  • The coverage contract was checked in both directions: removing the runtimeProtocol claim fails it at runtime; removing its table entry fails compilation with TS2741.
  • The unclaimed predicate is now asserted through itself, so dropping unclaimed_control_fact from it can no longer silently narrow both contracts to unsupported_event.
  • Claim strictness is untouched: fix(runtime): reload completed sessions after sandbox boundary decisions #1609's eight malformed-boundary counter-examples still assert the event stays unclaimed; only its severity moved.
  • Test-merged with fix/sandbox-boundary-pending-restart (fix: let a pending sandbox boundary request survive a host restart #1612), which touches the same file: auto-merges clean, combined suite green.
  • npm run format, npm run lint, npm run typecheck --workspaces clean. Not run: desktop E2E — this change does not reach renderer or main.

Review focus

A malformed control fact — one that half-matches a known shape — is soft alongside a genuinely unknown one. The read model is not the enforcement authority (boundary enforcement has its own durable revisions) and an unopenable session is the worst answer available, but if ledger corruption should outrank forward compatibility that argues for a third severity tier, which this PR does not add.

stateDelta is an open record, so the contract can only cover the field's existence, not new keys inside the delta. That limit is recorded in the table rather than papered over.

artifactDelta and transferToAgent are claimed as silent control facts before either has an emitter. If a hand-off should eventually render as a visible row, that claim is where it has to change.

RuntimeReadModel decided which projection diagnostics are fatal by restating
their codes, so the projection declared the diagnostics and its caller declared
what they mean. Move that decision next to the codes as a table keyed by
`RuntimeEventReadModelDiagnosticCode`: a new diagnostic cannot compile without
saying whether it means a user-visible row may be missing. The caller and the
persisted-compat test now ask that authority instead of listing codes.
No behavior change — the table restates today's hard set exactly.
One RuntimeEvent the projection did not claim made an entire session
unreadable. The catch-all emitted a hard `unsupported_event`, RuntimeReadModel
threw on it, and the whole projection went with it — so getMessages, listTurns,
branching, revising and every turn-scoped action failed over a fact that owns no
chat row. #1607 was one instance; #1609 claimed those two shapes but left the
amplification in place.
Split the catch-all on the RuntimeEvent's own structure: `content` is its
message payload, `actions` its control intent. Every row this projection emits
from an unclaimed shape would have come from content, so a content-bearing
event stays hard — "a message is never silently dropped" is the invariant the
hard failure exists for. A control-only fact has nothing to lose, so it becomes
`unclaimed_control_fact` and degrades the view instead of discarding it. A
projector that tried to build a row and failed still reports its own hard
diagnostic, so this softens nothing that attempted a message.
A future gap is still caught before a user meets it. The projection-coverage
contract now asserts on the unclaimed codes at either severity rather than the
hard one alone, so a new SessionEvent variant with no claim still fails CI, and
AiSdkFlow's exhaustiveness guard is what a variant becomes: a content-free
control fact that lands on the degradable side by construction.
Fixes#1613
…meet
The soft path rests on a premise that was not machine-checked: an unclaimed
content-free event degrades the view instead of withholding it, which is only
safe while no unclaimed action can owe a row. `content === undefined` does not
prove that on its own — permissionDecision, tokenUsage and the terminal fact all
produce rows, and runtime-event-backfill already writes a content-free event
that becomes a visible `permission_decision`. What actually holds the rule up is
claim coverage, so make coverage the thing that is proven.
The SessionEvent contract only covers events built by
`mapSessionEventToRuntimeEvent`; tool-runtime, terminal-run-commit and the
backfill write RuntimeEvents directly, so a new action field on those paths was
invisible to it. A second contract keyed on `RuntimeEventActions` gives every
field a reachable sample typed to its own key: a new field cannot compile
without one and cannot pass without being claimed.
Writing it found three fields the projection never claimed — `artifactDelta`,
`transferToAgent` and `runtimeProtocol`, the last of which real emitters already
write. All three are control-only, so claim them, and say in the fallback what
the rule actually depends on.
Two regressions the suite could not see. The unmapped-SessionEvent test compared
the raw code string, so dropping `unclaimed_control_fact` from
`isUnclaimedRuntimeEventDiagnostic` would have quietly narrowed the coverage
contract to `unsupported_event` with every test still green; it now filters
through the predicate itself. And the hard side was only asserted inside the
projector, so a caller that stopped enforcing the policy went unnoticed: append
a content-bearing unclaimed event to a completed run's ledger and getSessionView
must still refuse the view — the counterpart of the soft reproduction beside it.
@Astro-Han
Astro-Han merged commit 1f43ea7 into mainJul 29, 2026
3 checks passed
Sign up for freeto 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.

fix: isolate an unclaimed RuntimeEvent instead of rejecting the whole session view

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(runtime): isolate an unclaimed control fact from the session view - #1618

Merged
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event
Jul 29, 2026
Merged

fix(runtime): isolate an unclaimed control fact from the session view#1618
Astro-Han merged 4 commits into
mainfrom
fix/read-model-isolate-unclaimed-event

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

One RuntimeEvent the projection does not claim made an entire session unreadable, even when every message in it was intact. The projection emitted a hard unsupported_event for any unrecognised shape, RuntimeReadModel.buildView throws on any hard diagnostic, and the whole projection was discarded — failing every getSessionView caller, not just the transcript: branching, revising, and every turn-scoped action went with it. #1607 was one instance; #1609 claimed those two shapes but left the amplification in place.

The hard failure is deliberate — it exists so messages are never silently dropped — so this splits it rather than relaxing it wholesale:

  • unclaimed and content-bearing → a reader may be missing a row, the view is not faithful → stays unsupported_event, hard.
  • unclaimed and control-only → there is no row to lose, while discarding the projection costs every intact message beside it → new unclaimed_control_fact, soft.

Severity now lives in one table keyed by code, so a new diagnostic cannot exist without deciding which side it falls on, and buildView asks the projection instead of restating the codes.

What makes the soft side safe is claim coverage, not the absence of content. Actions do own user-visible rows — permissionDecision, tokenUsage, and the terminal fact all produce one, and runtime-event-backfill.ts already writes a content-free event that projects to a visible permission_decision. Nothing with a row reaches the degrading branch only because every action field a reader can meet is claimed, so the projection-coverage contract now has to prove exactly that.

The contract previously enumerated BackendSessionEvent['type'] alone, which left every RuntimeEvent produced outside mapSessionEventToRuntimeEvent uncovered — tool-runtime, terminal-run-commit, and runtime-event-backfill all write actions directly. It now also enumerates every field of RuntimeEventActions, keyed so a new field cannot compile without a sample and cannot pass without being claimed.

Writing that contract found three action fields the projection had never claimed: artifactDelta, transferToAgent, and runtimeProtocol. The first two have no emitter yet. runtimeProtocol doesruntime-runner.ts writes it, and RecoveryResolver reads it; it only ever avoided breaking a session because it has so far always ridden on an already-claimed carrier. All three are now claimed as control facts.

#1609 declined this downgrade because it "would hide a future projection gap". That is answered by keeping the two questions separate: severity decides whether a session opens, the coverage contract decides whether coverage is missing. The contract asserts on the union of both unclaimed codes, so softening a severity cannot soften the contract.

Closes#1613. Refs #1607, #1609.

Verification

  • @maka/runtime full suite: 2795 tests, 2786 pass, 0 fail, 9 skipped. @maka/headless 1428 pass, CLI 668 pass, 0 fail.
  • End-to-end reproduction: a backend emits a SessionEvent variant the mapping was never taught, the turn completes through a real sendMessage, and the session reads back intact with the unclaimed event reported as one unclaimed_control_fact. Reverting the fallback to always-hard makes it fail with the original RuntimeReadModelError.
  • Its counterpart pins the caller: appending a content-bearing unclaimed event to a completed run's ledger must make getSessionView throw. Verified to bite by making the fallback unconditionally soft.
  • The coverage contract was checked in both directions: removing the runtimeProtocol claim fails it at runtime; removing its table entry fails compilation with TS2741.
  • The unclaimed predicate is now asserted through itself, so dropping unclaimed_control_fact from it can no longer silently narrow both contracts to unsupported_event.
  • Claim strictness is untouched: fix(runtime): reload completed sessions after sandbox boundary decisions #1609's eight malformed-boundary counter-examples still assert the event stays unclaimed; only its severity moved.
  • Test-merged with fix/sandbox-boundary-pending-restart (fix: let a pending sandbox boundary request survive a host restart #1612), which touches the same file: auto-merges clean, combined suite green.
  • npm run format, npm run lint, npm run typecheck --workspaces clean. Not run: desktop E2E — this change does not reach renderer or main.

Review focus

A malformed control fact — one that half-matches a known shape — is soft alongside a genuinely unknown one. The read model is not the enforcement authority (boundary enforcement has its own durable revisions) and an unopenable session is the worst answer available, but if ledger corruption should outrank forward compatibility that argues for a third severity tier, which this PR does not add.

stateDelta is an open record, so the contract can only cover the field's existence, not new keys inside the delta. That limit is recorded in the table rather than papered over.

artifactDelta and transferToAgent are claimed as silent control facts before either has an emitter. If a hand-off should eventually render as a visible row, that claim is where it has to change.

RuntimeReadModel decided which projection diagnostics are fatal by restating
their codes, so the projection declared the diagnostics and its caller declared
what they mean. Move that decision next to the codes as a table keyed by
`RuntimeEventReadModelDiagnosticCode`: a new diagnostic cannot compile without
saying whether it means a user-visible row may be missing. The caller and the
persisted-compat test now ask that authority instead of listing codes.
No behavior change — the table restates today's hard set exactly.
One RuntimeEvent the projection did not claim made an entire session
unreadable. The catch-all emitted a hard `unsupported_event`, RuntimeReadModel
threw on it, and the whole projection went with it — so getMessages, listTurns,
branching, revising and every turn-scoped action failed over a fact that owns no
chat row. #1607 was one instance; #1609 claimed those two shapes but left the
amplification in place.
Split the catch-all on the RuntimeEvent's own structure: `content` is its
message payload, `actions` its control intent. Every row this projection emits
from an unclaimed shape would have come from content, so a content-bearing
event stays hard — "a message is never silently dropped" is the invariant the
hard failure exists for. A control-only fact has nothing to lose, so it becomes
`unclaimed_control_fact` and degrades the view instead of discarding it. A
projector that tried to build a row and failed still reports its own hard
diagnostic, so this softens nothing that attempted a message.
A future gap is still caught before a user meets it. The projection-coverage
contract now asserts on the unclaimed codes at either severity rather than the
hard one alone, so a new SessionEvent variant with no claim still fails CI, and
AiSdkFlow's exhaustiveness guard is what a variant becomes: a content-free
control fact that lands on the degradable side by construction.
Fixes#1613
…meet
The soft path rests on a premise that was not machine-checked: an unclaimed
content-free event degrades the view instead of withholding it, which is only
safe while no unclaimed action can owe a row. `content === undefined` does not
prove that on its own — permissionDecision, tokenUsage and the terminal fact all
produce rows, and runtime-event-backfill already writes a content-free event
that becomes a visible `permission_decision`. What actually holds the rule up is
claim coverage, so make coverage the thing that is proven.
The SessionEvent contract only covers events built by
`mapSessionEventToRuntimeEvent`; tool-runtime, terminal-run-commit and the
backfill write RuntimeEvents directly, so a new action field on those paths was
invisible to it. A second contract keyed on `RuntimeEventActions` gives every
field a reachable sample typed to its own key: a new field cannot compile
without one and cannot pass without being claimed.
Writing it found three fields the projection never claimed — `artifactDelta`,
`transferToAgent` and `runtimeProtocol`, the last of which real emitters already
write. All three are control-only, so claim them, and say in the fallback what
the rule actually depends on.
Two regressions the suite could not see. The unmapped-SessionEvent test compared
the raw code string, so dropping `unclaimed_control_fact` from
`isUnclaimedRuntimeEventDiagnostic` would have quietly narrowed the coverage
contract to `unsupported_event` with every test still green; it now filters
through the predicate itself. And the hard side was only asserted inside the
projector, so a caller that stopped enforcing the policy went unnoticed: append
a content-bearing unclaimed event to a completed run's ledger and getSessionView
must still refuse the view — the counterpart of the soft reproduction beside it.
@Astro-Han
Astro-Han merged commit 1f43ea7 into mainJul 29, 2026
3 checks passed
Sign up for freeto 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.

fix: isolate an unclaimed RuntimeEvent instead of rejecting the whole session view

1 participant

@Astro-Han