Merge origin/master into codex/truncated-design

This commit is contained in:
Dudu-0223
2026-07-17 18:21:54 +08:00
1057 changed files with 42961 additions and 18526 deletions

View File

@@ -17,3 +17,12 @@ Files land at `<root>/session-<hash>/​<random>-<safeName>`:
| `root` | private 0700 temp dir | Root directory for spill files. Set to keep them under a known location. |
`saveText` rejects on a real storage failure (permissions, ENOSPC); the spill policy treats a rejection as best-effort and keeps the inline result. See the seam README for the vocabulary and the [tool output spill RFC](../../../docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md) for the design.
## Model Experience
Indirectly, through spill consumers that render the local path and `read`/`grep` retrieval guidance.
## Known Limitations and Deferred Work
- **Local spill files persist until external cleanup** — the backend has no session-lifecycle deletion or age-based retention policy, because persisted, resumed, and forked sessions may still reference a path.
- **Locators require a co-located filesystem consumer** — a remote or virtual deployment needs another `SpillStore` backend whose locator and retrieval hint are meaningful there.

View File

@@ -31,3 +31,16 @@ This plugin registers **no service** and owns no storage or preview mechanics: p
## Scope
The policy sees only the FINAL formatted tool result — not a tool's internal resource. If a provider already truncated (e.g. `web-fetch-local.maxBodyChars`), the spill artifact holds the full formatted result the tool returned, not the full original source. Provider/resource caps stay mandatory and separate. Tool-owned early spill (bash streams, subagent rollouts) is future work — see the [tool output spill RFC](../../../docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md).
## Model Experience
### Oversized plain-text result
**What the model sees**: Results at or below `maxInlineBytes`, `read` results, blocked decisions, and results containing non-text blocks are unchanged. An oversized plain-text result becomes a bounded head/tail preview followed by `(Omitted <bytes> bytes. Full formatted result stored at: <locator>. <retrievalHint>)`; storage or ownership failures leave the original result visible.
**Token effect**: A successful replacement is at most `maxInlineBytes` UTF-8 bytes and remains in history until compaction; the full spill text is not resent to the model.
## Known Limitations and Deferred Work
- **Only final plain-text results are spillable** — mixed-content results, blocked feedback, and `read` pass through; provider truncation or tool-owned retention that happened earlier cannot be recovered here.
- **A notice that cannot fit disables replacement for that call** — a tiny cap or long locator leaves the oversized original inline after the backend has already saved an unreferenced spill.

View File

@@ -169,6 +169,6 @@ export function apply(ctx: Context, config: Config): void {
return decision
}
const replaced: ContentBlock[] = [{ type: 'text', text: replacedText }]
return { kind: 'accept', content: replaced, ...decision.additionalContext ? { additionalContext: decision.additionalContext } : {} }
return { kind: 'accept', content: replaced, ...decision.additionalContexts ? { additionalContexts: decision.additionalContexts } : {} }
})
}

View File

@@ -228,15 +228,15 @@ describe('composition', () => {
expect(textOf(result.content)).toContain('Full formatted result stored at')
})
it('preserves a downstream accept decision additionalContext when spilling', async () => {
it('preserves downstream accept-decision contexts when spilling', async () => {
const { ctx } = await setup({ maxInlineBytes: 200 })
const context = { content: [{ type: 'text' as const, text: 'note' }], source: { kind: 'plugin' as const, plugin: 'test' } }
ctx.on('tools/post-execute', async (_e, _r, _next) =>
({ kind: 'accept', additionalContext: context }))
({ kind: 'accept', additionalContexts: [context] }))
ctx.tools.register(textTool('big', 'x'.repeat(1000)))
const result = await ctx.tools.execute(exec('big'))
expect(textOf(result.content)).toContain('Full formatted result stored at')
expect(result.additionalContext).toEqual(context)
expect(result.additionalContexts).toEqual([context])
})
})

View File

@@ -25,3 +25,12 @@ Storage is grouped by the request's `owner` session as a save-time namespace; th
`SaveTextSpill` (owner, source, suggestedName, content) is the request; `SpillRef` (locator, bytes, retrievalHint) is the result. `SpillLocator` is [branded](../../util/brand) and rendered to the model as an opaque string — a local path for `dsh-spill-local`, but a future backend may return a URI, key, or command token without changing policy/tool consumers. `SpillOwner.sessionId` is the save-time storage namespace: forked sessions inherit existing locators from the seeded log without copying or re-owning them, and new spills after the fork use the child session id. `SpillSource` (toolName, callId, label) is descriptive provenance for backend naming and inspection, not access control. See `src/types.ts` for the full contracts.
See the [tool output spill RFC](../../../docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md) for the design rationale, including why creation belongs to the runtime spill seam rather than the model-facing `write` tool.
## Model Experience
Indirectly, through spill consumers that render a backend locator and retrieval guidance.
## Known Limitations and Deferred Work
- **The seam has no retrieval or deletion API** — consumers can only render the backend's locator and guidance; lifecycle and access semantics remain backend-specific.
- **Storage is not access control** — `SpillOwner` namespaces writes but does not authorize reads of a locator; each backend and retrieval consumer must enforce its own boundary.