Releases: santhreal/veyyon
Releases · santhreal/veyyon
Release list
v1.3.0
@veyyon/agent-core
Breaking Changes
AgentOptions.cursorRulesResolveris removed: an agent no longer supplies a second, per-api rule channel beside its system prompt.
Added
- A ChatGPT OAuth (Codex) session compacts server-side via the Responses compaction endpoint, preserving encrypted reasoning state.
Changed
- Compaction imports
ProviderHttpErrorfrom its owning module rather than the@veyyon/ai/errorbarrel, cutting 14 modules off the engine's load graph with no change in behavior. - Streaming
message_updatesnapshots share tool-call arguments by reference instead of deep-cloning them on every delta, cutting a large structured tool call's per-delta snapshot cost from ~0.5 s to ~8 ms, while terminal messages and the authoritative tool call atoolcall_endcarries keep the sanitizing deep clone. - Superseded and useless tool results are now pruned as a batch whose combined size pays for the prompt-cache rewrite it forces, instead of only when a single result sits within 8,000 tokens of the end of the conversation.
- The tokenizer takes
estimateTokensFromTextfrom@veyyon/utils/tokensrather than the package barrel, cutting the modules a token estimate loads from 92 to 10. - Compaction's directory-list documentation now uses canonical
searchfilesterminology instead of the retiredfindtool name. No runtime behavior changed. - A tool that blocks on only some of its operations declares interruptibility per call, so an interrupt arriving beside a non-blocking or malformed call no longer replaces that call's own result with a skipped placeholder.
- A tool result that ran and failed no longer supersedes an earlier successful read of the same path, which replaced that file's content with a supersede notice and left the conversation only the error text.
- A tool call whose id already carries a real result in the transcript is never executed a second time, whichever channel answered it; a never-ran placeholder still counts as unanswered and is retried.
- An interrupted
cursor-agentturn keeps a tool call whose arguments the start frame already delivered, instead of deleting it and telling the model its arguments never finished.
Fixed
- Converted message wrappers preserve reference identity across turns when inputs are unchanged, avoiding unnecessary allocations and memo invalidations.
- Fixed tool-result supersede pruning to parse multi-target
readcalls into target sets with per-target URL scheme exemption, retiring an earlier read result when all of its targets are covered by newer reads while preserving results with partial coverage. - Side requests derive a stable conversation ID per oneshot kind, preventing compaction, handoff, and branch summaries from overwriting live Cursor and Devin conversation state.
- Aborting while paused rejects the pause wait and prevents the agent loop from starting another provider turn or paused tool.
- A branch-summary reserve at or above the model's context window now falls back to the proportional 15% reserve instead of leaving a non-positive budget, which the entry preparation read as "no limit" and which sent the whole branch.
@veyyon/ai
Breaking Changes
StreamOptions.cursorRulesand the exportedCursorRuleInputtype are removed, andbuildCursorRulestakes only the system prompt: the Cursor provider builds exactly one request-context rule, the assembled prompt.
Added
ToolCallLoopGuarddetects consecutive redundant reads of unchanged files whose requested line ranges are already fully present in recent context, steering runaway exploration loops while preserving prompt cache prefixes.- Added Command Code API-key login through the Studio Provider page, with validation against its Provider API, and Nous Research Portal OAuth device login with rotating refresh tokens and short-lived inference JWTs.
explain(error)in@veyyon/ai/error/flagsreturns the classification id together with the names of the rules that produced it, and every classification rule states a name.- Added
nous-research-api-key, a second way into Nous Research that takes a key pasted from the Portal instead of running the device flow, validated against the inference API and stored as the samenous-researchcredential.
Changed
- The assistant-text extractor's one-owner check names the consolidated evals package path instead of the retired metaharness path. No behavior change.
ToolCallLoopGuardwaits for a third consecutive subsumed read before steering, up from the second, so two narrowing reads of one file are no longer treated as a loop;model.toolCallLoopGuard.readSubsumptionThresholdstill sets it.- Formatted tool-call loop guard whitespace; behavior is unchanged.
- The Anthropic provider reads its endpoint, credential placement, rejected betas and retry policy from the catalog's wire-capability table instead of comparing provider ids at seventeen call sites.
ToolCall.argumentsis aReadonly<Record<string, unknown>>, so a producer replaces the object instead of writing into one a streaming snapshot already shares.- A streaming request no longer pins a parsed clone of its wire payload for the life of the stream: every provider's diagnostic dump retains only the exact sent bytes and materializes a body when a 400/413 dump is built.
- The OpenAI-family, pi-native and Codex request builders serialize the request body once instead of deep-cloning the request graph, which took attempt preparation on a 32MiB context from 82ms to 9ms.
- A message that names a dead socket reads the same everywhere:
namesDeadSocketin@veyyon/ai/error/flagsis the one list of errnos and phrases, andENETUNREACH,EHOSTUNREACHandEAI_AGAINnow count as transient transport failures like the rest of them. - Formatted source files for Biome compliance.
withAuthimports the two error classes it throws from their owning modules instead of the@veyyon/ai/errorbarrel, so a consumer of the auth-retry wrapper no longer loads the provider-error registry and every error domain behind it; behavior is unchanged.
Fixed
- An auth-broker snapshot containing an API key or OAuth credential stored by an interactive login validates again; the
sourcefield on either credential type made every client reject the whole credential pool. - A long name whose tail cycles is no longer read as a runaway sampler: a folder, path segment, hex digest or identifier that repeats a short group past the 180-character threshold ended the turn with
Thinking loop detected: repeated "…" N× back-to-backand re-sampled a prompt that produced the same name for the same reason. A whitespace-free run that continues a longer token is data, on both the streamed detector and the completed-text scanner; a run that begins at a token boundary still trips. - A
cursor-agentmodel receives the operator's instructions again: the server rebuilds the prompt head with its own system prompt and applies none of the request-context rules, so the assembled prompt now rides on the active user turn inside an<operator-instructions>block. - A
cursor-agentrequest uploads the operator's instructions once instead of three times: the request-context rule payload and the prompt-head blobs, both discarded by that server, no longer carry a copy, and a request that would send any count other than one fails before it is written. - Each tool call in a
cursor-agentbatch keeps its own arguments: updates route by the frame'scall_idinstead of a single "current call" pointer, which let a completing call overwrite the arguments of the one opened after it and left the first call with{}. - A
cursor-agentturn that ends with tool calls still streaming closes every open call rather than only the one the pointer last named, so a second call of a batch is no longer dropped as unfinished, and each closed call keeps its own parsed arguments and carries no streaming marker. - A
503 auth_unavailablerefusal is classified as an authentication failure rather than a bare server status, so compaction falls back to an authenticated model instead of failing the whole compaction (#986). - A llama.cpp tool-call JSON parse failure explains itself and names the fix on every route to a local server, not only when the provider id is
ollama, so an LM Studio or llama-cpp user sees why the turn stopped instead of a bare HTTP 500 whose retry was already being suppressed. - A llama.cpp tool-call JSON parse failure stops the retry ladder whether it arrives thrown from a request or recorded on an assistant message; the two classifier entry points share one post-walk latch instead of each deciding, so the same 500 no longer burned every attempt on one route while surfacing immediately on the other.
- A bare
502 Bad Gatewayor504 Gateway Timeoutis read as the upstream failure it is and costs a twenty-second retry, matching500; both previously matched no rule, came back as an unreadable body, and suppressed the failing model for five minutes over a gateway blip. - A rate-limit message reads
503,529and500as the status codes they are rather than as digits inside a longer number, so an exhausted balance reporting5030 credits remainingrotates the credential instead of retrying the same account after a 45-second capacity backoff. - A Gemini or Cloud Code Assist body that carried a whole turn and then ended without a
finishReasonsettles on what arrived rather than failing as a truncated response, matching the four dialects that already read the shared end-of-stream judgement; a body carrying nothing usable is still refused. - A stream that ended without a terminal finish reason is classified as the truncation it is whatever the provider called it, so an OpenAI completions turn that stopped early is retried like the identically-worded Cloud Code Assist one instead of ending the turn; an empty response body is the same f...
v1.2.0
@veyyon/agent-core
Breaking Changes
- The minimum supported Bun runtime is now 1.4.0.
Fixed
- The remote-summarizer cap comment names the profile-scoped log directory it writes to (
~/.veyyon/profiles/<name>/logs) instead of the pre-migration~/.veyyon/logs. Comment only; the cap and the write path are unchanged. - A cancelled turn carries
Flag.Abortwhatever reason it was aborted with, instead of only when its text matched the genericRequest was abortedsentinel.
@veyyon/ai
Breaking Changes
- The minimum supported Bun runtime is now 1.4.0.
- One OAuth classifier, one name.
isDefinitiveOAuthFailurewas a wrapper that returnedisOAuthExpiry(msg)and nothing else, kept because callers had learned the second name, and@veyyon/ai/auth-storagere-exported it a third way. Two names for one predicate is how two behaviours appear later, so the wrapper is gone: the classifier lives in@veyyon/ai/error/flagsasisDefinitiveOAuthFailure, beside the patterns it tests. An embedder importingisOAuthExpiry, or importingisDefinitiveOAuthFailurefrom@veyyon/ai/auth-storageor@veyyon/ai/error/auth-classify, imports it from@veyyon/ai/error/flagsinstead;@veyyon/aiitself is unchanged.error/auth-classifykeepsisAuthRetryableError, which is a rule of its own. @veyyon/ai/utils/retryno longer re-exportsisCopilotTransientModelError. It was a compatibility line for importers that predated the classifier moving into the error module; that classifier is at@veyyon/ai/error/flagsand is imported from there.callWithCopilotModelRetrystill lives inutils/retry.Flag.OAuthExpiryis removed. Nothing ever set it, sois(id, Flag.OAuthExpiry)answered false for every dead grant there has ever been, and a bit that always reads false is worse than no bit. Whether a refresh failure is definitive isisDefinitiveOAuthFailure, which is a boolean because it decides whether to disable a credential and its two answers are not symmetric.- One home per error class.
AuthBrokerErrorandAuthBrokerStreamUnsupportedErrorwere declared in botherror/classes.tsandauth-broker/client.ts, andCodexWebSocketTransportError,CodexWhitespaceToolCallLoopErrorandCodexProviderStreamErrorin botherror/classes.tsandproviders/openai-codex-responses.ts. Two classes wearing one name makeinstanceofanswer false for an error that carries the name:RemoteAuthCredentialStoretested the stream-unsupported sentinel against a class its own thrower never constructed, andclassify()comparederror.nameto a string literal, which any object passes by assigning a field. The classes live inerror/classes.tsand the modules that throw them import from there, so classification keys off identity. An embedder importingAuthBrokerStreamUnsupportedErrororAuthBrokerErrorfrom@veyyon/ai/auth-broker, orCodexWebSocketTransportErrorfrom@veyyon/ai, imports it from@veyyon/ai/errorinstead.CodexProviderStreamErrortakes its retryability as a required named option (new CodexProviderStreamError(message, { retryable, code })) rather than a positional boolean, so a construction site cannot leave the answer to a default. - The Anthropic retry wrapper is
isAnthropicStreamRetryable. It was a second exportedisProviderRetryableError, wrapping the one inerror/retryable.tsand adding Copilot'smodel_not_supportedcheck, so a caller's retry decision depended on whether it imported@veyyon/aior@veyyon/ai/errorand neither call site said which rule it got. One production call site used the wrapper; every other caller already used the classification directly. An embedder importingisProviderRetryableErrorfrom@veyyon/ai/providers/anthropicimportsisAnthropicStreamRetryablefrom there, or the unwrapped predicate from@veyyon/ai/error. isUsageLimitOutcomeandisUsageLimitStatusare removed from@veyyon/ai/error/rate-limitand from the@veyyon/airoot. They were the quota decision tree written a second time, outside the classifier, and six call sites wroteisUsageLimit(error) || isUsageLimitOutcome(status, message)because each half missed a case the other caught. The question has one accessor,isUsageLimit(error)from@veyyon/ai/error/flags, which now also answers a failure that arrived as a bare status with no body. An embedder callingisUsageLimitOutcome(status, message)callsisUsageLimit({ status, message })instead; the parts the quota family reads —matchesUsageLimitText,isOpaqueStatusBody,parseRateLimitReason— are unchanged.isRetryableStreamEnvelopeErroris removed from@veyyon/ai/error; the out-of-order envelope wording is now a transport rule in the registry, andisStreamEnvelopeErrorstill answers the identity question a provider asks about a stream it was reading.
Changed
- The provider retry ladder asks the registry what the transport stage does about a failure instead of reading
Flag.Transientand five message patterns, so a fast-mode entitlement wall, a malformed tool call and a stream that ended without saying why are no longer re-sent against the same credential. - The service-tier decision reads the catalog's per-provider wire capability instead of naming providers in four functions, and the vocabulary and its readers are re-exported from
@veyyon/aiand@veyyon/ai/typesunchanged. - The Codex usage reader takes
toBooleanfrom@veyyon/catalog/utils, where its sibling Copilot reader already took it, instead of declaring a third copy of the same one-line typeof check. - The stream-corruption vocabulary — a corrupted TLS record, a peer-reported HTTP/2 stream error, the upstream code
1302, a body that stopped mid-JSON, an envelope whose events arrived out of order — is a transport rule in the registry rather than a prose block only the provider ladder read, so the turn sees the same fault the ladder does. - A framing violation carries
Flag.TransportRefused, so every reader of the retry decision refuses it and a wrapper sentence that classifies as a deadline no longer gets it re-sent. - The OAuth callback-path and dialect wire-tag ownership gates read declarations instead of source text. Both proved "declared here and nowhere else" by matching the owner's formatted line, which stayed green for a duplicate spelled any other way; they now ask the declaration census in
@veyyon/utils/source-declarationsand the parsed import lists, so a re-copied constant is red however it is written. - The leaked-markup healer reads one vocabulary.
getStreamMarkupHealingPatternno longer restates the Kimi and DeepSeek provider lists and asks the catalog'sleakedToolCallGrammar, keeping"thinking"as the floor because every pattern runs the reasoning scanner.StreamMarkupHealingPatternis the catalog's union rather than a second spelling of it, and the streaming engine's official-OpenAI check is the catalog'sisOfficialOpenAIEndpointrather than a third copy of the hostname test. - A strict-tools rejection has one vocabulary, and so does a transient sentence. The grammar family read
invalid_request_errorplus a grammar or schema complaint; the OpenAI paths read a wire-format code, astrictvalue the endpoint would not mix and a tool schema it would not take, at 400 or 422; and each recognised rejections the other let through. The words are the registry's now,matchesStrictToolsRejectionTextcovers both shapes,matchesCompiledGrammarTooLargeTextnames the narrow case a caller answers by dropping the capability for the session, and the grammar rule accepts 422 because the endpoints that answer with it reject the same request for the same reason. Codex's retryable-event predicate had its own copy of the transient wording, drifted from the shared pattern by two phrasings (temporarily,processing your request); those moved intoTRANSIENT_TRANSPORT_PATTERN, where every provider reads them, and the event's code set stays Codex's own. - Every provider in this package fetches through
fetchProviderWithRetry, so the retry verdict is the registry's for the whole package.@veyyon/utilsowns the loop — the attempt bound, the backoff, theRetry-Afterhint, the HTTP/2 verdict — and cannot own the decision, because a status means something only next to a body. Its gate admitted the transient set and each caller was free to narrow it, so Bedrock, Codex, Gemini CLI and the shared OpenAI stream path passed no gate at all and silently took the loop's reading: a bare 429 was re-sent against the same spent credential while the credential layer, looking at the identical failure, rotated away from it. A provider that needs the loop without the verdict still passes its ownshouldRetryResponse. - One verdict decides whether a failed response is sent again. Four ladders had written that decision out by hand with four transient vocabularies: the Anthropic client read the status set plus 409 and never looked at the body,
ollama.tsread 5xx and one body pattern, the Claude usage poll read the set minus 429, andfetchWithRetry's gate stood in front of all three with a fourth copy.error/response.tsis the one home: it classifies what the response says through the same registry the thrown-error path uses, then asksrecover(id, "transport"). What stays per provider is aResponseRetryPolicy— a status its own API documents as retryable (Anthropic's 409), a status it refuses on principle (the usage poll's 429), a body it knows a replay reproduces (llama.cpp's deterministic tool-call parse failure) — and nothing about what a status means. The body is part of the decision and is read through the same bounded, redacted read as the error message, so a 429 that saysoverloadedis still a throttle and one with nothing to read is a wall the credential layer answers.isTransientStatusno longer restates the 408/429/5xx set and delegates to@veyyon/utils'isRetryableStatus, its owner. - ...
v1.1.1
@veyyon/ai
Added
setAnthropicProviderModule,setOpenAICompletionsProviderModule,setOpenAIResponsesProviderModule,setGoogleProviderModule,setGoogleGeminiCliProviderModule,setGoogleVertexProviderModule,setOllamaProviderModule, andsetDevinProviderModuleare exported from@veyyon/ai/providers/register-builtinsto allow deterministic provider stream simulation across production provider API routing and lazy watchdog wrappers.
@veyyon/coding-agent
Changed
- The todo board is back to the tree list it was before 1.1.0: one status glyph per task, open work first, the phase in parentheses after the content, and the withheld count on the last row. The panel form shipped in 1.1.0 is reverted — Roman-numeral phase headings, per-phase fractions, the header progress gauge, the indented rail body and the fourteen-frame entrance are all gone. The board is static again: it is drawn once when the result lands and never repaints, so nothing on it moves after a write and no interval is armed for it.
Fixed
- A
todocall that names no operation is refused by validation instead of by the executor.opwas plainly optional for one release so a Claude/CursorTodoWritepayload (which carriestodosand noop) would validate, and that made the schema lie about every other shape:{"task":"Scaffold"}and{"operation":"start","task":"Scaffold"}both validated CLEAN — a missing optional field is legal, and an undeclared key is not refused for an ArkType-authored tool — after which the executor answeredMissing op; pass op explicitly, naming a field the call had just been told it could leave out. The repair layer reported the same callclean, so all three layers declined to act and a model that believed it had named its operation retried the identical payload.opis now required by a schema-level narrow that admits its absence only alongsidetodos, so the compatibility whole-board write keeps working and every other shape is refused where the repair loop and the model-facing error path can act on it. A narrow rather than a required property, and rather than a union of the two shapes, because a top-level union converts toanyOfand the Anthropic tool-schema builder readspropertiesoff the root, so a union would advertise the tool as an object with no fields at all. operationandactionare repaired ontoopfor any tool that declares one (todo,goal). A model reaching for the operation field writes the whole word; the value used to sit in the call untouched, because an undeclared key on an ArkType-authored tool is not refused, and the tool then reported the field missing.- A
todocall carrying an emptytodoslist is refused instead of guessed at. An empty container is not a read: withmerge: falseit says "replace my board with nothing", which is destructive, and withmerge: trueit says nothing at all. It used to be inferred as an init and could clear the board. The refusal names the operation that does mean it —An empty "todos" list cannot initialize or clear todos. Pass op explicitly: op "rm" clears the board— and the recorded board is returned unchanged. - An unrecognized op reaching
applyOpsToPhasesreturns the board and names the op instead of returningundefinedphases and crashing the next read of it. The tool schema cannot produce one, but the/todoslash command and any extension build ops themselves and never pass through it.
What changed
23 commits since v1.1.0.
Fixes
- fix(ci): repair provider format and docs link
- fix(ai): accept semantic OpenAI stream EOF
- fix(todo): dropping a task is not doing it
- fix(ci): bound the singleton bucket to a chunk like every other bucket
- fix(demo): the hero declares the size of the file that is tracked
- fix(todo): refuse a call that names no operation at the schema, not the executor
Refactors
- refactor(ai): keep provider override types internal
Reverts
- revert(todo): the board is the tree list it was before 1.1.0
Documentation
- docs: refresh internal verification claims
- docs(settings): regenerate the reference from the schema
- docs(upstream): the radar is a script, not a scheduled workflow
- docs(testing): plan whole-product Rust conformance
- docs: standardize terminal proof captures
Tests
- test(todo): accept dropped tally fragment in partition sweep
- test(ci): serialize watchdog simulations
- test(simulations): exercise lazy provider routing
- test(ci): isolate singleton partition collisions
Build & CI
- ci: stop mirroring upstream ports as issues
Chores
- chore: regenerate root changelog
- chore(legal): regenerate the embedded bundle after the UPSTREAM.md edit
- chore: bump version to v1.1.1
- chore(changelog): render the root from the package changelogs
Other changes
- demo(hd): re-record the hero take and publish it at its native size
v1.1.0
@veyyon/agent-core
Fixed
- A generated summary that repeats itself is refused, and the history it would have replaced survives. Emptiness was already refused in three places, because an empty summary deletes the conversation and reports success; a summary that samples one sentence until the budget runs out is the same loss wearing content, and it passed every check. It is reachable by design: compaction generates through
completeSimple, whose loop guard re-samples a stalled generation three times and then runs one final pass with the guard DISABLED so a stubborn loop returns raw output instead of a fatal stall — correct for a live turn that is on screen and can be interrupted, wrong for text that replaces the span it describes and is then read by every later turn as its own past. Every artifact a compaction leaves behind is covered, not just the one that was reported:generateSummaryrejects a degenerate summary the way it rejects an empty one,assertValidCompactionResultrejects one from any other source (remote summarizer, compaction hook) in bothsummaryandshortSummaryimmediately before history is rewritten,generateHandoffFromContextrejects a degenerate document rather than appending the<files>block to a loop, andgenerateBranchSummarydegrades to its explicit "No summary generated" fallback and logs a warning, since a throw there would block the branch switch on a provider hiccup, which is why its empty case does not throw either. The floors are the loop guard's own, so one text cannot be a loop in the transcript and a compaction in the archive. compactWithProviderforwards the live session identity (sessionId, provider session state, and the codex compaction context, taggedresponses_compact) to the transport, which is what a host keying request identity to the conversation needs. Server-side compaction now runs for a ChatGPT OAuth (codex) session, where it previously could not resolve a transport at all and every compaction fell through to a local summary.estimateTokenscountsfileMentionandpythonExecutionmessages, which both fell through todefault: return 0and cost a session nothing. A@filemention carries up to 50KB of file body per turn and a$cell carries its code and its output, all of it sent to the provider and billed, so the compaction trigger, the pruning budgets and the context gauge each read short by the whole payload: one measured session's gauge said "61% left" while the request it had just sent was 40459 tokens against a 32768-token window, and the provider refused it. Host-contributed roles are now counted from one table besidebashExecution, and a mentioned image is charged the same fixed estimate as any other inline image.
@veyyon/ai
Breaking Changes
AuthStorageOptions.loadBalancingnow defaults to off, which is what its own documentation had always claimed while the field was initialized totrue. Every embedder that passed nothing got account movement it never asked for, and the one host that passes the option explicitly masked the disagreement. Which account spends money is the caller's decision; the default is the one that decides nothing.markUsageLimitReachedon a default-constructed storage now records the exhaustion and returns{ switched: false, retryAtMs }instead of rotating. Opt back in withnew AuthStorage(store, { loadBalancing: true }).
Added
clearCredentialBlocks(provider, credentialId)is public and type-aware, so a host can lift a rate-limit hold on one account without knowing how the block scope for that credential type is keyed. Previously only the Codex reset-credit path could clear a hold, which left an xAI or Anthropic hold with no way back except waiting.detectDegenerateRepetition(text)asks the loop guard's verbatim question of a text that is already complete: is a unit repeated at least four times back to back, 180 chars of it, carrying a letter or an emoji. The streamed detector cannot answer it, because its unit is the lastlenchars of what it has seen — right for a stream that aborts on the first hit, blind to a run buried mid-text behind a tidy closing paragraph, which is exactly the shape a stored artifact carries. It finds the run directly (per unit length, measure how fartext[i] === text[i + len]holds) rather than re-asking the tail question at every offset, and reports the shortest unit that clears the floors, so the reason names the repeat instead of a multiple of it. Callers: anything that keeps generated text rather than displaying it as it arrives.
Changed
- An explicit credential choice outranks automation, load balancing on or off. A session pin (or the provider's stored selection) is exempted from its own rate-limit hold when the routing order is built, leads the candidate order, and is admitted by the OAuth pass ladder regardless of the hold, so a held account the caller chose keeps serving instead of being substituted. Automation among accounts nobody named is untouched: an unchosen held account is still passed over for a sibling. The setting governs the product's own initiative, never what the caller may ask for.
- A quota hold no longer displaces a chosen account, but a dead grant does. A hold is this library's own prediction of when a provider will serve again; an authentication failure is the provider's verdict.
rotateSessionCredentialrecords the latter in an in-memory auth-dead set, and a credential in it stops counting as the explicit choice until a refresh, a re-login, orclearCredentialBlocksretires the mark. The set is deliberately not persisted: after a restart the account earns exactly one more attempt. sessionCredentialRoutingreports a held choice as the account that serves, not as a prediction.activeIsPredictionis now reserved for a substitute nobody picked, which is what a host needs to tell "you chose X and it is serving through a hold" from "you chose X, it cannot serve, so the next request uses Y".peekApiKeynames the chosen account. It selects by credential type instead of going through the resolve, so availability ordering alone put a held account last and model discovery described a sibling while every real request went to the account that was chosen. Choice promotion now has one owner (#leadWithChosenAccount) called by every path that orders candidates, which is also what the availability sort's own documentation had been claiming while two callers relied on an exemption inside the sort. Those two callers were passing an argument the sort no longer takes: the package did not type-check, and one of them (sessionCredentialRouting's prediction) could never see a choice in the first place, because routing answers with the choice and returns before it asks for a prediction — that dead promotion is gone rather than kept.
Fixed
- OpenAI-compatible gateways that end a tool-use stream with a final usage frame but omit both
finish_reasonand[DONE]no longer strand a complete call behind the transient retry ladder. A trailing accounting frame now terminates only when every streamed call has an id, a name, and strictly complete JSON-object arguments; text-only and partial-call EOFs remain incomplete-stream errors, so transport truncation cannot execute repaired or ambiguous input. - A sentence repeated forever is now caught within a few repeats instead of a few thousand characters. The cheap verbatim detector probed unit lengths up to 60 chars inside a 250-char tail, and a real session streamed one 80-char sentence about fifty times with nothing complaining: the repeating unit was never a candidate at any length, and a 250-char window holds three repeats of it where four are required, so raising either number alone would still have missed it. The cap is 200 chars and the window 900, four repeats of the longest unit probed plus slack. The character test that rejects runs of digits and punctuation is answered once per window by measuring how far the nearest letter sits from the end, instead of re-scanning every candidate, so the wider ladder costs a comparison per length rather than a regex per length. Text repeated after a tool call in the same stream is still not watched — the guard disarms text detection on the first tool-call event — and that hole is pinned by a test asserting the current behaviour rather than left to be discovered.
- The output-loop guard watches every model, not just Gemini and DeepSeek.
isLoopGuardedModelgated the whole guard on a provider/id regex, so a Claude or GPT stream that repeated one word five hundred times was never inspected at all and the runaway was committed to the transcript. The detectors (verbatim repetition, near-duplicate paragraphs, recycled vocabulary) are model-agnostic and were calibrated to zero false positives across 536k real reasoning blocks, and a false hit costs a re-sample rather than a lost turn, so the carve-out only hid loops. The gate is nowisLoopGuardEnabled(options)— the only thing that turns it off ismodel.loopGuard.enabledorVEYYON_NO_THINKING_LOOP_GUARD=1. The Gemini-specific header-run detector keeps its narrower gate. - Google and Vertex requests no longer fail outright for anyone with secrets configured.
streamGoogleGenAIhanded itsonPayloadhook the SDK-shaped params object, whoseconfig.abortSignalis a liveAbortSignal, and the secret redactor behind that hook walks the payload and refuses any value JSON cannot express. Every request died with "the provider request contains a non-JSON object; confidentiality transform failed." The signal never crossed the wire (paramsToWireBodydrops it) and nothing downstream reads it, so it is stripped before the hook runs. - Gateway-routed requests (pi-native transport) no longer fail outright for anyone with secrets configured when the turn offers tools. The client handed the hook the raw
context, whosetools[].parametersare live arktype schemas — fun...
v1.0.49
@veyyon/catalog
Added
- Refreshed the bundled catalog from models.dev: 127 rows across 22 providers that upstream already served, including
gemini-3.7-flashon Google, Vertex, GitHub Copilot, OpenRouter, Kilo, NanoGPT, OpenCode Zen and Vercel AI Gateway, each carrying the declaredlow/medium/higheffort ladder. The bundle had drifted far enough that a model released the day before was absent from every provider that serves it. A row missing from the bundle still recovers its ladder at runtime through the models.dev fallback, so what this closes is the cold-start and offline window before that fetch lands, not a broken lookup.
Removed
- Dropped
novita/inclusionai/ling-3.0-tinyandumans/umans-deepseek-v4-flash-0731-lab, which upstream no longer lists.
@veyyon/coding-agent
Fixed
- A recursive delete through a variable nobody set no longer stops a
yolosession.rm -rf "$BUILD_DIR",rm -rf "$CARGO_TARGET_DIR",rm -rf "$WORKTREE"andrm -rf "$checkout"were allcritical, the one verdict/yolocannot lift and no standing grant can cover, so the most ordinary cleanup an agent writes ended an unattended run — and the reason given was that the variable MIGHT hold/. It does not: an unset name expands to nothing, so the guard was refusing a command on a guess about a value that does not exist, at the only severity that cannot be overridden. A floor that fires there is a floor an operator switches off, and then nothing is left for the realrm -rf /. An unsettled expansion now earnsdestroysonly where no assumption is needed — the EMPTY reading, which is what an unset variable already is, sorm -rf "$OUT"/*(the July 2026 incident,direxpanded to nothing),rm -rf "$D"/andrm -rf "$D/lib"all still stop evenyolo— while a barerm -rf "$D", catastrophic only if the name turns out to be/or the home directory, drops todangerous: still a prompt at every rung belowyolo, no longer a claim to be as certain asrm -rf /. A word that spells a protected component itself keeps the floor whatever the prefix expands to, sorm -rf "$D/.ssh"is refused exactly asrm -rf ~/.sshis. - Two words the guard reported as "unknown" carry opposite evidence, and reading them as one shape is a fail-open hole. A variable NOBODY SET expands to nothing. A variable whose value the scan READ and declined to paste is going to become a real path:
rm -rf $VwithV="/*"globs to every top-level entry,${NOPE:-/}carries/in its own text,cd / && rm -rf $PWDnames a variable the shell maintains and which is/by the time it runs, and~usernames another account's home directory. Each of those was measured deleting the root with no prompt at any rung before the guard read the environment at all, soExpandedWordnow records WHY a word is unknown and every one of them keeps thedestroysfloor. Only the genuinely-unset case is treated as speculative. - A recursive delete through a variable is judged by the path it can become, instead of being refused for holding a variable at all.
rm -rf "$DST/facet"wascritical, which is the one verdict/yolocannot lift and no standing grant can cover, so on a long unattended run every variable-shaped cleanup stopped the agent dead — and the reason given, "an expansion whose value is not knowable from the command text", was true of an ordinary staging command whose worst possible reading is/facet, a top-level path the literal spellingrm -rf /facethas always been allowed to delete. An unsettled expansion is now instantiated with the three values that make a path dangerous (empty, the root, the home directory) and each concrete result goes through the same classifier a literal path does, so the refusal names the reading it is about: "rm would recursively remove a protected system directory (/) when the expansion this command line does not settle is empty". Every incident shape is still refused, including the July 2026 one: the bare word reads as/,"$dir"/*reads as/,"$dir"/libreads as/lib, and"$D/.ssh"reads as the credentials directory. A value that CLIMBS is the residual this accepts, and it is named where the rule is written. - A glob is judged as the directory it reads, which closes the literal half of the same hole.
rm -rf /*,rm -rf ~/*,rm -rf /var/*andrm -rf ~/.config/*were all allowed, because the text/var/*equals no protected root, is not an ancestor of the home directory, and sits under none of the protected directories — so each of them destroys a directory whose glob-free spelling has always been refused. A component holding a glob is now dropped together with everything below it and the directory the glob reads is judged instead. A glob inside an ordinary directory (rm -rf ./dist/*,rm -rf /var/log/nginx/*) is untouched. rm -rf ""no longer reports the working directory as its target. A word an expansion collapsed to nothing was resolved against the working directory like any relative path, so a cleanup run from the home directory was refused as a delete of the home directory itself.- The re-root nudge waits for a habit and then says it once.
cwd-rerootfired on the FIRST call naming a path outside the working directory — one glance at one file in another project — and its own body then told the reader to ignore it if the read was a one-off, which is a paragraph of advice arriving before the behavior it is about exists. It repeated after a gap of eight messages on top of that, so a session that reads across two projects heard it several times about work that had already moved on. It now declareswarmupMatches: 3andrepeatMode: per-compact: silent until three separate calls have reached outside the working directory, and then silent until the transcript is replaced. - A rule may declare
warmupMatchesin its frontmatter: how many distinct streams it has to match in before it fires at all, default 1 (the old behavior, unchanged for every rule that says nothing). The unit is the stream rather than the match, which is the whole difficulty — one tool call is re-matched on every delta it streams, so a warm-up counted in matches clears inside the first call and the rule fires exactly as early as it did before, with nothing reporting it. A count is set aside when the reminder is claimed and restored if that claim is released undelivered, so an aborted turn costs neither the reminder nor the evidence for it; delivery starts the count over. veyyon updatereplaces the binary that is running, instead of whichever copy of the name PATH resolves first. Measured end to end: a 1.0.47 binary installed under a sandbox home reportedNew version available: 1.0.48,Checksum verifiedand✓ Updated to 1.0.48, yet stayed at 1.0.47 — while a different install nobody had named was overwritten with the 1.0.48 release asset. Both halves are silent, and the machine that produces them is ordinary: the installer prints a warning by name whenever it installs somewhere that is not first on PATH. The target now comes from the running executable whenever this process is the shipped binary, and an install with nothing on PATH at all no longer fails with "Could not resolve veyyon binary path in PATH". A source checkout still resolves its launcher through PATH, because bun sits at the executable path there and a checkout updates by advancing itself.
Removed
buildRuleFromMarkdownincapability/rule.ts, a second copy of the rule-frontmatter parser that nothing imported. Every provider, the TTSR CLI and the generated-rule path all go through the one indiscovery/helpers.ts, so the copy's only remaining effect was to look like the place a new frontmatter field belongs: a field added there parses in nothing and the rule silently ships without it.parseRuleConditionAndScope, which helpers.ts does import, stays.
@veyyon/simulations
Added
- New family
src/cache-sim/, which prices a prompt-cache change before anyone makes one. It drives the shipped Anthropic request builder to capture the real wire body and the real breakpoints, then bills the result against a modelled provider cache (longest prefix of the arriving request wins, entries expire on the retention they were written with, published read/write multipliers). Scenarios: every counterfactual arm sends byte-identical content so a delta is never a comparison of two different prompts; a system block that changes each turn is measured against the shipped anchor and against a deeper one; a retention switch is priced across gap lengths and shown to have exactly one crossover; and a rewritten earlier message is shown to forfeit the whole history behind it while rewriting the newest message does not. src/cache-sim/also runs a fleet: several sessions billed against one shared cache, interleaved by simulated time, which is the only shape in which the shipped anchor's justification can be measured. It prices the trade between anchoring the first system block (so a subagent can read the harness its parent cached) and anchoring one block deeper (so a parent with a changing system tail stops re-reading everything in between), and reports the fan-out at which the shallow anchor breaks even. It also shows that an entry is invisible to another session unless its marker carriesscope: "global", which no code path sets, so today that break-even is never reached at any fan-out.- The fleet scenario also prices its own recommendation under an adverse assumption: no published number says what a shared cache write costs, so the modelled cache takes a
globalWritePremiumand the scenario re-runs the switch as if a shared write cost the dearest write in the table. Sharing still wins from one subagent, and scoping every system marker rather than the anchor loses outright, because the deepest system marker sits on a block that changes every turn. src/cache-sim/now covers the implicit-cache surface too, which...
v1.0.48
@veyyon/coding-agent
Fixed
- Publishing a release no longer reports a correct deployment as a failure. The last step of a release asks veyyon.dev whether the new version's card is live, and it looked for
<h2 id="1.0.47">while the site writes<h2 id="v1-0-47">, so it could not pass on any release: 1.0.47 was published, the page was already serving its card, and the run retried twelve times against a site that was right, failed, and filed a release-train issue saying the release had stalled. The anchor has one owner now — the changelog generator exports it and the check imports it — and the check's own tests build every fixture from the real renderer instead of restating the format, which is how both halves came to be wrong together. The link the CLI prints afterveyyon updateand from the rollback picker is pinned to that same anchor, since it ships in the binary and cannot import the site tooling. Nothing in the CLI itself changed in this version.
What changed
4 commits since v1.0.47.
Fixes
- fix(release): look the changelog card up by the id the site writes
Documentation
- docs(changelog): record the release publication-proof fix
Tests
- test(exec): stop measuring fold when the subject is the output sink
Chores
- chore: bump version to v1.0.48
v1.0.47
@veyyon/agent-core
Added
AgentOptions.cacheEnforcement(and the matchingagent.cacheEnforcementaccessor) carries the prompt-cache enforcement level onto every provider request, so a host can decide whether a turn whose cache markers were demonstrably ignored is reported or fails the run. Defaults to the provider's own default, which reports rather than fails.- A partial-completion ledger on any tool batch that is cut short. When a provider stream dies mid-turn (for example an
NGHTTP2_INTERNAL_ERRORreset) or a steering interrupt lands mid-batch, one bounded summary now names every call in the batch asran, ok,ran, failed,started, no result recorded, ornever ran, so the model retries only the dropped calls instead of re-running discovery. It carries ids and outcomes only, never tool output, and is also exposed structurally asbatchLedgeron the placeholder result's details. - The ledger now also names a tool call whose arguments were still streaming when the turn was cut off. Those calls have their
toolCallblock deleted (partial arguments are unsafe to run and an unpairedtool_usebreaks replay), so before this they had no result, no block, and no mention anywhere: the model read a turn in which it had never asked for that tool. Their identity is carried on the newAssistantMessage.incompleteToolCallsand listed asnever ran, arguments never finished, with a line telling the model to reconstruct the arguments rather than copy them back. When EVERY call in the turn was still streaming there is no placeholder result to attach the ledger to, so it is delivered as a turn-level synthetic notice instead. @veyyon/agent-core/compaction/cache-aligned-context: a cache-aligned summarization request that replays the live session's own provider prefix instead of building a fresh one. Local compaction sent a standalone request (a different system prompt, no tools, and the whole conversation re-serialized into one synthesized user message), so it shared zero cached prefix with the session it was compacting, and it fired exactly when that session was largest. It was affordable only because it was lossy:TOOL_RESULT_MAX_CHARStruncates every tool result to 2,000 characters.buildCacheAlignedCompactionContextsends the session's tools, its system prompt, its whole message array byte-for-byte, and one appended instruction turn, which reads as cached prefix and carries the untruncated tool results. Measured on a synthetic 210-message session with a ~192k-token live window: 38,590 fresh input tokens today against 183,262 cache-read plus 457 fresh, which is $0.116 against $0.056 at Anthropic Sonnet rates, so 2.05x cheaper and no longer truncated. It replays the WHOLE array, not the span being discarded, because the message-side cache breakpoints sit on the trailing messages and a request that stops at the cut point diverges before any breakpoint.canUseCacheAlignedCompactionrefuses unless the model row carries the prompt-cache capability, a session system prompt was supplied, and the conversation does not end on an unanswered tool call (appending a user turn there is an invalid request); on every refusal the existing truncated path runs unchanged, because a cache-aligned request that misses costs about three times more than the one it replaced.SummaryOptions.serviceTierandGenerateBranchSummaryOptions.serviceTier, forwarded onto the request bygenerateSummary,generateTurnPrefixSummaryandgenerateBranchSummary. A host resolves a service tier per provider family (serving priority and cost, not reasoning depth) and could put it on a live turn but not on the summary of that turn, which is the largest request a session makes.compact()rebuilds itsSummaryOptionsfield by field, so the new field is restated there too.toolResultNeverRan(details): whether a tool result is a placeholder for a call that never reached the tool. Both placeholder shapes count (__syntheticwithexecuted: false, a call the loop never dispatched, and__skippedwithentered: false, a call an interrupt cut the batch short of) andentered: truedeliberately does not, because that tool was running when the interrupt arrived. It owns its own module (@veyyon/agent-core/tool-result-never-ran, re-exported from the package root as before, so no import changes) rather than living in the agent loop, because compaction reads it too and a pruning pass must not import the loop. Three decisions read it and must not disagree about whether work happened: whether a card may drop the model-facing placeholder text, whether a failed turn is safe to discard and replay, and whether a result counts as a read of the file it names.TOOL_BATCH_LEDGER_HEADLINE_PREFIX, the opening of the sentencerenderToolBatchLedgerwrites. The ledger is a standing instruction with an expiry its own text cannot express, so a host has to recognize a rendered ledger to stop sending one the model has already answered, and the turn-level form of it is a plain user message carrying no structured ledger to match on. The renderer now builds its headline from this constant, so the thing a reader matches and the thing the writer emits cannot drift apart.
Changed
- The three compaction prompts (
compaction-summary,compaction-update-summary,compaction-summary-context) are now oh-my-pi's text verbatim, replacing veyyon's. A test pins each one by SHA-256 so an unapproved edit fails the build rather than degrading summaries silently. Known behavior differences from the veyyon text: the summary prompt no longer states that compaction is in-place with the recent turns retained beside it, and it no longer separates the durable overarching goal from the mutable current task. - A compaction summary now enters the provider request as agent-attributed
usercontent instead of adevelopermessage, so model-generated history cannot outrank live developer policy.
Fixed
- A message the compaction passes just rewrote now measures what it currently holds.
estimateTokenscached its answer per message object on the stated assumption that a stored message is never edited in place, and three passes do exactly that: the shake/dedup elision assigns a placeholder overmessage.content, the overflow prune blanks a result, and the image drop splices blocks out. Object identity survives all three, so every later reader was told the size the message had BEFORE the bytes were removed, for the rest of the session. Since the compaction decision floors the provider's figure with that estimate, an estimate that could not fall meant maintenance could never bring a session back under the trigger: the "the dedup alone fixed it, skip the summarization" path could not fire, the dead-end rescue measured a residual that was already gone, the prune's own cache-warm suffix guard priced a tail it had already blanked, and the operator's context meter reported elided bytes as live. A cached entry is now trusted only while the content it was computed over still has the same shape (the fragment sequence and each fragment's length), which is one walk with no tokenizer in it, so a rewrite is visible on the next read while an unchanged message still never re-tokenizes. - A small-window model can compact again. The output budget a summarization request asks for is part of what the context window has to hold, and it was derived from the absolute
reserveTokensalone: on a 16k-window model the default reserve asked for 13107 output tokens on top of the history being summarized, soestimateCompactionRequestTokenspriced the request above the whole window, candidate admission skipped every candidate as unsendable, and the session never compacted at all. What the operator saw was a context gauge growing past 100% with "Auto-compaction failed: ... holds 16000 tokens and the summary needed 23418" repeating once per turn, and only error-driven overflow recovery doing any work. A reserve that large for the window now falls back to the same proportional reserve the trigger policy already uses (resolveBudgetReserveTokens), so a small window buys a SHORTER summary instead of no summary; the turn-prefix request of a split turn is bounded the same way, and both the estimate used for admission and the request actually sent read one owner so they cannot disagree. A window the reserve genuinely fits in asks for exactly what it asked for before, so nothing changed for a 200k-class model. - A provider that repeats a tool-call id no longer poisons the rest of the session, whether the repeat arrives inside one assistant message or on a later turn. Both calls ran, so the stored turn carried two
tool_useblocks sharing one id and twotool_results pointing at it, which no layer can pair: the outbound canonicalizer maps by original id and collapsed both onto one handle, and the wire form is rejected by every provider that validates the pairing. Since the malformed turn is stored, it replayed on every later request, so one glitched stream ended the conversation rather than one turn. A repeated id is now renamed (<id>_2,<id>_3, ...) where the finished message is assembled, before dispatch and before storage, so each call keeps its own result. Providers that hand out ids from a per-message counter (call_0,chatcmpl-tool-0) hit the cross-turn case on their second tool turn, and the canonicalizer's handle map is keyed by the original id and lives for the whole session, so two such calls collapsed onto one handle however many turns apart they were. An id already taken anywhere on the branch is now renamed too. - An absolute
compaction.thresholdlarger than the running model can reach is now capped at the auto point (the window minus the reserve) instead of one token below the window. A trigger inside the reserve can never fire, because the request that would push the context that high is refused or overflows first: a 256000 threshold on a 200000-token mode...
v1.0.46
@veyyon/agent-core
Breaking Changes
AgentLoopConfig.transformToolCallArgumentsreturns two forms of the arguments,{ execution, display }, instead of one record. The two exist because argument expansions disagree about their audience: a codec handle must be expanded before a person reads it, and a secret placeholder must not be, because the expanded form is a live credential and a display or a session file is exactly where it must never appear. One shared form cannot satisfy both, so the transform states which form each audience gets and the loop routes them.executionreachestool.executeandbeforeToolCall;displayis what is shown, streamed, traced and recorded. A host that returned a single record returns it as both fields to keep the previous behavior.
Added
- Added backward-compatible session-entry sequencing and complete tool-result span capture. Tool results can now preserve millisecond timing, terminal status, batch scheduling, bounded result weight, usefulness, argument fingerprints, and abort state at the detail selected by the host.
Changed
AgentOptions.pruneToolDescriptionsaccepts a per-model resolver as well as the existing boolean. The agent resolves it for main and side requests, so a host can move descriptors between the prompt and native schemas when the active model changes without reconstructing the agent.compaction/compaction.tstakesEffortfrom@veyyon/catalog/effortandwithAuthfrom@veyyon/ai/auth-retry, the modules that declare them, so its@veyyon/aiimport is type-only and the file carries no runtime edge to the barrel at all.- The agent loop and the
Agentclass name the modules that declare the functions they call rather than the@veyyon/aientry point, which re-exports the model catalogue, every provider and the usage backends. Both stream, so both reach the streaming engine either way; what changed is that ten other names stopped arriving with the whole package attached.agent-loop.tswent from 378 modules to 321 andagent.tsfrom 380 to 323, andcompaction/utils.tsfrom 198 to 164 by taking the dialect factory from its own module instead of the dialect barrel. - Importing a span attribute no longer imports a model provider.
telemetry.tsis span vocabulary, and it is used across this package by code that never calls a model, but it also heldinstrumentedCompleteSimple, the one helper in it that runs a completion. That helper names the streaming engine, so an attribute constant cost the provider stack, the model catalogue and the error taxonomy: 281 of the file's 366 modules. The helper moved toinstrumented-complete.tsand the remaining barrel imports were repointed at their owners, takingtelemetry.tsfrom 366 modules to 9 andcompaction/branch-summarization.tsfrom 394 to 333.instrumentedCompleteSimpleis still exported under the same name from the package entry point, which is where callers already took it from. proxy.tstakesEventStreamfrom@veyyon/ai/utils/event-stream, the module that declares it, instead of from the@veyyon/aientry point. That entry point re-exports the streaming engine, every provider, the model catalogue and the usage backends, so a 42-module class was arriving with 363 modules behind it. The proxy went from 364 modules to 118. Its types still come from the barrel, which costs nothing because type imports are erased.- Asking what a message costs no longer loads the machinery that compacts one.
estimateTokenslived in the compaction engine, which reaches 395 modules for the summarizer, the cut-point search and the provider round trip; the estimate needs a tokenizer. It moved tocompaction/token-estimate.tsat 85 modules, and the engine re-exports the name, soshake.tswent from 398 modules to 88 andpruning.tsfrom 398 to 204. The estimate decides when compaction triggers, how pruning spends its budget and what the context meter reads, so keeping it cheap to import is what lets those callers share one implementation. compaction/threshold.tsowns the whole compaction trigger now: theCompactionSettingsshape, the reserve policy (effectiveReserveTokens,resolveBudgetReserveTokens),shouldCompact, and the three threshold wrappers. They were incompaction/compaction.ts, which is the module that RUNS a compaction and therefore imports the@veyyon/aibarrel, the provider dialects, the prompt registry and the tokenizer. Deciding whether a token count is over the trigger needs none of that, so every host that wanted only the trigger paid for the summarizer:@veyyon/coding-agent'sconfig/settings.ts, the module 528 of its test files import, reached@veyyon/ai/stream.tsthrough this one edge.compaction.tsre-exports all of it, so no caller changed.thinking.tstakesEffortfrom@veyyon/catalog/effort, its owner, which imports nothing, instead of from the@veyyon/aibarrel. A six-entry ladder and a clamp were carrying the streaming engine to every consumer ofThinkingLevel.- This package owns the session-entry vocabulary:
SessionEntryBaseand the fourteen entry interfaces over it, plus theSessionEntryunion.@veyyon/coding-agenthad a second copy of all of them and the copies had diverged, soSessionInitEntryhere was missing thespawnsandreadSummarizefields the coding agent writes, andThinkingLevelChangeEntrywas missingconfigured. Those three fields are now on the shared declarations, and a consumer that persists its own entry kinds adds them throughCustomCompactionSessionEntriesrather than redeclaring the union. - The narrowing that answers "does this session entry carry a tool result" lives with the entry union it narrows, as
getToolResultMessageincompaction/entries.ts. Both compaction passes, pruning and shake, had a byte-identical private copy, and a pass that recognised one message shape while its sibling recognised another would prune output the other still counted. - The two compaction strategies now state distinct contracts instead of asking for the same document.
summaryis told it continues in the SAME session, so the recent turns survive alongside it and must not be restated;handoffis told it starts a NEW session where nothing survives, so it must carry cold-restart state (working directory, branch, uncommitted files, toolchain, the exact next command). Both prompts now ask for verification evidence explicitly (commands run verbatim, pass/fail counts, durations, run IDs, exact error text), and the summary prompt states the precedence between brevity and evidence rather than leaving "be concise" one sentence away from "keep the command results". - The handoff prompt gained a
Blockedsection, which only the summary prompt had. Handoff is the strategy whose reader starts cold with nothing but the document, so it is the one that most needs to carry blockers; without the section they had nowhere to go. In practice it now records constraints that cannot be re-derived from the repository at all, such as an action requiring explicit approval or a repository-owner UI step. - Compaction prompts now separate the overarching goal from the current task, in one shape shared by
compaction-summary,compaction-update-summary, andhandoff-document. A single Goal field meant the model wrote whichever goal was most concrete, which is always the immediate task, so the standing objective went unrecorded from the first compaction onward.compaction-update-summaryruns on every later compaction and permits dropping anything no longer relevant; the overarching goal is now carved out of that permission. - Compaction prompts ask for the HEAD commit and whether anything was committed during the session, not just the branch. A branch name does not say where the work started or whether any of it is saved anywhere but the working tree. Repository state stays conditional on the work actually being version controlled, so a session outside a repository is not pushed into inventing one.
- Both compaction strategies now fail loudly when the model returns an empty document instead of accepting it. A provider can return
stopReason: "stop"with output tokens spent entirely on reasoning and no text content;handoffthen returned just the deterministic<files>block, which reads as a real document while carrying no goal, no decisions, and no next step, andsummarywould have stored an empty summary in place of the history it replaces. - Mechanical compaction-request pruning has one owner,
pruneMessagesForCompaction. It previously existed as two inline copies insideserializeConversation, one per rendering branch, so it applied to thesummarystrategy and to nothing else. Both strategies now route through it. Dropping a useless result also drops its pairedtoolCall, so no call is sent without a result; non-text content blocks (images) survive pruning; a byte-identical repeat is collapsed only when the back-reference is shorter than the text it replaces; and stale reads are recognized withreadToolSupersedeKey, which moved tocompaction/utils.tsso the durable pruner and the request pruner share one definition of that rule. - Tool-result truncation is opt-out through
truncateToolResults.summarykeeps it;handoffturns it off, because handoff seeds a new session where a truncated result is evidence deleted rather than shortened. Across two real sessions the lossless passes accounted for 0.0% and 1.6% of message bytes while truncation accounted for 60.4% and 35.3%, so the size win and the data loss are the same pass. generateHandoffacceptsfileOpsand appends the same deterministic<files>block thesummarystrategy has always emitted. The block is machine-generated and byte-identical across models, so withholding it made handoff strictly worse for free.
Fixed
- Compaction and branch summaries now enter the provider request as agent-owned developer context,
not as...
v1.0.37
@veyyon/coding-agent
Added
- Added an eval-only per-section system-prompt override, reachable only through the
VEYYON_EVAL_SYSTEM_PROMPT_SECTIONSenvironment variable (a JSON object of section name to replacement text). It swaps individual banner sections of the default prompt (conventions,role,runtime,toolPolicy,executionWorkflow,deliveryContract) for a benchmark while leaving every other section, and every settings-gated block in it, byte-for-byte untouched. It is deliberately NOT a config key or CLI flag, so noconfig.ymlcan reach it and it cannot contaminate a normal run; when the variable is set,veylogs a loud warning that the prompt is not the production one. Unlike a whole-prompt override it cannot freeze a snapshot that stops responding to settings or silently drop a settings-gated section (for example the delegation block that renders only when delegation is on). Malformed JSON, an unknown section name, a non-string value, a replacement that drops its section banner, and combining the override with a custom whole-prompt template each fail loudly rather than silently.
Fixed
veyyon updatenow updates source installs for real: it fast-forwards the checkout, reinstalls dependencies, and regenerates build artifacts, instead of refusing with advice to rungit pullyourself.- A source checkout missing its generated tool-views bundle (any freshly pulled or cloned checkout) no longer dies at launch with a raw module-resolution error: the launcher regenerates the bundle before starting, and fails with the exact fix command if it cannot.
- The setup wizard now paints its own pure-black ground across the full frame (splash, scene transitions, and outro), so the launch sequence looks the same on every terminal background instead of inheriting the terminal's color.
- The Windows binary is now built as a modern (AVX2) Bun target instead of baseline. Baseline Windows standalone builds crash in the Bun runtime at startup before any Veyyon code runs (oven-sh/bun#32684), which made every published
veyyon-windows-x64.exeexit with a segmentation fault on launch. The modern target requires a CPU with AVX2 (Intel Haswell 2013 / AMD Excavator 2015 or newer). - The inline TUI no longer paints backgrounds by default, so nothing renders as a colored slab on a terminal whose background differs from the theme: the user-message bubble, custom/skill/hook message cards, tool-state tints, the composer band, and the status line all inherit the terminal's own background. The status line's painted bar is still available by turning off the new
statusLine.transparentdefault, and a theme can still declare an explicitcomposerBg. - Every built-in theme is presented again. The temporary alabaster-only picker is reverted along with its
tui.paintGround: alwaysdefault, which repainted the terminal's background color on launch;tui.paintGroundis back toauto(paint only when it cannot produce a visible seam) and the dark/light defaults are back totitanium/light.
@veyyon/hashline
Fixed
REMno longer deletes a file whose content drifted from the section tag. A whole-file delete is now the strictest op about the content tag (it was the most lenient: empty edits took the position-stable path and deleted through drift with only a soft warning), so a stale or fabricated tag can no longer discard edits the model never saw. The delete is refused with a mismatch error that forces a re-read, matching how an anchored edit on a drifted file behaves.MV DESTno longer silently overwrites an existing destination file. A move onto a different existing file is refused during prepare (aborting the whole batch before any write), so a wrong or hallucinated destination can no longer destroy the user's work. A rename that only respells one file (case-only on a case-insensitive volume, or through a symlink) is still allowed, matched by device+inode identity rather than by path string.
What changed
41 commits since v1.0.36.
Fixes
- fix(release): unwedge re-cuts after a dead tag; self-heal the release-train label
- fix(install): generate build artifacts in the source install flow
- fix(update): make veyyon update work for source installs, self-heal the source launcher
- fix(tui): revert ScrollView same-ref fast path — streaming rows mutate arrays in place
- fix(setup-wizard): paint the full-frame black canvas ground per the design Canvas rule
- fix(prompt): use the shared isRecord guard in the eval-sections parser
- fix(release): ship the Windows binary as a modern (AVX2) Bun target
- fix(lint): drop the unused theme import left by the hook-message bg removal
- fix(tui): paint no backgrounds by default; revert the alabaster-only stopgap
- fix(changelog): merge duplicate version sections in the changelog fixer
Performance
- perf(tui): skip ScrollView setLines copy on identical array reference
Tests
- test(tui): recover ghostty-web OOM traps via compacted-state replay
- test(tui): engine-mirrored shadow commit ledger, saturation-aware oracles, pinned regression replays
- test(tui): mirror the cursor-tail re-anchor in the render-stress shadow window
- test(update): e2e suite for the installRelease self-update pipeline
- test(edit): fix withTempDir race that deleted the temp dir before the async read
Build & CI
- ci: smoke the real source launcher under a PTY with the generated artifact removed
- ci: alert on a failed release cut, not only a failed publish
- ci: gate release tagging on green CI, alert on red release runs, un-cancel release gates
Chores
- chore: bump version to 1.0.37
- chore(changelog): dedup released bullets and reslot 1.0.37 after the merge
Other changes
- deepswe-bench: count each tool call once, not once per call and once per result
- evals skill: document treatment-applied proof, efficiency section, refusal asymmetry
- deepswe-bench: label an unreported efficiency metric "not measured", not "equal"
- deepswe-bench: detect encode in tool calls, surface error reasons per arm
- deepswe-bench: measure argot's real claim — paired efficiency + treatment-applied
- deepswe-bench: stamp per-arm input fingerprints into results.json
- deepswe-bench: document the paired arm comparison in README + evals skill
- deepswe-bench: add a paired arm-vs-arm comparison with an exact sign test
- deepswe-bench: pin + stamp the sampling temperature for every arm
- deepswe-bench: report Wilson 95% CI instead of degenerate binomial SE
- deepswe-bench: make --limit a representative even-stride sample, not the biased alphabetical head
- hashline: refuse REM when the file drifted from its tag instead of deleting unseen content
- hashline: refuse MV onto an existing different file instead of silently destroying it
- argot: surface unreadable dict-corpus files instead of dropping them silently
- argot: surface the non-git tree walk's silent truncations (Law 10)
- deepswe-bench: make the job-name round-trip testable and test it
- deepswe-bench: track the argot-setting-only and candidate-argot-nudge arms
- deepswe-bench: scientifically sound eval set (repeats, treatment guard, single-IV docs)
- argot: make the preamble invite adoption, not just describe the notation
- deepswe-bench: make per-section prompt override eval-only and uncontaminatable
Full Changelog: v1.0.36...v1.0.37
v1.0.36
@veyyon/ai
Fixed
- Fixed tool-call arguments for a
string | numberfield being coerced to a number when the model sent a quoted numeric string (for example"123"), which changed the argument's type and lost data such as leading zeros ("007"became7). A value that already satisfies its schema is now left unchanged; the numeric-string coercion still applies tonumber | nullfields, where the raw string matches neither branch.
@veyyon/coding-agent
Changed
veyyon updatenow printsChecksum verifiedafter it validates a downloaded binary against its published.sha256sidecar, so you can see the integrity check ran and passed rather than only hearing about it when it fails. This matches theverified sha256confirmation thecurlinstaller already prints. The automatic startup update stays silent to avoid corrupting the session UI.
Fixed
- Error messages no longer show a doubled
Error:prefix. A failure while adding, removing, updating, installing, uninstalling, linking, or toggling a plugin or marketplace, applying a personality, or changing the Mermaid rendering setting now readsFailed to …: <reason>instead ofFailed to …: Error: <reason>. veyyon update --check --forceon an already-up-to-date install now reportsUp to date at X; --force would reinstall itinstead of printingForcing reinstall of Xand then exiting without reinstalling anything. Check mode installs nothing, so the message now matches what the command actually does.- A failed release-binary download now reports the URL, the HTTP status, the requested version, and the missing asset instead of the bare
Download failed: Not Found. A 404 explains that the version may not exist or its build for your platform was not published and points atveyyon update --check; a 403/429 gives the rate-limit retry hint. This most helps installing a specific older version, where a mistyped or unpublished version previously failed with no clue what went wrong. - A
keybindings.yml/.jsonthat parses cleanly but is not a mapping (a top-level sequence or a bare scalar) is now quarantined and left at defaults instead of silently corrupting the user's map. Such a file previously reduced a scalar to an empty map and turned a sequence into bogus index-keyed bindings, which the migration writer then persisted over the original file. A blank or comments-only file still loads as an empty config with no complaint. - A settings file that parses cleanly but is not a mapping (a top-level YAML sequence, a bare scalar, or a string) is now preserved and reported instead of silently discarded. The loader previously collapsed any non-mapping root to an empty config with no signal, so a mis-edited settings file erased the user's whole configuration invisibly. Such a file is now quarantined and surfaced through
quarantinedFiles, exactly like an unparseable one, while a blank or comments-only file stays silent as a legitimately empty config. - The
apply_patchdefault filesystem now commits crash-atomically. The interactive editor already wrote through the crash-atomic LSP path, but the default filesystem behind programmatic and SDKapply_patchcallers still used a truncate-then-streamBun.write, so a crash mid-write could leave the target file truncated. Create, update, and move writes through the default now write a sibling temp and rename it over the target, preserving an existing file's permission bits. - A file move that overwrites an existing destination is now crash-atomic and mode-preserving, matching the edit/write path. The destination was previously written with a truncate-then-stream
Bun.write, so a crash mid-move could corrupt the file being overwritten; it now writes a sibling temp and renames it over the destination, carrying the destination's permission bits forward. - Edits and writes now commit crash-atomically. The file was previously written with a truncate-then-stream
Bun.write, so a crash,SIGINT, out-of-memory kill, or full disk mid-write could leave your source file truncated or empty. It now writes a sibling temp file and renames it over the target, so an interrupted write leaves either the whole old file or the whole new one. The existing file's permission bits (including a script's executable bit) are preserved across the write, and a write through a symlink keeps the symlink and updates its target. - The CLI no longer hangs while printing a fatal error whose cause chain forms a cycle. A wrapped error whose
causepointed back at itself (directly or through another error) made the cause walk loop forever; it now stops at the first repeat and notes the circular reference. - The Windows installer no longer destroys local edits in the source checkout. A source update ran
git reset --hard, and uninstall deleted the checkout outright, so local edits under~/.veyyon/src(an editedAGENTS.md) were lost. It now commits any local changes to aveyyon-local-<timestamp>branch before updating, moves an existing tree aside to<dir>.bak-<timestamp>instead of deleting it before a fresh clone, and refuses to delete a checkout that holds unpushed work on uninstall. This matches the protection the POSIX installer already had.
Security
- The self-updater now verifies a downloaded release binary against its published
.sha256sidecar before installing it, the same fail-closed integrity gate thecurland PowerShell installers already enforce. Previouslyveyyon updateand the automatic startup update downloaded and swapped the binary with only a post-install--versioncheck, which catches a wrong-version binary but not a corrupted or tampered same-version one. A missing, unparseable, or mismatched checksum now aborts the update and removes the partial download instead of installing something unverified.
@veyyon/hashline
Fixed
NodeFilesystem(the shipped disk-backed default) now writes crash-atomically.writeTextand the content form ofmovestream into a sibling temp file and rename it over the target, so a process death mid-write (SIGINT, OOM kill, full disk, power loss) leaves the user's source file whole rather than truncated. An existing file's permission bits and a symlinked target are preserved.
@veyyon/mnemopi
Fixed
BankManager.renameBanknow validates the source bank name, not only the destination. Every other bank operation rejects a name containing a path separator or.., but rename validated only the new name, so a source name like../outsideescaped the bank store and silently moved an out-of-tree directory into it. Both names are now validated before any filesystem change.- Content-addressed blob storage now writes crash-atomically.
storeBlobwrote the extracted blob straight to its finalsha256-named path withwriteFileSync, so a crash mid-write left a truncated file whose bytes no longer matched its name; theexistsSyncfast-path then treated that corrupt blob as present forever and every reader silently got wrong bytes. The write now goes through a sibling temp and rename, so a blob is always either absent or the exact correct bytes. - The one-time legacy triples-database migration now copies crash-atomically. It wrote the old database into its new location with
copyFileSync, which streams bytes into the destination, so a crash mid-copy left a truncated SQLite file that theexistsSyncguard then treated as a completed migration, silently losing the triple store. The copy now goes through a sibling temp and rename, so the destination only ever appears as the whole, valid database.
@veyyon/utils
Added
- Added
atomicWriteFilePreservingMode: an atomic write that carries the target file's current permission bits forward instead of stamping the0o600default (a new file gets0o644). Use it when overwriting an existing file whose mode must not change, such as a source file an editor rewrites or a script that must stay executable. - Added
splitReadSelector,stripReadSelector, and theREAD_SELECTOR_RANGE_LIST_SRCgrammar fragment: the one shared owner of the read-tool path-selector grammar (file.ts:50-200,:raw,:conflicts, andrange:rawcompounds). This grammar was previously hand-duplicated across packages with "keep in sync" comments; consolidating it here removes the drift risk.
What changed
41 commits since v1.0.27.
Features
- feat(update): confirm 'Checksum verified' on a successful self-update
Fixes
- fix(errors): remove remaining doubled 'Error:' prefixes; generalize the source-lock
- fix(plugin-cli): stop doubling the 'Error:' prefix in plugin command errors
- fix(update): honest --check --force message when already up to date
- fix(update): verify self-update binary checksum, fail closed (parity with installers)
- fix(update): rich release-binary download-failure message
- fix(settings-test): drop wrong SettingPath cast that broke the release typecheck
- fix(mnemopi): validate the source name in renameBank to close a traversal
- fix(keybindings): quarantine wrong-shape keybindings files instead of corrupting the map
- fix(settings): quarantine wrong-shape settings files instead of dropping them silently
- fix(hashline): crash-atomic NodeFilesystem writes
- fix(mnemopi): migrate the legacy triples database crash-atomically
- fix(mnemopi): write content-addressed blobs crash-atomically
- fix(coding-agent): make apply_patch default filesystem crash-atomic
- fix(coding-agent): make file moves crash-atomic; hoist mode-preserving atomic write
- fix(coding-agent): commit edits and writes crash-atomically
- fix(install): remove a partial binary download on failure (Windows)
- fix(cli): guard fatal-error cause walk against circular cause chains
- fix(ai): stop over-coercing string|number tool args to numbers
- fix(install): preserve local src edits on Windows update/uninstall (parity with install.sh)
Refactors
- refactor(slash-commands): drop unnecessary 'as SettingPa...