Skip to content

feat(rpc): opt-in seeded projection and the AgentSession structured lane (part of #4272) - #4349

Merged
code-yeongyu merged 14 commits into
mainfrom
feat/4272-s02b-projector-bridge
Oct 26, 2026
Merged

code-yeongyu merged 14 commits into
mainfrom
feat/4272-s02b-projector-bridge

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

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 head 4f42329ea3, including RULING-18a and its main merge) and on main (S01 #4276, S02a-1 #4277). Engine staged: nothing is selectable on a public socket before S17.

What this PR does

  1. toJsonEvent(event, { blockSeeds: 1 }) (modes/json-event.ts). This overload projects message_update from the producer-time capture only. Starts carry seed.block and coveredCodeUnits, plus rawArgumentPrefix and parsedThrough for tool calls. Deltas carry offset, and tool deltas also carry parsedThrough. Ends carry the frozen block, and done/error carry the terminal message. Every record carries frozen usage, and messageMetadata only when metadata changed. Assistant message_start/message_end take 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 use capture.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, and print-mode.ts and rpc-mode.ts keep calling it.

  2. 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).

  3. AgentSession bridge (core/structured-session-lane.ts, agent-session.ts). subscribeStructured holds 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.

  4. 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:

    Records of an invalid block are dropped, and the assistant message_end lists the block in invalidatedBlocks, so a reader rebuilds it from the terminal message.

  5. OFF by default. The lane exists only while <brand>_ENGINE_SOCKET_COMPACT_UPDATES is 1 or true. S01 added no flag, so this reuses the plan's named rollout flag OMO_ENGINE_SOCKET_COMPACT_UPDATES (SENPI_ prefix in senpi) through envValue. With the flag off, the binding supplies no writeStructured, nothing subscribes, and socket and stdout bytes are identical (tested).

LOW b: module-wide capture subscription (documented and measured)

subscribeAssistantCapture is 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 an ai package change outside this PR. Test measures the module-wide capture subscription on a stream no lane owns pins 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_state blocks 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.structuredLanes has 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.

id what test exit result
RED-0 base production (6a52e5432c src) 4272-seeded-json-event (then incl. changed-byte work) 1 3 failed / 2 passed: every-kind projection (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 [])
M1 flag ignored (lane always opened) bridge 1 flag-off test: expected [ true, true ] to deeply equal [ false, false ]
M2 late attach admitted (gate never waits for its own seed) bridge, late attach 1 expected [ 'message_block_state:insert', …(2) ] to deeply equal [ 'message_end' ]
M3 LOW a: seed seen + end missing not invalidated validity 1 expected undefined to deeply equal [ +0 ]
M4 controls bypass the session queue bridge, ordering 1 expected [ 'message_start', …(9) ] to deeply equal [ 'message_start', …(12) ]
RED-5 pre-fold gate (92a4dad8ec) against the #4307 fold validity 1 2 failed: rewrittenFrom (expected undefined to deeply equal [ 1, 2 ]), message_end without terminal capture (expected undefined to deeply equal [ +0 ])
M5 internal readers use the public contentIndex google index (real Google adapter, interleaved native parts) 1 expected [ +0, 3 ] to be undefined

M2 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 in test/suite/regressions/. D6 remains an explicitly staged documentation obligation, not a claim that fragmentation is implemented.

