mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
8a6a4f20d61bcf2ef3a7b4801fa497842974db73
137
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8a6a4f20d6 | feat: modernize studio interface and session recovery | ||
|
|
fea552a914 |
feat(events): which model wrote a frame is a fact the frame carries
A two-model turn put two producers on one stream and said nothing about which was which. The kernel knew — the planner's sink has labelled its events all along — but eventwire had no field for it, so the label died at the transport. Everything downstream was left with the same guess: read the last phase marker before the frame. That is an adjacency, not a fact, and the record does not even keep phase frames, so a rebuilt transcript could not make the guess at all. Producers now name themselves. An agent is built for a role and labels the frames it settles; a forwarder that re-parents another agent's work relabels on the way through, because in the parent's stream a sub-agent's call is the sub-agent's whatever role the child plays in its own. The field crosses eventwire, so it is on the frames the record keeps, and a card rebuilt from those frames carries it. Streaming deltas stay unlabelled, and finding out why was the useful part of this cut. Coalescing merges only deltas that carry nothing but text — it decides that with a zero-probe, so any new field on a delta silently turns the whole optimisation off, which is exactly what labelling every frame did: forty provider chunks reached the frontend as forty events. A delta is a fragment of a message whose settled frame names its producer, so naming the fragment says nothing new and costs the stream. The boot effect test is what caught it, at the boundary it was written for. That leaves one asymmetry already in the tree: the planner's own text deltas have carried a source since before this change, so they have never coalesced. Not touched here — it is the coordinator's direct emissions, not this seam, and the card takes its producer from the settled frame either way. The destructive control runs both ends. Strip the field from the transport and a real two-model turn fails on the frames themselves; strip it from the frames and the cards go unattributed rather than falling back to the nearest phase marker — which would look right in the fixture and be wrong the moment a frame arrives out of the order the markers suggest. Tool cards do not carry it yet. The fact is on the frame they fold from, so that is a projection when episode membership needs it, not another fact to establish. |
||
|
|
1c7cb8d680 |
feat(evidence): what a report may close is the host's grant, not its own label
review_report's `kind` carried two facts at once: which review this was, and which obligation it closed. The first is the worker's to state — it is the payload it was asked for. The second never was, and reading it out of the same field made the label the capability: a run contracted for one kind could submit the other, and every gate downstream accepted it, because by the time a gate looked there was nothing left but the word the model had written. So the permission is issued before the run and stamped from outside it. ReviewAuthority says which obligations a report from this execution may close; the host binds it to the report tool at mount time, the receipt records it off the mounted instrument, and ReceiptProves asks that question instead of reading the payload. The kind stays, as the delivery fact it always was: a submission naming a kind other than the one assigned is refused at the tool, before a receipt exists, rather than downstream where a gate has to unpick it. Authority is separate from Delivery all the way up — on the skill, on the profile, in the spec — because a worker can owe a report it may prove nothing with, and one did. Both come from the worker's own declaration, so no entry point widens either by asking differently, and the built-in security reviewer declares only the security obligation: whether a security review also answers for an ordinary one is undecided, and an undecided grant is not asserted. Two consequences are deliberate. The report tool leaves subRegistryFor and is mounted where the execution id exists, since a permission issued to a run that does not exist yet is not one — there is now no reachable state in which the instrument is present unbound. And read_only_skill, which runs outside the delegation lifecycle and has no execution to name, gets a grant with no authority: the verdict is still owed, a block from it still stops delivery, but evidence nothing can be joined back to does not close an obligation. The gate says which of these happened. ReviewProofGap separates "no report", "submitted by a run the host cannot name" and "submitted by a run granted no such authority", and the refusal names the one that occurred. Without that the model reads a demand for a review it believes it just delivered, and the rule it invents to explain the contradiction outlives the turn. Reporting is accepted once rather than called once: a refused attempt records nothing, so a run that got the schema or the kind wrong can still correct itself, while one that already has an accepted verdict cannot file a second. |
||
|
|
d19db0eaa8 |
feat(skill): a worker declares what it owes when it finishes
10a found the typed-review requirement had no owner: neither the skill nor the profile could express it, and what decided it was a switch on the worker's name consulted by one compiler. That made two independent errors at once — a reviewer renamed lost its obligation, and an ordinary worker that borrowed the name gained one — and left task(profile=review) with nothing to inherit. So the fix is a declaration, not parity. DeliveryContract lives on the definition, the built-in reviewers declare their own verdict there, and it projects into the execution spec the same way a tool ceiling does. Both entry points read only that projection. The name switch is deleted rather than moved: putting it in the task compiler too would have made the accident look like a design. task(profile=review) therefore changes behaviour and is meant to. The same reviewer owed different things depending on which button started it, and nothing about the product justified that — only the compilation path did. Moving the rule from wording to structure fixes the false negative and the false positive together; the parity is a consequence. The contract is declared by built-in definitions and deliberately not parsed from user frontmatter. Where the fact lives and who may assert it are different questions, and if a security verdict ever counts as verification evidence, a line of YAML should not be enough to claim it. That one stays open on purpose. The guard is the falsifier inverted. What proved the defect — a renamed reviewer losing its obligation, an impostor gaining one — now proves the fix, through both surfaces, and the built-ins are read from the store the way a session reads them so a definition that dropped its declaration fails loudly. Two files were extracted rather than widening a budget: the contract to its own file, and the frontmatter key vocabulary out of a 1400-line loader. |
||
|
|
9e49590cf3 |
feat(agent): execution identity stops being the parent call's
9a showed that a host-started execution had no name that could outlive the process. The store's ref does not exist while the scheduler is holding work back; the synthetic slash id may never be persisted, because a reader finding it would take it for a call the model made; and the parent call must stay empty, because there is none. Three candidates, none of them able to be the answer, so the answer is a fourth thing. SubagentMeta now carries ExecutionID beside ParentToolCallID. They are different questions — which execution this is, and which provider-visible call it descends from — and they were one field only because every durable execution so far came from a tool call. A model-delegated run still has them equal. A host-started run has the first and not the second: an execution the host mints before the work is opened, a root node with no spawn edge above it, and an empty lineage. That distinction also removed a conflation in the opener. Descending from a call and having somewhere to draw are separate facts, and treating them as one made a run outside any run loop unopenable. It is durable work; it gets an execution. The picture goes wherever a sink exists. Reads are lenient, writes are strict. A record naming its own execution is joined on it; one written before that did is aliased to its parent call only where the journal opened an execution by exactly that id, because the journal is what confirms the older writer used one id for both — a parent id alone proves only what it meant by lineage. A parent naming no execution stays unknown and is joined on by nothing: a single task once stored the call while the journal opened the node, and a prefix rule would have joined those two wrong. Six shapes are pinned as a table. New writes refuse an unnamed execution outright, since that is the one record no later reader can place. Nothing historical is rewritten, and this does not promise it will be. The safe legacy subset resolves losslessly on read; the unsafe one — a host-started record from before this, which never had an execution — cannot be recovered by any rule that is not inventing history. Resolving on read forever costs a map lookup and claims nothing. 9a's four arms rerun exact: what was record-only and LOST-SILENT is now an execution the journal opened, a root node on the graph, and a restart that can place the child. Both oracles still hold in every arm — no persisted lineage names a host event id, and every host-started parent is empty. The frozen matrix gains the host row and joins on the execution rather than the parent, and across 60 arms nothing is violated. |
||
|
|
189d106fb9 |
fix(boot): a subagent skill stops owning an execution loop of its own
8d-2 classified the normal skill runner as a shared delegated execution: it asks the same scheduler, runs a real child, keeps transcripts and supports continuation. What it did not do was leave a trace of any of it — an executing skill was named by no graph, no journal and no store record — and a stop that reached its child was filed as a failure. Both followed from the same thing: it had built a second execution owner beside the one every other delegation uses. So the fix is not to give it a lifecycle. It is to stop it having a loop. A skill now compiles to a ProfileExecSpec and hands it to RunProfileSpec, which already owns admission, the child's own run, and how an execution ended. The runner keeps what only it knows — the body, the workspace facts a child cannot discover for free, the tool ceiling, the whole-workspace claim a writer skill carries, the typed verdict a reviewer owes — and nothing about how an execution is admitted, carried out or ended. Three semantics had to survive the move, and each has a test rather than a promise: the prefix is the body plus the workspace facts, not the body alone; a writer without declared paths still claims the whole workspace so it serializes against fleet and task writers; and a reviewer still owes a typed report, which the spec now carries as a worker property because it follows from which profile this is and never from what one caller asked. That last one is deliberately not derived from the worker's name: doing so would have handed the same contract to task(profile=review), which does not have it today, and a convergence must not change a delivery contract on the way past. The cancellation defect is fixed by deletion. The legacy runner classified every error as failed; RunProfileSpec already had the one classifier that tells a deadline from a cancellation from a failure, so removing the duplicate makes skill-cancel read cancelled where it read failed. Nothing was written to address the symptom. Lineage keeps its existing contract. A host-started run carries a synthetic call id so the child's UI can nest under it, and that id must not be persisted as though the model had made the call — so the spec says the run is top-level and it opens no delegation and is stored under no parent. Whether a slash-invoked skill should instead join its own host-emitted dispatch is a question this cut does not answer; it leaves that path recording nothing, as before. Two gates: a skill must leave the records every delegation leaves — an opening, a slot grant, a settlement, and a child joined to that execution — and a host-started one must be filed under no call, including when a synthetic one exists. Reintroducing the private loop as a mutation turns the first red. The probe's own criteria are corrected where they were written for the world before this. A refused item's negative control is about the store alone: an opening is the orchestration recording that work entered it, which a refused item did, and demanding journal silence would forbid the record that tells a restart it was held back rather than lost. The identity row now checks the join rather than one spelling of it, because a record's parent is the execution's own identity and comparing it to the bare call id fails the chain that makes them joinable. Across 54 arms nothing else moves and nothing is violated. The read-only entry point is untouched: every one of its rows reads exactly as before, and whether an ephemeral run that holds a scheduler slot should be drawn stays open. |
||
|
|
85640c4a90 |
fix(agent): the store admits every execution that really started
8d-1 measured one store answering differently about executions it holds the same evidence for. A fan-out item whose slot the scheduler had granted, whose child had reached the provider, and whose start the journal had recorded was absent from the sub-agent store entirely — live and after a restart — while a lone delegation in the same death, the same store and the same join read running and then interrupted. The store is what list_subagents, read_subagent_result and adopt_ref address, so that item was work nobody could ask the host about. The cause was where the store's record hung, not what the fan-out is. A lone delegation marked it from the lifecycle that opened it; an item whose group did the opening had no lifecycle, so it reached the same runner and none of the same record. The boundary now belongs to the run rather than to whoever opened it: every delegated execution crosses beginExecution once it holds a slot and has a transcript, and there it is recorded, then drawn, and only then may act. Nothing else moves. The group still owns dependency, adoption, skipping and settlement; an item the scheduler refused is owed no record and still gets none, which is the proxy this line of work removed and must not reintroduce. What did have to move is the picture: the slot grant used to draw a fan-out item running, which put the drawing ahead of the record, so both entry points' start hooks now write the journal and nothing more. The item's node travels with the sink it was drawn on, because a fan-out item's own sink forwards tool events and drops graph deltas — the picture is reachable only through the group's. A refused record stops the run at both entry points, and that is now asserted rather than reasoned about: the child must not act, nothing may be drawn running, the slot must come back, and the journal keeps the start it honestly wrote. Two mutations were run against it — the boundary skipping the store for an item whose node someone else drew, and drawing before recording — and each turns the suite red, the first only on the fan-out side. The active-store arm's two cells flip as specified: an executing fan-out item reads running then interrupted where it read no record twice, list_subagents shows it, and the refused item stays absent. Across all 47 arms nothing else changes but the two fan-out arms whose store row now sees the child that was executing, and parallel_tasks converges on the same contract. |
||
|
|
a50ca8075e |
fix(agent): a delegation the scheduler never admitted owes the store nothing
8c asked one question: on each side of the slot grant, who owns the ending. The answer the code gave was wrong on the near side. A backgrounded delegation killed while it was still queued wrote a failed child record — and the store outranks the lifecycle on a rebuild, so a cancellation before anything ran came back as a failure that never happened. The journal said it plainly at the same time: opened, queued on slots, settled, never started. The cause was not vocabulary. A background run has its transcript before it has a slot, so "a run object exists" had come to stand in for "a child executed", and every early exit on that path wrote a terminal for it: a refused admission, a writer registration that failed, a store that would not mark it running. None of those is an execution outcome. They now return their error and leave the store alone, which the journal's opened-queued-settled already accounts for. Four arms hold the boundary from both sides. Backgrounded, a delegation killed while queued now leaves no child record and rebuilds as cancelled; one killed while running keeps its own cancelled terminal and rebuilds exactly. The rows carry a cross-authority check that refuses the combination neither layer may produce — a child record for work the journal proves never started — because a state comparison shows that only as "cancelled came back failed". The two foreground arms do not establish. Stopped through the turn's own cancel, the turn ends — running=false, no cancel pending — while the delegation is still started and never settles: its child keeps waiting on a context the cancel did not reach. That is a separate question from ownership and is left open rather than guessed at; the arms report it as their premise failing, with the reading that produced it. Cancellation at the delegation's own context settles and records correctly, which a unit test now holds. |
||
|
|
9b80052f95 |
feat(agent): a lone delegation is recorded before it is drawn, and drawn as what it is
8a and 8a.1 measured two failures with one cause. A single task reached the same runner a fan-out item does and none of the same records — nothing on disk for a restart to read — and it drew itself running before it had asked the scheduler for anything, so a delegation the ceiling was holding back appeared, to the graph and to the parent, as work in progress. Both came from the same place: states were being inferred from where control flow stood rather than projected from what the scheduler and the child's own ending had proven. The lifecycle is now one thing that decides nothing. It opens after every deterministic refusal has answered — a prompt that would not settle or a runtime that would not resolve is not work orchestration ever held — and writes the opening from the same delta the graph is drawn from, journal first. The node opens pending. Queued and its cause come only from the scheduler's first refusal; running only from the slot grant, and behind it the store's own record, so nothing shows a run executing that the store has never heard of. Two proxies are gone. The foreground no longer announces running before acquiring a slot, and a background run no longer reports queued because a job was registered: that word now means the scheduler refused, and an immediate grant goes straight to running. What tells a caller its work was handed to the background is the tool result, which says so in words. A backgrounded delegation is on the graph like any other. Hiding it was the lexical lifetime of the old opener, not a semantic: run_in_background decides whether the parent waits, not whether the delegation exists. Ownership moves with the handoff, exactly as the progress tracker's already does — the caller settles what it still owns, the job settles what it took. The store's parent is now the execution's own node. That join is what a rebuild reads, and with the journal keyed by node and the store by call, a completed lone delegation rebuilt as skipped: the outcome could not be found. The two spellings are one identity again. read_only_task keeps its promise: ephemeral entry points write no journal, so a research child stays out of the history a restart rebuilds from, while its caller still sees what it delegated. Six mutations were run against the new tests: publishing before recording, opening the node running, marking the store running before the slot, settling a handed-off run at the outer return, telling the parent running before the slot, and recording an ephemeral delegation. Each turns the suite red. The five process-boundary arms now read pending→queued(slots)→before-start and pending→running→during-execution on both entry points, and a completed delegation rebuilds exactly. The boot-wired skill runner prepares its own transcript rather than going through this path, so it inherits none of it yet; that is 8d's question, not a gap this closes quietly. |
||
|
|
01536b0ce0 |
fix(agent): a run the caller stopped is not a run that failed
The run graph tells cancellation from failure. The sub-agent store, which owns what happened to a child, did not: every error path saved a child as failed and SubagentStatus had no cancelled to save. The probe measured the result as graph cancelled=2 completed=1 failed=1 against store completed=1 failed=3, with the journal — which records orchestration, not outcome — unable to help. The distinction was not un-persisted, it was unkept. The store now keeps it. A cancelled run persists everything a failed one does; a transcript is as worth reading when work is cut short as when it goes wrong, and only what it says happened differs. One status covers both ways a run is stopped, because the graph maps a cancelled context and an expired deadline onto one state; which of them it was rides beside it, so the two owners share a vocabulary instead of splitting one. Only the two seams where a child actually ran may read a context error as a terminal outcome. The other four SaveFailed callers — a failed writer registration, a panic, a refused slot, a transcript that would not save — are host failures whose chains can carry a cancellation without the work having been stopped. They keep calling SaveFailed, which takes no error and therefore has nothing to classify: the negative control holds by shape rather than by judgement. Old sidecars are not migrated. A historical status=failed was written by a store that never knew this distinction, and the same error text can come from paths that were not cancellations; reclassifying on it would invent provenance nobody recorded. The probe's judgement is unchanged and flips on its own, VIOLATED to holds. Its journal row is part of that gate: teaching the journal about cancellation would have made the comparison green while breaking the boundary it exists to protect. |
||
|
|
0a1231529c |
fix(graph): a lone sub-agent is a delegation the run graph draws
publishGraph had two callers, fleet and parallel_tasks, so every other delegation — a task, a run_skill, a review capability — published no node at all. A sub-agent ran eight steps in its own context and the graph tab answered "这一轮没有派出并行的子代理", which reads as nothing having been delegated. The graph was a fan-out visualiser; a person opening it is asking what this run handed work to. The hook goes on RunProfileSpec, where task.go already states the invariant every entry point relies on: each one compiles to a spec and runs through it, so a boundary added there cannot be missed by one caller. The two fan-outs mark their items with withDeclaredGraphNode — only they can say it, having chosen the node's id and kind, and without it the shared runner would redraw a fleet worker as a group of one. A background run draws nothing: it settles after a handoff this return never sees. lanesOf needed no change — the group, the parentId and the spawn edge are the shape it already reads. Two extractions keep task.go and RunProfileSpec inside their budgets rather than widening them: subSink/subSinkFor move to nested_sink.go, which is what they are about, and the spec's prompt validation becomes settleSpecPrompts. Both hand budget back. |
||
|
|
27b463f593 |
feat(agent): count how often a child closes the way it was asked to
The typed handoff exists for read-only children now, and nothing insists on it.
So this measures the natural rate first: a nudge built on a compliance number
nobody collected would be a guess with a number on it.
It observes and decides nothing. No prompt byte moves, no notice is emitted, no
parent handoff changes, and the provider surface is untouched.
The denominator is the instruction, not a guess at one. ExpectCompletionReport
is set exactly where the contract is appended, never inferred from a registry
holding the tool — the same distinction this codebase keeps having to relearn:
a count is not its evidence.
Four things are counted per child run, one sample each, from a single defer so
a salvage, a review failure, a provider error and a clean answer are all
counted the same way:
adoption attempts, accepted, malformed. Attempted-and-refused is a schema
problem; never-attempted is a protocol one, and one number
cannot say which.
protocol report round against final round, and tool calls after it. The
contract asks for the report as the closing call, and
"submitted eventually" is different behaviour.
adjudication claimed status beside the host's, and how many claims it
lowered. A child that reports readily and is corrected every
time is not a compliance problem.
exit completed, error, cancelled, no_answer. A provider failure that
produced no report never had the chance to comply and leaves
the denominator.
Split by entrance and depth, because an aggregate hides the entrance that is
failing. Content-free throughout: counts, enums and rounds, never a prompt, a
path or a summary.
Verified by making each fail: stop separating malformed from accepted and the
schema-vs-protocol distinction disappears; classify a provider error as
no_answer and a failed run becomes a compliance failure. The cancellation test
carries a wrapped sentinel and a lookalike string, because the classifier reads
errors.Is and a message that merely says "context canceled" must not pass —
repolint's error-text rule caught the first draft doing exactly that.
Options moved to agent_options.go: it is 172 lines of a god object, and this is
the third field appended to it this round. That extraction carries ten essay
findings forward, declared with -allow-widen; agent.go drops from 1597 to 1424.
runReadOnlySubSession stays in task.go — the spawn-boundary guard is right that
its list must not grow for a file-size reason — and the framing both runners
apply is shared instead.
|
||
|
|
27f6e2d15c |
feat(agent): a read-only sub-agent reports the way a writer does
A writer child ends by calling complete_subtask, and the host adjudicates every citation against what it observed the child actually do. A read-only child ended in prose. Its receipts still reached the parent ledger, so the host knew which files were read and which commands ran — but the semantic half arrived as an unverifiable sentence, and nothing connected the two. With one researcher that is invisible. Across a fleet of eight it is the parent's whole picture. Strict children now get the same contract and the same tool. complete_subtask is already ReadOnly, so it survives the strict construction filter unchanged, and the parent's existing adjudication path needs nothing new: read-only runs through RunSubAgentWithSession like every other child. The order is the point, and it has a name now. readOnlySubRegistry builds the tools, proves the child can investigate something, and only then attaches the way to report. Attached first, complete_subtask would make an empty registry non-empty — a child with no research tools at all would pass the capability check by holding the tool it files findings with, and then report on work it could not do. The contract's wording asked for "the paths you changed", which a child that only looked at things cannot cite. It asks for changed or inspected. No provider surface moves: the tool joins a child's registry, not the parent's. |
||
|
|
4d320f7cd7 |
fix(agent): a change stays in review's scope until a review has covered it
The review gate read one index for two questions — what the change set is, and how fresh a review of it must be — so a later harmless write moved the window past everything before it. A sensitive edit followed by a README write scored Low and dropped the whole structured-review demand. UnreviewedMutationBaseline now answers the first question and the latest mutation still answers the second. It advances per review kind, so an accepted review does not discharge security_review's obligation with it. Alongside, the parts of that gate that promised more than they did: - A blocking verdict is honored at every role setting, and review subagents submit the typed report everywhere. A verdict a caller must act on carries an identity, not a sentence. The nudge runs everywhere too; the hard failure stays in Delivery, since outside it no report is no block either way. - Medium no longer demands a structured review "or" host-proven self-inspection. That was no alternative — the reader there is the author — so the cheap side was always taken and the demand never bound. The real requirement inside it moves to the self-inspection gate and grows teeth: inspect every changed path, fresh per path, not one of them. - warn findings were collected into a slice nothing read. They now reach the user, and they are collected from any review the turn ran rather than only from one the gate asked for. - verificationGapResult.advisory carried no text and reached nobody while two comments called it reported. The waiver it stood for is right and stays; the flag wearing a message's type is gone. The review and security-review bodies stop hardcoding git: fileutil owns one declared store table, environment reads it, and a skill subagent's prompt now carries the workspace fact its parent always had. They also drop the ~12-call self-cap, which was well under the host's real budget, and split the burden of proof — adversarial about what must be read, conservative about what may be reported. A pass is a negative claim and owes what any other does. complete_step: manual evidence no longer stands alone in a turn that already has receipts. The evidence ablation arm reaches the tool schema, so the two arms stop paying the same prompt and the A/B can answer for the contract rather than only for the gate. |
||
|
|
523791aff3 |
feat(agent): a contended slot goes to what is holding up the most work
The fleet plan proved a dependency DAG and published it, and the scheduler that decides which item actually runs never saw it: an AcquireRequest carried a write claim and a label and no position in the graph at all. So a slot went to whoever queued first, which is how the head of the longest chain ends up behind leaves that unblock nothing — and that wait lands on the run's makespan one for one. It is the loss fanoutMetricsOf already prices as wall above the critical path: measured every run, acted on by nothing. The plan now measures each item's rank, the longest chain of dependents below it, and carries it to the acquire, where the pump drains waiters heaviest first and keeps arrival order within equal weight. That covers every grant after the session's slots are full, which on a fan-out wider than the ceiling is nearly all of them. It does not cover the first fill, and sorting the launch loop does not either. Which of a burst of goroutines reaches the scheduler first is the Go runtime's decision: launching heaviest-first captured 41.8% of the ideal first-fill weight against declaration order's 51.9% at a ceiling of two, and tied at one and three — never better, sometimes worse, because the runtime favours the goroutine created last. The loop is left alone and says why. A run also queues before it builds rather than after. It used to construct the whole child — registry, runtime, and the transcript that holds a store lease — and only then join the line, so a fan-out wider than the session ceiling held one open per item it had not started, and every millisecond of that setup was billed to a concurrency ceiling that had not caused it. Foreground runs hold the slot from before the transcript exists; background ones still return a job id first and queue inside the job. Deriving the request moves to slot.go, the file that owns the acquire. |
||
|
|
e4a6dee1a3 |
refactor(kernel): delete the exported functions nothing references
Two fixes and one sweep, all from the same audit. The process history catalog was never closed. boot's cleanup releases LSP, plugin hosts, the runtime scope and session temp; the catalog's own Flush/Close had lived in the retired shell and were left to two test files. It is process-wide, shared by every pane and workspace, so it closes at the process boundary next to the usage catalogs, not with any one controller. Queued index writes are drained first — the reopen after an unclean exit re-scans every root instead. The sweep removes 57 exported functions with no reference anywhere in the tree, tests included, plus the private helpers that fell dead behind them. Iterated to a fixpoint: deleting a function exposes the next one, and both the orphan rule and golangci's unused ran again after each round until neither reported anything. The rule learned two things from being wrong here. Methods are now out of scope: chatTUI.Init is bubbletea's tea.Model, no file in the tree names it, and proving that needs type information an AST pass does not have — the first attempt at this sweep deleted it. And an orphan tests still reference is reported differently from one nothing references, because they need opposite fixes: repair.PrepareFileUpdate is a cross-package fixture eight test files build state with, and deleting it was the second thing that attempt got wrong. Only the unambiguous half is deleted here; the 124 remaining stay recorded. |
||
|
|
1511b331eb |
feat(agent): a run graph node says when it ran, and what it was waiting for
The graph declared StartedAt and EndedAt and no producer ever filled one, so every consumer's timing fallback was dead code and a run's own order survived only as the order its deltas happened to be published in. Waiting for a slot and holding one were the same word besides. A fan-out the session concurrency ceiling was throttling drew identically to one where every item started at once — and which of the two it is decides whether raising the ceiling would buy anything. - agentgraph gains StateQueued and Node.QueuedAt. Terminal() had to learn the new state: a queued node reported as settled leaves a reader no longer waiting for the answer it is still going to get. - SchedulerPolicy.OnStart fires once the slot is held, which is the only moment the wait can be read from. acquireSlot moves to its own file and becomes the single place that reports it, for both the foreground and the background path. - fleet and parallel_tasks publish queued, then running, then settled. An item now settles the moment it settles rather than when its group ends: a long fan-out used to draw every finished worker as still running until its slowest sibling returned, which is the whole time anyone is watching it. |
||
|
|
5ec7467396 |
fix(agent): a delegated run asks the person waiting for it, instead of itself
Reported as Studio never raising the ask card: the model kept answering with "ask 无交互响应(fallback)" and telling the user to go fix it. It was reading its own tool result back. A subagent's ask found no asker and returned the headless fallback - the text written for autonomous runs, which says in as many words that no interactive user answered. The root executor has an asker: EnableInteractiveApproval wires the controller in as one, and every interactive frontend calls it. A child never did. newAgentServices binds each collaborator from Options and there is no Asker among them, so SetAsker is the only way in and only the controller ever calls it - on the executor and the planner it holds, never on anything they spawn. The gap was silent because the fallback is a plausible answer rather than an error. The call context already carries the right answer and nobody was reading it. RunSubAgentWithSession is the one construction boundary every child goes through, and by the time it runs, the context says whether a question can be put to anyone: a delegated run has a parent blocked on it and a person waiting, so exactly one question can be outstanding; fleet items and parallel tasks build their context with the asker dropped on purpose, because a question from one of many has no reader who knows which asked it; a background job runs under a job context with no call on it at all. Inheriting from the context therefore needs no flag and no policy - the three cases already differ where it matters. Both halves are tests now, because "silently answers itself" is what this class of bug looks like and neither half fails loudly. The construction moved to its own file rather than growing task.go, which is 1200 lines past its budget. |
||
|
|
e0e7c0aada |
feat(agent): the run graph is a fact the kernel publishes, not a shape a frontend infers
The structure was being re-derived in five places and survived in none. A fleet's dependency DAG lived and died inside one Execute call — the dispatch it emitted carried a prompt and a profile, so depends_on never left the process at all. The persisted sidecar recorded a parent link and nothing else. The event stream implied the rest through id prefixes. By the time any of it reached a reader it was a flat list of children, which is why a panel could count delegates and never say which of them was waiting on which. internal/agentgraph is the vocabulary, and it is a leaf: nodes, and edges that carry an identity rather than leaving a reader to match words. depends and context are two edges on purpose. The measurement arm orders its endpoints while delivering nothing between them, so a single edge type would describe an ablated run and a real one identically — the same reason an error a caller must tell apart carries a sentinel rather than a sentence. The other half of that rule was already being broken on disk. InheritedContext.Upstream was a bool: it recorded that some dependency's answer opened a run, and a reader asking which one had nowhere to look. It names them now, and a sidecar written while it was a flag still decodes to "there were sources, unnamed" rather than to nothing. fleet and parallel_tasks publish what preflight already proved, before any of it runs — an item that never executes emits no tool events to infer a state from, and an adopted answer emits none at all. That collapses three copies of the same five-state vocabulary into one and two identical group classifiers into one, so a group's progress card and its graph node can no longer disagree about how the fan-out ended. A node is the call it names. Timing and output stay on the tool stream and a frontend joins them by id, rather than the kernel publishing the same clock twice under a second naming scheme. internal/boot asserts that at the frontend sink through the real Build assembly: a scripted two-item fleet with a dependency in it, and the edge has to arrive. |
||
|
|
6eedacc95f |
revert(agent): fleet for_each removed, three fixtures said no
Built, shipped behind the surface, and measured against the call it replaces — the same graph with one task handling every subject. Three fixtures, twenty-eight graded runs against a real provider, every run of both arms solved, and mapping spent 70%, 85% and 98% more tokens for it (exact permutation p <= 0.03 each). The last fixture was written to be the case mapping should win: ten modules of near-identical code, a hundred and sixty lines each, and an answer that is the sum of ten separate derivations so a slip anywhere is a wrong answer. It did not win there either, and one run solved it by grepping the constants out rather than reading anything — which is the reason. A mapped child pays a full sub-agent's fixed cost per subject, while a single child with a tool belt never has to hold the subjects at all: it reads them one at a time, and when the facts are extractable it extracts. "One context cannot hold N subjects" assumes an ingestion an agent does not have to perform. So the graph vocabulary goes back to id and depends_on, and SPEC 3.14 records the exclusion as measured rather than as taste. Two things survive the removal. The edge dedupe in newFleetPlan stays: a task named twice in one depends_on was already counted twice before for_each existed, and the item would have waited for a result nothing publishes. So does unresolvedDepCounts, which adoption needs — an adopted node enters resolved and its dependents have to be ready on the first pass. benchmarks/fleet-fanout goes with it. The history holds both if a case that genuinely needs per-subject isolation ever turns up. |
||
|
|
d484e88ce5 |
feat(agent): a fleet node's width may be dynamic, its shape may not
for_each names the item whose submitted subjects this item maps over: its prompt becomes a template run once per subject, and it reports one bounded aggregate that its dependents open with over the ordinary dependency edge. The mapped children are never vertices. The node stays one, so the reachability, write claims, and skip rules preflight proved are the ones that ran — adding N vertices mid-run would invalidate the precomputed reachability that skipDependents and the concurrent-claim check both read. Four things are fixed before anything starts, and that is what keeps for_each from being the first keyword of a workflow language: the template, the width ceiling (max_items, defaulted to 8 and hard-capped at 32), the read-only grant — which removes the write-claim analysis the host could not have done for an unknown number of writers — and the source. The source supplies only which subjects, through submit_items rather than prose, because reading a list out of an answer is the host guessing at a judgement the structure does not carry. The caller owns the topology, the worker owns the data: the same split the dependency edge makes. submit_items reaches only a source's registry, so a capability nothing in a run can use never enters its prompt. A source that finishes without submitting fails the mapped node rather than quietly mapping nothing, while an empty submission is a real answer that completes it. A source may not be adopted, having never run to submit anything, nor itself mapped, since it answers with an aggregate rather than a list. SPEC 3.14 said the graph vocabulary had no dynamic fan-out. It now says no computed edges, which is the invariant that actually mattered. Execute's preflight moved into preflight(), which returns a proven fleetCall, and the ctx-gated tool attach sits beside the other ctx-keyed host framing in the two sub-session entries; that is what paid for the new lines without widening the repolint baseline. |
||
|
|
01ee3830b1 |
feat(agent): a fleet dependency edge carries the answer, not only the order
depends_on ordered its endpoints and delivered nothing, so a research → implement pair paid for the same research twice. Three changes that compose: - The edge carries the dependency's answer. It travels as ContextRequest.Upstream, never inside TaskSpec.Objective, because the objective must stay the pristine text delivery classification judges — ClassifierTaskText is captured before any host framing. One 8 KiB budget is shared across a node's dependencies, so a wide fan-in cannot flood the opening turn and no dependency is starved out of it. - list_subagents enumerates the children a conversation owns. A reference is the only address a child answer has, and a background fleet that a restart repaired to interrupted never delivered the aggregate carrying those references, leaving its completed children unreachable. - adopt_ref stands a node down to an already-completed child's answer instead of running it. It is proven in preflight through ReadFinalAnswer, so what may be adopted cannot drift from what read_subagent_result may read. Nothing runs, so the node claims no write_paths and cannot collide with a live writer; prompt and every execution field are refused on it rather than silently ignored. Whether an adopted answer is still valid is not readable from structure, so the host does not decide it: adoption is addressed by reference, and the caller that listed the reference judges freshness. That is also why a plan fingerprint does not quietly reuse matching children — reuse the caller cannot see is reuse it cannot correct. A child now inherits something, which is exactly what InheritedContext exists to keep a decision rather than an accident, so the capsule records it and SPEC 3.13 says so. --ablate upstream leaves the edge ordering its endpoints while delivering nothing, which is what lets the payload be measured apart from the order. Against the real provider on benchmarks/upstream-edge/fleet-edge-wide, n=6 per arm, removing the payload cost +27.6% tokens (exact permutation p=0.011) and +59% sub-agent tool calls for the same answer, 6/6 solved either way. On a fact one grep away it buys nothing: the edge pays in proportion to what the upstream's work cost against how small its result is. boot.go's delegation registrations moved to delegation_tools.go and Execute's spec loop to buildItemSpecs; that, plus folding two defaulting branches into firstNonEmpty, is what paid for the new lines without widening the repolint baseline. |
||
|
|
b93b80d05c |
fix(agent): a dispatched sub-agent arrives with its name and its count
Every dispatcher is hidden behind use_capability, so the tool name on a dispatch is the proxy's. The profile that says who ran was resolved only for directly-called tools, and emitResolvedToolDispatch never carried one — so every sub-agent the model actually spawned reached a frontend anonymous, with no way to attribute a step or count the contexts opened. Profile now describes the delegation rather than just its model: Name says who runs, Count says how many. A task without a model override is still a delegation and says so; fleet and parallel_tasks report the size of the batch, which is the number nobody could previously see. The resolution behind the proxy reads the real target with the real inner arguments, so a proxied dispatch and a direct one carry the same identity. All of it sits in delegation_capability.go beside the alias contract: what a dispatcher answers to and what it reports are one subject. |
||
|
|
6e0b886446 |
feat(agent): a way to ask when the static tables run out of answers
shellsafe enumerates 145 commands and will never enumerate them all: a new formatter, linter, or query tool arrives every week, and each one that falls through reads as "might write". A read-only command classified that way blocks work — a real run spent seventeen consecutive rounds trying to see one failing test, rewriting the filter each time a pipe segment tripped the mixed-command contract. So the tables get an escalation path rather than more entries. They stay the fast path and answer everything they know; when they come up short and the host is about to refuse, one classification call decides, and the verdict is cached so a segment is asked about once. triage_model points those calls at a cheap tier — empty falls back to subagent_model, then the main model, so it works before anyone configures anything. The escalation cannot widen what actually runs. The OS sandbox still bounds every write, the verdict is refused unless the reply is exactly READONLY, and anything unestablished — no provider, a timeout, a hedged answer — lands on the refusing side. Delivery never escalates: there a mixed command is refused for what it does to the receipt, not for what it might write. Two things this cost to learn, both from running it rather than reading it: - A one-word answer needs thinking turned off. Under a tight token cap a thinking model spends the whole budget reasoning and returns nothing, so four classifications in a row silently failed. - The classifier gets its own provider instance from boot. Borrowing the turn's own would put extra calls into a stream the run is mid-conversation on — which is exactly what hung the agent tests against a scripted provider, for the full ten-minute default timeout. repolint grows -only, which reports the budgets of named files instead of the whole tree's recorded debt. A change could not tell its own violations from inherited ones; the model in one run tried to build a git worktree to compare against, and the sandbox refused it. Repo-wide ceilings still report, because a file can push the tree past one without exceeding its own budget. Carries the rest of the in-progress work in the same snapshot. |
||
|
|
d70d9d4686 |
perf(studio): opening a conversation no longer waits on a wallet read
Switching sessions felt slow, and the length of the transcript was not why: a 12MB record reads back in 8ms. What the reader waited on was /status, which queried the provider's wallet endpoint on every call — 87-167ms, uncached — while the pane held the transcript behind a Promise.all with it. Three layers, measured at each end: - billing.Cache serves one wallet answer for 30s, lets exactly one caller refresh it, and keeps a good reading standing in for five minutes when the endpoint starts failing: unreachable is not empty. billing.Store shares one cache per endpoint and credential, because opening a conversation builds a whole runtime and a per-runtime cache is cold on exactly the read a switch waits on. control.Options collapses its three balance fields into that one cache, which is also what keeps boot.build under its ratchet. - The pane reads the record and the numbers over it separately. __restore no longer carries the cache totals; __totals brings them when they arrive, so they are still read back from the kernel rather than restarted at zero. - fromHistory drops two O(n²) shapes — a linear search per tool result, and a full copy of the folded list per item. 3200 turns: 121.6ms to 2.9ms. /status 100ms to 15ms; transcript on screen 139-219ms to 48-125ms, and no longer a function of how long the conversation is. perf/switch.mjs is the bench that holds it, with PERF_STATUS_MS to separate the two waits. Carries the rest of the in-progress studio work in the same snapshot. |
||
|
|
e660218d6b |
refactor(compaction): retire what the multi-threshold scheme left behind, and hand its numbers over
Compaction is not the class of strategy the last few commits removed. The
context window is a physical constraint, not a judgment about how the model
should work, and every rejection in acceptCheckpointCandidate is already
grounded in a measured token count against the user's trigger or the provider's
ceiling. What did not survive review is the residue of the retired
multi-threshold scheme, and the numbers inside it that were never anyone's to
choose but the host's.
The residue:
- compaction.consecutive was incremented in one place and reset in four, and
read in none. It only ever existed to be cleared.
- soft_compact_ratio / tool_result_snip_ratio / compact_force_ratio are
forced to zero at config load, then threaded from boot through agent
Options and a twelve-parameter task constructor to fields nothing reads.
- SnipStaleToolResults and PruneStaleToolResults were exported no-ops with no
callers, and PruneStats existed to be their return value.
- ContextReport still declared SoftThreshold / SnipThreshold / ForceThreshold,
which nothing populated — and /context both printed them and branched on
them. "LatestPrompt >= ForceThreshold" against a permanent zero is always
true, so every session, including an empty one, reported "next: at the force
threshold". It now names the fold trigger and the hard ceiling, which are
the two thresholds that exist.
The numbers, now under [agent] and all off by default:
user_turn_keep_tokens your own turns kept verbatim through a fold
first_turn_pin_tokens first turn pinned into the cached prefix
checkpoint_ceiling_ratio how small a fold's result must be to be accepted
compact_ratio loses its 0.65–0.85 range. Compacting early to keep prompts small
and compacting late to keep history whole are both real preferences; only a
value that is not a fraction of the window is wrong. The desktop bridge's copy
of the range goes with it.
The user-turn retention budget was min(8192, 5% of window), so it stopped
growing at a 164K window: someone running a million-token model kept the same
8192 tokens of their own words as someone running 128K. It is now the window
share alone, with the fixed count kept only as the fallback when no window is
known. The separate 1500-token per-turn ceiling is gone — the remaining budget
is the only gate, and [[keep]] is still the way to insist on a specific turn.
Because the budget is the user's, /context reports what the last fold did with
it: turns kept, turns dropped and roughly what they cost, and the budget that
decided. noticeDroppedUserTurns already said it once, at the fold; this is what
can still answer afterwards.
Cache-impact: none - no prompt, tool schema, or memory text changes. Fold
behavior is identical for an unconfigured agent: every new setting defaults to
the value it replaced, and the retention budget's window share is unchanged at
every window where the old fixed count was not already the binding limit.
Cache-guard: go test ./internal/boot/ ./internal/agent/ (prefix_shape golden
unchanged; TestUserTurnKeepBudgetScalesWithTheWindow and
TestUnsetCompactionBudgetsKeepTheDefaults pin the defaults)
Documentation-impact: none - docs/*.md documents neither the retired thresholds
nor the fold budgets; the [agent] keys are self-describing in the rendered
config.
|
||
|
|
8da0a3de8b |
feat(kernel): give image routing a model of its own
An attachment the main model cannot read is already handed down to whatever sub-agent runs next, and that sub-agent's model was chosen for something else — usually cost. A cheap worker is text-only, so the provider serializer drops the image and the turn answers as though nothing was attached. The composer has been promising a vision-capable reader since it shipped; the only way to deliver one was to hand-edit subagent_model and give up the cheap worker. agent.vision_model names that reader. The routing travels in the turn context beside the image candidates, because it answers the question they raise: the parent could not read this, so who can. A sub-agent about to receive candidates it would drop is moved onto that model; one that already reads images, or a turn carrying no attachment at all, is left alone — a review sub-agent spawned in a turn that happened to have a screenshot keeps the model it was chosen for. /roles gains the assignment and the band's 看图 slot stops reporting a borrowed subagent, so what the settings pane shows is now a switch rather than a readout. config.roleModelRefs is the one list of model refs a role can name. The fallback repair and the referenced-provider scan both walk it, and they have to walk the same one: a role missing from either keeps alive a ref no provider resolves. Cache-impact: none - the system-prompt prefix is untouched. Vision routing only rewrites a per-turn context value and a sub-agent's model ref, and the new config key renders into user TOML, never into the prompt. Cache-guard: go test ./internal/boot/ (prefix_shape golden unchanged) System-prompt-review: esengine - config gains agent.vision_model; it is read at sub-agent spawn time and rendered into user TOML, and reaches no prompt-bearing surface. Documentation-impact: none - no docs/*.md describes image routing or the sub-agent model fields beyond SUBAGENT_PROFILES, whose precedence rules are unchanged. |
||
|
|
33fca85bc5 |
refactor(agent): make the turn a value the tool path is handed (#8588)
The turn was two types: perTurnState, embedded in Agent and zeroed by
assignment, and runLoopState, a genuine local threaded through the loop. Both
described one Agent.Run. They are now one turnRuntime, and beginRunTurn returns
a pointer to the agent's copy instead of opening a second object — the loop
fields used to be built into a fresh literal that discarded everything the
classification above had just computed onto perTurnState.
executeBatch, executeOne, resolveToolPolicy and applyDeliveryPolicyGates take
*turnRuntime. Reading it off the receiver compiled and behaved identically, and
would have kept doing so right up until a turn was not the agent's current one.
Two more fields moved after checking their write sites:
- classifierTaskText is written only by New, so it is agentConfig, and the
existing immutability guard now covers it.
- preserveEvidenceOnce, deliveryRecoveryPending and forkRestore are armed by
someone outside the Run — a sub-agent spawner, the turn that just failed
readiness, the fork capture — and consumed by the next one. They are
pendingTurn. Putting them in turnRuntime would have been wrong in a way
that compiles: beginRunTurn's replacement assignment would wipe state armed
before the turn existed.
capabilityLedger and capabilityGate stay on Agent for the same reason. Their
reset lives in internal/control, which calls SeedCapabilityRoute before a Run;
a controller cannot seed a value the Run has not created yet.
turnruntime_test.go guards all three properties: the type stays free of mutexes
and atomics so the single assignment keeps compiling, the tool path reads its
parameter rather than a.turn, and beginRunTurn's replacement literal stays
empty.
repolint: struct-state for agent.go 10 -> 3, repo total 122 -> 115. Verified
key-by-key that no baseline entry increases.
Part of #8523.
|
||
|
|
ea9ee16e4e |
refactor(agent): extract the task lifetime into taskRuntime (#8541)
Eight fields on Agent outlive a single Run but are narrower than a session: they belong to one delivery scope. resetTurnEvidence only runs when that scope changes, which is what made them a lifetime of their own rather than session state — recorded until now in comments rather than in a type. Group them into taskRuntime and reach them as a.task.<field>. Reset conditions are unchanged: the ledger group, the checkpoint, and the repeat-failure records each keep their own condition in beginRunTurn, so restartLedger carries scope, checkpoint, and failure records forward exactly as before. The gain is that the carry-over is one named list in a single assignment instead of an emergent property of which fields each call site happened not to mention. prepareRepeatFailureScope moves to taskRuntime.prepareScope verbatim. The mechanical rename went through gopls so internal/control's own deliveryCheckpoint and taskBudget — different types, same names — were left alone by construction. The repolint baseline goes down: struct-state 20 -> 18 for agent.go, essay 81 -> 76, file-size 2276 -> 2251. No entry increases. Part of #8523. |
||
|
|
5e03e03eaf |
feat(agent): light/balanced/delivery role settings with unified tools / 角色设定与统一工具面
Problem: Work modes (economy/full/delivery) mixed token surface trimming, delivery gates, and UI labels. Economy forced connect_tool_source schema growth; switching modes rebuilt controllers and busted prompt-cache prefixes. Skill profiles could hide tools from the model surface. Fix: - Introduce AgentPreset (light|balanced|delivery) + TaskPolicy host floors for plan route, verification, and independent review. - Share one provider-visible core surface across presets; optional tools dispatch only through use_capability (retire connect_tool_source boot). - Switch role settings in place without controller rebuild (CLI /preset, desktop, ACP agent_preset; legacy work_mode/tokenMode dual-write one version). - Emit turn_phase and completion_summary events; wire desktop/CLI UI. - Keep skill profiles diagnostic-only; dual-write agent_preset on tabs/branch meta. - Docs, i18n, golden surfaces, and regression matrix updated. Verification: - go test ./internal/boot ./internal/agent ./internal/cli ./internal/acp ./internal/control ./internal/skill ./internal/agentpreset ./internal/taskpolicy - desktop role-setting smoke tests |
||
|
|
397ed30ec8 |
feat(agent): content-driven context maintenance (single compact_ratio) / 内容驱动上下文维护(单一 compact_ratio) (#8244)
* feat(agent): content-driven context maintenance with single compact_ratio Replace multi-threshold prune/snip/native auto maintenance with one content-driven checkpoint path (stable prefix + one summary + recent tail) gated solely by compact_ratio (default 0.85). First-visible tool results are bounded at creation; prune/snip APIs remain no-ops for call-site compatibility while legacy config/sidecar fields load and migrate away. Also align max_output_tokens auto ladder (0=auto; 128K explicit only), desktop/settings/docs, and force CompactNow planning so mid-size strict-alternating sessions (e.g. guardian) still reduce tokens. Verification: go test ./...; desktop go test ./...; frontend test:all; cache-guard; race on core packages; live seed→resume→continue smoke (version=1, one summary, ~$0.0012). * fix(agent): make context projection durable on Windows and clear CI gates Problem: - SaveCompactionState fsynced the parent directory after rename. Windows returns ERROR_ACCESS_DENIED for directory Sync, so every projection install failed and Windows smoke tests went red. - gofmt, repolint, frontend AgentView fixtures, and required cache/docs PR-body lines also blocked CI for #8244. Root cause: - Custom atomic write path reimplemented durability with a directory Sync that is not portable, instead of using the shared Windows-safe helper. - Residual format/debt and a missing coldResumePrune fixture field. Fix: - Persist sidecars through fileutil.AtomicWriteFile (temp + file fsync + replace) without directory Sync. - gofmt touched Go files; shorten essay comments; extract fold helpers; carry residual structural debt in the repolint baseline after reducing new essay debt; restore coldResumePrune on settings fixtures. Verification: - gofmt clean; go run ./tools/repolint clean - go test ./internal/agent/ ./internal/config/ ./internal/provider/... ./internal/guardian/ ./internal/cli/ - pnpm settings-refresh-snapshot: 91 passed * chore: nudge PR head refresh after Windows durability fix Empty commit so GitHub re-resolves the fork head for #8244 after the merge of main-v2 and the Windows sidecar write fix. * fix(agent): close #8244 review blockers for content-driven maintenance Problem: Review of PR #8244 requested changes for CI failures, threshold under-count, legacy native lineage drop, non-atomic CAS/write, residual cold-resume UI, and duplicate context_maintenance notices. Root cause: - Threshold used message-only estimates without tools/role projection. - Sidecar lineage required exact PromptCacheKey and marked restored before covered-hash validation. - Checkpoint install checked transcript outside the install lock and used non-strict atomic write with EXDEV copy fallback. - coldResumePrune UI/API remained after behavior removal; locale gzip grew. - Maintenance notices ignored operationId on reconnect/replay. - golangci reported unused prune/summary helpers and minor modernize issues. Fix: - Estimate compact trigger from final request shape (messages + tools + provider role projection). - Normalize legacy |context-editing-native-* keys; restore only if projection still validates; persist normalized key without version bump/events. - CAS install under compactionMu; SaveCompactionState uses AtomicWriteFileStrict. - Remove coldResumePrune settings UI/Wails field/locales; keep config migration. - Dedupe context_maintenance notices by operationId (bounded FIFO) + tests. - Delete unused prune/summary helpers; clear golangci; trim locale strings for bundle budget; strip SPEC trailing space. Verification: - make lint (golangci + repolint) clean - go test agent/config/control/guardian/provider - pnpm frontend build (locale budgets pass) - context-maintenance-notice + settings-refresh-snapshot tests * fix(desktop): gofmt reasoning_display fixture after cold-prune removal Desktop CI failed gofmt on reasoning_display_app.go after ColdResumePrune was dropped from the AgentView fixture literal. * fix(agent): unblock context-maintenance merge blockers Problem: PR #8244 still had three merge blockers: emit under compactionMu (deadlock risk), restore rewriting exact-key sidecars, and initial raw bundle over budget. Root cause: commitSummaryProjection held the lock through synchronous sink.Emit; LoadProjectionSidecar persisted whenever the key already matched; ContextPanel/TranscriptSelectionMenu were eagerly imported into the initial chunk. Fix: - Unlock before emitContextMaintenance; reentrant sink test - needsNormalization only for legacy native lineage rewrite; log persist errors instead of ignoring them - Lazy-load ContextPanel and TranscriptSelectionMenu - Document pre-interceptor threshold estimate + overflow recovery - CLI/GOAL docs: compact_ratio default 85% Verification: - go test ./internal/agent/ (+ race focused tests) - make lint (repolint baseline essay bump) - pnpm build: initial raw 2236.8 / 2240 KiB - context-maintenance-notice test ok * fix(agent): close remaining context-maintenance plan gaps Problem: After the merge blockers landed, plan gaps remained: no parent-dir fsync on strict checkpoint writes, new writers still mirrored last_*/blocked_* fields, dead CompactionCapabler shell, and the pre-interceptor threshold contract lacked a regression test. Root cause: AtomicWriteFileStrict only fsynced the temp file; LastReceipt was authoritative but writers still dual-wrote mirrors; provider.CompactionCapabler had no production callers; Prepare never invoked interceptors by design without an explicit test. Fix: - Strict atomic write: fsync parent directory after rename (Windows/ unsupported FS soft-fail) - SaveCompactionState / commit / blocked paths: LastReceipt only; strip last_trigger/last_mode/token mirrors and top-level blocked_* - Restore receipt-only blocked sidecars on load - ContextReport/CLI/status prefer LastReceipt with legacy fallback - Delete unused CompactionCapabler/NativeCompaction shell - TestPrepareThresholdSkipsExtensionInterceptors Verification: - go test ./internal/agent ./internal/fileutil ./internal/provider - go test -race focused packages - make lint (repolint clean) * docs: drop removed context_editing example from zh GUIDE Problem: GUIDE.zh-CN still suggested context_editing = "native" after native Anthropic context editing was removed and startup migration deletes that key. Root cause: bilingual GUIDE config sample was not fully synced with the single compact_ratio local-summary contract. Fix: remove the obsolete config line from docs/GUIDE.zh-CN.md. English GUIDE already had no such example. Verification: grep docs for remaining context_editing user examples (only SPEC migration notes remain, which correctly describe removal). * fix(fileutil): never fail strict write after successful rename Problem: AtomicWriteFileStrict returned parent-dir fsync errors after rename had already published the new file. commitSummaryProjection and blocked-receipt writers treated that as "not committed" and rolled memory back, forking disk (new generation) from process memory (old generation) and allowing paid re-summary loops. Root cause: post-publish power-loss durability was modeled as a hard failure even though the atomic publish already completed. Fix: - After successful rename, parent-dir fsync is best-effort only - Returned error still means "destination not published" - syncParentDir also covers relative parents (".") - Agent tests: applied and blocked receipts survive injected post-publish dir-sync faults without generation fork / re-summary - PR body: document AtomicWriteFileStrict + best-effort dir fsync Verification: - go test ./internal/agent ./internal/fileutil - make lint |
||
|
|
d97bf94e75 |
fix(agent): a tool's own refusal reports as blocked, not as a completed call
A read-only subagent that rejected its own bash command returned the
refusal as ordinary output with a nil error. The agent had no way to see
it: the call was recorded as a success, frontends drew a green check on a
command that never ran, and the storm breaker — which counts blocked
outcomes to detect a model bouncing off the same host refusal — was blind
to it.
The host already has one shape for a refusal, toolOutcome{blocked, errMsg},
produced by the permission, plan-mode, and hook gates. What was missing was
a way for a tool enforcing its own policy to reach it. tool.Blocked carries
that signal out of Execute; executeOne turns it into the standard blocked
outcome, with the not-run shell metadata a blocked bash card already
renders elsewhere.
Both subagent bash wrappers now report through it, so the read-only command
policy and the background-execution rule refuse the same way. The review
and desktop-try registries share the wrapper, so their contract assertions
move with it.
Extracting blockedToolOutcome/shellPreflightExecution and the evidence
receipts keeps execute_one.go inside its ratchet budget.
|
||
|
|
64fc5ecf59 |
refactor(goal): split boundary helpers for lint ratchet
Problem: the Goal boundary implementation grew several grandfathered files beyond the repository's repolint ratchet. Root cause: run-boundary state, sidecar compatibility helpers, evidence novelty logic, and their tests were added directly to legacy owner files. Fix: extract the new logic into focused same-package files, reduce the affected owner functions, and keep the existing repolint baseline unchanged. Verification: go run ./tools/repolint; go test ./...; go test -race ./internal/agent ./internal/control; go vet ./...; desktop go test ./...; frontend pnpm build. |
||
|
|
bfdef5dfdc |
fix(goal): replace text-derived stalls with run boundaries
Problem: Goal mode could pause useful research after a text-derived 4/6/10 no-progress streak, while the outer 10/20/40 continuation budget did not bound actual model rounds inside one Run. Root cause: Cross-turn novelty was acting as both progress telemetry and a termination proxy. The agent exposed no typed host-owned Run boundary, so the controller could not distinguish default Goal budgets, explicit max_steps, or structural stuck conditions. Fix: Add a default 16-round Goal Run budget with one tool-free finalization response, typed resumable pause metadata, inherited child budgets, and Goal-only structural stuck detection. Keep cross-turn novelty observational, migrate legacy no_progress pauses, preserve unknown sidecar fields, and expose requestsUsed across CLI, ACP, and Desktop surfaces. Verification: - go test ./... - go test -race ./internal/agent ./internal/control - go vet ./... - cd desktop && go test ./... - cd desktop/frontend && pnpm build - live DeepSeek, LongCat, and GLM normal/stuck/16-round-resume matrix Co-authored-by: Will <[email protected]> |
||
|
|
84c257e56b |
feat(bench): measure delegation independence and anchor resistance (#8154)
Reasonix isolates a child's session but not its conclusions. The context capsule records, field by field, what a child inherited from its parent and every field is false — yet the one channel that actually carries information, the parent's free-text prompt, is not modelled at all. Two host-recorded instruments close that gap. Evidence origin splits what a delegation's own text pointed at into directory scope, which is the unavoidable cost of handing work off, and named files, which say where the answer is. The child is credited only for paths no named file already covered, so a scope hint never erases its work. The two are counted apart because one number reports the cheap act as the expensive one — the combined form read 0-3% independence where the split reads 94-100%. The -anchor arm hands the agent a hypothesis before it has read anything: the task's real cause, a plausible wrong one, or none. Tasks without an authored seed are skipped rather than run as the control, so a seeded denominator never absorbs a control run. Measured over 210 runs across two corpora (deepseek-flash, 5 reps each), anchor resistance is 1.00 on both — including one whose answers no command can check — while a wrong hypothesis costs ~2x wall and ~1.35x tokens to unwind. Children discovered 94-100% of their own evidence: this parent hands scope, not answers. Also guards two failures found while running it: the bench read metrics tags the agent had stopped emitting, and a failed grader's output never reached the record that had to explain the failure. |
||
|
|
1d0c0e7db2 |
Merge origin/main-v2 into refactor/subagent-spec-split
main-v2 had moved 84 commits ahead. Everything auto-merged except tools/repolint/baseline.json, which is generated: resolved by taking upstream's and regenerating, rather than hand-merging JSON. Regenerating surfaced one real violation instead of a carry-forward: internal/event/event.go had grown past its budget, so the delegation audit sink and its dispatcher move into audit_forwarder.go where the rest of that concern already lives. The only baseline entry above upstream is plugin_entry.go's essay, which is the field comments carried through the earlier extraction. |
||
|
|
e451757bdb |
refactor(event): forward optional sink capabilities by embedding, not by hand
Nine optional capabilities, repeated at every wrapper in the chain, silently lost three audits in this branch alone: control's goal-usage tee, the stats recorder, and the nesting sink fleet items run under. Each time the data simply stopped, each time with the tests green, and each time the fix was to write the same method one more place. event.AuditForwarder holds the nine forwards once. A pure forwarder embeds it and is complete by construction, so the next capability added there reaches every embedder without editing them. goalUsageTee, notify.Sink, telemetry's sink and the nesting sink now embed it, deleting roughly a hundred lines of identical methods; the tee had been forwarding eight of nine, and the nesting sink three of nine, before this. Wrappers that do real work per channel — the coalescer flushes first, stats and trajectory record, run metrics consumes — keep their own methods. Embedding is for the ones that only pass things through. The guard now asserts the forwarder itself covers every capability, which is the property that makes the wrapper-level guard mostly redundant rather than load-bearing. Verified end to end against the provider: a delegated run still reports its child. |
||
|
|
034dc7e1e7 |
feat(bench): delegation-shaped tasks, and count the fleet children they revealed
Four tasks: two whose structure rewards delegation with a neutral prompt, and two forced twins that differ only in telling the agent to delegate. Same workdir, same checks, so the pair prices delegation on identical work instead of comparing different work. Both seeds were confirmed to fail unfixed and pass fixed before either was used. The existing subagent-delegation task mandates delegation in its prompt, which tests the plumbing rather than the value: a single-agent arm cannot comply, so it loses by the rules rather than on the merits. The neutral tasks exist so an arm that declines to delegate is only penalised by the clock. Running them found the instrument under-counting. A fleet item runs under a sink built by subSinkFor, which was a bare event.FuncSink; optional sink capabilities are forwarded by method, so every delegation audit a fleet or parallel_tasks child produced was swallowed at the nesting boundary. The forced fleet task burned 3.8x the tokens of the neutral one and still reported zero child runs. subSinkFor now returns a type that forwards the audits, and the same task reports 3 child runs doing 71% of the tool calls. This is the third instance of one contract implemented at many wrappers and missed at one. The guard added earlier covers the session chain but not this nesting sink, which is a fair argument that the pattern wants a shared embedded forwarder rather than more guards. |
||
|
|
87b7d130f3 |
fix(context): make maintenance projection-idempotent
Problem: Long sessions could repeatedly snip the same canonical tool results, retry failed summaries, and report misleading context state. Root cause: Preflight and post-turn compaction evolved separate views, while pruning rebuilt from canonical history instead of the installed projection. Failed view fingerprints and maintenance outcomes were not durable. Fix: Make ContextManager the single request-time owner, evolve projection-only views with CAS and v3 receipts, persist blocked fingerprints, add Anthropic native tool clearing capability gates, and expose current context composition separately from summary-call cost. Verification: - go test ./... - go test -race ./internal/agent ./internal/provider/anthropic ./internal/control - go vet ./... - ./scripts/cache-guard.sh - cd desktop/frontend && pnpm typecheck - focused Desktop and frontend context contract tests - go run ./tools/repolint |
||
|
|
73fee3aa0a |
feat(cli): record per-run delegation counters so arms can be compared
Eight commits of orchestration machinery have landed on this branch without anything measuring whether delegation pays. More agents always cost more tokens, and the extra tokens alone can look like an improvement, so the comparison has to hold the model fixed and read host-recorded facts. Each finished child now emits a DelegationAudit through the same optional-sink path the readiness audit uses, and run metrics fold it into counters: how many children ran and at what depth, how many mutations they made, how many files two children both touched, how many runs ended in a checkable claim versus prose, how many claims the host refused to back, and how many writes escaped a declared scope. false_completions is the counter that matters. It comes from the completion adjudication, so it measures claims the host refused rather than a reviewer's opinion — the difference between "the fleet finished faster" and "the fleet said it finished". Emitting from one place is only possible because child construction converged on RunProfileSpec; before that this would have needed the same emission in five paths, and would have been missed in some. Two things were deliberately not added rather than approximated. The audit records the adjudicated status and the count of refused criteria, not an inferred "claimed" status the host never independently observed. And the counter for child tool calls is the existing SubagentToolCalls, counted from tool events, instead of a second field counting receipts under the same name. Task success rate and rework-after-handoff stay unmeasured: they need a task corpus with checkers and whole-run mutation ordering, which belong to the harness driving the arms, not to the instrument recording one run. SPEC 3.17 states what is and is not measured, and that --ablate subagent is the existing control axis for the single-agent arm. |
||
|
|
a6804d3afa |
refactor(agent): fold read_only_task into the one child-construction path
Five entry points were building sub-agents outside TaskTool.RunProfileSpec: read_only_task, boot's two skill runners, reasonix review, and desktop preview. Each re-resolved tools, depth, and permissions on its own, which is how a boundary added in one place gets missed in another — the same shape as the preview path that once built unconfined file tools and the profile editor that once dropped read-only on save. It already bit this branch: boot's writer skill runner receives complete_subtask in its registry but not the closing contract, because the contract is appended on the TaskTool path it bypasses. read_only_task, the one fork inside package agent, now compiles to a spec and runs through RunProfileSpec like every other in-package entry point. Its promise of no durable side effects is preserved as ContextRequest.Ephemeral — a field on the spec rather than a second construction path — so the convergence carries no behaviour change, which is the only safe way to do this kind of refactor. spawn_boundary_test.go enumerates the files still calling the low-level runners and fails on any new one, with the fix spelled out in the failure. The three remaining callers are recorded as known debt rather than precedent; folding them in touches boot, cli, and the desktop module and belongs in its own change. SPEC 3.15 states the rule: many APIs, one execution primitive. |
||
|
|
85cacad3b1 |
feat(agent): record an explicit context capsule for every sub-agent run
What a child sees was previously answerable only by reading the delegation path. Investigating it turned up a fact worth stating outright: a child sees no standing instructions at all. Its system prompt is DefaultTaskSystemPrompt or the profile body and nothing is composed into it, the memory queue is disabled so it cannot even record memory, and the parent conversation, Goal, planner output, and sibling results are never passed. Isolation is total, not partial. Each run now records a ContextCapsule in its transcript sidecar: workspace, system-prompt source and hash, resolved tool scope and schema hash, model and effort, parent session and tool-call id, any resumed transcript, and an inherited block whose fields are all false. capsuleHash is its stable identity, so "why did this reviewer not see that constraint" is answered from the record and two runs that behaved differently can be diffed. The capsule holds references and digests only — copying parent context would cost tokens and break the child prefix, which is the thing isolation was protecting. The spec member that says what a child should start from is renamed ContextRequest, freeing ContextCapsule for the resolved manifest. metaFromSpec moves next to the capsule it now builds. SPEC 3.13 tabulates what is and is not inherited. The non-inheritance is asserted in a test, so a future change that starts inheriting something has to flip the flag and update the table in the same commit. |
||
|
|
27ba95010f |
feat(agent): close sub-agent runs with a host-adjudicated completion claim
A delegated run used to end in prose, which is not an interface a parent can verify. Writer sub-agents now carry complete_subtask: they submit a status, a summary, the acceptance criteria they were held to with the command or paths proving each, and whatever they left unresolved. The submitted status is a claim, not a verdict. The host checks every citation against its own ledger before the parent sees it — a verification criterion must name a command the host recorded as run, diff/files must name paths it observed touched, and a manual note never backs itself. Unbacked criteria are lowered to unsatisfied, a report holding one cannot stay complete, and each downgrade is printed with its reason. The host only ever lowers. The mechanism mirrors review_report exactly: a subagent-only tool whose call args are the receipt, adjudicated from the same evidence ledger, so the two completion contracts stay one design rather than two. complete_subtask joins complete_step and todo_write in the receipts the parent ledger does not inherit, since the claim belongs to the child's run. Sub-agent answers are now composed as adjudicated claim, child prose, host receipts. SPEC 3.11 documents the contract; the write-claim section moves to 3.12. Cache note: writer sub-agent registries gain one tool and their task prompt gains the closing contract, so child prefixes change once. The parent prefix is untouched. |
||
|
|
16b6cbd9c0 |
feat(agent): host-authored subagent receipts and a typed delegation spec
Three related changes to the sub-agent delegation path. ProfileExecSpec splits into TaskSpec / WorkerSpec / CapabilityGrant / ContextCapsule / SchedulerPolicy, so a new field lands in the member that decides its value rather than whichever one is nearest the call site. The CapabilityGrant doc states the ceiling-intersection invariant that only IntersectToolLists implemented before. No behaviour change; tool schemas and the provider-visible prefix are untouched. evidence.Receipt now carries the exit status the shell runner actually observed and the host's verification classification, which were captured in tool.ShellExecution but dropped before reaching the ledger. Every sub-agent answer ends with a host-recorded attestation of what it changed and which commands it ran, attached at RunSubAgentWithSession so task, read_only_task, fleet, parallel_tasks, background jobs and the boot skill runners all inherit it. Only mutations and verification/failed commands are rendered, so read-only research children stay exactly as cheap as before. A declared write_paths claim is audited after the run: mutations the host recorded outside the claim are reported to the parent instead of passing silently. SPEC 3.11 documents every enforcement surface and states plainly that an omitted claim is a scheduling boundary only, and that declaring paths costs bash where the OS sandbox cannot enforce write roots. Adversarial tests pin symlink escape, parent traversal, move_file destinations, unbindable writers, and the deliberate absence of intra-workspace enforcement for undeclared writers. evidence.Receipt moves to its own file: evidence.go had no headroom under the repolint ceiling, and at 2896 lines the split was overdue regardless. |
||
|
|
3d011f9e73 |
merge: integrate latest main-v2
Problem: The PR branch was behind the current main-v2 base and GitHub reported merge conflicts. Root cause: Main-v2 advanced with contextual tool isolation after the previous integration merge. Fix: Merge the latest base, preserve both contextual tool identity and subagent image-candidate propagation, and regenerate the repolint baseline from the combined tree. Verification: - go test ./internal/agent ./internal/control ./internal/boot ./internal/jobs ./internal/memory ./internal/tool/... - go test ./internal/boot -run TestBuildDeepSeekTextParentCanUseImageReturningMCPAndVisionSubagent -count=1 -v - go run ./tools/repolint - git diff --cached --check |
||
|
|
0ef9de3029 |
merge: integrate latest main-v2
Bring the DeepSeek provider branch onto origin/main-v2 at
|
||
|
|
a6238139c4 |
fix(goal): harden contextual tool and migration boundaries
Problem: Context-dependent tools could be called outside their owning Goal, Plan, Jobs, or child-agent context, and failed legacy AutoResearch migration lost its retry identity after restart. Root cause: Availability was enforced by scattered execution checks while provider-visible registration, host metadata, mixed batches, and inherited child context used different state. The Goal sidecar writer also fenced every new write without preserving a pending legacy task id. Fix: Add a shared ContextualTool execution contract with one-repair run-loop handling, isolate child Goal/Jobs/memory state, project contextual schemas only for host metadata, and retain pending legacy task ids until a Goal-only sidecar is durably committed. Verification: go test ./... -count=1 cd desktop && go test ./... -count=1 go test -race ./internal/control ./internal/agent ./internal/tool ./internal/tool/builtin ./internal/jobs ./internal/memory -count=1 go vet ./... golangci-lint run --timeout=5m scripts/cache-guard.sh git diff --check |
||
|
|
f2929dc4cd |
fix: preserve the static provider tool contract
Problem: The PR made provider-visible schemas conditional on Goal, Plan, and Jobs context, removing update_goal from ordinary and economy requests and violating the required byte-stable tool contract. Root cause: A broad contextual-tool mechanism was added while hardening Goal recorder isolation, coupling an execution boundary to provider schema selection. Fix: Restore static Schemas() requests and the main-v2 tool order, remove contextual schema APIs and phase-specific visibility, and keep only execution-time Goal recorder isolation for planners and child agents. Correct the changelog and cache-impact coverage. Verification: go test ./internal/agent ./internal/boot ./internal/control ./internal/tool ./internal/tool/builtin -count=1; go run ./tools/repolint; git diff --exit-code origin/main-v2 -- internal/agent/extensions.go internal/agent/run_loop.go internal/agent/sampling_request.go internal/tool/tool.go internal/tool/builtin/bgjobs.go internal/tool/builtin/completestep.go internal/tool/builtin/updategoal.go internal/jobs/jobs.go internal/memory/queue.go; git diff --check |
||
|
|
e08ffbf4a2 |
refactor: isolate contextual workflow surfaces
Problem: The Goal/runtime fixes added lines and complexity to several files already at their repolint debt ceilings, causing the repository gate to fail after merging the latest baseline. Root cause: Contextual tool visibility, legacy durability helpers, and their tests were implemented inline in large owner files instead of dedicated modules. Fix: Extract workflow context, planner registry, subagent identity, Goal durability, Jobs context, CLI Goal handling, and focused tests into scoped files. Keep provider behavior unchanged and add the new schema-bearing files to cache-impact coverage. Verification: go test ./internal/agent ./internal/boot ./internal/control ./internal/jobs ./internal/tool/builtin -count=1; go run ./tools/repolint; git diff --check |
||
|
|
bf47eedce3 |
fix(goal): restore contextual tool and migration contracts
Problem: A concurrent branch reconciliation kept static provider schemas and moved legacy recovery state outside the Goal machine, diverging from the approved #7959 behavior after the latest-base merge. Root cause: The merge resolved overlapping Goal, Jobs, child-context, and tool-registry owner files in favor of an alternative execution-only isolation design. Fix: Restore ContextualTool provider visibility, bounded mixed-call repair, transactional retryable legacy migration, downgrade fencing, contextual metadata, and child Goal/Jobs/memory isolation on top of the merged base. Keep the portable Windows archive-read fixture. Verification: go test ./... -count=1 go test -race ./internal/control ./internal/agent ./internal/jobs ./internal/tool ./internal/tool/builtin ./internal/memory ./internal/autoresearch -count=1 go vet ./... golangci-lint run --timeout=5m cd desktop && go test ./... -count=1 scripts/cache-guard.sh scripts/check-cache-impact.sh go run ./tools/repolint git diff --check |