Merge branch 'codex/simp-agent-entry-state' into codex/simp-unify-agent-session-id

# Conflicts:
#	docs/cordis-catalog/events.md
#	docs/event-producer-consumer.md
#	packages/core/agent-loop/src/agent.ts
#	packages/core/agent-loop/tests/contract-regressions.spec.ts
#	packages/core/agent-loop/tests/loop.spec.ts
#	website/zh-CN/api/harness/events.md
This commit is contained in:
Tianyi Cui
2026-07-18 14:50:55 +08:00
21 changed files with 434 additions and 137 deletions

View File

@@ -46,7 +46,7 @@ The core `context/message` envelope is disabled for these messages because the p
## State And Refresh
Model-visible text contains no hidden state markers. Each dynamic context event instead carries JSON metadata with a versioned list of `{ action, scope, path, previousPath?, digest? }` changes. On every relevant tool touch, the plugin reconstructs loaded state from its visible session events and overlays a short in-memory pending window for context present on the immutable top-level `tools/result` but not yet appended by the loop. A matching durable `context/message` confirms the pending transition. If the owning `step/end` arrives first because a later tool aborted the step and the loop discarded its context buffer, the plugin clears the pending transition and its version fast path so the next successful touch can load it again. Nested Code Mode results stage pending changes under the outer execution token for same-run duplicate suppression; the outer result rolls that state back and recommits only contexts that survived outer policy.
Model-visible text contains no hidden state markers. Each dynamic context event instead carries JSON metadata with a versioned list of `{ action, scope, path, previousPath?, digest? }` changes. On every relevant tool touch, the plugin reconstructs loaded state from its visible session events and overlays a short in-memory pending window for context present on the immutable top-level `tools/result` but not yet appended by the loop. A matching durable `context/message` confirms the pending transition. If the owning `step/end` arrives before a matching context reaches the log, the plugin clears the pending transition and its version fast path so the next successful touch can load it again. Nested Code Mode results stage pending changes under the outer execution token for same-run duplicate suppression; the outer result rolls that state back and recommits only contexts that survived outer policy.
An unchanged path and SHA-1 content digest is not injected again. A per-session, per-scope metadata cache stores only `{ path, version, digest }`: when the provider's opaque `FsVersion` and the effective visible state both match, reconciliation skips the content read; a changed version triggers a bounded read and SHA-1 confirmation before any model-visible update. Resume works because SHA-1 state is persisted in the session log, while an empty in-memory version cache merely causes one confirming read. Compaction re-arms a scope after its context event leaves the visible surface even when the cached version is unchanged. A removal is a tombstone, so a later candidate reappearance is loaded again. Only model-visible changes actually rendered within the byte budget enter metadata, pending state, and the version cache; an omitted change remains eligible for a later touch, while a same-digest version refresh updates metadata only.

View File

@@ -255,8 +255,8 @@ function invalidateInstructionVersions(
/**
* Settle provisional tool-result state against durable session events.
* A matching context event confirms the transition. If its owning step closes
* first, the loop discarded its context buffer, so both duplicate suppression
* and the metadata fast path must be re-armed for the next successful touch.
* first, both duplicate suppression and the metadata fast path are re-armed for
* the next successful touch.
* @param session - session whose append-only log emitted `event`.
* @param event - newly committed session event.
* @param pendingBySession - provisional transitions awaiting log confirmation.

View File

@@ -1562,7 +1562,7 @@ describe('workspace context request injection', () => {
})
describe('dynamic nested workspace context injection', () => {
it('re-arms a buffered instruction change when a later tool aborts the step before context append', async () => {
it('commits a buffered instruction change before a later tool abort closes the step', async () => {
const root = await tempRepo()
const home = await tempRepo()
const ctx = new Context()
@@ -1604,12 +1604,14 @@ describe('dynamic nested workspace context injection', () => {
agent.send([{ type: 'text', text: 'read and abort' }])
await agent.whenIdle()
expect(agent.session.events.filter(event => event.type === 'context/message')).toHaveLength(0)
expect(agent.session.events.filter(event => event.type === 'context/message')).toHaveLength(1)
agent.send([{ type: 'text', text: 'retry the read' }])
await agent.whenIdle()
const contexts = agent.session.events.filter(event => event.type === 'context/message')
// The aborted batch drained its accepted context before step close, so the
// retry sees durable history without producing a duplicate instruction.
expect(contexts).toHaveLength(1)
expect(adapter.requests).toHaveLength(3)
expect(adapter.requests[2]?.messages.map(blocks => blocksText(blocks.content)).join('\n'))