namzu.ai
Namzu changelog · the kernel and every provider package

Every release, cut in the open.

The canonical version history of @namzu/sdk and every first-party provider package — the kernel, providers, and the computer-use surface. Releases are cut from cogitave/namzu and refreshed hourly.

@namzu/sdk

17 releases

  1. v50.1.0

    Minor Changes

    • —

      4a6ec17: Refuse to claim strict tool input generation where the wire would refuse the request.

      edit and write set enforceModelInput, and the Zen driver answered that with strict: true for both on the chat-class wires. The Zen free-tier gateway enforces the strict subset's structural rule itself — every object that declares properties must list all of them in required — and refuses the WHOLE request when one tool fails, so edit's optional fields (a find-and-replace OR an insertion) took every other tool in the call down with it. cat-jam-free is the model that enforces; the symptom was Invalid schema for function 'edit'.

      • —@namzu/sdk gains acceptsStrictSchema(schema), the wider question a driver asks before claiming strict: true on one request: the keyword deny-list that findStrictSchemaViolations already runs, plus the structural rules, plus a guard for a tool that declared no schema at all (the deny-list walks nothing in undefined and finds nothing). It is additive; findStrictSchemaViolations and assertStrictSchema keep the registration-time job they had.
      • —@namzu/zen now sends strict: acceptsStrictSchema(...) for a tool named in enforceToolInputSchema, so write keeps the guarantee and edit sends strict: false. Responses still sends strict: false for every tool, as it did. Nothing about a schema changes, and Namzu validates tool inputs before execution either way.
      • —edit's insertLine union branch { const: 'end' } states no type, which the strict dialect has no spelling for; it is now { type: 'string', const: 'end' }. The validator reports any union branch that never states a type.

      What breaks. A caller that relied on Zen sending strict: true for edit loses constrained decoding for that one tool on the chat-class wires. To keep it, the schema would have to require every field and make the optional ones nullable, which changes what edit accepts on every provider.

  2. v50.0.0

    Major Changes

    • —

      efb7b06: Extend the closed AcpSessionUpdate output union with agent_thought reasoning boundaries and agent_message completed messages. Exhaustive consumers must add handlers for both kinds before upgrading. Existing variants and required fields remain available. Reasoning boundaries contain no private replay payload; readable reasoning still arrives through agent_thought_chunk.

      Message chunks carry optional actual message/turn identity and public commentary/final-answer metadata. Completed messages provide selected settled content and ordered public text parts. turn_ended can provide the authoritative result and its actual answer-message identity; consumers must replace the preview when a result is present, including an empty result that clears blocked output.

      Keep the coarse AcpStopReason union and add optional exact reason to terminal updates and prompt responses. Current runtime guardrails/refusals map to refused, resource limits to max_turns, and parked segments retain reason: 'paused' under the older coarse cancellation label. Legacy aliases remain accepted. Prompt preparation cancellation returns an explicit cancellation reason even if no runtime event was produced. Streamed updates are sent in admission order and finish before the prompt response. Tool completion retains optional runtime-measured duration.

      Permission questions wait for already-admitted updates and fail closed when that context was not delivered. Update errors are caught immediately, retain the first failure and release the prompt slot after flushing; failed question sends settle their pending wait. Delivery failure does not undo runtime work already recorded.

      Update the CLI session-isolation regression expectations to include the exact cancellation and completion reasons; the CLI runtime is unchanged.

    • —

      0f5aa96: Ready-computer Pal prompts now default to general application work guidance: understand references and acceptance criteria, make reversible changes, observe and correct the actual result, and validate saved files and requested exports before delivery. SDK hosts that need the previous prompt behavior can pass workGuidance: 'basic' to buildPalSystemPrompt. This guidance shapes model behavior; it does not certify semantic correctness.

      The SDK read tool now refuses real PNG, JPEG and WebP bytes as text. Mount the optional view_image / createViewImageTool for saved-image inspection, or use a format-aware binary reader when pixels are not the desired result. getBuiltinTools() retains its previous tool roster. Saved artifact images provide no GUI screenshot authority.

      The CLI and desktop Pal runtime now explicitly mount view_image and import_reference_images. Image import is a reviewed file-write operation using only the current operator input and admitted guest. Original image bytes and manifests survive durable review; current plan, pause and control guards remain in force. Ordinary chat and unavailable-computer sessions do not acquire guest tool access.

      The local Pal sandbox adds authenticated, acknowledged bounded byte reads. Rebuild the local Pal image from this release to inspect saved images; an older worker fails explicitly before file contents are requested. Existing whole-file requests and generic worker defaults are unchanged.

      The Pal image also includes the standard file utility for format checks. This reports file type; it does not establish application or visual quality.

    • —

      6ff961f: Add an owner-conversation source to the shared Pal input ledger, so an ordinary conversation (not a Pal) can leave a message in a Pal's durable inbox. PalOperatorMessageBroker and createPalOperatorMessagingTools (list_pals and an always-approved send_pal_message that takes only palId and body) are new; the host fixes the tenant and the executing call supplies the conversation, so the model cannot choose or forge the sender, and the source carries no Pal address. Acceptance is a receipt, never delivery or an answer, and the existing Pal-to-Pal rows, route hashes, IDs and receipt namespaces are unchanged.

      Why this is a major release: the exported unions PalIngressIntent, PalIngressInboxMessage, PalIngressRouteKey, PalIngressAuthorizationRequest and the acceptIngress/readIngress results gain an operator member (kind: 'operator', source operator-conversation), and ingressMessageRef can return the new namzu-pal-operator/1 namespace. TypeScript code that narrows these unions with kind === 'observation' ? … : <channel fields> no longer compiles; handle kind === 'operator' (its source is { kind: 'operator-conversation', tenantId, sessionId }) before reading channel fields. A custom authorize callback must also return a decision for an operator request: allow accept and deliver only for a host that has already shown the owner a review, and refuse wake unless the owner started the Pal. The public RuntimeContextMessageKind union is not widened: a delivery is recorded as peer-message, and its rendered envelope names the owner's conversation and says it is untrusted context, not an approval.

      An older SDK process sharing a communication store that holds an owner-conversation message fails to read that recipient's record rather than overwriting it; stop older processes before sharing the path.

    • —

      53aed2a: Add a shared durable Pal input ledger for existing Pal messages, closed host activity facts and authenticated channel messages. Generic intake and finite dispatch retain exact original-log receipts, current authorization and one unresolved recipient claim across all sources. Existing Pal-only APIs, row shapes and message IDs remain supported; their writes preserve the new input families.

      The public RuntimeContextMessageKind output union and RUNTIME_CONTEXT_MESSAGE_KINDS catalogue now include host-observation and channel-message. Consumers that exhaustively handle runtime context must add those cases when upgrading. Treat both as untrusted context; neither grants approval or operator authority. This output-type expansion requires a major SDK release even though the new intake APIs are additive.

      Persisted communication and operation records upgrade to schema version 2. Stop older SDK processes before sharing these store paths; older binaries refuse the new records instead of overwriting mixed input state. Observation and channel messages remain untrusted runtime context, never operator approval or permission grants.

    • —

      f7f48f9: Add the explicit cmd ShellDialect and change the native Windows host-shell metadata default from sh to cmd. The physical command shell remains Node's platform shell (normally CMD); no PowerShell or WSL switch occurs. Update exhaustive dialect handling to include cmd and pass the actual value to authorization helpers.

      CMD lines are conservatively opaque until a matching parser exists. Command-specific allow rules and skill patterns no longer pre-approve native CMD commands. The existing unknown-program escalation still requires exact-call review, even when the gate has a whole-tool allowance. Use that review or explicitly select a POSIX interpreter rather than incorrectly labelling CMD as sh. Sandboxed Linux guests retain sh.

      SDK query prompts disclose the active command tools' host or guest execution dialect, including minimal and cached prompts. Explicit Windows bash.exe and sh.exe overrides retain their matching POSIX dialects.

      Native Windows CLI model shell calls inherit the exact-call review requirement. The scheduled-run floor refuses CMD commands rather than checking them with POSIX quoting; confirmed script jobs still accept only explicit bash/sh interpreters. Use an installed POSIX interpreter where appropriate or run reviewed interactive CMD calls.

    Minor Changes

    • —

      dd7ee4b: A failed model request on a turn that has no token limit anywhere above it no longer blocks that turn's next request. Before, an unanswered request (a 502, a dropped stream) left the account "in flight" and unresolved, so the same turn could never ask again and a host could only abandon it. The request still stays recorded as unknown spend, never as zero, and a late usage frame for it is still added.

      A turn with any finite limit (its own, or on an account above it) behaves exactly as before: unknown spend still refuses the next request, because the unknown amount may already exceed the limit. SessionTokenBudget.bounded reports which of the two an account is. If you depend on the old refusal for an unlimited turn, check summary().unresolvedRequests yourself; nothing else changes for limited turns.

    • —

      4314f90: A tool call a person declined at review is now recorded as declined by them, with what they said, so a host that reopens the conversation can show it. Nothing you already send or receive changes shape, and no code needs to change to upgrade.

      New and optional: ToolReviewAnswer's reject, AcpPermissionOutcome's reject and the reject_tools decision take declined?: { note?: string }; an ACP client's reject answer takes the same field; and a generic ToolCallView result carries declined?: { note?: string }. A host that sets declined on its reject gets, on that call's tool_completed, a view whose label is the call's target (path or command) and whose note is the person's words, cut at 4,000 characters. A host that does not set it, a call the authorization gate refused even when the person rejects the rest of its batch, and every other refusal by policy (strict or plan mode, no one to ask, the authorization gate, a repeated failure), is recorded exactly as before: an error result with no presentation. The text the model reads is unchanged. ToolExecutor.executeBatch takes an optional fifth argument for the same purpose.

      The CLI's ACP server passes the field through, so a client such as Namzu Desktop that reports "the person said no" now shows "Declined" after a reload instead of "details unavailable". The terminal UI does not report it yet, so a call declined there still reloads as before.

    • —

      b8852a1: abandonTurn takes a new option, withdrawMessages. Pass it when you give the turn's prompt back to a person to send again (a message box restored after "continue without this reply"): the abandoned turn then contributes no message to the next turn's context, so the provider receives the re-sent prompt once instead of twice. The records stay in the log; the closing turn_failed carries failure.details.messagesWithdrawn: true, which the fold honours. Nothing changes unless you pass it: an abandoned turn's messages stay in the context by default. SessionLog.abandonTurn accepts the same option as a fourth argument; a custom SessionLog that wraps another should forward it.

      The CLI's namzu/sessions/abandon-paused passes it, which fixes Desktop's Continue without this reply sending the person's message to the provider twice.

    • —

      7f5ac50: Expose explicit paused-turn recovery through optional AcpAgentGateway.retry and ACPServer.retrySession. Hosts retain the existing ordered update, review, cancellation and single active execution owner without inventing a new prompt.

      The desktop CLI host advertises scoped retry-status and retry methods. It resumes only the exact verified checkpoint of a classified retryable provider pause with resolved original accounting. Human decision holds and uncertain provider usage remain blocked, including unlimited turns; this feature never resets a ledger or abandons an active turn. Same-process retries preserve captured approval/effort and the recorded model. After reconnection, default Retry is unavailable when the original approval settings cannot be verified; a deliberate host retry must explicitly select a permission mode in ordinary conversations. Pal recovery also pins the authenticated original computer generation and environment identity before sending, and rechecks them at resumed provider/tool entries. A cold, originally offline, replaced or unavailable Pal computer lifetime cannot be substituted with the currently ready computer. Pal ownership and current computer-control guards remain required.

    • —

      56d88dc: Add optional namzu/tasks/update planning notifications, exported AcpTask and AcpTaskUpdate shapes, and ACP_TASK_CAPABILITY. Hosts enable supportsTaskNotifications and clients declare namzu/tasks; existing core updates and default wire behavior remain unchanged. Notifications preserve planning IDs, failed status, dependency and owner clears, and deletion without forwarding private task descriptions or metadata.

    • —

      3727e2b: Add the read-only ACPServer.getSessionCwd(sessionId) lookup for the exact workspace of a session published on the current connection. Reserved loads, unknown identities and stopped servers return undefined. Hosts can authorize new-session model preparation before the first durable turn without accepting an arbitrary client-provided session ID. Folder trust and durable/Pal ownership checks remain the host's responsibility.

    • —

      84452ec: Add ToolManager.prepareExecutionAsync(name, rawInput, signal?) and use it for direct, nested and structured tool calls. Asynchronous refinements and JSON-safe transforms now prepare one normalized input for authorization, review and execution. Execution retries reuse that input; actual rewrites are revalidated. The existing synchronous preparation API remains unchanged.

      Cancellation ends preparation without admitting a late result. Schema callbacks remain trusted host code: keep external work free of effects or cooperative with host cancellation. Non-JSON host inputs must not be mutated while preparation is pending.

      Observation deduplication no longer replays historical schema callbacks. It uses current executor evidence tied to the actual successful call and exact result. Historical or recovered results without this proof remain full, so a new turn or resume may retain more context under the existing compaction limits.

    • —

      53aed2a: Add authenticated Pal channel ingress with captured host connections, a fresh actor per event, immutable native conversation targets, and shared durable inbox delivery. Recorded reply and action routing verifies the original owned conversation receipt and current authority; it does not send remote messages automatically. Hosts must provide a trusted event verifier and explicit receive, wake, reply and action policy. The CLI includes a private local HMAC fixture adapter and a bridge to exact native parked tool-review actions; other channel actions remain unsupported.

    • —

      6194bda: Observe a background server's output without waiting for it to exit. BackgroundJobRegistry.waitForOutput and the optional BackgroundJobRegistryRef.waitForOutput match a bounded UTF-8 literal on stdout, stderr or either pipe independently, preserve combined byte cursors, and report match, exit, stop, timeout or cancellation outcomes with explicit retention gaps.

      Set output_contains and optional output_stream on wait_for_job for one readiness observation. This mode never marks the process as work that must hold a finishing turn open. Omitting the condition preserves the existing exit wait. Custom hosts must implement the optional output-observation method to support readiness; unsupported hosts refuse it rather than waiting for exit. Observers have per-owner and registry limits and are cleaned up on every outcome. Process output remains evidence, not a guarantee of service health.

      Byte caps and cursors now skip partial UTF-8 code points and report those skipped bytes instead of introducing replacement characters into the retained output.

    • —

      301f5b3: Add opt-in structuredResultSpilling: true to built-in session logs. Large screened structured tool candidates and accepted final JSON can use separate checked spill references while their records remain within the 4 MiB ceiling. The default keeps existing inline records; each spilled JSON body is limited to 16 MiB. Live events and returned turns still carry the full value.

      Use readStructuredOutput on a completed record from a verified log read to restore accepted JSON. Completed-call recovery hydrates only the latest selected completion and refuses missing or corrupt evidence without replay. readSpill and spill stores accept optional byte limits and cancellation; built-in bounded reads verify actual size, UTF-8, regular files and hashes. Custom backends remain responsible for their own I/O limits. This does not restore a pre-crash review decision or introduce general tool-data artifacts.

    • —

      6194bda: Add resumeSessionId to local child-task admission and expose the admitted conversation as TaskHandle.childSessionId. A follow-up keeps the child's conversation and history while creating a fresh task, parent-turn attribution, cancellation channel, budget reservation and current configuration. Existing terminal task handles remain unchanged. Continuation requires a retained capability in the same manager and an unchanged registered definition; replayed history alone cannot authorize execution, and isolated workspaces are refused.

      Existing builders supplying a custom log for another session keep their first-invocation behavior. Such a log does not create continuation authority for the admitted child, so a later follow-up is refused instead of replaying unrelated history.

      Add optional invocation identifiers to immutable summaries: SessionSummaryRef.turnRef, SessionSummaryMaterializer.materialize({ turnId }), and SessionStore.getSummary(sessionId, tenantId, turnId). Built-in stores seal follow-up summaries independently without replacing the original conversation summary. Custom session stores enabling continuation must declare supportsInvocationSummaries: true and implement this optional invocation key; their existing calls without a turn id retain their behavior.

      Continuation also requires supportsOwnerVersionCas: true: custom stores must honor the optional expected ownership version on updateSession and recordSummary. Admission, rollback and pre-invocation checks refuse changed ownership, and materialize({ expectedOwnerVersion }) prevents an old invocation from idling a new owner during completion. Built-in disk stores serialize these writes within a process; this does not establish a cross-process lease. Calls omitting the version preserve their existing behavior.

    • —

      56d88dc: Add DiskTaskStore.listStrict() for hosts replacing a complete task projection. It rejects unreadable directories and corrupt records rather than silently presenting an empty or partial list. A session without a task directory still returns an empty list; existing list() retains its tolerant behavior.

    • —

      f7f48f9: Add optional terminate(signal?): Promise<void> to SandboxDetachedProcess and background JobProcess for providers that must confirm a remote process tree stopped. The registry waits for this confirmation before reporting termination. A failed confirmation rejects the stop call and retains the owned running job with optional recoveryRequired and a safe stopError, so the host can retry recovery without forgetting live work. Providers that use the existing synchronous kill method retain their current behavior.

    • —

      9ef6506: Expose live tool progress and actual turn failure messages through optional fields on existing ACP update variants. Add explicit optional namzu/* ACP host extensions and advertise their method names. Gateway history loading now receives the requested workspace as an optional second argument; existing one-argument gateways remain compatible.

      The CLI now loads durable, project/tenant-scoped ACP conversations and refuses archived writers. namzu acp --desktop enables operator methods for folder trust, scoped conversation/history, safe provider metadata and background jobs. Ordinary ACP connections retain the core method set. The private desktop preview uses this existing runtime and log rather than a second conversation store.

    • —

      56e846c: Add optional inline user attachments to ACP prompt requests. Hosts opt into delivery with supportsPromptAttachments: true; initialization then advertises promptAttachments: true. Clients can check this capability before sending bytes, and gateways receive the validated attachment payload alongside the prompt. Stored attachment references are refused across this boundary; inline payloads are limited to eight attachments and 3 MiB of decoded bytes per message.

      The CLI opts in and preserves image/document attachments in the actual user message and settled conversation history. Existing plain text requests keep their behavior. The private desktop admits native file picks or dropped/pasted bytes as bounded images and UTF-8 text, keeps draft and queue ownership, and refuses image submission to an older CLI rather than silently dropping it.

      New image/document inputs are refused before send when the selected live provider explicitly declares that it cannot receive them. Desktop files remain available for retry with a suitable model. Attachment/settings-only drafts survive a connection rebuild, and queue editing restores captured options together with its text and files. Pre-turn CLI errors retain their actual diagnostic rather than becoming a generic failed turn.

      ACP also accepts optional AcpPromptOptions with a reasoning effort and tool review mode. Hosts opt in with supportsPromptOptions: true, advertised as promptOptions: true; unsupported or malformed explicit settings are rejected before a turn starts. The CLI validates reasoning effort against the actual selected runtime's supported menu and applies the captured review mode to that turn.

    • —

      2057fd6: Surface provider-hosted web search and fetch activity through existing ACP tool updates, using qualified IDs and only provider-reported state and bounded public captions. These display receipts do not grant local execution or approval.

      Desktop conversation history adds optional validated journal message IDs and timestamps for messages and work, and restores saved hosted search receipts. Missing timing and unfinished operations remain explicit; reopening history does not invent a current clock or a successful outcome. Existing message text and execution authority are retained.

    • —

      6f46da3: Add an optional durableInbound source to query, runAgent and query-backed agent configuration. Deliveries use ordinary session-log message records and await both their append queue and the host's exact acknowledgement before inference. An explicit sessionLog is required for this optional source.

      Export durable claim/reference/receipt contracts and bounded reference validation. Runtime context can retain an optional delivery reference without acquiring operator authority. Existing initial input, synchronous queues and outstanding-work holds retain their defaults.

    • —

      be9f8b6: Add opt-in structuredOutput.toolResultRetention: 'durable' for tool mode. Retain screened, post-hook JSON independently of capped tool previews, so larger valid candidates can reach review and final structured settlement. The default receipt behavior is unchanged; native mode rejects the durable tool option.

      The full JSON is an additional host-visible tool_completed.structuredResultJson field and a verified completion recovery field, not provider-message content or proof of acceptance. Guardrails, hooks, skips, cancellation and the session record size ceiling still apply. Resume preserves execution evidence without automatically accepting a pre-crash candidate or replaying its tool.

    • —

      24cb6b4: Add createHarnessSession and host-composed external engine contracts. External sessions record an immutable engine/profile/native-session/cwd binding, authored prompts before dispatch, native item identities and exact review decisions under the existing session writer lease. Admission is required before native execution or approval; interrupted connections require matching native history and never automatically resend prompts. Existing Namzu query, provider and tool execution APIs and defaults are unchanged. New optional harness payloads use the existing session record kinds.

      New external-bound journals cannot be continued through kernel query, resumeSession or TurnRecorder.open; use their recorded harness adapter or a separate Namzu session. Reading their owned history remains available.

    • —

      d5030ca: On Windows, bash with run_in_background now runs the command: before, the job registry spawned /bin/sh, which does not exist there, so every background job ended within milliseconds with no output and no explanation. Background jobs, foreground bash calls and plugin shell hooks now share one spawn (spawnHostShell, newly exported). A command that cannot be started is reported as "Could not start the command: <reason>" (and as failed to start by job and wait_for_job, with an error field) rather than as a plain exit, and Windows console output is decoded in its OEM code page instead of garbled as UTF-8. On Windows the command now runs under UTF-8 (an outer cmd sets code page 65001 and starts the real one with the command in the environment), so letters the OEM page lacks, such as ğ and ş, are no longer flattened to g and s, and a missing working directory is reported as such. Nothing to change on upgrade; BackgroundJob, BackgroundJobOutput and the job tool results gain an optional error.

    • —

      6f46da3: Add durable Pal addresses, owned conversation routes, incoming message storage, captured senders, explicit directed consent and executor-correlated messaging tools. Acceptance is separate from verified session-log delivery and recipient execution; unresolved claims are retained for recovery without repeating effects.

      CLI and desktop Pal sessions use the same SDK messaging tools and durable query input. Add pal grant/revoke/inbox/dispatch: wake consent is explicit and finite dispatch uses preapproved tools. No external transport, automatic listener or Pal Team membership is installed by this feature.

    • —

      56d88dc: Add CompletionInbox.deferDelivery(taskId, settlement) and drainAsync(signal?) so hosts can await an owned task's tracking writes before delivering its result. The existing synchronous drain() keeps pending deferred results queued and reports rejected writes without consuming results. Cancelling an asynchronous drain releases its waiter while retaining the tracking promise and result. Query notification waits respect the caller's existing cancellation/deadline, and finalizers and outstanding-work holds never wait again on pending tracking.

      Coordinator background workers and abandoned foreground waits now settle their linked planning task and original approved plan step from the actual worker outcome exactly once. Completion notifications and explicit waits await that settlement. Replacing the active plan cannot redirect an old worker's outcome to a reused step ID. Direct background launches without a completion channel are refused before starting a worker.

      Rejected scheduler admission also fails the linked planning record and original plan step rather than leaving them running. The original admission error is retained, an uncertain worker dispatch is not represented as absent, and failed tracking persistence reports both errors without retrying the worker.

    • —

      7f5ac50: Add optional MCPClientConfig.transportFactory for caller-owned transports, preserving built-in transport configurations and the existing MCP protocol lifecycle without launching a host subprocess as fallback.

      Add optional Sandbox.openStdio with allocation-owned interactive guest pipes, byte streaming, confirmed process-group shutdown and explicit per-request device operation barriers. Idle services permit operator takeover; an unknown issued application operation permanently fences the allocation until computer stop and cannot be cleared by a late response or closing an external application's MCP server. The local Pal worker must be rebuilt alongside the runtime to advertise interactive support. Older workers refuse this capability before spawning; ordinary foreground, background, file and desktop APIs remain available.

      Keep quiet interactive services alive with bounded worker heartbeat frames, discarded by the matching runtime before application output and without changing request ownership or uncertain-effect barriers. Rebuild the worker and load the matching runtime together for the interactive protocol.

    • —

      6f46da3: Add PalRuntime.onLifecycle for live computer and admission observations, with unsubscribe and isolated listener failures. Add createPalActivitySource for authorized, bounded pages of approved metadata from an owned original Pal journal. No private message, tool argument, result or error body is exposed in activity facts.

      Activity cursors must remain unchanged previously emitted outputs stored by a trusted host. Their scope hash binds ownership but does not authenticate a client cursor, and previously consumed prefixes are not rescanned. Hosts must resolve stored cursors rather than accept model, renderer or remote anchors directly.

      These observation APIs do not grant execution, approve actions or wake Pals.

    • —

      5e2fb3a: DiskPalActivitySubscriptionStore.list() is an additive trusted-host read of frozen latest subscription records, including disabled subscriptions. A missing root returns an empty list, while malformed or unreadable committed records and aliased directories reject the complete projection. Hosts must still enforce tenant and participant access before exposing metadata.

      Custom PalActivitySubscriptionStore implementations do not acquire a new required method, and existing create, get, consent and progress behavior is unchanged.

    • —

      84a9472: Add optional provider-owned Pal computer takeover through PalComputerControl, PalComputerInput and runtime control methods. Hosts can transfer an idle guest to human input using its exact generation, return it without waking the Pal, and observe current control authority. Providers without the optional port retain normal Pal execution and explicitly refuse takeover.

      The runtime serializes control operations, refuses new Pal admissions during operator control, and requires each subsequent admission to obtain its own fresh screenshot before GUI input. CLI desktop ACP exposes take-over, return and bounded input methods; optional screen generation pinning rejects stale captures.

    • —

      c1027fb: Add a connected computer observation to PalSystemPromptOptions.computer, with the current control mode, so hosts can describe an existing device independently of permission to execute tools.

      Fix Pal chat composition when the operator holds its connected computer. The Pal now receives its actual connected/control state instead of being told the computer is unavailable. Operator-held chat retains zero guest tools and performs no guest inspection or allocation; Return control admits the existing guest capabilities on the following turn.

    • —

      d8d562e: Add buildPalSystemPrompt and PalSystemPromptOptions for hosts to compose a Pal's saved identity, conversational language, public output discipline and truthful computer availability. The helper does not acquire execution authority. CLI Pal sessions use these shared instructions while retaining their pinned model and execution policy. Authenticated name and appearance edits update the next turn's display identity without rewriting the original introduction.

      Add PalRuntime.admitConversation, PalConversationAdmission and PalRuntime.computerChanging. Hosts can admit exclusive model-only chat without starting a computer, with current pause, cancellation and ownership checks. Guest acquisition remains separate and requires the real guarded environment; the existing mandatory admit contract is unchanged. CLI manual chat supplies zero tools while offline or operator-controlled, then adopts the actual guest toolset on a later turn after explicit startup or returned control. Directed dispatch and checkpoint resume continue to require guest admission. The standalone pal chat command keeps its existing computer-startup preflight.

      Add palConversationGreeting and PalConversationGreeting to reconstruct a stable English onboarding prelude from an owned conversation's original profile revision. CLI Pal claim and list results expose this separate host-authored intro without starting a model request or manufacturing a journal turn. Pal desktop history omits explicit assistant commentary and tool-call narration from its public display projection while retaining the original journal.

    • —

      53aed2a: Add durable subscriptions for closed Pal activity metadata from an exact owned original conversation. Observation, disclosure, recipient receipt and idle wake have independent current permission checks. Custom publication authorizers must also implement the final accept phase requiring current observation, disclosure and receipt together. Accepted facts enter the shared Pal inbox before cursor progress; retries preserve identity and verified observation trails suppress feedback.

      The CLI adds finite pal subscribe, subscription, activity and unsubscribe commands. Publication performs no inference or guest startup; explicit dispatch still requires current wake/tool authorization. Original turn intake and exact recorded delivery receipts are required for causality, and unknown evidence or exhausted journal read budgets reject without advancing progress.

    • —

      ce52b90: Add optional host-only PalComputerScreenStream, PalEnvironmentLease.screenStream and PalRuntime.computerScreenStream(palId, generation) for observing an exact current Pal computer over read-only RFB. Existing providers remain compatible; providers without the optional capability explicitly refuse live observation. Keep allocation authorization private in the embedding host and enforce read-only observation server-side; the stream does not grant operator input authority.

      The CLI exposes the owning desktop ACP stream method with geometry and generation rechecks, and reuses initialized registry roots to avoid repeated Windows ACL subprocesses while retaining fresh Pal definitions and directory identity checks. The local computer image adds x11vnc and authenticated binary WebSocket transport. Rebuild the installed image explicitly and restart the owning computer to enable live observation; older images retain screenshot/input behavior and advertise no stream capability. Persistent workspace and browser profile volumes survive.

      The guest desktop includes a themed wallpaper, a real dock, Files, terminal and browser launchers, plus a local browser home page. Normal browser-window closure keeps the desktop alive; owned profile shutdown retains browser flush ordering.

    • —

      815433f: Add DiskPalStore.delete(id, expectedRevision) and the desktop host's namzu/pals/delete metadata endpoint. Deletion uses a terminal immutable revision, hides the current Pal, and refuses future updates and execution admission while preserving historical profiles, journals, host files, and guest data volumes. Hosts must stop active work and confirm computer cleanup before deleting its identity. Existing custom PalStore implementations require no new method. Readers from older releases do not understand terminal deleted profiles and should not be used to read a registry after deletion has been published.

      Add the desktop host's namzu/conversations/archive endpoint. It performs a scoped, writer-gated soft removal while retaining the durable conversation and files. Successful retries return archived: true; a physically absent strict journal returns archived: false, missing: true, never claiming that an absent conversation was archived. Hosts may discard only their own known unsent local projection from this missing receipt. Existing terminal archive commands are unchanged.

      Release a connection's exact idle native-engine writer before desktop archival, reserving the conversation until the operation settles. Active or unresolved work is refused and a failed native close retains cleanup authority for retry. The runtime reservation method is optional for existing embeddings.

      Keep a Pal's approved stored control-path spelling when Windows native path resolution changes only its case, after verifying the same physical directory. Physical project identity remains canonical, while existing Pal profile revisions and journal ownership paths remain unchanged. Different directory objects and non-case aliases are refused.

    • —

      4367edc: Add opt-in held keyboard input for a Pal's operator-controlled guest. Providers advertise heldKeyboard: true only after negotiating the owned guest's support; PalComputerInput then accepts session-scoped key_down, key_up and release_keys. Providers must release their tracked held keys before confirming returned Pal authority. Existing complete key taps and AI computer_use actions keep their behavior, and older local images do not advertise holds.

      The desktop captures a new keyboard lifetime for each focus session, forwards press and release events without replaying browser key repeats, and releases only that lifetime on focus/view loss. Cleanup retains exact Pal/generation and operator authority checks and cannot release another lifetime's keys. Rebuild the local Pal image to enable this capability; a runtime update alone retains the older image's tap input.

    • —

      6f46da3: Add optional PalAppearance preferences to saved Pal definitions and create/update inputs. Character and color selections persist in immutable profile revisions; existing records retain their absent appearance and hosts may choose their own display default.

      The CLI accepts --appearance <character>/<color> on pal create and pal update, and the desktop ACP create/update extensions carry the same validated preference. Supported characters are pixel, sprout, and spark; supported colors are green, blue, amber, violet, and rose.

    • —

      f7f48f9: Add persistent Pal identities, immutable profile revisions and an SDK runtime that requires an explicitly provided virtual computer before execution. Hosts can retain one warm computer per Pal, serialize active conversations, enforce current pause state and retry failed computer cleanup without losing ownership.

      Add namzu pal list/create/show/update/pause/resume/chat and desktop host Pal methods. Pal conversations pin a saved profile and bind exactly one Pal in their session log. File, shell and computer tools run in the Pal guest, with no host folder, host browser or host plugin fallback. The local computer image and engine must be set up explicitly before chat can execute.

    • —

      846557b: Add dryRunEdit(content, input) and the AcpFileChangePreview type, and let each tool call in a session/request_permission request carry an optional preview ({ path, before, after }) that the bridge forwards untouched. dryRunEdit runs the edit tool's own schema and apply code (single replacement, replace_all, edits[], insertLine) on a string, so a host can show exactly what an approved edit will write without touching the file. resolveWithinAnyReal is now exported beside resolveWithinReal. Nothing existing changes: a client that ignores preview and an agent that never sets it behave as before.

    • —

      56d88dc: Allow task_update to mark a planning task failed, matching the existing TaskStore status. Add stats.failed to task_list and show failures separately in model receipts and operator presentation. Failed blockers stop waiting without being counted as completed; earlier terminal tasks remain excluded from later-turn planning views.

    • —

      e447a47: tool_completed gains an optional savedPresentation: one label line (at most 200 characters) or a command's first line, never the output, journaled for every call that has no diff, and for an external engine's tools from a name map. Nothing existing changes: presentation keeps its diff and cancelled/declined shapes, and live hosts that ignore the new field behave as before. A host that replays history can read presentation ?? savedPresentation so a reopened conversation names its actions instead of showing a placeholder. The desktop host does.

    • —

      57c99b2: Scheduled script jobs can opt out of a project folder with workspace: none (--workspace none in the CLI). Namzu creates a private working directory only after operator confirmation, and a script never loads that directory as a project configuration.

      Interactive schedule proposals now bind noteworthy results to the exact source conversation. The CLI stores them as durable host notices and displays them on resume without adding a fake model turn or changing the model's conversation history. An optional JSON script report lets a polling script distinguish a quiet check from a change and carry a bounded scheduler-owned state value between runs.

      The CLI default for newly confirmed interactive proposals changes from detached results to source-conversation delivery. An unavailable source keeps its result pending and holds later runs until delivery succeeds. Existing jobs retain their previous behavior. To keep a new job detached, review namzu schedule edit <job> --delivery none; the operator must confirm any exact pending results being waived. New workspace, source-delivery and report fields use job format v3, which older CLIs refuse rather than silently ignore. Upgrade the scheduler service together with the CLI.

    • —

      c590142: New scheduled agent phases now default to unlimited tokens (tokenBudget: 0) instead of 500,000 tokens. A positive request or configured limit still applies, and existing saved jobs retain their confirmed values. Set limits.tokenBudget to 500000 or pass --token-budget 500000 to retain the old default for new jobs. To remove an existing job's limit, run namzu schedule edit <name> --token-budget 0 and confirm the edit. Iteration and timeout defaults remain unchanged.

      The SDK schedule tool accepts zero for an unlimited agent token allowance. Unlimited previews omit the numeric daily ceiling and identify the absence of a token limit; usage accounting continues to measure actual tokens.

    • —

      1fa60b9: The ACP bridge now sends a provider_retry session update when a model call fails transiently and will be tried again: attempt, maxRetries, delayMs, an optional HTTP status, serverDirected and the turnId. It carries no provider body, header or credential. A client can show "waiting, retrying in 6 seconds" instead of a silent spinner.

      AcpSessionUpdate gains this member. A client that already ignores update kinds it does not know needs no change; a client with an exhaustive switch over update.kind should add a provider_retry case (or a default branch) before it upgrades the type.

    • —

      5a0a1f7: Expose optional skipped: true metadata on pre-tool hook completions, recovered completed-call records and step tool results. This distinguishes a non-error skip receipt from output produced by executing a tool. Existing records without the field remain unchanged.

      A skipped standalone structured-output or terminal tool now returns its receipt to the model instead of failing JSON integrity checks or settling with an unexecuted answer. Skips do not consume structured schema corrections; cancellation and iteration limits still apply. The runtime creates the marker only for pre-tool hook skips and does not infer it from receipt text or tool-authored result fields.

    • —

      e447a47: task_created and task_updated session events, and the ACP AcpTask row, gain an optional activeForm (what the task reads as while it is being worked on, such as "Running the tests"). It is absent when the model gave none, so nothing existing changes; a host that ignores it behaves as before. A host that draws a live plan can show it beside the task in progress. The desktop host passes it on when it lists a session's tasks.

    • —

      bebe135: A tool result can now say what the call did as typed fields, so a host builds what a person sees from fields and not from the model's text. Nothing you already send or receive changes shape, the text the model reads is unchanged, and no code needs to change to upgrade.

      New and optional: ToolResult.facts, a closed union (ToolFacts) of a command's exit code and its stdout and stderr as head-and-tail windows (ToolTextWindow) or a note that the output was binary, a read's path and size, a search's and a listing's counts, whether a write created, updated or left a file, and the kind of a failure (ToolFailureReason). The built-in bash, read, grep, glob, edit and write tools set it, tool_completed and the ACP tool_call update carry it, a generic ToolCallView may carry facts for what the call is about, and admitToolFacts reads a saved one back. A call refused without running, an unknown tool and a thrown filesystem error also carry a failure fact. execHostShell additionally returns stdoutBytes and stderrBytes. A post-tool hook that replaces the output also drops facts, so a redaction is never undone.

      If you build a view of tool output yourself, read these fields instead of the STDOUT: and STDERR: labels, the "Found 2 match(es)" and "1 replacement(s)" sentences and the notes about withheld credentials or shortened output, which are the model's interface and are not stable text to parse.

      The Desktop app now words every tool row from those fields (Read a.ts, Searched for export, Ran npm test · failed (exit 3), Couldn't read missing.txt: file not found), draws a command's colours, folds long output to its first and last lines with Show more, says "Binary output, 3 KB" instead of printing the bytes, counts a finished edit in the reply's changes card, shows paths in approvals relative to the project, and leaves "0s" off work that took under a second. The CLI host restores a saved conversation's facts (without a command's output) so a reopened row reads the same.

    Patch Changes

    • —

      7f5ac50: Preserve the recorded failure explanation in ACP turn_ended.error when a provider fault pauses a turn at a checkpoint. Hosts can display the actual failure instead of silently showing only a stopped turn. The existing coarse cancelled category and exact paused reason remain compatible; ordinary review pauses and user cancellation do not produce an error.

    • —

      1af4531: Preserve the runtime's completed tool presentation in ACP updates. File diffs and terminal results now reach ACP clients with the real result data instead of falling back to a generic text label. Older event producers without a presentation retain the existing fallback.

    • —

      c8613e0: Make the held sandbox acquisition timeout regression deterministic by controlling its clock after sandbox creation starts. Runtime cancellation and timeout behavior are unchanged.

    • —

      c1027fb: Enforce the existing tool-mode structured-output retry allowance for unrepaired invalid JSON, truncated arguments and schema mismatches, as well as missing output. All such responses share one counter; multiple invalid calls in a response consume one correction after every sibling result is recorded. The default remains two correction opportunities. Valid paired candidates, ordinary tool work, successful local repairs, host refusals and reviewer rejections retain their separate behavior.

      Persist corrections in optional Checkpoint.review.toolStructuredAttempts, saving feedback and answered results before another request. Exhausted resumes stop without another model call, and completed argument-failure evidence survives pending-batch recovery without revalidation. Old checkpoints and tool records remain readable without rewriting their bytes or hashes. Hosts must continue supplying the output configuration when resuming.

    • —

      b6ea5ca: Models work on first open with nothing to set up. With nothing saved, Namzu Desktop and the terminal app now start on what they find, in this order: a sign-in Namzu already reuses (Claude Code, Codex), an API key in the environment, a local server (Ollama on port 11434, LM Studio on port 1234; each probe waits at most 300 ms and they run together), then the keyless Zen free models. They say what they picked in plain words ("Using your Claude sign-in, found on this computer.") and save nothing, so a key or sign-in added later wins on the next open. /model and the model picker change it.

      What changes for you: in the terminal app a first run with an API key or a local server in the environment no longer stops at the provider picker; it starts on that provider. The picker still opens when nothing is available. In Desktop the keyless Zen free models now count as usable, as they already did in the terminal app, so a new person's message box is ready and Send works at once; the "Add an API key or sign in to start" card now appears only when nothing at all is available. To keep the old terminal behaviour of choosing yourself, save a choice once with /model.

      When Zen declines a free request that carried no key, both apps now say "Zen didn’t accept the free model right now. Try again in a while, or choose another provider. A Zen API key also works." and keep the technical detail under Details (Desktop) or --debug (terminal). The generic provider.permission explanation in @namzu/sdk no longer says the key authenticated: a 403 only says the request was refused.

    • —

      bb0ad2e: Fail structured tool output explicitly when its retained receipt is no longer valid JSON, including without a reviewer, instead of publishing truncated or rewritten text as a successful structured result. Increase maxToolOutputChars for larger tool results or select native structured output with a capable provider.

      Make wait_for_task report the underlying turn outcome and retain failure explanations even with an empty answer. Completed scheduler lifecycles containing failed, partial or cancelled turns no longer appear successful; legacy completed results without a turn status retain their existing behavior. Blocking create_task and linked planning failure notes also preserve those explanations and describe empty legacy completions accurately.

      Report a planning-task status change that cannot be confirmed as failure with the requested and returned statuses. The store may have refused the request or a concurrent update may have advanced it; other requested edits may already have applied. Existing store transition policies remain unchanged; successful task updates retain their quiet presentation.

    • —

      7f5ac50: Recognize a source-exact bare [ POSIX test command as a literal program instead of falsely classifying it as a glob-expanded command. This removes unnecessary unknown-program approval prompts for ordinary shell predicates and copy loops in automatically approved Pal guest work. Real bracket patterns, runtime-produced program names, opaque shell constructs and dangerous-command denials retain their existing review/refusal behavior.

    • —

      7473449: Interactive terminals in the same project can discover and message one another with /peers, list_sessions and send_session_message. Idle terminals can start a turn for peer context; busy terminals receive it at the next safe request boundary. Peer input remains separate from operator instructions and cannot grant approval. Different permission modes are refused. Use /peers off to refuse new mail and pause delivery in this terminal.

      The mailbox is bounded and process-local; queued is an acceptance receipt, not proof of model delivery. Conversation switches cannot redirect pending mail. Child-task activity now labels accepted messages as queued, and SDK manager documentation describes the inbound callback that query-backed agents actually consume. The SDK peer envelope refers to the host's reply tool rather than prescribing a CLI child-task tool.

    • —

      6194bda: Forward tool starts and progress through the query event stream while an approved batch is still running. Previously these events were recorded immediately but reached the operator only after the batch finished, leaving long commands and readiness waits without a live tool row. Event-driven observation retains the existing authorization, result ordering and provider-valid batch boundaries.

      Closing the stream during a live tool event cancels the captured batch and waits for executor settlement before releasing the recorder and borrowed turn resources. Tools must still honor their cancellation signal; this does not promise forced termination of arbitrary custom code.

    • —

      f4b7d1d: Honor MCP input schemas that explicitly declare dictionary values through schema-valued additionalProperties, including the empty {} value schema. Previously local validation silently removed those dictionary keys before dispatch, so application input could report success with empty event data. Retain declared keys, validate typed values and bound recursive dictionaries. Existing omitted/false closed-object behavior, explicit true passthrough, server trust, result provenance and execution guards remain unchanged.

    • —

      6ff961f: A tool call made from inside another tool (for example through run_code) to a tool that declares requiresApproval for that input is now refused and audited, even when an allow rule covers it. An approval cannot be requested from inside another tool; call such a tool directly. Hosts that mounted run_code with an allow rule on an approval-gated tool will see it refused.

    • —

      023224a: A failed OpenAI or Google request now reaches the host with the provider's own type, code, parameter and request ID, as an Anthropic failure already did, so Details read "Status: 403 · Provider said: “…” · Type: invalidrequest_error · Code: model_not_found" with "Request ID: …" on its own line. The error's detail for a vendor SDK error whose message is only a sentence is now a JSON body of those fields ({"error":{"message","type","code","param"},"request_id"}) instead of the bare sentence; read providerCode or the Details of a host rather than matching the sentence text. The Google driver now reads its refusal body and request-ID header. The vendor project line in Details reads "Project: proj…".

    • —

      12b5af9: Compare actual error/output/content fingerprints before refusing identical failed calls. Different errors restart the streak, and a successful potentially mutating execution allows a new check after repair. Denied calls and unrelated reads cannot reset the guard. Repetition advice no longer claims an unobserved future result is certain.

    • —

      093d657: A saved engine tool named wait, wait_agent or waitagent now reads "Waited for agent", and close_agent, interrupt_agent or kill_agent (any casing, with or without the underscore) read "Stopped agent", where they read "Used wait agent" and so on before. No export or type changes; a consumer that matched on the old label text should match the new one.

    • —

      f0aeb9b: The package page (README) of @namzu/sdk now explains the install, a first agent that needs no key, tools, a real model driver and the subsystem map. Only the README changed; no code, API or behaviour changed.

    • —

      3727e2b: Keep an assistant message's runtime stream ID when recording its durable history and final settlement, including forced closing summaries. ACP clients can now reconcile the settled answer with its streamed message instead of displaying it twice. Distinct messages with identical text retain separate identities; caller-authored message IDs are not trusted as stream identities.

    • —

      56d88dc: Creating a task with blockedBy and deleting a task now emit task.updated for each existing task whose reciprocal dependency list actually changed, before task.created or task.deleted. Disk and in-memory task stores keep live event projections aligned with their dependency records, without announcing unchanged rows or the deleted task's self-edge as a surviving task. Repeated blocker references no longer add duplicate reciprocal edges in the in-memory store; supplied blockedBy references remain intact.

      Confirmed disk neighbor writes are announced immediately, even when a later neighbor write, new-task record write, or target-file deletion fails. No created or deleted event is emitted for an unsuccessful creation or deletion, and already persisted changes remain observable.

    • —

      98e8469: Clarify that task_list returns open tasks and tasks closed in the current turn, with earlier completed or failed tasks retained in storage but omitted. Its output now reports retained omissions and distinguishes a filtered empty view from finding no records; the human presenter also qualifies its counts. Task selection, input and data.stats remain unchanged. Hosts needing all durable records should read their authorized TaskStore rather than infer deletion from this filtered tool result.

  3. v49.0.0

    Major Changes

    • —

      b7d83ec: ToolManager.toPromptSection() no longer includes server-authored description hints for deferred MCP tools. It still lists their names. If a host calls this method directly and relied on those hints in its system prompt, call toUntrustedDeferredContext() separately and put its labelled output in a request-only context message after history. The SDK query path does this automatically, bounds the hint list to 4,000 characters plus its provenance frame, prices the largest permitted rendering before step preparation and filters it to permitted tools. Hosts preparing their own steps can call snapshotUntrustedDeferredContext() for the same bounded snapshot. MCP tools remain searchable by their full descriptions through search_tools.

    • —

      b7d83ec: Plugin user_prompt_submit annotations now reach the model as user-role request context instead of system instructions. The CLI gives operator shell output and idle background-job exits the same request-context role. Hosts that used these observation channels to issue policy must move that policy to an explicit trusted instruction; the observations remain visible to the model on the current request.

      The namzu mcp test HTTP 401 hint now uses a literal <name> placeholder instead of embedding the configured server name in a runnable shell command.

    • —

      b7d83ec: Memory and saved resident state now reach the model as request-only user-role context rather than system guidance. In the SDK, createMemoryRecallStep returns PrepareStepResult.context instead of system; hosts that read or compose the hook result must use context and pass it through the normal preparation chain. Resident continuation, wake evidence, skill catalogues and learned text also move from system dynamic or turn contributions to context; hosts that inspect placements must migrate. context contributions now render once before prepareStep so their estimated cost reduces preparation headroom; hosts relying on a contribution to observe preparation side effects in the same model step must move that logic to a later stage. The CLI moves file-backed USER.md and MEMORY.md, its stored-memory index, and automatic recall out of system prompts on new sends, resident turns and resumes. Interactive resident turns now place admitted data in the new SendOptions.extraContext request-only field; their fixed doctrine remains in extraSystem. Hosts that relied on memory files or resident data as system instructions must pass trusted guidance explicitly. This changes default provider-visible roles and may change model behavior and prompt-cache accounting; request-only context still uses input tokens on each request and the CLI bounds memory against estimated remaining request room.

    • —

      b7d83ec: Plugin MCP server declarations now reject fields they do not implement, including url, headers, inheritEnv and requireApproval. Those fields were previously accepted and silently removed while the declared stdio command still started. Remove unsupported fields from plugin.json or an in-code plugin; configure a remote server through the host's MCP settings and express approval in host source permissions. Plugin MCP startup now has a ten-second deadline across connection and discovery, configurable with connectTimeoutMs up to one hour, and cleans up a failed connection before rolling back the plugin.

      Hosts supplying ConfigRegistry must move plugin reconnect-policy overrides from mcp.<server-name> to mcp.plugin.<plugin-name>.<server-name>; the old key was shared by unrelated plugins and is no longer read. A ConfigScope now has dispose() to release its live namespace and watchers while retaining saved overrides. Plugin disable and failed enable release their scopes, and overlapping enable, disable and uninstall calls for one plugin id now settle in call order.

      Plugin MCP drift is now tracked by full plugin source id rather than bare server name: hosts that relied on a drift event between two unrelated plugins using the same local server label should compare those sources explicitly. The baseline now follows the one admitted listing actually mounted as tools, including reconnect and list-change refreshes; a second startup listing is gone.

    • —

      b7d83ec: wait_for_job now returns success: true when its idle or total wait bound is reached. Previously that outcome returned success: false and was treated as a failed tool call. Callers that used success === false to detect a wait timeout must check data.timedOut (idle or wall) instead. The job keeps running in either case.

      The tool now includes a bounded partial output preview and absolute nextOffset on timeout, and accepts from_offset on the next call so callers can continue without receiving the same bytes again. It reports output omitted by the wait's 32 KiB cap separately from bytes lost by the job registry's retention cap.

    Minor Changes

    • —

      2b1cbb5: Hosts can use QueryParams.onJobNoticeDelivered(jobIds) to distinguish background job exits recorded for the model from exits that still need delivery on a later turn. The CLI now keeps a late exit pending for the next turn, retains it after a failed or aborted send, and shows its transcript row once.

    • —

      b7d83ec: Built-in memory stores now offer opt-in revision-checked updates and deletion. Read an opaque token with getVersionedRecord or read_memory, then pass it to updateIfRevision, deleteIfRevision, update_memory, or delete_memory. A stale token reports a conflict without applying the change. Existing calls without a revision keep their last-writer-wins behavior; custom stores need to implement the full conditional interface to accept revisions.

      In-memory revisions cover cloneable BigInt, Map, Set, Date, cyclic metadata and the full backing buffers visible through typed-array views. Direct resizable ArrayBuffer values include their resize attributes. When metadata cannot be fingerprinted synchronously, including views backed by SharedArrayBuffer or resizable ArrayBuffer, read_memory still reads it but offers no revision; conditional writes fail closed.

      When a tool-result guardrail rewrites text, it also replaces separate model-visible content blocks and the original failure error, so neither channel can bypass redaction.

    • —

      b7d83ec: Memory searches can now bound candidate scans with maxScanned and continue using scanOffset. Results report truncated, scannedCount and nextScanOffset, with totalCount counting only matches in an incomplete page. search_memory uses a 256-candidate page and accepts scan_offset; repeat with the returned offset when an older memory may match. Automatic recall also searches at most 256 recent candidates by default, configurable through MemoryRecallOptions.maxScanned. Disk stores select the page before reading its bodies. A direct list call without maxScanned retains its full-search behavior.

    Patch Changes

    • —b7d83ec: The SDK unit test runner now uses at most four workers on machines with many CPU cores. This prevents valid asynchronous tests from hitting their wall-clock timeout under test-worker contention; package runtime behavior and APIs are unchanged.
  4. v48.3.1

    Patch Changes

    • —

      57ae7c7: Background jobs whose process cannot start now emit one exit update after the child closes, without reporting the spawn error as a process exit code. An error on a process that already started no longer marks its job as exited while it is still running. SDK API signatures are unchanged.

      Completed and aborted job or delegated-task waits release their polling timers instead of keeping them alive for the rest of a long session. Idle-stream, agent front-door, awaited-job and large-input lexer regression tests now avoid real-time races that could fail on a busy CI runner.

      The CLI's background-child and scheduled-run integration tests now wait for the event they assert, leaving their normal process deadline to Vitest instead of letting a short poll or unrelated watchdog decide the outcome.

  5. v48.3.0

    Minor Changes

    • —072bb74: Windows computer use can capture pixels for one named window with screenshot {window_id} and keep background clicks and drags, text and keys bound to that window. Window type_text and key can explicitly request delivery_mode: 'foreground' for browser text or modifier shortcuts that cannot run in the background; this may briefly bring the verified window forward. The pinned driver sets windowScroll: false and refuses scoped pixel scrolling because its wheel input cannot prove which window receives it; a host that supports safe window scrolling can offer it. Capture identity and window geometry are checked before input; moved windows, old captures and restarted driver sessions require a new window screenshot. Hosts can offer window capture without display capture, in which case window_id is required and plain display screenshots remain unavailable. A failed window capture never broadens to a display capture. When both browser and computer-use tools are ready, the CLI routes a named existing browser window to computer use, leaving browser tools for the separate Namzu-managed profile.
  6. v48.2.0

    Minor Changes

    • —

      25966a5: Scheduled script proposals now explain the permission set they actually need, including why read-only blocks every script and why a home-directory working folder is refused. The CLI shows a compact, scrollable confirmation for model proposals and /schedule confirmations, with the exact source available for review. It labels pure-script permissions accurately and explains a blanket bash denial without suggesting a script rewrite.

      New notifyOnFinish on the schedule tool and --notify-finished true|false on the CLI let a polling job suppress Namzu's generic success notice while keeping failure notices. The default remains true; existing jobs keep their behavior until changed.

  7. v48.1.0

    Minor Changes

    • —3a0daac: Allow delegated CLI agents to opt into a separate managed Git worktree with workspace: "worktree". The child runs from the selected checkout's committed HEAD, preserving the parent's directory beneath the Git root. The worktree remains available for review after any outcome. SDK callers can choose a per-child shared or isolated workspace and explicitly retain it; isolated requests accept a checked relative subdirectory. A manager can make omitted choices shared with workspaceDefault: 'shared'; existing SDK backend provisioning and cleanup remain the defaults.
    • —3a0daac: MCPClient.callTool accepts onProgress and requests a unique MCP progress token per tool call. Concurrent calls receive only their own validated, bounded progress updates; callbacks stop on completion or cancellation. Streamable HTTP dispatches progress while the SSE response remains open and releases the reader as soon as the matching final reply arrives. MCP tools forward updates through ToolContext.report for live host and CLI status.
    • —3a0daac: Modern MCP connections now follow advertised tool, prompt, and resource catalogue changes through subscriptions/listen. mcpToolset refreshes its live definitions after an acknowledged change, and Streamable HTTP reads the subscription incrementally instead of waiting for the long-lived response to end. Hosts using StreamableHttpTransport directly can use sendSubscription for a bounded SSE stream; existing legacy notification behavior is unchanged.

    Patch Changes

    • —c8bb87a: Describe a browser http-auth handoff as an HTTP authentication challenge. The old "password prompt" wording was inaccurate for responses using challenge schemes such as Bearer.
    • —3a0daac: Reject an explicitly isolated child workspace when a registered Git worktree driver returns a missing path or a path that resolves to the caller's directory. SDK hosts using a custom driver must return an existing, separate checkout.
    • —3a0daac: MCP Streamable HTTP connections now surface a 401 or 403 response to the modern protocol probe as an access failure, without attempting a legacy initialize handshake. Correct the server credentials or access policy and reconnect.
    • —3a0daac: Modern MCP subscriptions now stop retrying a server that permanently refuses subscriptions/listen, including HTTP 404 with JSON-RPC method-not-found. Temporary server errors and rate limits still retry with backoff. Operators get a warning and can reconnect after changing the server configuration. An HTTP 200 JSON-RPC method-not-found refusal also stops retries, while a capacity refusal still retries. The response body is read with size and time bounds and released on completion, timeout or disconnect, preventing held responses from accumulating.
  8. v48.0.0

    Major Changes

    • —

      567ada8: The SDK now accepts application-defined agent type strings and provides QueryAgent as the generic managed query adapter. defineAgent gives its run callback a required fourth AbortSignal parameter and creates fresh instances for delegated turns. Code that invokes a DefineAgentOptions.run callback directly must supply that fourth argument; existing three-argument callback implementations remain assignable. ReactiveAgent and the supervisor, pipeline and router exports remain available as deprecated compatibility names. CLI delegated turns use a CLI-owned NamzuCliAgent while keeping their existing configuration behavior.

    • —

      567ada8: PluginHookDefinition now checks each hook's returned action against its event. A TypeScript plugin whose handler is declared to return the full PluginHookResult union, or returns an action the runtime cannot use for that event, may stop compiling. Narrow the handler return type to PluginHookResultFor<'event_name'> or let TypeScript infer its specific result. For example, retry belongs to post_tool_use, modify to pre_tool_use, and annotate to user_prompt_submit. Session start/end and turn interrupt hooks can only declare continue as a result. JavaScript runtime behavior is unchanged.

    • —

      567ada8: Plugin tools now carry a source for their owning plugin (plugin:<name>) or MCP server (plugin:<name>/mcp:<server>), so source authorization can distinguish them. Plugin MCP servers use mcpToolset, gaining live tool and prompt discovery, policy-filtered deferred resources and held changed definitions. Plugin MCP prompt tool names change from <plugin>__mcp_prompt_<server>_<name> to <plugin>__mcp__<server>__prompt__<name>; update saved tool names and permission rules that refer to them. MCPToolsetOptions.reconnect also accepts a function that returns the current reconnect policy.

    • —

      567ada8: Every registry now throws on a duplicate id by default instead of silently overwriting or skipping. ManagedRegistry's own default flips from warn-and-overwrite to throw (RegistryCollisionError), and the same convergence applies to registries that were not built on it: AdvisorRegistry.register now throws AdvisorCollisionError instead of overwriting in silence; SkillRegistry.add now throws SkillCollisionError instead of overwriting in silence; EnvironmentConnectorManager.registerEnvironment and TenantConnectorManager.registerTenant now throw EnvironmentCollisionError/TenantCollisionError instead of logging a warning and silently keeping the original. AgentRegistry, ConnectorRegistry and HostCommandRegistry (built on ManagedRegistry with no registry-specific override) throw by the same default flip.

      PluginRegistry is the named exception and keeps its overwrite behaviour. ToolRegistry is removed in this release; toolset name collisions throw ToolsetConflictError. Pass onCollision: 'warn-overwrite' explicitly if you build your own ManagedRegistry subclass and need the old default. A single call that needs to replace one entry on a registry that now throws calls the new ManagedRegistry.replace(id, item) instead — see docs/sdk/registries.md.

      Every SDK *CollisionError (ToolNameCollisionError, HostCommandNameCollisionError, ConfigNamespaceCollisionError, PromptContributionCollisionError, ReadModelCollisionError, InvariantNameCollisionError, ProbeNameCollisionError, DuplicateProviderError, plus the four new ones above, plus DelegateIdCollisionError (scheduler/delegating.ts)) now extends the new RegistryCollisionError base class; names, messages and fields are unchanged, and a catch block naming the concrete class still works. A catch that wants "some registry collided" without naming every class can catch RegistryCollisionError. See docs/sdk/registries.md.

      If you called register/registerEnvironment/registerTenant/SkillRegistry.add a second time under the same id expecting a silent overwrite or a silent skip, that call now throws. Call unregister first, or (on a registry built on ManagedRegistry) call the new replace(id, item) instead.

    • —

      567ada8: Removed ToolCatalog, createToolCatalogFromRegistry, loadingFromAvailability, toolDefinitionToCatalogEntry, ToolsetDefinition, ToolsetPolicy, ToolCatalogEntry, ToolCatalogSearchResult, ToolCatalogSnapshot and ToolLoadingMode. This was a parallel, unwired model of sources/toolsets/tools with a per-tool policy: no query run ever called toLLMTools/getToolsByLoading/searchTools on it. This release also removes ToolRegistry and moves live discovery to ToolManager and ToolResult.reveals; see the tool-registry-removed changeset. ToolSource and ToolSourceKind stay.

      If you constructed a ToolCatalog directly or called createToolCatalogFromRegistry, pass toolsets to the runtime and use ToolManager's listNames() / availability(name) for an advanced host's roster view. Nothing else in the kernel ever populated the catalog from live state.

    • —

      fbeac55: A scheduled job can now be a fixed shell script instead of a model prompt, or a cheap "wake-gate" script that decides whether to run the model at all — namzu schedule add --kind script|script+agent, or the model's schedule tool with kind/script. A script job spends zero tokens and opens no session; a script+agent job runs a gate script first and calls the model only when it prints {"wake": true, "context": "..."}. The script body is checked against the scheduled-run floor (the whole text, exactly as a live bash call is read) and then every deny rule — the operator's own and any config file's — per lexed command; allow/ask rules and unmatched are never consulted for a script, since it is fixed, human-confirmed text, not a call a model improvises. A script+agent job's allow/ask/unmatched rules still govern its AGENT phase only, unaffected by the script's own (deny-only) check.

      @namzu/sdk (major)

      • —Breaking type correction: ScheduleJobPreview.model is optional because a pure script job has no model. Hosts that display it must handle its absence for runKind: 'script'; previews for agent and script+agent still carry the pinned model.
      • —ScheduleJobPreview.networkAccess now includes a host script or an allowed host shell that could reach the network, even without web/browser tool grants. Its new optional networkGrantAccess separates those grants from shell capability; a host implementing previewUpdate should set it so a kind-only conversion can be checked against the network-beside-host-shell rule without refusing every script. An older host that omits it is treated conservatively using networkAccess.
      • —An unattended sandbox-escape opt-in no longer waives review of a runtime-chosen program name or another call's outside-root path in the same batch.
      • —Security fix: a recognised wrapper such as env, nice, sudo, busybox or toybox can no longer hide a shell's -c payload from review. Literal payloads are read for permission checks; an expanding payload requires review and is refused in unattended runs. script -t -c now recognises -c as the command option; -T consumes its required timing-file value.
      • —Security fix: find's -exec family now follows wrappers and shell -c payloads inside each clause, including when another wrapper launches find. A dynamically selected program or a find placeholder in a shell payload requires live review and is refused in an unattended scheduled script. The scheduled-run floor also sees service-changing tools behind wrappers in these clauses.
      • —Breaking: ScheduleJobDraft.prompt is now optional (prompt?: string), since a pure script draft has none. Any ScheduleToolHost implementation that reads draft.prompt as a bare string needs a null check (or to keep reading it only when draft.runKind is 'agent'/'script+agent', where it is still guaranteed present).
      • —ScheduleJobDraft gains runKind?: 'agent' | 'script' | 'script+agent' (absent = 'agent', unchanged from before) and script?: { body, shell, timeoutMs? }. ScheduleJobPreview gains the same runKind/script fields so a host's confirmation screen can show the exact text.
      • —The schedule tool's input schema gains kind and script; create requires script unless kind is 'agent' and requires prompt unless kind is 'script', and refuses an agent draft that also carries a script. The existing network-beside-a-host-shell refusal now also covers a script/script+agent job on the host: the script IS shell code, not a rule a live call might reach later.
      • —Security/UX fix: update accepted kind/script in its input schema but silently dropped both from the change it actually applied, reporting success with no error or warning that the model's new script was ignored. ScheduleJobChanges gains runKind/script (additive, so this alone would be minor) and update now forwards both to ScheduleToolHost.previewUpdate, re-verified fresh against the scheduled-run floor and every deny rule exactly like a new job's, and shown in full — with what changed — on the same confirmation screen create uses. A host that does not yet read the new fields off ScheduleJobChanges silently keeps a job's existing kind/script on every update, the same as before this release; only @namzu/cli's own host (below) newly acts on them.
      • —Security fix: update checks the effective preview's network grants after applying every change, so a kind-only conversion cannot inherit web/browser access beside a newly introduced host script. A call that changes the kind and leaves permissions unset is checked too.
      • —New exports from the root entry: execHostShell, ExecHostShellProgress, CommandShell, CommandShellProbe, findCommandShell, hostCommandShell, hostShellSpawn, withoutBashStartup — the shell-resolution and spawn machinery the bash tool already used internally, now available to a host that runs a command line outside a live tool call.
      • —findCommandShellForDialect and installedCommandShellForDialect select an installed bash or sh explicitly, returning no shell if the requested interpreter is absent. execHostShell accepts that CommandShell through its optional shell option, keeping its existing process supervision and output capture for hosts that choose an interpreter themselves.
      • —Security fix, in the scheduled-run floor, so a live bash call benefits too: trap 'ACTION' SIGNAL now reads ACTION as a command line (it runs when the signal fires); find … -exec|-execdir|-ok|-okdir … ;|+ is refused when its clause contains {} and the search root is unknown or could reach NAMZU_HOME, a -name/-path filter could match NAMZU_HOME's own name, or the clause's own program can stop or remove a service; running a file the SAME command line wrote earlier (>, >>, tee, cp/mv, then ./x, sh x, bash x, . x, source x) is refused, since its content was never confirmed.
      • —lexShellCommandLine now reads $(…) and backtick bodies as nested command lines (the same treatment bash -c payloads already got) instead of calling the whole containing line opaque outright: each command inside a substitution is in ShellLexResult.commands, marked origin: 'substitution', checked by the scheduled-run floor and by every deny rule exactly like the rest of the line. Two shapes stay opaque because what actually runs cannot be pinned down from the text alone, measured against real bash (5.2.21 and 5.3.15): a command substitution beside brace expansion in the same word ($(cmd){a,b} runs cmd once per alternative, not once) and an unquoted substitution used as a </> redirection target (a ${var:-…}-style default value can run its substitution twice when the target turns out ambiguous) — both are refused the same way an unmodeled construct always was. ${ list; } (bash 5.3's brace-form substitution) and process substitution (<(…), >(…)) are unchanged, still opaque; out of this scope. ShellWord gains substitutes: boolean (additive). Proven differentially against a real bash on the host (command_not_found_handle argv capture, disabled builtins, empty PATH): every existing corpus line, the exhaustive up-to-three-token sweep, a 6,000-line seeded random sample (manually run to 150,000 with zero mismatches before landing), and new hand-written cases for nesting, mixed quoting, and the two constructs above. decomposeCommandLine's segments/opaque change the same way for these two constructs.
      • —Security fix, follow-up to the above: a second review found that the scheduled-run floor's own pkill/killall/systemctl/launchctl/schtasks/D-Bus detection read a wild (expanding) program-name word as NOT being the tool in question unless its raw, unevaluated text happened to contain the tool's name as one contiguous run — $(echo pk)ill -f node and $(echo system)ctl stop $(echo namzu)-scheduler.service both passed. A wild word in the program-name position (the head, or right after a sudo/env-style one-level prefix) now counts as being every tool this checks for; a wild word elsewhere (an ordinary argument, a path) is unaffected. find ~/.namzu -exec rm {} \;-style commands are unaffected: the fix is scoped to the program-name position, not to "any wild word anywhere."
      • —Security fix, live tool authorization behaviour changes (packages/sdk/src/runtime/query/): the same review found that a deny rule written against a command's real name ("git push*": deny) never matches one produced by a substitution or a variable ($(echo git) push, `echo rm` -rf x, $X push, or a variable in the program's path, "$HOME"/bin/tool), since the name never appears as such anywhere in the call's text — for the scheduled-run floor's script check (verifyScheduledScript, which now refuses such a command outright, naming it, the same posture as an opaque script) and, pre-existing and more broadly, for the live bash tool's own permission rules. An operator now sees more review prompts: a bash call whose lexed command's own program-name word expands is escalated (ToolCallSummary.escalation.unknownProgram, new field) the same unconditional way a sandbox escape already is — never approved by an allow rule, a remembered grant, accept-edits or auto/an unattended turn, asked about in every mode that gets that far (naming why: "the program this runs is decided at runtime: <text>"), and refused outright with no prompt (UNKNOWN_PROGRAM_UNATTENDED_REFUSAL, new export) — there is no unattended opt-in for this the way unattendedSandboxEscape: 'allow' is for a sandbox escape. A deny rule that matches some other way (the tool's own name, an unrelated word) still refuses the call outright, unaffected. Argument-level expansion alone does not trigger this — only the program-name word — so an ordinary echo $(date) is unaffected. New audit actions unknown_program (approved/refused). See docs/sdk/escalations.md.
      • —Security fix, follow-up to both of the above — one extra word defeated every one of them: a third review found that all three consumers (the escalation, the scheduled-run floor, verifyScheduledScript) only ever looked at a command's literal head word, so env $(echo git) push, command $(echo git) push, exec $(echo git) push, and a re-exec wrapper with a mandatory argument of its own before the program (timeout 5 …, stdbuf -oL …, chrt 0 …, env VAR=value …) all ran, or passed the script check, unverified — the floor's one-hop REEXEC_PREFIX guess put the program at the wrong word entirely for these, and exec/command/taskset/time/builtin/xargs were not in it at all. eval/source/. were not read as unverifiable either, though command-line.ts already treats them as opaque for deny/allow rules. New SDK module, packages/sdk/src/authorization/program.ts, exported from @namzu/sdk: programPositions(command, dialect) is the one place "where does this command actually exec a program" now lives — it unwraps a chain of re-exec wrappers (sudo, doas, pkexec, env, nice, ionice, nohup, setsid, timeout, stdbuf, chrt, taskset, time, command, builtin, exec, xargs) with each one's own real option grammar, failing closed on an option it does not recognise, and reads each find -exec-family clause as its own position. source path/. path and eval word… are read the same way a nested bash -c '<literal>' payload already is: a LITERAL path is known, exactly as bash path is (nothing inspects either one's contents), and a LITERAL eval payload is joined and lexed as a command line of its own, with every position inside it folded in (so eval 'env $(echo git) push' is still unknown, transitively); an expanding path/argument word, or a payload that does not read cleanly, is unknown. hasPoisoningPrefix/poisonsLaterCommands/poisonedProgramPosition/resolveScriptPrograms/DYNAMIC_RESOLUTION_VARIABLES cover the other half: a command-prefix or export/bare assignment to PATH, LD_PRELOAD, LD_LIBRARY_PATH, BASH_ENV, ENV or IFS makes a LATER literal program name unverifiable too. Behaviour change: unknownProgram's text is now the command plus why its program's position could not be resolved, not a bare word — a host that pattern-matches the exact string should match on substring/prefix, not equality. ToolCallEscalation.unknownProgram's doc comment, escalationReason's wording, and the floor's own heads/REEXEC_PREFIX mechanism (removed, replaced by programPositions) all changed accordingly. packages/sdk/src/authorization/__tests__/program.test.ts (new). See docs/sdk/escalations.md, docs/sdk/command-lines.md, docs/cli/scheduled-tasks.md.
      • —Security fix, two CRITICAL bugs from a final review of program.ts, plus wrapper coverage: (1) sudo's own case never skipped a NAME=value pair the way env's already did, so sudo VAR=value $(echo ls) reported the ASSIGNMENT WORD as the program and never examined the real, wild one after it; both sudo's and env's assignment-skipping now also feed a poisoning name (PATH=, LD_PRELOAD=, …) into the poisoning check instead of silently passing it through. (2) executor.ts's unknownProgramOf ignored the reading's own opaque/!complete flag, so bash -c "$X" — whose payload the lexer cannot follow at all — was never escalated; a new exported function, unknownProgramInLine(value, dialect), checks that first, and executor.ts now delegates to it rather than re-implementing the walk. New wrapper coverage: busybox/toybox (transparent applet dispatch); declare -x/typeset -x (any option cluster with it, never +x) now poison like export, and export -n correctly does not; su -c/runuser -c/script -c/flock -c read a literal payload recursively like a nested bash -c '<literal>' (su/runuser with no -c are unknown: an interactive login shell); flock's positional form; unshare, nsenter, chroot (unknown with no command), setpriv, prlimit, numactl; watch (default payload joined and lexed like eval's, -x execs the argv directly); sudo -i/-s/-e/--login/--shell/--edit and bare sudoedit are explicit unknowns. ssh host cmd is deliberately not modelled (the command is remote); docs/sdk/escalations.md now documents this residual boundary — the full modelled-wrapper list, and that an unmodelled one is treated as the program itself, with deny rules, the floor and ordinary review still applying to it. packages/sdk/src/authorization/program.ts, runtime/query/executor.ts, packages/sdk/src/authorization/__tests__/program.test.ts.

      @namzu/cli (major)

      • —A pure script job now creates and runs without a configured provider or --model. New script jobs omit the unused model field; older confirmed script jobs that already store a real model still read with their confirmation digest intact and keep it on a script edit. Explicit --model, --effort, prompt, agent budget, provider wait, approval TTL, session retention and browser grant inputs are refused for a pure script instead of silently ignored; older stored fields remain readable but inert. agent and script+agent still require a model; converting a model-free script to either kind requires one. Text previews, the SDK ScheduleJobPreview.budget, and schedule show report zero model tokens and the script timeout for pure scripts. The schedule tool's success message reports pure script history without claiming a session.
      • —Breaking, on disk: the job/run-result/history file format moves to v: 2 for a job that actually uses runKind/script, a check-failed status, or gateResult/scriptOutput. An older namzu that reads such a file refuses it loudly ("was written by a newer namzu...") rather than misreading a script job as a malformed agent job. A plain agent job stays v: 1, byte-for-byte as before, and every existing job keeps reading and confirming exactly as it did — downgrade only affects a job you have actually converted to script/script+agent.
      • —Fix: a job file an older namzu (or a hand edit) left in a format the current one refuses to fully parse was previously invisible to createJob's name-uniqueness check and to findJob (by name or id prefix) — a name collision with it went undetected, and looking it up said "No scheduled job is named…" instead of the real reason. Both now read a refused file's id/name leniently, off the raw JSON, without validating anything else about it: a name collision with such a file is now refused, naming the file and the reason; looking it up by its own name or a matching id prefix now reports the real reason (e.g. "written by a newer namzu…"); a name or id prefix matching more than one job — readable, unreadable, or a mix — is an error listing every match and how to address each. schedule list says N job files could not be read instead of No scheduled jobs when every job file is such a file.
      • —New schedule add/edit flags: --kind agent|script|script+agent, --script/--script-file, --shell bash|sh, --script-timeout. A script job needs --shell and a non-empty script and has no --prompt; --unmatched park is refused for a pure script job (nothing can wait for the operator mid-script); --execution sandbox is refused for script/script+agent in this release; script/script+agent job creation is refused outright on native (non-WSL) Windows, where the only available shell reading is a loose cmd.exe approximation the floor cannot fully verify.
      • —Fix: --shell sh previously only labeled a script for verification while fire always used the live bash tool's automatically chosen shell. On a bash host, a newly confirmed sh job was guaranteed to stop at first fire. A script now executes under its chosen installed bash or sh; creation and every confirmation refuse an absent interpreter, and fire blocks if it has since disappeared. Native Windows remains refused.
      • —ScheduleRunStatus gains check-failed (a script or wake-gate malfunctioned: non-zero exit, its own timeout, or stdout that is not the wake-gate's JSON contract) — CLI-internal, not part of the SDK's public surface. It joins the failure bucket for the failure streak and automatic pause, and gets its own notification and noticeText case.
      • —schedule show/history --json gain gateResult ({ wake, contextChars }) and scriptOutput ({ stdout, stderr }, capped with an explicit truncation marker) on a fired script/script+agent run; show's text view gains a "Script"/"Wake-gate script" section beside "Prompt".
      • —schedule list (text and --json) now shows each job's kind: [script, 0 tokens] and [script+agent] in the text view, kind (absent for agent) in --json. The TUI's /schedule list marks the same two kinds. Found missing in a UX review — nothing distinguished a zero-token script job from an agent job in either listing.
      • —Security fix: a script/script+agent run's own output — a wake-gate parse failure's quoted stdout, a script-check refusal's quoted command, a check-failed notification's reason, scriptSummaryOf's extracted summary line, and schedule show/history's text-view reason/summary — is untrusted runtime text, not anything an operator confirmed, and is now run through the same sanitizeLine() a model's own answer already is before it reaches a terminal or a desktop notification, so it cannot plant control characters, terminal escape sequences or invisible/bidi characters on the operator's screen. --json output is unaffected and stays raw.
      • —Security fix: CLI and TUI schedule add/edit/confirm/list/show text views now show terminal controls and bidi characters in script text, prompts, changed lines and folder paths as visible code point escapes, so a script cannot erase or reorder its own confirmation. The model's schedule-tool confirmation uses the same projection. Persisted jobs and JSON output preserve the original source text.
      • —Security/UX fix: the TUI's schedule tool host now acts on an update's runKind/script: previously it always carried a job's existing kind and script forward unchanged, silently, whatever the model asked to change. Moving to kind: 'agent' drops the script (an agent job cannot carry one); a change that touches kind or script shows up in "Changed since it was last confirmed" the same as every other field, alongside the new script's full text in its own section (packages/cli/src/tui/schedule/tool-host.ts updateRequest, packages/cli/src/schedule/changes.ts confirmationView).
      • —Security fix: namzu schedule __fire now refuses a script/script+agent run outright on native (non-WSL) Windows, the same way job creation already does. Job creation alone was not enough: a job confirmed on WSL or Linux, whose daemon later finds itself running on native Windows (a moved NAMZU_HOME, a machine re-imaged from WSL to a native install), previously ran anyway whenever the job's confirmed shell dialect happened to still read as sh — the label native Windows's cmd.exe approximation and POSIX sh both report, so the existing dialect-mismatch check alone could not tell them apart.
      • —A script/script+agent script (and the scheduled-run floor generally, so a live bash call benefits too) may now use $(…)/backtick command substitution: it is checked the same as the rest of the script instead of refusing the whole thing outright (see the SDK's lexShellCommandLine entry above). The schedule-task skill's guidance on this is updated to match; the two narrow shapes still refused (a substitution beside brace expansion, or unquoted as a </> redirection target) are named there with why.

      Upgrade step: nothing changes for an existing agent job. Adopting a script/script+agent job on a NAMZU_HOME you may downgrade the CLI on later means that job's files become unreadable by the older version until you upgrade again; keep that in mind before converting a job you might need to roll back.

    • —

      567ada8: Continuing a session with a different project, tenant or topic now fails with invalid_config before its history, budget or queued topic messages are used. A supplied session log must also name the requested session. Pass the complete identity returned by the first runAgent call on later turns, or use the same recorded scope with query and QueryAgent. resumeSession, loadTurnState, loadSelectedTurnState and prepareForkState verify the source log before reading checkpoints. PrepareForkInput.scope.topicId is now required: change scope: checkpointScope to scope: { ...checkpointScope, topicId: sourceTopicId }, using the source session's recorded topic.

      Older session logs whose session_started record lacks tenantId or topicId remain readable but can no longer be continued, resumed or forked because their owner cannot be established. To migrate, verify the owner outside the log, start a new session under that scope and seed it with trusted conversation messages. Keep the old hash-chained log unchanged.

      Custom SessionLog.claim implementations must honor the new repairTornTail: false option used during owner admission. Defer torn-tail truncation and repair records until a later authorized append when that option is false. The session-log conformance contract is now version 2; update a custom backend's declared contractVersion after it passes the new deferred-repair case.

    • —

      567ada8: ToolMessage.revealedTools now stores { name, sourceId, sourceKind } receipts instead of name strings. Code that constructs these messages directly must include the source of each revealed tool; old name-only receipts in stored sessions no longer load deferred tools and must be rediscovered. A failed tool call no longer reveals tools. Hosts whose tools need a live connection should wrap their toolset with readyWhen(toolset, () => connectionIsReady); deferred(...) only delays schema loading and search_tools can load it whenever the host reports ready. A caller-provided search_tools must be active and ready when deferred tools are present, and a runtime override cannot defer or suspend it.

    • —

      567ada8: ToolRegistry is removed. The runtime now resolves tools from Toolsets (plan.md v3 §2) through a ToolManager (toolsets/manager.ts) that query()/drainQuery builds for itself, once per turn, from the toolsets you pass — it is never mutated by the runtime, and its own generated tools (task tools, search_tools, the structured-output tool, advisory tools) are combined in as a runtime toolset rather than injected into your input.

      What breaks, and what to do about it:

      | Removed / changed | Replace with | | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | tools: ToolRegistryContract on QueryParams, ReactiveAgentConfig, RunAgentOptions, BidiTurnParams | toolsets: readonly Toolset[]. Wrap a plain list with toolset('name', [...]); deferred(toolset(...)) for a toolset whose tools start deferred. | | tools?: ToolRegistryContract on SupervisorAgentConfig | toolsets?: readonly Toolset[] (same shape). | | ToolRegistry class, ToolRegistryContract, ToolRegistryRef, ToolRegistryForkOptions | ToolManager (new ToolManager({ toolsets, messages: () => turnMessages, resultGuardrails?, tierConfig? })), exported as an advanced API. ToolContext.toolRegistry is now a ToolsView (has/availability/searchDeferred — no activate, no searchActive). | | registry.fork({ deferExcept }) | No replacement: build the toolsets array you want for that turn/send instead of forking a shared registry. There is no live shared registry to fork from any more. | | registry.activate(names) / .defer(names) / .suspendAll() / .hasSuspended() / .assignTiers() | Gone. .defer/.suspendAll/.hasSuspended/.assignTiers had no production caller; .activate() did (runtime/query/executor.ts's reveal handling, tools/builtins/search-tools.ts's search_tools), and both are now the same derived mechanism. Availability is now DERIVED, never mutated: a tool is 'active' unless its toolset declared 'deferred' and no tool message in the turn's post-compaction history has revealed it (ToolResult.reveals, persisted as ToolMessage.revealedTools). A tool's own result reveals names for the rest of the turn; nothing calls activate any more, including search_tools. | | registry.searchActive(query) | Gone. ToolManager.searchDeferred(query, limit?) remains, with an optional result cap. | | ToolDefinition.provenance / ToolProvenance on a tool | ToolManager.sourceOf(name) — a lean ToolSourceRef (id, kind, and for mcp_server, server + readOnlyHintTrusted) that names the OWNING TOOLSET, not the tool. A definition can no longer claim its own source. tools/trusted-read-only.ts's isTrustedReadOnly now takes that source as a third argument. ToolProvenance itself stays, as the shape screenToolResult reads. | | filterReadOnlyTools(registry) / filterToolsNamed(registry, names) (tools/roster.ts) | filtered(toolset, (tool, source) => isTrustedReadOnly(tool, undefined, source)) / filtered(toolset, names) (toolsets/wrappers.ts) — keeps the inner toolset's own availability and stays live over a live source, instead of freezing an always-'active' snapshot. source is the ONE toolset filtered runs on: filter each of a wider roster's contributing toolsets this way before combining them, never a toolset combineToolsets already merged from several sources (tools/roster.ts says why). | | ConnectorToolRouter class (registerTools/unregisterTools/refreshTools mutating a registry) | connectorTools(manager, { strategy? }) (connector/tools/router.js) — a plain function returning ToolDefinition[]; wrap it in toolset(...) yourself. Never had a production caller. | | mcp_<server>_<tool> naming from the CLI's own MCP path | mcp__<server>__<tool>; update saved prompts, permissions and integrations that name these tools. The CLI now mounts mcpToolset entries for each server. mcpToolToToolDefinition no longer sets .provenance; source identity and read-only trust belong to the owning toolset. | | PluginLifecycleManagerConfig.toolRegistry | Removed. PluginLifecycleManager owns its tool contributions and exposes .toolsets: readonly Toolset[] for a host to fold into its own toolsets array. Each plugin has its own live file-tool source and each plugin MCP server has its own source; see the plugin-source-toolsets changeset. | | PluginResolver's second constructor argument | Was ToolRegistryContract; now Pick<ToolManager, 'listNames' | 'has'>. | | ToolRegistryConfig, ToolCatalog and companions | ToolManagerConfig (toolsets/manager.ts) is the manager's construction config. See the remove-tool-catalog changeset for the catalog removal. |

      The CLI, AG-UI adapter and other affected workspace packages migrate in this same release; see their package changesets. @namzu/files and @namzu/lsp do not use the removed registry API.

      A caller toolset that contributes a name query() also generates internally (a task-tool name, search_tools, the structured-output tool's name, or an enabled advisory tool's name) is refused at construction with ToolsetConflictError, naming both sources — this generalises SupervisorAgent's old hand-written refusal of a caller tool shadowing one of its six coordinator names, which is now the same mechanism (its coordinator tools are just another toolset).

      search_tools no longer activates anything: it returns reveals on its own result, like any other tool now can. Its receipt for "no deferred match" no longer echoes already-active matching tools, since the tool-body-facing ToolsView has no active-tool search — only searchDeferred.

      Security-relevant, additive: ToolCallContext (authorization/gate.ts) gains an optional toolSource?: ToolSourceRef, and AuthorizationGate's allow_read_only rule now reads it the same way isTrustedReadOnly always has. Without ToolDefinition.provenance travelling with the tool object, the live gate had no way to tell an operator-trusted MCP server's own readOnlyHint: true from an untrusted one's — both call sites in runtime/query/iteration/phases/tool-review.ts and the two in runtime/query/executor.ts (nested tool calls) now pass ToolManager.sourceOf(name). A host that builds its own AuthorizationGate calls and does not thread toolSource through gets the same "no untrusted party known" default as before (host-defined tools were never affected), but should pass it wherever it has a ToolManager to ask. Separately, ToolPredicate (toolsets/types.ts) widens to (tool, source: ToolSourceRef) => boolean; filtered/requireApproval now supply the toolset's own source as the second argument, existing single-arg predicates are unaffected. See the filterReadOnlyTools row above for the one-source-at-a-time rule this predicate needs to stay correct across a combined, multi-source roster.

    Minor Changes

    • —

      567ada8: Releases live toolset change listeners when a turn or duplex session ends, including abandoned streams and failed duplex connections, so repeated runs using one MCP toolset do not retain earlier tool managers. ToolManager.dispose() unsubscribes without closing caller-owned toolsets; direct ToolManager users should call it when finished. Agent front doors now check every exposed config field against its runtime destination, and the deprecated runAgent.verificationGate option again applies the authorization policy. Supplying distinct policies under both gate names fails before the model runs.

    • —

      567ada8: Managed hosts can now pass tenant, project, topic and session attribution per invocation through ManagedAgentInput.managedScope when calling QueryAgent. AgentManager supplies the admitted child scope itself, and QueryAgent accepts that scope while continuing to accept the four flat config fields. If both are supplied with different IDs, the run fails before a model call. The general AgentInput and other agent implementations keep their existing contracts. Idempotent QueryAgent calls are now deduplicated only within the same managed scope.

    • —

      567ada8: Authorization rules can now match the owning toolset's source id with { type: 'by_source', sources: ['mcp:github'], decision: 'review' }. This lets a host ask, allow or deny every tool from one or more MCP servers without listing names individually. A rule with no matching host-supplied source leaves the call to later rules.

      The CLI accepts [permissions.sources] with source ids or globs mapped to allow, ask or deny, and its permissions display shows those rules and their decisions.

    • —

      567ada8: Added findUndescribedProperties(schema), exported alongside findPortableSchemaViolations from the package root. It walks a rendered tool schema and returns every named object property, at any depth, whose schema carries no non-empty description — the same non-throwing, test-time sweep shape as the portability check, for a different defect: a Zod field with no .describe() reaches the model with no account of what it is for. Purely additive; nothing existing changes behavior.

    • —

      567ada8: Add defineCapability and dynamicCapability for reusable host-authored behavior passed to runAgent({ capabilities }). A capability can contribute toolsets, instructions, prompt contributions, input and output guardrails, and per-run model settings. Existing runAgent calls need no migration.

    • —

      567ada8: Two additive changes to the MCP connector, both opt-in and default-preserving:

      • —MCPInitializeResult.instructions and MCPClientState.serverInstructions capture the server's initialize instructions string (legacy handshake only — the modern era has no initialize round trip and never populates this field). This is observability only: nothing folds it into an agent's instruction set automatically, matching namzu's existing rule that server-authored text never reaches instruction/system position unframed. A host that wants to display or log a server's instructions can read client.getState().serverInstructions.
      • —mcpToolToToolDefinition takes a new optional 5th parameter, maxRetries?: number. Left unset, behaviour is unchanged: no maxRetries on the returned ToolDefinition. When set, the adapter also now marks ToolResult.retryable: true on the two failures it already classifies as not having reached the server's side effect (mcp_tool_input_required, mcp_tool_missing_client_capability) — the field the executor's retry gate actually reads. An HTTP-redirect outcome-unknown failure and any raw transport error stay non-retryable, since whether either one reached the server is exactly what is not known.

      Nothing to change for an existing caller: both are new optional fields/parameters with no effect until set.

    • —

      567ada8: MCPToolDiscoveryOptions and MCPToolsetOptions gain onRefused. The callback reports the current policy refusals for each tool, prompt and resource listing, including an empty list when a later listing clears them. Hosts can show which server names were excluded and why.

    • —

      567ada8: New mcpToolset(client, options) (packages/sdk/src/connector/mcp/): the path from a connected MCPClient to two live toolsets mounted together — tools and prompts under the configured availability, plus resources as two always-deferred tools (mcp__<server>__list_resources, mcp__<server>__read_resource, admitting only a uri the server has actually listed). options.allow/options.deny govern all three surfaces — tools, prompts and resources — matched on the server's own name for each. Every name is mcp__<server>__<rest> (tools), mcp__<server>__prompt__<rest> (prompts), shortened deterministically rather than refused when it would exceed the wire's 64-character limit. Reacts to list_changed notifications (only for a capability the server actually advertised) and to reconnection by re-discovering and reporting through onChange — including whether the server supports resources at all, re-checked on every reconnect rather than fixed at construction. Owns and stops an MCPReconnectSupervisor (reconnect: { enabled: false } opts out), and releases its onNotification subscription on close(). Discovery goes through the existing MCPToolDiscovery allow/deny policy and rug-pull drift detection, now including a discoverResourcesFrom alongside the existing discoverFrom/discoverPromptsFrom. Also new: mcpToolsetName(serverName, ...segments) and the MCPToolsetOptions type. See docs/sdk/mcp-toolset.md.

      MCPClient.onNotification now returns an unsubscribe function, mirroring onLifecycle — additive; any existing caller ignoring the return value is unaffected.

      Purely additive: no existing export changed or was removed. mcpToolToToolDefinition/mcpPromptToToolDefinition (the pieces mcpToolset wraps) remain exported for direct SDK callers. The CLI and plugin lifecycle now use mcpToolset, with their naming changes covered by separate changesets. A caller building an MCP server's tools by hand can move to mcpToolset at its own pace.

    • —

      567ada8: Plugin manifests can declare instructions. An enabled plugin contributes the text as labelled, untrusted request context; disabling it revokes the contribution, including for an already-running turn. SDK hosts can also call definePlugin({ name, tools, hooks, instructions, mcpServers }) and PluginLifecycleManager.installDefined(plugin) to install a host-authored plugin without a manifest file or dynamic module import.

    • —

      567ada8: PromptContributionRegistry.unregister(id) removes a prompt contribution and returns whether it existed. Hosts can revoke instructions when a plugin or other contribution owner is disabled, then register the id again when it is re-enabled.

    • —

      567ada8: runAgent accepts a caller-owned pluginManager and mounts its toolsets, enabled plugin instructions, and hooks for the invocation. Hosts using the high-level agent entry point can now pass a configured PluginLifecycleManager without manually reassembling its contributions. The host still owns plugin admission, enablement, and cleanup.

    • —

      567ada8: Added ToolManager (packages/sdk/src/toolsets/manager.ts), the runtime-owned resolver of Toolsets that plan.md v3 §2 describes: new ToolManager({ toolsets, resultGuardrails?, tierConfig?, messages }) resolves toolsets in order, then tools in order, throwing ToolsetConflictError on a name conflict at construction. refresh() adopts a live toolset's change only when called, reporting added/removed/drifted/refused names. availability(name) is derived — 'active' or 'deferred', never stored — from the owning toolset's own declaration and whether a tool message in the turn's post-compaction history has revealed the name (ToolMessage.revealedTools, also added, the persisted form of ToolResult.reveals). sourceOf(name), toLLMTools/toPromptSection/toTierGuidance/searchDeferred(query, limit?), a narrow view() for ToolContext, and the prepareExecution/executePrepared/execute pipeline (copied from ToolRegistry's, reading availability and source from the derivation instead of a stored map and ToolDefinition.provenance) round it out.

      ToolManager is exported as an advanced API. query() and the other runtime entry points use it in this release; see the tool-registry-removed changeset for the breaking input and export migration.

    • —

      567ada8: A tool can now declare that it always needs a person's approval, and carry free-form metadata a host can filter on. Both are additive; nothing existing changes behaviour.

      • —ToolDefinition.requiresApproval?(input): boolean and defineTool({ requiresApproval }) (a boolean or a function of the input) mark a call as needing a person's approval no matter what the host's authorization rules say or what mode the turn is running in. It is stronger than isDestructive: a gate allow rule and auto/unattended mode both still let a destructive call through, but neither can let a requiresApproval call through — it survives a gate allow rule the way an escalated call does, is asked about in every mode that reaches it, refused when nobody can be asked, refused outright under strict, and covered by no grant, skill grant or accept-edits exemption. A deny rule still wins. Reach for this on a call that is sensitive for a reason other than being destructive (it costs money, it leaves its own audit trail, it is policy-sensitive) instead of misdeclaring isDestructive to get a review the host might not otherwise configure. See The review policy.
      • —An unattended sandbox-escape opt-in confirms only the escape. It no longer waives a tool's requiresApproval declaration or another call's outside-root review in the same batch.
      • —ToolDefinition.metadata?: Readonly<Record<string, unknown>> and defineTool({ metadata }) carry free-form, tool-author-declared data for filtering and behaviour customization. Never sent to the model and never read by the runtime itself. The new matchesToolSelector(selector, tool) matches a tool against a list of names, a partial deep-equal match against metadata, or a predicate — for a capability, toolset wrapper or host to target a group of tools without maintaining a parallel name list.
      • —mcpToolToToolDefinition now carries a connected server's title, idempotentHint, openWorldHint and any tool-listing _meta into the new metadata field instead of dropping them (readOnlyHint/destructiveHint keep their existing typed home on isReadOnly/isDestructive). It never populates requiresApproval: a connected server cannot demand, or waive, its own review requirement. MCPToolDefinition gained a matching _meta?: Record<string, unknown> field for the raw listing value.

      Nothing to do to keep existing behaviour: every new field is optional and absent unless a tool author sets it.

    • —

      567ada8: Added ToolResult.reveals?: readonly string[]. A tool's own result can name further deferred tools to make callable — the same mechanism search_tools uses. The runtime persists admitted names on the tool message, so discovery survives later sends and resume until compaction. Unknown, already-active and out-of-allowlist names are ignored. There is no suspended state or mutable registry activation. See docs/sdk/tool-discovery.md for the migration from ToolRegistry.activate().

    • —

      567ada8: New packages/sdk/src/toolsets/ module: Toolset (the unit every tool comes from), toolset(source, tools), the composable wrappers prefixed, renamed, filtered, deferred, requireApproval, withMetadata and mapTools, and combineToolsets(source, toolsets) with atomic name-conflict detection (ToolsetConflictError, extending the shared RegistryCollisionError — see docs/sdk/registries.md — with combineToolsets as its registry name and the tool name as the colliding id; name, message and firstSource/secondSource are unchanged). Also matchesSourceIdGlob and toToolSourceRef. See docs/sdk/toolsets.md.

      filtered's { metadata } selector and requireApproval compose with the ToolDefinition.metadata and requiresApproval fields (see the tool-requires-approval-and-metadata changeset). Existing callers must migrate from ToolRegistry to Toolset as described in the tool-registry-removed changeset.

    Patch Changes

    • —

      567ada8: The AG-UI adapter now reads tools and review metadata from the query's toolsets, preserving review exemptions and per-tool timeouts after the ToolRegistry removal. Hosts must return QueryParams.toolsets from createQuery instead of passing a ToolRegistry as tools. Wrap definitions with toolset(source, definitions) and require @namzu/sdk >=48.0.0; use @namzu/ag-ui 2.x with SDK 45.1–47. MCP resource tools likewise rely on their owning toolset for source trust instead of a removed per-tool provenance field.

    • —

      567ada8: CLI MCP tool names change from mcp_<server>_<tool> to mcp__<server>__<tool>. Update any permissions, saved prompts or integrations that refer to those names; the CLI warns once for each configured permission rule still using the old form. MCP tools now update after server notifications and reconnects. Configure allow, deny, maxRetries, requireApproval, readOnlyHintTrusted and instructions per server under mcpServers; instructions: true includes the server's text as labelled untrusted request context. /mcp tools shows the server's current tool names and instructions.

      The SDK now skips prompt discovery when a server did not advertise prompt support and keeps approval-wrapped tool definitions stable across unchanged toolset snapshots.

    • —

      567ada8: Fixed: combineToolsets dropped every input toolset's own availability ('active'/'deferred') — the merged Toolset it returned had no availability field, which ToolManager and everything else that reads it defaults to 'active', regardless of whether some of the merged inputs were wrapped with deferred(...). query() already avoided this by keeping its own eager and deferred halves as two separate array entries instead of combining them (see its own comment in runtime/query/index.ts), but combineToolsets itself stayed exported with this defect for any other caller.

      combineToolsets now refuses to merge toolsets that disagree on availability, naming both sources, instead of silently reporting the deferred side's tools as active. Toolsets that agree — all active, or all deferred — combine as before, and the combined toolset now carries availability: 'deferred' when every input does. No current caller passes a mixed set (this was latent, not user-visible); a caller with a genuine eager/deferred split keeps them as two separate entries in its own toolsets array, the way query() does.

    • —

      567ada8: combineToolsets now releases earlier live-toolset listeners if a later source fails to subscribe. Its unsubscribe also attempts every inner cleanup when one throws, so failed setup and teardown do not leave other sources observing a finished manager.

    • —

      567ada8: MCP tool definitions that change after initial admission keep serving their earlier definition across turns; new and removed names still update. /mcp and exec --json now report server discovery changes and current policy refusals, including changed definitions held for review.

    • —

      567ada8: Former MCP permission-name warnings now use the CLI's structured logger instead of writing plain text to stderr. Consumers of exec --json stderr can continue parsing log records when a legacy permission rule is configured. Plugin MCP disconnect warnings use a fixed message and put the operation in a log attribute.

    • —

      567ada8: Shipped prose no longer names other named products. matchesToolSelector's doc comment (packages/sdk/src/tools/roster.ts) and Tool metadata and selectors describe its synchronous-only design against "some other agent frameworks' equivalent selector" instead of one by name; Tool servers describes the wider ${VAR}/${VAR:-default} interpolation some MCP clients accept as "several desktop and editor MCP clients" instead of naming two. Wording only: no type, default or behaviour changed.

    • —

      567ada8: Invalid JavaScript plugin tools now fail when a plugin is enabled, with the plugin and module named, if they omit a usable inputSchema. Before, a deferred plugin tool could load successfully and then crash the turn with a Zod _def error after search_tools revealed it. Supply a Zod inputSchema, or a compatible parser and explicit modelInputSchema.

    • —

      567ada8: @namzu/cli: the session's tool roster is now composed from named Toolsets (@namzu/sdk's toolsets, plan.md v3 §8) instead of a ToolRegistry. This is internal composition, not a config or CLI-flag change — nothing in namzu.config.json or the command line changes — but it is a breaking change for anything that imported @namzu/cli's internals directly:

      Removed / changedReplace with
      createCliPluginRuntime(config, tools: ToolRegistry, cwd, hooks?, skillTool?)createCliPluginRuntime(config, cwd, hooks?) — it no longer takes or mutates a tool registry. Read runtime.skills.size and fold runtime.manager.toolsets into your own toolsets list; the skill tool is your own toolset now, not something this runtime registers.
      SubagentRuntimeOptions.buildTools: () => ToolRegistryContractbuildTools: () => readonly Toolset[].
      SubagentRuntimeOptions.configureWebSearch?: (provider, model, tools: ToolRegistryContract) => WebSearchConfig | undefinedconfigureWebSearch?: (provider, model, toolsets: readonly Toolset[]) => WebSearchConfig | undefined — a pure decision now; it no longer removes web_search itself, so a caller building the child's final toolsets filters it out when this returns truthy (filtered(ts, (t) => t.name !== 'web_search')).
      McpConnection.tools: readonly ToolDefinition[] (still present)McpConnection.toolsets: readonly Toolset[], two per connected server (main tools and deferred resources, both kind mcp_server, id mcp:<server>) — prefer these; .tools is now just the flattened union across all of them.

      Additive: AgentSession gained a presenter: ToolPresenter field — the session's real tool-call/result presenter, for a host that renders events outside send()'s own stream. The ACP bridge (namzu acp) now uses it instead of a presenter built over a permanently empty registry, so tool-specific presentCall/presentResult views (a richer diff, a custom label) render over ACP for the first time; every ACP tool view had silently fallen back to the generic label before.

      Behaviour: because tool availability is now derived from the turn's own revealed-tool history (@namzu/sdk's ToolManager) rather than a per-send registry fork, a tool search_tools reveals in one send now stays active in a later send of the same session, and after namzu resume — it no longer resets every send.

      @namzu/sdk: two combineToolsets usage bugs, found while wiring the CLI onto toolsets, are fixed — query()'s own generated tools and SupervisorAgent's coordinator tools are now two separate array entries (an eager one and a deferred one) rather than one combineToolsets'd together, so runtimeToolOverrides: { name: 'deferred' } (task tools, coordinator tools) is honoured again instead of silently falling back to 'active' for both halves. query() also no longer adds its own search_tools when a caller's toolsets already contribute a tool under that exact name (a connector genuinely named search_tools), matching the pre-toolsets registry's behaviour and avoiding a spurious ToolsetConflictError. Pure bug fixes, no API change.

  9. v46.0.0

    Major Changes

    • —

      82769f1: computer_use now shows the model a screenshot sized for it and reads every coordinate as a pixel of that screenshot. On a display larger than 1568 px (or 1568 28-pixel patches) the coordinates a caller sends mean something different from before, so this is a major release.

      What changes for a caller of the tool

      • —Each capture is fitted to STANDARD_SCREENSHOT_LIMITS (Anthropic's standard tier, within OpenAI's detail: "high" budget) and numbered s1, s2, …. mouse_*, scroll and zoom coordinates are pixels of the latest screenshot (or of screenshot_id) and are mapped onto the display; a coordinate outside the screenshot is refused. On a display that already fits, the mapping is the identity. To keep host pixels, pass screenshotLimits large enough that nothing is resized — but a model will then be sent an image its provider shrinks or rejects.
      • —Coordinate actions, type_text and key are refused until the tool instance has taken a screenshot. Call screenshot first.
      • —Results changed shape. An action returns <label>: done plus, by default, a new screenshot after 500 ms (settleMs, screenshotAfterActions: false to turn it off), in content with a text block first; output is no longer "ok". cursor_position returns text in screenshot pixels, not JSON in host pixels. data carries steps and screenshot (id, width, height, display).
      • —screenshot, zoom, cursor_position, wait and list_windows are now read-only, so a review policy that exempts read-only calls no longer asks before them. Keep asking with an explicit ask rule for computer_use.
      • —ActionInput gains zoom, wait, list_windows, focus_window and batch, and an optional screenshot_id; code that switches exhaustively over input.type needs the new cases.

      New

      • —createComputerUseTool(host, options) with screenshotLimits, settleMs, screenshotAfterActions, maxBatchActions (20), maxWaitMs (10 000) and unavailableReason.
      • —{ type: "batch", actions: [...] }: checked in full first, run in order, stopped at the first failure (which it names), one screenshot at the end.
      • —zoom (full-resolution crop, through captureRegion when the host declares regionCapture), wait, and list_windows/focus_window when the host declares windows.
      • —computerUseUnavailableReason(provider): why a provider that cannot put an image in a tool result cannot drive the tool; pass it as unavailableReason.
      • —screenshotTargetSize, STANDARD_SCREENSHOT_LIMITS, HIGH_RES_SCREENSHOT_LIMITS and the types ComputerUseToolOptions, ScreenshotLimits, ImageSize.
      • —New dependencies fast-png and pica (pure JavaScript and WASM, MIT; no native build), loaded only when a capture has to be resized.
    • —

      82769f1: computer_use can read a window's controls and act on them by reference, on a host that declares uiTree — which the Windows backend of @namzu/computer-use now does.

      What changes for a caller of the tool

      • —Two new actions when the host declares uiTree and implements uiSnapshot and uiAct: ui_snapshot { window_id? } returns the window's accessibility tree as text, each control that can be acted on with a ref such as e12; ui_act { ref, action, value? } performs invoke, set_value, toggle, select, expand, collapse, focus or scroll_into_view on it and returns a screenshot afterwards. ui_act may be in a batch; ui_snapshot may not. Refs count up across snapshots and only the latest snapshot's are accepted, so a stale ref is refused rather than acting on another control. ActionInput gains both, so code that switches exhaustively over input.type needs two more cases — that is why this is a major release for @namzu/sdk.
      • —ui_snapshot is read-only; ui_act is destructive.
      • —list_windows now returns its window lines inside an untrusted-content frame (<namzu-untrusted kind="desktop-windows">), since titles are whatever each application shows. The lines themselves are unchanged; code that parsed the whole output text needs to skip the frame's three header lines.
      • —createComputerUseTool returns a ComputerUseTool, a ToolDefinition<ActionInput> with describeUiRef(ref) (experimental), which names the control a ref points at for a review screen.
      • —UiSnapshot gains optional title and app; UiElement.ref is empty for an element nothing can be done with. Both types stay experimental.

      @namzu/computer-use

      On Windows and WSL with the cua-driver backend, capabilities.uiTree is true and SubprocessComputerUseHost offers uiSnapshot(windowId?) and uiAct(ref, action, value?), read from cua-driver's UI Automation walk. A control is invoked in the background, without moving the pointer or bringing its window to the front; a field without a settable value is typed into when it is empty. An action whose driver died mid-request comes back as not done with an unknown outcome, never retried. The PowerShell fallback and the other platforms do not declare uiTree, so nothing changes there.

    • —

      82769f1: The interactive CLI now asks once per session before the model first sees your screen, and strict mode no longer lets a screenshot run on its own — that second part is why this is a major release for @namzu/cli. In prompt, accept-edits and plan mode the first computer_use call that would send the screen to the provider — a screenshot, a zoom, the list of open windows, a window's controls, or an action that returns a screenshot — opens a "Share your screen" box naming the provider; after a yes, later screenshots in that session run without asking, and clicks, typing and ui_act are still reviewed as before. A mode switch keeps the answer; a new session (/new, /clear, /model) asks again. auto never asks. strict now refuses a screenshot unless a rule allows it: to keep screenshots running in strict, add permissions: { computer_use: 'allow' } (which also allows clicks) or a narrower rule for the actions you want.

      Approving a ui_act call shows the control by name (Press Button "Beş" (e30)), and ui_snapshot as the window it reads.

      @namzu/sdk

      • —ToolDefinition.capturesScreen?(input) and defineTool({ capturesScreen }) declare which calls send the operator's screen to the model provider. computer_use declares its observations and every action followed by a screenshot.
      • —Breaking: the authorization gate's allow_read_only rule (allowReadOnlyTools: true, in every shipped preset) no longer allows a call that declares capturesScreen; it goes to the review policy instead. With createReviewHandler and no screenConsent nothing changes (a screenshot is approved as a read); a custom ResumeHandler now sees computer_use screenshots, zooms, window lists and UI snapshots. To keep them out of review, add an explicit rule such as { type: 'allow_by_name', toolNames: ['computer_use'] } (which allows clicks too).
      • —createReviewHandler / createReviewPolicy take screenConsent: { sessions: Set<string> } and an optional capturesScreen(name, input) predicate (default: the tool's declaration from registry). With them, the first such batch in a session is put to prompt with the new ToolReviewRequest.screenConsent: true; strict refuses it unless a rule allowed it, auto does not ask, and without a prompt it is refused with the new SCREEN_CONSENT_UNATTENDED_REFUSAL. A no is SCREEN_CONSENT_DECLINED_FEEDBACK. New type ScreenConsentRecord. Without screenConsent nothing changes.

    Minor Changes

    • —

      82769f1: ComputerUseHost can now describe the display it captured and offer window and region operations. Everything is additive and optional, so an existing host compiles and behaves as before.

      • —ScreenshotResult.display (DisplayInfo: id, x, y, width, height, scaleFactor, primary) says which display a capture shows. Set it in a host you maintain; without it the SDK assumes one display at the origin, the size of the capture, at scale factor 1.
      • —The contract is now written down as physical pixels everywhere: action points are relative to the display last captured, window bounds are virtual-desktop pixels. A host that clicks in logical units (points, or a DPI-unaware process) must convert at its own boundary.
      • —Optional methods listWindows(), focusWindow(id) and captureRegion(rect) are used only when the new capability flags windows and regionCapture are true. uiSnapshot()/uiAct() behind uiTree are experimental and may change in a minor release.
      • —New exported types: DisplayInfo, Rect, WindowInfo, FocusWindowResult, UiElement, UiElementAction, UiSnapshot, UiActResult.
    • —

      28102e1: A scheduled run can no longer reach the scheduler through a shell the command-line reader does not follow. powershell -c 'namzu schedule stop', pwsh -command '…', fish -c '…', tcsh -c '…' and bash.exe -c '…' were allowed whatever their text held: the scheduled-run floor took any shell at the head of a command with a -c option as read, but only the payloads of sh, bash, dash, zsh, ksh, ash and mksh (and busybox running one) are. Such text now goes through the floor's tripwire like any other text a program runs as code. A job whose commands use those shells without naming the scheduler, NAMZU_HOME or the browser profiles runs as before.

      SDK: new exports nestedShellCommand(words), NESTED_SHELLS and the type NestedShellCommand. nestedShellCommand says, for one simple command's words (without its leading assignments), which -c payload lexShellCommandLine reads as a command line of its own, why it made the line opaque instead, or null when it reads none. The lexer uses it itself, so its reading is unchanged.

    • —

      28102e1: A scheduled run may now run a command whose code merely mentions namzu. powershell.exe -NoProfile -Command "[System.Windows.MessageBox]::Show('Namzu: scheduled job running','Namzu')" was refused by the scheduled-run floor, because text passed to PowerShell, cmd, Python, Node or a shell reading its input was refused whenever it contained namzu or schedul. Such text is now refused only when it holds something that can reach the scheduler or NAMZU_HOME: a schedule subcommand other than list, show, status, history or logs with the CLI or an expansion in reach, the service's name (namzu-scheduler…, com.namzu.…), a service tool with a namzu name or a *, pkill/killall, NAMZU_HOME by name, .namzu as a path segment, a path into either protected folder, or LOCALAPPDATA with a namzu segment. A job that was refused for naming the product runs; nothing that reached the scheduler before is let through (checked against bash on 35 159 generated lines, and on 1 189 labelled lines of PowerShell, cmd, Python, Node and sh code).

      The refusal now says which rule matched and where (the argument `~/.namzu/x` names NAMZU_HOME (/home/you/.namzu), … it holds `schedule stop` (a `namzu schedule` subcommand other than …), in the argument `namzu schedule stop` ) instead of listing everything the floor protects.

      SDK: a predicate authorization rule may carry describe(call), asked after decide returned a decision; its answer is the gate's reason for that call instead of the rule's fixed description (a null, empty or thrown answer keeps description). describeRule(rule, call?) takes the call as an optional second argument. Both are additions; existing rules and callers are unchanged.

    • —

      28102e1: In the TUI, the model's schedule tool now lists every scheduled job, not only those of the session's folder. A model that created a job in a folder below the session's and then called list was told "No scheduled jobs." while namzu schedule list showed the job, because the TUI's host filtered by folder unless the model passed allFolders: true, which models do not. Jobs of the session's own folder are marked (this folder) and are still the only ones whose prompt the model sees.

      SDK: ScheduleJobSummary gains an optional inSessionFolder, which the schedule tool prints as (this folder) on the job's list line. A host may use it to list every job regardless of allFolders. The tool still asks host.list({ allFolders: false }) unless the model sets it, so a host that filters by folder behaves as before.

    • —

      28102e1: The model can change a scheduled job instead of deleting and recreating it. The schedule tool has a new action, update: job names the job, and only the fields the model sets change (prompt, when, folder, tz, budget, or permissions as the whole new set, under the same limits as create). Asked whether it could change a job, a model with no such action deleted the job and created it again, and the job's history was lost.

      In the TUI the operator confirms every change on one screen: what changes first (the -/+ lines namzu schedule edit shows), a THE PERMISSIONS CHANGE warning when a run's rules, unmatched, execution or browser grant change, then the job as it will run. Cancel is the default. Save writes the same job — its id, creation time and history kept — with the operator's new confirmation, keeps a paused job paused, and records edited by tool with the changes in its history. A change is refused if the job was changed while the operator was being asked. Like create, update is not preceded by the ordinary permission review in prompt, accept-edits and auto.

      SDK: ScheduleToolHost gains three optional methods, previewUpdate(job, changes), confirmUpdate(request, signal?) and update(preview), and new types ScheduleJobChanges, ScheduleJobUpdateProposal and ScheduleUpdateRequest. A host without them keeps working: the tool refuses update there and tells the model not to delete and recreate the job.

    • —

      28102e1: The skill tool can tell the model which directory a skill's files are in (#536).

      New createSkillTool({ resolveModelDirectory }), with the types SkillToolOptions, SkillDirectoryResolver, SkillDirectoryRequest and SkillDirectoryContext. resolveModelDirectory(skill, { sandbox }) receives the registered name and the directory the host reads the skill from, and returns the directory the model's tools can open, or undefined when they cannot reach one. With it:

      • —a load opens with [Skill directory: <dir>. …] on its first page and reports data.directory; when the resolver answers undefined it says instead that the skill's directory is not reachable in this session and not to search the filesystem for it;
      • —the listing (skill without a name) gives each skill's directory and no longer the registry's location, which is the host's path;
      • —${CLAUDE_SKILL_DIR} in allowed-tools expands to the resolver's directory, and an entry using it is ignored when there is none.

      The skill is still loaded from the registry's own path. SkillTool is createSkillTool() and behaves exactly as before, so nothing changes unless you pass the option. SkillRegistryRef.catalog() entries may now carry an optional directory, and SkillRegistry.catalog() sets it to the skill's directory; a registry that omits it still satisfies the interface.

    Patch Changes

    • —

      28102e1: The two errors that tell a model which tools it can use now give the same list: the tools the current step can call, meaning registered, active and allowed by allowedTools or prepareStep. Before, they could send a model back and forth between them until the run was stopped. A call refused as Tool "X" is not available on this step repeated the step's allow-list word for word. That list is taken when the request is built, so it could name a tool unregistered since (a connector that disconnected) or one that is deferred or suspended. A call to a name the registry does not hold listed the whole registry, including the tools the step refuses.

      That second error now reads Unknown tool "X". Available: … instead of Unknown or unavailable tool "X": Not found: "X". Available: …. (none) means the step can call nothing. This applies to a model's direct call, to a batch run without review preparation, and to a call a tool makes through ToolContext.dispatchTool (such as run_code). ToolCallRepairContext.message for unknown_tool carries the new wording; availableTools still lists every registered tool. No exported types change. Code that parses the old Not found wording out of a tool result must look for the new wording instead.

    • —

      82769f1: computer_use clicks and drags with the left button when a call leaves button out, instead of refusing the call (a model asked to click the Start button sent no button and spent a round trip on the refusal). Its description now tells the model to batch the steps it can predict but to look before typing into a window a step should have opened, and, on Windows, that a shell command is the surest way to start a program (Start-menu search on a non-English Windows turned an English program name into a web search in the browser).

      With a host that lists windows (the Windows cua-driver backend), type_text and key are refused while a terminal is in front: in a real session a key chord that did not open the Run dialog sent "notepad" and ENTER into the user's own terminal and submitted what they were typing. A call that types into a terminal on purpose now fails with that reason; bring the target window to the front first, or run the command through a shell tool. With a host that reads controls, the first screenshot says so. No change is needed on your side.

    • —

      28102e1: claude-opus-5-5, claude-fable-5-1 and claude-mythos-5-1 now have prices in the built-in catalogue. Before, their tokens were reported as unpriced. A turn that set turnConfig.costLimitUsd without its own pricing table was refused at the start on these models, or stopped with cost_unmeasurable when it reached one through a step or a fallback.

      The rates are the vendor's list prices per million tokens:

      ModelInputOutputCache readCache write (5 min)
      claude-opus-5-5$4$20$0.20$5
      claude-fable-5-1, claude-mythos-5-1$10$50$0.25$12.50

      On these three models a cache read costs less than the usual 0.1x of input: 0.05x on Opus 5.5, 0.025x on the 5.1 pair. The catalogue has one write rate, the five-minute one the Anthropic driver requests, so the one-hour rates ($8 and $20) are not represented. Nothing to change on your side.

    • —

      28102e1: claude-sonnet-5 is priced at $2 input and $10 output per million tokens, with $0.20 for a cache read and $2.50 for a five-minute cache write. The catalogue had $3 / $15 / $0.30 / $3.75. That was the increase the vendor scheduled for 2026-09-01 and then cancelled; $2 / $10 is now its standard price.

      What changes for you: a Sonnet 5 turn reports two-thirds of the cost it reported before. A turnConfig.costLimitUsd set against Sonnet 5 now allows about 1.5 times as many tokens before it stops the turn. To keep the old ceiling, lower the limit to two-thirds of its value, or pass your own pricing table.

  10. v45.1.0

    Minor Changes

    • —

      2b96136: Add the browser tool contract, let argument_pattern rules ask for review, and let a scheduled-job proposal carry a browser grant.

      Browser tools (new). createBrowserTools(host) returns two tools over a BrowserHost you supply: browser (navigate, back, forward, reload, snapshot, screenshot, scroll, wait_for, tabs) and browser_act (click, type, fill_form, select, press, hover, upload, dialog). The SDK ships no browser engine. Every browser_act call carries a required origin, and the host contract says the host compares it with the live page before acting. url and origin are canonicalised by the input schema (WHATWG form, punycode, no default port, no trailing dot on the host). Other schemes, credentials in the URL and cloud metadata addresses in any spelling are refused. Because the gate sees the canonical value, a site rule matches the address the browser loads. Page text is returned inside wrapUntrusted, under a Page: <origin> — "<title>" (tab tN) header. Hosts refuse by throwing structural shapes (browser_origin_mismatch, browser_stale_ref, browser_human_required, browser_outcome_unknown, browser_site_denied). New exports: createBrowserTools, BROWSER_TOOL_NAME, BROWSER_ACT_TOOL_NAME, isBrowserCallReadOnly, browserHostErrorOf, formatBrowserPageHeader, canonicalizeBrowserUrl, canonicalizeBrowserOrigin, canonicalizeBrowserSitePattern, isCloudMetadataHost, BROWSER_URL_MAX_LENGTH, BROWSER_SNAPSHOT_MAX_CHARS, BROWSER_WAIT_MAX_MS, BROWSER_FILL_FORM_MAX_FIELDS, and the types in their signatures (BrowserHost, BrowserCapabilities, the action, result and error types). Both tools are category: 'network'. Do not pair them with a preset that allows network tools by category because a sandbox confines egress: the browser runs outside the sandbox.

      argument_pattern with decision: 'review' (widened union). AuthorizationRule's argument_pattern variant now accepts 'review'. It matches as deny does: the whole value, then every segment of a command line. Code that switches exhaustively over that variant's decision, or narrows it to 'allow' | 'deny', needs a review branch. Existing rules behave as before.

      ToolDefinition.urlArgument (new, optional). A tool whose input schema canonicalises an argument to a URL can declare it. An argument_pattern rule on that argument then tests the value whole, instead of cutting it at &, ; or | as a command line, which made an allow for a site decline every address with a query string. Tools that do not declare it are unchanged. defineTool accepts it too.

      Reason text. A custom_pattern rule with decision: 'review' is now described as sent for review by a pattern rule …; it used to say allowed by …. If you match on the old text, update the match.

      Scheduled jobs. ScheduleJobDraft.permissions.browser?: ScheduleBrowserGrant (profile, sites mapping each site to read, ask or act, headed?) and ScheduleToolHost.browserGrants?: boolean are new. The schedule tool refuses a proposed browser grant unless your host sets browserGrants: true, so a host that does not know the field keeps working unchanged and never silently drops a grant. It canonicalises site keys and refuses *. It also treats the grant as network access, so the grant is refused beside a shell on the host. The refusal for web access beside a host shell now says "web or browser access".

      See docs/sdk/browser-tools.md and docs/sdk/review-policy.md#rules-that-ask.

    • —

      2b96136: A generic tool result view may now set outcome: 'cancelled' for a call the person declined on the tool's own screen. save_skill and the schedule tool's create, resume and delete use it (the schedule tool also sets data.cancelled on those results), and the TUI shows the row as ○ … Cancelled — nothing was saved instead of ✗ … failed: Error: The operator cancelled. The model still receives the same refusal text.

    • —

      2b96136: A tool call a person declines without giving a reason now tells the model not to get the same content or result another way — another tool, another site or address, or a web search — unless it asks first and the person agrees. The text was "User declined to run the proposed tool(s)." (and "The user rejected this tool call." on a resumed decision); in a live session a model whose browser navigation was declined fetched the same page through web search instead.

      New export: DECLINED_TOOL_CALL_FEEDBACK, the text. A host that passes its own feedback with a refusal is unchanged. A test or host that matched the old default text must match the new one.

    • —

      d233f26: Add a predicate authorization rule, and export the command-line lexer the gate uses.

      • —AuthorizationRule gains { type: 'predicate', description, decide }. decide(call) receives the tool name, input, definition and the commandDialect the line runs in (sh when the caller did not say) and returns allow, deny, review or null; it sits in its place in the rule list, after the dangerous-command floor. A decide that throws is read as deny. New types AuthorizationPredicate and AuthorizationPredicateCall.
      • —lexShellCommandLine is exported, with ShellLexResult, ShellCommand, ShellWord, ShellRedirection and ShellLexOptions, so a predicate reads a command line exactly as the gate does.
      • —A lexer result also reports compoundWords (a for or select loop's variable and list, a case statement's subject and patterns) and, on a here-document redirection, its body.

      Nothing existing changes behaviour. AuthorizationRule is also the type of GateEvaluationResult.matchedRule: code that switches over rule.type with an exhaustive check (const x: never = rule) needs a predicate case or a default: branch to compile.

    • —

      2b96136: ScheduleToolHost.create() may return an optional note, which the schedule tool appends to what the model is told about the new job. Existing hosts that return only { name } are unaffected. The CLI uses it to tell the model that no scheduler is installed, so its reply no longer promises a run that cannot happen until you run namzu schedule install.

    • —

      d233f26: Add a schedule time engine and evaluator, and the schedule and session_loop tools.

      New exports, all additive: parseScheduleSpec, parseCronExpression, nextFireTime, previousFireTime, upcomingFireTimes, countOccurrences, describeSchedule, validateTimeZone, hostTimeZone, parseDuration, evaluateJob, ScheduleValidationError, SCHEDULE_CATCH_UP_WINDOW_MS, SCHEDULE_LATE_GRACE_MS, buildScheduleTools, buildSessionLoopTools, scanSchedulePrompt, revealHiddenCharacters, SCHEDULE_TOOL_NAME, SESSION_LOOP_TOOL_NAME, generateScheduleJobId, generateScheduleRunId, and the types in their signatures (ScheduleSpec and its three variants, CronExpression, ScheduleEvaluationInput, ScheduleDecision, ScheduleSkipReason, ScheduleMissedReason, ScheduleToolHost, ScheduleJobDraft, ScheduleJobPreview, SessionLoopHost, SessionLoop and their neighbours).

      Everything here is pure: Intl for time zones, no clock, no I/O, no new dependency. Cron follows cronie's DST rule (a fixed-time job fires once across a change; a wildcard job fires at every matching instant). Nothing existing changes. ScheduleDecision, ScheduleSkipReason and ScheduleMissedReason may gain members in a later minor release, so switch over them with a default: branch. See docs/sdk/schedules.md.

    • —

      2b96136: loadSkill (and so SkillRegistry) skips frontmatter keys it does not read, whatever YAML they are written in, instead of refusing the whole file: a SKILL.md carrying argument-hint: [file] or a hooks: block with a list now loads with those fields ignored. The keys it does read are still parsed strictly; they are exported as SKILL_FRONTMATTER_KEYS. disable-model-invocation: true now reads as invocation: operator; a value other than true or false, or one that contradicts an explicit invocation, refuses the file. parseFrontmatter takes an optional third argument, { readsKey }, to get the same leniency for a caller's own vocabulary; without it every key is parsed and refused as before. A skill file that used to be refused over an unread key now loads, and one with disable-model-invocation: true is no longer offered to the model — nothing else changes.

    • —

      2b96136: A tool can now stop the turn for a person: return handoff: { kind: 'human-required', reason, detail? } on its ToolResult (new type ToolHandoff). The kernel commits the batch's results, writes a checkpoint and ends the segment with turn_paused instead of calling the model again. The event and its session-log record carry the new optional handoff field, and the SSE (turn.paused) and A2A bridges forward it. resumeSession continues the turn from that checkpoint as it does after a provider pause, with a model call that sees the results. In a delegated child (a turn with parentSessionId) the handoff fails the child's turn with a non-retryable tool_error naming the reason, so the parent receives a failed child result.

      Nothing changes for tools that do not set the field. A consumer that switches exhaustively on turn_paused fields, or validates session-log records with its own strict schema, should accept handoff.

    Patch Changes

    • —2b96136: The browser tool's cursor field tells the model it is only the nextCursor a previous snapshot returned, to be left out for the top of the page, and never a URL. A model passed the page's address as the cursor and got a refusal before it could read the page.
    • —2b96136: A browser_human_required refusal from a browser host now pauses the turn. The browser tools set ToolResult.handoff (reason in words such as https://github.com is showing a sign-in page; detail carries tool: 'browser', cause, origin, profile and loginCommand) as well as data.handoff, so the kernel stops before the next model call and waits for the person, as it does for any tool handoff. Before this, the model read the refusal and carried on. Nothing changes for other refusals.
    • —2b96136: The coding-agent working doctrine now tells the model never to change git configuration (user.name/user.email, hooks, remotes, credential helpers) or other persistent settings without asking. When a commit fails because no identity is set, it tells you the command instead of inventing an identity.
    • —2b96136: The schedule tool's input schema now says that budget limits one run and that maxIterations counts model steps, not repetitions of the job. A model proposed maxIterations: 1 for a job meant to post once per run, and every run stopped after its first model call. The CLI's confirmation of a job, on a terminal or in the TUI, warns when it allows fewer than 10 iterations.
    • —2b96136: The schedule tool asks the model to leave optional fields (folder, time zone, execution, budget, a visible browser window) unset unless the user asked for them, and the TUI's confirmation of a proposed job marks every such value the model set that differs from what you would get by default, e.g. Chosen by the model, not the default: time zone America/New_York, not this machine's Europe/Istanbul.
    • —2b96136: A job the model proposes, resumes or deletes in the TUI is confirmed once, on the job's own screen. The permission review no longer asks "Do you want to run schedule?" first in prompt, accept-edits or auto. plan and strict still refuse the call, a schedule rule of ask or deny still applies, and pause is still reviewed. The SDK's schedule tool no longer declares delete destructive, since the host confirms it before anything is removed; a host that relied on that flag to review deletes should add an ask rule for schedule.
    • —2b96136: The schedule tool no longer refuses a proposal that uses the read-only preset with web or browser access as "a shell on the host". The preset denies bash, but the check looked only at explicit rules and unmatched, so { preset: 'read-only', unmatched: 'park', browser: … } was refused. A proposal with edit-in-folder, or with a bash rule that is not deny, is still refused.
    • —2b96136: The schedule tool's input schema describes budget.tokenBudget as what the whole run may spend, with every model call resending the prompt, and the CLI's confirmation of a job warns when it allows fewer than 50 000 tokens. A model proposed 4 000 tokens for a browser job whose runs each took about 110 000.
    • —2b96136: The skill tool now presents its calls as Read skill <name> (or List skills) and hides a successful result, so a host shows one row instead of the raw input and the skill's body. In the TUI, the Working row says Waiting for you while the screen that saves a skill is open, instead of counting on as if the turn were working.
  11. v45.0.0

    Major Changes

    • —

      2e2ea14: A skill's allowed-tools now pre-approves its tools. It no longer restricts the tool set.

      The field comes from the Agent Skills format, and that format defines it this way: the listed tools skip the approval prompt for the rest of the turn that loaded the skill, and every other tool stays callable. Namzu read it the other way. After the skill tool loaded a skill, the next batch was narrowed to the listed tools and the model was told to "restrict yourself to" them. A skill with allowed-tools: Read Grep therefore took bash away, and the model stopped doing the work.

      What breaks in @namzu/sdk

      • —A loaded skill no longer narrows ToolContext.allowedTools. If a host relied on allowed-tools to confine the model, it should narrow the turn itself with QueryParams.allowedTools, a step's allowedTools, or deny rules.
      • —ToolContext.adoptSkillScope is deprecated. The kernel never supplies it, so a tool that calls it through ?. now does nothing. It will be removed in the next major. Use ToolContext.grantSkillTools.
      • —createReviewHandler / createReviewPolicy in prompt and accept-edits modes now approve a batch without asking when every call it would ask about is covered by a skill loaded earlier in the turn. To keep asking about every call, pass skillGrants: 'ignore'. plan and strict still refuse such calls, and an operator deny or ask rule, a destructive call, or a path outside the roots or the sandbox is never covered. Each approval made this way is written to the audit trail under the skill's name.
      • —The bash tool runs bash -c instead of /bin/sh -c wherever bash exists (the first bash on PATH, then /bin/bash, then /usr/bin/bash), including for background jobs. On a host whose /bin/sh is dash or busybox sh (Debian, Ubuntu, Alpine), commands now run in bash; a command that relied on dash behaviour, such as echo expanding \n, behaves as bash does. BASH_ENV, ENV, SHELLOPTS, BASHOPTS and BASH_FUNC_* are removed from its environment. To keep /bin/sh, set NAMZU_BASH_SHELL=/bin/sh in the environment of the process that runs the SDK. In a sandbox the tool now passes the guest /bin/sh -c '<launcher>' sh '<command>', which runs bash when the guest has it; a custom Sandbox.exec or spawnDetached that inspected its arguments for ['-c', command] sees the launcher instead. With no bash, the host still runs /bin/sh -c.
      • —A command line is read for the shell that runs it. AuthorizationGate.evaluate, evaluateRule and SkillGrantSet.coveringSkill called without a dialect read it for any POSIX shell (sh), so an allow rule or Bash(<pattern>) entry no longer approves a line using a bash-only construct ($'…', &>, |&, <<<, [[, arrays, brace expansion, time and others) unless the caller says the shell is bash. The kernel says so for the bash tool on a host that has bash; inside a sandbox it reads in sh, because the guest may not have bash. A host calling the gate itself passes commandDialect: 'bash' in the ToolCallContext (or { commandDialect: 'bash' } as the last argument of evaluateRule and coveringSkill) when the command will run in bash. Deny rules are unaffected: they still see every command.
      • —parseAllowedTools splits on whitespace as well as commas, and keeps Tool(pattern) entries whole. "read write edit" used to be one name and is now three.
      • —The skill manifest in the system prompt renders the field as <pre_approved_tools> instead of <allowed_tools>. The skill tool's notice lists what was pre-approved and what was ignored, and says every other tool remains available.

      Added: ToolDefinition.commandDialect (and the defineTool option), ToolCallContext.commandDialect, EvaluateRuleOptions, the ShellDialect type, and NAMZU_BASH_SHELL; ToolContext.grantSkillTools, ToolCallSummary.skillGrant, approve_tools.skillGranted, ReviewPolicyOptions.skillGrants, SkillGrantSet, compileSkillGrant, SKILL_TOOL_NAME_ALIASES, permissionPatternToRegExpSource, and a FrontmatterOptions third argument to parseFrontmatter (lists), which the skill loader uses so that allowed-tools can be a YAML list. Names are matched case-insensitively and through the format's aliases (Read → read, WebFetch → web_fetch). Bash(git status *) uses the CLI permission-table glob, applied to each command in the line. ${CLAUDE_SKILL_DIR} and ${NAMZU_SKILL_DIR} expand to the skill's directory. An unknown name is ignored and reported, and never widens the grant. A tool that is destructive for every input (the shipped write and run_code) is ignored and reported too, because each of its calls is reviewed anyway. BashOutput, KillShell, TaskOutput and TaskStop map to job, and TaskCreate, TaskUpdate and TaskList to task_*. ToolContext.grantSkillTools returns a commit(), and the skill tool records the grant only once it has delivered the skill's instructions. The grant also ends when the operator sends a message while the turn is still running (inboundMessages or steering), through the new SkillGrantSet.clear(). A Bash(<pattern>) entry never covers a line that redirects output into a file (git status > ~/.bashrc); /dev/null and 2>&1 stay covered. A whole-tool Bash entry grants the tool as it is, writes outside the working directory included, because bash has no path argument for the escalation check. An entry naming a tool that allowedTools withholds from the turn or step is ignored as not available, and SkillGrantToolResolver gains an optional unavailable field for that.

      @namzu/cli: a plugin skill's allowed-tools pre-approves for the turn and no longer takes tools away. SKILL.md files whose allowed-tools is a YAML list are now listed instead of refused. The [permissions] glob now comes from the SDK, and it matches the same commands as before. permissionChecks read a builtin tool's command line in the dialect that tool reports, as the runtime does.

    Patch Changes

    • —

      2e2ea14: Permission rules on a command line now read it the way bash does. A deny rule catches commands it used to miss, and a few lines that used to be approved or refused by accident are now decided on what they run.

      An argument_pattern rule on a command argument, a skill's Bash(<pattern>) entry and the check that such an entry never covers a write through redirection used to rely on three separate readers of bash quoting, which disagreed in places. With Bash(git status *) granted, git status $'\'' ; touch pwned #' and git status $'\'' > ~/.bashrc #' were pre-approved, because $'\'' was read as a closed quote and an open one. One lexer now serves all three (see docs/sdk/command-lines.md). It was checked against bash 5.2 and 5.3 on 2.9 million generated lines with no disagreement.

      What changes for a host:

      • —A deny rule also matches each command's words after quote removal. ^git push now denies 'git' push, g\it push, $'\x67it' push, GIT_DIR=. git push, bash "-c" "git push" (the quoted -c is still the flag) and bash -lc 'git push'. None of these were denied before.
      • —A line whose only quoting is an ANSI-C quote is decoded rather than refused: an allow rule or a Bash(<pattern>) entry that matches git status $'-s' now approves it, since it runs git status -s. > $'/dev/nul\x6c' is /dev/null and is not a write.
      • —A here-document body is data, not commands, so cat <<EOF … EOF is matched as cat <<EOF.
      • —A line is refused by allow (opaque) in some cases it used to approve: a syntax error, [[ … ]], arithmetic on a variable ($((x))), ${!x}, a function definition, shopt/set -o posix, time followed by an option (bash as /bin/sh runs the command time there), and two forms bash itself reads two ways ("$${…" in double quotes, and a quoted or expanding >& target, whose substitution bash 5.2 runs). They now go to review.
      • —A segment no longer carries a trailing comment: git push #' is git push.
      • —An argument_pattern rule on an argument the tool declares as its pathArgument tests the whole value and does not read it as shell, so src/app/(auth)/page.tsx is not refused as a syntax error.

      No configuration change is needed.

  12. v44.3.0

    Minor Changes

    • —4375e72: A provider-hosted web search now says what it searched for. StreamChunk.delta.hostedTool, and so the hosted_tool session event and the hosted.tool SSE event, may carry query (the search query), url (a page the provider opened instead of running a query) and results (how many sources it reported). Codex fills them from the search call's action, Anthropic from the search block's streamed input and its result list, Google from the grounding metadata. Each field is optional and absent when the provider does not say; nothing that already reads id, name and status changes. A custom driver can start emitting them whenever it knows them.

    Patch Changes

    • —1b65e2e: CODING_AGENT_ORCHESTRATE_DOCTRINE gains one sentence: agents in the same phase are launched in the same response, with run_in_background: true when they are to be waited for together, never one at a time. The text only reaches a prompt when a host turns orchestrate mode on (codingAgentDoctrineContribution({ orchestrate: true })); with the option off the rendered doctrine is byte-identical to before. A host that snapshots the orchestrate text will see the new sentence.
  13. v44.2.0

    Minor Changes

    • —

      9bd0c99: Plan mode now holds inside delegated work.

      @namzu/sdk: new optional BaseAgentConfig.reviewAllowedCalls and AgentTaskContext.reviewAllowedCalls. AgentManager stamps the spawning context's function onto every child (after the configBuilder runs) and onto the child's spawn context, so grandchildren inherit it too; SupervisorAgent hands its own to its workers, and ReactiveAgent and SupervisorAgent pass it to query(). Before, only the turn a host called query() with saw QueryParams.reviewAllowedCalls: inside a child, a batch a rule allowed, or one an approval earlier in the child's turn covered, ran without reaching the borrowed review handler. Nothing changes for a host that never sets the field. A child config that sets its own value keeps it, but it is OR-ed with the inherited one: a child can ask for more review and can no longer answer false over a parent that answers true. A host that builds its own AgentTaskContext for a TaskScheduler should set the field from the function it passes its own query().

      @namzu/cli: plan mode entered with Shift+Tab while a sub-agent runs now refuses that sub-agent's next change even when a permissions rule allows it; before, the child ran it. /resume on a parked turn now continues it under the permission mode the operator is in, read at each decision like a new turn's, for the turn and its sub-agents; before, the interactive terminal resumed it under auto whatever the mode, so /resume in plan mode let its changes through and a Shift+Tab during it did nothing. With nobody asked on a resumed turn, prompt and accept-edits still approve what the rules leave to review; plan and strict now refuse. namzu exec resumes under the mode it was started with, as before, and in plan mode that now also covers changes a rule allows.

  14. v44.1.0

    Minor Changes

    • —

      51af1b3: Plan mode now refuses a change that a permission rule allows. Before, a batch whose every call a rule allowed (for example permissions: { bash: 'allow' }) ran without reaching the review handler, so a turn in plan mode, whether it started there or the operator entered it with Shift+Tab mid-turn, still ran touch or any other allowed command, while the model had been told that every change is refused. The CLI now asks the kernel to send such batches to review while it is in plan mode; nothing changes in any other mode.

      For SDK hosts: QueryParams.reviewAllowedCalls?: () => boolean is new and optional. Read once per batch; true sends a batch a rule allows, or a grant from earlier in the turn covers, to resumeHandler instead of running it, each call carrying the gate's decision in authorization. A rule's deny still refuses. Absent, behaviour is unchanged.

    • —

      406e7a2: A task removed with task_update status deleted is now distinguishable on the stream: its task_updated event carries deleted: true (the SSE task.updated event too), with the subject and status the task had when it went. Before, a removal arrived as an update that changed nothing, so the interactive terminal kept drawing the removed task as an open step in the checklist. It now drops the task and writes Removed task · <subject>. The field is optional and absent on every other update; a consumer that ignores it sees the same events as before.

    Patch Changes

    • —0d23de8: The planning tools task_create, task_update and task_list now present themselves in words to a host: Add task · <subject>, Start task, Complete task, Check tasks, a hidden result view when a call succeeds, and Tasks · N/M done for a listing. No presented view carries a task id, an owner or the JSON arguments any more; a host that renders the generic presenter used to show Task update({"id":"01a0…","status":"completed"}) above Task 01a0… updated — status: completed. The model-facing output still names the id it needs. task_create and task_update add subject to their result data, and task_list says 1 task: rather than 1 tasks:. Nothing to change on upgrade unless a host matched the old presented strings.
  15. v44.0.0

    Major Changes

    • —

      9355755: Checkpoint retention is part of every checkpoint store, and a failed prune no longer ends a turn.

      • —SessionCheckpointStore.prune(scope, keepLast) is required. It deletes a turn's oldest committed checkpoints until keepLast newer ones remain and returns the deleted ids, which the kernel records as checkpoint_pruned. It never deletes a checkpoint an open decision references, and it counts and deletes only checkpoints a checkpoint_written record commits. A custom store implements it; the optional pruneCheckpoints it replaces is gone. selectSessionCheckpointsToPrune is the selection the built-in stores use.
      • —A prune that throws after an iteration's checkpoint is logged (Checkpoint retention failed; older checkpoints are kept for now) and the turn continues. Before, the error failed the live turn.
      • —Listing parked work no longer reads checkpoints at all: SessionIndex.listPendingDecisions and listTurns answer from the index.
    • —

      755a81a: Consolidation no longer writes the same learning twice. consolidationEntry now adds a knowledge:<digest> tag, metadata.knowledgeDigest and type: 'project', the digest covering the turn's decisions, discoveries and failures. Before writing, the runtime calls the new isConsolidated(store, entry) and skips the write — and its memory_consolidated event — when a consolidation with that digest already exists, archived included. This is the deduplication createMemoryPromoter already did.

      Why major: the duplicate records were a defect — search filled with copies of one learning — but a documented event and a store write that used to happen on every settled turn with consolidateInto now do not happen for a turn whose learnings match an earlier one. A host that counted memory_consolidated events or records per turn observes that as a changed default, whatever the reason for it.

      What changes for a host passing consolidateInto: a turn whose learnings are identical to an earlier consolidation's writes no record and emits no memory_consolidated event. A host that expected one record per turn should look for the earlier record by its knowledge:<digest> tag. A host comparing consolidationEntry(...).tags exactly sees one more tag. There is no switch to restore a write per turn; a host that needs one writes the entry itself with store.create(consolidationEntry(...)).

    • —

      cb1f00c: A session held in memory stays in memory: its checkpoints, its token ledger and the child sessions it delegates to. Before, such a session kept only its evidence in memory and wrote its ledger and checkpoints to disk, one tree per call with nothing that removed it.

      What changes for you.

      • —A query() (or agent) whose sessionLog is an InMemorySessionLog and which names no paths and no checkpointStore keeps its checkpoints and ledger in memory, with the log. A later call with the same log can resume the same turn. A host that wants those files on disk passes paths (a SessionPaths) or its own checkpointStore and tokenBudgetStore.
      • —With no tokenBudgetStore, the ledger lives where the checkpoints live. A host that passes an InMemorySessionCheckpointStore and later copies its checkpoints into a new one to resume must pass the same InMemorySessionTokenBudgetStore to both calls; otherwise the resume fails because the ledger its checkpoint names is missing.
      • —BaseAgentConfig takes sessionLog and checkpointStore, which ReactiveAgent and SupervisorAgent forward to query(). A SupervisorAgent held in memory sets AgentTaskContext.childStorage (ChildSessionStorage), and AgentManager then gives each delegated child its own InMemorySessionLog (and the supervisor's checkpointStore, if it named one) unless the child's config names a sessionLog or paths. Such a child writes nothing under NAMZU_HOME. A host that pairs query() with its own LocalTaskScheduler sets childStorage on the context it builds.

      InMemorySessionTokenBudgetStore is a process-local SessionTokenBudgetStore that refuses the same regressions DiskSessionTokenBudgetStore refuses.

    • —

      933ba6d: read_memory's output is no longer the memory body alone for text and Markdown records.

      For a store that implements getRecord — DiskMemoryStore, InMemoryMemoryStore and MarkdownMemoryStore all do — read_memory now appends, after the body, a --- line, then when the memory was last updated and how old it is, a notice to verify named files and functions when it is older than today, and each [[name]] link resolved. This is what lets the model see that a memory is a point-in-time claim.

      What breaks: a host that displayed or parsed read_memory's output as the stored body sees the footer after it. A record saved with format: 'json' is unaffected — its output is still exactly the stored text and still parses — so only prose records change.

      What to do: take the body from the store (store.get(id)) rather than from the tool's output, or strip everything from the last \n\n---\n. data carries updatedAt, name, type and the resolved links for every format.

    • —

      3e7a97b: The run is gone. The kernel's model is now session → turn → message: a session is one conversation with one agent, a turn is one unit of work inside it (a prompt, a goal round, a resident step, a verification step), and a subagent is a child session. Each session is recorded in one append-only, hash-chained JSONL log under NAMZU_HOME, and that log is the source of truth; SQLite is only an index rebuilt from the logs. See docs/sdk/session-log.md for the record schema.

      Before you upgrade: resolve or abandon parked runs on 43.x. Run drainRuns (or resumeRun) on the old version until nothing is parked. This version cannot read a run tree, a RunState, an old checkpoint or an old token ledger, so a run still parked when you upgrade cannot be resumed. There is no migration.

      What breaks

      Identity and ids.

      • —RunId, generateRunId, asRunId, parseRunId and isEntityId(v, 'run') are removed. Use TurnId, generateTurnId, asTurnId, parseTurnId and 'turn'. A new RecordId names a log record.
      • —Every generate*Id mints a UUID version 7 instead of v4, so ids sort by creation time as strings. Every id check still accepts versions 1 to 8.
      • —Anywhere a runId field named the owner of something, it is now sessionId, plus turnId where the thing belongs to one turn: ToolContext (third-party tools read context.sessionId and context.turnId), the context a guardrail receives, StepResult, PrepareStepContext, AnswerReviewContext, probes and provider instrumentation, deliverables, plans and approval requests, bus owners, background-job owners, tasks, working memory and HandoffLockRejected.details (turnId: TurnId). parentRunId becomes parentSessionId plus parentTurnId.

      Core types (rename, same meaning unless noted).

      RemovedUse
      Run, AgentRun, AgentSessionTurn, AgentTurn (Turn gains sessionId; replayOf becomes forkedFrom)
      RunExecutionStatus, RunStatus, deriveRunStatusTurnExecutionStatus, TurnStatus, deriveTurnStatus (same members)
      RunCancelledTurnCancelled
      AgentRunConfig, RunConfigSnapshot, RunMetadata, RunStateMetadataTurnConfig, TurnConfigSnapshot, TurnMetadata
      RunPersistence, RunPersistenceConfig (.runConfig)TurnRecorder, TurnRecorderConfig (.turnConfig; pruneKeepLast stays on it)
      RunEvent, RunEventListener, PersistedRunEvent, RUN_EVENT_SCHEMA_VERSION, RunEventSchemaVersionSessionEvent, SessionEventListener, SessionRecord, SESSION_RECORD_SCHEMA_VERSION, SessionRecordSchemaVersion (the event field schemaVersion becomes the record field v)
      RunEventCursor, RunEventReplay, RunEventReplayRefusal, RunEventLogHead, resolveRunEventReplaySessionLogCursor, SessionLogReplay, SessionLogReplayRefusal, SessionLogHead, resolveSessionLogReplay
      RunState, RUN_STATE_VERSION, parseRunState, RunStateVersionError, captureRunState, loadRunState, RunStateScopeTurnState, TURN_STATE_VERSION (1), parseTurnState, TurnStateVersionError, captureTurnState, loadTurnState, loadSelectedTurnState, TurnStateScope. parseTurnState refuses a RunState by name.
      resumeRun, ResumeRunParamsresumeSession, ResumeSessionParams (same outcomes: no-checkpoint, awaiting-decision, resumed)
      claimRun, releaseRun, RunLease, ClaimRunOptionsclaimSession, releaseSession, SessionLease, ClaimSessionOptions
      drainRuns, DrainRun, DrainRunsParams, DrainRunsResultdrainParkedTurns, DrainTurn, DrainTurnsParams, DrainTurnsResult
      listDurableRuns, paginateDurableRuns, toDurableRunEntry, DurableRunEntry, DurableRunOrder, DurableRunPage, ListDurableRunsOptions, CheckpointListingScope, assertContiguousListingScopeSessionIndex.listTurns and listPendingDecisions; DurableTurnEntry, DurableTurnOrder, DurableTurnPage, ListDurableTurnsOptions
      DelegatedChildRun, ChildRunStorage, RunHierarchyNodeChildSessionSummary (SessionIndex.listChildren), ChildSessionStorage, SessionTreeNode
      replayRun, RunSummaryreplayAudit, AuditSummary. Audit entries are audit records in the session log; AuditEvent.runId becomes turnId?.
      RunQuery, RunQueryOptions, RunTranscriptUnavailableError, RunTranscriptUnavailableReasonSessionQuery, SessionQueryOptions, SessionTranscriptUnavailableError, SessionTranscriptUnavailableReason
      RunReporter, createRunReporter, recordRunDurationTurnReporter, createTurnReporter, recordTurnDuration
      RunApprovalPolicySessionApprovalPolicy (ApprovalPolicy is unchanged and still the { name, handler } value)
      RunEvidence*, RunTextEvidence*, DiskRunEvidenceOptions, createDiskRunEvidenceSource, createDiskRunTextEvidenceSourceSessionEvidence*, SessionTextEvidence*, SessionEvidenceSourceOptions ({ scope, logPath, … }), createSessionEvidenceSource, createSessionTextEvidenceSource; ToolContext.captureRunEvidence becomes captureSessionEvidence. Its source's scope is the session, not the turn: it reads every earlier turn in the session log and the running turn up to its latest record (the old capture read the running run's own transcript and nothing else). A reader for one turn only is createSessionTextEvidenceSource with scope.turnId over the session log.
      RUN_STATUS_READ_MODEL_ID, RunStatusReadModelOptions, RunStatusState, createRunStatusReadModelSESSION_STATUS_READ_MODEL_ID, SessionStatusReadModelOptions, SessionStatusState, createSessionStatusReadModel
      SharedRunWorkspace*, RegisterSharedRunPlanInputSharedSessionWorkspace*, RegisterSharedSessionPlanInput
      BidiRun, BidiRunParams, BidiRunEvent, startBidiRunBidiTurn, BidiTurnParams, BidiTurnEvent, startBidiTurn
      deriveRunOptions, DeriveRunOptionsInputderiveTurnOptions, DeriveTurnOptionsInput
      EvalRun, evalRunFromQuery, evalRunFromRunEvalTurn, evalTurnFromQuery, evalTurnFromTurn
      RunMemoryCandidate, RUN_MEMORY_TAGSessionMemoryCandidate, SESSION_MEMORY_TAG
      SubsessionSpawnedEvent, SubsessionMessagedEvent, SubsessionIdledEvent, SubsessionLifecycleEventChildSessionSpawnedEvent, ChildSessionMessagedEvent, ChildSessionIdledEvent, ChildSessionLifecycleEvent
      ReadRunEventsOptionsReadSessionLogOptions
      CheckpointRunScopeCheckpointScope ({ tenantId, projectId, sessionId, turnId })
      ReplayAttribution (Run.replayOf)TurnForkOrigin (Turn.forkedFrom, { sessionId, turnId, checkpointId })
      validateTokenBudgetSnapshot, OpenTokenBudgetOptions, DiskTokenBudgetStoreConfigvalidateSessionTokenBudgetSnapshot, OpenSessionTokenBudgetOptions, DiskSessionTokenBudgetStoreOptions
      TurnStatusResolver.blockingRun, the handoff deps field runStatusblockingTurn, turnStatus (on SingleHandoffDeps and the broadcast deps)
      Agent.forRun(), AbstractAgent.forRun()forTurn()
      AgentHandle.queueForNextRun()queueForNextTurn()
      CaseResult.runCaseResult.turn
      AdvisoryBudget.maxCallsPerRun, maxCostPerRunmaxCallsPerTurn, maxCostPerTurn (the advisory stack is built per turn, so the bound always was)

      query(), drainQuery() and runAgent(). query() is now AsyncGenerator<SessionEvent, Turn> and drainQuery() resolves to a Turn. QueryParams.runConfig is turnConfig. runStore, pathBuilder, claimFence, emergencySave and supersedesEmergencySave are gone (ReactiveAgentConfig.emergencySave goes with them); pass paths (a SessionPaths), sessionLog, checkpointStore, lease, budget and tokenBudgetStore instead, and turnId with resumeFromCheckpoint to continue a turn. RunAgentResult.run is renamed turn; runAgent, RunAgentOptions and RunAgentResult keep their names.

      One active turn per session. Starting a turn while the session has one running, paused or interrupted throws TurnInProgressError (sessionId, activeTurnId, state); isTurnInProgressError recognises one across package copies. A paused turn is continued with resumeSession (same turnId) or closed with the new abandonTurn(sessionId, turnId, reason). Parallel work goes to child sessions or separate sessions. Manual compaction between turns is compactSession.

      A rate limit on a turn's first request pauses it. A retryable provider fault (a 429, an outage, a stalled stream) that survives the retries used to fail the turn when it hit before the turn's first checkpoint, which in practice meant its first request; the same fault after a checkpoint paused it. It now pauses there too, on a checkpoint of the turn where its loop began (iteration: 0 for a fresh turn, the restored count for a resumed one), and resumeSession restarts the turn from where that loop began. Everything the turn did since then is discarded: every iteration it ran before the fault, completed ones included (a partial max_tokens answer and the continuation prompt after it, a structured-output re-prompt), is not in the resumed request, and none of those iterations' guards is counted. So after such a fault the session holds a paused turn, and the next query() or runAgent in it throws TurnInProgressError until the turn is resumed or closed with abandonTurn: a caller that retried by starting a fresh turn resumes instead. The returned Turn has stopReason: 'paused' rather than status: 'failed', the event is turn_paused rather than turn_failed, and a paused turn's lastProviderError now carries the classification (retry delay included) that only a failed turn carried before. A permanent fault still fails the turn.

      Event literals. run_started, run_completed, run_failed, run_paused and run_resuming are turn_started, turn_completed, turn_failed, turn_paused and turn_resuming. subsession_spawned, subsession_messaged and subsession_idled are child_session_spawned, child_session_messaged and child_session_idled. Every event drops runId, carries sessionId, and carries turnId inside a turn. The other 54 literals are unchanged. isEphemeralEvent is now exported at runtime.

      Other string literals. HandoffLockRejectedReason (and HandoffLockRejected.details.reason) 'active_run' is 'active_turn'. ResidentConsumptionAttempt.receiptStatus 'duplicate-run' is 'duplicate-turn'. A caller that compares either value must match the new one.

      Delegation records. The parent turn appends child_session_spawned and child_session_ended to its own log, reading them from the task scheduler's new optional onChildSessionEvent(callback). LocalTaskScheduler and DelegatingTaskScheduler implement it. A host TaskScheduler that does not still delegates, but its parent log names no children, so SessionIndex cannot list them: implement it to keep them listable.

      Storage.

      • —RunStore, RunDiskStore, InMemoryRunStore and RunStoreConfig are removed, with writeRunMeta, writeMessages, readMessages, writeReport, addToIndex, listRuns and listChildren. Use SessionLog (DiskSessionLog, InMemorySessionLog): metadata comes from records, messages from foldSessionMessages, and listing from SessionIndex (openSessionIndex, SqliteSessionIndex, or ScanSessionIndex where node:sqlite is unavailable). readRunEventsIn and readRunMessagesIn are readSessionLog and foldSessionMessages. RunMessageSnapshot is removed.
      • —CheckpointStore, DiskCheckpointStore and InMemoryCheckpointStore are SessionCheckpointStore, DiskSessionCheckpointStore and InMemorySessionCheckpointStore, keyed by CheckpointScope { tenantId, projectId, sessionId, turnId }. prune(scope, keepLast) is required; it was the optional pruneCheckpoints. A checkpoint document holds no messages and is refused on restore when it no longer matches its log. A child session's checkpoints are in its own directory, <parent-session-dir>/subagents/<child-id>/checkpoints/; a DiskSessionCheckpointStore you build for a child takes that place as its session option (DiskSessionLog.locator, which a log opened from its file path reads from that path, and which names the right place only for a file in that project's layout). IterationCheckpoint and the kind run-checkpoint are replaced by the Checkpoint document (kind: 'checkpoint'); an old one is refused by name.
      • —TokenBudget, TokenBudgetStore, DiskTokenBudgetStore, InMemoryTokenBudgetStore, openTokenBudget and their snapshot types are SessionTokenBudget, SessionTokenBudgetStore, DiskSessionTokenBudgetStore, InMemorySessionTokenBudgetStore, openSessionTokenBudget and SessionTokenBudget*Snapshot. The ledger is keyed by (rootSessionId, rootTurnId), so each root turn has its own limit and a limit changed between turns no longer throws Token budget root limit mismatch. TokenBudgetBinding is TurnBudgetBinding. A version 1 snapshot is refused.
      • —SessionStore.appendMessage, replaceMessages, loadMessages and loadSessionMessages are removed: messages are written only by the turn recorder and read with foldSessionMessages. A custom SessionStore drops those methods. SqliteSessionStore and SqliteSessionStoreConfig are removed; SessionIndex replaces them.
      • —Tasks live under <session-id>/tasks/<task-id>.json, one store per session: DiskTaskStoreConfig is { paths, session, tenantId?, logger? }, and a store opened on another session's tasks throws TaskSessionMismatchError. Each task records the turn that created it; selectTaskContext(tasks, { turnId, turnStartedAt }) gives the tasks a turn sees (every open task, plus those it closed). The task tools take a TaskToolScope of { sessionId, turnId, turnStartedAt }.
      • —MessageFeedbackStore is keyed by (sessionId, messageId) under <session-id>/feedback/, and feedback on a message the session log does not hold throws UnknownMessageError.
      • —Custom log backends are checked with defineSessionLogConformance from @namzu/sdk/testing. defineCheckpointStoreConformance and its types are removed, and so are CHECKPOINT_STORE_CONTRACT_VERSION, CheckpointStoreCapabilities and DiskCheckpointStoreAttribution: a checkpoint store is keyed by CheckpointScope, and prune is part of the interface rather than a capability to probe.
      • —validateTokenBudgetBinding is removed with no replacement of its own. It checked the budget binding a checkpoint carried; a checkpoint's binding is now checked with the rest of the document by parseCheckpoint (the budget.binding of CheckpointSchema).

      The emergency surface is removed. EmergencySaveManager, EmergencySaveData, EmergencySaveConfig, EmergencySaveConfigSchema, EmergencySaveId, generateEmergencySaveId, asEmergencySaveId, EMERGENCY_DIR_NAME, EMERGENCY_EVENTS, EMERGENCY_SIGNALS and projectEmergencyToCheckpoint are gone. The log and the per-iteration checkpoints hold everything a dump held. prepareReplayState, PrepareReplayInput and PreparedReplayState are prepareForkState, PrepareForkInput and PreparedForkState, and fromCheckpoint: 'emergency' is no longer accepted: the selector (CheckpointSelector) is a checkpoint id or 'latest'. A replay forks a new session whose session_started.forkedFrom names the source { sessionId, turnId, checkpointId }.

      Layout. Everything lives under NAMZU_HOME (default ~/.namzu, resolveNamzuHome): projects/<slug>/<session-id>.jsonl plus projects/<slug>/<session-id>/ for child logs, spills, checkpoints, budgets, tasks, feedback, goals and the lease, and projects/<slug>/{memory,residents, worktrees}/. SessionPaths, ensureProject, slugForCwd and tempRoot compute it. DefaultPathBuilder and PathBuilder are removed. Nothing is generated under the working directory: the default git worktree directory moves from <repoRoot>/.namzu/worktrees to projects/<slug>/worktrees/. A project id is minted once per working directory into projects/<slug>/project.json. Old run trees, sessions.sqlite, checkpoints, ledgers and resident learning databases are not read.

      defaultStateRoot, NAMZU_STATE_DIR and projectIdForDirectory were on unreleased main and never shipped; they are not in this release. Neither is the per-run message history of that branch (RunHistory*: openRunHistory, loadRunHistory, resolveRunHistory, compactRunHistory and the rest, with their history/messages.<g>.jsonl files). Its idea, that each message is stored once, is the session log's message record: a message is appended once, when it ends, and a resume or a compaction refers to it by its messageId instead of copying it.

      Hooks. Hook events run_start, run_end and run_interrupt are turn_start, turn_end and turn_interrupt; a config naming an old event is refused with a message naming the new one (RENAMED_PLUGIN_HOOK_EVENTS). Shell-hook stdin run_id becomes turn_id and parent_run_id becomes parent_session_id plus parent_turn_id; env NAMZU_RUN_ID becomes NAMZU_TURN_ID. session_id and NAMZU_SESSION_ID are always present; turn_id and NAMZU_TURN_ID are absent on session_start and session_end. PluginHookContext.runId is turnId?.

      Memory. Consolidation tags are session:<id> and turn:<id> (not run:<id>), with metadata.sessionId and metadata.turnId. The source value the session promoter stamps is 'session-memory' (was 'run-memory'), and MarkdownMemoryStore leaves records with that source out of its index.

      Protocols and wire contracts.

      • —WireRun, WireRunStatus, toWireRunStatus, RunConfig, RunConfigSchema, RunIdSchema, CreateRunRequest, CreateRunSchema, CreateStatelessRunRequest, CreateStatelessRunSchema, RunUsage, RunStopReason, StreamEvent and StreamEventType are WireTurn, WireTurnStatus, toWireTurnStatus, WireTurnConfig, TurnConfigSchema, TurnIdSchema, CreateTurnRequest, CreateTurnSchema, CreateEphemeralSessionRequest, CreateEphemeralSessionSchema, WireUsage, WireStopReason, SessionStreamEvent and SessionStreamEventType.
      • —WireTurnStatus gains awaiting_input: awaiting_hitl and awaiting_hitl_resolution map to it (they mapped to running), and A2A maps it to input-required.
      • —SSE event names run.* are turn.*, with session_id and turn_id. mapRunToStreamEvent is mapSessionEventToStreamEvent.
      • —A2A: contextId now names a session; it used to carry the project id. Any string is accepted and mapped to one session. A client that sent its project UUID as contextId gets one session for it, and its tasks queue there one at a time. Task.id is the turn id. runToA2ATask, mapRunToA2AEvent, runStatusToA2AState, RUN_STATUS_TO_A2A, CreateRunFromA2A and a2aMessageToCreateRun are mapTurnToA2ATask, mapTurnToA2AEvent, turnStatusToA2AState, TURN_STATUS_TO_A2A, CreateTurnFromA2A and a2aMessageToCreateTurn. TURN_STATUS_TO_A2A is keyed by WireTurnStatus, so it has the new awaiting_input key.
      • —ACP: the default session id is a UUIDv7 instead of acp_<n>, and a prompt while a turn is active is refused with INVALID_REQUEST.
      • —Telemetry: NAMZU.RUN_ID, NAMZU.RUN_STATUS and NAMZU.RUN_PARENT_ID are removed; spans carry NAMZU.TURN_ID (namzu.turn.id), NAMZU.TURN_STATUS and NAMZU.SESSION_PARENT_ID, plus gen_ai.conversation.id set to the session id. The span namzu.agent.run <name> is namzu.agent.turn <name> and the histogram namzu.run.duration is namzu.turn.duration.

      New

      • —The session-log schema: SessionRecord, SessionRecordSchema, parseSessionRecord, one *RecordSchema per record type, TurnBoundSessionEventType, Checkpoint with parseCheckpoint, and recordSha256, parseSessionLogLine and formatSessionLogLine for one log line.
      • —resolveA2AContext (with A2AContextResolution) and resolveExternalSession (with ExternalSessionLookup, ExternalSessionResolution and ResolveExternalSessionOptions): how a host server resolves an A2A contextId, or any protocol's own session id, to a session through the index.
      • —PrepareStepContext.turnStartedAt: when the turn began. A resumed turn keeps the time of its turn_started record, whichever process wrote it.
      • —ChildSessionStorage gains a disk kind ({ kind: 'disk', paths }). A parent on disk hands its layout to the children it delegates to, so their logs nest under it whether or not a layout was named. Code that switched on kind must handle the new member.
      • —resolveNamzuHome and NamzuHomeError (moved from the CLI, same behaviour), SessionPaths, ensureProject, slugForCwd, hashedSlugForCwd and tempRoot.
      • —fixtureId.turn and fixtureId.record in @namzu/sdk/testing.
      • —releaseHeldSessionLeases({ timeoutMs? }) (with ReleaseHeldSessionLeasesOptions and ReleaseHeldSessionLeasesResult), for a process about to exit: it releases every session lease the process holds, leaving a running turn interrupted so the next writer can abandon or resume it at once instead of waiting out the lease. After the first call every claim in the process is refused with SessionLeasesReleasedError. A host that stops on SIGTERM calls it from its signal handler; the CLI does. SessionLog.release now waits for the log's append in flight, so a release never lands under a half-written record.

      What to do

      1. —On 43.x, drain or abandon every parked run.
      2. —Rename imports per the table above; the compiler finds every one.
      3. —Replace runStore/pathBuilder with sessionLog (or paths), and read results from Turn and the session log instead of run.json or messages.json.
      4. —Re-implement a custom store against the session-log conformance suite, and a custom checkpoint store with the required prune.
      5. —Update hook configs, dashboards and any A2A client that put a project id in contextId.
    • —

      a84dc1c: runAgent no longer mints a new projectId for every call. When you pass no projectId, it is the project of workingDirectory (default process.cwd()): ensureProject mints one UUIDv7 the first time a directory is used, writes it to <NAMZU_HOME>/projects/<slug>/project.json, and every later call in that directory adopts it. Two processes that start in a new directory at the same moment get the same id. Every session started in one directory is filed under one Project, in one projects/<slug>/ tree.

      What changes for you. Two runAgent calls in the same directory now return the same identity.projectId. sessionId, topicId and tenantId are still generated per call, so the calls stay separate conversations. To get the old behaviour, pass projectId: generateProjectId() on each call. An explicit projectId always wins.

    • —

      b064cea: A turn can make a file tool's path outside its roots, or a command outside its sandbox, a reviewed question instead of a refusal. Both are opt-in and default to the old refusal: QueryParams.outsideRootAccess: 'review' and QueryParams.sandboxEscape: 'review' (also on ReactiveAgentConfig).

      What can break. AuditOutcome (and the session log's audit.outcome) gains 'approved', written for each approved crossing. A consumer that switches exhaustively over the outcome must handle it; replayAudit skips it. A session log written by this version with such a record does not parse in an older SDK.

      New, all optional. ToolCallSummary.escalation names what a call reaches (outsidePaths, sandboxEscape); an escalated call is always reviewed, and a gate allow, a remembered grant, a read-only declaration and accept-edits decline to approve it alone. A sandbox escape runs only when the decision lists its id in the new confirmedEscalations (on approve_tools and modify_tools); createReviewHandler asks for it in every mode, and refuses it without a prompt (SANDBOX_ESCAPE_UNATTENDED_REFUSAL) unless unattendedSandboxEscape: 'allow'. A path outside the roots is likewise asked about in every mode (auto and a remembered approve-all included) and refused without a prompt (OUTSIDE_ROOTS_UNATTENDED_REFUSAL); strict refuses it, and plan asks about it when every call in the batch only reads. A refused crossing is an audit record with outcome refused, a deny rule's refusal included. Tools declare pathArgument and sandboxEscapeArgument; the executor hands an approved call ToolContext.approvedPaths or sandboxEscapeApproved. The shipped file tools declare pathArgument: 'path'; bash gains an optional dangerously_disable_sandbox input, refused with SANDBOX_ESCAPE_NOT_APPROVED unless approved. pathOutsideRoots, toolRoots and OUTSIDE_ROOTS_GUIDANCE are exported. resolveWithinReal now canonicalizes a root that does not exist yet (an approved path a write is about to create) instead of refusing every path under it. A file tool's refusal of a path outside its roots now says how the boundary is widened instead of ending at "Tools may only reach".

    • —

      3641102: Working memory leaves the system messages of every model request, a new PromptPlacement value 'context', and closing requests end with their closing directive.

      What breaks.

      • —The working-memory slot is no longer a system message in a request. The slot (pins, and a workingMemoryProvider's block) keeps its place in the session's history, checkpoints and compaction: the last leading system message. Every request now takes it out of the system messages and sends it after the history as a user-role message with source { type: 'runtime-context', kind: 'step-context' }. It is therefore missing from the system messages every provider receives, from the request_envelope event's systemPrompt, and from the system messages a pre_llm_call hook sees. There is no option to keep the old placement. The message's first line is the label every step-context message carries, Current step context (runtime-generated; not a new user request):, and the slot follows verbatim, so the content no longer starts with WORKING_MEMORY_HEADER and isWorkingMemoryMessage(content) answers false for it.
      • —PromptPlacement gains 'context'. The union is also an output type: PromptContribution.placement and PromptContributionRegistry.list() can now hand back 'context', so a switch over it with an exhaustiveness check stops compiling.
      • —The closing directive is last. On the forced-final step at a resource limit and on the request after an empty completion, the [SYSTEM] ... message of kind limit-finalization now comes after every request-only context message (working memory, context contributions, step context, derived work context) instead of before some of them.

      What to do.

      • —A host that finds the slot in a captured request by looking for a system message starting with WORKING_MEMORY_HEADER looks instead for a step-context message, drops its first line, and tests the rest with isWorkingMemoryMessage. A host that rewrites or strips the slot in a pre_llm_call hook does the same. Code reading the session's history or a checkpoint needs no change.
      • —Code that switches over PromptPlacement adds a 'context' case.
      • —A test asserting the closing directive's position expects it last.

      Why, and what is new. A driver may hoist every system message ahead of the conversation (Anthropic renders tools, then system, then messages), so a slot that changes whenever a pin does invalidated the cached conversation prefix and re-read the whole history at full price. After the history, a caching driver ends its breakpoint before it and a pin costs only its own tokens. A PromptContribution registered with placement: 'context' is rendered before every model request, with iteration, exactly as turn is, and delivered the same way: one labelled step-context message after the history, never pushed onto it. turn is unchanged and keeps system authority; use context for observations, turn for text that must act as an instruction.

    Minor Changes

    • —

      8805360: BaseAgentConfig.pruneKeepLast sets checkpoint retention for a turn started through ReactiveAgent or SupervisorAgent, the same way turnConfig.pruneKeepLast does for a raw query() turn. Both agents build their turn config from a hand-listed literal, so a host that bounded its own turns could not bound a delegated child session's, and the child kept every checkpoint. Absent keeps every checkpoint, as before.

    • —

      a14b013: Memories can be typed and kept as one Markdown file each.

      • —MarkdownMemoryStore({ directory }) implements MemoryStore with one <name>.md per memory (YAML frontmatter name, description, type, status, createdAt, updatedAt, tags, then the body) and a generated MEMORY.md index, one line per active memory of at most 150 characters. The index lists only what someone chose to keep: a record with metadata.source: 'session-memory' (the session memory promoter) or metadata.kind: 'consolidation' stays in the store and in search but is never listed, so a host carrying the index in its system prompt keeps the same prompt, and its prompt cache, when a turn writes one. Lines are ordered feedback and user memories not saved by the model first, then the model's (save_memory stamps metadata.source: 'agent-memory'), then the rest, by name within each, so the 200-line cap drops project and reference lines before an operator's feedback or user one. It keeps DiskMemoryStore's guarantees: the shared operation lock, atomic private (0600) writes, and refusal — naming the file — of anything it cannot read. It never writes what it would refuse: a NUL character in the body, title, summary, description or a tag, or a file over 256 KiB, is refused before the write with the new MemoryContentRejectedError (reason: 'too_large' | 'nul_byte'), which save_memory and update_memory return as a failed result. Two files left with one id by an interrupted rename — both stating their own, different updatedAt, or an older hand-written file with no id and no updatedAt beside the renamed file that carries its name-derived id — are resolved by the later one (the hand-written file dated by its modification time; when it is the newer file it is a new memory under a renamed one's old name, and the pair is refused), the older set aside as <name>.md.superseded at the next write; any other pair claiming one id, a hand copy that states its id but no updatedAt included, is refused naming both files. readIndex({ maxLines }) renders the index for a prompt, capped at 200 lines by default with a note pointing to search_memory, and readIndex({ derived: true }) lists only the derived records, newest first, for an operator's inspection; importRecord moves records in from another store idempotently by id. While a DiskMemoryStore index.json sits in the same directory, every other operation is refused until its records are imported and the index moved aside.
      • —MemoryIndexEntry, CreateMemoryParams and UpdateMemoryParams gain optional name, description and type (user | feedback | project | reference). All three shipped stores persist them, and refuse a name another record holds with the new MemoryNameConflictError, which names that record. Omitting the fields behaves exactly as before; an existing store, index or custom MemoryStore needs no change.
      • —save_memory and update_memory take name, type and description; a taken name returns a failed tool result pointing at update_memory. read_memory and update_memory accept a memory's name as well as its ID. search_memory lines show name, type and age. (read_memory's age footer changes its output and is described in its own changeset.)
      • —createMemoryRecallStep adds name, type and, for a record older than ageNoticeAfterMs (default one day), an age field, and then ends its block with MEMORY_VERIFY_NOTICE. A block of records updated within the day is unchanged apart from the new fields.
      • —New helpers: renderMemoryIndex(records, { maxLines }) (over MemoryRecords, since who wrote a record is in its metadata), memoryIndexLine, memoryLinkNames, describeMemoryAge, slugifyMemoryName (names derived from a title are at most 32 characters, so an index line keeps room for its description), isMemoryName, isMemoryType, assertMemoryType, MEMORY_TYPES, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_LINE_MAX_CHARS, MemoryContentRejectedError and its MemoryContentRejection type.
      • —DiskMemoryStore.get and getRecord no longer return the file's schemaVersion stamp as a field of MemoryContent, which the type never declared.

      Nothing is removed or renamed. A host that wrote a second record under the same title through save_memory still can: names are only enforced when a caller supplies one.

    Patch Changes

    • —9238347: A child session's log opened through a symlinked path is now recognised as the log in its place in the layout. Before, when NAMZU_HOME (or the SessionPaths home) was reached through a symlink and the log was opened by its real path, or the other way round, the two spellings compared unequal and the child's checkpoints were written a second time under its parent. Nothing to change on your side.
  16. v43.0.0

    Major Changes

    • —

      e83dfe5: ModelInfo.inputPrice and ModelInfo.outputPrice are now optional. Six drivers wrote 0 wherever the vendor listing carries no rate; they omit the field instead.

      A price of zero is not "I do not know" — it is a billing fact. It says the model is free, and it reaches every consumer as a quote: a menu prints (free), a total reports the model as costing nothing, and an operator believes both. The drivers had no way to say anything else, because the type required a number.

      This is the defect 9d6c482c removed from contextWindow, in the field where a wrong value is money rather than a compaction pass. The distinction was already the house rule one level down: resolveModelPricing returns undefined for a rate nobody has and { inputCostPer1M: 0, outputCostPer1M: 0 } for a driver that genuinely bills nothing, and its own docblock says a caller that flattens the two reproduces the defect the module exists to remove. ModelInfo carried the flattened version to every consumer that never reached that module.

      What breaks. Code reading model.inputPrice or model.outputPrice as a number must handle undefined. That is the point — the value was already absent in fact, and the type was asserting otherwise. Treat undefined as unknown rather than as free: a caller that renders a price should say "unknown" rather than $0.00, and only 0 means the model is free.

      What does not change. A driver that knows a rate of zero still writes 0. ollama and lmstudio bill nothing by construction, and the Zen catalogue's free tier is priced from a source that names those models free. Values that were genuinely known are untouched.

    Minor Changes

    • —

      9661d16: Stop an optional inference from ending the run that made it.

      PreparationTextRequest.timeoutMs is deprecated and no longer bounds the request. An auxiliary provider request that ends without its final usage receipt leaves the run's shared token ledger unresolved, and an account with unresolved spend admits nothing further — so a deadline that fired in normal use did not bound the call, it stopped the run. Against a reasoning model whose auxiliary answer took 17 s, every turn after the first ended with "usage for a model request could not be confirmed" before the model was ever asked, so no tool call in the run could ever execute.

      What a host observes: a run that used to stop after its first turn now continues. A preparation or review inference is bounded by the provider's own request timeout and by the run's cancellation — the same two bounds every other model request in the run has — instead of by a 10 s deadline of its own; set runConfig.timeoutMs to bound the whole turn. Stop still cancels an in-flight auxiliary call, and a cancelled call still leaves its unresolved receipt visible in /cost. Passing timeoutMs changes nothing and is still validated; the field will be removed in a later major.

  17. v42.0.2

    Patch Changes

    • —

      175dab2: Nothing a consumer installs changes. These are devDependencies and toolchain pins: no runtime dependency, export, type or default moved, and the published surface is identical.

      Four advisories, all dev-time, closed by moving the package that carried them rather than by patching around them:

      • —knip ^6.4.1 → ^6.37.0, which carries smol-toml ≥ 1.8.0 — the version that fixes GHSA-7w5x-hrqm-74c2 (a malformed-TOML denial of service).
      • —tsx ^4.19.0 → ^4.23.13, which depends on esbuild ~0.28.0, fixing GHSA-g7r4-m6w7-qqqr (arbitrary file read from the development server, and only on Windows) — and collapsing two esbuild copies in the tree into one.
      • —js-yaml, twice, fixed by a workspace override rather than a parent upgrade, because both copies arrive deep inside the Changesets CLI's own tree: ^3.15.2 for the 3.x line and ^4.3.2 for the 4.x line, closing GHSA-2883-xcg3-v3hh (a merge-key CPU exhaustion). Neither override widens beyond what the parent already declared — 3.15.2 satisfies ^3.13.1 and 4.3.2 satisfies ^4.1.0 — so no package is forced outside its range.
    • —

      8d5223b: Nothing a consumer installs or calls changes, and that is the whole of this release. vitest moves from ^3.2.6 to ^4.1.11 in the devDependencies of all nineteen packages that declared it, and @vitest/coverage-v8 moves with it in @namzu/sdk. Every occurrence is a devDependency — checked, not assumed — so dependencies, peerDependencies, exports, types, defaults and the wire shape are untouched, and the published tarballs differ from the previous release only in package.json#devDependencies.

      The reason is a security fix with no 3.x backport. GHSA-82fw-gwwq-j7x9 ("Path Traversal / Arbitrary File Read via @vitest/mocker Redirect Mock") covers vitest and @vitest/mocker from 2.1.0 up to 4.1.11, so ^3.2.6 can only be resolved by leaving the 3.x line. 4.1.11 is the first patched release and is what the lockfile now resolves for both.

      What this costs anyone who works on the repository rather than with it: the upgrade was not a version bump. Vitest 4 changed test discovery, coverage configuration, mock construction and reporter output, and each of those broke something here that had to be migrated rather than worked around. Those fixes are all under __tests__/, vitest.config.ts files and scripts/, none of which is published, which is why this is a patch and not a major.

      You do not need to do anything. If you pin vitest yourself to run this project's own suites, note that the config files it ships are now written for >= 4.1.11 and will not run under 3.x.

@namzu/anthropic

5 releases

  1. v6.3.0

    Minor Changes

    • —3727e2b: Add AnthropicProvider.listModelsStrict(signal?) for hosts that need the account's actual Models API rows and errors. Empty responses remain empty and authentication/network failures reject instead of returning bundled models. Pagination includes later account rows, preserves caller cancellation across pages, and rejects oversized catalogues rather than silently truncating them. Use it to present current account availability without treating an offline catalogue as proof of access. Existing listModels fallback behavior is unchanged.
  2. v6.2.2

    Patch Changes

    • —567ada8: Updated package examples and evaluation fixtures for the SDK's ToolRegistry removal. Browser, computer-use and LSP documentation now show toolsets; the evals use toolsets when they call the kernel. Provider changes are test fixture migrations only, with no provider runtime API change. Consumers that pass tools to the SDK should use toolsets: readonly Toolset[] and wrap their definitions with toolset(source, definitions).
  3. v6.2.0

    Minor Changes

    • —

      28102e1: A forced tool choice that the vendor always rejects is now refused before it is sent. This covers toolChoice: 'required' and a named function on Claude Opus 5.5, Claude Fable 5.1 and Claude Mythos 5.1. It also covers a forced choice on any model when the request carries manual extended thinking (thinking: { type: 'enabled' } after the driver resolves it). These requests used to go to the API and fail there with a 400. On the three models the message is tool_choice: type "tool" and "any" are not supported for this model.

      What changes for you:

      • —The request still fails, but earlier. You get a ProviderRequestError with kind: 'bad_request' and providerCode: 'forced_tool_choice_unsupported'. It has no HTTP status, because nothing was sent. The message tells you what to use instead: toolChoice: 'auto' with the tool named in the prompt, or responseFormat for a fixed JSON shape.
      • —New export acceptsForcedToolChoice(model, thinking?). It says whether a forced choice will be taken, using the same resolution the request uses. Use it to decide whether a prepareStep stage can force a call on the model it is about to run.
      • —No change for 'auto' or 'none', or for forced choices on models that accept them (claude-opus-5, claude-fable-5, claude-sonnet-5 and earlier, with adaptive thinking or none).

    Patch Changes

    • —

      28102e1: claude-opus-5-5 can no longer be asked to turn thinking off. The driver treated it like claude-opus-5, which accepts thinking: { type: 'disabled' } at effort high or below. Opus 5.5 cannot stop thinking and answers disabled with a 400 at every effort level.

      What changes for you:

      • —A thinking: { type: 'disabled' } intent is left out of Opus 5.5 requests. It used to be sent, and the request failed with "thinking.type.disabled" is not supported for this model. Now the model runs its default adaptive thinking, as it already did for the Fable and Mythos families. To spend less on thinking, lower effort. Note that Opus 5.5 defaults to effort medium when you leave effort unset.
      • —disabled with effort xhigh or max is no longer refused on Opus 5.5. It used to fail before sending with effort "max" is not supported by model "claude-opus-5-5", although the model accepts all five levels.
      • —resolveThinkingCapability('claude-opus-5-5') returns canDisable: false and all five levels in both effort and effortWhenDisabled. provider.effortLevelsFor('claude-opus-5-5', { type: 'disabled' }) returns all five levels.
      • —A later Opus id (claude-opus-5-6, claude-opus-6) resolves the same way.

      claude-opus-5 keeps accepting disabled at high or below.

  4. v6.1.0

    Minor Changes

    • —4375e72: A provider-hosted web search now says what it searched for. StreamChunk.delta.hostedTool, and so the hosted_tool session event and the hosted.tool SSE event, may carry query (the search query), url (a page the provider opened instead of running a query) and results (how many sources it reported). Codex fills them from the search call's action, Anthropic from the search block's streamed input and its result list, Google from the grounding metadata. Each field is optional and absent when the provider does not say; nothing that already reads id, name and status changes. A custom driver can start emitting them whenever it knows them.
  5. v6.0.1

    Patch Changes

    • —a729b17: The error a stalled stream raises now says the turn lifecycle can emit turn_failed, where it said the run lifecycle could emit run_failed: the event it names was renamed in @namzu/sdk 44. Only the ProviderRequestError detail text changed; its kind (network) and providerId are the same. A caller that matched the old sentence should match on kind instead.

@namzu/bedrock

2 releases

  1. v5.1.4

    Patch Changes

    • —

      49491b9: The drivers now report more accurately how a response ended. The runtime uses this to tell a tool call the output limit cut off from one the model wrote badly.

      • —@namzu/anthropic: a tool call whose JSON does not parse no longer fails the whole stream with "the provider stream returned malformed data". The block close and the finish reason now reach the runtime. model_context_window_exceeded is reported as length, and refusal as content_filter. Both used to read as a normal stop.
      • —@namzu/bedrock: model_context_window_exceeded is reported as length, and guardrail_intervened as content_filter. A tool call opens with the id the driver keeps, so its arguments never arrive before an id.
      • —@namzu/http: the OpenAI dialect maps finish_reason instead of passing the server's string through. function_call becomes tool_calls; max_tokens and max_output_tokens become length; model_length, context_length, context_length_exceeded and model_context_window_exceeded become length with finishDetail: 'context_window'; eos, eos_token, end_turn and stop_sequence become stop. error fails the stream with a ProviderRequestError (kind: 'server'), as OpenRouter's does. A value it does not know reports no finish reason at all, which the runtime reads as a response that did not say how it ended, rather than as a normal finish. The Anthropic dialect gets the Anthropic mapping above and opens a tool call with the id its arguments carry.
      • —@namzu/openrouter: finish_reason is mapped the same way. error (the upstream model failed mid-generation) now fails the stream with a ProviderRequestError (kind: 'server').
      • —@namzu/deepseek: insufficient_system_resource now fails the stream with a ProviderRequestError (kind: 'server'). It used to read as a finished answer.
      • —@namzu/openai: Codex's response.incomplete is reported as length, or as content_filter when that is the stated reason, with its usage. The stream used to end with no finish reason, so auto-continuation never ran.
      • —@namzu/anthropic, @namzu/bedrock and @namzu/http's Anthropic dialect: a model_context_window_exceeded stop also carries finishDetail: 'context_window', so the runtime does not ask the model to continue a reply that filled the whole context window.
      • —@namzu/anthropic, @namzu/google and @namzu/openai (Codex): the list of sources a driver appends after a hosted search now carries contentOrigin: 'driver' on its stream chunk. The text is unchanged. Without the mark, a tool call the output limit cut off before that list would be reported as malformed.

      If you branch on finishReason, expect length or content_filter where you saw stop for these cases.

    • —

      2d4ff9a: Earlier entries in this package's CHANGELOG no longer name one particular application built on namzu, or quote a path from outside this repository. This changes only the CHANGELOG.md that ships in the tarball; no code or types change, and there is nothing to do to upgrade.

  2. v5.1.3

    Patch Changes

    • —

      3641102: Two fixes to the Converse request.

      • —The message cache point ends the history. It was placed after the last message. The runtime now appends request-only context there (the working-memory slot, context prompt contributions, step context), which changes on every request, so every request wrote a cache entry no later request could read. The point now sits after the last block before the first request-only context message, as in the Anthropic driver.
      • —Consecutive same-role messages are merged. Converse requires the conversation to alternate between user and assistant, and the runtime sends tool results followed by request-only context — two user turns — on every step after a tool call. Their content blocks are now concatenated in order into one message.

      No configuration changes.

@namzu/computer-use

4 releases

  1. v3.1.0

    Minor Changes

    • —072bb74: Windows computer use can capture pixels for one named window with screenshot {window_id} and keep background clicks and drags, text and keys bound to that window. Window type_text and key can explicitly request delivery_mode: 'foreground' for browser text or modifier shortcuts that cannot run in the background; this may briefly bring the verified window forward. The pinned driver sets windowScroll: false and refuses scoped pixel scrolling because its wheel input cannot prove which window receives it; a host that supports safe window scrolling can offer it. Capture identity and window geometry are checked before input; moved windows, old captures and restarted driver sessions require a new window screenshot. Hosts can offer window capture without display capture, in which case window_id is required and plain display screenshots remain unavailable. A failed window capture never broadens to a display capture. When both browser and computer-use tools are ready, the CLI routes a named existing browser window to computer use, leaving browser tools for the separate Namzu-managed profile.
  2. v3.0.0

    Major Changes

    • —c8bb87a: The Windows cua-driver backend now revives an implicit session after its five-minute idle expiry and retries a call that the driver explicitly refused before dispatch. UI Automation snapshots now return adapter-owned refs instead of raw cua-driver element tokens. Callers passing raw tokens to uiAct must use refs from the latest uiSnapshot; old refs cannot address a different control when the driver reuses a token after session revival or process restart. Calls whose outcomes are unknown are still not retried automatically.
  3. v2.0.2

    Patch Changes

    • —567ada8: Updated package examples and evaluation fixtures for the SDK's ToolRegistry removal. Browser, computer-use and LSP documentation now show toolsets; the evals use toolsets when they call the kernel. Provider changes are test fixture migrations only, with no provider runtime API change. Consumers that pass tools to the SDK should use toolsets: readonly Toolset[] and wrap their definitions with toolset(source, definitions).
  4. v2.0.0

    Major Changes

    • —

      82769f1: On Windows and under WSL, SubprocessComputerUseHost now drives the desktop through cua-driver (MIT, github.com/trycua/cua) instead of starting powershell.exe for every action, and every adapter now takes and returns physical pixels. Both are changes a caller can see; read on before upgrading.

      What changes by default on Windows / WSL. The first initialize() downloads cua-driver 0.28.2 for the machine's architecture (a 27–29 MB archive from the project's GitHub releases), checks the SHA-256 of the archive and of cua-driver.exe against values pinned in this package, and keeps only the executable in <NAMZU_HOME>/computer-use/cua-driver/0.28.2/ (~/.namzu unless NAMZU_HOME is set). One cua-driver.exe process then runs until host.dispose(); call dispose() when you are done. It runs with its telemetry and release check off and without environment variables whose names look like secrets. If it cannot be downloaded, verified or started, the host falls back to PowerShell and says why in host.fallbackReason.

      To keep the old behaviour — no download, no long-lived process — set NAMZU_CUA_DRIVER=off or pass new SubprocessComputerUseHost({ windows: { backend: 'powershell' } }). To use a cua-driver you installed yourself, set NAMZU_CUA_DRIVER=<path to cua-driver.exe> or windows: { cuaDriverPath }. windows: { download: false } uses only a cached or configured build.

      Physical pixels everywhere. Points you pass to execute() and sizes you get back are pixels of the captured bitmap, as the SDK's host contract now states. This changes results only on scaled displays:

      • —macOS Retina: getDisplayGeometry() returns the capture's resolution (2880x1800, not 1440x900; 3360x2100 in a "Looks like 1680x1050" mode), cursor_position is in pixels, and click/move/drag points are pixels; the adapter divides by the capture's pixels per point for cliclick and System Events, once. Before, a click aimed at the middle of a Retina screenshot landed near the bottom-right corner. Screenshots show the main display only, the one the input tools address. If you converted screenshot pixels to points yourself, stop.
      • —Windows above 100 % scaling: both backends are DPI aware, so the capture is the whole display (it was the top-left part) and points are physical (they were DPI-virtualised).
      • —Every capture now carries result.display (id, origin, size, scaleFactor).

      Also new. With the cua-driver backend, capabilities.windows is true and host.listWindows() / host.focusWindow(id) work (focus restores a minimised window and reports what is actually in front); host.captureRegion() exists and throws where the adapter has no region capture. host.backend names the backend in use. Measured on a 3440x1440 display: a click went from 0.68–0.83 s to 0.13 s, a screenshot from 0.40 s to 0.08–0.13 s.

      Smaller differences on the cua-driver backend. capabilities.clipboard is false (no clipboard action was ever offered); win/super in a key chord press the Windows key (PowerShell's SendKeys pressed Ctrl); a single punctuation key such as / is typed as text so the keyboard layout cannot change it. Only the primary display is captured, as before. The PowerShell fallback now types text through Unicode key events instead of SendKeys, so + ^ % ~ ( ) { } and non-Latin text arrive as written.

    Minor Changes

    • —

      82769f1: computer_use can read a window's controls and act on them by reference, on a host that declares uiTree — which the Windows backend of @namzu/computer-use now does.

      What changes for a caller of the tool

      • —Two new actions when the host declares uiTree and implements uiSnapshot and uiAct: ui_snapshot { window_id? } returns the window's accessibility tree as text, each control that can be acted on with a ref such as e12; ui_act { ref, action, value? } performs invoke, set_value, toggle, select, expand, collapse, focus or scroll_into_view on it and returns a screenshot afterwards. ui_act may be in a batch; ui_snapshot may not. Refs count up across snapshots and only the latest snapshot's are accepted, so a stale ref is refused rather than acting on another control. ActionInput gains both, so code that switches exhaustively over input.type needs two more cases — that is why this is a major release for @namzu/sdk.
      • —ui_snapshot is read-only; ui_act is destructive.
      • —list_windows now returns its window lines inside an untrusted-content frame (<namzu-untrusted kind="desktop-windows">), since titles are whatever each application shows. The lines themselves are unchanged; code that parsed the whole output text needs to skip the frame's three header lines.
      • —createComputerUseTool returns a ComputerUseTool, a ToolDefinition<ActionInput> with describeUiRef(ref) (experimental), which names the control a ref points at for a review screen.
      • —UiSnapshot gains optional title and app; UiElement.ref is empty for an element nothing can be done with. Both types stay experimental.

      @namzu/computer-use

      On Windows and WSL with the cua-driver backend, capabilities.uiTree is true and SubprocessComputerUseHost offers uiSnapshot(windowId?) and uiAct(ref, action, value?), read from cua-driver's UI Automation walk. A control is invoked in the background, without moving the pointer or bringing its window to the front; a field without a settable value is typed into when it is empty. An action whose driver died mid-request comes back as not done with an unknown outcome, never retried. The PowerShell fallback and the other platforms do not declare uiTree, so nothing changes there.

@namzu/http

1 release

  1. v5.0.2

    Patch Changes

    • —

      49491b9: The drivers now report more accurately how a response ended. The runtime uses this to tell a tool call the output limit cut off from one the model wrote badly.

      • —@namzu/anthropic: a tool call whose JSON does not parse no longer fails the whole stream with "the provider stream returned malformed data". The block close and the finish reason now reach the runtime. model_context_window_exceeded is reported as length, and refusal as content_filter. Both used to read as a normal stop.
      • —@namzu/bedrock: model_context_window_exceeded is reported as length, and guardrail_intervened as content_filter. A tool call opens with the id the driver keeps, so its arguments never arrive before an id.
      • —@namzu/http: the OpenAI dialect maps finish_reason instead of passing the server's string through. function_call becomes tool_calls; max_tokens and max_output_tokens become length; model_length, context_length, context_length_exceeded and model_context_window_exceeded become length with finishDetail: 'context_window'; eos, eos_token, end_turn and stop_sequence become stop. error fails the stream with a ProviderRequestError (kind: 'server'), as OpenRouter's does. A value it does not know reports no finish reason at all, which the runtime reads as a response that did not say how it ended, rather than as a normal finish. The Anthropic dialect gets the Anthropic mapping above and opens a tool call with the id its arguments carry.
      • —@namzu/openrouter: finish_reason is mapped the same way. error (the upstream model failed mid-generation) now fails the stream with a ProviderRequestError (kind: 'server').
      • —@namzu/deepseek: insufficient_system_resource now fails the stream with a ProviderRequestError (kind: 'server'). It used to read as a finished answer.
      • —@namzu/openai: Codex's response.incomplete is reported as length, or as content_filter when that is the stated reason, with its usage. The stream used to end with no finish reason, so auto-continuation never ran.
      • —@namzu/anthropic, @namzu/bedrock and @namzu/http's Anthropic dialect: a model_context_window_exceeded stop also carries finishDetail: 'context_window', so the runtime does not ask the model to continue a reply that filled the whole context window.
      • —@namzu/anthropic, @namzu/google and @namzu/openai (Codex): the list of sources a driver appends after a hosted search now carries contentOrigin: 'driver' on its stream chunk. The text is unchanged. Without the mark, a tool call the output limit cut off before that list would be reported as malformed.

      If you branch on finishReason, expect length or content_filter where you saw stop for these cases.

    • —

      2d4ff9a: Earlier entries in this package's CHANGELOG no longer name one particular application built on namzu, or quote a path from outside this repository. This changes only the CHANGELOG.md that ships in the tarball; no code or types change, and there is nothing to do to upgrade.

@namzu/lmstudio

1 release

  1. v2.1.4

    Patch Changes

    • —

      49491b9: A response contextLengthReached cut off, after it had produced content, now carries finishDetail: 'context_window' alongside finishReason: 'length'. It used to report a plain 'length' finish, indistinguishable from maxPredictedTokensReached, so the runtime auto-continued it — sending a reply that had just filled the model's context window straight back in, in a prompt now longer than the window it had just overflowed. maxPredictedTokensReached is unchanged: still a plain 'length' finish, since the output-token budget (not the conversation's length) is what ran out, and the runtime's ordinary auto-continue is right for it.

      contextLengthReached with no content is unaffected: chatStream still fails that turn with a context_overflow error before any finish reason is reported.

    • —

      2d4ff9a: Earlier entries in this package's CHANGELOG no longer name one particular application built on namzu, or quote a path from outside this repository. This changes only the CHANGELOG.md that ships in the tarball; no code or types change, and there is nothing to do to upgrade.

@namzu/ollama

1 release

  1. v2.2.5

    Patch Changes

    • —2d4ff9a: Earlier entries in this package's CHANGELOG no longer name one particular application built on namzu, or quote a path from outside this repository. This changes only the CHANGELOG.md that ships in the tarball; no code or types change, and there is nothing to do to upgrade.

@namzu/openai

4 releases

  1. v4.1.4

    Patch Changes

    • —023224a: A failed OpenAI or Google request now reaches the host with the provider's own type, code, parameter and request ID, as an Anthropic failure already did, so Details read "Status: 403 · Provider said: “…” · Type: invalidrequest_error · Code: model_not_found" with "Request ID: …" on its own line. The error's detail for a vendor SDK error whose message is only a sentence is now a JSON body of those fields ({"error":{"message","type","code","param"},"request_id"}) instead of the bare sentence; read providerCode or the Details of a host rather than matching the sentence text. The Google driver now reads its refusal body and request-ID header. The vendor project line in Details reads "Project: proj…".
  2. v4.1.3

    Patch Changes

    • —567ada8: Updated package examples and evaluation fixtures for the SDK's ToolRegistry removal. Browser, computer-use and LSP documentation now show toolsets; the evals use toolsets when they call the kernel. Provider changes are test fixture migrations only, with no provider runtime API change. Consumers that pass tools to the SDK should use toolsets: readonly Toolset[] and wrap their definitions with toolset(source, definitions).
  3. v4.1.1

    Patch Changes

    • —82769f1: The Codex (Responses) driver sends images returned by tools with detail: "high" instead of "auto". With auto the backend could pick the 512-pixel low view of a screenshot, so a model answered in coordinates of an image the tool never produced. Images the user attaches still go with auto. No change is needed on your side.
  4. v4.1.0

    Minor Changes

    • —4375e72: A provider-hosted web search now says what it searched for. StreamChunk.delta.hostedTool, and so the hosted_tool session event and the hosted.tool SSE event, may carry query (the search query), url (a page the provider opened instead of running a query) and results (how many sources it reported). Codex fills them from the search call's action, Anthropic from the search block's streamed input and its result list, Google from the grounding metadata. Each field is optional and absent when the provider does not say; nothing that already reads id, name and status changes. A custom driver can start emitting them whenever it knows them.

@namzu/openrouter

2 releases

  1. v3.0.3

    Patch Changes

    • —567ada8: Updated package examples and evaluation fixtures for the SDK's ToolRegistry removal. Browser, computer-use and LSP documentation now show toolsets; the evals use toolsets when they call the kernel. Provider changes are test fixture migrations only, with no provider runtime API change. Consumers that pass tools to the SDK should use toolsets: readonly Toolset[] and wrap their definitions with toolset(source, definitions).
  2. v3.0.1

    Patch Changes

    • —

      3641102: Prompt caching now places explicit breakpoints instead of sending a top-level cache_control: { type: 'auto' }, a value OpenRouter does not define (its top-level field takes { type: 'ephemeral' } and puts the breakpoint on the last cacheable block — with request-only context at the tail, the part the next request replaces).

      For anthropic/, google/gemini and qwen/ models, the models OpenRouter documents as needing explicit breakpoints, a cache request marks the last static system message and the last non-system message before request-only context, each inside a content part (a string becomes one text part). A system message directly before the context is the step's guidance, which changes from step to step, so it is passed over rather than marked. Other models, which OpenRouter documents as caching automatically, are sent no marker and keep their plain string content.

ag-ui

3 releases

  1. v3.0.1

    Patch Changes

    • —6194bda: Keep questions and frontend tool calls answerable when live tool-start, progress or question-park events arrive around the interrupt boundary. The adapter now expires a wait on evidence that its own call or turn ended, rather than on any unread native event. Buffered events retain their order on resume, and existing client-answer deadlines and cancellation remain in effect.
  2. v3.0.0

    Major Changes

    • —567ada8: The AG-UI adapter now reads tools and review metadata from the query's toolsets, preserving review exemptions and per-tool timeouts after the ToolRegistry removal. Hosts must return QueryParams.toolsets from createQuery instead of passing a ToolRegistry as tools. Wrap definitions with toolset(source, definitions) and require @namzu/sdk >=48.0.0; use @namzu/ag-ui 2.x with SDK 45.1–47. MCP resource tools likewise rely on their owning toolset for source trust instead of a removed per-tool provenance field.

    Patch Changes

    • —567ada8: Exercise repeated AG-UI runs with one stable native session owner and verify that a host resolving the same session under another tenant is refused before its history reaches a model. This updates the integration checks for the SDK's stricter session attribution; the adapter API is unchanged.
  3. v1.0.0

    Major Changes

    • —

      3e7a97b: Requires @namzu/sdk >=44.0.0 (was >=36.0.0), where the kernel's run became a turn inside a session. Earlier @namzu/ag-ui versions break against SDK 44.

      What changes on the wire. The AG-UI ids themselves do not: RUN_* events still echo the client's threadId and runId verbatim. Only the Namzu-owned names change.

      • —Error codes on RUN_ERROR: NAMZU_RUN_ERROR, NAMZU_RUN_CANCELED and NAMZU_RUN_PAUSED are NAMZU_TURN_ERROR, NAMZU_TURN_CANCELED and NAMZU_TURN_PAUSED.
      • —New NAMZU_TURN_IN_PROGRESS: a second run on a thread whose Namzu session already has an active turn is answered with RUN_ERROR carrying this code instead of starting a parallel one.
      • —The pause custom event namzu.run.paused is namzu.turn.paused.

      What changes for a host. A thread maps to a Namzu session and each AG-UI run to a new turn in it. The client's runId is recorded as the turn's origin.externalTurnId, never used as a Namzu id. MESSAGES_SNAPSHOT is the session's folded transcript, so it shows the answer after any guardrail or review rewrite, never the raw text.

      Renamed exports. AGUIRunContext, AGUIRunOptions, AGUIRunUI and AGUIRunUIOptions are AGUITurnContext, AGUITurnOptions, AGUITurnUI and AGUITurnUIOptions, with the same shapes. The AG-UI protocol's own names (RunAgentInput, onRunFailed and the other callbacks) are unchanged.

      What to do. Upgrade @namzu/sdk and @namzu/ag-ui together, rename imports of the four types above, and rename any client code that matched the old error codes or custom event name.

browser

4 releases

  1. v2.0.1

    Patch Changes

    • —

      d8782dd: The interactive terminal and namzu exec start about 150 ms to 250 ms faster. They no longer import @namzu/sandbox, @namzu/browser, @namzu/telemetry and the browser engine just to learn that those packages are installed; the launch-time capability line now reads the packages' manifests, and @namzu/browser loads playwright-core on the first browser launch instead of when it is imported.

      One consequence: a capability package that is installed but fails to load is no longer reported at launch (the namzu.capability.broken row). It surfaces where the package is first used, and namzu doctor still imports each package and reports it. probeOptionalPackage takes an optional { load: false } for the manifest-only answer, and probeCapabilities uses it by default; nothing a caller of either passed before changes.

  2. v2.0.0

    Major Changes

    • —d3e35af: Visible password or one-time-code fields on an ordinary public page no longer trigger a whole-page sign-in or second-factor handoff. Component galleries and settings pages can now be read and navigated. Actions targeting credential controls, submission of forms containing them, and all untargeted keys are refused; unrelated controls remain usable. Page-created dialogs can be dismissed but not accepted by the agent. Screenshots mask input and textarea pixels that Playwright can locate, including child frames, but closed shadow roots and text drawn elsewhere may remain visible; snapshots still hide detected credential values. A recognized sign-in or second-factor address, HTTP authentication challenge, CAPTCHA, bot wall, or a bare HTTP 401 page with a credential field still triggers a handoff. If your application relied on a field alone to hand off a sign-in page at an unusual address, add that address to signInAddresses.
  3. v1.0.0

    Major Changes

    • —c8bb87a: The exported classifyHumanRequired no longer returns http-auth for a bare HTTP 401 or 407. Callers that relied on that result must supply the matching wwwAuthenticate or proxyAuthenticate signal, or handle undefined as access denied. The browser host now fails bare 401/407 page loads instead of asking a person to sign in. Pages with separately detected sign-in, CAPTCHA or bot-check signals still request a human handoff. Failed navigation attempts, history commands with no destination and same-document URL changes retain the current document's access result, including its challenge headers. A back-forward cache restore reuses the restored document's access result.
  4. v0.1.1

    Patch Changes

    • —567ada8: Updated package examples and evaluation fixtures for the SDK's ToolRegistry removal. Browser, computer-use and LSP documentation now show toolsets; the evals use toolsets when they call the kernel. Provider changes are test fixture migrations only, with no provider runtime API change. Consumers that pass tools to the SDK should use toolsets: readonly Toolset[] and wrap their definitions with toolset(source, definitions).

cli

21 releases

  1. v36.1.1

    Patch Changes

    • —f86a456: A Pal's conversation now uses the Pal's current model. Before, the model the Pal had when its conversation opened was kept for good, so changing the Pal's model afterwards (or setting it after the page filled in a default) never reached the conversation. Nothing to change on your side; a model picked by hand in the Pal's composer is still kept for that conversation.
    • —Updated dependencies [d328c1b]
      • —@namzu/zen@4.0.2
  2. v36.1.0

    Minor Changes

    • —

      fa24b15: Scroll back through the conversation without leaving Namzu. Press PgUp at the message box and a view of the whole conversation opens above it, one page up; PgUp and PgDn page, the arrow keys move a row, Home goes to the start. While you are reading older text, one quiet line above the message box says ↓ Back to bottom · esc (shorter on a narrow terminal), and starts with New activity · if the reply keeps arriving while you are scrolled up. Press End or Esc, or scroll down to the last page, and the view closes with the session exactly as you left it. Your terminal's own scrollback is untouched and nothing changes unless you press PgUp.

      This release also brings existing users the @namzu/sdk 50.1.0 and @namzu/zen 4.0.1 tool-schema fix. The CLI did not publish a new version with it, so namzu upgrade had nothing to install; this version does. If a free Zen model refused a request that included the edit tool, upgrading fixes it and you need to do nothing else.

  3. v36.0.0

    Major Changes

    • —

      c82b951: Change the local Pal provider's default foreground ownership from strict command lifetime to computer lifetime. Rebuild the shipped local-computer image from this release to use the new default. To retain the previous behavior and continue using an existing strict image, set normalExitPolicy: 'strict' when calling createLocalVirtualComputerProvider. A successful foreground launcher can leave Blender or another application open without retiring the entire computer; the application remains owned by that guest allocation until the computer stops. An authenticated execution-reservation acknowledgment prevents an upgraded host from silently admitting this policy against an older image.

      Generic workers and registered background jobs retain strict process ownership. Failed launchers and timed-out or cancelled commands cannot transfer live descendants; if termination cannot be confirmed, the worker retires the allocation. A completed launcher does not claim its surviving application has terminated.

      CLI Pal computers use the same new default. Set NAMZU_PAL_COMPUTER_NORMAL_EXIT_POLICY=strict before starting the CLI or desktop host to retain their previous command lifetime and use an older strict image.

    • —

      0f5aa96: Ready-computer Pal prompts now default to general application work guidance: understand references and acceptance criteria, make reversible changes, observe and correct the actual result, and validate saved files and requested exports before delivery. SDK hosts that need the previous prompt behavior can pass workGuidance: 'basic' to buildPalSystemPrompt. This guidance shapes model behavior; it does not certify semantic correctness.

      The SDK read tool now refuses real PNG, JPEG and WebP bytes as text. Mount the optional view_image / createViewImageTool for saved-image inspection, or use a format-aware binary reader when pixels are not the desired result. getBuiltinTools() retains its previous tool roster. Saved artifact images provide no GUI screenshot authority.

      The CLI and desktop Pal runtime now explicitly mount view_image and import_reference_images. Image import is a reviewed file-write operation using only the current operator input and admitted guest. Original image bytes and manifests survive durable review; current plan, pause and control guards remain in force. Ordinary chat and unavailable-computer sessions do not acquire guest tool access.

      The local Pal sandbox adds authenticated, acknowledged bounded byte reads. Rebuild the local Pal image from this release to inspect saved images; an older worker fails explicitly before file contents are requested. Existing whole-file requests and generic worker defaults are unchanged.

      The Pal image also includes the standard file utility for format checks. This reports file type; it does not establish application or visual quality.

    • —

      4367edc: Manual Pal conversations now default to automatic approval for tools in their owned virtual computer across the terminal, ACP and desktop. Previously a review callback or the interactive composer selected Ask first, requiring repeated approvals. To retain that behavior, explicitly choose Ask first, use /permissions prompt in the terminal, or send permissionMode: 'prompt' through ACP. Existing explicit desktop selections are preserved.

      Ordinary conversations retain their existing defaults. Explicit Plan, strict and review selections, configured deny rules, host escape refusal, ownership, pause and operator-control fences remain effective. This grants no authority for host files, other Pals or ungranted communication routes.

    • —

      57c99b2: Scheduled script jobs can opt out of a project folder with workspace: none (--workspace none in the CLI). Namzu creates a private working directory only after operator confirmation, and a script never loads that directory as a project configuration.

      Interactive schedule proposals now bind noteworthy results to the exact source conversation. The CLI stores them as durable host notices and displays them on resume without adding a fake model turn or changing the model's conversation history. An optional JSON script report lets a polling script distinguish a quiet check from a change and carry a bounded scheduler-owned state value between runs.

      The CLI default for newly confirmed interactive proposals changes from detached results to source-conversation delivery. An unavailable source keeps its result pending and holds later runs until delivery succeeds. Existing jobs retain their previous behavior. To keep a new job detached, review namzu schedule edit <job> --delivery none; the operator must confirm any exact pending results being waived. New workspace, source-delivery and report fields use job format v3, which older CLIs refuse rather than silently ignore. Upgrade the scheduler service together with the CLI.

    • —

      c590142: New scheduled agent phases now default to unlimited tokens (tokenBudget: 0) instead of 500,000 tokens. A positive request or configured limit still applies, and existing saved jobs retain their confirmed values. Set limits.tokenBudget to 500000 or pass --token-budget 500000 to retain the old default for new jobs. To remove an existing job's limit, run namzu schedule edit <name> --token-budget 0 and confirm the edit. Iteration and timeout defaults remain unchanged.

      The SDK schedule tool accepts zero for an unlimited agent token allowance. Unlimited previews omit the numeric daily ceiling and identify the absence of a token limit; usage accounting continues to measure actual tokens.

    • —

      12b5af9: Session task snapshots now reach model requests as user-role runtime context instead of system instructions, including checkpoint resumes and compaction. Agent-maintained subjects and descriptions are planning data. Move any host policy formerly encoded in task descriptions to trusted instructions; task storage, tools and durable conversation records are unchanged.

    • —

      f7f48f9: Add the explicit cmd ShellDialect and change the native Windows host-shell metadata default from sh to cmd. The physical command shell remains Node's platform shell (normally CMD); no PowerShell or WSL switch occurs. Update exhaustive dialect handling to include cmd and pass the actual value to authorization helpers.

      CMD lines are conservatively opaque until a matching parser exists. Command-specific allow rules and skill patterns no longer pre-approve native CMD commands. The existing unknown-program escalation still requires exact-call review, even when the gate has a whole-tool allowance. Use that review or explicitly select a POSIX interpreter rather than incorrectly labelling CMD as sh. Sandboxed Linux guests retain sh.

      SDK query prompts disclose the active command tools' host or guest execution dialect, including minimal and cached prompts. Explicit Windows bash.exe and sh.exe overrides retain their matching POSIX dialects.

      Native Windows CLI model shell calls inherit the exact-call review requirement. The scheduled-run floor refuses CMD commands rather than checking them with POSIX quoting; confirmed script jobs still accept only explicit bash/sh interpreters. Use an installed POSIX interpreter where appropriate or run reviewed interactive CMD calls.

    Minor Changes

    • —

      4314f90: A tool call a person declined at review is now recorded as declined by them, with what they said, so a host that reopens the conversation can show it. Nothing you already send or receive changes shape, and no code needs to change to upgrade.

      New and optional: ToolReviewAnswer's reject, AcpPermissionOutcome's reject and the reject_tools decision take declined?: { note?: string }; an ACP client's reject answer takes the same field; and a generic ToolCallView result carries declined?: { note?: string }. A host that sets declined on its reject gets, on that call's tool_completed, a view whose label is the call's target (path or command) and whose note is the person's words, cut at 4,000 characters. A host that does not set it, a call the authorization gate refused even when the person rejects the rest of its batch, and every other refusal by policy (strict or plan mode, no one to ask, the authorization gate, a repeated failure), is recorded exactly as before: an error result with no presentation. The text the model reads is unchanged. ToolExecutor.executeBatch takes an optional fifth argument for the same purpose.

      The CLI's ACP server passes the field through, so a client such as Namzu Desktop that reports "the person said no" now shows "Declined" after a reload instead of "details unavailable". The terminal UI does not report it yet, so a call declined there still reloads as before.

    • —

      7f5ac50: Expose explicit paused-turn recovery through optional AcpAgentGateway.retry and ACPServer.retrySession. Hosts retain the existing ordered update, review, cancellation and single active execution owner without inventing a new prompt.

      The desktop CLI host advertises scoped retry-status and retry methods. It resumes only the exact verified checkpoint of a classified retryable provider pause with resolved original accounting. Human decision holds and uncertain provider usage remain blocked, including unlimited turns; this feature never resets a ledger or abandons an active turn. Same-process retries preserve captured approval/effort and the recorded model. After reconnection, default Retry is unavailable when the original approval settings cannot be verified; a deliberate host retry must explicitly select a permission mode in ordinary conversations. Pal recovery also pins the authenticated original computer generation and environment identity before sending, and rechecks them at resumed provider/tool entries. A cold, originally offline, replaced or unavailable Pal computer lifetime cannot be substituted with the currently ready computer. Pal ownership and current computer-control guards remain required.

    • —

      53aed2a: Add authenticated Pal channel ingress with captured host connections, a fresh actor per event, immutable native conversation targets, and shared durable inbox delivery. Recorded reply and action routing verifies the original owned conversation receipt and current authority; it does not send remote messages automatically. Hosts must provide a trusted event verifier and explicit receive, wake, reply and action policy. The CLI includes a private local HMAC fixture adapter and a bridge to exact native parked tool-review actions; other channel actions remain unsupported.

    • —

      8cf64b3: Offer reasoning effort for the second external engine. Its model rows now carry the effort levels the engine reports (none for a model without support), so the Desktop and terminal effort controls appear. A turn that asks for a different level restarts the engine on the same session between turns with --effort; a level the model does not offer is refused before anything is sent. Nothing to change for callers who select no effort: the engine keeps its own default.

    • —

      24cb6b4: Compose installed Codex CLI app-server and Claude Code persistent stream-json engines behind the ordinary desktop conversation host, with their native model catalogues, exact session/profile/workspace binding, streamed message replacement, one-request native tool approval and confirmed interruption/shutdown. Keep these routes separate from Namzu's borrowed Codex and Anthropic provider credentials. The desktop selects the engine before first dispatch and manages conversations as peer tabs with their own drafts, engine bindings and selected models.

      The native app-server adapter offers its declared review policies, including explicitly confirmed Full access; the initial stream-json adapter offers supervised and plan modes. Both omit unsupported controls and explicitly deny unsupported typed questions. Codex models advertise only their discovered effort options; the initial Claude adapter offers no effort control, and neither route accepts attachments yet. Native metadata is not proof of successful account authentication. Retain uncertain sends and approvals for reconciliation rather than replaying them; the initial engine has no authoritative history-query adapter.

    • —

      afc5bf5: The desktop host adds two methods, namzu/conversations/archived (list a project's archived conversations) and namzu/conversations/unarchive (restore one by sessionId). Both require folder trust, and restore only admits an owned, archived, non-Pal conversation. Existing methods are unchanged; nothing to do on upgrade.

    • —

      cea5487: namzu computer setup takes --resume-command <command>, the command Windows runs once at the next sign-in when setup needs a restart. Nothing changes if you do not pass it.

    • —

      4d8f504: Add namzu computer status, namzu computer setup and namzu computer stop-idle for Pal computers on Windows. status says in one sentence whether this PC is ready; setup shows what it will install, the download size, the time, and whether Windows will ask permission or a restart may follow, asks Continue? [y/N], then runs; stop-idle shuts down the idle Linux machine. --json prints versioned JSON lines for Namzu Desktop (setup --json needs --plan <id> as the consent). setup without a terminal and without --yes or --plan exits with the new code 75 after printing the plan. --bundle <dir> or NAMZU_PAL_COMPUTER_BUNDLE names the folder with the bundled image; with NAMZU_PAL_COMPUTER_ENGINE=wslc and no NAMZU_PAL_COMPUTER_IMAGE, the Pal runtime uses the manifest's imageRef. On Windows a Pal computer that is not set up now says "Run namzu computer setup to set this up.", and the model is told that the computer is not set up. Nothing changes for existing commands; the agent runtime still never installs anything.

    • —

      3c2bda7: namzu acp --desktop now hosts pseudo-terminals for the desktop application: ten namzu/terminal/* methods (status, create, list, attach, detach, write, resize, ack, kill, close) and two notifications (namzu/terminal/data, namzu/terminal/exit). A terminal keeps a bounded replay ring and a headless screen, one view holds the keyboard at a time, output is paused when the viewer falls behind, and killing a terminal ends its whole process tree, including programs that detached themselves with nohup or setsid (the host lists a terminal's descendants before the polite stop and kills the survivors), and a terminal's folder must be inside the project after links are resolved. Nothing changes for namzu, exec or any other command, and a host that predates this answers -32601 for the new methods.

      node-pty@1.1.0 is a new optional dependency and @xterm/headless (now pinned to 6.0.0, previously a development dependency) and @xterm/addon-serialize are new runtime dependencies. node-pty ships Windows and macOS binaries and compiles on Linux at install time; if that fails the install still succeeds, the rest of the CLI is unaffected, and namzu/terminal/status answers available: false with the reason. With pnpm 10, a workspace that depends on @namzu/cli and wants terminals on Linux must allow node-pty's build script (onlyBuiltDependencies).

    • —

      3c2bda7: The interactive namzu (and namzu resume) now accepts --provider <id>, --model <id>, --effort <level> and --permission-mode <prompt|accept-edits|auto|strict|plan>, written before any command. They choose how that one launch starts and are never saved: preferences.json is not touched, so the next plain namzu starts where you last left it, and /model, /effort and /permissions still change the session as before. --provider replaces the provider chain with that provider alone, --model alone re-models the saved primary (it needs a saved provider, otherwise the provider list opens and says so), --permission-mode wins over --yolo, and an --effort the model does not offer is reported and the provider default stays. An unknown provider opens the provider list with the reason instead of starting unusable. Before a subcommand other than resume these flags are refused with exit 64 rather than ignored; exec and drain keep their own --provider, --model, --effort and --permission-mode after the command name.

    • —

      fc86c05: NAMZU_PAL_COMPUTER_ENGINE accepts wslc (Windows' own container CLI) besides docker and podman, and NAMZU_PAL_WSLC_BINARY points at a specific wslc.exe. Unset keeps Docker, exactly as before. Any other value still stops the Pal computer with an error, which now reads "must be docker, podman or wslc".

    • —

      846557b: namzu acp now attaches a file change preview to the permission request of an edit or write call: the path as the tool resolves it, the file's current body (null for a new file) and the body after the call, computed with the SDK's own apply code. Files over 1 MiB, binary files, paths outside the turn's directory and calls the tool would refuse get no preview. Clients that do not read the new preview field are unaffected.

    • —

      fae17a3: The desktop host now answers namzu/project/untrust, and the trust store exports untrustDir(dir). They remove only the entry that names the exact folder from ~/.namzu/trust.json and report an ancestor entry that still covers it as stillTrustedBy, so a caller can say the folder remains trusted instead of claiming it was removed. Nothing existing changes: trustDir, isTrusted and namzu/project/trust behave as before, and a host that predates the method is simply not asked. trustDir and untrustDir now write trust.json to a uniquely named temporary file and rename it, so a concurrent reader never sees a partial file.

    • —

      888e37b: A paused provider turn that has no token limit can now be retried in place even when earlier requests have unknown usage (a 502 is the usual cause). namzu/sessions/retry-status then answers { retry, unknownUsage: <count> } instead of a notice, and the retry is a new request; the unknown usage stays recorded. A turn with a finite limit still answers the notice and refuses the retry, so an ACP or Desktop client that treated every unresolved-usage pause as "cannot retry" will now see a retry target for unlimited turns.

      Desktop connections gain namzu/sessions/abandon-paused (optional turnId): it closes the paused provider turn without repeating it (turn_failed, code abandoned, its unknown usage kept on the ledger) so the same conversation takes the next message. It refuses a turn that waits for a person's decision or did not stop on a provider error. Hosts that do not advertise the method keep today's behaviour.

    • —

      0b247f4: namzu acp --desktop: namzu/sessions/retry-status adds two optional fields, closable: true (the paused turn stopped on a provider failure and waits for no decision, so namzu/sessions/abandon-paused may close it before a new message) and retry.original ({provider, model} that stopped). namzu/sessions/retry now runs the paused turn on the model the conversation has selected, not on the model that failed, and no longer refuses when the provider differs; the approval settings the turn began under are kept across a model change. A client that ignores the new fields behaves as before, except that a retry after choosing another model now runs on that model instead of being refused.

    • —

      1fa60b9: An API key for a provider can now be saved once and kept. Namzu stores it in api-keys.json in the Namzu home (created private to you and verified private after writing, like the other credential files; Google keeps its own file), reads it after the environment, and shows it as saved API key · this device. A key from an environment variable still wins, so nothing you exported is replaced. Until now a key pasted in the terminal picker was held for that session only; that path is unchanged, but a key saved some other way is now found by every Namzu screen.

      namzu acp --desktop advertises five new methods for the desktop app's Settings: namzu/providers/connections (every provider and how it is connected, never a key), save_key, remove_key, test (a cheap authenticated check, not a model turn) and refresh. DetectionSource gains a stored-api-key kind and the provider status rows may carry anonymous: true for the keyless free tier.

      Removing the last saved key deletes api-keys.json; delete it by hand to forget keys without the app.

    • —

      cdb4aec: File history now outlives the session. Closing the CLI used to delete the snapshots /restore rolls back to; it no longer does. Each conversation keeps file-history/turns/<turn id>.json and content-addressed file-history/blobs/<sha256> (both the file before and after every edit and write) until the conversation is deleted, 30 days after its last edit, or the history passes 512 MiB, whichever comes first, oldest turn first. Expect file-history/ to take disk space you did not have before; deleting a conversation removes it, and no flag or config key tunes the 30 days or 512 MiB yet. After a crash the next open settles any half-recorded edit, /restore lists the turns of the conversation that is open (including after a restart or /resume), and edits from a resumed stream land in their own turn instead of a stray one. /restore N no longer overwrites blindly: it undoes turn N and every later turn newest first, checking each file against what the turn left, and a file you (or a shell command) changed since is left as it is and listed with the reason instead of being replaced; if a later turn's change to a file could not be undone, the older turn's change to it is left too. The undone turns are no longer deleted: their manifests and bodies stay, marked undone, they leave the /restore list, and a turn with files left alone stays listed as partly undone so /restore N run again finishes the rest. The report also names files no history covers and says when a turn ran shell commands. There is no force option in the TUI: to restore a file it kept, put it back to what the model left (or restore it by hand) and run /restore N again. This is a minor, not a major: where nothing changed since the turn the result is the same as before; the only difference is that a destructive overwrite of your own later edits no longer happens. A symlink is resolved before it is recorded, a sandboxed edit is never snapshotted from the host path of the same name, a restore writes through a temporary file and rename and keeps permission bits, and a failed edit leaves no entry. Nothing exported, no flag, config key or wire shape changes, so this is a minor. namzu acp --desktop also gains three extension methods, namzu/turns/undo-status, namzu/turns/undo-preview and namzu/turns/undo, for a client that undoes one reply's file changes (see docs/cli/turn-undo.md); a client that does not call them sees no change.

    • —

      33f9109: Add optional desktop ACP live-input extensions for active ordinary Namzu conversations. A bounded, scope-owned mailbox delivers operator text at safe query boundaries and releases delegation waits without cancelling the child. Hosts can reconcile pending or delivered input IDs after cancellation or a lost acknowledgement; core overlapping prompt refusal, existing next-turn queues and native-engine routing remain unchanged.

    • —

      56e846c: Expose the selected model's exact reasoning effort choices and per-message tool permissions to the desktop operator. Read installed plugin metadata without starting a model session or executing plugin modules; plugin changes remain scoped to an existing idle conversation.

      Add scoped desktop-host methods for reasoning settings and plugin inventory. Explicit plugin enable/disable choices remain local to the conversation and are restored when changing its model; they do not update startup configuration.

      Agent sessions expose optional image/document attachment support declarations from their exact primary provider route. ACP rejects new attachments before a model request when that route explicitly cannot receive them; undeclared support retains the existing runtime policy.

    • —

      a5e0038: Codex model names in the Desktop model list now read like the Codex app ("GPT-5.6 Sol" instead of "GPT-5.6-Sol"), and each engine's model list marks that engine's own recommended default with an optional default: true flag on the row. Namzu provider lists no longer say "Namzu default" in a row's note; the flagged row replaces it. Model ids are unchanged, so saved selections keep working; clients that read the model list can ignore the new flag.

    • —

      e94e149: namzu acp --desktop gains four methods for the Desktop conversation header: namzu/conversations/rename ({ sessionId, title } returns { title }; an empty title restores the title derived from the first message), namzu/conversations/fork ({ sessionId } returns { id, title }; refused while a reply is running or when there is nothing to copy), namzu/conversations/markdown ({ sessionId } returns { markdown, truncated }, cut at 4 MiB of UTF-8) and namzu/project/git (returns { branch, subject }, or null when the folder is untrusted, not a repository, git is missing or git takes over 3 seconds). All are new; existing methods and clients are unaffected. Each conversation method needs a trusted folder and a conversation this project owns, the same as archive.

    • —

      9ef6506: Expose live tool progress and actual turn failure messages through optional fields on existing ACP update variants. Add explicit optional namzu/* ACP host extensions and advertise their method names. Gateway history loading now receives the requested workspace as an optional second argument; existing one-argument gateways remain compatible.

      The CLI now loads durable, project/tenant-scoped ACP conversations and refuses archived writers. namzu acp --desktop enables operator methods for folder trust, scoped conversation/history, safe provider metadata and background jobs. Ordinary ACP connections retain the core method set. The private desktop preview uses this existing runtime and log rather than a second conversation store.

    • —

      3ff96b4: Add the namzu/providers/models desktop ACP extension for reading a configured provider's model catalogue on demand. Results contain model IDs, labels, optional notes and an explicit fallback notice when listing fails or is unsupported. The extension reuses the terminal picker's access filter, keeps permitted default and session-specific choices, and never returns provider credential envelopes or raw driver error text. Existing exact model selection remains available.

    • —

      5e2fb3a: Desktop hosts expose six additive Pal communication methods for known peer consent, inbox delivery metadata, and activity subscription listing, creation and disabling. Reads and writes use the authenticated Pal workspace and captured application home, exact revisions, and original source conversations. The first communication request pins the physical home for that host connection; replacing it requires reconnecting even when copied IDs and revisions match. Incoming permission is read-only; reverse and wake grants remain separate explicit actions.

      These management methods do not invoke a model, start a computer, dispatch messages or run a CLI subprocess. Snapshots omit message bodies, host paths and cursors; source labels include explicit user names, while derived prompt titles remain New conversation. Unreadable records reject with redacted errors rather than erasing known rows. Partial subscription setup remains disabled and unknown writes are not automatically retried.

    • —

      56d88dc: Add the desktop host's read-only namzu/tasks/list extension and opt into negotiated namzu/tasks/update notifications. Restore the existing planning list from its durable session store before new work, preserving failed outcomes and deletions. Scope reads to the authorized project, session, tenant and Pal claim; exclude task descriptions, metadata and filesystem paths. Older clients continue using the existing ACP update union.

    • —

      56e846c: Add optional inline user attachments to ACP prompt requests. Hosts opt into delivery with supportsPromptAttachments: true; initialization then advertises promptAttachments: true. Clients can check this capability before sending bytes, and gateways receive the validated attachment payload alongside the prompt. Stored attachment references are refused across this boundary; inline payloads are limited to eight attachments and 3 MiB of decoded bytes per message.

      The CLI opts in and preserves image/document attachments in the actual user message and settled conversation history. Existing plain text requests keep their behavior. The private desktop admits native file picks or dropped/pasted bytes as bounded images and UTF-8 text, keeps draft and queue ownership, and refuses image submission to an older CLI rather than silently dropping it.

      New image/document inputs are refused before send when the selected live provider explicitly declares that it cannot receive them. Desktop files remain available for retry with a suitable model. Attachment/settings-only drafts survive a connection rebuild, and queue editing restores captured options together with its text and files. Pre-turn CLI errors retain their actual diagnostic rather than becoming a generic failed turn.

      ACP also accepts optional AcpPromptOptions with a reasoning effort and tool review mode. Hosts opt in with supportsPromptOptions: true, advertised as promptOptions: true; unsupported or malformed explicit settings are rejected before a turn starts. The CLI validates reasoning effort against the actual selected runtime's supported menu and applies the captured review mode to that turn.

    • —

      0f3df20: Add an optional versioned work display snapshot to ordinary desktop conversation history. It restores retained message and turn identities, actual saved action outcomes and bounded recorded public tool views from the same strict journal read. Existing text messages and limits are unchanged. Legacy missing details stay explicit; reopened history grants no execution, approval or retry authority.

    • —

      2057fd6: Surface provider-hosted web search and fetch activity through existing ACP tool updates, using qualified IDs and only provider-reported state and bounded public captions. These display receipts do not grant local execution or approval.

      Desktop conversation history adds optional validated journal message IDs and timestamps for messages and work, and restores saved hosted search receipts. Missing timing and unfinished operations remain explicit; reopening history does not invent a current clock or a successful outcome. Existing message text and execution authority are retained.

    • —

      dc306f3: The Desktop host (namzu acp --desktop) gains two read-only ACP methods, namzu/project/changes and namzu/project/diff, so the Desktop Changes view can review uncommitted work. changes lists the working tree against HEAD (status, added and removed lines, renames, binary and untracked files, at most 2,000 files) and answers null for an untrusted folder or a folder that is not a repository; diff returns both sides of one changed file, confined to the project folder and capped at 2 MiB. Nothing to change on upgrade: an older Desktop never calls them, and a newer Desktop talking to an older CLI simply hides the view.

    • —

      6194bda: Send an operator message directly from a selected child transcript while preserving the parent's draft. Running children accept bounded input at the next valid request boundary; finished shared-workspace children retained by this process can receive a new task in the same conversation with current permissions, credentials and budget. Saved-only and isolated-workspace continuation is refused. The parent receives host-attributed assignment notices and explicitly framed child results; a durable observation journal preserves unacknowledged reports across restart without restoring execution authority.

      Only a parent response ending with end_turn acknowledges its captured observation snapshot. Timeouts and other unsuccessful stops keep notices pending, including stops before a model request; observations arriving after that snapshot are not acknowledged accidentally.

    • —

      e447a47: The desktop model catalogue (namzu/providers/models) now carries an optional current: true on a row when the execution engine itself says the model is current rather than an older release: the Claude Code engine's alias rows (a model id that names no version) and the Codex engine's default row. A client that ignores the field is unaffected, and rows without it read as before. Nothing needs changing to keep the old behaviour.

    • —

      d063837: Opening an external engine from the desktop host (namzu acp --desktop) starts the engine's process once instead of two or three times. The Codex app-server that model discovery starts is kept (closed after two idle minutes, or when the connection closes) and the first conversation opened in that folder takes it over instead of spawning again; re-reading the model list reuses a running server too. A model list read in the last ten minutes is answered from memory, so opening the picker no longer spawns the engine each time, and the last good list of each installed engine build is kept in engine-models.json in NAMZU_HOME (ids, labels and effort levels only), so a later launch names the default model without waiting for the engine. A new build of the engine (its executable's path, size or modification time changes) drops its old list at once.

      What changes for an ACP client: namzu/harnesses/select no longer waits for the engine's model list, so an engine that cannot list models (signed out, no models) is accepted there and the error comes from namzu/providers/status or namzu/providers/models instead; the status row of an external engine carries an optional identity (a short hash naming the installed build) for clients that cache the list; the harness view and the model listing carry optional timings (spawn, initialize and model-list milliseconds, no paths or account details) that a client may record. Nothing changes for callers that ignore the new fields. To keep the old always-fresh read, there is no flag: a list older than ten minutes is read again on the next namzu/providers/models.

    • —

      d063837: namzu acp --desktop gains the host method namzu/harnesses/release, which takes an engine (codex-cli or claude-code) and ends the idle engine servers the connection keeps for it, answering { released: true }. It never touches a conversation that is running; it only closes the Codex app-server that model discovery parked, so a client can replace the engine's program on disk (a running executable cannot be overwritten on Windows) before it updates it. The Desktop uses it before running an engine's update command. A client that never calls it sees no change, and a host that predates the method answers -32601, which the Desktop treats as "nothing to release".

    • —

      b6ea5ca: Models work on first open with nothing to set up. With nothing saved, Namzu Desktop and the terminal app now start on what they find, in this order: a sign-in Namzu already reuses (Claude Code, Codex), an API key in the environment, a local server (Ollama on port 11434, LM Studio on port 1234; each probe waits at most 300 ms and they run together), then the keyless Zen free models. They say what they picked in plain words ("Using your Claude sign-in, found on this computer.") and save nothing, so a key or sign-in added later wins on the next open. /model and the model picker change it.

      What changes for you: in the terminal app a first run with an API key or a local server in the environment no longer stops at the provider picker; it starts on that provider. The picker still opens when nothing is available. In Desktop the keyless Zen free models now count as usable, as they already did in the terminal app, so a new person's message box is ready and Send works at once; the "Add an API key or sign in to start" card now appears only when nothing at all is available. To keep the old terminal behaviour of choosing yourself, save a choice once with /model.

      When Zen declines a free request that carried no key, both apps now say "Zen didn’t accept the free model right now. Try again in a while, or choose another provider. A Zen API key also works." and keep the technical detail under Details (Desktop) or --debug (terminal). The generic provider.permission explanation in @namzu/sdk no longer says the key authenticated: a 403 only says the request was refused.

    • —

      79a7354: Add --message <text> to the interactive namzu: the session sends the text once, as a plain prompt (a leading /, ! or # is not a command), when its composer is ready, with the provider, model, effort and mode chosen by the other launch flags. It is session-only, shown as the first user message, and not sent again on resume or reload. Before a subcommand it is refused with exit 64, like --model. Nothing changes for callers that do not pass it. Pass --message=<text> when the text may start with a dash.

    • —

      7473449: Interactive terminals in the same project can discover and message one another with /peers, list_sessions and send_session_message. Idle terminals can start a turn for peer context; busy terminals receive it at the next safe request boundary. Peer input remains separate from operator instructions and cannot grant approval. Different permission modes are refused. Use /peers off to refuse new mail and pause delivery in this terminal.

      The mailbox is bounded and process-local; queued is an acceptance receipt, not proof of model delivery. Conversation switches cannot redirect pending mail. Child-task activity now labels accepted messages as queued, and SDK manager documentation describes the inbound callback that query-backed agents actually consume. The SDK peer envelope refers to the host's reply tool rather than prescribing a CLI child-task tool.

    • —

      6f46da3: Add durable Pal addresses, owned conversation routes, incoming message storage, captured senders, explicit directed consent and executor-correlated messaging tools. Acceptance is separate from verified session-log delivery and recipient execution; unresolved claims are retained for recovery without repeating effects.

      CLI and desktop Pal sessions use the same SDK messaging tools and durable query input. Add pal grant/revoke/inbox/dispatch: wake consent is explicit and finite dispatch uses preapproved tools. No external transport, automatic listener or Pal Team membership is installed by this feature.

    • —

      9dad339: namzu/sessions/abandon-paused accepts an optional withdrawMessages boolean. Absent or true it behaves as before: the closed turn's messages leave the history the next request carries. Send false when the person goes on with a different message and the closed turn's message should stay in what the provider is shown. A reloaded Desktop conversation no longer shows the stopped partial reply of a turn whose message was withdrawn. Nothing breaks for a caller that sends only the conversation.

    • —

      84a9472: Add optional provider-owned Pal computer takeover through PalComputerControl, PalComputerInput and runtime control methods. Hosts can transfer an idle guest to human input using its exact generation, return it without waking the Pal, and observe current control authority. Providers without the optional port retain normal Pal execution and explicitly refuse takeover.

      The runtime serializes control operations, refuses new Pal admissions during operator control, and requires each subsequent admission to obtain its own fresh screenshot before GUI input. CLI desktop ACP exposes take-over, return and bounded input methods; optional screen generation pinning rejects stale captures.

    • —

      53aed2a: Add durable subscriptions for closed Pal activity metadata from an exact owned original conversation. Observation, disclosure, recipient receipt and idle wake have independent current permission checks. Custom publication authorizers must also implement the final accept phase requiring current observation, disclosure and receipt together. Accepted facts enter the shared Pal inbox before cursor progress; retries preserve identity and verified observation trails suppress feedback.

      The CLI adds finite pal subscribe, subscription, activity and unsubscribe commands. Publication performs no inference or guest startup; explicit dispatch still requires current wake/tool authorization. Original turn intake and exact recorded delivery receipts are required for causality, and unknown evidence or exhausted journal read budgets reject without advancing progress.

    • —

      53aed2a: Resume a Pal's real parked tool review in its original session and local guest computer. Explicit review holds record durable checkpoints, preserve the conversation's pinned profile and original turn limits on resume, and hold later batches for their own decision. Existing live terminal permission prompts keep their behavior when no hold is requested.

      Trusted hosts can authenticate one-batch review actions against the exact original journal request and current consent. Durable operation and decision reservations prevent automatic replay, and acknowledgement requires an actual recorded decision resolution. Unconfirmed attempts retain their ownership for reconciliation rather than executing again.

      The required trusted host currentPermissionMode callback keeps current plan mode stricter than a restored approval and later rule-allowed batches. Channel action payloads cannot change the mode or substitute the captured host ports. For this action, auto and accept-edits are limited to prompt on later review requests, so a one-batch answer never grants automatic later approval. Independent operator rules and ordinary CLI automatic modes retain their existing behavior.

    • —

      5d91ac1: The desktop host's namzu/pals/communication/inbox answer now carries receivedAt (epoch milliseconds, when the Pal's inbox accepted the input) on every row, and, for a message the owner sent from their own conversation (sourceKind: operator-conversation), the message text (up to 4,000 characters) so the owner can read back what they approved. Messages from Pals and channels still carry no text, and the existing fields are unchanged. Nothing breaks: a client that ignores unknown fields keeps working, and a strict one that compared whole rows must allow the two new keys.

    • —

      7f5ac50: Allow owned Pal conversations to use explicitly installed application MCP servers inside their original virtual computer. Install a reviewed guest bridge and a version 1 /home/namzu/.config/namzu/mcp.json manifest with absolute guest paths, an explicit tool allowlist and outcomeProtocol: "namzu-v1"; host MCP preferences and credentials remain excluded.

      Retain rich MCP output and provenance, current permissions, writer and computer lifetime checks. Do not retry or take over after a bridge loses application completion evidence. The bundled pinned Blender bridge reports uncertain socket operations before upstream turns them into successful text replies, preserving the computer recovery fence.

    • —

      ce52b90: Add optional host-only PalComputerScreenStream, PalEnvironmentLease.screenStream and PalRuntime.computerScreenStream(palId, generation) for observing an exact current Pal computer over read-only RFB. Existing providers remain compatible; providers without the optional capability explicitly refuse live observation. Keep allocation authorization private in the embedding host and enforce read-only observation server-side; the stream does not grant operator input authority.

      The CLI exposes the owning desktop ACP stream method with geometry and generation rechecks, and reuses initialized registry roots to avoid repeated Windows ACL subprocesses while retaining fresh Pal definitions and directory identity checks. The local computer image adds x11vnc and authenticated binary WebSocket transport. Rebuild the installed image explicitly and restart the owning computer to enable live observation; older images retain screenshot/input behavior and advertise no stream capability. Persistent workspace and browser profile volumes survive.

      The guest desktop includes a themed wallpaper, a real dock, Files, terminal and browser launchers, plus a local browser home page. Normal browser-window closure keeps the desktop alive; owned profile shutdown retains browser flush ordering.

    • —

      815433f: Add DiskPalStore.delete(id, expectedRevision) and the desktop host's namzu/pals/delete metadata endpoint. Deletion uses a terminal immutable revision, hides the current Pal, and refuses future updates and execution admission while preserving historical profiles, journals, host files, and guest data volumes. Hosts must stop active work and confirm computer cleanup before deleting its identity. Existing custom PalStore implementations require no new method. Readers from older releases do not understand terminal deleted profiles and should not be used to read a registry after deletion has been published.

      Add the desktop host's namzu/conversations/archive endpoint. It performs a scoped, writer-gated soft removal while retaining the durable conversation and files. Successful retries return archived: true; a physically absent strict journal returns archived: false, missing: true, never claiming that an absent conversation was archived. Hosts may discard only their own known unsent local projection from this missing receipt. Existing terminal archive commands are unchanged.

      Release a connection's exact idle native-engine writer before desktop archival, reserving the conversation until the operation settles. Active or unresolved work is refused and a failed native close retains cleanup authority for retry. The runtime reservation method is optional for existing embeddings.

      Keep a Pal's approved stored control-path spelling when Windows native path resolution changes only its case, after verifying the same physical directory. Physical project identity remains canonical, while existing Pal profile revisions and journal ownership paths remain unchanged. Different directory objects and non-case aliases are refused.

    • —

      6f46da3: Add optional PalAppearance preferences to saved Pal definitions and create/update inputs. Character and color selections persist in immutable profile revisions; existing records retain their absent appearance and hosts may choose their own display default.

      The CLI accepts --appearance <character>/<color> on pal create and pal update, and the desktop ACP create/update extensions carry the same validated preference. Supported characters are pixel, sprout, and spark; supported colors are green, blue, amber, violet, and rose.

    • —

      f7f48f9: Add persistent Pal identities, immutable profile revisions and an SDK runtime that requires an explicitly provided virtual computer before execution. Hosts can retain one warm computer per Pal, serialize active conversations, enforce current pause state and retry failed computer cleanup without losing ownership.

      Add namzu pal list/create/show/update/pause/resume/chat and desktop host Pal methods. Pal conversations pin a saved profile and bind exactly one Pal in their session log. File, shell and computer tools run in the Pal guest, with no host folder, host browser or host plugin fallback. The local computer image and engine must be set up explicitly before chat can execute.

    • —

      6943add: The Desktop can now start a Pal for a message waiting in its inbox, on the person's click. After an approved send_pal_message to a Pal that is not running, the sender's transcript asks "Start name now?" and the Pal's own page offers the same (or Resume for a paused Pal). Start runs the same finite dispatch as namzu pal dispatch in the Pal's own host, in the background: the sender's turn is never held, the Pal reads the message in its own conversation under its own permission rules, and its approvals stay in its own tab. A Pal that is already running is only reported, repeated clicks start once, and a missing Pal computer is explained in plain words with Retry instead of starting.

      The CLI's desktop host gains two methods, namzu/pals/inbox/status and namzu/pals/inbox/start; an older runtime simply shows no question. Nothing about sending changes: a message still only reaches the inbox, and send_pal_message still never starts a Pal itself. If you embed the CLI's authorization, an owner-conversation wake now also accepts a click-evidence object (operatorWake: { evidence }) besides true; with neither, a wake is still refused.

    • —

      6ff961f: The Namzu engine's ordinary conversation (terminal and Desktop) can now give a Pal work. It mounts list_pals and send_pal_message in every such session, and its system prompt names the owner's Pals, separate from sub-agents, only while some exist; a session with no Pals leaves no Pal files. send_pal_message always asks the person (in every permission mode and even after "allow all"; strict and a session with nobody to ask refuse it) and returns "Sent to name's inbox" as durable acceptance, never delivery or an answer. A paused or removed Pal is refused. The message lands in that Pal's inbox as untrusted context from the owner's conversation; it does not start a stopped Pal, which namzu pal dispatch (typed by the owner) still does. namzu pal inbox and the Desktop Pal communication view list such a message as operator-conversation. External engines and a Pal's own sessions are unchanged.

      If you keep an exact list of the ordinary session's tool names (a plugin allowlist test, a snapshot), it now includes list_pals and send_pal_message; withhold them with withheldTools (withholding send_pal_message also drops the prompt block).

      Desktop: the approval card reads "Message to name" with the whole message and a note that it goes to the inbox only, and the action row reads "Messaged name" with the hover text "Sent to inbox", including after a reload.

    • —

      53aed2a: Read and dispatch Pal messages, authorized activity observations and authenticated channel messages through one SDK ledger. namzu pal inbox preserves existing peer row fields and adds explicit source metadata for observation/channel rows without exposing message bodies. Pal sessions and finite dispatch check source-specific current consent; channel execution denies unless a trusted host adapter is explicitly injected.

    • —

      083e4f9: The terminal app now reports problems in plain words. A failed or paused reply is one sentence that names the provider (for example "OpenAI did not accept your API key. Check it in /setup, or pick another provider with /model."), with no bracketed error ids, checkpoint ids, raw JSON or config-key names; a paused reply continues with Enter. The exit hint prints namzu resume <id> instead of the node path, and an unexpected crash prints one line and saves the stack to ~/.namzu/logs/crash-<time>.log. If you scraped the old terminal text ([provider.auth], Checkpoint preserved:, Fatal:), read the structured output of namzu exec --json instead, which is unchanged; set NAMZU_DEBUG=1 or pass the new global --debug flag to see the technical detail on screen.

    Patch Changes

    • —

      b8852a1: abandonTurn takes a new option, withdrawMessages. Pass it when you give the turn's prompt back to a person to send again (a message box restored after "continue without this reply"): the abandoned turn then contributes no message to the next turn's context, so the provider receives the re-sent prompt once instead of twice. The records stay in the log; the closing turn_failed carries failure.details.messagesWithdrawn: true, which the fold honours. Nothing changes unless you pass it: an abandoned turn's messages stay in the context by default. SessionLog.abandonTurn accepts the same option as a fourth argument; a custom SessionLog that wraps another should forward it.

      The CLI's namzu/sessions/abandon-paused passes it, which fixes Desktop's Continue without this reply sending the person's message to the provider twice.

    • —

      efb7b06: Extend the closed AcpSessionUpdate output union with agent_thought reasoning boundaries and agent_message completed messages. Exhaustive consumers must add handlers for both kinds before upgrading. Existing variants and required fields remain available. Reasoning boundaries contain no private replay payload; readable reasoning still arrives through agent_thought_chunk.

      Message chunks carry optional actual message/turn identity and public commentary/final-answer metadata. Completed messages provide selected settled content and ordered public text parts. turn_ended can provide the authoritative result and its actual answer-message identity; consumers must replace the preview when a result is present, including an empty result that clears blocked output.

      Keep the coarse AcpStopReason union and add optional exact reason to terminal updates and prompt responses. Current runtime guardrails/refusals map to refused, resource limits to max_turns, and parked segments retain reason: 'paused' under the older coarse cancellation label. Legacy aliases remain accepted. Prompt preparation cancellation returns an explicit cancellation reason even if no runtime event was produced. Streamed updates are sent in admission order and finish before the prompt response. Tool completion retains optional runtime-measured duration.

      Permission questions wait for already-admitted updates and fail closed when that context was not delivered. Update errors are caught immediately, retain the first failure and release the prompt slot after flushing; failed question sends settle their pending wait. Delivery failure does not undo runtime work already recorded.

      Update the CLI session-isolation regression expectations to include the exact cancellation and completion reasons; the CLI runtime is unchanged.

    • —

      a32c679: Mark unfinished Claude messages closed by native failure or interruption as cancelled instead of completed replies. Preserve partial text, the failed turn's actual status and error, and messages the engine already completed.

    • —

      642104d: Preserve observed Claude tool calls and results when the engine emits thinking, text and tool blocks under one native message ID. Complete streamed messages at the native message boundary instead of the first content block, retain authoritative inputs and interrupted partial text, and ignore duplicate or older-operation receipts. Public wire shapes and permission admission are unchanged.

    • —

      6ff961f: Removing a sidebar conversation in the desktop app no longer fails with "This conversation does not belong to this project." when its journal is gone or was saved under a regenerated identity; the host now names the identity change and leaves the journal untouched.

    • —

      dbc466a: The terminal app's header now names the product: "Namzu v35.0.0 · by Cogitave".

    • —

      2852da1: The CLI now ships as a code-split bundle, so a launch reads a handful of files instead of about 1,700 modules: namzu --version drops from about 49 ms to 36 ms, the terminal's first frame from about 555 ms to 265 ms, and namzu exec reaches the model about 120 to 210 ms sooner. The namzu bin name and path (dist/bin.js), every command, option and output stay the same, and no code needs to change to upgrade.

      What to know if you inspect the install: dist/ holds a few dozen bundled files and chunks plus the type declarations, not one file per source module, so a deep import of @namzu/cli/dist/... (never in exports) no longer resolves. quickjs-emscripten-core and @jitl/quickjs-wasmfile-release-sync are now direct dependencies of the CLI, because the bundled SDK finds them by path; they are the same versions the SDK already required. @namzu/browser, @namzu/sandbox, @namzu/telemetry, @namzu/computer-use and node-pty are still loaded from your install, not bundled.

    • —

      42ef5e4: namzu computer and the Pal computer hint read better on Windows. The "Run namzu computer setup" hint is chosen from the sandbox's setup code, not by matching its message, and its wording no longer mentions wslc. A failed setup's Details: line is a plain sentence with the Windows error number when there is one, not the internal code. A pending restart reads "Restart your PC to finish setting up Pal computers." instead of "Nothing needs to be set up.", and namzu computer setup says it once without asking to continue. --json results from a failed setup gain an optional exitCode.

      namzu computer setup now also says to run it again after the restart when setup steps are still waiting, and the setup hint no longer appears for a failure that setup cannot fix.

    • —

      4367edc: Keep terminal and ACP conversations running when refused or truncated tool arguments produce an invalid presentation. A malformed write call no longer throws while drawing its label: the CLI uses the tool name, preserves the original error and lets the model repair its input and complete the turn. Valid tool views, tool validation and permission checks retain their behavior.

    • —

      d521813: On Windows, the interactive screen now re-measures its terminal every 250 ms, so a Namzu terminal tab that is narrowed or split redraws to the new width instead of keeping the width it started with (lines cut mid-word, a message box with its right edge in the wrong place). Nothing changes on other platforms. The footer also shortens a Windows path at a backslash and shows the home folder as ~ from USERPROFILE. No option or output format changes.

    • —

      1fa60b9: The terminal app no longer opens with "Computer use is unavailable in this session: The openai provider cannot return images in tool results…" when the chosen provider cannot show screenshots. The reason still travels with the computer-use tool, so the model tells you the moment computer use is actually asked for. A desktop that cannot start at all is still reported when the session opens.

    • —

      0ef4884: On Windows, reading a Pal computer's status no longer starts the Linux machine Pal computers run on. Namzu Desktop reads that status every few seconds while a Pal's panel is open, so the machine used to start again right after it was stopped for being idle. Status now reads what setup recorded; starting the computer still starts the machine. Nothing changes for Docker or Podman.

    • —

      9d422c6: On Windows with the wslc engine, a Pal computer status that cannot be read now reports "not ready" with the setup hint instead of probing the engine, because that probe starts the Linux machine (which may have just been stopped for being idle). Nothing changes for Docker or Podman.

    • —

      adb0498: The terminal app no longer tells you to check your internet connection when a server on your own computer or network (Ollama, LM Studio, a local address) is down: it names the server and says to start it. Startup problems (the working folder, saved data, plugins, computer use, the browser) now print one plain sentence, with the error text shown only under --debug as "Details:". The setup screen says how a credential was found in words ("Signed in with Codex") instead of an internal name, and a long folder name in CJK or with emoji is shortened by its display width. Nothing to change on your side; scripts that matched the old Working directory is unavailable or Failed to construct <provider> text should match the new sentences.

    • —

      6f2eb8e: On Windows, folders in the terminal app's sentences (the trust prompt, saved-file notices, /status) use backslashes like the footer does (~\Work\clients\leaf-folder). The trust prompt now wraps to the box's real width, so at 54 columns it no longer leaves "files" alone on a line, and its title, sentences and key line break evenly at every width from 40 up. Nothing to change on your side.

    • —

      0888f71: An approval request for a write that would leave the file exactly as it is now carries a file preview with before equal to after, where it carried none. A client that treated a missing preview as "cannot show the file" can now say "no change". A client that draws before and after as a diff shows an empty diff.

    • —

      f0aeb9b: The npm package page of @namzu/cli is rewritten for people who use the terminal app. Only the README changed; the CLI code and its behaviour are unchanged.

    • —

      2e8f811: A conversation's title no longer includes the text of an attached file. Desktop sends an attachment to the model as Attached text file: "name" after the typed words; the saved title now comes from the typed words only, and is the first file's name when only files were sent. A title that was already saved with attachment text keeps it until the conversation is renamed.

    • —

      3c2bda7: The interactive screen now reads keys from the Windows console input device when process.stdin is not a terminal but the output is, which is the case when the CLI runs as Electron's plain-interpreter mode inside a pseudo-console (a terminal started by the desktop application). Before, it stopped with Raw mode is not supported on the current process.stdin. An ordinary launch, and every non-Windows launch, keeps process.stdin; nothing to change on upgrade.

    • —

      2d2a6c3: On Windows, opening a Pal or a conversation no longer starts three Windows processes (whoami, icacls, icacls /save) every time it checks that a state folder is private. The check still runs the first time this process meets a folder, and it runs again when the path names a different folder than before (another device, file id or creation time), so a folder replaced under the same name is re-proved. What changed is that a folder this process already proved private is not re-proved while it is still the same folder, so an access-list edit made to that same folder while Namzu runs is no longer noticed until the next start. The signed-in account's id is also asked for once per process. Nothing changes on other systems. On a Windows machine with antivirus scanning, this removes seconds from opening a Pal.

    • —

      0de3571: The model pickers (TUI and Desktop catalogue) list Zen's free models before the ones that need an API key and show them under separate "Free" and "API key" headings (a model with no published price follows under "Other models"; a list with only one group draws no headings and keeps an "(API key)" note on its key models), and mark a model whose limits the catalogue has not published yet with "Limits not published yet". Nothing to change on your side; only the order and headings of the Zen list and one extra note differ. The Desktop catalogue row gains an optional group field ('free' or 'key', Zen only). A client that ignores group draws no headings and loses the "(API key)" marker on a list that has both groups, because the per-row note is dropped once the headings carry it.

    • —

      815433f: Fix installed Codex conversations rejecting effort choices offered by their native model catalogue. SDK admission now receives the discovered effort capabilities; dispatch still checks each selection against the current selected model before sending a native turn.

    • —

      9279c97: The terminal agent's composer is usable as soon as the saved or automatic model is known, instead of after the provider's model list (GET /models) answers. With a gateway or local server that answers in 1 second, the composer used to appear 1 second later; now it appears at the same time as with a fast server. The reasoning-effort menu follows when the list arrives; a message sent before then uses the chosen model and the provider's default effort (a launch --effort applies once the menu arrives), hypermode pins its effort when the menu arrives, and a list that fails leaves the effort menu out without a message. namzu exec and ACP session creation still wait for the list, as before. AgentSessionOptions gains an optional deferEffortCatalogue; leave it off to keep the old behaviour.

    • —

      479adc4: Reopened ordinary Desktop conversations now keep an assistant reply you stopped mid-answer. Previously the partial text was visible live but vanished after a restart; it is now restored from the saved journal and shown as stopped. A stopped reply that had no text, or that was later replaced, still adds nothing, and Pal chat history is unchanged. The namzu/conversations/history result gains an optional stopReason: 'cancelled' on assistant rows; existing clients that ignore unknown fields keep working and nothing needs to change.

    • —

      24cb6b4: Distinguish a subscription credential removed or cleared by its owner from a remote authentication rejection in desktop model catalogues. Show a fixed sign-in-unavailable notice without exposing owner paths or provider diagnostics. TLS and network failures retain their catalogue-loading notice.

    • —

      7e1068a: Do not display a tool-only assistant record as a media message when reopening ordinary desktop history. Text accompanying tools and actual media placeholders remain visible; the model's complete recorded history is unchanged.

      Desktop task, retry-status, and metadata ownership reads no longer synchronize the installation-wide session index. ACP history loading verifies and folds one scoped journal snapshot without preparing a provider. Retry inspection retains the authenticated application home, trust, project, tenant, Pal claim, and checkpoint checks; actual retry and write admission remain enforced. Existing injected storage openers are preserved until the host supplies a direct-read opener.

    • —

      2ae7169: The desktop app shows a failed reply with the same quiet row a finished reply ends with: Retry and Copy as icons, other recovery actions as small muted text, Details as a text toggle and the time (leading, as on a finished reply) on one line, instead of filled buttons and a native disclosure. No flag, setting or stored data changes.

    • —

      292ed84: Read desktop conversation history through one fresh owned session scope without opening or synchronizing the installation index. Authorization, strict journal reads, and Pal reply filtering use the same admitted application home, with folder trust rechecked before reading the journal. Project, tenant, and Pal ownership checks remain enforced; conversation listing and other operations retain their indexed behavior.

    • —

      3727e2b: Show only the provider's actual listed model rows in the desktop catalogue. Do not insert unavailable registry defaults or saved models as available choices. Keep the current route visible in provider status and report omitted, failed, unsupported and timed-out catalogues with explicit notices. Credential rejections receive a safe authentication notice without remote diagnostics. Recognize a typed rejected subscription refresh grant as an authentication failure while keeping TLS and network failures distinct from invalid credentials. Use the driver's strict account listing when provided instead of mistaking a bundled fallback for a live account catalogue.

      Check the selected model's credential access and supported wire before closing the previous conversation session. An invalid anonymous Zen switch now leaves the previous session intact, and a new conversation can select an eligible free model before preparing an unavailable saved provider.

      Fix model preparation on a newly created ordinary ACP conversation before its first journal exists. Authorize only a session actually published on the same connection with the exact canonical workspace; unknown IDs, foreign workspaces and stopped slots remain refused. Pal conversations still require their durable ownership claim.

    • —

      a32c679: Preserve a stored assistant message's explicit commentary or final-answer phase in desktop conversation history when its public text still matches the recorded parts. This keeps progress narration in the work details after reopening an ordinary conversation. Pal history also omits an unchanged interrupted reply only when its own durable completion records cancellation; completed earlier replies and revised or legacy messages remain visible. These display projections leave journal messages and model replay unchanged.

    • —

      e7c61d2: namzu exec "<prompt>" no longer adds its quarter-second wait for piped input to the start of the turn when stdin is a pipe nobody writes to (a CI step, a script, a host that spawned it and kept stdin open): the wait now runs alongside the rest of the startup, which took the request about 150 ms to 250 ms sooner in that case. The rule itself is unchanged: input that begins within a quarter second of exec first looking at stdin is read to its end and appended to the question, and exec without a prompt argument, or with -, still waits as long as the pipe takes. Only the quarter second is promised: a producer that is slower to write its first byte (for example a slow curl | namzu exec "summarise") may or may not be picked up, depending on how busy the rest of startup is; put the text in the prompt if you depend on it.

    • —

      e366729: The first model request is smaller: computer_use, browser, browser_act, list_pals and send_pal_message are no longer sent while they cannot work (the provider cannot return images or the desktop host is unavailable, no browser engine can run, the owner has no Pals), and are sent again as soon as they can. About 1,150 tokens fewer in the terminal on a machine with none of those. Nothing a working tool does changes; a model that names a suspended tool is told it is suspended instead of receiving its own refusal text.

    • —

      a923f6b: Check complete Godot input batches before the original MCP handler and runtime WebSocket send. Invalid nested boolean strings previously aborted the pinned runtime coroutine and could leave earlier input dispatched without a final reply. Return a field-specific, repairable not_dispatched error instead, without coercing values or sending any event from the refused batch.

      Advertise the accepted boolean/number constructor types separately for each input event in live tool listings. Use literal pressed: false to release a key, mouse button or action; action strength: 0 does not release it. Retain open dictionaries and encoded top-level event compatibility without adding required fields or injected defaults, and keep all other tools unchanged.

      Preserve valid Godot constructor inputs, optional defaults, top-level server coercion and ignored application dictionary keys. The generic SDK dictionary schema remains unchanged. Existing unknown application effects still require computer stop; validation never clears that barrier or replays input.

    • —

      c8613e0: Clarify Godot input timing in the installed bridge's tool description. Put a bounded hold duration on the press event's delay_after_ms, or on the release event's delay_before_ms; a delay after release waits with the input already released. Show a 250 ms press-hold-release example and distinguish empty key events from waits and composite click's internal click_delay_ms.

      Argument types, defaults, dispatch behavior and pinned upstream source remain unchanged. Update an existing guest bridge from the shipped asset while idle, then reconfigure normally to load its updated description.

    • —

      b0b6afa: The Desktop model list writes a GPT display name such as "GPT-5.6-Sol" as "GPT-5.6 Sol", the way the Codex app does. Only names that start with "GPT-" and end in "-<Word>" change; model ids sent to providers, and every other family's labels, are unchanged. Nothing to do on upgrade.

    • —

      e03a000: On Windows, namzu upgrade now removes the namzu.ps1 wrapper that npm writes next to namzu.cmd after each update (only when it belongs to @namzu/cli). PowerShell prefers that wrapper and the default execution policy refuses it, so without this, typing namzu failed with "running scripts is disabled" after every upgrade; now it runs namzu.cmd. Nothing changes on macOS or Linux. When namzu upgrade cannot tell which installation it is running from, its message now links the installation guide (https://github.com/cogitave/namzu/blob/main/docs/cli/installation.md) instead of a README section that no longer exists.

    • —

      7f3b74b: Reset Codex reasoning effort to the selected model's actual native default when Default effort is selected, including after a model change. Refuse an omitted effort when the native model catalogue supplies no valid default, rather than silently retaining a previous turn's effort. Admit the native Codex review modes already advertised by its adapter; Claude Code retains its supported Ask first and Plan modes.

      Retain the actual completed native final-answer identity in Codex terminal receipts and history reconciliation so successful turns no longer erase streamed replies when the terminal notification contains no text.

    • —

      c1027fb: Add a connected computer observation to PalSystemPromptOptions.computer, with the current control mode, so hosts can describe an existing device independently of permission to execute tools.

      Fix Pal chat composition when the operator holds its connected computer. The Pal now receives its actual connected/control state instead of being told the computer is unavailable. Operator-held chat retains zero guest tools and performs no guest inspection or allocation; Return control admits the existing guest capabilities on the following turn.

    • —

      4e5693f: Continue without this reply now works in a Pal conversation. The Pal session had no way to close a paused turn, so the Desktop app showed "This connection cannot close paused turns." and left the failed reply in place. Nothing to change on your side. The two refusals a host can still give for a paused reply are now plain sentences.

    • —

      a923f6b: List Pal conversations with one per-call session-index synchronization instead of reopening and synchronizing the entire index for each candidate. Membership, current workspace identity, pinned revision, tenant, project, exact working directory and archive state remain validated from fresh profile and journal reads before the output limit. Standalone membership checks and ordinary-workspace or alias refusals retain their existing behavior.

    • —

      d8d562e: Add buildPalSystemPrompt and PalSystemPromptOptions for hosts to compose a Pal's saved identity, conversational language, public output discipline and truthful computer availability. The helper does not acquire execution authority. CLI Pal sessions use these shared instructions while retaining their pinned model and execution policy. Authenticated name and appearance edits update the next turn's display identity without rewriting the original introduction.

      Add PalRuntime.admitConversation, PalConversationAdmission and PalRuntime.computerChanging. Hosts can admit exclusive model-only chat without starting a computer, with current pause, cancellation and ownership checks. Guest acquisition remains separate and requires the real guarded environment; the existing mandatory admit contract is unchanged. CLI manual chat supplies zero tools while offline or operator-controlled, then adopts the actual guest toolset on a later turn after explicit startup or returned control. Directed dispatch and checkpoint resume continue to require guest admission. The standalone pal chat command keeps its existing computer-startup preflight.

      Add palConversationGreeting and PalConversationGreeting to reconstruct a stable English onboarding prelude from an owned conversation's original profile revision. CLI Pal claim and list results expose this separate host-authored intro without starting a model request or manufacturing a journal turn. Pal desktop history omits explicit assistant commentary and tool-call narration from its public display projection while retaining the original journal.

    • —

      56d88dc: Restore the existing durable planning tools and bounded task context to Pal computer-work conversations. Tasks survive reopening and checkpoint resume in the same conversation; offline chat remains tool-free and planning does not grant host filesystem or computer access.

      Use the task tools' compact activity labels for active calls and receipts in terminal and desktop views, including resumed turns.

    • —

      a08ad64: The terminal app's notices for a provider that cannot use tools, images or documents, and for a repaired conversation history, are now plain sentences that name the provider by product name ("Ollama can’t use tools, so Namzu will only chat while you use it. Switch to another provider to let Namzu work with files and commands.") instead of "Capability warning (tools): Provider 'ollama' does not support tools — 42 registered tool(s) were stripped…". A repaired history says what happened in words, without counts, and the Zen free-model rows read "experimental, and Zen may refuse them". The setup screen and the provider list are reworded the same way and wrap without a stray leading space. The previous wording still appears under --debug. Exported transcripts use the new sentences. Scripts that match the old transcript text should match on the new sentences, or run with --debug.

    • —

      f74677f: Provider failures and key checks in the desktop app read in plain words. A model the account cannot use no longer blames the key, and an account out of credit, a timeout, a missing model and an oversized message each say what happened. The CLI host's namzu/providers/test can now answer an unchecked key with a cause (quota, busy, down, no-answer); callers that ignore it behave as before. The model list's failure sentences name the provider, and the signed-out Codex message now reads "Codex needs you to sign in with its own account."

    • —

      e447a47: tool_completed gains an optional savedPresentation: one label line (at most 200 characters) or a command's first line, never the output, journaled for every call that has no diff, and for an external engine's tools from a name map. Nothing existing changes: presentation keeps its diff and cancelled/declined shapes, and live hosts that ignore the new field behave as before. A host that replays history can read presentation ?? savedPresentation so a reopened conversation names its actions instead of showing a placeholder. The desktop host does.

    • —

      8b9cb0a: Correct regression verification of the skill save confirmation when a long home path wraps the suggested destination label across terminal rows.

    • —

      800f80f: Launches after the first reuse V8's compiled code for Namzu's own modules, which takes about 100 ms to 150 ms off the interactive terminal and namzu exec. The cache is kept in cache/compile inside the Namzu home (~/.namzu, or NAMZU_HOME), and only when that folder already exists; a read-only or unwritable home just runs uncached. Set NODE_DISABLE_COMPILE_CACHE=1 to turn it off, or NODE_COMPILE_CACHE=<dir> to put it somewhere else. Deleting the folder is always safe.

    • —

      ab97512: namzu --version and namzu --help answer in about 50 ms instead of about 480 ms. The command shell no longer imports every command and @namzu/sdk before it parses the command line; each command's module is imported when the command runs. Help text, option choices, exit codes and the output of every command are unchanged (the root help and every subcommand's --help were diffed against the previous build).

      Anything that imported emitBootNarrative from the package's internal cli.js now finds it in boot-narrative.js; it was never part of the package's exports.

    • —

      d8782dd: The interactive terminal and namzu exec start about 150 ms to 250 ms faster. They no longer import @namzu/sandbox, @namzu/browser, @namzu/telemetry and the browser engine just to learn that those packages are installed; the launch-time capability line now reads the packages' manifests, and @namzu/browser loads playwright-core on the first browser launch instead of when it is imported.

      One consequence: a capability package that is installed but fails to load is no longer reported at launch (the namzu.capability.broken row). It surfaces where the package is first used, and namzu doctor still imports each package and reports it. probeOptionalPackage takes an optional { load: false } for the manifest-only answer, and probeCapabilities uses it by default; nothing a caller of either passed before changes.

    • —

      e447a47: task_created and task_updated session events, and the ACP AcpTask row, gain an optional activeForm (what the task reads as while it is being worked on, such as "Running the tests"). It is absent when the model gave none, so nothing existing changes; a host that ignores it behaves as before. A host that draws a live plan can show it beside the task in progress. The desktop host passes it on when it lists a session's tasks.

    • —

      56d88dc: Use the SDK's existing task context selector for interactive task snapshots. Failed tasks closed before the current turn no longer stay open or crowd out pending work; a failed dependency is terminal without being reported as successful. Preserve current-turn outcomes and the original start time during resume.

    • —

      ca8656b: The terminal app and the command line read in plainer words. Nothing about how they behave changes, and exit codes stay as they were (64 for a usage mistake). A script that matches the old text needs the new one:

      • —An argument mistake such as namzu --effort turbo no longer prints commander's error: option '--effort <level>' argument 'turbo' is invalid. Allowed choices are …. It prints “turbo” is not a valid value for --effort. Choose one of …, wrapped at the terminal's width, then Run namzu --help to see what is available.; an unknown command reads Unknown command 'x'..
      • —A command step in the transcript is headlined from the kernel's typed result (ToolResult.facts): Ran npm test · exited with code 2, the command's own first and last lines with … N lines not shown, no STDOUT: labels. A conversation saved before facts existed keeps the old reading. A call you declined reads You declined this, so it did not run.
      • —A picker or prompt prints its key hints once, in its own box, in one vocabulary (↑↓ select · enter apply · esc back); the footer under the composer stays quiet while a box is open, and a hint line that is too wide loses whole hints, never half of one.
      • —“Turn”, “session”, “checkpoint” and “compaction” no longer appear in what a person reads: replies, conversations and summaries instead (/config lists “Reply limits”). Sizes read 48 KB and 1.2 MB, counts are pluralised, no millisecond figures are shown, and the app’s errors give a sentence and a next step instead of a pasted message. First-run with no key says Choose “Enter an API key” below, and subscription sign-in rows say uses a one-time code or opens your browser.
      • —Person-facing text uses ’ in contractions, never inside code, flags, paths or JSON.

      The .tsx files of the terminal app are now covered by the package's lint and format settings.

    • —

      e3b84d1: Closing a terminal on Linux now also ends a program that left the terminal's process tree straight away. setsid started from an interactive shell forks and exits at once, so the program was reparented before the host could find it and kept running after the terminal closed. Every program started in a terminal now inherits NAMZU_TERMINAL_ID, set to the terminal's id, and the host ends this user's processes that still carry it once the terminal has ended. This includes a background service first started from that terminal, such as a tmux server or ssh-agent, even if another terminal uses it by then. Nothing to change on upgrade; start a program that must outlive its terminal outside it. Windows is unchanged and adds no variable.

    • —

      12c7682: The terminal app's key lines stay on one row, a cut list says what it hides, and a stopped start reads cleanly. Nothing about what a key does changes. A script that matches the old screen text needs the new one:

      • —The line of keys at the bottom of the provider picker (and every other picker and dialog) is one row at any width, from 40 columns up. Where it used to wrap, it now drops its least important hints or says them short: ↑↓ or 1–9 select becomes ↑↓ select, esc or ctrl+c exit becomes esc exit, k enter an API key becomes k API key. ↑↓ select, enter apply and the key that leaves stay to the end. The sign-in and key-entry screens put their keys on a line of their own instead of at the end of a sentence.
      • —A provider list that is cut to the height of the terminal shows ↓ 3 more under the last row and ↑ 2 more above the first, and keeps the row under the cursor in view.
      • —A namzu.config.json that cannot be read shows its sentence alone, without a leading · and without the second "Startup stopped / Resolve the error above, then restart Namzu." block, which stays only under errors that do not say what to do. A screen with no composer no longer prints shift+tab to cycle in its footer.
      • —Node 22's ExperimentalWarning: SQLite is an experimental feature and might change at any time is no longer printed above the banner. Every other Node warning still prints.
      • —The slash menu (and the file menu) fits its key line to the width it really has, so tab complete no longer wraps at 60 columns or shrinks to a bare tab at 40, and a command's description is at most two lines ending in … where it goes on, so the menu is no taller than a 40-column terminal. A model list that keeps the current model says esc keep rather than losing enter apply, and "Could not list models" no longer ends in two full stops.
      • —The copy, edit and resume pickers, the permission prompt, the tool-output viewer and the provider-setup screen fit their key line to the room inside the box (two columns narrower than they assumed), so esc cancel no longer wraps at 58 to 62 columns and the edit picker keeps enter and esc at 40.
    • —

      5f7bb9b: The terminal app words three more things plainly. A namzu.config.json that is not valid JSON now reads "namzu.config.json isn't valid JSON (line 3, column 5). Fix it, then start Namzu again." with the line and column of the mistake; the parser's own wording is shown only with --debug. A read, search, write or edit step is headlined from the kernel's typed facts ("Read a.ts", "Searched for x · 2 matches in 2 files", "Edited a.ts +2 −1", "Created n.txt +2") instead of the model's receipt with a full path; conversations saved before those facts existed keep their old text. A provider picker shows its setup key once, in its bottom key line, instead of in a line of its own.

      If you match the old text of the config error (<path> is not valid JSON: <parser text>), match the file name and "isn't valid JSON" instead. ConfigLoadError.message for invalid JSON carries the new sentence and the parser's text is on cause.

    • —

      b8831a6: The terminal app reads a namzu.config.json (and the user and managed settings files) saved with a leading byte-order mark, which Windows editors add, as if the mark were absent; it used to refuse the file as invalid. A settings file it cannot parse is now named the way a person finds it ("namzu.config.json in this project", ~/.namzu/config.yaml, or the managed file's path) with the line and column, and the YAML message no longer carries the parser's wording unless you pass --debug. The provider list no longer squeezes a source to one character at 40 columns: below 63 columns the source is drawn under the provider's name. In the model list a long model name is no longer cut short to make room for "(current)" and "(default)"; the marks move to the line under it. No setting or flag changes.

    • —

      899ed00: The terminal app no longer shows a provider's or program's own text where a plain sentence belongs. A model list that could not be fetched says, for example, "OpenAI rejected the saved API key. Press Esc, then k to enter a new key."; a key the provider rejects says so without the provider's message; provider setup shows "Installed · version 0.46.0" or "Couldn’t check Codex. Details: --debug" instead of the program's warning lines; and the folder trust prompt writes the folder with ~ and a middle ellipsis that keeps the last folder whole. A failed model switch or effort check is plain too. The raw text is still available with --debug, after "Details:", with the request ID labelled. Nothing to change on your side.

    • —

      bebe135: A tool result can now say what the call did as typed fields, so a host builds what a person sees from fields and not from the model's text. Nothing you already send or receive changes shape, the text the model reads is unchanged, and no code needs to change to upgrade.

      New and optional: ToolResult.facts, a closed union (ToolFacts) of a command's exit code and its stdout and stderr as head-and-tail windows (ToolTextWindow) or a note that the output was binary, a read's path and size, a search's and a listing's counts, whether a write created, updated or left a file, and the kind of a failure (ToolFailureReason). The built-in bash, read, grep, glob, edit and write tools set it, tool_completed and the ACP tool_call update carry it, a generic ToolCallView may carry facts for what the call is about, and admitToolFacts reads a saved one back. A call refused without running, an unknown tool and a thrown filesystem error also carry a failure fact. execHostShell additionally returns stdoutBytes and stderrBytes. A post-tool hook that replaces the output also drops facts, so a redaction is never undone.

      If you build a view of tool output yourself, read these fields instead of the STDOUT: and STDERR: labels, the "Found 2 match(es)" and "1 replacement(s)" sentences and the notes about withheld credentials or shortened output, which are the model's interface and are not stable text to parse.

      The Desktop app now words every tool row from those fields (Read a.ts, Searched for export, Ran npm test · failed (exit 3), Couldn't read missing.txt: file not found), draws a command's colours, folds long output to its first and last lines with Show more, says "Binary output, 3 KB" instead of printing the bytes, counts a finished edit in the reply's changes card, shows paths in approvals relative to the project, and leaves "0s" off work that took under a second. The CLI host restores a saved conversation's facts (without a command's output) so a reopened row reads the same.

    • —

      ea9e812: The terminal app now draws in a terminal even when CI or similar variables are set in the shell (dev containers, inherited terminal tabs); before, the screen stayed blank until exit. The variables are not changed for the commands the agent runs. Nothing to do on upgrade.

    • —

      f7f48f9: Secure and verify private state and credential files on long Windows paths by passing extended absolute paths to icacls. Keep the existing current-user ACL proof and fail-closed behavior. Project and conversation directory names remain compatible with existing installations.

    • —

      f7f48f9: Fix operator !command execution on native Windows by using its platform shell instead of /bin/sh. Preserve /bin/sh on POSIX, decode split UTF-8 output, refuse pre-aborted launches, and share host process-tree cancellation. Transcript output remains capped at 20,000 characters; a command is also stopped if either stream exceeds 80,000 bytes. Process-start failures now remain visible in the transcript.

    • —

      afd7ba4: Show namzu.cmd resume on Windows when the npm command wrapper belongs to the running installation. Keep explicit Node and entrypoint commands for alternate installations and source launches. Scheduled-run resume commands now use PowerShell 5.1-compatible directory changes and quoting on native Windows, preserving the job's project and additional directories.

      Scheduled-task guidance now distinguishes tool permissions from available tools, local HTML files from artifact services, and cumulative token budgets from context-window size so proposals do not promise unavailable tools or outputs.

    • —

      Updated dependencies [dd7ee4b]

    • —

      Updated dependencies [4314f90]

    • —

      Updated dependencies [b8852a1]

    • —

      Updated dependencies [7f5ac50]

    • —

      Updated dependencies [7f5ac50]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [3727e2b]

    • —

      Updated dependencies [1af4531]

    • —

      Updated dependencies [efb7b06]

    • —

      Updated dependencies [84452ec]

    • —

      Updated dependencies [53aed2a]

    • —

      Updated dependencies [6194bda]

    • —

      Updated dependencies [301f5b3]

    • —

      Updated dependencies [6194bda]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [f7f48f9]

    • —

      Updated dependencies [9ef6506]

    • —

      Updated dependencies [56e846c]

    • —

      Updated dependencies [2057fd6]

    • —

      Updated dependencies [c8613e0]

    • —

      Updated dependencies [6f46da3]

    • —

      Updated dependencies [be9f8b6]

    • —

      Updated dependencies [c1027fb]

    • —

      Updated dependencies [24cb6b4]

    • —

      Updated dependencies [b6ea5ca]

    • —

      Updated dependencies [bb0ad2e]

    • —

      Updated dependencies [d5030ca]

    • —

      Updated dependencies [7f5ac50]

    • —

      Updated dependencies [7473449]

    • —

      Updated dependencies [6194bda]

    • —

      Updated dependencies [6f46da3]

    • —

      Updated dependencies [f4b7d1d]

    • —

      Updated dependencies [6ff961f]

    • —

      Updated dependencies [023224a]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [7f5ac50]

    • —

      Updated dependencies [6f46da3]

    • —

      Updated dependencies [5e2fb3a]

    • —

      Updated dependencies [c82b951]

    • —

      Updated dependencies [0f5aa96]

    • —

      Updated dependencies [a5dbeb2]

    • —

      Updated dependencies [2d5d18d]

    • —

      Updated dependencies [84a9472]

    • —

      Updated dependencies [c1027fb]

    • —

      Updated dependencies [d8d562e]

    • —

      Updated dependencies [53aed2a]

    • —

      Updated dependencies [81d1721]

    • —

      Updated dependencies [ce52b90]

    • —

      Updated dependencies [84a9472]

    • —

      Updated dependencies [f7f48f9]

    • —

      Updated dependencies [815433f]

    • —

      Updated dependencies [4367edc]

    • —

      Updated dependencies [6ff961f]

    • —

      Updated dependencies [6f46da3]

    • —

      Updated dependencies [f7f48f9]

    • —

      Updated dependencies [4367498]

    • —

      Updated dependencies [7f5ac50]

    • —

      Updated dependencies [ad2273f]

    • —

      Updated dependencies [846557b]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [53aed2a]

    • —

      Updated dependencies [12b5af9]

    • —

      Updated dependencies [42ef5e4]

    • —

      Updated dependencies [9d422c6]

    • —

      Updated dependencies [eddf7aa]

    • —

      Updated dependencies [f9f672b]

    • —

      Updated dependencies [fc86c05]

    • —

      Updated dependencies [e447a47]

    • —

      Updated dependencies [093d657]

    • —

      Updated dependencies [57c99b2]

    • —

      Updated dependencies [c590142]

    • —

      Updated dependencies [1fa60b9]

    • —

      Updated dependencies [f0aeb9b]

    • —

      Updated dependencies [5a0a1f7]

    • —

      Updated dependencies [d8782dd]

    • —

      Updated dependencies [3727e2b]

    • —

      Updated dependencies [3727e2b]

    • —

      Updated dependencies [e447a47]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [98e8469]

    • —

      Updated dependencies [a2a4b17]

    • —

      Updated dependencies [bebe135]

    • —

      Updated dependencies [f7f48f9]

    • —

      Updated dependencies [4367edc]

    • —

      Updated dependencies [2395a85]

    • —

      Updated dependencies [01b926c]

    • —

      Updated dependencies [4367edc]

    • —

      Updated dependencies [134932b]

    • —

      Updated dependencies [0de3571]

      • —@namzu/sdk@50.0.0
      • —@namzu/openai@4.1.4
      • —@namzu/google@1.1.2
      • —@namzu/sandbox@25.0.0
      • —@namzu/browser@2.0.1
      • —@namzu/anthropic@6.3.0
      • —@namzu/zen@4.0.0
      • —@namzu/computer-use@3.1.0
      • —@namzu/ollama@2.2.5
      • —@namzu/openrouter@3.0.3
  4. v35.0.0

    Major Changes

    • —

      b7d83ec: An MCP server that finishes connecting or discovering tools after its startup deadline is now closed after that late result. The configured connectTimeoutMs applies to connection and discovery as one budget, rather than allowing each phase the full time separately. Servers whose combined handshake and discovery took longer than the configured deadline but whose individual phases each fit previously connected; raise that server's connectTimeoutMs if it needs more time. Timed-out servers remain named failures and cannot leave an unreported live connection behind. Values above Node's maximum timer delay of 2,147,483,647 ms are now refused by server name; lower any such value to that limit or less.

    • —

      b7d83ec: Plugin user_prompt_submit annotations now reach the model as user-role request context instead of system instructions. The CLI gives operator shell output and idle background-job exits the same request-context role. Hosts that used these observation channels to issue policy must move that policy to an explicit trusted instruction; the observations remain visible to the model on the current request.

      The namzu mcp test HTTP 401 hint now uses a literal <name> placeholder instead of embedding the configured server name in a runnable shell command.

    • —

      b7d83ec: Memory and saved resident state now reach the model as request-only user-role context rather than system guidance. In the SDK, createMemoryRecallStep returns PrepareStepResult.context instead of system; hosts that read or compose the hook result must use context and pass it through the normal preparation chain. Resident continuation, wake evidence, skill catalogues and learned text also move from system dynamic or turn contributions to context; hosts that inspect placements must migrate. context contributions now render once before prepareStep so their estimated cost reduces preparation headroom; hosts relying on a contribution to observe preparation side effects in the same model step must move that logic to a later stage. The CLI moves file-backed USER.md and MEMORY.md, its stored-memory index, and automatic recall out of system prompts on new sends, resident turns and resumes. Interactive resident turns now place admitted data in the new SendOptions.extraContext request-only field; their fixed doctrine remains in extraSystem. Hosts that relied on memory files or resident data as system instructions must pass trusted guidance explicitly. This changes default provider-visible roles and may change model behavior and prompt-cache accounting; request-only context still uses input tokens on each request and the CLI bounds memory against estimated remaining request room.

    • —

      b7d83ec: Plugin MCP server declarations now reject fields they do not implement, including url, headers, inheritEnv and requireApproval. Those fields were previously accepted and silently removed while the declared stdio command still started. Remove unsupported fields from plugin.json or an in-code plugin; configure a remote server through the host's MCP settings and express approval in host source permissions. Plugin MCP startup now has a ten-second deadline across connection and discovery, configurable with connectTimeoutMs up to one hour, and cleans up a failed connection before rolling back the plugin.

      Hosts supplying ConfigRegistry must move plugin reconnect-policy overrides from mcp.<server-name> to mcp.plugin.<plugin-name>.<server-name>; the old key was shared by unrelated plugins and is no longer read. A ConfigScope now has dispose() to release its live namespace and watchers while retaining saved overrides. Plugin disable and failed enable release their scopes, and overlapping enable, disable and uninstall calls for one plugin id now settle in call order.

      Plugin MCP drift is now tracked by full plugin source id rather than bare server name: hosts that relied on a drift event between two unrelated plugins using the same local server label should compare those sources explicitly. The baseline now follows the one admitted listing actually mounted as tools, including reconnect and list-change refreshes; a second startup listing is gone.

    Minor Changes

    • —2b1cbb5: Show running background shells beside the composer and let operators inspect their retained output or stop a shell in /jobs. Use /jobs list for the previous text summary.
    • —b7d83ec: Add namzu mcp test <name> to check the effective MCP server selected by the current profile and project. It connects only that server, reports the usable tool count or a safe failure reason, and closes the connection. The working directory must be trusted; pass --trust to accept it for this test only. After adding a server, run this command before starting an agent to find connection, authentication or tool discovery problems.
    • —2b1cbb5: Show submitted steering and next-turn messages in the TUI with their text and attachments. Alt+Up recalls the latest authored queued message, and Esc preserves pending input when it interrupts a turn.

    Patch Changes

    • —b7d83ec: Malformed mcpServers entries with invalid args, env, headers, inheritEnv, command, URL or working-directory types now fail by server name. One bad entry no longer aborts a session or prevents earlier successful connections from being closed.
    • —b7d83ec: The CLI test runner now uses at most four workers on many-core machines. This prevents healthy session and scheduled-run tests from hitting their wall-clock timeout under heavy test contention; CLI runtime behavior and APIs are unchanged.
    • —2b1cbb5: Hosts can use QueryParams.onJobNoticeDelivered(jobIds) to distinguish background job exits recorded for the model from exits that still need delivery on a later turn. The CLI now keeps a late exit pending for the next turn, retains it after a failed or aborted send, and shows its transcript row once.
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [2b1cbb5]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
      • —@namzu/sdk@49.0.0
      • —@namzu/browser@2.0.0
      • —@namzu/computer-use@3.1.0
      • —@namzu/anthropic@6.2.2
      • —@namzu/ollama@2.2.5
      • —@namzu/openai@4.1.3
      • —@namzu/openrouter@3.0.3
  5. v34.1.1

    Patch Changes

    • —

      57ae7c7: Background jobs whose process cannot start now emit one exit update after the child closes, without reporting the spawn error as a process exit code. An error on a process that already started no longer marks its job as exited while it is still running. SDK API signatures are unchanged.

      Completed and aborted job or delegated-task waits release their polling timers instead of keeping them alive for the rest of a long session. Idle-stream, agent front-door, awaited-job and large-input lexer regression tests now avoid real-time races that could fail on a busy CI runner.

      The CLI's background-child and scheduled-run integration tests now wait for the event they assert, leaving their normal process deadline to Vitest instead of letting a short poll or unrelated watchdog decide the outcome.

    • —

      b53bdb2: On Windows, the PowerShell installer now invokes npm and Namzu through their .cmd shims, so a Restricted execution policy does not block their .ps1 wrappers. A successful npm install that writes a warning to stderr no longer aborts the installer. It requires the CLI's actual minimum, Node 22.13. Provider diagnostics now name the Claude Code credential files checked when no Claude session is available, and /setup explains that Desktop sign-in does not guarantee a reusable Code CLI session. Command Prompt installation instructions use npm.cmd.

    • —

      Updated dependencies [57ae7c7]

      • —@namzu/sdk@48.3.1
  6. v34.1.0

    Minor Changes

    • —f0505b5: Anonymous Zen users can now select all eight bundled free models instead of only Space Bunny. The seven previously blocked models use an experimental OpenCode-compatible request identity on the official Zen endpoint; Space Bunny keeps its verified direct request path, and keyed Zen and Zen Go requests are unchanged. Zen may still reject an experimental model or route, so a free catalogue entry is not a guarantee of service. The CLI labels this access as experimental and keeps Space Bunny as its default.

    Patch Changes

    • —Updated dependencies [f0505b5]
      • —@namzu/zen@3.1.0
  7. v34.0.0

    Major Changes

    • —

      29b142f: Zen Go no longer includes glm-5.1, qwen3.6-plus, or qwen3.7-max in its bundled supported model roster. The service still lists these IDs, but its current documentation provides no route or price for them. Applications relying on these IDs should select a documented model or, if they have independently verified the wire, configure ZenGoProvider with an explicit protocol. The CLI no longer offers these reviewed omissions.

      The default anonymous Zen model changes from muse-spark-1.3-contributor-free to space-bunny-free in both the SDK and CLI. Callers that need the old choice must pass model: 'muse-spark-1.3-contributor-free' explicitly and try credentialed Zen access, subject to gateway admission. The gateway currently rejects direct keyless Namzu requests to Muse and six other documented free models with HTTP 403; only Space Bunny Free completed a live keyless text and tool-continuation test. Anonymous model discovery now admits only that verified model while retaining the other free models for credentialed use.

      Zen and Zen Go now follow the reference client's route overrides for DeepSeek V4 Flash and MiniMax, handle Muse Spark reasoning continuation without replaying gateway-bound ciphertext, and read the current Zen Go price table when refreshing the catalogue. These keyed routes have wire-fixture coverage but were not live-tested with an account. The CLI distinguishes model listing from verified inference and explains the gateway's specific HTTP 403 free-tier refusal.

    Patch Changes

    • —Updated dependencies [29b142f]
      • —@namzu/zen@3.0.0
  8. v33.2.1

    Patch Changes

    • —c926384: Hypermode now appears at the composer’s upper right. Activating it briefly sends a soft lavender glow from the input border’s center toward both sides; the border then rests in graphite. The footer no longer repeats the label.
  9. v33.2.0

    Minor Changes

    • —072bb74: Windows computer use can capture pixels for one named window with screenshot {window_id} and keep background clicks and drags, text and keys bound to that window. Window type_text and key can explicitly request delivery_mode: 'foreground' for browser text or modifier shortcuts that cannot run in the background; this may briefly bring the verified window forward. The pinned driver sets windowScroll: false and refuses scoped pixel scrolling because its wheel input cannot prove which window receives it; a host that supports safe window scrolling can offer it. Capture identity and window geometry are checked before input; moved windows, old captures and restarted driver sessions require a new window screenshot. Hosts can offer window capture without display capture, in which case window_id is required and plain display screenshots remain unavailable. A failed window capture never broadens to a display capture. When both browser and computer-use tools are ready, the CLI routes a named existing browser window to computer use, leaving browser tools for the separate Namzu-managed profile.

    Patch Changes

    • —d3e35af: Continuing a turn paused for a person no longer tells the model that the sign-in or challenge was completed. The model is told the operator chose to retry, and to stop if the tool still needs a person.
    • —Updated dependencies [d3e35af]
    • —Updated dependencies [072bb74]
      • —@namzu/browser@2.0.0
      • —@namzu/sdk@48.3.0
      • —@namzu/computer-use@3.1.0
  10. v33.1.1

    Patch Changes

    • —6cdf009: Show the session's hypermode label once in the composer footer. Turning it on now briefly sweeps colour across the message frame, then returns to a still border; screen-reader and colour-disabled terminals keep the static display.
  11. v33.1.0

    Minor Changes

    • —

      25966a5: Scheduled script proposals now explain the permission set they actually need, including why read-only blocks every script and why a home-directory working folder is refused. The CLI shows a compact, scrollable confirmation for model proposals and /schedule confirmations, with the exact source available for review. It labels pure-script permissions accurately and explains a blanket bash denial without suggesting a script rewrite.

      New notifyOnFinish on the schedule tool and --notify-finished true|false on the CLI let a polling job suppress Namzu's generic success notice while keeping failure notices. The default remains true; existing jobs keep their behavior until changed.

    Patch Changes

    • —25966a5: Accept the first selection key after a choice menu appears. A goal menu could previously ignore Enter immediately after it became visible, leaving the objective editor unopened.
    • —25966a5: Keep the composer ready for the first keystroke after a text prompt closes. The previously hidden composer could briefly lose that keystroke while its input listener was being reattached.
    • —25966a5: Use plain text labels in terminal output where emoji could crowd the next field. Scheduled jobs now appear without a clock prefix, and older saved run titles display with Scheduled: in conversation lists and notices without changing stored sessions.
    • —Updated dependencies [25966a5]
      • —@namzu/sdk@48.2.0
  12. v33.0.0

    Major Changes

    • —3a0daac: Gemini API keys pasted into Namzu are now saved automatically in its private credential store and reused on later launches. This changes the former session-only default. To keep a key out of Namzu's store, provide it through a per-process GEMINI_API_KEY environment variable instead of pasting it into the picker. namzu logout gemini or /logout gemini removes the saved key; an environment variable remains under your control.

    Minor Changes

    • —3a0daac: Archived conversations can now be listed and restored in their original project with /unarchive or namzu archive list|restore. Restore one before using namzu resume <id>; the conversation history stays in its original log and remains scoped to its project. Long-lived CLI sessions refresh their project archive listing when another process archives or restores a conversation.
    • —3a0daac: Add namzu mcp list|get|add|remove for user-owned tool servers. Existing server config continues to work, including names outside the new-entry naming rule. These commands write ~/.namzu/config.yaml, preserve other settings, accept environment-backed Bearer headers, require HTTPS for URL queries or credential headers outside loopback, and hide credential-bearing values in their output. HTTP summaries show only the origin, keeping tokenized paths as well as queries out of text and JSON. User-config writes replace even an existing broadly readable file with one restricted to its owner.
    • —3a0daac: Add namzu mcp login <name> and namzu mcp logout <name> for configured HTTP tool servers. Login uses the official MCP OAuth authorization-code flow, a loopback or pasted callback, PKCE, callback state and authorization-server issuer checks. The CLI stores the resulting Bearer and refresh tokens privately for that exact server URL. Ordinary tool calls reuse or refresh the saved grant without opening a browser; a configured Authorization header continues to take precedence. HTTP authorization refusals stop connection setup instead of falling back to a legacy handshake.
    • —3a0daac: Allow delegated CLI agents to opt into a separate managed Git worktree with workspace: "worktree". The child runs from the selected checkout's committed HEAD, preserving the parent's directory beneath the Git root. The worktree remains available for review after any outcome. SDK callers can choose a per-child shared or isolated workspace and explicitly retain it; isolated requests accept a checked relative subdirectory. A manager can make omitted choices shared with workspaceDefault: 'shared'; existing SDK backend provisioning and cleanup remain the defaults.
    • —3a0daac: Add namzu worktree create|list|fork|resume and /worktree for managed Git checkouts. Each checkout keeps its own conversation history; forking copies a settled conversation into the new checkout's project. Uncommitted source files stay in the source checkout, and Namzu never removes a worktree automatically.
    • —3a0daac: Add namzu skills --audit to check which file skills the model can actually load. It exits with status 1 for an invalid enabled skill, reports the SDK loader's reason, and can estimate manifest overflow with --context-window <tokens>.

    Patch Changes

    • —c8bb87a: When a browser returns an HTTP authentication challenge, the terminal handoff and namzu browser login output ask the operator to check access without claiming that a sign-in or password screen appeared. browser login does not update the profile's "last sign-in" time from that challenge alone. Other browser handoff messages keep their existing wording.
    • —3a0daac: Reflow completed conversation text when the terminal expands, preserving the draft and one copy of history. Show when the model picker has more models above or below its visible rows; wider terminals count hidden models. Pause and error messages name the provider that reported a failure when available. A paused turn now tells operators to use /resume or /abandon, and switching providers keeps dependent prompts held until the turn is successfully resumed.
    • —Updated dependencies [c8bb87a]
    • —Updated dependencies [c8bb87a]
    • —Updated dependencies [c8bb87a]
    • —Updated dependencies [3a0daac]
    • —Updated dependencies [3a0daac]
    • —Updated dependencies [3a0daac]
    • —Updated dependencies [3a0daac]
    • —Updated dependencies [3a0daac]
    • —Updated dependencies [3a0daac]
      • —@namzu/browser@1.0.0
      • —@namzu/sdk@48.1.0
      • —@namzu/computer-use@3.0.0
  13. v32.0.0

    Major Changes

    • —

      567ada8: CLI MCP tool names change from mcp_<server>_<tool> to mcp__<server>__<tool>. Update any permissions, saved prompts or integrations that refer to those names; the CLI warns once for each configured permission rule still using the old form. MCP tools now update after server notifications and reconnects. Configure allow, deny, maxRetries, requireApproval, readOnlyHintTrusted and instructions per server under mcpServers; instructions: true includes the server's text as labelled untrusted request context. /mcp tools shows the server's current tool names and instructions.

      The SDK now skips prompt discovery when a server did not advertise prompt support and keeps approval-wrapped tool definitions stable across unchanged toolset snapshots.

    • —

      567ada8: Plugin tools now carry a source for their owning plugin (plugin:<name>) or MCP server (plugin:<name>/mcp:<server>), so source authorization can distinguish them. Plugin MCP servers use mcpToolset, gaining live tool and prompt discovery, policy-filtered deferred resources and held changed definitions. Plugin MCP prompt tool names change from <plugin>__mcp_prompt_<server>_<name> to <plugin>__mcp__<server>__prompt__<name>; update saved tool names and permission rules that refer to them. MCPToolsetOptions.reconnect also accepts a function that returns the current reconnect policy.

    • —

      fbeac55: A scheduled job can now be a fixed shell script instead of a model prompt, or a cheap "wake-gate" script that decides whether to run the model at all — namzu schedule add --kind script|script+agent, or the model's schedule tool with kind/script. A script job spends zero tokens and opens no session; a script+agent job runs a gate script first and calls the model only when it prints {"wake": true, "context": "..."}. The script body is checked against the scheduled-run floor (the whole text, exactly as a live bash call is read) and then every deny rule — the operator's own and any config file's — per lexed command; allow/ask rules and unmatched are never consulted for a script, since it is fixed, human-confirmed text, not a call a model improvises. A script+agent job's allow/ask/unmatched rules still govern its AGENT phase only, unaffected by the script's own (deny-only) check.

      @namzu/sdk (major)

      • —Breaking type correction: ScheduleJobPreview.model is optional because a pure script job has no model. Hosts that display it must handle its absence for runKind: 'script'; previews for agent and script+agent still carry the pinned model.
      • —ScheduleJobPreview.networkAccess now includes a host script or an allowed host shell that could reach the network, even without web/browser tool grants. Its new optional networkGrantAccess separates those grants from shell capability; a host implementing previewUpdate should set it so a kind-only conversion can be checked against the network-beside-host-shell rule without refusing every script. An older host that omits it is treated conservatively using networkAccess.
      • —An unattended sandbox-escape opt-in no longer waives review of a runtime-chosen program name or another call's outside-root path in the same batch.
      • —Security fix: a recognised wrapper such as env, nice, sudo, busybox or toybox can no longer hide a shell's -c payload from review. Literal payloads are read for permission checks; an expanding payload requires review and is refused in unattended runs. script -t -c now recognises -c as the command option; -T consumes its required timing-file value.
      • —Security fix: find's -exec family now follows wrappers and shell -c payloads inside each clause, including when another wrapper launches find. A dynamically selected program or a find placeholder in a shell payload requires live review and is refused in an unattended scheduled script. The scheduled-run floor also sees service-changing tools behind wrappers in these clauses.
      • —Breaking: ScheduleJobDraft.prompt is now optional (prompt?: string), since a pure script draft has none. Any ScheduleToolHost implementation that reads draft.prompt as a bare string needs a null check (or to keep reading it only when draft.runKind is 'agent'/'script+agent', where it is still guaranteed present).
      • —ScheduleJobDraft gains runKind?: 'agent' | 'script' | 'script+agent' (absent = 'agent', unchanged from before) and script?: { body, shell, timeoutMs? }. ScheduleJobPreview gains the same runKind/script fields so a host's confirmation screen can show the exact text.
      • —The schedule tool's input schema gains kind and script; create requires script unless kind is 'agent' and requires prompt unless kind is 'script', and refuses an agent draft that also carries a script. The existing network-beside-a-host-shell refusal now also covers a script/script+agent job on the host: the script IS shell code, not a rule a live call might reach later.
      • —Security/UX fix: update accepted kind/script in its input schema but silently dropped both from the change it actually applied, reporting success with no error or warning that the model's new script was ignored. ScheduleJobChanges gains runKind/script (additive, so this alone would be minor) and update now forwards both to ScheduleToolHost.previewUpdate, re-verified fresh against the scheduled-run floor and every deny rule exactly like a new job's, and shown in full — with what changed — on the same confirmation screen create uses. A host that does not yet read the new fields off ScheduleJobChanges silently keeps a job's existing kind/script on every update, the same as before this release; only @namzu/cli's own host (below) newly acts on them.
      • —Security fix: update checks the effective preview's network grants after applying every change, so a kind-only conversion cannot inherit web/browser access beside a newly introduced host script. A call that changes the kind and leaves permissions unset is checked too.
      • —New exports from the root entry: execHostShell, ExecHostShellProgress, CommandShell, CommandShellProbe, findCommandShell, hostCommandShell, hostShellSpawn, withoutBashStartup — the shell-resolution and spawn machinery the bash tool already used internally, now available to a host that runs a command line outside a live tool call.
      • —findCommandShellForDialect and installedCommandShellForDialect select an installed bash or sh explicitly, returning no shell if the requested interpreter is absent. execHostShell accepts that CommandShell through its optional shell option, keeping its existing process supervision and output capture for hosts that choose an interpreter themselves.
      • —Security fix, in the scheduled-run floor, so a live bash call benefits too: trap 'ACTION' SIGNAL now reads ACTION as a command line (it runs when the signal fires); find … -exec|-execdir|-ok|-okdir … ;|+ is refused when its clause contains {} and the search root is unknown or could reach NAMZU_HOME, a -name/-path filter could match NAMZU_HOME's own name, or the clause's own program can stop or remove a service; running a file the SAME command line wrote earlier (>, >>, tee, cp/mv, then ./x, sh x, bash x, . x, source x) is refused, since its content was never confirmed.
      • —lexShellCommandLine now reads $(…) and backtick bodies as nested command lines (the same treatment bash -c payloads already got) instead of calling the whole containing line opaque outright: each command inside a substitution is in ShellLexResult.commands, marked origin: 'substitution', checked by the scheduled-run floor and by every deny rule exactly like the rest of the line. Two shapes stay opaque because what actually runs cannot be pinned down from the text alone, measured against real bash (5.2.21 and 5.3.15): a command substitution beside brace expansion in the same word ($(cmd){a,b} runs cmd once per alternative, not once) and an unquoted substitution used as a </> redirection target (a ${var:-…}-style default value can run its substitution twice when the target turns out ambiguous) — both are refused the same way an unmodeled construct always was. ${ list; } (bash 5.3's brace-form substitution) and process substitution (<(…), >(…)) are unchanged, still opaque; out of this scope. ShellWord gains substitutes: boolean (additive). Proven differentially against a real bash on the host (command_not_found_handle argv capture, disabled builtins, empty PATH): every existing corpus line, the exhaustive up-to-three-token sweep, a 6,000-line seeded random sample (manually run to 150,000 with zero mismatches before landing), and new hand-written cases for nesting, mixed quoting, and the two constructs above. decomposeCommandLine's segments/opaque change the same way for these two constructs.
      • —Security fix, follow-up to the above: a second review found that the scheduled-run floor's own pkill/killall/systemctl/launchctl/schtasks/D-Bus detection read a wild (expanding) program-name word as NOT being the tool in question unless its raw, unevaluated text happened to contain the tool's name as one contiguous run — $(echo pk)ill -f node and $(echo system)ctl stop $(echo namzu)-scheduler.service both passed. A wild word in the program-name position (the head, or right after a sudo/env-style one-level prefix) now counts as being every tool this checks for; a wild word elsewhere (an ordinary argument, a path) is unaffected. find ~/.namzu -exec rm {} \;-style commands are unaffected: the fix is scoped to the program-name position, not to "any wild word anywhere."
      • —Security fix, live tool authorization behaviour changes (packages/sdk/src/runtime/query/): the same review found that a deny rule written against a command's real name ("git push*": deny) never matches one produced by a substitution or a variable ($(echo git) push, `echo rm` -rf x, $X push, or a variable in the program's path, "$HOME"/bin/tool), since the name never appears as such anywhere in the call's text — for the scheduled-run floor's script check (verifyScheduledScript, which now refuses such a command outright, naming it, the same posture as an opaque script) and, pre-existing and more broadly, for the live bash tool's own permission rules. An operator now sees more review prompts: a bash call whose lexed command's own program-name word expands is escalated (ToolCallSummary.escalation.unknownProgram, new field) the same unconditional way a sandbox escape already is — never approved by an allow rule, a remembered grant, accept-edits or auto/an unattended turn, asked about in every mode that gets that far (naming why: "the program this runs is decided at runtime: <text>"), and refused outright with no prompt (UNKNOWN_PROGRAM_UNATTENDED_REFUSAL, new export) — there is no unattended opt-in for this the way unattendedSandboxEscape: 'allow' is for a sandbox escape. A deny rule that matches some other way (the tool's own name, an unrelated word) still refuses the call outright, unaffected. Argument-level expansion alone does not trigger this — only the program-name word — so an ordinary echo $(date) is unaffected. New audit actions unknown_program (approved/refused). See docs/sdk/escalations.md.
      • —Security fix, follow-up to both of the above — one extra word defeated every one of them: a third review found that all three consumers (the escalation, the scheduled-run floor, verifyScheduledScript) only ever looked at a command's literal head word, so env $(echo git) push, command $(echo git) push, exec $(echo git) push, and a re-exec wrapper with a mandatory argument of its own before the program (timeout 5 …, stdbuf -oL …, chrt 0 …, env VAR=value …) all ran, or passed the script check, unverified — the floor's one-hop REEXEC_PREFIX guess put the program at the wrong word entirely for these, and exec/command/taskset/time/builtin/xargs were not in it at all. eval/source/. were not read as unverifiable either, though command-line.ts already treats them as opaque for deny/allow rules. New SDK module, packages/sdk/src/authorization/program.ts, exported from @namzu/sdk: programPositions(command, dialect) is the one place "where does this command actually exec a program" now lives — it unwraps a chain of re-exec wrappers (sudo, doas, pkexec, env, nice, ionice, nohup, setsid, timeout, stdbuf, chrt, taskset, time, command, builtin, exec, xargs) with each one's own real option grammar, failing closed on an option it does not recognise, and reads each find -exec-family clause as its own position. source path/. path and eval word… are read the same way a nested bash -c '<literal>' payload already is: a LITERAL path is known, exactly as bash path is (nothing inspects either one's contents), and a LITERAL eval payload is joined and lexed as a command line of its own, with every position inside it folded in (so eval 'env $(echo git) push' is still unknown, transitively); an expanding path/argument word, or a payload that does not read cleanly, is unknown. hasPoisoningPrefix/poisonsLaterCommands/poisonedProgramPosition/resolveScriptPrograms/DYNAMIC_RESOLUTION_VARIABLES cover the other half: a command-prefix or export/bare assignment to PATH, LD_PRELOAD, LD_LIBRARY_PATH, BASH_ENV, ENV or IFS makes a LATER literal program name unverifiable too. Behaviour change: unknownProgram's text is now the command plus why its program's position could not be resolved, not a bare word — a host that pattern-matches the exact string should match on substring/prefix, not equality. ToolCallEscalation.unknownProgram's doc comment, escalationReason's wording, and the floor's own heads/REEXEC_PREFIX mechanism (removed, replaced by programPositions) all changed accordingly. packages/sdk/src/authorization/__tests__/program.test.ts (new). See docs/sdk/escalations.md, docs/sdk/command-lines.md, docs/cli/scheduled-tasks.md.
      • —Security fix, two CRITICAL bugs from a final review of program.ts, plus wrapper coverage: (1) sudo's own case never skipped a NAME=value pair the way env's already did, so sudo VAR=value $(echo ls) reported the ASSIGNMENT WORD as the program and never examined the real, wild one after it; both sudo's and env's assignment-skipping now also feed a poisoning name (PATH=, LD_PRELOAD=, …) into the poisoning check instead of silently passing it through. (2) executor.ts's unknownProgramOf ignored the reading's own opaque/!complete flag, so bash -c "$X" — whose payload the lexer cannot follow at all — was never escalated; a new exported function, unknownProgramInLine(value, dialect), checks that first, and executor.ts now delegates to it rather than re-implementing the walk. New wrapper coverage: busybox/toybox (transparent applet dispatch); declare -x/typeset -x (any option cluster with it, never +x) now poison like export, and export -n correctly does not; su -c/runuser -c/script -c/flock -c read a literal payload recursively like a nested bash -c '<literal>' (su/runuser with no -c are unknown: an interactive login shell); flock's positional form; unshare, nsenter, chroot (unknown with no command), setpriv, prlimit, numactl; watch (default payload joined and lexed like eval's, -x execs the argv directly); sudo -i/-s/-e/--login/--shell/--edit and bare sudoedit are explicit unknowns. ssh host cmd is deliberately not modelled (the command is remote); docs/sdk/escalations.md now documents this residual boundary — the full modelled-wrapper list, and that an unmodelled one is treated as the program itself, with deny rules, the floor and ordinary review still applying to it. packages/sdk/src/authorization/program.ts, runtime/query/executor.ts, packages/sdk/src/authorization/__tests__/program.test.ts.

      @namzu/cli (major)

      • —A pure script job now creates and runs without a configured provider or --model. New script jobs omit the unused model field; older confirmed script jobs that already store a real model still read with their confirmation digest intact and keep it on a script edit. Explicit --model, --effort, prompt, agent budget, provider wait, approval TTL, session retention and browser grant inputs are refused for a pure script instead of silently ignored; older stored fields remain readable but inert. agent and script+agent still require a model; converting a model-free script to either kind requires one. Text previews, the SDK ScheduleJobPreview.budget, and schedule show report zero model tokens and the script timeout for pure scripts. The schedule tool's success message reports pure script history without claiming a session.
      • —Breaking, on disk: the job/run-result/history file format moves to v: 2 for a job that actually uses runKind/script, a check-failed status, or gateResult/scriptOutput. An older namzu that reads such a file refuses it loudly ("was written by a newer namzu...") rather than misreading a script job as a malformed agent job. A plain agent job stays v: 1, byte-for-byte as before, and every existing job keeps reading and confirming exactly as it did — downgrade only affects a job you have actually converted to script/script+agent.
      • —Fix: a job file an older namzu (or a hand edit) left in a format the current one refuses to fully parse was previously invisible to createJob's name-uniqueness check and to findJob (by name or id prefix) — a name collision with it went undetected, and looking it up said "No scheduled job is named…" instead of the real reason. Both now read a refused file's id/name leniently, off the raw JSON, without validating anything else about it: a name collision with such a file is now refused, naming the file and the reason; looking it up by its own name or a matching id prefix now reports the real reason (e.g. "written by a newer namzu…"); a name or id prefix matching more than one job — readable, unreadable, or a mix — is an error listing every match and how to address each. schedule list says N job files could not be read instead of No scheduled jobs when every job file is such a file.
      • —New schedule add/edit flags: --kind agent|script|script+agent, --script/--script-file, --shell bash|sh, --script-timeout. A script job needs --shell and a non-empty script and has no --prompt; --unmatched park is refused for a pure script job (nothing can wait for the operator mid-script); --execution sandbox is refused for script/script+agent in this release; script/script+agent job creation is refused outright on native (non-WSL) Windows, where the only available shell reading is a loose cmd.exe approximation the floor cannot fully verify.
      • —Fix: --shell sh previously only labeled a script for verification while fire always used the live bash tool's automatically chosen shell. On a bash host, a newly confirmed sh job was guaranteed to stop at first fire. A script now executes under its chosen installed bash or sh; creation and every confirmation refuse an absent interpreter, and fire blocks if it has since disappeared. Native Windows remains refused.
      • —ScheduleRunStatus gains check-failed (a script or wake-gate malfunctioned: non-zero exit, its own timeout, or stdout that is not the wake-gate's JSON contract) — CLI-internal, not part of the SDK's public surface. It joins the failure bucket for the failure streak and automatic pause, and gets its own notification and noticeText case.
      • —schedule show/history --json gain gateResult ({ wake, contextChars }) and scriptOutput ({ stdout, stderr }, capped with an explicit truncation marker) on a fired script/script+agent run; show's text view gains a "Script"/"Wake-gate script" section beside "Prompt".
      • —schedule list (text and --json) now shows each job's kind: [script, 0 tokens] and [script+agent] in the text view, kind (absent for agent) in --json. The TUI's /schedule list marks the same two kinds. Found missing in a UX review — nothing distinguished a zero-token script job from an agent job in either listing.
      • —Security fix: a script/script+agent run's own output — a wake-gate parse failure's quoted stdout, a script-check refusal's quoted command, a check-failed notification's reason, scriptSummaryOf's extracted summary line, and schedule show/history's text-view reason/summary — is untrusted runtime text, not anything an operator confirmed, and is now run through the same sanitizeLine() a model's own answer already is before it reaches a terminal or a desktop notification, so it cannot plant control characters, terminal escape sequences or invisible/bidi characters on the operator's screen. --json output is unaffected and stays raw.
      • —Security fix: CLI and TUI schedule add/edit/confirm/list/show text views now show terminal controls and bidi characters in script text, prompts, changed lines and folder paths as visible code point escapes, so a script cannot erase or reorder its own confirmation. The model's schedule-tool confirmation uses the same projection. Persisted jobs and JSON output preserve the original source text.
      • —Security/UX fix: the TUI's schedule tool host now acts on an update's runKind/script: previously it always carried a job's existing kind and script forward unchanged, silently, whatever the model asked to change. Moving to kind: 'agent' drops the script (an agent job cannot carry one); a change that touches kind or script shows up in "Changed since it was last confirmed" the same as every other field, alongside the new script's full text in its own section (packages/cli/src/tui/schedule/tool-host.ts updateRequest, packages/cli/src/schedule/changes.ts confirmationView).
      • —Security fix: namzu schedule __fire now refuses a script/script+agent run outright on native (non-WSL) Windows, the same way job creation already does. Job creation alone was not enough: a job confirmed on WSL or Linux, whose daemon later finds itself running on native Windows (a moved NAMZU_HOME, a machine re-imaged from WSL to a native install), previously ran anyway whenever the job's confirmed shell dialect happened to still read as sh — the label native Windows's cmd.exe approximation and POSIX sh both report, so the existing dialect-mismatch check alone could not tell them apart.
      • —A script/script+agent script (and the scheduled-run floor generally, so a live bash call benefits too) may now use $(…)/backtick command substitution: it is checked the same as the rest of the script instead of refusing the whole thing outright (see the SDK's lexShellCommandLine entry above). The schedule-task skill's guidance on this is updated to match; the two narrow shapes still refused (a substitution beside brace expansion, or unquoted as a </> redirection target) are named there with why.

      Upgrade step: nothing changes for an existing agent job. Adopting a script/script+agent job on a NAMZU_HOME you may downgrade the CLI on later means that job's files become unreadable by the older version until you upgrade again; keep that in mind before converting a job you might need to roll back.

    • —

      567ada8: @namzu/cli: the session's tool roster is now composed from named Toolsets (@namzu/sdk's toolsets, plan.md v3 §8) instead of a ToolRegistry. This is internal composition, not a config or CLI-flag change — nothing in namzu.config.json or the command line changes — but it is a breaking change for anything that imported @namzu/cli's internals directly:

      Removed / changedReplace with
      createCliPluginRuntime(config, tools: ToolRegistry, cwd, hooks?, skillTool?)createCliPluginRuntime(config, cwd, hooks?) — it no longer takes or mutates a tool registry. Read runtime.skills.size and fold runtime.manager.toolsets into your own toolsets list; the skill tool is your own toolset now, not something this runtime registers.
      SubagentRuntimeOptions.buildTools: () => ToolRegistryContractbuildTools: () => readonly Toolset[].
      SubagentRuntimeOptions.configureWebSearch?: (provider, model, tools: ToolRegistryContract) => WebSearchConfig | undefinedconfigureWebSearch?: (provider, model, toolsets: readonly Toolset[]) => WebSearchConfig | undefined — a pure decision now; it no longer removes web_search itself, so a caller building the child's final toolsets filters it out when this returns truthy (filtered(ts, (t) => t.name !== 'web_search')).
      McpConnection.tools: readonly ToolDefinition[] (still present)McpConnection.toolsets: readonly Toolset[], two per connected server (main tools and deferred resources, both kind mcp_server, id mcp:<server>) — prefer these; .tools is now just the flattened union across all of them.

      Additive: AgentSession gained a presenter: ToolPresenter field — the session's real tool-call/result presenter, for a host that renders events outside send()'s own stream. The ACP bridge (namzu acp) now uses it instead of a presenter built over a permanently empty registry, so tool-specific presentCall/presentResult views (a richer diff, a custom label) render over ACP for the first time; every ACP tool view had silently fallen back to the generic label before.

      Behaviour: because tool availability is now derived from the turn's own revealed-tool history (@namzu/sdk's ToolManager) rather than a per-send registry fork, a tool search_tools reveals in one send now stays active in a later send of the same session, and after namzu resume — it no longer resets every send.

      @namzu/sdk: two combineToolsets usage bugs, found while wiring the CLI onto toolsets, are fixed — query()'s own generated tools and SupervisorAgent's coordinator tools are now two separate array entries (an eager one and a deferred one) rather than one combineToolsets'd together, so runtimeToolOverrides: { name: 'deferred' } (task tools, coordinator tools) is honoured again instead of silently falling back to 'active' for both halves. query() also no longer adds its own search_tools when a caller's toolsets already contribute a tool under that exact name (a connector genuinely named search_tools), matching the pre-toolsets registry's behaviour and avoiding a spurious ToolsetConflictError. Pure bug fixes, no API change.

    Minor Changes

    • —

      567ada8: mcpServers entries in namzu.config.json may now reference the operator's own environment from inside an env or headers value with a bare ${VAR_NAME} (identifier characters only — no ${VAR:-default} fallback). This is narrower than the interpolation some other MCP clients' configs use: only env and headers values are expanded (never command, args, url or cwd), and a reference to a variable that is not set fails that one server with a named reason instead of substituting an empty string. inheritEnv remains the primary way to grant a named variable to a stdio child under its own name; ${VAR_NAME} is for the value itself, and is the only secret-safe option headers has ever had.

      Purely additive: a config with no ${...} in its env/headers values behaves exactly as before. A config that happened to contain a literal ${SOMENAME} string matching a variable name that is set in the operator's environment now has that string substituted rather than passed through literally.

    • —

      567ada8: Plugin manifests can declare instructions. An enabled plugin contributes the text as labelled, untrusted request context; disabling it revokes the contribution, including for an already-running turn. SDK hosts can also call definePlugin({ name, tools, hooks, instructions, mcpServers }) and PluginLifecycleManager.installDefined(plugin) to install a host-authored plugin without a manifest file or dynamic module import.

    Patch Changes

    • —

      567ada8: Fixed: namzu acp's tool-call/result presenter (added in this release to fix the previously-always-empty ACP presenter) delegated through one connection-global "active session" slot. Two ACP sessions with prompts in flight at the same time could have session A's tool-call/result rendered with session B's tool-specific view once B's prompt started — worse than the pre-fix behaviour (always generic, but never wrong) for exactly the concurrent case acp-session-isolation.test.ts otherwise covers.

      The presenter now tracks which session's event it is presenting for only the synchronous span of that one event — bracketed around each record.route(event) call rather than held for a whole turn — so a second session's prompt starting, or finishing, while the first is still streaming can no longer be read as "active" while the first session's own event is being presented.

    • —

      567ada8: The SDK now accepts application-defined agent type strings and provides QueryAgent as the generic managed query adapter. defineAgent gives its run callback a required fourth AbortSignal parameter and creates fresh instances for delegated turns. Code that invokes a DefineAgentOptions.run callback directly must supply that fourth argument; existing three-argument callback implementations remain assignable. ReactiveAgent and the supervisor, pipeline and router exports remain available as deprecated compatibility names. CLI delegated turns use a CLI-owned NamzuCliAgent while keeping their existing configuration behavior.

    • —

      567ada8: Authorization rules can now match the owning toolset's source id with { type: 'by_source', sources: ['mcp:github'], decision: 'review' }. This lets a host ask, allow or deny every tool from one or more MCP servers without listing names individually. A rule with no matching host-supplied source leaves the call to later rules.

      The CLI accepts [permissions.sources] with source ids or globs mapped to allow, ask or deny, and its permissions display shows those rules and their decisions.

    • —

      567ada8: Releases live toolset change listeners when a CLI session or turn ends, including a send with extra per-turn tools. Closing the session keeps its plugin and MCP connection cleanup while dropping the host-side manager subscriptions.

    • —

      567ada8: CommandNameCollisionError (tui/slashCommands.ts, thrown when a kernel command and a CLI-local one claim the same name) now extends @namzu/sdk's RegistryCollisionError, matching every SDK *CollisionError after its registry-collision convergence. Name, message and behavior are unchanged; a catch block naming CommandNameCollisionError still works, and one that wants "some registry collided" without naming every class can now catch RegistryCollisionError instead. See docs/sdk/registries.md.

    • —

      567ada8: MCP tool definitions that change after initial admission keep serving their earlier definition across turns; new and removed names still update. /mcp and exec --json now report server discovery changes and current policy refusals, including changed definitions held for review.

    • —

      567ada8: Former MCP permission-name warnings now use the CLI's structured logger instead of writing plain text to stderr. Consumers of exec --json stderr can continue parsing log records when a legacy permission rule is configured. Plugin MCP disconnect warnings use a fixed message and put the operation in a log attribute.

    • —

      567ada8: Shipped prose no longer names other named products. matchesToolSelector's doc comment (packages/sdk/src/tools/roster.ts) and Tool metadata and selectors describe its synchronous-only design against "some other agent frameworks' equivalent selector" instead of one by name; Tool servers describes the wider ${VAR}/${VAR:-default} interpolation some MCP clients accept as "several desktop and editor MCP clients" instead of naming two. Wording only: no type, default or behaviour changed.

    • —

      567ada8: Invalid JavaScript plugin tools now fail when a plugin is enabled, with the plugin and module named, if they omit a usable inputSchema. Before, a deferred plugin tool could load successfully and then crash the turn with a Zod _def error after search_tools revealed it. Supply a Zod inputSchema, or a compatible parser and explicit modelInputSchema.

    • —

      567ada8: Fixed: under toolLoading: 'deferred', search_tools was refused by review in every mode but auto — plan and strict refused it outright, and the interactive default (prompt) put up an approval dialog for it on every discovery call, where before the toolsets migration it ran silently. The session's own review-exemption manager is built from sessionToolsets alone and never learns about search_tools (mounted internally, per turn, by query()), so reviewExemptionFor's lookup resolved it as an unknown tool and treated it as never exempt.

      reviewExemptionFor now falls back to the SDK's own search_tools definition for a name the session's manager does not carry, read exactly the way the kernel reads a known tool (isTrustedReadOnly), so the exemption holds regardless of whether this turn's query() actually mounts it. This does not add search_tools to /tools or /permissions (promptExemptToolNames), which still read the session's roster bare and stay fixed at boot, as tools-and-permissions-agree.test.ts requires.

      This is also what makes the earlier claim in this same release — that a tool search_tools reveals in one send stays active in a later send — true for permissionMode: 'plan' too; it previously held only for auto, since a blocked search_tools call never revealed anything to persist.

    • —

      567ada8: Restores file-defined sub-agent shadowing: a project agent file named explore or general-purpose again replaces the matching built-in, using the new AgentRegistry.replace instead of register now that registries throw on a duplicate id by default (@namzu/sdk's registry-collision convergence). No change from the last published @namzu/cli behavior — this restores parity with a regression introduced earlier in this same line of work, before it ever shipped.

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [fbeac55]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

    • —

      Updated dependencies [567ada8]

      • —@namzu/sdk@48.0.0
      • —@namzu/browser@0.1.1
      • —@namzu/computer-use@2.0.2
      • —@namzu/anthropic@6.2.2
      • —@namzu/deepseek@2.0.2
      • —@namzu/openai@4.1.3
      • —@namzu/openrouter@3.0.3
      • —@namzu/zen@2.5.2
      • —@namzu/ollama@2.2.5
  14. v30.0.0

    Major Changes

    • —

      82769f1: The interactive CLI now asks once per session before the model first sees your screen, and strict mode no longer lets a screenshot run on its own — that second part is why this is a major release for @namzu/cli. In prompt, accept-edits and plan mode the first computer_use call that would send the screen to the provider — a screenshot, a zoom, the list of open windows, a window's controls, or an action that returns a screenshot — opens a "Share your screen" box naming the provider; after a yes, later screenshots in that session run without asking, and clicks, typing and ui_act are still reviewed as before. A mode switch keeps the answer; a new session (/new, /clear, /model) asks again. auto never asks. strict now refuses a screenshot unless a rule allows it: to keep screenshots running in strict, add permissions: { computer_use: 'allow' } (which also allows clicks) or a narrower rule for the actions you want.

      Approving a ui_act call shows the control by name (Press Button "Beş" (e30)), and ui_snapshot as the window it reads.

      @namzu/sdk

      • —ToolDefinition.capturesScreen?(input) and defineTool({ capturesScreen }) declare which calls send the operator's screen to the model provider. computer_use declares its observations and every action followed by a screenshot.
      • —Breaking: the authorization gate's allow_read_only rule (allowReadOnlyTools: true, in every shipped preset) no longer allows a call that declares capturesScreen; it goes to the review policy instead. With createReviewHandler and no screenConsent nothing changes (a screenshot is approved as a read); a custom ResumeHandler now sees computer_use screenshots, zooms, window lists and UI snapshots. To keep them out of review, add an explicit rule such as { type: 'allow_by_name', toolNames: ['computer_use'] } (which allows clicks too).
      • —createReviewHandler / createReviewPolicy take screenConsent: { sessions: Set<string> } and an optional capturesScreen(name, input) predicate (default: the tool's declaration from registry). With them, the first such batch in a session is put to prompt with the new ToolReviewRequest.screenConsent: true; strict refuses it unless a rule allowed it, auto does not ask, and without a prompt it is refused with the new SCREEN_CONSENT_UNATTENDED_REFUSAL. A no is SCREEN_CONSENT_DECLINED_FEEDBACK. New type ScreenConsentRecord. Without screenConsent nothing changes.

    Minor Changes

    • —

      172fd9e: New open_url tool: in the interactive terminal and namzu exec, the model can open an http(s) page in your default browser (under WSL, the Windows browser) instead of guessing a shell command. It is a network tool, so prompt mode asks before it runs. Add a [permissions] rule for open_url to allow or deny it. exec --json, drain, ACP, scheduled runs and sub-agents do not have it. Each request in those two surfaces now carries about 120 more tokens of tool schema.

    • —

      28102e1: The model can change a scheduled job instead of deleting and recreating it. The schedule tool has a new action, update: job names the job, and only the fields the model sets change (prompt, when, folder, tz, budget, or permissions as the whole new set, under the same limits as create). Asked whether it could change a job, a model with no such action deleted the job and created it again, and the job's history was lost.

      In the TUI the operator confirms every change on one screen: what changes first (the -/+ lines namzu schedule edit shows), a THE PERMISSIONS CHANGE warning when a run's rules, unmatched, execution or browser grant change, then the job as it will run. Cancel is the default. Save writes the same job — its id, creation time and history kept — with the operator's new confirmation, keeps a paused job paused, and records edited by tool with the changes in its history. A change is refused if the job was changed while the operator was being asked. Like create, update is not preceded by the ordinary permission review in prompt, accept-edits and auto.

      SDK: ScheduleToolHost gains three optional methods, previewUpdate(job, changes), confirmUpdate(request, signal?) and update(preview), and new types ScheduleJobChanges, ScheduleJobUpdateProposal and ScheduleUpdateRequest. A host without them keeps working: the tool refuses update there and tells the model not to delete and recreate the job.

    • —

      28102e1: A scheduled run that completed with refused tool calls now says so. A job whose only command was refused on every run was recorded completed each time, with a finished notification and nothing else to go on.

      A run now records refusedCalls (calls that never ran: a permission rule, the scheduled-run floor, unmatched: deny, a tool its permissions withhold) and failedCalls (calls that ran and returned an error), each { count, first: { tool, reason } }, in its result file and history record, and as counts in the job state's lastRun. namzu schedule list shows last completed (1 call refused), show and history print the first reason under the run, run-now prints it, and so do /schedule and the model's schedule tool list. The finished notification reads done at …, but 1 call was refused (bash); namzu schedule show <job> says why; the reason itself appears in the notification only for a job created with --notify-summary, because it can quote the command the model wrote.

      The run's status is unchanged: completed still means the turn ended normally, so failure counting, automatic pausing and which notification is sent behave as before. The new fields are additions to the --json output of list, show and history.

    Patch Changes

    • —

      82769f1: Computer use no longer runs blind on a provider that cannot show the model an image in a tool result (OpenAI via API key, Bedrock, OpenRouter, LM Studio, Ollama, the generic HTTP driver). The computer_use tool is still listed, says why it cannot be used, and refuses every call; the desktop is never touched, and the session notices say Computer use is unavailable in this session: …. Use Anthropic, Codex or Google for computer use.

      Approving a computer_use call now shows each desktop action on its own line, in the order it will run, with any text to be typed shown in full.

    • —

      28102e1: A scheduled run can no longer reach the scheduler through a shell the command-line reader does not follow. powershell -c 'namzu schedule stop', pwsh -command '…', fish -c '…', tcsh -c '…' and bash.exe -c '…' were allowed whatever their text held: the scheduled-run floor took any shell at the head of a command with a -c option as read, but only the payloads of sh, bash, dash, zsh, ksh, ash and mksh (and busybox running one) are. Such text now goes through the floor's tripwire like any other text a program runs as code. A job whose commands use those shells without naming the scheduler, NAMZU_HOME or the browser profiles runs as before.

      SDK: new exports nestedShellCommand(words), NESTED_SHELLS and the type NestedShellCommand. nestedShellCommand says, for one simple command's words (without its leading assignments), which -c payload lexShellCommandLine reads as a command line of its own, why it made the line opaque instead, or null when it reads none. The lexer uses it itself, so its reading is unchanged.

    • —

      28102e1: A scheduled run may now run a command whose code merely mentions namzu. powershell.exe -NoProfile -Command "[System.Windows.MessageBox]::Show('Namzu: scheduled job running','Namzu')" was refused by the scheduled-run floor, because text passed to PowerShell, cmd, Python, Node or a shell reading its input was refused whenever it contained namzu or schedul. Such text is now refused only when it holds something that can reach the scheduler or NAMZU_HOME: a schedule subcommand other than list, show, status, history or logs with the CLI or an expansion in reach, the service's name (namzu-scheduler…, com.namzu.…), a service tool with a namzu name or a *, pkill/killall, NAMZU_HOME by name, .namzu as a path segment, a path into either protected folder, or LOCALAPPDATA with a namzu segment. A job that was refused for naming the product runs; nothing that reached the scheduler before is let through (checked against bash on 35 159 generated lines, and on 1 189 labelled lines of PowerShell, cmd, Python, Node and sh code).

      The refusal now says which rule matched and where (the argument `~/.namzu/x` names NAMZU_HOME (/home/you/.namzu), … it holds `schedule stop` (a `namzu schedule` subcommand other than …), in the argument `namzu schedule stop` ) instead of listing everything the floor protects.

      SDK: a predicate authorization rule may carry describe(call), asked after decide returned a decision; its answer is the gate's reason for that call instead of the rule's fixed description (a null, empty or thrown answer keeps description). describeRule(rule, call?) takes the call as an optional second argument. Both are additions; existing rules and callers are unchanged.

    • —

      28102e1: In the TUI, the model's schedule tool now lists every scheduled job, not only those of the session's folder. A model that created a job in a folder below the session's and then called list was told "No scheduled jobs." while namzu schedule list showed the job, because the TUI's host filtered by folder unless the model passed allFolders: true, which models do not. Jobs of the session's own folder are marked (this folder) and are still the only ones whose prompt the model sees.

      SDK: ScheduleJobSummary gains an optional inSessionFolder, which the schedule tool prints as (this folder) on the job's list line. A host may use it to list every job regardless of allFolders. The tool still asks host.list({ allFolders: false }) unless the model sets it, so a host that filters by folder behaves as before.

    • —

      28102e1: A skill the model loads now names the directory its files are in (#536), so a skill whose instructions say scripts/… or references/… can be followed. On the host it is the directory the skill was read from. With sandbox.enabled, a skill under the working directory or an added directory gets the same path, which the sandbox mounts; every other skill (the user and shared-user tiers, the built-ins, .agents/skills above the working directory, user plugins, and every skill under an ephemeral workspace) is reported as not reachable instead of being given a host path the sandbox refuses. Nothing to do on upgrade.

    • —

      28102e1: namzu schedule status on Windows and WSL now says what the Task Scheduler's last result means, keeping the number: last result: running (267009, 0x41301) instead of last result 267009. The result that appears every five minutes while the scheduler runs, -2147020576 (0x800710E0), reads an instance was already running, so a new one was not started; expected, since the task checks every five minutes, so it is not mistaken for a failure. Task Scheduler's own codes, the common Windows errors and the scheduler's own exit codes are named; any other result shows its number and its hexadecimal form.

    • —

      172fd9e: Under WSL, the Claude, Codex, Gemini and OpenCode sessions you are signed in to on Windows are now found even when /etc/wsl.conf moves the Windows drives ([automount] root). The paired Windows home used to be looked up only under /mnt/c. A scheduled run's PATH now also drops the Windows drive entries under that root, instead of only those under /mnt/. On the default /mnt/ nothing changes.

    • —

      172fd9e: Under WSL, several things now look for Windows programs under the drive mount root set in /etc/wsl.conf ([automount] root) rather than always under /mnt/c: the scheduler service's programs (schtasks.exe, cmd.exe, powershell.exe, …) and the directory they start in, Windows notifications, and the paths the model is told about. On a distro that moved the root, namzu schedule install used to refuse and notifications never appeared. On the default /mnt/ nothing changes. A service installed while the root was elsewhere keeps the program paths recorded in its manifest. If you have since moved the root, reinstall it with namzu schedule install.

    • —

      172fd9e: Under WSL, namzu login now opens the sign-in page in the Windows browser. It launches Windows PowerShell by absolute path, under the drive mount root set in /etc/wsl.conf ([automount] root, /mnt/ by default), and the address is passed as data, never as script text. It used to call xdg-open, which under WSL usually opened nothing, so you had to copy the URL. When interop is off or PowerShell is missing, it still falls back to xdg-open. Nothing to change on your side.

    • —

      Updated dependencies [28102e1]

    • —

      Updated dependencies [28102e1]

    • —

      Updated dependencies [82769f1]

    • —

      Updated dependencies [82769f1]

    • —

      Updated dependencies [82769f1]

    • —

      Updated dependencies [82769f1]

    • —

      Updated dependencies [82769f1]

    • —

      Updated dependencies [82769f1]

    • —

      Updated dependencies [28102e1]

    • —

      Updated dependencies [28102e1]

    • —

      Updated dependencies [28102e1]

    • —

      Updated dependencies [28102e1]

    • —

      Updated dependencies [28102e1]

    • —

      Updated dependencies [28102e1]

    • —

      Updated dependencies [28102e1]

    • —

      Updated dependencies [82769f1]

    • —

      Updated dependencies [28102e1]

      • —@namzu/anthropic@6.2.0
      • —@namzu/sdk@46.0.0
      • —@namzu/openai@4.1.1
      • —@namzu/computer-use@2.0.0
      • —@namzu/browser@0.1.0
      • —@namzu/ollama@2.2.4
      • —@namzu/openrouter@3.0.1
  15. v29.1.0

    Minor Changes

    • —

      2b96136: The interactive terminal can drive a web browser. @namzu/browser is now a dependency, and the TUI mounts the browser and browser_act tools on a namzu-owned profile; in WSL they drive the Windows Chrome. Nothing launches until the model's first browser call. namzu exec, exec --json, drain, acp and the resident step do not get the tools.

      What changes for you:

      • —Browser calls are reviewed by default. With no config, opening any site and every action on a page is reviewed ("*": ask); looking at the page the browser holds is not. Set browser.sites to allow sites (read, act) or refuse them (deny), or browser.enabled: false to turn the tools off.
      • —New config key browser (enabled, defaultProfile, engine, headless, sites, keepOpen). An unreadable site key or level stops namzu from starting and names the key. The key is merged across files per site; a deny in any file holds. A project file that sets browser.defaultProfile is refused, and namzu will not start in that folder until the key is removed.
      • —Site rules come before your [permissions] table for the browser tools. A table deny for browser or browser_act still wins; a table allow or ask for them applies only to back, forward and reload.
      • —New commands: namzu browser login <profile> [url], list, status, install, remove; the /browser slash command (status, profile <name>).
      • —namzu doctor reports browser.installed and browser.engine. The boot capability line gains browser yes|no.
      • —The review screen names the site rule, profile and engine for a browser call. A turn paused because a page needs you (sign-in, CAPTCHA) says where to do it and how to continue.
      • —New exports: browserInstalledCheck, browserEngineCheck; NAMZU_OPTIONAL_CAPABILITIES includes @namzu/browser.

      See docs/cli/browser.md.

    • —

      2b96136: The CLI ships three built-in skills and a way to make your own from the TUI.

      • —skill-creator (model and operator), browser-automation (model only, offered only when the browser tool is present) and schedule-task (offered where the schedule tool is: the TUI). Every session now carries the skill tool and lists skill-creator in its skills manifest. To go back to no built-ins, set skills.builtin: false; to drop one, name it in skills.disabled; a skill of the same name in ~/.namzu/skills, ~/.agents/skills or the project replaces it.
      • —/skills new [what it should do] starts an interview with the model, which drafts a SKILL.md and proposes it through the new save_skill tool. Nothing is written until you choose Save to user (~/.namzu/skills), Save to project (./.namzu/skills) or Cancel on a screen that shows the whole file with invisible characters revealed and names any skill it replaces. The screen asks in every permission mode, auto included; plan and strict refuse the tool. save_skill exists only in the interactive TUI, never in exec, drain, a scheduled run or a sub-agent. /skills new is now a subcommand, so a skill named new is activated from the /skills picker rather than by /skills new.
      • —The skill tiers are read again at the start of every turn, so a skill added while a session runs is offered from the next turn.
    • —

      2b96136: Scheduled jobs can drive the browser, on the sites you list and no others.

      • —New flags on namzu schedule add and edit: --browser <profile>, --browser-site <site>=read|ask|act (repeatable) and --browser-headed; on edit, <site>=none takes a site off and --no-browser removes the grant. Sign in first with namzu browser login <profile> <url>. * is refused: every site not listed is denied. ask needs --unmatched park. A browser grant alone (no --permissions) needs --unmatched.
      • —New job field permissions.browser (profile, sites, headed), covered by the confirmation. The model's schedule tool in the TUI can now propose it; you confirm it on screen.
      • —A job's [permissions] rules may no longer name browser or browser_act; creating or editing such a job is refused with a pointer to the grant. Without a grant both tools are denied in a scheduled run, as before.
      • —A run with a grant checks its browser before the model: the profile exists and has its data, the browser it was signed in with can start (WSL interop for the Windows browser), a display for --browser-headed. Otherwise it is blocked-config with the namzu browser login command.
      • —A run a page parked for you (a sign-in, a CAPTCHA) reads needs you: <reason> in schedule list, show, status, /schedule and the TUI's startup line instead of "waiting for approval". --json gains activeRun.handoff (list) and awaitingApproval[].handoff and waitingFor (status).
      • —The scheduled-run floor also refuses tool arguments naming the Windows browser's profile folder (…/AppData/Local/namzu, %LOCALAPPDATA%\namzu). A site a config file's browser.sites denies stays denied for every job.
      • —DISPLAY, WAYLAND_DISPLAY and XAUTHORITY now reach a scheduled run's environment.

      See docs/cli/scheduled-tasks.md#browser-access.

    • —

      d233f26: Add scheduled jobs: prompts that run later in a folder while namzu is closed, under a permission set you write down, run by a scheduler service.

      New commands, all under namzu schedule: add, edit, confirm, list, show, history, pause, resume, remove, run-now, prune, install, uninstall, status, start, stop, logs and daemon. install registers a systemd user unit, a launchd agent or a Windows scheduled task (under WSL, a task that runs the daemon through wsl.exe). New config key schedule (maxConcurrentRuns, notifications), read from the user and managed files only. New files under NAMZU_HOME/schedule/. namzu doctor gains a scheduler.service check. namzu upgrade asks a running scheduler to restart on the new code.

      In the TUI: /schedule lists jobs and what needs you and acts on them, /loop re-sends a prompt to the open conversation on an interval between turns, the model gets a schedule tool (every job it proposes is confirmed by you on a screen namzu computes) and a session_loop tool, and one startup line reports scheduled work since you last looked.

      A job is confirmed on a terminal or in the TUI; schedule add --yes without a terminal (a script, or a model's own shell call) creates it inert. This is a tripwire, not a lock against a program running as you: see "Only a terminal or the TUI confirms a job" in the docs. A scheduled run never approves a call on its own: a call its rules do not allow is refused or parks for you, and you answer it later from /resume in the job's folder, under the job's rules. Allows come only from the job; every deny in your config files still holds.

      Wording only, no behaviour change: namzu serve now says namzu has no server (it used to say no daemon, which the scheduler made untrue), and namzu drain --help no longer says namzu has no daemon. A script matching the old serve sentence must match the new one.

      See docs/cli/scheduled-tasks.md and docs/cli/scheduler-service.md.

    • —

      2b96136: The interactive terminal now proposes saving a multi-step task as a skill. After a turn that answered with at least six successful tool calls across two or more tools (one of them changing something), it prints one dim line under the reply: ✻ That took 9 steps across 4 tools. Save it as a reusable skill? /skills save [name] · /skills save off to stop suggesting. It appears at most once per conversation, takes no keys and costs no model call; nothing is saved unless you type /skills save, which drafts the skill in the same conversation and saves it only from the confirmation screen.

      To turn it off, type /skills save off (it writes skills.suggest: false to ~/.namzu/config.yaml) or set that key yourself; /skills save on restores it. It also stops by itself after three proposals in a row go unused. skills.suggestMinToolCalls changes the threshold. namzu exec, drain, ACP, scheduled runs and sub-agents never show it.

    • —

      2b96136: SKILL.md skills now reach the model on their own. Every session lists the usable skills in the prompt's skills manifest and mounts the skill tool, which the model calls to load one when a task matches its description; until now only plugin skills did, and a file skill needed /skills <name> or exec --skills. Skills are read from six tiers, the later shadowing the earlier: built-in (skills/ in this package), ~/.agents/skills, ~/.namzu/skills, ./skills, .agents/skills from the checkout root down to the working directory, and ./.namzu/skills. The manifest is capped at 2% of the context window or 4 KB, whichever is smaller; skills that do not fit are named in one line and stay loadable.

      What may change for you:

      • —A model working in a folder with skills now sees them and may load one. To keep a skill away from the model, add disable-model-invocation: true (or invocation: operator) to its frontmatter, or name it in the new skills.disabled config list; skills.builtin: false leaves the built-in tier out.
      • —A skill whose metadata.namzu-requires-tools names a tool the session lacks is not offered.
      • —namzu skills-json can now report "source": "system" for a built-in skill, and leaves disabled skills out. A host that switches on source should accept the new value.
      • —namzu skills and /skills list show each skill's directory tier and the files it shadows; namzu --format json skills items gain tier, and shadows, disabled, invocation and requiresTools when they apply.
    • —

      2b96136: Handles a tool's request for a person (ToolResult.handoff, see @namzu/sdk). The paused AgentEvent gains handoff. The terminal shows the reason with press Enter to continue · Esc to stop: Enter resumes the turn and Esc abandons it. namzu exec prints Turn paused — needs you: <reason>. A scheduled run paused this way records awaiting-approval with the reason (reason and the new handoff.reason in its run result), and its notification says needs you: <reason>. namzu resume <session-id> and /resume offer Continue or Abandon for such a run instead of a permission screen. Nothing to change on your side.

    Patch Changes

    • —

      2b96136: The review screen reads a browser call in words. It was titled browser and asked "Do you want to run browser?" over action: "navigate"; it now says Open a web page / Do you want to open this page?, Go back in the browser, or Click on https://shop.example / Do you want to do this on https://shop.example?. An address opens with its path as a person writes it (/wiki/İstanbul) and, when that differs, the address actually sent (/wiki/%C4%B0stanbul) below it; the host stays in its punycode form.

    • —

      2b96136: A generic tool result view may now set outcome: 'cancelled' for a call the person declined on the tool's own screen. save_skill and the schedule tool's create, resume and delete use it (the schedule tool also sets data.cancelled on those results), and the TUI shows the row as ○ … Cancelled — nothing was saved instead of ✗ … failed: Error: The operator cancelled. The model still receives the same refusal text.

    • —

      2b96136: A tool call a person declines without giving a reason now tells the model not to get the same content or result another way — another tool, another site or address, or a web search — unless it asks first and the person agrees. The text was "User declined to run the proposed tool(s)." (and "The user rejected this tool call." on a resumed decision); in a live session a model whose browser navigation was declined fetched the same page through web search instead.

      New export: DECLINED_TOOL_CALL_FEEDBACK, the text. A host that passes its own feedback with a refusal is unchanged. A test or host that matched the old default text must match the new one.

    • —

      2b96136: Continuing a turn a tool paused for a person — Enter at "The browser needs you", or Continue on a parked scheduled run — now tells the model that you dealt with it and to try the step again. It used to see only the tool's "needs a person" result and end the turn, so a scheduled post you had signed in again for was reported as not possible. The Continue/Abandon card for a scheduled run names the site, the profile and the sign-in command in words.

    • —

      2b96136: A scheduled run the browser parked on a sign-in page says how to sign in again: its reason, and so the notification, schedule list, show, status and /schedule, read … is showing a sign-in page; sign in again with namzu browser login <profile> <url>.

    • —

      2b96136: /abandon, /resume and Abandon on a parked scheduled run no longer print the turn's id. They say Stopped the paused turn. Your next message starts a new one in this conversation., Continuing where it paused… and Stopped this run. The job stays scheduled. Nothing else changes.

    • —

      d233f26: The permission screen shows a shell command as it would be typed. It used to escape the command the way JSON does, so printf '%s\n' "$out" read as printf '%s\\n' \"$out\". A multi-line command now shows one row per line, later lines indented under the first; a carriage return and other invisible characters are spelled out as \u{....}, and d still shows the exact input. Nothing to change on your side.

    • —

      2b96136: A tool result whose first line is up to 300 characters long shows that line whole on its ⎿ row and not again underneath. It used to be cut at 120 characters on the row and repeated in full as the first line of the output below, so a blocked browser navigation's note appeared twice. A longer first line is still shortened on the row and kept whole in the output.

    • —

      2b96136: A parked scheduled run whose job cannot run shell commands (bash denied, as in the read-only preset) can be continued from a TUI session whatever its sandbox setting. It used to be refused unless the session's sandbox matched the job's execution, which for a browser job created with execution: sandbox meant editing your config to continue it. A job that can run commands is still continued only from a matching session. The refusal now says a run a page parked "is waiting for you" rather than "waiting for approval".

    • —

      2b96136: Esc or Ctrl+C on the screen that saves a skill now only cancels the save. It also interrupted the turn, so the model never heard that nothing was written.

    • —

      2b96136: The schedule tool's input schema now says that budget limits one run and that maxIterations counts model steps, not repetitions of the job. A model proposed maxIterations: 1 for a job meant to post once per run, and every run stopped after its first model call. The CLI's confirmation of a job, on a terminal or in the TUI, warns when it allows fewer than 10 iterations.

    • —

      2b96136: ScheduleToolHost.create() may return an optional note, which the schedule tool appends to what the model is told about the new job. Existing hosts that return only { name } are unaffected. The CLI uses it to tell the model that no scheduler is installed, so its reply no longer promises a run that cannot happen until you run namzu schedule install.

    • —

      2b96136: A job the model proposes in the TUI and you create now says, when no scheduler is installed, that it will not run until namzu schedule install. namzu browser login ends by saying a scheduled job uses the profile with namzu schedule add … --browser <profile>.

    • —

      2b96136: Confirming an edited scheduled job now lists what changed since it was last confirmed, as + and - lines above the question, in namzu schedule edit, namzu schedule confirm and /schedule confirm. An edit saved with --yes records the lines in the job's history (changes on the edited record in schedule history --json) so the later confirmation can show them.

    • —

      2b96136: The schedule tool asks the model to leave optional fields (folder, time zone, execution, budget, a visible browser window) unset unless the user asked for them, and the TUI's confirmation of a proposed job marks every such value the model set that differs from what you would get by default, e.g. Chosen by the model, not the default: time zone America/New_York, not this machine's Europe/Istanbul.

    • —

      2b96136: A job the model proposes, resumes or deletes in the TUI is confirmed once, on the job's own screen. The permission review no longer asks "Do you want to run schedule?" first in prompt, accept-edits or auto. plan and strict still refuse the call, a schedule rule of ask or deny still applies, and pause is still reviewed. The SDK's schedule tool no longer declares delete destructive, since the host confirms it before anything is removed; a host that relied on that flag to review deletes should add an ask rule for schedule.

    • —

      2b96136: The schedule tool's input schema describes budget.tokenBudget as what the whole run may spend, with every model call resending the prompt, and the CLI's confirmation of a job warns when it allows fewer than 50 000 tokens. A model proposed 4 000 tokens for a browser job whose runs each took about 110 000.

    • —

      2b96136: A parked scheduled run you approve or continue from the TUI is told the current local time again, since an answer can come long after the run started, and every time line now says it is the current time already looked up, so the model does not try to run date for it.

    • —

      2b96136: A scheduled run is told the time it started, in the job's time zone with its UTC offset (It is now Wednesday, 23 September 2026 at 21:04 GMT+03:00 (Europe/Istanbul)). It had only the date; a job that wrote the time into a post, with no shell to ask, wrote the time in UTC.

    • —

      2b96136: A scheduled run is no longer sent the tools its job can never use, which every model call used to resend. For a browser job that posts once to a site with the read-only preset and --unmatched deny, a run went from about 106 000 tokens to about 78 000. A tool withheld this way is refused as an unknown tool if the model names it anyway; nothing a job's rules allow is withheld. New AgentSessionOptions.withheldTools.

    • —

      2b96136: The skill tool now presents its calls as Read skill <name> (or List skills) and hides a successful result, so a host shows one row instead of the raw input and the skill's body. In the TUI, the Working row says Waiting for you while the screen that saves a skill is open, instead of counting on as if the turn were working.

    • —

      2b96136: /skills save off and /skills save on now warn when a project file, profile or managed config sets its own skills block that would override the value you just wrote to ~/.namzu/config.yaml. The warning names that file and says what skills.suggest will be the next time namzu starts. Add suggest under skills in that file to make the change stick. /skills save and /skills new typed while a turn is running now wait and run when that turn ends, instead of being injected into it.

    • —

      2b96136: The confirmation of a job the model proposes is shown whole again. When the model wrote a sentence along with the schedule call, the confirmation was drawn below that still-open reply and, taller than the screen, lost its top lines — the job, the model, the budget and its warnings — while you were asked to create it. A tool's own screen now closes the reply first.

    • —

      2b96136: Skills drafted by /skills save and /skills new, job prompts the model proposes through the schedule tool, and a scheduled run's final summary now follow the language you write in (the job prompt's language for a run), instead of defaulting to English.

    • —

      2b96136: While a question is waiting for you during a turn — a permission review, a scheduled job's confirmation, the Continue/Abandon card — the activity row reads "Waiting for you · the question below" instead of a running Working timer, and the time spent answering is not counted in the turn's elapsed time.

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [d233f26]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [d233f26]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

    • —

      Updated dependencies [2b96136]

      • —@namzu/sdk@45.1.0
      • —@namzu/browser@0.1.0
  16. v29.0.1

    Patch Changes

    • —

      2e2ea14: A skill's allowed-tools now pre-approves its tools. It no longer restricts the tool set.

      The field comes from the Agent Skills format, and that format defines it this way: the listed tools skip the approval prompt for the rest of the turn that loaded the skill, and every other tool stays callable. Namzu read it the other way. After the skill tool loaded a skill, the next batch was narrowed to the listed tools and the model was told to "restrict yourself to" them. A skill with allowed-tools: Read Grep therefore took bash away, and the model stopped doing the work.

      What breaks in @namzu/sdk

      • —A loaded skill no longer narrows ToolContext.allowedTools. If a host relied on allowed-tools to confine the model, it should narrow the turn itself with QueryParams.allowedTools, a step's allowedTools, or deny rules.
      • —ToolContext.adoptSkillScope is deprecated. The kernel never supplies it, so a tool that calls it through ?. now does nothing. It will be removed in the next major. Use ToolContext.grantSkillTools.
      • —createReviewHandler / createReviewPolicy in prompt and accept-edits modes now approve a batch without asking when every call it would ask about is covered by a skill loaded earlier in the turn. To keep asking about every call, pass skillGrants: 'ignore'. plan and strict still refuse such calls, and an operator deny or ask rule, a destructive call, or a path outside the roots or the sandbox is never covered. Each approval made this way is written to the audit trail under the skill's name.
      • —The bash tool runs bash -c instead of /bin/sh -c wherever bash exists (the first bash on PATH, then /bin/bash, then /usr/bin/bash), including for background jobs. On a host whose /bin/sh is dash or busybox sh (Debian, Ubuntu, Alpine), commands now run in bash; a command that relied on dash behaviour, such as echo expanding \n, behaves as bash does. BASH_ENV, ENV, SHELLOPTS, BASHOPTS and BASH_FUNC_* are removed from its environment. To keep /bin/sh, set NAMZU_BASH_SHELL=/bin/sh in the environment of the process that runs the SDK. In a sandbox the tool now passes the guest /bin/sh -c '<launcher>' sh '<command>', which runs bash when the guest has it; a custom Sandbox.exec or spawnDetached that inspected its arguments for ['-c', command] sees the launcher instead. With no bash, the host still runs /bin/sh -c.
      • —A command line is read for the shell that runs it. AuthorizationGate.evaluate, evaluateRule and SkillGrantSet.coveringSkill called without a dialect read it for any POSIX shell (sh), so an allow rule or Bash(<pattern>) entry no longer approves a line using a bash-only construct ($'…', &>, |&, <<<, [[, arrays, brace expansion, time and others) unless the caller says the shell is bash. The kernel says so for the bash tool on a host that has bash; inside a sandbox it reads in sh, because the guest may not have bash. A host calling the gate itself passes commandDialect: 'bash' in the ToolCallContext (or { commandDialect: 'bash' } as the last argument of evaluateRule and coveringSkill) when the command will run in bash. Deny rules are unaffected: they still see every command.
      • —parseAllowedTools splits on whitespace as well as commas, and keeps Tool(pattern) entries whole. "read write edit" used to be one name and is now three.
      • —The skill manifest in the system prompt renders the field as <pre_approved_tools> instead of <allowed_tools>. The skill tool's notice lists what was pre-approved and what was ignored, and says every other tool remains available.

      Added: ToolDefinition.commandDialect (and the defineTool option), ToolCallContext.commandDialect, EvaluateRuleOptions, the ShellDialect type, and NAMZU_BASH_SHELL; ToolContext.grantSkillTools, ToolCallSummary.skillGrant, approve_tools.skillGranted, ReviewPolicyOptions.skillGrants, SkillGrantSet, compileSkillGrant, SKILL_TOOL_NAME_ALIASES, permissionPatternToRegExpSource, and a FrontmatterOptions third argument to parseFrontmatter (lists), which the skill loader uses so that allowed-tools can be a YAML list. Names are matched case-insensitively and through the format's aliases (Read → read, WebFetch → web_fetch). Bash(git status *) uses the CLI permission-table glob, applied to each command in the line. ${CLAUDE_SKILL_DIR} and ${NAMZU_SKILL_DIR} expand to the skill's directory. An unknown name is ignored and reported, and never widens the grant. A tool that is destructive for every input (the shipped write and run_code) is ignored and reported too, because each of its calls is reviewed anyway. BashOutput, KillShell, TaskOutput and TaskStop map to job, and TaskCreate, TaskUpdate and TaskList to task_*. ToolContext.grantSkillTools returns a commit(), and the skill tool records the grant only once it has delivered the skill's instructions. The grant also ends when the operator sends a message while the turn is still running (inboundMessages or steering), through the new SkillGrantSet.clear(). A Bash(<pattern>) entry never covers a line that redirects output into a file (git status > ~/.bashrc); /dev/null and 2>&1 stay covered. A whole-tool Bash entry grants the tool as it is, writes outside the working directory included, because bash has no path argument for the escalation check. An entry naming a tool that allowedTools withholds from the turn or step is ignored as not available, and SkillGrantToolResolver gains an optional unavailable field for that.

      @namzu/cli: a plugin skill's allowed-tools pre-approves for the turn and no longer takes tools away. SKILL.md files whose allowed-tools is a YAML list are now listed instead of refused. The [permissions] glob now comes from the SDK, and it matches the same commands as before. permissionChecks read a builtin tool's command line in the dialect that tool reports, as the runtime does.

    • —

      Updated dependencies [2e2ea14]

    • —

      Updated dependencies [2e2ea14]

      • —@namzu/sdk@45.0.0
      • —@namzu/computer-use@1.4.3
      • —@namzu/anthropic@6.1.0
      • —@namzu/ollama@2.2.4
      • —@namzu/openai@4.1.0
      • —@namzu/openrouter@3.0.1
  17. v29.0.0

    Major Changes

    • —

      eae8e31: Read-only agents start without asking in prompt mode. An Agent call that starts explore, or an agent file with readOnly: true, on the session's own provider and model no longer opens the "Start an agent" review in prompt or accept-edits mode, and plan mode now lets it start instead of refusing it. Every call such a child makes is still reviewed as before, and its roster still has no tool that writes.

      Still asked about: a general-purpose agent, an agent file without readOnly: true, a read-only agent given provider, effort or a model other than the session's, an agent file that names another model, and any batch that includes one of those. strict still refuses every launch no rule allows.

      What breaks: a prompt-mode operator who relied on seeing and declining each explore launch is no longer asked, and a plan-mode turn can now start read-only agents.

      To keep the old behaviour — every launch asked about in prompt and accept-edits, refused in plan — add an ask rule for the tool:

      { "permissions": { "Agent": "ask" } }
      

      in namzu.config.json (or permissions: { Agent: ask } in ~/.namzu/config.yaml). An ask rule is an explicit review, which the read-only exemption never skips. /permissions details says which rule is in force.

    Minor Changes

    • —

      64489b3: Delegated work split into phases reads by phase, as the reference terminal's workflow view does. Nothing is removed and no key changes meaning; screens and transcripts look different.

      • —The agent rail keeps a phased workflow together. Agents sharing a workflow label stay on the rail as one piece for the parent turn: a finished phase is one line, ✓ Phase 1 · 2/2 · 4.0s, and a live phase has its agents beneath it. Unlabelled agents are grouped by launch, as before. Under 24 rows a finished phase's line is left out.
      • —The rail's header counts the whole workflow: ● <workflow> · 1 running · 2/3 done · 6.5s · 18.0k tokens · ↓ / ctrl+t. Time and spend show from 96 columns.
      • —The agent cockpit opens on the phase still working, and on its first working agent, instead of on the first phase. Its header reads 2/3 agents done · 1 running · 7.7s · 18.0k tokens while work runs and 3/3 agents · 9.5s · 27.0k tokens · done after, in place of N active · N total. Phase rows add the phase's time; the agent pane is titled by its phase (Phase 2 · 1 agent) instead of Agents · 1/2.
      • —The closing line adds the phase count when two or more were named, the tokens spent, and failures: ✻ Worked for 11s · 3 agents in 2 phases · 27.0k tokens.
    • —

      5fb75c2: Delegated work reads more like the reference terminal. Nothing is removed and no key changes meaning; transcripts and screenshots will look different.

      • —Launch receipts. Each batch of agents one response launched now writes one ● Launched 2 agents · <workflow> / <phase> (ctrl+t to manage) row, with the agents named beneath it as a ├/└ tree.
      • —Completion rows read ✓ <name> · 1.7s · 9.0k tokens or ✗ <name> · failed after 2.9s · <reason> instead of <name> · Completed · ctrl+t · agent details. A completed agent's final answer is attached collapsed; Ctrl+O opens it. The hint is ctrl+o result · ctrl+t details.
      • —A closing line, ✻ Worked for 38s · 3 agents, ends a turn that launched agents. Other turns add nothing.
      • —The rail is a borderless tree under the footer: a ● <workflow> · N running · N queued · ↓ / ctrl+t header, one ├/└ branch per agent, and the running agent's activity on a ⎿ line beneath it on terminals at least 24 rows tall. Tool uses and spend now come before the model. It shows up to three agents on a 30-row terminal where it showed four, because each agent takes two rows there.
      • —The rail stays visible while an approval dialog is open, reduced to its header line. It used to disappear for as long as the dialog was up.
      • —One waiting line. When the parent is only waiting on its agents, the Waiting · <name> rows fold into ✻ Waiting for N agents to finish.
      • —The /effort picker is a left-to-right slider on terminals at least 60 columns wide: default, the model's levels, then ┆ orchestrate in violet with <highest> + delegate by default under it. ←/→ (and ↑/↓) move it, digits select, Enter applies, Esc goes back. Narrower terminals, and menus too long for one row, keep the vertical list.
      • —Orchestrate mode shows on the message box: the top border carries orchestrate in violet on the right, with a still colour gradient where colour is allowed; the footer's orchestrate is violet. Below 40 columns the border stays plain.
    • —

      4375e72: The terminal draws research turns the way the reference terminal does.

      • —Web searches and fetches are one row naming the query or address, ✓ Web search("…") / ✓ Web fetch(https://…), with a ⎿ line that reads Searching: … while the call runs and settles to Found 3 results in 4.1s, Did 1 search in 9.0s or Received 7.5KB in 1.2s. Consecutive calls sit together without blank lines, and a long query is cut at the terminal's width with an ellipsis. A fetched page or result list stays behind Ctrl+O. They used to read ✓ Web search · 9.0s with no query and a blank line between every row.
      • —Markdown tables are drawn as a box (┌┬┐ ├┼┤ └┴┘) sized to the terminal, with long cells wrapped inside their column and **bold**, `code` and links drawn rather than shown as source. Where the columns cannot fit, the table becomes Header: value records separated by a ─ rule. A table streaming in no longer flashes raw | a | b | rows. Tables used to be a header, one rule and cells cut mid-word at 32 characters.
      • —The Working row counts the turn's output, Working (46s · ↓ 1.1k tokens · esc to interrupt).
      • —A finished turn that took three seconds or more closes with ✻ Worked for 46s, as a turn that delegated work already did.
      • —Wrapped text in replies, your own messages and notices no longer starts a row with the space it wrapped at, so every row of a paragraph starts in the same column.

      Nothing to change on your side.

    Patch Changes

    • —

      710e684: A wide row in the agent cockpit no longer runs one cell past its pane: the model or token count at the end of the row sat on the frame's padding and touched the border.

    • —

      4f14e2d: On a terminal under 64 columns the agent rail's header now counts agents done out of the total (2/3 done), the figure its wide form and the agent cockpit's header give at the same moment. It used to print the agents still running over the total with no word after it (1/3), which read as one agent done. Nothing to change on your side.

    • —

      45a79f3: Three places in the delegated-work screens named a key or text that did not work or was cut off:

      • —An agent's ctrl+o result row that had scrolled into history could not be opened. Now, once the live bodies are open, the next Ctrl+O opens the newest settled body in the output viewer, and ←/→ move to the others. With nothing settled, the second press still just folds the live bodies.
      • —While an approval dialog is open, the reduced agents rail no longer shows ↓ / ctrl+t. The dialog holds both keys, so neither reached the rail.
      • —On an 80-column terminal the effort slider's <highest> + delegate by default sub-label moves left so it fits on screen instead of being cut. The spend warning wraps instead of being cut. The rows it uses stay reserved on the other stops, so the picker keeps the same height when the caret moves.

      No configuration or API changes.

    • —

      d9b1d30: An Agent call that names the session's own model, with no provider or effort, now runs on the session's provider, as a call with no model does. It used to be looked up in the model catalogue, and when the session provider's listing failed or did not list that id, the child ran on any other connected provider that did — including a read-only explore launch, which starts without the "Start an agent" review on the promise that it stays on the session's provider. To send a child to another provider, name provider (still reviewed).

      The agent cockpit's phase pane is titled Phases. It used to read Phases · 1/2, the cursor position, above phase rows whose 2/2 means agents done, so a finished two-phase workflow read as one phase of two. Nothing to change on your side.

    • —

      5bacecf: A transcript row written while the reply above it is still streaming no longer reaches scrollback before that reply. It used to settle first on a short terminal; when the reply then finished it was placed below rows already printed, so one row was printed twice and the reply's sentence was never printed. Nothing to do on upgrade.

    • —

      f934968: A transcript row keeps its column and its wrap when it settles into scrollback. Settled rows used to lose the one column of padding live rows have, so a finished screen mixed rows starting at column 0 and column 1, and a long settled row wrapped two columns wider than it had while live. The brand header, printed the same way, moves one column right with them. Nothing to do on upgrade.

    • —

      0a39453: A reply that is still streaming is drawn where it stands in the conversation. It used to be drawn below every finished row, so a row written while it streamed, such as the launch receipt of an agent running in the foreground, appeared above the text that came before it and the two swapped when the turn ended. Rows no longer move after they are drawn. Nothing to do on upgrade.

    • —

      a31aeb7: A reply taller than the terminal keeps its last line once it finishes. The redrawn part of the screen is now held one row shorter than the terminal, so the renderer no longer clears the screen and replays the session on every frame of a long reply, and no longer erases the reply's final line when the turn settles. While such a reply streams, its newest rows stay on screen; the whole reply reaches scrollback when it ends.

      A list item or blockquote containing a pipe directly under a markdown table is drawn as a list item or quote again, not as an extra table row.

    • —

      e3c9228: Typing that reaches the composer in one long read (keystrokes queued behind a busy screen, with no bracketed-paste markers) is now inserted as typed text. It used to become a Pasted text chip once it passed 80 characters, and the chip was joined back to what had been typed before it with a blank line — so the message the model received could have a word split in two ("subag" / "ents"). A bracketed paste over 80 characters, and any unbracketed chunk containing a newline, is still held as a chip. Nothing to do on upgrade.

    • —

      Updated dependencies [4375e72]

    • —

      Updated dependencies [1b65e2e]

      • —@namzu/sdk@44.3.0
      • —@namzu/openai@4.1.0
      • —@namzu/anthropic@6.1.0
      • —@namzu/google@1.1.0
  18. v28.1.1

    Patch Changes

    • —

      9bd0c99: Plan mode now holds inside delegated work.

      @namzu/sdk: new optional BaseAgentConfig.reviewAllowedCalls and AgentTaskContext.reviewAllowedCalls. AgentManager stamps the spawning context's function onto every child (after the configBuilder runs) and onto the child's spawn context, so grandchildren inherit it too; SupervisorAgent hands its own to its workers, and ReactiveAgent and SupervisorAgent pass it to query(). Before, only the turn a host called query() with saw QueryParams.reviewAllowedCalls: inside a child, a batch a rule allowed, or one an approval earlier in the child's turn covered, ran without reaching the borrowed review handler. Nothing changes for a host that never sets the field. A child config that sets its own value keeps it, but it is OR-ed with the inherited one: a child can ask for more review and can no longer answer false over a parent that answers true. A host that builds its own AgentTaskContext for a TaskScheduler should set the field from the function it passes its own query().

      @namzu/cli: plan mode entered with Shift+Tab while a sub-agent runs now refuses that sub-agent's next change even when a permissions rule allows it; before, the child ran it. /resume on a parked turn now continues it under the permission mode the operator is in, read at each decision like a new turn's, for the turn and its sub-agents; before, the interactive terminal resumed it under auto whatever the mode, so /resume in plan mode let its changes through and a Shift+Tab during it did nothing. With nobody asked on a resumed turn, prompt and accept-edits still approve what the rules leave to review; plan and strict now refuse. namzu exec resumes under the mode it was started with, as before, and in plan mode that now also covers changes a rule allows.

    • —

      Updated dependencies [9bd0c99]

      • —@namzu/sdk@44.2.0
  19. v28.1.0

    Minor Changes

    • —fad53e3: Shift+Tab now changes the permission mode while a turn runs, and its only reply is the footer. It used to be refused mid-turn with "Permissions were not changed. Finish or stop the current work first." (once per press), and every accepted press appended "Permissions: <mode> for this session. …" to the transcript. Now the running turn's next approval decision, the delegated turns that borrow its review, and every later turn are decided under the new mode; an approval dialog already on screen keeps the mode it was asked under, entering plan mode refuses the next call that would change something, and leaving it approves nothing already refused. Each change is recorded as approval_policy_changed in the session log before it takes effect, and the model is told once. /permissions still answers in the transcript. The footer's hold mark is ‖ instead of ⏸, an emoji code point that Windows Terminal draws as a blue two-cell tile; the same mark replaces it on the interrupted-turn notice and the paused-queue line.
    • —f2ec6e7: The model's plan is drawn once, as a checklist in the transcript. Consecutive task calls fold into one block with a header in words (Added 2 tasks, Started · <subject>, Completed · <subject>, Tasks · 1/2 done) and the checklist as it stood afterwards; /tasks draws the same checklist. No task id, owner, JSON argument or model receipt (Task created: <uuid> — "…" [owner: namzu], 1 tasks: 0 completed, …) is shown any more. The marks are one-cell text characters with exactly one space before the subject — □ pending, ■ in progress (bold), ✓ completed (dimmed, struck through), ✗ failed — instead of ☐/☑/☒/◐, which emoji-capable fonts drew as two-cell colour pictures. The eight-row task list above the composer is gone; a single row naming the current step appears there only while the checklist is out of view. A system notice identical to the row directly before it is no longer printed a second time.

    Patch Changes

    • —

      51af1b3: Plan mode now refuses a change that a permission rule allows. Before, a batch whose every call a rule allowed (for example permissions: { bash: 'allow' }) ran without reaching the review handler, so a turn in plan mode, whether it started there or the operator entered it with Shift+Tab mid-turn, still ran touch or any other allowed command, while the model had been told that every change is refused. The CLI now asks the kernel to send such batches to review while it is in plan mode; nothing changes in any other mode.

      For SDK hosts: QueryParams.reviewAllowedCalls?: () => boolean is new and optional. Read once per batch; true sends a batch a rule allows, or a grant from earlier in the turn covers, to resumeHandler instead of running it, each call carrying the gate's decision in authorization. A rule's deny still refuses. Absent, behaviour is unchanged.

    • —

      406e7a2: A task removed with task_update status deleted is now distinguishable on the stream: its task_updated event carries deleted: true (the SSE task.updated event too), with the subject and status the task had when it went. Before, a removal arrived as an update that changed nothing, so the interactive terminal kept drawing the removed task as an open step in the checklist. It now drops the task and writes Removed task · <subject>. The field is optional and absent on every other update; a consumer that ignores it sees the same events as before.

    • —

      Updated dependencies [51af1b3]

    • —

      Updated dependencies [406e7a2]

    • —

      Updated dependencies [0d23de8]

      • —@namzu/sdk@44.1.0
  20. v28.0.0

    Major Changes

    • —

      a75f5c2: namzu run and namzu run-stream are removed. Use namzu exec.

      BeforeAfter
      namzu run "<prompt>"namzu exec "<prompt>" (or namzu e "<prompt>")
      namzu run-stream "<prompt>"namzu exec --json "<prompt>"

      exec takes every option the two old commands took, with the same meanings: --cwd, --provider, --model, --effort, --skills, --continue/-c, --resume, --session, --gate, --gate-retries, --max-iterations, --token-budget, --wait-for-provider, --permission-mode, --trust, --yolo and --. The default mode prints the reply exactly as run did and keeps its exit codes (0, 1, 2, 64, 75, 77, and death by SIGTERM/SIGHUP/SIGINT). --json writes the same NDJSON events, with the same kinds and fields, and the same exit codes (0, 1, 75, 77) that run-stream did. Typing run or run-stream now fails with commander's unknown command error, exit 64, and a line naming the replacement. Update scripts, CI jobs and host UIs that spawn either command.

      New: --output-schema <file> on exec. It binds the final answer to a JSON Schema through the provider's native structured output, in either mode. Before, --output-schema was accepted only before the command, for the TUI, and refused everywhere else; given before exec it is still refused, now with a message that says to pass it after exec. A schema file that cannot be read or represented exits 64 (in --json mode: an error event and exit 0).

      Also changed:

      • —namzu exec --session <id> without --json is refused with exit 64. run accepted --session and ignored it, answering against no history; the default mode resumes with --continue or --resume <id>.
      • —namzu resident refuses --json and --output-schema, which it would otherwise have accepted and ignored.
      • —The --log-format help and the TUI's --output-schema help name exec.

    Minor Changes

    • —

      ab9108b: Refresh the Zen and Zen Go model catalogue in the background on every launch. The interactive TUI, resume, run, run-stream, acp, drain, resident run and the background resident runner start one refresh that never delays startup, gives up after 30 seconds and is cancelled when the command ends. When it lands, the model picker and routing use it immediately, including for providers already built, and it is saved as a last-good copy at cli/zen-catalogue.json under NAMZU_HOME. When it fails, the session keeps the last-good copy, or otherwise the bundled catalogue, and logs one warning. A damaged copy on disk is ignored rather than read in part. A model the service serves with no known wire format is not offered in the picker, since the CLI has no setting that names a protocol for it.

      This is on by default, and each of those launches now reads OpenCode's documentation pages, models.dev and the two Zen /models endpoints over the network. To keep the previous behaviour (bundled catalogue only, no network read), set modelCatalogueRefresh: false in ~/.namzu/config.yaml, or NAMZU_MODEL_CATALOGUE_REFRESH=0.

    Patch Changes

    • —128913f: The TUI says "turn" where it still said "run" for the unit of work that no longer exists: /cost, /status and the status panel label spend as "current or latest turn", a message held after a paused turn says so, the /config notice names turn limits, and the headless trust refusal says a headless turn approves tools without asking, and namzu doctor's remediation for an unusable fallback says turns will still start. Wording only; nothing a script parses changed.
    • —Updated dependencies [ab9108b]
      • —@namzu/zen@2.5.0
  21. v27.0.0

    Major Changes

    • —

      518b0d3: Every turn the CLI starts keeps its newest 10 checkpoints instead of all of them. That covers interactive turns, namzu run, resumed and drained turns, and delegated child sessions. A turn takes a checkpoint every iteration plus one per tool review, and nothing set a limit, so a long session kept every one. On one machine that came to 19,014 checkpoint files and 6.33 GB.

      What changes for you. Only the newest 10 checkpoint documents of each turn stay in <session-id>/checkpoints/, and namzu drain reports at most that many per turn. A checkpoint whose decision is still outstanding is never pruned. Resuming is unaffected, because every resume reads the checkpoint it was handed or the newest one. If you inspect intermediate checkpoints by hand, copy them out while the turn is still going.

    • —

      b064cea: The OS sandbox is now off by default. Shell commands and file tools run on the host under the permission system, the way other coding agents run them: each shell command goes through your permission rules and mode, and a file tool's path outside the working directory and the added directories is shown to you as an approval request (Outside the working directory: <path>) instead of being refused. It is asked every time — auto, --yolo, plan (for a read) and an earlier "allow all tools for this session" included — while strict and a deny rule still refuse it. A headless turn refuses it: pass the directory with --add-dir (or additionalDirectories) when an unattended run needs it. Each answer is an audit record.

      To keep the previous behaviour (every command confined), write sandbox.enabled: true in namzu.config.json or ~/.namzu/config.yaml. The sandbox is also on, without that line, when sandbox.requireIsolation names a control or sandbox.workspace is ephemeral. sandbox.enabled: false keeps working as before.

      With the sandbox on, a bash call may set dangerously_disable_sandbox: true to run one command on the host. It is asked about every time, auto and --yolo included (plan and strict refuse it); it is refused when nobody can be asked, unless sandbox.allowUnattendedEscape: true; sandbox.allowEscape: false refuses it always. Each approval or refusal is an audit record.

      /add-dir of a directory outside the working directory now asks before adding it, deciding after links are followed (a link inside that points outside is asked about under the path it leads to, and that canonical path is what is added). A directory inside the working directory is no longer added: the tools already reach it. The system prompt states where tools run and how to reach past that, and on WSL where the Windows drives are and how to start a Windows program.

    • —

      7cac6dc: #note and /memory add now save a typed memory file instead of appending a bullet to <project>/.namzu/MEMORY.md.

      What changes for you:

      • —Where a note goes. #note <text> and /memory add <text> write <name>.md (type project; /memory add --type user|feedback|project|reference <text> picks another) into the project's stored memory — <NAMZU_HOME>/projects/<slug>/memory/ by default — the same files the model's save_memory, search_memory and read_memory use. The terminal names the file. /memory --user add <text> is unchanged and still appends to ~/.namzu/MEMORY.md. To keep writing a note into the curated project file, edit <project>/.namzu/MEMORY.md directly; it is still read into every turn.
      • —Old notes are copied only when you ask, and your file is never changed. The project's curated MEMORY.md is not rewritten, at launch or ever: nothing can tell a bullet #note appended from one you wrote. When it holds top-level bullets, the launch says how many, once per curated file, and /memory import-notes copies every top-level bullet (its first line) into a typed project memory, skipping any already stored — by an earlier import, a #note with the same text, or a copy you archived — so running it twice creates no duplicates. The bullets stay curated text in every turn until you delete them from the file yourself.
      • —A one-time move of the JSON store on first launch. An earlier JSON memory store (index.json + content/) in the same directory is imported with its ids and renamed *.migrated; the launch says so. A record too large for a memory file (over 256 KiB) is not imported and is named, with the retired file that still holds it.
      • —The prompt. Every turn carries a ## Stored memories (index) section — one line per memory you or the model saved, at most 200, your feedback and user memories first. What the session memory promoter (or compaction.consolidate) writes after a turn is searchable but not listed, so a turn's record does not change the next turn's system prompt — and the curated sections are renamed ## Curated memory (all projects) and ## Curated memory (this project) (they were ## Durable memory and ## Project memory). Anything that matched those headings in a captured prompt must match the new ones.
      • —/memory show lists stored memories' index lines before the curated files, and what turns recorded on their own in a separate Recorded by turns (N) section, so those records are visible even though the prompt's index leaves them out.
      • —Note names are short. A note's file is named after its first words, at most 32 characters, so its index line keeps room for the note itself.

      Nothing in the CLI's library exports changed.

    • —

      3e7a97b: The CLI moves to the SDK's session → turn → message model and to one state layout under NAMZU_HOME (default ~/.namzu). Conversations, checkpoints, memory and resident state written by 26.x are not read by this version.

      Before you upgrade.

      • —Run namzu drain on 26.x until it reports nothing parked. A turn still parked on a decision or a provider wait when you upgrade cannot be resumed.
      • —Export any conversation you want to keep with namzu history --session <id> > conversation.json on 26.x (with no --session, the latest one in the folder); it prints the messages as JSON. There is no migration: nothing is imported, so nothing is lost silently or brought back half-read.

      What changes for you

      • —Where state lives. Each working directory gets ~/.namzu/projects/<slug>/, where the slug is the directory's canonical path with every character outside [A-Za-z0-9] replaced by -. A session is <session-id>.jsonl there, with its child sessions, checkpoints, tasks, feedback, goals, tool-result spills and file-history snapshots under <session-id>/. Memory moves to projects/<slug>/memory/, resident state to projects/<slug>/residents/, and git worktrees to projects/<slug>/worktrees/. ~/.namzu/index.sqlite is an index rebuilt from the logs whenever it is missing or out of date; deleting it loses nothing. The CLI still never writes into the working directory; .namzu/ there is read only, for the agents, skills, commands, plugins and MEMORY.md you put in it.
      • —Old state is left alone and reported. namzu state now prints report version: 2 with a legacy category: the old top-level state/, sessions/, titles.json, desktop-sessions.json, delegation-history/, checkpoints/, tenants/, goals/, feedback/, learning/, residents/, worktrees/ and memory/, and every projects/<uuid>/ directory. It lists their paths and sizes and never opens, moves or deletes them; remove them yourself once you have exported what you need. A new projects/<slug>/ is never reported as legacy.
      • —One turn at a time per session. A session has at most one active turn. namzu run and namzu run-stream against a session whose turn is still running, paused or interrupted now exit 75 (EX_TEMPFAIL) and name that turn: on stderr for run, as an NDJSON {"kind":"error","code":"turn_in_progress",…} event for run-stream. 75 keeps its existing meaning for a provider pause too; a wrapper that already retries later on 75 needs no change. In the TUI the refusal offers /resume or the new /abandon, which closes the paused turn so the next prompt can start.
      • —A rate limit on the first request pauses the turn. A provider rate limit or outage on a turn's first request used to fail it (namzu run exit 1) with nothing to continue; it now pauses it at a checkpoint like the same fault later in the turn: namzu run exits 75 naming the checkpoint, --wait-for-provider waits and resumes it, and /resume or namzu drain continues it.
      • —/agents runs is now /agents batches, with no alias. /agents runs prints the unknown-subcommand usage.
      • —NDJSON gains ids. run-stream's done and usage events carry sessionId and turnId, and a paused event names its turnId instead of a run id.
      • —Hooks. The events run_start, run_end and run_interrupt are turn_start, turn_end and turn_interrupt; a config that names an old event is refused at load with a message naming the new one. A hook's stdin carries turn_id (not run_id) and, on subagent_stop, parent_session_id and parent_turn_id (not parent_run_id); its environment carries NAMZU_TURN_ID (not NAMZU_RUN_ID). session_id and NAMZU_SESSION_ID are always set. session_start and session_end hooks receive no turn id: the CLI no longer invents one for them.
      • —namzu drain keeps its flag names and exit codes (0, 1, 64, 77), but --store means something else: it was the runs/ directory a checkpoint store wrote to, and it is now the namzu home (NAMZU_HOME, ~/.namzu by default) whose projects/ hold the session logs. A --store without a projects/ directory is refused with 64, so a wrapper that still passes its old runs/ path must pass the home instead. A scope the store does not hold (an unknown session, one under another project or tenant, a child session) is also 64; state that cannot be read is 1. It finds parked turns through the session index, takes each session's lease, and continues the same turn from its checkpoint; a turn whose session lease another worker holds is skipped and reported.
      • —Delegation history is not carried over. The history block and /agents read finished child sessions from their logs; children recorded by 26.x do not appear.
      • —Checkpoints. A session keeps each turn's newest 10 checkpoints under <session-id>/checkpoints/; one an open decision references is never pruned. /restore snapshots are under <session-id>/file-history/.
      • —Crash dumps are gone. The session log and per-iteration checkpoints hold everything an interrupted turn needs, so no emergency/ dumps are written. An interactive session that was interrupted closes that turn as interrupted when you send the next prompt.
      • —A stopped process gives its conversation back. On SIGTERM, SIGHUP (a closed terminal) or SIGINT, the TUI, namzu run and namzu run-stream release the conversation's writer lease first, then stop the turn, close the session (tool servers, background jobs, the session_end hook) and give the terminal back, and then die of the signal they were sent: a wrapper sees 143, 129 or 130 as before. The turn is left interrupted, so /abandon, /resume, namzu drain or the TUI's next prompt take it at once; before, they were refused as "leased by a live writer" for up to five minutes. A second signal exits immediately. run-stream writes {"kind":"error","code":"terminated",…} and a final done before it exits, and run names the session on stderr. SIGKILL still leaves the lease to expire. The message follows where the signal found the turn: a turn that had already ended (the session still closing) is reported as recorded, not interrupted, and run-stream's one done is then the turn's own; a run --wait-for-provider stopped during its wait says the turn is paused at its checkpoint, and does not resume it.

    Patch Changes

    • —

      96ac3ec: A session never writes generated state into its working directory, and a session started in the home directory no longer reads the same memory file twice.

      Every entry point — namzu, namzu run, namzu run-stream, namzu drain and resident runs — keeps its sessions, memory and task state under the application home (NAMZU_HOME, else ~/.namzu), in the working directory's projects/<slug>/, and files them under that directory's one Project. <cwd>/.namzu is only read, for the agents, skills, commands, plugins and MEMORY.md you keep there.

      Started in $HOME with no NAMZU_HOME, the project's .namzu/MEMORY.md and the user's ~/.namzu/MEMORY.md are one file. It was injected into every prompt twice, under both headings. It is now read once, as the user memory.

    • —

      3641102: The turn-start repository snapshot (git status and recent commits, sent on a send's first request) now reaches the model as request-only context after the conversation, not as a system message. On Anthropic, each new send used to change the system prompt and re-read the whole conversation uncached. Now the cached conversation is kept and only the snapshot's own tokens are new.

      What the model reads changes in two ways: the snapshot arrives in a user-role message rather than a system message, and it follows the line Current step context (runtime-generated; not a new user request):, which every request-only context message opens with. The snapshot text itself is unchanged.

    • —

      Updated dependencies [8805360]

    • —

      Updated dependencies [a729b17]

    • —

      Updated dependencies [9355755]

    • —

      Updated dependencies [755a81a]

    • —

      Updated dependencies [cb1f00c]

    • —

      Updated dependencies [3641102]

    • —

      Updated dependencies [933ba6d]

    • —

      Updated dependencies [3e7a97b]

    • —

      Updated dependencies [a84dc1c]

    • —

      Updated dependencies [b064cea]

    • —

      Updated dependencies [9238347]

    • —

      Updated dependencies [a14b013]

    • —

      Updated dependencies [3641102]

    • —

      Updated dependencies [3641102]

    • —

      Updated dependencies [3e43fc2]

      • —@namzu/sdk@44.0.0
      • —@namzu/anthropic@6.0.1
      • —@namzu/openrouter@3.0.1
      • —@namzu/zen@2.4.0
      • —@namzu/computer-use@1.4.3
      • —@namzu/ollama@2.2.4
      • —@namzu/openai@4.0.0

deepseek

1 release

  1. v2.0.2

    Patch Changes

    • —567ada8: Updated package examples and evaluation fixtures for the SDK's ToolRegistry removal. Browser, computer-use and LSP documentation now show toolsets; the evals use toolsets when they call the kernel. Provider changes are test fixture migrations only, with no provider runtime API change. Consumers that pass tools to the SDK should use toolsets: readonly Toolset[] and wrap their definitions with toolset(source, definitions).

evals

2 releases

  1. v1.0.1

    Patch Changes

    • —567ada8: Updated package examples and evaluation fixtures for the SDK's ToolRegistry removal. Browser, computer-use and LSP documentation now show toolsets; the evals use toolsets when they call the kernel. Provider changes are test fixture migrations only, with no provider runtime API change. Consumers that pass tools to the SDK should use toolsets: readonly Toolset[] and wrap their definitions with toolset(source, definitions).
  2. v1.0.0

    Major Changes

    • —

      3e7a97b: Requires @namzu/sdk >=44.0.0 (was >=5.0.0): the suites build their cases on the SDK's turn API (evalTurnFromTurn), which does not exist before 44. An earlier @namzu/evals breaks against SDK 44, and this one does not load against an older SDK. No case, score or report shape changed.

      What to do: upgrade @namzu/sdk, @namzu/cli and @namzu/evals together.

    Patch Changes

    • —018187a: The kernel suites remove each case's scratch directory when the case ends and keep the session state inside it. Every case used to leave a directory with a full state tree in the system temporary directory.

files

1 release

  1. v1.1.2

    Patch Changes

    • —

      2d4ff9a: Shipped text no longer names one particular application built on namzu. Nothing to do to upgrade: no type, default or behaviour changes.

      • —@namzu/sandbox: when a self-hosted Firecracker orchestrator returns a network-mode (mtls) agent handle and the backend was given no client certificate, the error now tells you to pass mtls: { ca, cert, key } in the backend config. It used to name environment variables that only one host defines, which no other installer has. The message still starts with firecracker: orchestrator returned an mtls agent handle but no client cert material was injected, so code matching on that prefix keeps working.
      • —Doc comments in the published .d.ts files and sources (ContainerBackendConfig.labels, the ACI and Azure Blob name options, the sandbox mount-source types) say "the host" or "the consumer", and label examples use the placeholder acme. namespace. The microvm tier's (MicroVMBackendConfig, AgentSnapshotRef, OrchestratorNetworkPolicy) describe the orchestrator as a self-hosted one the host runs, not as namzu's own: namzu ships only the client.
      • —Earlier entries in the @namzu/sandbox, @namzu/sdk and @namzu/anthropic CHANGELOGs are reworded the same way; the @namzu/files CHANGELOG named no one and is unchanged. Versions already on npm keep their old text.

google

3 releases

  1. v1.1.2

    Patch Changes

    • —023224a: A failed OpenAI or Google request now reaches the host with the provider's own type, code, parameter and request ID, as an Anthropic failure already did, so Details read "Status: 403 · Provider said: “…” · Type: invalidrequest_error · Code: model_not_found" with "Request ID: …" on its own line. The error's detail for a vendor SDK error whose message is only a sentence is now a JSON body of those fields ({"error":{"message","type","code","param"},"request_id"}) instead of the bare sentence; read providerCode or the Details of a host rather than matching the sentence text. The Google driver now reads its refusal body and request-ID header. The vendor project line in Details reads "Project: proj…".
  2. v1.1.1

    Patch Changes

    • —

      49491b9: The drivers now report more accurately how a response ended. The runtime uses this to tell a tool call the output limit cut off from one the model wrote badly.

      • —@namzu/anthropic: a tool call whose JSON does not parse no longer fails the whole stream with "the provider stream returned malformed data". The block close and the finish reason now reach the runtime. model_context_window_exceeded is reported as length, and refusal as content_filter. Both used to read as a normal stop.
      • —@namzu/bedrock: model_context_window_exceeded is reported as length, and guardrail_intervened as content_filter. A tool call opens with the id the driver keeps, so its arguments never arrive before an id.
      • —@namzu/http: the OpenAI dialect maps finish_reason instead of passing the server's string through. function_call becomes tool_calls; max_tokens and max_output_tokens become length; model_length, context_length, context_length_exceeded and model_context_window_exceeded become length with finishDetail: 'context_window'; eos, eos_token, end_turn and stop_sequence become stop. error fails the stream with a ProviderRequestError (kind: 'server'), as OpenRouter's does. A value it does not know reports no finish reason at all, which the runtime reads as a response that did not say how it ended, rather than as a normal finish. The Anthropic dialect gets the Anthropic mapping above and opens a tool call with the id its arguments carry.
      • —@namzu/openrouter: finish_reason is mapped the same way. error (the upstream model failed mid-generation) now fails the stream with a ProviderRequestError (kind: 'server').
      • —@namzu/deepseek: insufficient_system_resource now fails the stream with a ProviderRequestError (kind: 'server'). It used to read as a finished answer.
      • —@namzu/openai: Codex's response.incomplete is reported as length, or as content_filter when that is the stated reason, with its usage. The stream used to end with no finish reason, so auto-continuation never ran.
      • —@namzu/anthropic, @namzu/bedrock and @namzu/http's Anthropic dialect: a model_context_window_exceeded stop also carries finishDetail: 'context_window', so the runtime does not ask the model to continue a reply that filled the whole context window.
      • —@namzu/anthropic, @namzu/google and @namzu/openai (Codex): the list of sources a driver appends after a hosted search now carries contentOrigin: 'driver' on its stream chunk. The text is unchanged. Without the mark, a tool call the output limit cut off before that list would be reported as malformed.

      If you branch on finishReason, expect length or content_filter where you saw stop for these cases.

  3. v1.1.0

    Minor Changes

    • —4375e72: A provider-hosted web search now says what it searched for. StreamChunk.delta.hostedTool, and so the hosted_tool session event and the hosted.tool SSE event, may carry query (the search query), url (a page the provider opened instead of running a query) and results (how many sources it reported). Codex fills them from the search call's action, Anthropic from the search block's streamed input and its result list, Google from the grounding metadata. Each field is optional and absent when the provider does not say; nothing that already reads id, name and status changes. A custom driver can start emitting them whenever it knows them.

live

2 releases

  1. v3.0.0

    Major Changes

    • —567ada8: NamzuQueryConfig now passes toolsets: readonly Toolset[] to the SDK instead of tools: ToolRegistry. If you construct a NamzuModel, replace its tools field with toolsets and wrap tool definitions with toolset(source, definitions). This release requires @namzu/sdk >=48.0.0; use @namzu/live 2.x with SDK 44–47.
  2. v2.0.0

    Major Changes

    • —

      3e7a97b: Requires @namzu/sdk >=44.0.0 (was >=34.0.0). The SDK's run became a turn inside a session, and the ids @namzu/live reports from the model changed with it. Earlier @namzu/live versions break against SDK 44.

      • —LiveModelEvent: the usage, completed and cancelled events carry sessionId and turnId (the model's own session and turn — for NamzuModel, the SDK session and turn the query ran as) instead of runId.
      • —LiveSessionEvent: the usage and turn_completed events carry modelSessionId and modelTurnId instead of runId. Their turnId is still the live session's own turn.
      • —LiveTurnResult.runId is replaced by modelSessionId and modelTurnId.
      • —LiveErrorCode loses 'run_not_speakable' and gains 'turn_not_speakable': the error NamzuModel raises when the SDK turn did not complete with a stop reason it can speak. A caller that switches on err.code must match the new value.

      NamzuModel maps the SDK turn's result, status, tokenUsage and stopReason exactly as before. A createQueryParams callback passes turnConfig instead of runConfig, and an InMemorySessionLog as sessionLog where it used to pass an InMemoryRunStore as runStore.

      What to do: rename runId reads to modelTurnId (and modelSessionId where you need the session), match 'turn_not_speakable' where you matched 'run_not_speakable', and update the query params your callback builds.

lsp

1 release

  1. v0.3.3

    Patch Changes

    • —567ada8: Updated package examples and evaluation fixtures for the SDK's ToolRegistry removal. Browser, computer-use and LSP documentation now show toolsets; the evals use toolsets when they call the kernel. Provider changes are test fixture migrations only, with no provider runtime API change. Consumers that pass tools to the SDK should use toolsets: readonly Toolset[] and wrap their definitions with toolset(source, definitions).

namzu

1 release

  1. desktop-latestpre-release

    Namzu Desktop 0.2.1 — preview for Windows

    The desktop app for working with AI agents: projects, conversations, Pals with their own computer, and the Namzu terminal agent bundled in.

    If you already have Namzu Desktop installed, it updates itself to this version — no need to download again.

    Download (new install): Namzu-Setup-0.2.1.exe below (about 506 MB). Windows 10 or 11, 64-bit Intel or AMD.

    SHA-256: fbdb7b790ffedccee2c370c3cd030670989ef5e5181e9d5c266c8e8eb0e0aa31 To check it in PowerShell: Get-FileHash .\Namzu-Setup-0.2.1.exe

    What's new since 0.2.0

    • —A Pal now answers on the model you chose for it: the conversation follows the Pal's current model instead of the one it opened with, and the first message no longer pins the model shown.
    • —The left icon rail shows Spaces, Scheduled and Plugins as in progress — they do not open yet and return when that work lands.
    • —The menu bar reads like an app, not a browser: the View menu drops Reload, Force Reload and the developer tools, keeping zoom and full screen.
    • —The bundled terminal agent is now 36.1.1.
    • —A reliability fix for free-model access.

    This preview is not code-signed

    Windows warns you before it runs. That is expected for this preview.

    • —"Windows protected your PC": choose More info, then Run anyway.
    • —"Open File – Security Warning" with "Unknown Publisher": choose Run.

    Installing

    • —Installs for your user only. No administrator rights are needed. Adds Desktop and Start menu shortcuts.
    • —Takes a minute or two; the app opens by itself when it finishes.
    • —To start chatting, connect a model provider: paste an API key, or sign in with Claude Code or Codex and Namzu finds that sign-in.

    Pal computers

    A Pal can have its own Linux computer, which runs on Windows Subsystem for Linux (WSL). Chatting with a Pal works without a computer.

    Updates

    Namzu checks for new versions and installs them for you. You can change this in Settings ▸ Updates.

    Uninstalling

    Use Settings ▸ Apps ▸ Namzu. This removes the app and its shortcuts, and keeps your data: %APPDATA%\Namzu (app settings) and .namzu in your user folder (conversations, Pals, projects).

    Checked before release

    These exact bytes were downloaded from this page and their SHA-512 matches the update feed.

sandbox

9 releases

  1. v25.0.0

    Major Changes

    • —

      c82b951: Change the local Pal provider's default foreground ownership from strict command lifetime to computer lifetime. Rebuild the shipped local-computer image from this release to use the new default. To retain the previous behavior and continue using an existing strict image, set normalExitPolicy: 'strict' when calling createLocalVirtualComputerProvider. A successful foreground launcher can leave Blender or another application open without retiring the entire computer; the application remains owned by that guest allocation until the computer stops. An authenticated execution-reservation acknowledgment prevents an upgraded host from silently admitting this policy against an older image.

      Generic workers and registered background jobs retain strict process ownership. Failed launchers and timed-out or cancelled commands cannot transfer live descendants; if termination cannot be confirmed, the worker retires the allocation. A completed launcher does not claim its surviving application has terminated.

      CLI Pal computers use the same new default. Set NAMZU_PAL_COMPUTER_NORMAL_EXIT_POLICY=strict before starting the CLI or desktop host to retain their previous command lifetime and use an older strict image.

    • —

      7f5ac50: Update the bundled local Pal computer's normal Blender launcher from the distribution Blender 3.4 to checksum-pinned Blender 5.2.2 LTS and its Godot launcher from Godot 3.2 to checksum-pinned Godot 4.7.2. Godot 3 projects must be converted and tested before opening with the new default. Preserve the original project and use /usr/bin/godot3 for explicit legacy work; /usr/bin/blender also remains available. A custom older image retains its own application versions.

      Normal Godot launches use the guest's OpenGL compatibility renderer. Blender MCP startup addon update checks and telemetry are disabled for guest launches. These settings do not install a server or addon automatically. Rebuild the local image explicitly, then stop and reopen an idle Pal computer to select it; rebuilding a tag does not replace a running allocation or change saved guest files.

    Minor Changes

    • —

      7f5ac50: Add optional MCPClientConfig.transportFactory for caller-owned transports, preserving built-in transport configurations and the existing MCP protocol lifecycle without launching a host subprocess as fallback.

      Add optional Sandbox.openStdio with allocation-owned interactive guest pipes, byte streaming, confirmed process-group shutdown and explicit per-request device operation barriers. Idle services permit operator takeover; an unknown issued application operation permanently fences the allocation until computer stop and cannot be cleared by a late response or closing an external application's MCP server. The local Pal worker must be rebuilt alongside the runtime to advertise interactive support. Older workers refuse this capability before spawning; ordinary foreground, background, file and desktop APIs remain available.

      Keep quiet interactive services alive with bounded worker heartbeat frames, discarded by the matching runtime before application output and without changing request ownership or uncertain-effect barriers. Rebuild the worker and load the matching runtime together for the interactive protocol.

    • —

      0f5aa96: Ready-computer Pal prompts now default to general application work guidance: understand references and acceptance criteria, make reversible changes, observe and correct the actual result, and validate saved files and requested exports before delivery. SDK hosts that need the previous prompt behavior can pass workGuidance: 'basic' to buildPalSystemPrompt. This guidance shapes model behavior; it does not certify semantic correctness.

      The SDK read tool now refuses real PNG, JPEG and WebP bytes as text. Mount the optional view_image / createViewImageTool for saved-image inspection, or use a format-aware binary reader when pixels are not the desired result. getBuiltinTools() retains its previous tool roster. Saved artifact images provide no GUI screenshot authority.

      The CLI and desktop Pal runtime now explicitly mount view_image and import_reference_images. Image import is a reviewed file-write operation using only the current operator input and admitted guest. Original image bytes and manifests survive durable review; current plan, pause and control guards remain in force. Ordinary chat and unavailable-computer sessions do not acquire guest tool access.

      The local Pal sandbox adds authenticated, acknowledged bounded byte reads. Rebuild the local Pal image from this release to inspect saved images; an older worker fails explicitly before file contents are requested. Existing whole-file requests and generic worker defaults are unchanged.

      The Pal image also includes the standard file utility for format checks. This reports file type; it does not establish application or visual quality.

    • —

      2d5d18d: The Pal computer image is built in two stages. local-computer/Dockerfile now has base (the light computer: desktop, Chromium with the Namzu New Tab page, Terminal, Files, Text Editor and the guest worker, about 1.2 GB) and creative (built FROM base, adding Blender, GIMP, Inkscape, FreeCAD, KiCad and the other heavy applications). Building the Dockerfile without --target still produces the last stage, the same full image as before, so existing build commands are unaffected; pass --target base for the light one. Each stage lists on the home page, and checks at build time, only the applications it installed (app-catalog.cjs tags every application with its stage and prepare-home.cjs --stage records the built stage in /opt/namzu-computer/stage). The guest browser no longer shows a "Sign in to Chromium?" button (a managed policy turns browser sign-in, sync and promotions off), the Terminal tile on the home page uses the dock's terminal icon, and setuid and setgid bits are stripped from the image because nothing in the guest needs them.

      local-computer/build-image.mjs builds one stage with a chosen engine binary (docker, podman or wslc), saves it, compresses it (zstd by default; xz, gzip or plain tar also load directly with wslc load -i) and writes a manifest.json (revision, stage, imageRef, imageId, protocol, file, sha256, tarSha256, sizeBytes, compressedBytes, format) that the Windows installer carries beside the archive.

      The default image reference of the provider is unchanged: LOCAL_COMPUTER_IMAGE is still namzu-local-computer:1, the full image. The light base is tagged namzu-local-computer:2; a caller that wants it (the desktop app, from the image bundled in its installer) passes image: 'namzu-local-computer:2'. Nothing is renamed or removed.

    • —

      81d1721: The optional local computer image now includes 15 real application launchers, including Blender, FreeCAD, GIMP, Inkscape and LibreOffice Draw. Normal Chromium home and new tabs use the bundled Namzu New Tab page with a blank omnibox; navigated sites retain their normal address bar. Launchers use a guest-only native messaging host with a fixed extension origin and fixed application commands. The image activates its bundled New Tab extension and waits for its own startup tab to become ready, without rewriting browser policies or the saved profile. Application launches discard Chromium's disabled D-Bus session address. Kdenlive includes its video effects and uses a virtual audio driver so its editor can start without a host sound device.

      Rebuild the optional local image and restart the owning computer to use the new applications and New Tab page. Existing running images are unchanged, and the persistent home volume and browser profile are retained. The bundled Godot is Debian's Godot 3; no unavailable reference apps are advertised.

    • —

      ce52b90: Add optional host-only PalComputerScreenStream, PalEnvironmentLease.screenStream and PalRuntime.computerScreenStream(palId, generation) for observing an exact current Pal computer over read-only RFB. Existing providers remain compatible; providers without the optional capability explicitly refuse live observation. Keep allocation authorization private in the embedding host and enforce read-only observation server-side; the stream does not grant operator input authority.

      The CLI exposes the owning desktop ACP stream method with geometry and generation rechecks, and reuses initialized registry roots to avoid repeated Windows ACL subprocesses while retaining fresh Pal definitions and directory identity checks. The local computer image adds x11vnc and authenticated binary WebSocket transport. Rebuild the installed image explicitly and restart the owning computer to enable live observation; older images retain screenshot/input behavior and advertise no stream capability. Persistent workspace and browser profile volumes survive.

      The guest desktop includes a themed wallpaper, a real dock, Files, terminal and browser launchers, plus a local browser home page. Normal browser-window closure keeps the desktop alive; owned profile shutdown retains browser flush ordering.

    • —

      84a9472: Add optional exclusive operator control to local Pal computer leases. Hosts can transfer an idle guest desktop to human mouse/keyboard input while all agent sandbox and desktop mutation calls are fenced, then return control without starting a query. Pending work refuses transfer; unknown input or file-write outcomes require stopping the owned computer. A fresh agent screen capture is required after returning control. This local container boundary does not hide same-user guest files or credentials from arbitrary Pal commands.

    • —

      f7f48f9: Add createLocalVirtualComputerProvider for SDK Pals. Each Pal receives its own local Linux container desktop, visible Chromium browser, terminal command and file access, guest background processes with confirmed tree cancellation, and a persistent private volume. The provider requires an explicitly installed local Docker engine (the unchanged default) or an explicitly selected registered Windows WSL Podman machine, and a prebuilt Namzu computer image; it refuses remote engines and never installs, builds, or pulls images automatically. This is container isolation with a shared kernel, rather than a dedicated virtual machine. The Podman adapter verifies the local machine and pins owned loopback SSH forwards without changing machine settings or default connections; it has no per-container CPU, memory, or PID quota. Native Podman commands use a validated Windows profile directory even when an SDK caller starts in a WSL UNC directory. Custom command runners receive an optional cwd and must honor it. Graceful lease shutdown flushes Chromium's persistent profile before removing the guest. Existing sandbox providers remain available.

    • —

      4367edc: Add opt-in held keyboard input for a Pal's operator-controlled guest. Providers advertise heldKeyboard: true only after negotiating the owned guest's support; PalComputerInput then accepts session-scoped key_down, key_up and release_keys. Providers must release their tracked held keys before confirming returned Pal authority. Existing complete key taps and AI computer_use actions keep their behavior, and older local images do not advertise holds.

      The desktop captures a new keyboard lifetime for each focus session, forwards press and release events without replaying browser key repeats, and releases only that lifetime on focus/view loss. Cleanup retains exact Pal/generation and operator authority checks and cannot release another lifetime's keys. Rebuild the local Pal image to enable this capability; a runtime update alone retains the older image's tap input.

    • —

      ad2273f: @namzu/sandbox/local-virtual-computer/setup gains the pieces that need Windows' permission: runElevatedWslcSteps turns on Virtual Machine Platform (and, on Windows 10, the WSL optional component) and installs the pinned WSL package in one elevated Windows PowerShell process, so a plan shows Windows' permission prompt at most once; PINNED_WSL_PACKAGE names Microsoft's WSL 3.0.1.0 x64 package with its size and SHA-256; downloadWslPackage fetches it with resume, retry and a digest check; registerResumeAtSignIn and clearResumeAtSignIn ask Windows to continue setup once at the next sign-in. Nothing existing changes. These functions did not work before this release (they threw "not implemented yet"), so no caller relies on an earlier behaviour.

    • —

      f9f672b: Add the Windows Pal computer setup core under @namzu/sandbox/local-virtual-computer/setup: inspectWslcReadiness (reads policy, Virtual Machine Platform, WSL and wslc versions and the loaded image without starting a virtual machine unless probeSession is set), planWslcSetup, runWslcSetup (refuses a consent whose plan no longer matches the PC), loadWslcImage (checks the bundled archive's size and SHA-256 and the loaded image's id and protocol before recording it), stopIdleWslcSession and the restart record. Nothing existing changes; the download of the WSL package, the elevated process and the sign-in resume entry arrive separately.

    • —

      fc86c05: The local Pal computer provider accepts a third engine, engine: 'wslc', for Windows' own container CLI, with an optional wslcBinary path. Nothing changes for Docker and Podman: their command lines are byte for byte as before, and wslc is never chosen unless you ask for it.

      wslc cannot take --cap-drop, --security-opt, --read-only or --ipc, so a computer on this engine runs without those four controls; its boundary is the per-user virtual machine and the unprivileged guest user. Keep using Docker or Podman for a Pal that runs code you do not trust.

      LocalComputerSetupError is now exported and carries an optional code (engine-missing, engine-too-old, virtualization-off, blocked-by-policy, image-missing, sdk-mismatch) so an application can react without parsing the message. EngineDialect and LocalComputerSetupCode are exported as types. No existing export changes.

      Each readiness probe of a starting local Pal computer now times out after 3 seconds and is retried inside the startup deadline, so a published port that accepts a connection and never answers no longer stalls startup. On wslc, an image check that fails for a reason other than a missing image is reported as itself rather than as "not set up".

    Patch Changes

    • —

      a5dbeb2: Remove Chromium's separate stock New Tab footer and its Customize Chromium button from the local Pal browser home. The guest-only launcher disables NtpFooter; the native blank home omnibox, installed application launchers, normal website address bars and existing browser profile are preserved. The home header uses the same block-letter wordmark as the desktop and CLI, bundled as font-independent SVG geometry.

      Rebuild the installed local computer image and restart its owning computer to apply the change. Running containers keep their previous image; the persistent workspace and browser profile volume survives the restart.

    • —

      4367498: Setting up a Pal computer on Windows now names the step Windows is actually working on. While Windows asks for permission and turns Windows features on, the row reads "Turning on Windows features…" instead of "Installing Linux support…"; the next step is shown only once the elevated script reaches it. runWslcSetup announces just the first elevated step before the elevated call and reports every elevated step, in order, when the call ends, so a progress listener sees each step start and finish once.

      A setup step that Windows could not finish now has a Details line that says which step stopped and with which exit code ("Turning on Windows features stopped with exit code 1."), or that it did not finish when Windows gave no code, instead of repeating the main sentence. The detail text of a failed result from an elevated step changes accordingly; match on code, step and exitCode rather than on the text.

      Namzu Desktop no longer says the same thing twice. A failed setup, a setup that needs an administrator and a setup waiting for a restart are toasted only when the Pal computer row is not on screen saying it already.

    • —

      42ef5e4: Harden the Windows Pal computer setup (@namzu/sandbox/local-virtual-computer/setup). Every readiness probe now has its own time limit and a stuck one no longer hangs inspectWslcReadiness: it becomes an unknown fact, or a LocalComputerSetupError with a plain message when the state cannot be decided. An unexpected wslc failure during a session probe is now that error instead of image-missing. A PC that has not restarted since Virtual Machine Platform was turned on reads restart-pending (not virtualization-off), and an unreadable restart record reads restart-pending. The processor type comes from the PC, wslc is found under ProgramW6432, and the image-load estimate is shorter. WslcSetupHost gains keyExists; implement it if you pass a complete host (a partial host override is unaffected). A failed runWslcSetup result may carry exitCode, and its detail no longer includes the number. LocalCommandError gains an optional exitCode, and the provider's probe() returns setupCode beside reason. The engine-missing message now reads "Linux container support is not installed on this PC." (it no longer names wslc); match on code, not on the text.

      A registry key that Windows would not let the setup read is no longer taken for one that is not there. That applies to the sign-in resume entry and to the pending-restart markers, so a PC that only needs a restart is no longer told its virtualization is off. An unreadable restart record now ends with the next restart instead of never. With a session probe, the pending-restart markers are not consulted and 0x80370102 stays virtualization-off. The Pal computer probe reports a setup code only for a setup failure.

      A registry key whose parent is also absent is now reported absent instead of unknown, so a PC with virtualization off and no Windows Update key reaches virtualization-off. A policy read that times out is "could not check" rather than "not set". A bare wslc exit is read as a missing image only for status 1. The leading "Windows " is dropped from the Linux container support messages (engine-missing, engine-too-old and the catch-all); match on code, not on the text.

    • —

      9d422c6: runWslcSetup now answers { outcome: 'ready' } and records the image when the plan it was asked to run is gone only because the PC is already ready (the image was loaded before any record existed). It used to answer plan-changed and leave the record unwritten, so the next inspectWslcReadiness without a session probe still said image-missing. A caller that treated plan-changed as the only answer for that case now also sees ready; any other changed plan still answers plan-changed.

    • —

      eddf7aa: A Windows restart record that cannot be read and has no readable file date no longer makes the readiness inspection throw LocalComputerSetupError. The inspection now reports restart-pending with no restart field and no remaining steps. Callers that caught that error to show "delete restart.json" now receive a normal readiness result.

    • —

      a2a4b17: In a Kubernetes terminal, keystrokes typed right after a resize now reach the program after the new window size does. Before, the resize was applied by a separate process that could finish after the input was written, so a command such as stty size, or a full-screen program's first redraw, could report the old size, depending only on how busy the pod was. Input sent while no resize is pending is written immediately, as before.

      A resize that cannot be applied (the stty fails, or has not finished after 2 seconds) no longer holds typing back: the keystrokes queued behind it are written once it fails or the 2 seconds pass, so a stuck resize delays input by at most that long.

    • —

      Updated dependencies [dd7ee4b]

    • —

      Updated dependencies [4314f90]

    • —

      Updated dependencies [b8852a1]

    • —

      Updated dependencies [7f5ac50]

    • —

      Updated dependencies [7f5ac50]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [3727e2b]

    • —

      Updated dependencies [1af4531]

    • —

      Updated dependencies [efb7b06]

    • —

      Updated dependencies [84452ec]

    • —

      Updated dependencies [53aed2a]

    • —

      Updated dependencies [6194bda]

    • —

      Updated dependencies [301f5b3]

    • —

      Updated dependencies [6194bda]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [f7f48f9]

    • —

      Updated dependencies [9ef6506]

    • —

      Updated dependencies [56e846c]

    • —

      Updated dependencies [2057fd6]

    • —

      Updated dependencies [c8613e0]

    • —

      Updated dependencies [6f46da3]

    • —

      Updated dependencies [be9f8b6]

    • —

      Updated dependencies [c1027fb]

    • —

      Updated dependencies [24cb6b4]

    • —

      Updated dependencies [b6ea5ca]

    • —

      Updated dependencies [bb0ad2e]

    • —

      Updated dependencies [d5030ca]

    • —

      Updated dependencies [7f5ac50]

    • —

      Updated dependencies [7473449]

    • —

      Updated dependencies [6194bda]

    • —

      Updated dependencies [6f46da3]

    • —

      Updated dependencies [f4b7d1d]

    • —

      Updated dependencies [6ff961f]

    • —

      Updated dependencies [023224a]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [7f5ac50]

    • —

      Updated dependencies [6f46da3]

    • —

      Updated dependencies [5e2fb3a]

    • —

      Updated dependencies [0f5aa96]

    • —

      Updated dependencies [84a9472]

    • —

      Updated dependencies [c1027fb]

    • —

      Updated dependencies [d8d562e]

    • —

      Updated dependencies [53aed2a]

    • —

      Updated dependencies [ce52b90]

    • —

      Updated dependencies [815433f]

    • —

      Updated dependencies [4367edc]

    • —

      Updated dependencies [6ff961f]

    • —

      Updated dependencies [6f46da3]

    • —

      Updated dependencies [f7f48f9]

    • —

      Updated dependencies [846557b]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [53aed2a]

    • —

      Updated dependencies [12b5af9]

    • —

      Updated dependencies [e447a47]

    • —

      Updated dependencies [093d657]

    • —

      Updated dependencies [57c99b2]

    • —

      Updated dependencies [c590142]

    • —

      Updated dependencies [1fa60b9]

    • —

      Updated dependencies [f0aeb9b]

    • —

      Updated dependencies [5a0a1f7]

    • —

      Updated dependencies [3727e2b]

    • —

      Updated dependencies [e447a47]

    • —

      Updated dependencies [56d88dc]

    • —

      Updated dependencies [98e8469]

    • —

      Updated dependencies [bebe135]

    • —

      Updated dependencies [f7f48f9]

      • —@namzu/sdk@50.0.0
  2. v24.0.0

    Patch Changes

    • —b7d83ec: Sandbox unit tests now limit worker parallelism on many-core hosts. Kubernetes transport tests also use a stable unreachable address for an unchanged pod and accept an acquire deadline that expires during the initial API request before readiness polling starts. Published runtime behavior is unchanged.
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [2b1cbb5]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
    • —Updated dependencies [b7d83ec]
      • —@namzu/sdk@49.0.0
  3. v23.0.1

    Patch Changes

    • —25966a5: Workers started with NAMZU_SANDBOX_PORT=0 now report the actual bound port in their startup log, so callers can connect to the ephemeral listener.
  4. v23.0.0

    Patch Changes

    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [fbeac55]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
    • —Updated dependencies [567ada8]
      • —@namzu/sdk@48.0.0
  5. v21.0.0

    Patch Changes

    • —Updated dependencies [28102e1]
    • —Updated dependencies [82769f1]
    • —Updated dependencies [82769f1]
    • —Updated dependencies [82769f1]
    • —Updated dependencies [82769f1]
    • —Updated dependencies [28102e1]
    • —Updated dependencies [28102e1]
    • —Updated dependencies [28102e1]
    • —Updated dependencies [28102e1]
    • —Updated dependencies [28102e1]
    • —Updated dependencies [28102e1]
    • —Updated dependencies [82769f1]
    • —Updated dependencies [28102e1]
      • —@namzu/sdk@46.0.0
  6. v20.0.0

    Patch Changes

    • —Updated dependencies [2e2ea14]
    • —Updated dependencies [2e2ea14]
      • —@namzu/sdk@45.0.0
  7. v19.0.0

    Major Changes

    • —

      3e7a97b: No API change. The peer dependency on @namzu/sdk moves to the 44 major, because the SDK itself had a major release (its run became a turn inside a session). @namzu/sandbox reads no renamed field, but its peer range is published as a caret on the SDK version it was built with, so the SDK major takes it out of range and forces this bump.

      What to do: upgrade @namzu/sdk and @namzu/sandbox together. Nothing in your code changes.

    Minor Changes

    • —

      0a89444: New: egress profiles. defineEgressProfile({ name, hosts: [{ host, ports? }] }) validates one named host allowlist, and createSandboxProvider takes it as egressProfile instead of defaultEgress. On docker, runsc and firecracker a profile becomes deny-all (no hosts) or a static allowlist. Whatever a backend cannot honour is refused at construction with SandboxEgressProfileError: ports on firecracker, any profile on the ACI standby pool or beside defaultEgress, and a brokeredCredentials host the profile does not allow. On docker and runsc a live setNetworkPolicy under a profile may only name hosts the profile covers. On kubernetes, createSandboxProvider refuses egressProfile; put kubernetesEgressFromProfile(profile, { engine: 'cilium' }) in backend.egress instead, which also bounds workspaces and writes the profile label only with profileLabel: true.

      Nothing changes for a config without egressProfile.

    • —

      aee81a6: The docker and runsc egress proxy now enforces the ports of an egress profile, on the port it actually dials: an upgraded http://host/ and a CONNECT with no port are checked on 443. A host may use the union of the ports of every rule that matches it. The same check is available on EgressProxy as allowedPorts, and egressPortsForRules(profile.hosts) builds that option from a profile's rules with the same union rule.

      Rebuild egressProxyImage from packages/sandbox/egress-proxy/Dockerfile before using ports. A profile with ports sends the proxy a new configuration variable, NAMZU_EGRESS_PROXY_CONFIG_V2, and the backend first reads the image's ai.namzu.egress-proxy.config label with docker image inspect, refusing an image that does not declare version 2 (or that is not on the daemon: docker image inspect does not pull). Profiles without ports, and every policy without a profile, need nothing: the proxy gets the same configuration and argv as before.

    • —

      c366ca7: New: sandbox seeds. ensureSandboxSeed(sandbox, seed, { root }) makes the git repositories of a defineSandboxSeed({ name, repositories }) present under root inside any sandbox, through exec only. Every call checks each repository (origin URL, and that its pinned or recorded commit is an ancestor of HEAD; a ref changed since the clone, or a pin dropped, is drift unless the ref's commit is exactly HEAD) and clones only what is missing, so running it after each create or workspace resume costs one check when nothing changed. Drift is refused before anything is cloned (onDrift: 'report' records it instead), and nothing is ever deleted or re-cloned. root is required; on docker, use layout.scratch, not the outputs root. A repository directory may not sit inside another's, nor under .namzu/. URLs with a user name or password, ssh:// and git@host:path are refused, so no credential enters the guest. The guest needs sh, git, find, mkdir, mktemp, rm and GNU mv.

      Nothing changes for code that does not call it.

    Patch Changes

    • —251d815: On the docker backend, two overlapping setNetworkPolicy() calls on one sandbox could leave no egress proxy running while a caller was told its policy was in force, or resolve one call while the other call's policy was the one applied. Calls on one sandbox now run one at a time, in the order they were made, and each resolves only once its own policy is running. A call that asks for the allowlist already in force no longer restarts the proxy. No action is needed.
    • —Updated dependencies [8805360]
    • —Updated dependencies [9355755]
    • —Updated dependencies [755a81a]
    • —Updated dependencies [cb1f00c]
    • —Updated dependencies [933ba6d]
    • —Updated dependencies [3e7a97b]
    • —Updated dependencies [a84dc1c]
    • —Updated dependencies [b064cea]
    • —Updated dependencies [9238347]
    • —Updated dependencies [a14b013]
    • —Updated dependencies [3641102]
      • —@namzu/sdk@44.0.0
  8. v18.1.1

    Patch Changes

    • —

      006fdf3: A Kubernetes openTerminal no longer depends on a kernel build option to find the terminal it just started: where /proc/<pid>/task/<pid>/children does not exist, the guest agent discovers the tree by scanning /proc for the parent instead (#516, a follow-up to #512).

      What was wrong. findPtySlave walks /proc for the shell util-linux script forked, and its only way in was processChildren reading /proc/<pid>/task/<pid>/children. That file is CONFIG_PROC_CHILDREN — not part of the kernel's base /proc support — and the reporter's guest (a 6.6 mshv guest on a sandbox cluster) has no such entry at all: cat /proc/108/task/108/children answers No such file or directory for a script that is alive with a shell under it, and the entry is simply absent from /proc/<pid>/task/<pid>/ where it would sort between cgroup and clear_refs. So the walk's queue was empty on every one of its 200 attempts, probePtySlave was never called at all, and the tty_nr fallback #512 added — which is per pid the walk has already dequeued — was unreachable, because no pid was ever dequeued. The reported symptom is exactly what that reads as: terminal PTY slave did not appear (200 attempts, 1112ms; last children(): ENOENT; last fd/0: never read; last stat: never read; script: alive).

      What changed. parseProcessStat now also carries field 4, ppid, and one pass over /proc groups every live pid by its parent — the DIRECT-children relation the walk needs, from the same kernel, through a file (/proc/<pid>/stat) that is world-readable and gated on nothing. It is not the file's relation exactly, and the code says so rather than calling the two the same thing: field 4 is the parent's thread-group id, so the map is the thread-group-collapsed graph — WIDER than the file at a leader node, where a child forked by any thread of the group is listed under the leader and the file's children names the leader's own children only, and EMPTY at a non-leader tid, a node readdir('/proc') never yields and this walk never asks about. Neither difference takes a candidate away: the file's listing at a leader is a subset of the scan's, every node this walk visits is a process rather than a thread, and what the scan can add is a child the file's own walk would have missed. findPtySlave takes the file first and always, and reaches the scan only where a live pid answers ENOENT for it: ENOENT alone is ambiguous, and /proc/<pid> being present is what tells a kernel without the option from a process the kernel has released. A pid that is actually gone still ends the walk on the same observation it always did, and a refused read (EACCES, EMFILE) still takes the path it did before — no scan, and the errno reported as it was.

      The scan is discovery and nothing else: it cannot accept a candidate, only probePtySlave does that, so fd/0 → tty_nr order, the pid that comes back with the slave (the shell's, which the session teardown rests on), the breadth-first order and the early stop are all unchanged. One /proc pass is shared by every pid an attempt visits, so a walk over K candidates costs one pass per 5ms poll rather than K of them, and on any kernel that HAS the file — every guest this agent has been measured in — the scan never runs at all. The pass reads each pid's stat synchronously, which is the one reader in the agent that does: measured, readFileSync answers in ~12µs per pid where the promisified read costs ~276µs, so a sixty-process guest spends ~0.7ms per pass rather than ~15ms, and what the synchronous call costs is syscall time and nothing else — procfs builds that line in kernel memory, so there is no device behind it to block on. The listing that precedes the loop stays asynchronous, being one call per pass rather than one per pid.

      That synchronous read is also why the pass is bounded, not merely measured, and the bound is the reason this is worth a second look: the agent is single-threaded and serves every terminal, exec, stream and healthz on that one event loop, and on the kernel family this fix exists for the scan runs on EVERY attempt — so a pass that never yielded would hold all of them for as long as the guest has processes, in one lump per attempt per terminal. The loop hands the loop back every 64 reads (setImmediate), which caps the uninterrupted stretch at 64 reads rather than at the size of the machine. Measured by driving the real function — extracted from the shipping file — over a /proc holding a copy of sleep per spawned process: the longest gap between two yields, which is the longest the loop went without a turn, was 0.97-1.05ms at 4041 live pids and 0.69-0.93ms at 1041, where the same passes with the yield removed held the loop for 40.1-42.9ms and 10.6-11.8ms. The pass pays for that in event-loop turns and nothing else: 41.7ms → 45.1ms at 4041 pids (median of five) for 62 turns, and at 41 pids no yield fires at all, the pass being shorter than the stretch it is allowed.

      Where the scan does run, the failure message now says so (last children(): ENOENT, discovered by a /proc scan) instead of leaving ENOENT to read as a broken walk, which is the reading that cost the reporter a cluster to correct. A scan whose own readdir('/proc') FAILED says that instead — last children(): ENOENT, and the /proc scan that stands in for it could not list /proc: EACCES — because a listing that never happened must not be reported as a scan that ran and came back empty, which would be the same misreading one level down.

      What is not proven. That the reporter's guest answers ready with this agent. Not pinned by a test, and deliberately: a timing assertion on the yield would be flaky, and the suite has no seam for a denied /proc listing. Both are verified out of suite instead — the stall table above by measurement, the failed-listing message by driving discoverChildren with a faked fs — which is why those numbers are quoted here rather than asserted in the file. The absence cannot be produced here — this machine's kernel has the option, and the guest is a cluster — so the regression test arranges it the way this suite arranges what it cannot reproduce: a copy of the shipped agent with its one children read pointed at a path that is not there, patched by anchored edits that assert each anchor occurs exactly once, run over a real loopback socket against real processes (packages/sandbox/src/backends/kubernetes/__tests__/terminal-pty-slave.test.ts). Against the pre-fix source those cases fail with the reporter's own line, field for field — terminal PTY slave did not appear (200 attempts, 1043ms; last children(): ENOENT; last fd/0: never read; last stat: never read; script: alive) — and against this one they answer ready, resize the shell's real pty and end a SIGHUP-immune background job through the shell's session. The same file pins the cost decision from the other side, asserting with the shipped agent that a walk which CAN read the file never runs the scan at all — and the two cases that must scan assert the counter saw theirs, so a rename cannot turn that bound into a vacuous pass.

      Redeploy the guest image to get it. agent/agent.cjs is baked into the guest images (k8s/Dockerfile COPYs it), and npm pack @namzu/sandbox@18.1.0 --dry-run confirms it is not in the published tarball — 0 entries. No public type, export, wire shape or documented default changes here, which is why this is a patch.

  9. v18.1.0

    Minor Changes

    • —

      ef570fe: createSandboxProvider now forwards brokeredCredentials, and ContainerBackendConfig carries the field.

      What this makes possible is the thing the container tier's egress boundary was built for. A host can now name credentials the sandbox never holds: the sandbox carries a placeholder, and the real value is stamped onto outbound requests at the egress proxy, so a prompt injection that exfiltrates the sandbox's environment does not carry the token with it. Until now the field could only be read from DockerBackendInternalConfig, which is not part of this package's public surface, so brokering was reachable only by calling buildDockerBackend directly — not by building a provider the documented way.

      Nothing existing changes behaviour. The field is optional and unset by default, and it is forwarded in the guarded style its neighbours use, so a config that does not set it produces a backend config with no such key at all rather than one with an explicit undefined; egressProxyContainerConfig reads either as "no credentials to stamp". A provider built without the field starts exactly the proxy container it started before. The one caller whose behaviour does change is the one that was passing the field through a type cast: it now takes effect.

      Set it where it can be honoured, and not elsewhere. The field is on ContainerBackendConfig — the docker and gVisor (runsc) tier — because that is the only tier with an egress proxy to stamp a credential at. The microVM tier hands its allowlist to its guest orchestrator and the kubernetes tier writes NetworkPolicy/CiliumNetworkPolicy; neither has a proxy, so the field is not on their configs. Within the container tier it is applied only where a proxy is actually started, which is a static or resolver policy with an egressProxyImage: deny-all and allow-all run no proxy, and credentials set beside either are never applied.

      Each entry names one host (host, matched by the allowlist's own rules so .internal.example covers subdomains), the header to set, and the value. The value leaves this process on its way to the proxy container, so anything with access to the docker daemon can read it — treat daemon access as credential access. See docs/sdk/sandbox-egress.md.

    Patch Changes

    • —

      a52727a: The Kubernetes/Firecracker guest agent now finds a terminal's PTY slave without readlink('/proc/<pid>/fd/0'), which was the one probe it had and was answered by a kernel permission check it never declared (#512).

      What was wrong. handleTerminal spawns util-linux script, then walks /proc for the shell script forked, looking for a candidate whose fd 0 readlinks to /dev/pts/<n>. That path is used for three things — the stty -F resize, the pid whose kernel session a teardown signals, and never mistaking a pipe for a terminal — and the probe behind it is ptrace_may_access, which refuses a reader whose credentials do not match the target's. /proc/<pid>/fd is dr-x------ owned by the target; /proc/<pid>/stat is world readable.

      Measured in a real kind cluster: with the reader's uid differing from the target's, readlink fails EACCES every attempt for the whole one-second budget while script is still alive, and the terminal answers {"type":"error","error":"terminal PTY slave did not appear"} — the reported symptom, reproduced byte for byte. With a root reader and a uid-1001 shell: fd/0 = <EACCES> while /proc/<pid>/stat reads and names 136:3 -> /dev/pts/3, which is exactly the slave root sees through fd/0 for the same live process.

      What changed. readProcessStat now also carries field 7, tty_nr, decoded with the kernel's own new_decode_dev (major 136 is devpts). findPtySlave still asks fd/0 FIRST and behaves identically wherever it can answer — the fallback is only reached when the readlink fails or answers with something that is not a PTY slave, and the walk still returns the first candidate in breadth-first order, so the pid it reports is the one the fd 0 probe alone returned whenever that probe can answer at all. It also stops as soon as /proc/<script> is gone rather than spending the remaining budget on a released pid — and a terminal whose script has already exited is answered with its own exit frame instead of with this error, since the walk's failure there is a claim about a terminal that had already ended. The failure names what it could not read, and names only what it read: terminal PTY slave did not appear (200 attempts, 1002ms; last children(): EACCES; last fd/0: EACCES; last stat: EACCES; script: alive). Every field is the LAST OBSERVATION of the probe it names rather than the last error that probe ever had, and script's own line is a third value — alive, gone, or the errno that stopped it being read, so a host is never told script alive by a read that never happened. The leading phrase is unchanged, so anything matching on it still matches.

      What is not proven. Which condition in the reporter's cluster made the readlink fail for the whole budget. The uid-mismatch shape reproduced here looks impossible in the shipped image, where the agent, script and the shell are all uid 1001, and no run of the unmodified agent in a kind cluster (task-pod or workspace-pod shape) reproduced the failure. This release removes the dependency and makes the failure legible; it does not claim to have found the trigger.

      Redeploy the guest image to get it. agent/agent.cjs is baked into the guest images (k8s/Dockerfile, packages/sandbox/agent/), not shipped in the published tarball, so a deployment keeps the old agent until its image is rebuilt. agent.cjs, entrypoint.sh, the Dockerfile and the worker are all guest/deployment files outside the published tarball: no public type, export, wire shape, or documented default changes here, which is why this is a patch.

telemetry

2 releases

  1. v3.0.0

    Major Changes

    • —

      3e7a97b: Requires @namzu/sdk >=44.0.0 (was >=1.0.0), and the span, metric and attribute names the kernel emits changed with it. Dashboards, alerts and queries that filter on the old names stop matching.

      • —agentRunSpanName (from @namzu/telemetry/attributes) is agentTurnSpanName, and the span it names is namzu.agent.turn <name> (was namzu.agent.run <name>).
      • —The histogram namzu.run.duration is namzu.turn.duration.
      • —The attributes namzu.run.id, namzu.run.status and namzu.run.parent_id (NAMZU.RUN_ID, NAMZU.RUN_STATUS, NAMZU.RUN_PARENT_ID) are removed. Spans carry namzu.turn.id (NAMZU.TURN_ID), namzu.turn.status (NAMZU.TURN_STATUS) and, on a child session, namzu.session.parent_id (NAMZU.SESSION_PARENT_ID).
      • —Every span the kernel opens for a turn — the turn's root span, each namzu.agent.iteration span, each chat <model> span and each namzu.tool.execute span — now carries gen_ai.conversation.id (GENAI.CONVERSATION_ID), set to the Namzu session id, and namzu.turn.id, so one conversation's turns group together and any span finds its turn. A tool a host calls directly, outside a turn, gets them only when its ToolContext names a session and a turn.
      • —Session export forwards the SDK's renamed events: turn_* instead of run_* and child_session_* instead of subsession_*, each with sessionId and, inside a turn, turnId instead of runId.

      What to do: upgrade both packages together, then replace namzu.run.* with namzu.turn.* (and namzu.run.parent_id with namzu.session.parent_id) in every dashboard and query.

  2. v2.2.1

    Patch Changes

    • —

      8d5223b: Nothing a consumer installs or calls changes, and that is the whole of this release. vitest moves from ^3.2.6 to ^4.1.11 in the devDependencies of all nineteen packages that declared it, and @vitest/coverage-v8 moves with it in @namzu/sdk. Every occurrence is a devDependency — checked, not assumed — so dependencies, peerDependencies, exports, types, defaults and the wire shape are untouched, and the published tarballs differ from the previous release only in package.json#devDependencies.

      The reason is a security fix with no 3.x backport. GHSA-82fw-gwwq-j7x9 ("Path Traversal / Arbitrary File Read via @vitest/mocker Redirect Mock") covers vitest and @vitest/mocker from 2.1.0 up to 4.1.11, so ^3.2.6 can only be resolved by leaving the 3.x line. 4.1.11 is the first patched release and is what the lockfile now resolves for both.

      What this costs anyone who works on the repository rather than with it: the upgrade was not a version bump. Vitest 4 changed test discovery, coverage configuration, mock construction and reporter output, and each of those broke something here that had to be migrated rather than worked around. Those fixes are all under __tests__/, vitest.config.ts files and scripts/, none of which is published, which is why this is a patch and not a major.

      You do not need to do anything. If you pin vitest yourself to run this project's own suites, note that the config files it ships are now written for >= 4.1.11 and will not run under 3.x.

zen

13 releases

  1. v4.0.2

    Patch Changes

    • —

      d328c1b: Serve anonymous free-model calls from https://opencode.ai/inference/v1 instead of the configured https://opencode.ai/zen/v1.

      The free-tier gateway answers 403 FreeTierError — "OpenCode's free tier can only be used from within OpenCode" — to anonymous traffic on /zen/v1, and does so however faithfully the request is shaped. Measured on cat-jam-free with the OpenCode-shaped identity headers, the four injected tool names and a session and request id in the client's own format, both against the same session in the same minute: the same request is 403 on /zen/v1/chat/completions and 200 on /inference/v1/chat/completions. The request id's random half is not what the gateway reads — flipping it, keeping the timestamp half fresh or stale, and re-serialising the body all leave the verdict unchanged — only the route decides.

      To upgrade and keep working, nothing is required: anonymous free-model calls move to /inference by themselves, and every keyed call stays on ZEN_BASE_URL exactly as before. A caller-owned baseURL proxy is likewise untouched, and only the chat route is reachable anonymously — the Anthropic and Responses routes answer 401 Missing API key on either host, as they did before. Verified live on cat-jam-free: a plain turn and a bash tool round both complete.

  2. v4.0.1

    Patch Changes

    • —

      4a6ec17: Refuse to claim strict tool input generation where the wire would refuse the request.

      edit and write set enforceModelInput, and the Zen driver answered that with strict: true for both on the chat-class wires. The Zen free-tier gateway enforces the strict subset's structural rule itself — every object that declares properties must list all of them in required — and refuses the WHOLE request when one tool fails, so edit's optional fields (a find-and-replace OR an insertion) took every other tool in the call down with it. cat-jam-free is the model that enforces; the symptom was Invalid schema for function 'edit'.

      • —@namzu/sdk gains acceptsStrictSchema(schema), the wider question a driver asks before claiming strict: true on one request: the keyword deny-list that findStrictSchemaViolations already runs, plus the structural rules, plus a guard for a tool that declared no schema at all (the deny-list walks nothing in undefined and finds nothing). It is additive; findStrictSchemaViolations and assertStrictSchema keep the registration-time job they had.
      • —@namzu/zen now sends strict: acceptsStrictSchema(...) for a tool named in enforceToolInputSchema, so write keeps the guarantee and edit sends strict: false. Responses still sends strict: false for every tool, as it did. Nothing about a schema changes, and Namzu validates tool inputs before execution either way.
      • —edit's insertLine union branch { const: 'end' } states no type, which the strict dialect has no spelling for; it is now { type: 'string', const: 'end' }. The validator reports any union branch that never states a type.

      What breaks. A caller that relied on Zen sending strict: true for edit loses constrained decoding for that one tool on the chat-class wires. To keep it, the schema would have to require every field and make the optional ones nullable, which changes what edit accepts on every provider.

  3. v4.0.0

    Major Changes

    • —

      2395a85: Breaking: fledge-alpha-free leaves the bundled catalogue. Zen no longer documents or serves it, so findZenModel('zen', 'fledge-alpha-free') and getZenModels('zen') no longer return it, and a request naming it fails as an unknown model. Pick another free Zen model, for example step-5-preview-free or mimo-v2.6-flash-free.

    • —

      4367edc: Remove the default 4096-token per-call output ceiling when maxTokens is omitted. Requests now use the known model's advertised output ceiling instead of selecting a smaller service default by omitting the wire field. Unknown Chat, Responses and Google models without catalogue metadata leave the field omitted. Messages requires max_tokens, so an unknown Messages model without a known ceiling requires explicit maxTokens or model catalogue metadata. Explicit limits remain unchanged and include manual thinking tokens on Messages.

      Calls that relied on the previous default can produce longer output and consume more tokens. Set maxTokens: 4096 explicitly, or maxResponseTokens: 4096 on SDK turns, to preserve the previous behavior. Service and model limits still apply. This fixes Pal requests where Space Bunny spent the 4096-token allowance on reasoning and stopped before using tools, even with unlimited SDK turn guards.

    • —

      0de3571: Breaking: Zen Go's space-bunny-free leaves the bundled catalogue. Go no longer serves that id, so findZenModel('go', 'space-bunny-free') and getZenModels('go') no longer return it, and a Go request naming it fails as an unknown model. On Go, use space-bunny, which the snapshot now carries. Zen's own space-bunny-free is unchanged.

      Zen now carries models that the service routes, prices and serves but that models.dev has not described yet, where it used to omit them. These models get conservative limits: 65,536 context, 8,192 output, text input, tool use on and no effort levels. They also get a new optional ZenModel.limitsVerified: false. Every other model leaves the field out, so existing code reads the same shape. Once models.dev publishes a model's limits, the entry uses them and the flag goes away. Tool use is assumed for these models, so one that cannot call tools fails when it runs. Models are still never carried when their price cannot be read or the service does not serve them, and a missing price never becomes zero.

      The free rule changed earlier without a changeset. Any Zen model priced at zero in the active catalogue is now offered without a key through the experimental request path, where previously only a fixed list of eight ids was. An id outside the old list used to be refused without a key; now it can be selected, though the gateway may still refuse it.

      The bundled snapshot adds mimo-v2.6-flash-free, exo-free, fledge-alpha-free, ling-3.1-flash-free, grok-4.7, gpt-6.1-sol, claude-sonnet-5-5 and claude-haiku-5-5 on Zen. On Go it adds MiMo V2.6 Flash and Pro, claude-haiku-5-5 and space-bunny.

      parseZenCatalogue accepts limitsVerified: false in a stored copy and rejects any other value. An older @namzu/zen rejects any stored copy that contains the field and falls back to its bundled snapshot.

    Minor Changes

    • —134932b: The bundled catalogue adds step-5-preview-free on Zen and on Zen Go, which both services now document and serve. Nothing else changes.

    Patch Changes

    • —

      4367edc: Deliver screenshot and saved-image tool results to Chat Completions models as real, attributed image content after the complete tool-result batch. Previously these results failed local history conversion, despite the driver's advertised image capability, stopping Space Bunny Pal work after its first screenshot.

      Preserve original tool IDs, text and failure markers; leave the durable journal and operator messages unchanged. Images with persisted delivery omissions stay omitted, and text-only histories keep their previous request shape. The model must support image input; chat tool documents remain explicitly unsupported.

    • —

      01b926c: Keyless Zen requests work again. The free-tier gateway now refuses every model that does not carry OpenCode's client identity — including space-bunny-free, which used to answer — and the driver was sending that identity only for a hand-picked verified path, so an anonymous call to big-pickle and to the new free models returned FreeTierError.

      Anonymous calls to the official Zen endpoint now all take the same path: an OpenCode User-Agent at 1.18 or newer (resolved from OPENCODE_UA, then an installed opencode --version, then a 1.18.35 floor), a descending ses_ session id, the client and project headers, and the four tool names bash, glob, grep and read in the request body. The tool names are injected only where the caller registered none; a caller that has tools keeps its own set and its own tool_choice. Namzu's own attribution headers still travel on credentialed calls, on Zen Go, on a caller-owned baseURL, and on catalogue reads.

      Nothing in the public surface changes. Pass OPENCODE_UA if you want the advertised version pinned; without it the driver probes an installed binary once per process and falls back.

  4. v3.1.0

    Minor Changes

    • —f0505b5: Anonymous Zen users can now select all eight bundled free models instead of only Space Bunny. The seven previously blocked models use an experimental OpenCode-compatible request identity on the official Zen endpoint; Space Bunny keeps its verified direct request path, and keyed Zen and Zen Go requests are unchanged. Zen may still reject an experimental model or route, so a free catalogue entry is not a guarantee of service. The CLI labels this access as experimental and keeps Space Bunny as its default.
  5. v3.0.0

    Major Changes

    • —

      29b142f: Zen Go no longer includes glm-5.1, qwen3.6-plus, or qwen3.7-max in its bundled supported model roster. The service still lists these IDs, but its current documentation provides no route or price for them. Applications relying on these IDs should select a documented model or, if they have independently verified the wire, configure ZenGoProvider with an explicit protocol. The CLI no longer offers these reviewed omissions.

      The default anonymous Zen model changes from muse-spark-1.3-contributor-free to space-bunny-free in both the SDK and CLI. Callers that need the old choice must pass model: 'muse-spark-1.3-contributor-free' explicitly and try credentialed Zen access, subject to gateway admission. The gateway currently rejects direct keyless Namzu requests to Muse and six other documented free models with HTTP 403; only Space Bunny Free completed a live keyless text and tool-continuation test. Anonymous model discovery now admits only that verified model while retaining the other free models for credentialed use.

      Zen and Zen Go now follow the reference client's route overrides for DeepSeek V4 Flash and MiniMax, handle Muse Spark reasoning continuation without replaying gateway-bound ciphertext, and read the current Zen Go price table when refreshing the catalogue. These keyed routes have wire-fixture coverage but were not live-tested with an account. The CLI distinguishes model listing from verified inference and explains the gateway's specific HTTP 403 free-tier refusal.

  6. v2.5.2

    Patch Changes

    • —567ada8: Updated package examples and evaluation fixtures for the SDK's ToolRegistry removal. Browser, computer-use and LSP documentation now show toolsets; the evals use toolsets when they call the kernel. Provider changes are test fixture migrations only, with no provider runtime API change. Consumers that pass tools to the SDK should use toolsets: readonly Toolset[] and wrap their definitions with toolset(source, definitions).
  7. v2.5.0

    Minor Changes

    • —

      ab9108b: Derive the Zen and Go model catalogue at run time, opt-in. The new @namzu/zen/catalogue subpath exports fetchZenCatalogue(), which reads the same five sources the bundled snapshot is generated from (both documentation pages, models.dev and both /models answers) with a deadline and byte limit per source, and returns a whole validated catalogue plus a report, or rejects. It never returns a partial list. buildZenCatalogue() does the same derivation from texts you fetched yourself, and parseZenCatalogue() re-admits a stored copy strictly. A derivation is held to the same checks as a stored copy, so a result is never one its own saved copy would fail to read back.

      Pass the result as the new ZenConfig.catalogue, or a function returning it, and the provider checks it before the bundled snapshot for routing, anonymous admission, listing, context windows and effort levels. With a runtime catalogue, a served id that no source gives a wire format for is listed as <id> (no known wire format) and can be called only with an explicit protocol; the bundled snapshot's entry for such an id, if it has one, is not used. A model name carrying a control, format or line-separator character is refused, in a derivation and in a stored copy.

      Nothing changes unless you opt in. Importing the package, constructing a provider and getZenModels/findZenModel still read the bundled snapshot and make no network request. Without catalogue, listModels() returns what it did before. ZEN_OMITTED_MODELS (the reviewed omissions the snapshot was generated under) is now exported from @namzu/zen and @namzu/zen/models. The bundled roster is unchanged: 61 Zen and 28 Go models.

  8. v2.4.0

    Minor Changes

    • —3e43fc2: The Go catalogue carries grok-4.7 on the Responses protocol, as the Go page routes it. Four ids that both pages route but models.dev does not describe yet — zen/grok-4.7, zen/mimo-v2.6-flash-free, go/mimo-v2.6-flash and go/mimo-v2.6-pro — are not carried, because their limits cannot be derived. A caller that needs one names the protocol and limits itself.

    Patch Changes

    • —3641102: On the Messages protocol, a cache request now places block-level breakpoints — after the static system text, and on the last message before request-only context — instead of the request-level cacheControl option, which the native adapter sends as a top-level cache_control. That is Anthropic's automatic caching, which puts its breakpoint on the last block: the request-only context at the tail, which the next request replaces, so no later request read the cached history. Other protocols are unchanged.
  9. v2.3.0

    Minor Changes

    • —

      54429af: muse-spark-1.3 no longer advertises the max reasoning effort.

      Upstream dropped it from the model's own menu, so a regeneration writes ['minimal', 'low', 'medium', 'high', 'xhigh'] where it wrote six values. A caller that passes effort: 'max' for this model is now refused with the refusal the driver has always given for an unadvertised level — "The selected model does not advertise this reasoning effort level." Pass a lower level for this model, or use a model whose menu still carries max.

      Why minor rather than major. The catalogue's own rule, in Zen and Zen Go, is that a removal is major because a carried id stops resolving; an addition or a repricing is minor. This model still resolves — getZenModels() and findZenModel() return it unchanged, its id, protocol, prices, context window and tool support are identical, and the public ReasoningEffort union in @namzu/sdk still carries max for the models that do advertise it. The page's rule has two buckets, and this is the one that is not a removal of a carried id: an addition, a repricing, or any other change that leaves a carried id resolving is minor. Nothing was removed from the API and no default changed.

      No code changed. The only edit is the generated catalogue, regenerated rather than hand-edited: scripts/generate-zen-models.mjs derives it from the live documents and models.dev, which is also what turned this into a red gate before anyone noticed by hand.

  10. v2.2.0

    Minor Changes

    • —

      7b3323f: The Zen catalogue gains two models. They arrive in the same release as a generator fix that had to come first: the Zen page had started publishing a route row this script refused to read, and while that row stood the catalogue could not be checked against upstream at all.

      New on Zen, both selectable once your key admits them: qwen3.8-flash on the Messages wire at $0.15/$0.47 per million tokens, and deepseek-v4.1-flash on Chat Completions at $0.30/$1.20. Both were already carried on the Go service; upstream has since added them to the Zen route table as well, which is what made this roster move. Nothing was removed, no price changed, and getZenModels, findZenModel, ZenModel and ZenProtocol keep their signatures and types, so the bump is minor.

      Two ids stay uncarried, and no longer stop the catalogue being checked. The Zen page routes jev-1.13 and jev-1.13-free on an endpoint whose AI SDK package column carries a dash rather than a package: the page names the model, its id and its endpoint, and no source states a wire for it — models.dev holds a name and limits for both and no package either. That row used to stop the generator as a page that had changed shape, which is the defect this release fixes: a row stating no wire is now read as exactly that, and both ids are omitted by name in src/models.review.json instead. The service does serve both, so if you call one of them the wire is yours to state — there is none to carry them on, which is the whole reason they are omitted — and that needs a real credential, because anonymous admission is only claimed for models the catalogue carries. The page's free-model list names jev-1.13-free; that is a statement upstream makes about it, not an anonymous access this driver grants.

  11. v2.1.1

    Patch Changes

    • —

      8d5223b: Nothing a consumer installs or calls changes, and that is the whole of this release. vitest moves from ^3.2.6 to ^4.1.11 in the devDependencies of all nineteen packages that declared it, and @vitest/coverage-v8 moves with it in @namzu/sdk. Every occurrence is a devDependency — checked, not assumed — so dependencies, peerDependencies, exports, types, defaults and the wire shape are untouched, and the published tarballs differ from the previous release only in package.json#devDependencies.

      The reason is a security fix with no 3.x backport. GHSA-82fw-gwwq-j7x9 ("Path Traversal / Arbitrary File Read via @vitest/mocker Redirect Mock") covers vitest and @vitest/mocker from 2.1.0 up to 4.1.11, so ^3.2.6 can only be resolved by leaving the 3.x line. 4.1.11 is the first patched release and is what the lockfile now resolves for both.

      What this costs anyone who works on the repository rather than with it: the upgrade was not a version bump. Vitest 4 changed test discovery, coverage configuration, mock construction and reporter output, and each of those broke something here that had to be migrated rather than worked around. Those fixes are all under __tests__/, vitest.config.ts files and scripts/, none of which is published, which is why this is a patch and not a major.

      You do not need to do anything. If you pin vitest yourself to run this project's own suites, note that the config files it ships are now written for >= 4.1.11 and will not run under 3.x.

  12. v2.1.0

    Minor Changes

    • —

      a38d843: The catalogue was refreshed from its sources. The roster is unchanged — 59 Zen and 27 Go — so nothing a caller routes today stops resolving.

      gpt-5.6-sol is repriced: input 2.0 → 4.0, output 10.0 → 20.0 per million tokens. The page had carried a 50% discount through 2026-09-18 and now states the full tier. A consumer that estimates cost from the bundled prices sees the increase here rather than at the invoice.

      Two omissions were retired. zen/union-alpha and go/union-alpha were recorded as omissions on 2026-09-18 after models.dev deleted their entry — the catalogue derives limits, tool support and modalities from there and never invents them. Hours later the service stopped serving the id and both pages stopped routing it, so the omission no longer applied and the stale rule dropped it. Neither id was ever carried, so this changes no caller's roster; it is recorded because a decision in models.review.json is re-examined on every run rather than left standing.

  13. v2.0.0

    Major Changes

    • —

      565ffa7: The Zen catalogue tracks upstream again, and the roster it returns has moved. The incompatible part is one removal, not the refresh: omen-alpha is gone from the Go roster, so a caller that pins it by name now gets a refusal where it used to get a request.

      omen-alpha is still served. The service answers it on Go exactly as it answers a model that is carried — 401 AuthError "Missing API key", the same answer kimi-k3 gives, where an id it does not know answers 401 ModelError "Model no-such-model-xyz is not supported". What changed is that the Go page no longer ROUTES it, and a route is the one thing that cannot be derived: the service's own /models answer carries an id and nothing about how to call it. So the catalogue stops claiming a wire for it. If you depend on it, you can keep calling it by supplying the wire yourself — new ZenGoProvider({ apiKey, model: 'omen-alpha', protocol: 'chat' }) — and the driver will route it, as client.ts documents for any id whose protocol the host states explicitly. That is the only way to keep it, and it is a statement about a model nothing upstream documents any more.

      One model is new, and selectable once your key admits it: deepseek-v4.1-flash on Go (Chat Completions, $0.15/$0.60 per million off-peak).

      union-alpha is not in this catalogue, and not for our reasons. It was added to the roster while this release was being prepared, and upstream removed it before the release shipped: models.dev — where the catalogue derives limits, tool support, modalities and effort options — deleted its entry on 2026-09-18, from every provider that carried it and not only from ours. Nothing about the service changed, and nothing here is invented to compensate: a model whose limits cannot be derived is omitted rather than carried with made-up numbers. You lose nothing by it. The published @namzu/zen 1.0.2 does not carry this id either, and the roster this release ships is back to 86 models (59 Zen, 27 Go); if the free-model list in the docs made you expect it, that is the explanation. Both services still serve it and both pages still route it on the Messages wire, so you can call it the same way as any id whose wire the host supplies — new ZenProvider({ apiKey, model: 'union-alpha', protocol: 'messages' }) — which needs a real credential, since anonymous admission is only claimed for models the catalogue carries. It comes back on its own, at minor, when models.dev restores the entry.

      Two Go prices are lower, because upstream repriced them: deepseek-v4-flash and deepseek-v4-flash-vision-exp move from $0.22/$0.66 to $0.15/$0.60 per million input/output. Reported cost and anything you set costLimitUsd to are affected downward. These are estimates for SDK accounting, not invoices.

      Everything else is unchanged: getZenModels, findZenModel, ZenModel and ZenProtocol keep their signatures and types, and every model that was already carried keeps its protocol.

      For future refreshes: additions and repricing are minor, because a consumer's code keeps working. A refresh that DROPS an id is major, for the reason above — a pin on that name stops resolving — and it should always say which id and why, as this one does. The scheduled refresh derives that intent from the generator's own report rather than from anybody's judgement, and puts it in the pull request it opens, so a refresh cannot be merged as a change nobody is ever asked to release.

      src/models.ts is now generated by node scripts/generate-zen-models.mjs from the two service documentation pages, from models.dev, and from the two services' own /models answers, rather than a snapshot pinned to a revision. src/models.review.json records the curation decisions, and the CI gate Zen catalogue matches its source fails, naming the model, whenever a service documents OR SERVES one the catalogue neither carries nor omits. That second half is why the roster moved: the service answers 71 ids on Zen and 38 on Go, and twelve of them — three on Zen, nine on Go, the free deepseek-v4-flash-free and muse-spark-1.2-contributor-free among them — are served while appearing on no page. None of the twelve is carried, because a model's wire is stated on a page and the /models answer carries an id and nothing else; each is now omitted by name with a reason, so listModels() dropping it is a recorded decision instead of a model nobody noticed. The gate also reads those two answers live, so a source it cannot reach is reported as a failure rather than passing quietly — the check runs in CI and needs the network to opencode.ai, which it has.

      The catalogue does not update itself, and nothing here says it does. A refresh is one command: it fetches, rewrites the module if the roster moved, and prints what it added, removed and repriced. What is new is that staleness is loud and the refresh is proposed: the scheduled .github/workflows/zen-catalogue-refresh.yml runs that command daily and opens a pull request when upstream has moved, carrying the generator's own report and the list of served ids that need curating. A person reviews and merges it — nothing is written to main by the job, and nothing is curated by it.

      Unchanged, and worth knowing before you file it as a regression: the service currently refuses anonymous use of every free model — including muse-spark-1.3-contributor-free, which an earlier release verified working — with HTTP 403 FreeTierError, "OpenCode's free tier can only be used from within OpenCode". That is upstream admission policy and predates this release; the catalogue still records the free models the pages publish, and the providers documentation now says so with the date and the evidence.