Merge worktree-hooks-b-bash-seam into worktree-hooks-c-interception
Bring the interception-seams branch onto current master (via A→B). The substantive reconciliation is master's compaction `agent/pre-step` serial seam meeting C's interception seams: - types.ts: keep BOTH master's `agent/pre-step` AND C's new interception events (`agent/prompt-submit`, `agent/session-start`, `agent/turn-continuation`→ `ContinuationDecision`); drop the turn-mirror declarations (removed on A). - loop.ts: the merged per-turn order is `turn/start` → per queued msg `agent/prompt-submit` (rewrite/inject/block) → (fully-blocked ⇒ zero-step `rejected`) → per step: drain steering → assemble system prompt → `agent/pre-step` (compaction, OUTSIDE the step) → `step/start` → single `deriveMessages()` → model → tools/pre-execute·dispatch·post-execute. No turn-mirror emits; `closeTurn()` is the A-simplified single-call form. - Docs (architecture, core.md, agent/agent-loop READMEs, catalog) reconciled to show C's interception seams alongside `agent/pre-step`, no turn/step mirrors. - rfc/README: dropped the stale `proposed/` compaction row (master moved that RFC to implemented/); kept C's new `pre-tool-input-rewrite` proposed row. - interception.spec.ts: migrated its two `agent/turn-end` reason collectors to the `turn/end` session event, and ADDED a cross-test proving a `prompt-submit` rewrite + additionalContext is VISIBLE to an `agent/pre-step` listener on the same turn — pinning the merged seam ordering (compaction sees the post-prompt-submit surface, not stale history).
This commit is contained in:
@@ -162,9 +162,6 @@ function checkEvent(trace: SessionTrace, event: SessionEvent): void {
|
||||
trace.surface.push(event.seq)
|
||||
} else {
|
||||
const { start, end } = se.surfaceOp
|
||||
if (start > end) {
|
||||
throw new InvariantError(`surface replace: start ${start} must be <= end ${end}`)
|
||||
}
|
||||
const startIdx = trace.surface.indexOf(start)
|
||||
if (startIdx === -1) {
|
||||
throw new InvariantError(`surface replace: start seq ${start} is not on the surface`)
|
||||
|
||||
@@ -530,16 +530,17 @@ describe('surface invariants', () => {
|
||||
}).toThrow(/unknown seq 2/)
|
||||
})
|
||||
|
||||
it('rejects replace op with start > end', async () => {
|
||||
it('rejects a replace whose start is positioned after its end on the surface', async () => {
|
||||
const { ctx } = await setup()
|
||||
const session = ctx.sessions.create()
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
session.append('user/message', { content: [{ type: 'text', text: 'a' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) // seq 2
|
||||
// start > end is invalid (reversed order).
|
||||
session.append('user/message', { content: [{ type: 'text', text: 'b' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) // seq 3
|
||||
// Reversed range: start seq 3 is at a later surface position than end seq 2.
|
||||
expect(() => {
|
||||
session.append('assistant/message', { turn: 1, step: 1, content: [] }, { surfaceOp: { op: 'replace', start: 2, end: 1 }, sourceEventSeqs: [2] })
|
||||
}).toThrow(/must be <= end/)
|
||||
session.append('assistant/message', { turn: 1, step: 1, content: [] }, { surfaceOp: { op: 'replace', start: 3, end: 2 }, sourceEventSeqs: [2, 3] })
|
||||
}).toThrow(/is after end seq 2 .* on the surface/)
|
||||
})
|
||||
|
||||
it('rejects a replace whose sourceEventSeqs omits a shadowed surface node', async () => {
|
||||
@@ -608,6 +609,23 @@ describe('surface invariants', () => {
|
||||
}).toThrow(/is after end seq 4 .* on the surface/)
|
||||
})
|
||||
|
||||
it('accepts a replace whose start seq exceeds its end seq when the surface position order is valid', async () => {
|
||||
const { ctx } = await setup()
|
||||
const session = ctx.sessions.create()
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
session.append('user/message', { content: [{ type: 'text', text: 'a' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) // seq 2
|
||||
session.append('user/message', { content: [{ type: 'text', text: 'b' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) // seq 3
|
||||
// Replace node 2 (position 0) with seq 4 — surface becomes [4, 3], so the
|
||||
// head seq (4) is numerically GREATER than the tail seq (3): the surface is
|
||||
// not seq-ordered. A replace spanning start=4 (pos 0) … end=3 (pos 1) is
|
||||
// valid positionally and must be accepted even though start seq > end seq.
|
||||
session.append('assistant/message', { turn: 1, step: 1, content: [{ type: 'text', text: 's' }] }, { surfaceOp: { op: 'replace', start: 2, end: 2 }, sourceEventSeqs: [2] }) // seq 4
|
||||
expect(() => {
|
||||
session.append('assistant/message', { turn: 1, step: 1, content: [] }, { surfaceOp: { op: 'replace', start: 4, end: 3 }, sourceEventSeqs: [4, 3] }) // seq 5
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects a replace that omits sourceEventSeqs entirely', async () => {
|
||||
const { ctx } = await setup()
|
||||
const session = ctx.sessions.create()
|
||||
|
||||
Reference in New Issue
Block a user