You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: docs/testing.md — every kind of weft test in one guide (make test/vet/lint/fmt and core/contract_test.go, godoc examples, make offline and WEFT_MODEL_REQUESTS=deny, wefttest Script/Record/Replay and the conformance suite, the live tag, make fuzz/fuzz-thread and their eleven targets, make stress and TestNestedRandomChains' POOL_CHAINS_* knobs, the nightly soak-thread, ClickHouse, studio-check's budgets and ledgers, devtools-vite-check, mcp-check, apidiff and the facades, the Go 1.26.0 floor, the phase gates, the arena): what each proves, the command, when it runs, measured durations, knobs, how to reproduce a failure; before-push and before-release checklists; which tests a new feature adds. Linked from AGENTS.md and README.md. AGENTS.md said make fuzz runs five targets; it runs core's six (and make fuzz-thread five more). ci.yml's studio-job comment said a 600 KiB gzip budget; the gate (Makefile studio-check) is 700 KiB.
studio: a test fails when a route has no auth-matrix row. TestAuthMatrix now reads every route Studio registers — the package's HandleFunc/Handle pattern literals (each resolved on the matrix's server) and the run and session sub-route tables — and fails naming any route (method + pattern) with no row, and any row that reaches no registered route (rows pinning the API's 404 are marked as such). The OTLP receiver's POST /v1/traces and POST /v1/logs were the routes without rows (checked in a separate block); they are rows now: the ingest token as a bearer opens them, every API identity is 401. serveSessionRoutes dispatches from one table (sessionSubRoutes), unchanged in behaviour, so the check enumerates what it serves. docs/testing.md said this was a review duty; it says the test enforces it. TestAuthMatrixCompletenessCheck.
Fixed
core: the step's dispatch table no longer shares its backing array with the ModelRequest.Tools a model is handed. The loop dispatched from the very slice it passed down the WrapModel chain, with or without a PrepareStep chain, so a model or middleware reassigning req.Tools[i] changed what the step ran — another tool of the same name ran instead, and a nil element panicked findTool (under Stream outside the contained region, killing the process). The model now gets its own copy of the slice. Each message's Content slice is copied for the request too (one backing array per step, one allocation), so reassigning a part in req.Messages[i].Content no longer rewrites the run's transcript — under a PrepareStep chain too, where the model was handed the chain's own Messages (a message a function splices in and keeps across steps, a pinned instruction, was rewritten by a model writing into its request; TestPrepareStepRetainedMessageSurvivesTheModel). ModelRequest's godoc names the copied slices; the bytes inside parts (Args, Data) stay shared and read-only.
core: OutputDecoder partials only grow. The prefix closer dropped an open array whose tail could not close yet — a key cut mid-string, a literal cut mid-token inside it — back to the last top-level member, so a streamed partial could go 2 items → 0 → 2 and a UI flickered. An uncloseable tail is now cut back to where it began and no further: a number keeps its longest valid prefix (1e → 1), a cut literal goes and its key closes as null, a cut \u escape goes, and otherwise only the innermost member is dropped, back to the last comma or open bracket at any depth (a first top-level member still being written still reports no partial). FuzzPartialJSON now also asserts monotonic partials across every truncation.
core: a PrepareStep or a WrapModel middleware writing through req.Params — *req.Params.Temperature = 0.9, req.Params.Stop[0] = … — no longer reaches the agent's Params, a run option's, or the caller's variables behind them. The loop handed both seams the agent's own pointers, so one write changed the sampling knobs of every later step and run of the agent (concurrent runs raced on them), the request record's prepared[] missed params (before and after shared the pointer), and a retried attempt's request record carried what the model had written. PrepareStep's godoc already promised "a copy the function may mutate freely"; the chain and the model now each get their own copy of the values (one allocation for the pointer knobs, one for Stop, none when nothing is set), and ModelRequest's godoc says so. TestParamsNeverWrittenThrough.
core: an OutputDecoder string partial only grows by appending. A string cut right after a lone backslash closed as an escaped one ("x\" showed x\, then the escape arrived: x⏎), a rune cut mid-sequence showed U+FFFD before the rune, and a \uD83D escape showed U+FFFD before its low surrogate; each tail is now held back until it completes (mw.RepairJSON's closer is unchanged). Only a \u escape waits for a low surrogate: a \\, \n or \t followed by four hex characters in D800–DBFF is text (the first 0.18.1 patch held it back too, so C:\\DBFF\\file streamed C:\DBF, then C:). Only JSON whitespace (space, tab, CR, LF) is trimmed around the document: a raw NBSP, NEL, U+2028/2029, U+2000–200A or U+3000 at the end of an open string is text (0.18.0 trimmed any Unicode whitespace, so {"s":"prix : 10"} streamed prix, prix , prix, …). FuzzPartialJSON and the decoder's partial tests now hold strings to a prefix, bools fixed and every scalar's type stable, seeded with these cases; TestClosedPrefixStringsGrowByPrefix, whose generator now writes \uXXXX escapes (surrogate pairs as escapes, lone surrogates), non-u escapes followed by hex, raw Unicode whitespace, and invalid UTF-8.
core/mw: Retry and Fallback hand each attempt its own copy of the request — fresh Messages, Content and Tools slices and fresh Params values. Every attempt was given the one request the layer received, so what a failed attempt's model or an inner middleware wrote into it (*req.Params.Temperature, req.Messages[0].Content[0], req.Tools[0] = nil, all allowed by ModelRequest's godoc) reached the next attempt and the next model of the chain: a middleware inside Retry rewriting by element assignment compounded per attempt, and a nil tool reached the adapter. Paid on the Retry/Fallback path only (about five allocations per attempt). TestRetryAndFallbackAttemptsGetTheirOwnRequest.
core/mw: RepairJSON keeps Unicode whitespace in the payload. It trimmed the arguments (and a fence's payload) with strings.TrimSpace, so a raw NBSP, NEL, U+2028/2029, U+2000–200A or U+3000 ending a truncated open string was cut away and the tool got {"s":"prix"} for the model's {"s":"prix<NBSP> — model-visible content lost, against the godoc's byte-for-byte promise. At a cut inside an open string only JSON whitespace (space, tab, CR, LF) is trimmed now, as in the OutputDecoder's closer. Outside the document nothing changes: Unicode whitespace before the arguments, after a document that closes, before a fence (an NBSP before ``` still opens one) and around a fence's payload is trimmed as 0.18.0 trimmed it, and ASCII whitespace exactly as before. TestRepairJSONKeepsUnicodeWhitespace (its outside-the-document cases pinned to v0.18.0's output), TestRepairJSON's new cases.
core: the request recorder's catalog memo keeps its own copy of the tool slice it hashed, not the slice the model was handed — a model reassigning req.Tools[i] to the next step's tool made that step record the previous catalog's hash and no tools record of its own. TestRequestRecorderCatalogMemoIsItsOwn.
thread/pool: a two-level sync park (lead → child → grandchild, each a sync wrap) decided once on the top parent resumes the whole chain — the grandchild runs the decided call, its answer resumes the middle child, whose answer is the lead's tool result. 0.18.0 settled the middle delegation failed with "nothing to resume: the child's resume did not survive a restart" though nothing had restarted (the middle child's replay started no resume of its own: its boundary is its grandchild's delegation, whose resolution now hands its resume to the middle delegation's run). The chain also resumes after a restart that wraps the agents again; the restart words are left to a grandchild no agent can resume. A grandchild that parks again while the middle child's resume waits on it climbs the chain like the first park: the middle child parks in turn and mirrors the new request to the lead, at any depth (the first 0.18.1 patch left it in the middle — the lead's Pending empty, its receipt running, Wait blocked for good). The second mirror keeps the namespaced call id and names the new occurrence by its run, superseding the first (ADR 0022 §C): one request offered, undecided, the earlier decision not carried over. Cancel or Close of the middle child while its resume waits ends the grandchild first (a parked one is canceled at its boundary; Close leaves it parked) and waits for it to rest, at most 30 s, so the grandchild settles canceled in the middle's ledger before the middle's session closes and the lead reads SUBAGENT_CANCELED (it read the grandchild running for good). The pool's bound holds through the hand-off: the grandchild's slot passes straight to the middle child's resume, which used to run its first model call slotless beside the next delegation to take the freed slot (two model calls in flight on New(1)). TestNestedTwoLevelApprove, …Deny, …AcrossRestart, …RestartWithoutGrandchildAgent, TestNestedTwoLevelReparkApprove, …ReparkDeny, …ReparkThreeTimes, TestNestedThreeLevelRepark, TestNestedCancelWhileAwaitingTheGrandchild, TestNestedResumeHoldsTheBound.
thread/pool: nested delegations end settled, whatever ends them. A grandchild that ends holding no slot — canceled while it queued, canceled at its boundary, panicked — queues for one on the middle child's behalf before its answer starts the middle's resume; the resume ran its first model call slotless, beside the bound (the second 0.18.1 patch lent only a slot the grandchild held). A grandchild that parks again while a sibling grandchild still runs its approved call no longer parks the middle child: the middle waits until every delegation it has left is parked (a Cancel or Close of the middle parked that early closed its session under the running sibling — its receipt running for good, "settlement receipt not recorded: session is closed" — and resumed the re-parking one with the cancel's denial). Cancel of a middle child parked on its grandchild's request ends the grandchild before it returns — canceled at its boundary, never resumed, settled canceled in the middle's ledger (0.18.0: the cancel's denial re-armed the grandchild, whose resume failed into the middle's closed session, and the middle's ledger read it parked for good); from the moment a middle child starts ending, none of its sync grandchildren is armed again or resolves its delegating call in it. A Cancel of a running child whose run reaches its approval boundary first settles it canceled there instead of leaving it parked (Cancel's godoc: "the receipt settles canceled"). A resumed sync child canceled before its parent's turn had parked the delegating call — the parent still running another call of the same step — resolves that call once it parks, waiting on the pool's context — and, once the pool has closed, for at most 30 s more, Close's drain waiting for it (in 0.18.0 the resolution was tried at once, failed "delegating call no longer pending", and the parent parked on the call with nothing left to resolve it: Pending empty, Next nil; the third 0.18.1 patch still did so after Close, whose canceled context ended the wait at once). The 30 s bound on a middle child's wait for its grandchildren has a defined outcome: a grandchild still running past it is settled canceled by the pool in the middle's ledger while that session is open, its stop naming the bound, and its abandoned run records nothing when it ends (it was logged and left, its settlement failing into the closed session, running until a Recover); Cancel's and Close's godoc, ADR 0022 §F and docs/thread-operations.md say so. Ending a middle child no longer cancels its async children (Submit, an Async wrap): they follow D4. TestNestedResolutionQueuesASlotWhenTheGrandchildHasNone, TestNestedReparkWaitsForTheRunningSibling, TestNestedCancelParkedMiddleEndsTheGrandchild, TestCancelBeforeTheWrapperParksStillResolvesIt, TestNestedEndBoundSettlesTheStuckGrandchild, TestNestedEndLeavesAsyncChildren, TestCloseResolvesACanceledResumeOnceTheParentParks, and TestNestedRandomChains — random chains of depth 2–4 and fan-out 1–3 on pools of one and two, with Submit children beside them, a request expiry, Forwards to running receipts; decided and denied to the end, or interrupted half-way by a Close racing a Decide, a Cancel racing a Decide on the same lead, or a restart (Close, the leads reopened on a second pool that Recovers them, driven on to the end); every Wait returning, every receipt terminal at the end, no delegating call delegated twice, the bound held, no goroutine left (POOL_CHAINS_ITERS, POOL_CHAINS_MODE, POOL_CHAINS_EXPIRY for a longer run).
thread/pool: Cancel of a sync child parked while its parent's turn still runs a sibling call returns at once, under its own context: the settlement is recorded before it returns, and the delegating call — not parked yet — resolves with SUBAGENT_CANCELED on a pool goroutine once the turn parks it (Wait waits for that). The third 0.18.1 patch waited for that park on Cancel's caller, ignoring its context, and a Cancel made from inside that sibling's own handler deadlocked. Under a RequestExpiry a delegating call is never offered as a decidable call, never lapses as one and never runs its delegation twice: a middle child whose grandchild's mirrored request lapsed in it before it parked (its step still running another call) parked with nothing to mirror, so the lead's Pending offered the middle's delegating call itself (Child ""), the middle's later answer failed "does not delegate to child session", the lead stayed parked, and approving the offered call ran the delegation again (in 0.18.0 too). A child now parks only on a boundary that offers something — one open only on its own sync delegations is waited on, like a resumed child's — and a sync grandchild resumes only while the child above it waits on it (a lapse in a parked middle resumed the grandchild alone, its answer starting a resume of the middle nobody followed). Close of a middle child waiting on its grandchildren cancels a parked one at its boundary with the middle, as the middle's own end does, instead of leaving it parked under a middle that settles canceled (nothing could resume it, and Recover of the lead does not descend into a settled child); Close's godoc now says which parked children stay parked for a later pool: those under a parent at rest. A steer Forward sent to a running child whose run Cancel or Close ended before draining it no longer runs its follow-up turn: that turn ran on the Forward caller's context after the run had given its slot up, a model call beside the bound (in 0.18.0 too). ADR 0022 §D, §F, §H, Cancel's, Close's, Forward's and Decide's godoc and docs/thread-operations.md say so. TestCancelParkedChildBesideARunningSibling, TestCancelFromTheSiblingHandler, TestNestedExpiryNeverOffersTheDelegatingCall, TestCloseCancelsParkedGrandchildUnderAnEndingMiddle, TestNestedReparkWaitsForTheRunningSibling (its Close case now wants the parked grandchild canceled), TestForwardedSteerDoesNotOutliveACanceledRun, TestNestedAbandonedRunParkingLaterWritesNothing.
thread/pool, docs: Recover's godoc and docs/thread-operations.md §10 said a restart never re-runs anything; a restart while an approved call of a parked child runs — the decision recorded and replayed, the call's result not — re-runs that call when Recover resumes the decided boundary (the session's own rule: an audited-but-unfinished resume stays resumable). The docs now say so; the code is unchanged. The playground's weft.Replay side-effect classes do not apply here: make such a tool idempotent. TestNestedRestartMidApprovedCallRunsItAgain.
thread, Studio: a call Session.Resume carries undecided reads no_decision — on the resume's weft.resume.calls, the call's execute_tool span (weft.tool.denied_kind) and Studio's decided block — not denied: thread no longer synthesizes Deny(id, "no decision") for it, leaving it to the core's rule 12 (the model still reads DENIED: no decision). Record contract §3's weft.resume.calls row said thread resumes read denied; it now says they read no_decision. TestResumeUndecidedCallReadsNoDecision, TestDecidedUndecidedCallFromARealResume.
runtime: a playground decision on a parked thread: fork turn reports weft.tool.approved_byruntime, as the ephemeral path does (AGENTS block 4a); 0.18.0 read user, the fork session's Decide door (the record contract said policy; it now says runtime in both modes). Reporting only: the decision is the session's, unchanged. TestPlaygroundForkParkedDecision, TestDecideOnBehalfNamesTheApproval.
thread/pool: Pool.Close canceling a wrapped call made outside any session (a bare Generate) gives its model SUBAGENT_CANCELED: agent "<name>" was canceled before it finished — ADR 0022's table and Wrap's godoc (Cancel or Close ended the child) — not the core's SUBAGENT_FAILED … context canceled; the call's own cancellation or timeout keeps the core's words. TestCloseCoversEverything.
thread/pool: a Forward landing between a delegation's running mark and its child's first Send is refused — a *StateError reading accepted (parked in the same window of a resume), retryable at once — instead of becoming the child session's first turn: the task's prompt queued behind the steer, the steer's turn parked on an approval no one mirrored, and the receipt read running for good, Wait and Close never returning (since v0.5). Forward now needs the run's turn in flight, as its godoc says. TestForwardInTheStartWindowIsRefused; TestNestedRandomChains forwards into start windows. The first cut marked the turn sent only once the start's caller ran again after its Send returned, so a Forward to a child whose model was already running could still be refused as accepted (TestForwardRequiresRunning and TestForwardedSteerDoesNotOutliveACanceledRun under make stress); once the start's Send is under way, Forward waits for it to return (its ctx bounds the wait) and steers the turn it sent. TestForwardAfterTheStartReturnedSteers. A restarted pool's first resume of a child it rebuilt (Register or Recover, then Decide) read accepted in that window too — the refusal was picked by the delegate's park count, which a rebuilt delegate starts at zero; it reads the run's kind now and refuses as parked. TestForwardInARestartedResumesStartWindowReadsParked; TestNestedRandomChains asserts the exact state (accepted before a first run, parked before a resume) instead of either.
thread: Session.Request on a lapsed request sweeps it, as every other path does — the expiry audit and the deny, a mirrored request's denial reaching its child through the pool, the boundary resumed when that completes it — before it returns ErrExpired. Under RequireSigned (no unsigned Decide to sweep it) Pending kept offering a request no challenge could be minted for. TestRequestSweepsALapsedRequest, TestRequestSweepsALapsedMirroredRequest.
thread/pool: two pools delegating from one parent session no longer trip on each other: each pool watches the parent, and every decision recorded there made the pool that did not make the child return ErrNoAgent from its Decide and log an ERROR from its watch. A pool now skips the receipts whose child another pool watching the parent holds (a live delegation, a Register, the wrap it was made under), in its pumps and its Recover, silently; Pool's godoc and docs/thread-operations.md §10 say how to recover several pools after a restart. Session.MirrorWatchers (new, pool plumbing) lists the watching pools. TestTwoPoolsOneParent, TestTwoPoolsOneParentRecover. Two windows stayed open, both a closed pool answering for nothing: a pump that read another pool's child unsettled, then found it settled and its pool closed by the time it asked, and a pump reaching the child of a pool whose Close had canceled it but not yet recorded its settlement, each reported ErrNoAgent and logged the watch's ERROR ("a decision recorded on the parent did not reach its children") — TestTwoPoolsOneParent under make stress, in 6 of 13 loaded package runs. The lookup now reads the ledger again before it answers ErrNoAgent, and a closed pool still holds the children its Close is ending (never one it left parked). TestForeignChildSettledAndItsPoolClosedDuringTheLookup, TestForeignChildOfAClosingPool.
thread/pool: a Decide made while a child is parking (its receipt reads parked, its phase not yet), then Close, no longer leaves the child parked on a decided boundary that nothing arms. The decision's pump leaves such a child to its park's own re-read, and Decide returns as if the resume were queued; a Close landing before the park finished made the park keep the child parked — its delegating call parked in the parent until a Recover — and Close returned at once, where a Decide that found the child parked had its resume queued and Close canceled it (SUBAGENT_CANCELED). The park now reads the parent's verdicts once it sees Close, and a boundary they complete is canceled at the boundary as a queued resume is; Close's godoc says so. TestCloseWhileParkingOnADecidedBoundary; TestCloseResolvesACanceledResumeOnceTheParentParks waited only for the parked receipt (written before the park is done) and now waits for the child at rest, its own premise (it failed under make stress, GOMAXPROCS=1).
Studio (docs): POST /api/runs/{id}/approvals with a call_id the run does not hold pending is 400 before any command — now said in studio/README.md; a proposal for an agent no manifest names stays 404 (409 only when a served manifest file does not name it), as documented.
make: make test passes -timeout 20m like CI (TEST_TIMEOUT ?= 20m, overridable per invocation). It ran Go's default 10-minute per-package timeout, which studio (about 520 s under -race) came close to on a loaded machine; docs/testing.md described the workaround and now says it is fixed.