Contract condition Production / evidence anchor Test file:line and exact test name
RULING-05 D3: unchanged parse frontier and proportional delta work src/modes/json-event.ts:94 4272-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)
RULING-05 D4: internal controls only src/core/structured-session-lane.ts:34, src/modes/rpc/session-binding.ts:86 4272-structured-lane-bridge.test.ts:125 - T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacy
RULING-05 D5: no fabricated seed or identity src/modes/json-event.ts:130 4272-seeded-json-event.test.ts:229 - T6 gives an uncaptured assistant update or start no seeded form (D5: nothing invented); identity assertions in 4272-seeded-json-event.test.ts:55 - T6 projects every block kind from capture taken when the producer was ahead of the consumer
RULING-05 D6: over-F starts/ends/resets remain staged until S11/S13/S17 This body's D6 staged-gap section; src/modes/rpc/session-binding.ts:86 4272-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)
Placement: message_block_state absent from legacy sockets, classic stdout and extensions src/core/structured-session-lane.ts:34 4272-structured-lane-bridge.test.ts:125 - T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacy
Placement: message_block_metadata absent from legacy sockets, classic stdout and extensions src/core/structured-session-lane.ts:34 4272-structured-lane-bridge.test.ts:125 - T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacy
Placement: message_tool_arguments_reset absent from legacy sockets, classic stdout and extensions src/core/structured-session-lane.ts:34 4272-structured-lane-bridge.test.ts:125 - T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacy
Placement: controls retain order against ordinary events; attach before provider emission on each bind src/core/agent-session.ts:1704, src/core/structured-session-lane.ts:36 4272-structured-lane-bridge.test.ts:125 - T6 orders all three controls among ordinary records on the lane, on every bind, and never anywhere legacy
Late attach: no block controls or guessed seed src/modes/rpc/structured-session-records.ts:39 4272-structured-lane-validity.test.ts:33 - T6 gives a lane attached mid-message no seed, delta or control for that message (late attach)
#4277 LOW a: seed seen, end capture missing src/modes/rpc/structured-session-records.ts:52, :71 4272-structured-lane-validity.test.ts:102 - T6 invalidates a block whose seed was seen but whose end capture failed (LOW a)
#4277 LOW b: module-wide subscription measured and documented src/core/structured-session-lane.ts:14; this body's LOW-b section 4272-structured-lane-bridge.test.ts:161 - measures the module-wide capture subscription on a stream no lane owns (LOW b)
#4285 L1/L2: prefer capture.contentIndex after interleaved Google native parts src/modes/json-event.ts:93, src/modes/rpc/structured-session-records.ts:47 4272-structured-lane-google-index.test.ts:43 - T6 addresses interleaved Google blocks by the capture's own index on the structured lane
RULING-18 condition 2(a): uninterpretable terminal capture invalidates ALL turn blocks src/modes/rpc/structured-session-records.ts:70 4272-structured-lane-terminal-authority.test.ts:50 - T6 invalidates ALL turn blocks when the reader cannot interpret a terminal capture
RULING-18 condition 2(b): no terminal capture invalidates ALL turn blocks src/modes/rpc/structured-session-records.ts:70 4272-structured-lane-validity.test.ts:165 - T6 invalidates every block of a message that ends without a terminal capture (two independently ended blocks)
RULING-18 condition 2(c): drop records at/above rewrittenFrom src/modes/rpc/structured-session-records.ts:70 4272-structured-lane-validity.test.ts:129 - T6 invalidates every block at or above a rewriting terminal's rewrittenFrom (invalidates 1 and 2, preserves 0)
RULING-18a: replace block CONTENT from end/terminal, not append-only live records src/modes/json-event.ts:132 4272-structured-lane-terminal-authority.test.ts:20 - T6 replaces streamed block CONTENT from the Kimi recovery terminal: hi<|open|>response there becomes hi there
Default projection bytes src/modes/json-event.ts:167 4272-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 there terminal 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):

  • Setup: 1,024 text deltas of 1 KiB (1 MiB in total) plus 64 no-growth tool deltas.
  • The message and the event are proxied. Reads other than type, contentIndex and delta must be none.
  • JSON.parse, JSON.stringify and structuredClone calls during projection must be 0.
  • Projected delta sizes differ by at most 6 bytes, the offset's digits only; no record carries the growing prefix.
  • No-growth tool deltas keep parsedThrough: 0, so the parse frontier did not advance.

Gates at 73e935815b

