Merge master into compact-tool-result-prune

This commit is contained in:
Tianyi Cui
2026-07-20 16:41:59 +08:00
807 changed files with 15501 additions and 3177 deletions

View File

@@ -39,13 +39,13 @@ Agent status (per agent):
Model requests (on `llm/stream`):
- **a loop-built request is exactly what the log reconstructs** — a frozen request with a live `sessionId` (the loop-built marker; hand-built one-shots like compaction's summarize are unfrozen and skipped) must carry frozen `messages` deep-equal to the derivation over the log prefix strictly before the in-flight step's `step/start` (rebuilt through a FRESH `Session`, so the live cache cannot vouch for itself — and boundary-correct: content logged after `step/start` legitimately belongs to the next request), and every non-content field must equal the latest logged `request/header` (see [the reconstructability RFC](../../../docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)). Registered with `prepend: true` so a short-circuiting `llm/stream` listener (the replay adapter) cannot silence it; prepend orders it against append-registered listeners only — correctness rests on the seq-bounded rebuild, never listener timing.
- **a loop-built request is exactly what the log reconstructs** — a frozen request with a live `sessionId` (the loop-built marker; hand-built one-shots like compaction's summarize are unfrozen and skipped) must carry frozen `messages` deep-equal to the derivation over the log prefix strictly before the in-flight step's `step/start` (rebuilt through a FRESH `Session`, so the live cache cannot vouch for itself — and boundary-correct: content logged after `step/start` legitimately belongs to the next request), and every non-content field must equal the latest logged `request/header` (see [the reconstructability Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)). Registered with `prepend: true` so a short-circuiting `llm/stream` listener (the replay adapter) cannot silence it; prepend orders it against append-registered listeners only — correctness rests on the seq-bounded rebuild, never listener timing.
On any violation it throws `InvariantError` (`code: 'INVARIANT'`).
## Why runtime assertions remain useful
Session enforces the per-record storage boundary at runtime, where a cast cannot bypass it. Pervasive `DeepReadonly<SessionEvent>` types would add noise across consumers without expressing relationships such as turn/step nesting, subject-correct scoped dispatch, or equality between a request and its log reconstruction. This plugin checks those relationships wherever it is mounted while `dsh-session` keeps history immutable in every composition. See [source-owned session immutability and dev-mode invariants](../../../docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md).
Session enforces the per-record storage boundary at runtime, where a cast cannot bypass it. Pervasive `DeepReadonly<SessionEvent>` types would add noise across consumers without expressing relationships such as turn/step nesting, subject-correct scoped dispatch, or equality between a request and its log reconstruction. This plugin checks those relationships wherever it is mounted while `dsh-session` keeps history immutable in every composition. See [source-owned session immutability and dev-mode invariants](../../../.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md).
## Seeded sessions
@@ -55,6 +55,10 @@ A seeded or forked session arrives with events already in its log because constr
None, as this observer only validates events and frozen requests and never rewrites prompts, schemas, messages, or streams.
#### KV Cache effect
None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **The request-reconstructability assertion covers loop-built requests only** — hand-built one-shots (e.g. compaction's summarize call) carry no live `sessionId` marker and are skipped.

View File

@@ -86,7 +86,7 @@ function validateEvent(trace: SessionTrace, event: SessionEvent): SessionTraceTr
// Boundary/step-scoped events have explicit cases; every OTHER event type —
// including plugin-added (merge-extensible) SessionEventMap keys — is caught
// by the `default` and must be turn-enclosed (the turn-enclosure RFC). No assertNever: an
// by the `default` and must be turn-enclosed (the turn-enclosure Agent Note). No assertNever: an
// unknown variant is valid, not a compile error.
switch (event.type) {
case 'turn/start': {
@@ -172,7 +172,7 @@ function validateEvent(trace: SessionTrace, event: SessionEvent): SessionTraceTr
pendingCalls = { kind: 'delete', callId: event.data.callId }
break
}
// Turn-enclosure (the turn-enclosure RFC): EVERY session event not handled by a boundary
// Turn-enclosure (the turn-enclosure Agent Note): EVERY session event not handled by a boundary
// case above must sit inside an open turn. The durable session log uses the
// turn as its commit/replay boundary (the JSONL backend treats anything
// after the last turn/end as a crash tail), so a bare event between turns is
@@ -340,7 +340,7 @@ export function apply(ctx: Context): void {
}
}, { global: true })
// Request-reconstruction cross-check (the reconstructability RFC): a
// Request-reconstruction cross-check (the reconstructability Agent Note): a
// loop-built request — frozen envelope + live sessionId is the marker; a
// hand-built one-shot (compaction summarize) is unfrozen and skipped — must
// be EXACTLY what the session log reconstructs:

View File

@@ -149,7 +149,7 @@ describe('session-log invariants', () => {
it('rejects a message event appended outside any open turn (turn-enclosure)', async () => {
const { ctx } = await setup()
const session = ctx.sessions.create()
// No turn open: every message-bearing event must be turn-enclosed (the turn-enclosure RFC).
// No turn open: every message-bearing event must be turn-enclosed (the turn-enclosure Agent Note).
expect(() => session.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, { surfaceOp: 'append' }))
.toThrow(/outside any open turn/)
expect(() => session.append('context/message', { content: [{ type: 'text', text: 'ctx' }], source: { kind: 'user' } }, { surfaceOp: 'append' }))
@@ -160,7 +160,7 @@ describe('session-log invariants', () => {
const { ctx } = await setup()
const session = ctx.sessions.create()
// steering/message is turn-scoped: outside a turn it would land past the
// commit boundary and be dropped on resume (the turn-enclosure RFC).
// commit boundary and be dropped on resume (the turn-enclosure Agent Note).
expect(() => session.append('steering/message', { turn: 1, content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }, { surfaceOp: 'append' }))
.toThrow(/outside any open turn/)
// A PLUGIN-added (merge-extensible) event type is caught by the default too.