Repository navigation
feat(rpc): opt-in seeded projection and the AgentSession structured lane (part of #4272) - #4349
Merged
Merged
Conversation
code-yeongyu
marked this pull request as ready for review
October 26, 2026 06:50
…ion instead of exporting it (part of #4272) Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
S02b: opt-in seeded projection and the AgentSession structured lane (part of #4272)
Part of #4272: contract R3.C6.2, part b (ruling 05 D1 split; S02a is #4277/#4285/#4307). Stacked on #4307 (
feat/4272-s02a3-recovery-remap, merged head4f42329ea3, including RULING-18a and its main merge) and onmain(S01 #4276, S02a-1 #4277). Engine staged: nothing is selectable on a public socket before S17.What this PR does
toJsonEvent(event, { blockSeeds: 1 })(modes/json-event.ts). This overload projectsmessage_updatefrom the producer-time capture only. Starts carryseed.blockandcoveredCodeUnits, plusrawArgumentPrefixandparsedThroughfor tool calls. Deltas carryoffset, and tool deltas also carryparsedThrough. Ends carry the frozen block, anddone/errorcarry the terminal message. Every record carries frozenusage, andmessageMetadataonly when metadata changed. Assistantmessage_start/message_endtake the capture's frozen message. A record with no capture has no seeded form (undefined), so no seed is ever read from the live partial (D5). Identity fields are passed through and never minted. Internal readers usecapture.contentIndex ?? event.contentIndex(feat(ai): capture block seeds at emission for the remaining provider families (part of #4272) #4285 L1/L2). The no-options call is unchanged byte for byte, andprint-mode.tsandrpc-mode.tskeep calling it.Three internal controls (
message_block_state,message_block_metadata,message_tool_arguments_reset). They come from the S02a capture records and travel on the same ordered lane as ordinary records. They are internal until S17 (D4).AgentSession bridge (
core/structured-session-lane.ts,agent-session.ts).subscribeStructuredholds the agent's internal channel only while a structured listener exists. Controls are queued on_agentEventQueue, so they keep their position among the session's events. Each published event carries the capture of the agent event it came from. The RPC binding attaches the lane when the connection binds, before the session's next provider push. Every rebind re-attaches it.Block validity gate (
modes/rpc/structured-session-records.ts). A block's records are admitted only after its own seed or insert reached this subscriber, so a late attach gets nothing for that message, never a wrong seed. A block becomes invalid in four cases:rewrittenFrom(feat(ai): carry capture records through recovery wrappers with remapped indices (part of #4272) #4307 fold);message_endhas no interpretable terminal capture (no captured terminal message), as on an agent cancel or idle timeout (feat(ai): carry capture records through recovery wrappers with remapped indices (part of #4272) #4307 fold).Records of an invalid block are dropped, and the assistant
message_endlists the block ininvalidatedBlocks, so a reader rebuilds it from the terminal message.OFF by default. The lane exists only while
<brand>_ENGINE_SOCKET_COMPACT_UPDATESis1ortrue. S01 added no flag, so this reuses the plan's named rollout flagOMO_ENGINE_SOCKET_COMPACT_UPDATES(SENPI_prefix in senpi) throughenvValue. With the flag off, the binding supplies nowriteStructured, nothing subscribes, and socket and stdout bytes are identical (tested).LOW b: module-wide capture subscription (documented and measured)
subscribeAssistantCaptureis a module-wide counter. While one session holds a structured listener, every capture-enabled stream in the process that first pushes afterwards builds capture records. Streams owned by other sessions then drop those records at their public iterator. I did not scope it per stream. Doing that needs an owner key on S02a's carrier, which is anaipackage change outside this PR. Testmeasures the module-wide capture subscription on a stream no lane ownspins the behaviour on an unrelated stream: it is captured before = false, during = true, after = false. The cost is bounded by the flag, because nothing subscribes while it is off.D6 staged gap: start/end/reset/native payloads over F
Seeded starts, ends, resets, native
message_block_stateblocks and terminal messages stay complete logical records in this PR. Nothing here fragments or leases them. S11 and S13 own fragmenting (F/D) and immutable leases. Under "engine staged" no public socket can select the compact form: the lane is OFF by default and has no wire destination (SessionEventWriter.structuredLaneshas only in-process observers). So an over-F record cannot reach a peer before S11/S13. This PR does not touch the existing capability gate.RED -> GREEN (logs:
s02b-red.log,s02b-green.log)Each mutation was applied alone, run, then restored from git. Status was checked clean after every restore.
6a52e5432csrc)4272-seeded-json-event(then incl. changed-byte work)expected {...} to match object), D5 uncaptured (expected {...} to be undefined), changed-byte work (an ordinary delta must not touch the message, partial or block: expected [ 'message.role', …(3263) ] to deeply equal [])expected [ true, true ] to deeply equal [ false, false ]expected [ 'message_block_state:insert', …(2) ] to deeply equal [ 'message_end' ]expected undefined to deeply equal [ +0 ]expected [ 'message_start', …(9) ] to deeply equal [ 'message_start', …(12) ]92a4dad8ec) against the #4307 foldexpected undefined to deeply equal [ 1, 2 ]), message_end without terminal capture (expected undefined to deeply equal [ +0 ])contentIndexexpected [ +0, 3 ] to be undefinedM2 and M4 first passed against a weaker test; I strengthened the tests (a second, early lane; an extension listener holding the session queue) and re-ran both mutations, which then failed as shown. GREEN at
73e935815b: 5 files, 13/13 (4272-seeded-json-event,4272-seeded-changed-byte-work,4272-structured-lane-bridge,4272-structured-lane-validity,4272-structured-lane-google-index).Contract condition coverage (finish pass)
Paths below are relative to
packages/coding-agent/. Each named test is intest/suite/regressions/. D6 remains an explicitly staged documentation obligation, not a claim that fragmentation is implemented.src/modes/json-event.ts:944272-seeded-changed-byte-work.test.ts:42-T6 never reads, clones, parses or stringifies content on an ordinary delta (D3: 0 parses over no-growth deltas)src/core/structured-session-lane.ts:34,src/modes/rpc/session-binding.ts:864272-structured-lane-bridge.test.ts:125-T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacysrc/modes/json-event.ts:1304272-seeded-json-event.test.ts:229-T6 gives an uncaptured assistant update or start no seeded form (D5: nothing invented); identity assertions in4272-seeded-json-event.test.ts:55-T6 projects every block kind from capture taken when the producer was ahead of the consumersrc/modes/rpc/session-binding.ts:864272-structured-lane-bridge.test.ts:151-keeps legacy socket and classic stdout bytes identical with the flag off, and opens no lane(isolation only; fragmentation is not implemented here)message_block_stateabsent from legacy sockets, classic stdout and extensionssrc/core/structured-session-lane.ts:344272-structured-lane-bridge.test.ts:125-T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacymessage_block_metadataabsent from legacy sockets, classic stdout and extensionssrc/core/structured-session-lane.ts:344272-structured-lane-bridge.test.ts:125-T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacymessage_tool_arguments_resetabsent from legacy sockets, classic stdout and extensionssrc/core/structured-session-lane.ts:344272-structured-lane-bridge.test.ts:125-T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacysrc/core/agent-session.ts:1704,src/core/structured-session-lane.ts:364272-structured-lane-bridge.test.ts:125-T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacysrc/modes/rpc/structured-session-records.ts:394272-structured-lane-validity.test.ts:33-T6 gives a lane attached mid-message no seed, delta or control for that message (late attach)src/modes/rpc/structured-session-records.ts:52,:714272-structured-lane-validity.test.ts:102-T6 invalidates a block whose seed was seen but whose end capture failed (LOW a)src/core/structured-session-lane.ts:14; this body's LOW-b section4272-structured-lane-bridge.test.ts:161-measures the module-wide capture subscription on a stream no lane owns (LOW b)src/modes/json-event.ts:93,src/modes/rpc/structured-session-records.ts:474272-structured-lane-google-index.test.ts:43-T6 addresses interleaved Google blocks by the capture's own index on the structured lanesrc/modes/rpc/structured-session-records.ts:704272-structured-lane-terminal-authority.test.ts:50-T6 invalidates ALL turn blocks when the reader cannot interpret a terminal capturesrc/modes/rpc/structured-session-records.ts:704272-structured-lane-validity.test.ts:165-T6 invalidates every block of a message that ends without a terminal capture(two independently ended blocks)src/modes/rpc/structured-session-records.ts:704272-structured-lane-validity.test.ts:129-T6 invalidates every block at or above a rewriting terminal's rewrittenFrom(invalidates 1 and 2, preserves 0)src/modes/json-event.ts:1324272-structured-lane-terminal-authority.test.ts:20-T6 replaces streamed block CONTENT from the Kimi recovery terminal: hi<|open|>response there becomes hi theresrc/modes/json-event.ts:1674272-seeded-json-event.test.ts:243-keeps the default projection byte-identical (golden)The uninterpretable-capture test injects a real nonterminal capture (with a block but no terminal message) at the session reader boundary. All blocks have already ended, so passing requires invalidating them because terminal authority is absent, not merely because they remained open. The Kimi test drives the real recovery wrapper through AgentSession and the structured projector; it observes raw marker-bearing text at block end, then the corrected
hi thereterminal with no identity rewrite marker.T6 changed-byte work assertion (details)
Test
T6 never reads, clones, parses or stringifies content on an ordinary delta (D3: 0 parses over no-growth deltas):type,contentIndexanddeltamust be none.JSON.parse,JSON.stringifyandstructuredClonecalls during projection must be 0.parsedThrough: 0, so the parse frontier did not advance.Gates at
73e935815bbun run buildnode_modules/.bin/tsc --noEmitnode scripts/lint-ratchet.mjsbun run checknode_modules/.bin/oxfmt --check(21 changed files)packages/aitestpackages/agenttestpackages/coding-agenttestai/agentdist stale (Unmet test prerequisite: the workspace build is stale) for 26 files. Afterbun run build(exit 0), the 34 failed files were re-run: 32 passed, 135/138 tests. The 2 remaining files are environmental:terminal-bash-session-envinherits the agent host'sPI_SESSION_CWD(8/8 pass with it unset);3064-owned-tui-socketfails identically on base production (6a52e5432c) under the long mandatedTMPDIR. Logs:s02b-coding-agent-full.log,s02b-coding-agent-rerun.logProduction size
S02b's own production commits (
980cf6cecf,9c85b55e0a): +356 / −11 lines. That breaks down asjson-event.ts120,structured-session-records.ts98,structured-session-lane.ts65,session-structured-lanes.ts23,agent-session.ts19,session-binding.ts11,connection-handler.ts10,session-record-stream.ts4,session-event-writer.ts2,internal-events.ts2,agent/src/index.ts1 andai/.../assistant-capture.ts1 (reader note). Every file I added stays under 250 code lines (the largest,json-event.ts, has 150). Tests: 6 files, 1,012 lines.Finish-pass verification after merging
4f42329ea3Final local head:
4ded5cc6364465a30dcc0e01efc946104cbbc224(fix(rpc): fail closed on nonterminal capture (part of #4272)), authorYeonGyu-Kim <code.yeon.gyu@gmail.com>. Commit exit 0; worktree clean; not pushed.STEP 0 merge:
4a1c4f54cbc7926894318391d312ac5ed7a5fb87, authorYeonGyu-Kim <code.yeon.gyu@gmail.com>, no conflicts. The required rebuild exited 0 before testing; there were no stale-dist test failures in this pass.The new fail-closed test was RED against the merged pre-fix source: 1 failed / 1 passed, exit 1,
expected undefined to deeply equal [ +0, 1 ]at terminal-authority line 87. The Kimi probe already passed. Requiringcapture.messageat the terminal gate fixes the distinction between any capture object and a capture the reader can interpret as terminal authority. GREEN: all 6 S02b files / 15 tests passed, exit 0, including all three separate condition-2 cases and the real Kimi recovery probe.nice -n 10 bun run builds02b-finish-build.logs02b-finish-green.logbun run --cwd packages/ai test test/tool-call-middleware/capture-s02b-finish-ai-capture.lognode_modules/.bin/tsc --noEmits02b-finish-tsc.loggit add, thennode scripts/lint-ratchet.mjss02b-finish-ratchet.lognode_modules/.bin/oxlint --deny-warningson the complete S02b diffs02b-finish-oxlint.lognode_modules/.bin/oxfmt --checkon the complete S02b diffs02b-finish-oxfmt.lognice -n 10 bun run checks02b-finish-check.loggit diff --cached --checkThe finish brief requests focused suites plus AI capture suites, not another full multi-hour package run. The full AI/agent/coding-agent counts in the earlier table are historical evidence at
73e935815b, not claims about this finish head. No full package suite or real CLI QA was run in the finish pass; the changed internal lane was exercised through real AgentSession and RPC binding owner tests.Current aggregate S02b production diff against
origin/feat/4272-s02a3-recovery-remap(4f42329ea3): +353 / -8, 12 production files (net +345), below the 600-line scope ceiling. The finish pass changes one executable production line (+1/-1) plus two comment lines. Changed-file code sizes: gate 70, validity tests 181, terminal-authority tests 82; all below 250. Aggregate test/support diff: 7 files, 1,105 lines. The older production-size paragraph is historical commit-sum evidence, not the current net diff.Assumptions:
AssistantCaptureis an internal typed carrier, so a terminal capture is interpretable here when it carries its typed terminalmessage; there is no new public schema validator. RULING-18 invalidation is communicated throughinvalidatedBlocksbecause previously published ordered records cannot be retracted. Downstream consumers rebuild invalidated content from the authoritative terminal, as already specified by the staged contract.Option B construction-capability fold (local commit, not pushed)
Local commit:
dd78052997cb1803861a11fb5db2e3a65cb89d18. The PR branch still points to6c6135383dc54f1d6df9544e64f8e2b10d8e02db; the fold below has not been pushed.The review MEDIUM is resolved at its root in this local fold: coding-agent no longer imports
pi-agent-core/internal-events, the package exports map no longer publishes that subpath, and the Vitest subpath aliases are gone. The session SDK creates the channel and passes it at Agent construction. Agent keeps it in a JavaScript private field, with no getter, attach method, enumerable property, Symbol.for key, or globalThis registry. The factory, channel type, and constructor option retain their@internalannotations in the emitted declarations for senpi's session layer only.An AgentSession supplied with an externally constructed Agent without the construction channel has no structured lane. A structured subscription fails explicitly with StructuredLaneUnavailableError; ordinary session events and prompts still work. There is no fallback that attaches a channel after construction.
Local evidence at this commit: the Agent package has 125 passing test files, 1,290 passing tests and one existing skip; the seeded/structured/session regression set has seven passing files and 16 passing tests. For test (e), all six
senpi-codemode/test/js-kernel-webview-lifecycle.test.tstests passed under Bun with the agent build output hidden, then restored in a finally block. The former missing-internal-events-package failure is gone. The Node invocation imports successfully but skips these Bun-only tests; the Bun invocation executes all six. The complete build, typecheck, lint ratchet, changed-file oxlint/oxfmt, full static check, real CLI mock-tool turn, and RPC reasoning turn exited 0. The changelog gate exits 0 using this PR's existing no-changelog label; no label was added by this fold.