gate exit
bun run build 0
node_modules/.bin/tsc --noEmit 0
node scripts/lint-ratchet.mjs 0 (every count matches the baseline)
bun run check 0
node_modules/.bin/oxfmt --check (21 changed files) 0
packages/ai test 0: 419 files passed, 27 skipped; 4,077 tests passed, 909 skipped
packages/agent test 0: 121 files passed; 1,281 tests passed, 1 skipped
packages/coding-agent test 1 on the full run: 34 files failed / 2,564 passed / 15 skipped; 9 tests failed / 20,063 passed / 72 skipped (6,121 s). Root cause: the stacked-base merge left ai/agent dist stale (Unmet test prerequisite: the workspace build is stale) for 26 files. After bun 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-env inherits the agent host's PI_SESSION_CWD (8/8 pass with it unset); 3064-owned-tui-socket fails identically on base production (6a52e5432c) under the long mandated TMPDIR. Logs: s02b-coding-agent-full.log, s02b-coding-agent-rerun.log

Production size

S02b's own production commits (980cf6cecf, 9c85b55e0a): +356 / −11 lines. That breaks down as json-event.ts 120, structured-session-records.ts 98, structured-session-lane.ts 65, session-structured-lanes.ts 23, agent-session.ts 19, session-binding.ts 11, connection-handler.ts 10, session-record-stream.ts 4, session-event-writer.ts 2, internal-events.ts 2, agent/src/index.ts 1 and ai/.../assistant-capture.ts 1 (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 4f42329ea3

Final local head: 4ded5cc6364465a30dcc0e01efc946104cbbc224 (fix(rpc): fail closed on nonterminal capture (part of #4272)), author YeonGyu-Kim <code.yeon.gyu@gmail.com>. Commit exit 0; worktree clean; not pushed.

STEP 0 merge: 4a1c4f54cbc7926894318391d312ac5ed7a5fb87, author YeonGyu-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. Requiring capture.message at 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.

Finish gate Exit Observed result / local log
nice -n 10 bun run build 0 s02b-finish-build.log
Focused S02b projector / bridge suites 0 6 files, 15 tests passed; s02b-finish-green.log
bun run --cwd packages/ai test test/tool-call-middleware/capture- 0 6 files, 54 tests passed; s02b-finish-ai-capture.log
node_modules/.bin/tsc --noEmit 0 s02b-finish-tsc.log
Explicit git add, then node scripts/lint-ratchet.mjs 0 Every count matches the baseline; s02b-finish-ratchet.log
node_modules/.bin/oxlint --deny-warnings on the complete S02b diff 0 19 supplied TypeScript paths; s02b-finish-oxlint.log
node_modules/.bin/oxfmt --check on the complete S02b diff 0 18 matched files (19 supplied paths); s02b-finish-oxfmt.log
Final nice -n 10 bun run check 0 s02b-finish-check.log
git diff --cached --check 0 No whitespace errors

The 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: AssistantCapture is an internal typed carrier, so a terminal capture is interpretable here when it carries its typed terminal message; there is no new public schema validator. RULING-18 invalidation is communicated through invalidatedBlocks because 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 to 6c6135383dc54f1d6df9544e64f8e2b10d8e02db; 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 @internal annotations 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.ts tests 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.

@code-yeongyu code-yeongyu added the no-changelog No changelog required label Oct 26, 2026
@code-yeongyu
code-yeongyu marked this pull request as ready for review October 26, 2026 06:50
@code-yeongyu code-yeongyu added review-queued Queued for batched Opus review and removed review-queued Queued for batched Opus review labels Oct 26, 2026
Base automatically changed from feat/4272-s02a3-recovery-remap to main October 26, 2026 09:27
code-yeongyu and others added 3 commits October 26, 2026 18:55
…ion instead of exporting it (part of #4272)

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
@code-yeongyu
code-yeongyu merged commit a83f65a into main Oct 26, 2026
29 checks passed
@code-yeongyu
code-yeongyu deleted the feat/4272-s02b-projector-bridge branch October 26, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog No changelog required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant