executeSyncTool's success-result append sat inside the try whose catch
appends an error tool result. A transient repository failure (disk
pressure, EMFILE) on that append was therefore durably recorded as the
TOOL having failed — after its side effect already happened — inviting
the model to retry the side effect on the next call (double-sent email,
duplicate issue). This violated §21.4: repository failures must reject
the execution, not masquerade as tool outcomes.
The try now covers execution and result parsing only (a tool returning
garbage is still a tool error). The success append happens outside: a
failure there propagates as an infrastructure error, the log keeps the
invocation without a result, and the existing §23 recovery closes it
honestly on re-advance ("outcome is unknown and it was not retried")
without re-executing.
Tests: a repo fake failing exactly the success append — asserts no
false error is persisted, the outcome rejects, recovery yields the
indeterminate result without re-execution, and a committed sibling
result survives untouched with correct model-facing ordering; plus a
parse-failure case pinning garbage returns as tool errors. Both repo
tests fail against the old code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The permission card offered "Allow for Session" and "Always Allow" on the
turns path (full-screen chat and side-pane), but the sessions layer never
persists grants — the scope rode respondToPermission metadata that nothing
consumes, so both buttons behaved exactly like "Approve" once while
telling the user otherwise. Grant persistence itself is deferred until
there is a sound notion of grant keys (executeCommand parsing is not
AST-based), and auto-permission mode covers the fatigue meanwhile.
The dropdown now renders only when the caller wires the scope handlers.
The legacy code-mode chat keeps them (runs:authorizePermission does
persist grants there); the turns-path call sites stop passing them and
drop the dead scope plumbing from their handlers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The permission checker returned {required: false} for every non-builtin
toolId and for any builtin outside the executeCommand/file-tools switch,
so composio-execute-tool (email sends, GitHub/Jira writes), executeMcpTool,
and mcp:* attachments on user agents executed with no permission check —
even in manual mode — and never reached the auto-permission classifier.
The extension contract was inverted: a new side-effecting tool shipped
ungated unless someone remembered to extend a switch in another module.
Every builtin now declares its permission policy in the catalog itself
(required field, compile-enforced): "none", "prompt", "command-allowlist",
"file-boundary", "composio-execute", or "mcp-execute". The checker reads
the declaration and FAILS CLOSED: undeclared builtins, mcp:* attachments,
and unknown toolId families require permission. Composio and MCP requests
carry family-specific payloads (new ToolPermissionMetadata kinds, rendered
by the permission card; generic fallback for everything else). All
previously-ungated builtins keep today's behavior via explicit "none"
declarations, except addMcpServer which now prompts; a catalog test pins
the audited gated set so policy changes stay intentional.
Auto-permission flows (background tasks, live notes, channels) route the
newly gated calls through the existing classifier per the §9.3 matrix;
defer still denies without a human. No grant persistence yet — every
gated call prompts (or classifies) each time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sessions layer tracked live advances in a map keyed by bare turnId
with overwrite-on-set and unconditional delete-on-settle. A turn can
legally have several live advances at once — a running invocation plus
external-input invocations queued on the turn lock (e.g. a permission
response while an async result is pending). The overwrite made stopTurn
abort the wrong controller and await an outcome that couldn't settle;
the unconditional delete untracked a sibling advance, so a later
stopTurn missed the abort path, fell back to a cancel input, and could
reject with TurnInputError if the turn had completed meanwhile — a
user-visible stop failure. Durable state was never affected.
Track a set of advances per turn: each advance removes only its own
entry on settle, and stopTurn aborts every live controller and awaits
all outcomes. A cancel input that loses the race with a concurrent
settle now counts as a successful stop (the turn is already terminal)
instead of rejecting.
Adds four tests: concurrent invocations both aborted, an earlier settle
not untracking a later advance, the settle-race cancel treated as
success, and non-terminal rejections still surfacing. Updates
session-design.md §9.3 and §13.5 accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The commit ritual persisted first and reduced second, so an illegal
append (e.g. a misbehaving tool reporting progress after its terminal
result) became durable before the reducer rejected it — permanently
corrupting the turn file and, through context references, blocking
every later turn in the session. Reduce the batch against the in-memory
history first: illegal batches now reject in memory for their caller
only, the file stays legal, and a failed commit no longer poisons
this.events for subsequent appends in the same invocation.
Adds a test that fires a stashed reportProgress after the tool's result
is durable (while a sibling keeps the invocation alive) and asserts the
late append rejects, nothing illegal reaches the file/stream/bus, and
the turn completes and stays re-advanceable. Updates the design doc
(§4.5, §24) to the reduce → persist → stream order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Phase B docs pass introduced two errors in VIDEO_MODE.md's prompt
catalog (composer still at the pre-move agents/ path; frame-context
pointed at legacy/engine.ts when convertFromMessages lives in
runtime/assembly/message-encoding.ts) and missed ANALYTICS.md entirely
(copilot_chat emit point still cited agents/runtime.ts:1313, file_parse
still cited the deleted builtin-tools.ts monolith).
turn-runtime-design.md referenced the repo-root AGENTS.md, which is not
committed — the carve-out is now stated inline. Also drops a leftover
debug comment in skills/index.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getErrorDetails was the last live import out of runtime/legacy/ that
isn't part of the code-mode / mini-apps carve-out — four knowledge
pipelines pulled it from legacy/utils.ts, blocking 'legacy deletes as
a unit'. Their agents run via the headless runner now, so the
RunFailedError unwrap branch was dead for them; the live version in
application/lib/errors.ts is the plain Error->message idiom, and the
legacy copy (with the RunFailedError coupling) is deleted as
consumerless.
isPathInside backs path-grant permission decisions, and byte-identical
copies lived in filesystem/files.ts, assembly/permission-metadata.ts,
and legacy/repo.ts — a place for a future divergence to become a
permission bypass in exactly one caller. files.ts now exports the one
live copy; permission-metadata imports it (it already imports files.ts,
so no new edge). legacy/repo.ts deliberately keeps its frozen private
copy: the quarantine must not import live modules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getComposioToolsPrompt still called the raw composio client's
isConfigured; everything else (skill catalog availability, the other
prompt connection blocks) goes through assembly/connections.ts, whose
check wraps in try/catch -> false. On an errored config read the two
could diverge: the prompt advertises Composio tools while the catalog
hides the composio skill the model would need to use them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/paths.ts' lenient expandHome passed blank input through, and
both call sites feed the result to path.resolve — so a model calling
code_agent_run with cwd: '' spawned the coding agent in the Electron
process cwd, and resolveCodeProject('') could register that cwd (a
real, existing directory) as a code project.
The 'lenient variant' turns out to be filesystem/files.ts'
expandHomePath minus exactly this guard, so this deletes tools/
paths.ts and exports the filesystem one instead of keeping a third
variant. A blank path now surfaces as a clear tool error ('Path is
required') the model can react to. Also drops the stray
resolveCodeProject comment the extraction left in paths.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
withActiveSkills injected carried-forward skills into every non-inline
agent request, while the resolver only honors them for agents with the
skillCarryForward trait (real-agent-resolver). The mismatch persisted
an ever-growing activeSkills list into the requested composition of
every turn for agents that would never use it.
The gate now lives in withActiveSkills, mirroring the resolver. That
required a structural fix: sessions cannot import assembly/registry.js
(its agent builders transitively reach di/container, which imports
sessions — the module cycle broke SessionsImpl registration under
test). Traits move to assembly/traits.ts, a zero-import leaf, with
registry re-exporting them so existing assembly-level importers are
unchanged. Regression test pins the non-trait agent case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ALS-miss fallback still imported fetchRun from the legacy runs
store, but ctx.runId has been a turn id since the runs->turns
migration — the lookup always threw and fell through, so a
background-task notification delivered outside its starting ALS
context (crash recovery, resume paths) lost its background_task
gating and fired as a plain notification.
The durable equivalent lives on the turn itself: turn_created's
agent.resolved.agentId, and only the background-task runner starts
'background-task-agent' turns. Also removes one of the last live
imports into runtime/legacy/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Test files were typechecked by nothing: tsconfig.build.json excludes
them (correctly — they must not land in dist), and vitest transforms
with esbuild, which strips types without checking them. That gap is
where the review's two tsc breaks and the fixture drift accumulated.
Adds 'typecheck' scripts per package (dev tsconfigs, which include
src/**/*.test.ts; renderer reuses its existing tsc -b), a root
orchestrator mirroring the test script, and a CI step in the vitest
job. All three packages are currently clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same class as the two review findings — type errors only tsc sees
(tests run untyped under vitest; the build tsconfig excludes them):
- headless.test.ts read .agent.overrides without narrowing the
RequestedAgent union; the assertions now compare the whole agent
object, which is also stricter
- real-agent-resolver.test.ts's byte-identity fixture predates the
videoMode/coachMode flags (explicit false = the schema defaults,
so the composed bytes are unchanged)
- real-tool-registry.test.ts's permission asks predate isRead being
required (options: [] was from the older ask shape)
npx tsc --noEmit -p tsconfig.json is now clean for the whole package.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The interface grew subscribe/subscribeAll when the IPC bridge moved
to resolving ITurnEventBus, but the runtime-test fake still only had
publish — a TS2739 invisible to vitest and the build tsconfig. The
fake now declares 'implements ITurnEventBus' so the next interface
change fails loudly here instead of silently drifting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Phase A extraction moved getToolPermissionMetadata to
assembly/permission-metadata.ts but this test's type-only import was
mechanically retargeted to legacy/engine.js during the tree move,
where the symbol is imported rather than exported — a TS2459 that
vitest and the build tsconfig never see, and one of the last
live->legacy edges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bundled-skill CATALOG_PREFIX still pointed at the deleted
src/application/assistant/skills tree, so the system prompt and the
loadSkill example advertised paths that no longer exist — while the
real post-reorg path (src/runtime/assembly/skills) was NOT a
resolvable alias, so a model referencing the actual source tree got
'Skill not found'.
The new path is now canonical in the catalog and tool description;
the old prefix is kept as a loadSkill alias because agent snapshots
baked before the reorg still carry it. Regression test pins both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Path pass over turn-runtime-design.md, session-design.md,
VIDEO_MODE.md, and LIVE_NOTE.md: every source pointer updated to the
runtime/ layout, the turn doc's suggested-module-layout section (§28)
refreshed to the real tree (it predated the event hub, bridges, and
composer), and the session doc's §14 layout fixed including its old
§11/§14 contradiction about where the headless runner lives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
domains/support.ts bundled five unrelated concerns. Each moves to its
owner: the catalog schema → tools/types.ts (every domain's typing);
lenient ~-expansion → tools/paths.ts (documented as distinct from
filesystem's throwing variant); the bg-task input schemas and
resolveCodeProject → domains/background-tasks.ts; the code-run
coalescer → domains/code.ts (catalog re-export retargeted); parser
plumbing → domains/parsing.ts, now module-private.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything "model runtime" now lives under core/src/runtime/, whose
five children are the architecture: turns/ (the engine + bridges),
sessions/, assembly/ (what an agent is: registry, composer, workspace
context, message encoding, permission metadata, headless runners,
spawn-agent, copilot/ definition, capabilities/, skills/ — no longer
buried at application/assistant), tools/ (catalog + domains + exec
plumbing + descriptors, which leave turns/bridges and fix the upward
lib→bridges edge), and legacy/ (the quarantined runs engine, with
agents/runtime.ts finally honestly named engine.ts).
Pure moves via git mv (history follows) with mechanical import
rewrites; no durable schema, wire format, or behavior changes — the
golden prompt snapshots and the catalog key-order pin pass unchanged.
di/ stays top-level (whole-app composition root); application/ shrinks
to browser-control/browser-skills/notification and true cross-cutting
lib utils.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Chat history: shared tab design (heading, bg, centered column, card
table), hover kebab + context menu with rename/delete/open-in-new-tab
- Search: chat search now targets the session store (titles from the
sessions index, content grepped from turn logs) — results open again
and new chats are searchable; segmented scope control, scope-aware
placeholder and empty states, Tab flips scope, keyboard-hint footer
- Sidebar pins: stale ids of deleted chats no longer eat pin slots
- Chat header: session token-usage as a ghost bar-chart button with
tooltip that opens stats directly; options menu (download log) moved
into the shared ChatHeader so full-screen chats get it too; caffeinate
indicator renders nothing while off (was a 32px hole)
- Turn usage dots: borderless, aligned with message text edge
- Workspace: breadcrumb baseline alignment
Apps store user settings in data/config.json (e.g. pr-dashboard's
tracked repo) and rely on their bundled agents to turn config into
data. Without this, changing a setting meant staring at an empty app
until the next cron tick. The apps-server watcher now debounces
config.json writes and fires a one-shot manual run of the app's owned
tasks (app--<slug>--*). Generic for any app; dynamic import keeps the
server decoupled from the bg-task runner at load time.
Ollama returns errors as {"error":"<string>"}, but ollama-ai-provider-v2's
error parser expects OpenAI-style {"error":{"message":...}}. The schema
mismatch makes the AI SDK fall back to the HTTP statusText, so users only
ever saw "Bad Request"/"Not Found" while Ollama's actual reason (model not
found, no tool support, ...) was swallowed -- which is why reports like
#696 were impossible to debug.
makeOllamaThinkFetch now rewraps failed responses' plain-string error
bodies into the nested shape the provider parses, preserving status,
statusText and headers (content-length dropped so the runtime recomputes
it). Successful responses, already-nested errors and non-JSON bodies pass
through untouched. The wrapper's two fetch call sites were merged into one
so both the /api/chat path and the passthrough get the same error handling
without a duplicate-request hazard.
Refs #696
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generic app-copilot bridge (no app-specific wiring anywhere):
- app-read-data builtin: read (or list) an installed app's data/ files —
the agent-maintained JSON the app renders. Same path confinement as
app-set-data; 50k char cap with truncation flag; parsed JSON returned
when possible. Attached via the app-navigation skill.
- read-view view=apps: installed apps with name/description/dataFiles/
agentSlugs, navigating the user's screen to the Apps view (same
show-while-telling contract as email/bg-tasks). Renderer routes the
new view value.
- copilot instructions embed the installed-apps list (name +
description per app) with routing guidance: prefer an app's fresh
data over external calls, answer from it, open-app to surface it.
The apps:list handler fingerprints the app set and invalidates the
instructions cache when it changes (installs, deletes, copilot-
created folders all flow through that poll).
- app-navigation skill: apps section + a worked example framing the
pattern generically (match apps by their own descriptions, never a
hardcoded topic map).
Verified against real data: app-read-data returns pr-dashboard's live
10-PR data.json, escape paths rejected, key-order golden test updated
and passing.
Findings from a fresh post-refactor review. The turn-runtime bridges
imported convertFromMessages and getToolPermissionMetadata from
agents/runtime.ts — the legacy engine file — making it undeletable;
both move to neutral modules (agents/message-encoding.ts,
agents/permission-metadata.ts) that both engines now share. The
abort registry, used by the live tool registry, moves out of the
legacy runs/ cluster into turns/. Dead surface goes: the zero-consumer
CopilotInstructions, skillCatalog, RunLogger, and MappedToolCall
exports are deleted, mapAgentTool/StreamStepMessageBuilder become
module-private, and the mid-file import scar from the composer
extraction is hoisted.
Three review fixes ride along: ITurnEventBus gains
subscribe/subscribeAll so consumers stop resolving the concrete hub;
skill carry-forward becomes its own registry trait instead of
overloading workspaceContext; and connection checks (slack/composio/
code-mode/google) collapse into one shared connections.ts consumed by
both skill availability and the copilot prompt blocks — plus stale
doc pointers (VIDEO_MODE.md to moved prompt text, CLAUDE.md's dangling
AGENTS.md reference).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Thorough-effort Anthropic runs through the gateway failed on the tool
loop: Bedrock rejected the echoed thinking block ("Invalid `signature`
in `thinking` block"). OpenRouter streams reasoning_details as
per-delta FRAGMENTS on reasoning events — the part-level capture kept
only the last fragment (unsigned) — while the fully accumulated,
signed array rides the finish event's providerMetadata, and OpenRouter
gives message-level reasoning_details precedence on read-back.
The bridge now attaches finish-step providerMetadata to the assistant
message as message-level providerOptions (restoring parity with the
legacy StreamStepMessageBuilder, which always did this);
convertFromMessages already echoes it. The authoritative signed array
wins over the per-part fragments, which providers then ignore.
Also persist provider failure detail on turn_failed: errorMessage()
appends the API error's status code and responseBody (bounded to 2KB)
— "Failed after 3 attempts" alone was undebuggable from the turn log.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously catalog readers (listOnboardingModels, getChatModelIds)
fetched models.dev inline on a 24h TTL, while the reasoning-capability
paths read the cache only — so a signed-in install could run forever
with no cache and the effort chip never appeared, and catalog calls
could stall on a slow third-party fetch.
Now main calls startModelsDevRefresh() once at boot: refresh if the
cache is missing or older than 24h, then every 24h while running, with
a 10s fetch timeout, errors logged and swallowed (existing cache stays
in use). Every consumer reads the on-disk cache only. Catalog-shaped
readers (models:list, gateway annotation) await the warm-up's first
attempt so a fresh install's first list sees the fetched data; the
turn-start capability gate never waits. A missing cache is an empty
catalog, not an error — chat is unaffected either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reasoning-capability join failed for most gateway models, hiding
the effort chip for everything except the Gemini default. Two id
dialects broke it: OpenRouter-style ids spell versions with dots
("anthropic/claude-opus-4.8") where models.dev uses dashes
("claude-opus-4-8"), and the rowboat gateway serves OpenAI models with
no vendor prefix at all ("gpt-5.4").
The lookup is now a pure index (buildReasoningIndex/lookupReasoningFlag,
unit-tested) that normalizes ids case-insensitively with dots folded to
dashes, keyed both vendor-qualified and bare; bare ids that clash across
vendors with different flags are dropped rather than guessed. Verified
against the live gateway list: all 11 models now resolve.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Brain chip next to the model picker (Auto / Fast / Balanced /
Thorough) in both the full-screen and side-pane composer. Selection is
per-tab, next-turn intent like the model picker (same ref pattern), but
unlike model it is never frozen on a run — it applies turn by turn,
riding sendConfig.reasoningEffort into turn_created.config.
The chip renders only when the effective model (run-frozen, picked, or
app default) is known-reasoning per the models:list capability flag,
and a stale selection resets when switching to a non-reasoning model.
Turns that ran at a non-auto effort show the level beside the existing
token-usage affordance, read retroactively from the persisted turn
config; reasoning tokens were already displayed there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Widens the models.dev schema to keep the per-model `reasoning` boolean
and carries it through normalizeModels → ProviderSummary → models:list,
so the composer can gate the reasoning-effort control on actual model
capability. Gateway model lists (bare "vendor/model" ids from the
server) are annotated from the models.dev cache in one batched,
cache-only read; unknown models keep the flag absent, which the UI
treats as "hide the control".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a canonical reasoning-effort ladder (low|medium|high, absent =
auto/provider default) that travels: send config → turn_created.config →
every model call's persisted parameters (§8.3) → provider-specific
options at invoke time. Behavior-neutral: nothing sends an effort yet,
and auto produces byte-identical requests to today.
- shared: ReasoningEffort enum (single source, reused by models.json
provider config), TurnCreated.config.reasoningEffort (additive
optional on a non-strict object — old builds strip it, no
schemaVersion bump), sessions:sendMessage IPC schema.
- turns: CreateTurnInput/SendMessageConfig/HeadlessAgentOptions carry
the value; runModelStep stamps it on each call's parameters so every
step durably records what it ran with.
- bridge: maps canonical effort to provider options, transport-only
like prompt caching — OpenAI reasoningEffort, Anthropic thinking
budgets (raising maxOutputTokens to the budget floor, never lowering
an explicit value), Gemini thinkingLevel/thinkingBudget by
generation, OpenRouter/rowboat reasoning.effort. Capability-gated
via a cache-only models.dev lookup (never blocks a turn on the
network); unknown support fails closed on strict flavors, while
OpenRouter-shaped flavors map permissively since OpenRouter drops
the field for non-reasoning models. Explicit persisted
providerOptions win over the mapping. Ollama keeps its existing
provider-level think rewrite; openai-compatible endpoints get
nothing (no safe universal parameter).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bridge dropped providerMetadata from content-block stream events,
so reasoning parts persisted as bare {type, text}. Anthropic thinking
signatures (which arrive on a reasoning-delta with an empty text delta),
Gemini thoughtSignatures (on text-end / reasoning-end / tool-call), and
OpenAI encrypted reasoning were all lost — breaking multi-step tool
turns whenever extended thinking is enabled, since providers require
signed blocks to be echoed back verbatim within the tool loop.
Now every -start event opens a new part (distinct blocks keep distinct
signatures) and metadata from each event of a block is merged opaquely
onto that part's providerOptions, which convertFromMessages already
echoes verbatim. The bridge never interprets the contents; each
provider reads back only its own keys. Empty blocks with no metadata
are dropped, matching the previous lazy-creation behavior. finish-step
metadata stays top-level as before.
Schemas already allowed providerOptions on all part types, so persisted
files remain readable in both directions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>