Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
675546a
fix(guard): a withheld tool result says where its original is (#277)
Oct 10, 2026
085a5b9
fix(guard): each withheld original gets its own file; a branch change…
Oct 10, 2026
9626707
test(guard): pin writeBinary's tree entry before its first yield, whi…
Oct 10, 2026
ef11718
fix(guard): a withheld original's write names its branch, checked whe…
Oct 10, 2026
47c1483
fix(guard): the annotation's waits are bounded and cover every sent c…
Oct 11, 2026
48f8c71
fix(guard): the stub says "its result", as stored; a thrown or absent…
Oct 11, 2026
353d66f
chore(guard): the code and tests say what they do, without the house'…
Oct 11, 2026
ee7338d
fix(guard): a withheld batch stays held until its stub lands; compile…
Oct 12, 2026
1b1ba56
fix(guard): writes that stalled together each hold the workspace; an …
Oct 12, 2026
625a377
fix(guard): keep a guard for retries exactly while it has work; name …
Oct 12, 2026
12c52b4
docs(guard): a withheld batch's stub waits for the host's report on e…
Oct 12, 2026
52fb88a
Merge #281 (12c52b4) into #248 (4fa06c5): writeBinary's branch check …
Oct 12, 2026
5cdd08b
test(guard): a write for a branch that waits for its mount while the …
Oct 10, 2026
e711ecf
fix(guard): a withheld materialize, sync or delete keeps its file lis…
Oct 10, 2026
da916dc
refactor(types): keepWhenWithheld goes last in ToolResult, clear of #…
Oct 10, 2026
032dd39
fix(guard): the stub labels its kept fields 'Kept from its result' (#…
Oct 10, 2026
be442c5
fix(guard): a failed result keeps nothing; kept fields say when they'…
Oct 11, 2026
1f2c9cc
test(guard): a test of #281's passes storeResults' two required argum…
Oct 12, 2026
a5ace24
docs(guard): the guide says what a withheld materialize, sync or dele…
Oct 12, 2026
8aff46c
Merge main (dbe701b5) into fix/guard-withheld-names-original
Oct 13, 2026
137c751
Merge #281's restack onto main (8aff46c6) into fix/guard-withheld-kee…
Oct 13, 2026
5eb7d19
fix(guard): bound withheld batches and retain request ownership
Oct 13, 2026
a2824e7
fix(guard): take request ownership out of this change; a restart is a…
Oct 13, 2026
ba5520b
fix(guard): say that stop() waits for a stalled write's mount turn; a…
Oct 13, 2026
1742f90
fix(guard): a withheld spill with no finite deadline takes its own
Oct 13, 2026
229f8ed
fix(guard): a batch that used its deadline is logged once, by the gua…
Oct 13, 2026
8bb5b9e
Merge #281's final round (229f8edb) into fix/guard-withheld-keeps-fil…
Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions changelog.d/guard-withheld-keeps-file-lists.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- A result of the workspace's `materialize`, `sync` or `delete` that the
tool-result guard withholds now keeps what it acted on in its stub, after
where the original is ("Kept from its result: {…}"): what `materialize`
removed from disk and wrote to it, any root `sync` accepted and what it took
from disk, gave up or holds as a conflict, per mount, and the path `delete`
removed (a `delete` that failed keeps nothing, as any failed result). The
result as stored still goes where every withheld original goes. Past the
inline cap the kept fields are cut in that order, so a later list can be
cut away entirely, and the stub says so. While the store is away from the
batch's branch, the stub stays the bare notice, without the kept fields, as
every withheld stub does then. A tool marks which fields of its result's
data are kept with the new `ToolResult.keepWhenWithheld`; only these three
mark any. Before this, a withheld receipt hid even which files a write had
touched, such as the clobbered `LOG.md` in agent-framework #277.
60 changes: 60 additions & 0 deletions changelog.d/guard-withheld-names-original.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
- A tool result the guard withholds now says where its original is. The
guard's notice ("Tool result withheld by the guard. The tool has already
executed.") is followed by the original's place: a workspace file under the
first writable mount's `tool-results/` (named
`<date>-withheld-<batch>-<index>-<toolUseId>`, one per result, with any
character outside `A-Za-z0-9._-` replaced and the name cut to 80
characters, so a long tool use id is cut), or, with no writable workspace
or a failed write, the guard's audit record (`framework/tool-result-guard`),
for an operator. The place holds the result as stored, which is its text:
for a result over the inline cap, its capped text, whose own note names the
file with the whole output. A failed write's own error stays in the audit
record and the logs; the audit record also says when no file was written or
a write threw. When saving the audit record itself
had failed as the result arrived, the stub says so rather than that the
original is kept there. This holds for every withheld settlement: a refusal,
an aborted stream, a batch that was never submitted, and one whose audit sync
failed. The notice still names no refusal and no category. Before this, a
withheld result left only the notice, so the agent couldn't tell what the
tool had done (agent-framework #277). The file is an ordinary workspace
file: the agent can read it back deliberately, and so can whoever else reads
the mount, such as another agent sharing it or a synced copy.
- How the stub is written. Where each original went is queued first, as an
audit record of its own, and the stub's edit is then made at once. If it
fails, it is retried before the next activation, compile of a request that
is sent, maintenance pass or stop, including for an ephemeral run or a
closed fork that has left the agent map, and the batch stays held out of
summaries until it lands. Until then, or if the store keeps failing or the
process stops first, the stub is the bare notice, and the `annotated` audit
record describes the spill's outcome when the annotation finishes, with `unfinished` for a write still running. That record is not revised when the write later settles. A file whose copy on disk fails
is still in the workspace's tree, where its tools read it, so the stub names
it; for this, `WorkspaceModule.writeBinary`'s result for that case carries
`data: { inChronicle: true }`, including when a post-commit operation throws. Inline truncation notes also name that readable tree file, and the disk-copy failure is logged and traced. Each batch has one 30-second deadline for all its withheld writes. A write still under way then is named as unfinished, with the audit record as the fallback. The guard stops waiting for it there; the write goes on, and may finish or fail later. `stop()` still waits for the workspace's own turns, which such a write holds, bounded only by a teardown timeout passed to `stop()` (indefinitely without one, as since #248). Until it
settles, no other withheld original is written, for this batch or a later
one of any agent: their stubs name the audit record. Writes can start before the first deadline fires; those batches each use their own deadline. A write that never settles keeps new withheld spills disabled for the process lifetime. A deadline that fires names its file on stderr; a stall's skips are logged once, naming the first file, and counted when it clears; an exhausted batch's skips are logged once per batch. The inline cap's spills are outside this deadline. Requests sent by the framework and the direct Agent API are compiled only once each annotation started so far has finished,
including one that began while the compile waited or ran, and the refusal
retry; a conversation fork seeded from its template copies only stubs whose
annotation has finished. A request preview doesn't wait, so rollback,
undo and redo never wait on a workspace write, and a preview can show a stub
before its place is added, as can public `compileContext()`. Stop lets running annotations finish while the
workspace is up; one that starts after that writes no file and is saved
before the store closes. A crash before a stub is written leaves the notice
alone, and the staged audit record still holds the original. If the active
branch changes while the originals are being written, no more are written:
a write that has not set its tree entry is refused where it commits, and it and the rest are recorded as skipped for the branch change. A write whose tree entry was already set remains on that branch. While the store is away the
stub stays as it was; if the branch is back by the edit, those results'
stubs name the audit record. So no original's tree entry lands on another
branch. For this, `WorkspaceModule.writeBinary` takes an optional `branch`:
the file's tree entry lands on that branch or isn't written, and a refused
write's result carries `data: { branchLeft: true }` (it isn't traced as a
failed spill). A mount has one directory on disk for every branch, so with
`autoMaterialize` a file's copy there, made after its tree entry, is there
whichever branch is active, like any other write's. The audit records `annotated`
(where each original went) and `stub_updated` (the stub's edit) carry the
batch's `sourceBranch`. The direct Agent API's refusal retry is now
compiled afresh, as the framework's is, so its longer stubs fit the budget.

- With `maintenanceIntervalMs: 0`, periodic retries of retained guards are disabled; stop still retries them. A failing audit record blocks later queued edits and their compression holds until its write succeeds. Shutdown reports an error if that retry still fails.
- A framework-driven turn that compiled across stop is discarded before driving its stream, with a line saying why, even if the framework has restarted. A `start()` after a finished `stop()` starts a new lifecycle, with the spill gate open again; a `start()` made while a `stop()` is still draining isn't a supported order. Direct Agent calls remain governed by their caller's abort signal; a refusal retry checks that signal again after waiting for annotations. The direct API reads its unguarded tool results once, after the first compile, and sends the same ones with the refusal retry.
- Concurrent starts on one Agent, a direct inference call beside a framework-driven turn, are not made safe by this change: an Agent compiles while its status still reads idle, as before, and the annotation wait lengthens that window by up to the batch's deadline. `abortInference()` returns false during a direct call's initial assembly.
- `save_recent_image` reports a tree-only write as saved, including its path, size, MIME type, `inChronicle` and `diskCopyError` (the failure after the tree entry, whichever it was). A later failure in a multi-image save, returned or thrown, includes that file among the already-saved paths. An RFC-005 reference fetched to such a file names it.
Loading
Loading