Anthropic caching is opt-in, and nothing sent breakpoints: observed
sessions show 0% cache hits on Claude models (~1.27M of 1.36M sampled
input tokens billed at full rate) vs ~82% implicit hits on Gemini. Two
ephemeral breakpoints fix that: the system prompt (whose cache prefix
also covers the tool schemas — both immutable per turn by construction)
and the last message (Anthropic's incremental-conversation pattern).
Conservative simulation on the sampled traffic floors the saving at 44%
with no cross-turn reuse; realistic reuse lands 70-85%.
Applied in the model registry bridge just before streamText, gated by
provider flavor or model id (covers direct Anthropic, OpenRouter, and
the gateways — the installed OpenRouter provider reads the same
providerOptions.anthropic key). Transport-only: nothing is persisted,
message content is untouched, and non-Anthropic requests pass through
byte-identical. Verify with cachedInputTokens on model_call_completed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The resolver helper's inline policy type predated middlePaneContent and
no longer satisfied ElisionPolicy; use the exported type. Coerce message
content to string where the preview assertions call string methods.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- catalog is ranked by GitHub star count (per-repo lookup with a 10-min
cache; unauthenticated works, the publish token is used when present)
- star button on each catalog card stars/unstars the app's repo as the
signed-in user (public_repo scope covers it), optimistic with revert
and a sign-in hint when signed out
- the info panel now offers Delete for local apps (apps:delete already
existed and cleans up app-owned agents; only installed apps had a
removal button before)
The original suite covered the tool-result path well but left holes:
loadElisionPolicy had no tests at all (now: missing file, full config,
partial merge, unparseable JSON, and the all-or-nothing malformed-key
behavior — via an injectable config path so tests stay off the real
WorkDir), images/note policies had no idempotency or determinism tests
(the properties prefix caching depends on), the note floor boundary was
unpinned, resolveAgent delegation was unverified, and nothing exercised
a multi-turn reference chain or the per-resolve hot config reload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Elision reads config/context.json at compose time, so the composed
payload is no longer a pure function of the durable log — inspecting an
old turn after a config edit can show different prefix bytes than were
transmitted. Document the exception against §8.3 and make the inspect
CLI print the policy in effect so divergence is visible instead of
silent. The gold fix (recording the applied policy on the turn) is
noted for when exact-bytes replay becomes a hard requirement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TurnRepoContextResolver was still freely constructible, so a future
call site could silently lose elision. The factory now accepts an
injectable policy loader (also keeps tests off the machine's real
context.json), the raw class carries a do-not-construct note, and
factory-level tests pin both the decorator wiring and the default
policy behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Real session data shows ~5k-char skill bodies are the most common
oversized historic result; the 10k default replayed them on every model
call for the life of the session. With the head preview in place the
model retains enough scent to re-load on demand, so the aggressive
default is the right trade. Still tunable via config/context.json.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A bare placeholder tells the model only the tool name and size; the
first 400 characters tell it what the output actually was (a skill
guide reads very differently from a fetched page), which is what it
needs to judge whether re-running the tool is worth it. The preview is
capped at the threshold so small thresholds still shrink content, and
the transform stays a pure per-message function (byte-stable prefixes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every user message sent while a note is open carries a full snapshot of
that note in userMessageContext, so a long chat over one open note
resends N full copies on every model call. The elision decorator now
rewrites prior-turn user messages to keep the pane kind and path but
replace note content above a small floor with a placeholder pointing at
the still-readable file. The current message's snapshot is untouched,
so "summarize this" keeps working; the system prompt already tells the
model later middle-pane context overrides earlier. Config:
elideHistoricMiddlePaneContent, default on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Video-mode webcam and screen-share frames matter for the response they
were captured for; afterwards the assistant's own text carries the
takeaway and fresh frames arrive with every new call message, yet each
message's frames (~10k tokens) were resent on every subsequent model
call. The elision decorator now strips image parts from prior-turn user
messages, leaving a text part recording how many frames of each kind
were dropped. The current turn's just-captured frames are always sent
verbatim, and the policy generalizes the existing config
(elideHistoricImages, default on).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tool results from prior turns (skill loads, file reads, HTTP fetches)
dominate resent context and are rarely needed verbatim. A decorator over
the context resolver now replaces prior-turn tool results above a size
threshold with a short placeholder telling the model to re-run the tool
if it needs the output. The current turn's in-flight results are always
sent verbatim, the durable log is untouched, and elision is a pure
per-message function so resolved prefixes stay byte-stable for provider
prefix caching.
Policy lives in config/context.json (elideHistoricToolResults, default
on; elideHistoricToolResultsThresholdChars, default 10000). The inspect
CLI composes through the same decorated resolver so debug output still
matches transmitted bytes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundled agents can't ship a model override (the author's providers are
not the installer's), so materialized tasks ran on the bg-task category
default — gemini flash lite — which reliably mangles large app-set-data
payloads (invalid JSON string → contract rejection → the installed app
never gets data; observed with pr-dashboard's refresh agent).
Materialization now resolves getDefaultModelAndProvider() on the host
and pins that on the new task — the installer's machine picks the
strong model, the package still pins nothing. Existing tasks keep
their user-set model (update path unchanged).
The skill treated mirroring the bg-task into agents/<slug>.yaml as
optional ('if the app should ship the agent'), so the copilot built
agent-backed apps (e.g. pr-dashboard) with the refresher as a loose
personal bg-task only. Publishing such an app ships a dead UI: data/
is user state and never packaged, and installers get no agent.
Bundling is now a required step with the failure mode spelled out.
GitHub rate-limits the device-code token endpoint: a poll arriving even
slightly under the flow interval returns slow_down and permanently raises
the required interval. The renderer polled on a fixed 5s timer (exactly
the limit), so one jittery request tripped slow_down and every poll after
it was 'too fast' — GitHub answered slow_down forever and the dialog sat
on 'Waiting for authorization' even after the user approved.
Core now tracks lastTokenPollAt and skips the request until the flow's
current interval (base +1s margin) has elapsed, so slow_down bumps take
effect and the flow recovers. The renderer timer is just a heartbeat (2s).
- start the apps server before createWindow instead of after the full
service-init chain: the Apps view was reachable ~10s before port 3210
was listening, so every app iframe opened to connection-refused
- retry EADDRINUSE binds (quick relaunch races left the server disabled
for the whole session with no retry)
- app frame: auto-retry the iframe instead of a dead manual Retry, and
surface when the apps server itself is down (new apps:serverStatus)
- log render-process-gone/child-process-gone reasons; renderer helper
crashes were previously invisible in packaged builds
Re-reading refs/heads/main immediately after the bootstrap commit can 409 on
a stale replica (GitHub eventual consistency) — the Contents API response
already carries the commit sha, so use it directly.
Resumed publishes skipped already-done steps without emitting progress, so
the dialog showed stale spinners for steps that finished in a prior attempt.
GitHub's Git Data API (blobs/trees/commits) returns 409 'Git Repository is
empty' on a repo with no commits — it cannot create the first commit. Ensure
main exists via a Contents API bootstrap commit (the generated README), then
chain the publish commit onto it.
Verified the wire calls are correct (both endpoints behave with this client
id from a plain node run), so instrument the runtime path instead of
guessing:
- log every poll outcome ([GitHubAuth] lines) at the token exchange, the
identity fetch, and the IPC handler
- identity failures now retry at most 3 polls then fail loudly with the HTTP
status — never silently degrade to 'pending' forever
- explicit User-Agent on all GitHub calls (defensive)
GitHub's OAuth endpoints take form-encoded params; the JSON token request
produced an unrecognized error that fell through to 'pending' forever, so the
dialog never advanced after the user authorized.
- form-encode device/code and access_token requests
- unknown token-endpoint errors now fail loudly instead of spinning;
incorrect_device_code maps to expired (restart offered)
- issued-token is remembered so a transient identity-fetch failure retries on
the next poll instead of burning the consumed device code
- dialog catches hard poll failures and surfaces them
- env: GITHUB_OAUTH_CLIENT_ID (device flow enabled on the Rowboat OAuth app;
overridable via ROWBOAT_GITHUB_CLIENT_ID)
- packager (§4.4): allowlist-only .rowboat-app ZIP (yazl), sorted entries,
symlink skip, sha256
- github-auth (§10): device-code start/poll, identity fetch, token stored
0600 with safeStorage encryption injected from main (core stays
electron-free); githubAuth:* IPC + external open of the verification page
- registry client (§9.2): unauthenticated tarball index with 5-min cache and
stale fallback, raw-record resolve, substring search, quota-free
latestManifest via release-asset redirect with name-mismatch guard
- registry repo contents (docs/apps-registry): record JSON schema +
validate-and-merge Action implementing §9.3 checks 1-7 with rejected:<code>
comments and per-name concurrency
- fix: host-api copilot-run adapted to dev's ModelSelection {provider,model}
(this is the same fix dev needs for Ramnique's packaging break)
- pnpm 11: blockExoticSubdeps=false (electron-forge has a git subdep)
The runs->turns migration broke codex live streaming in copilot chat: the
turns bridge's publish shim forwarded only tool-output-stream and silently
dropped code-run-event / code-run-permission-request (the latter would
deadlock a turn under policy 'ask'). An interim fix persisted every stream
event as durable tool_progress, but that wrote each text chunk to the turn
file — unsustainable.
Final architecture — live and durable paths split:
- Live (ephemeral, bypasses the turn runtime): code_agent_run broadcasts
each ACP event on a new CodeRunFeed (core DI singleton), forwarded by
main over a dedicated codeRun:events channel, buffered per toolCallId in
a module-level renderer store and rendered by CodingRunBlock. The buffer
survives session switches; nothing is persisted.
- Durable (one line per run): when the run settles (success, error, or
cancel), code_agent_run publishes a single code-run-events-batch with
the whole ordered timeline, consecutive same-role message chunks
coalesced (display-lossless — the timeline concatenates them anyway).
The turns bridge maps it to tool_progress {kind:'code-run-events'};
turn-view derives the replay timeline from it, so reloads keep history.
- Permissions stay durable per-ask (request + resolved marker): the
renderer overlay resets on session switch, so an ephemeral-only ask
would strand a blocked turn with no card to answer. Pending = requests
minus resolutions (handles concurrent asks), cleared on tool result.
The legacy code-section path (runs bus per-event) is untouched; its
per-event ctx.publish remains and is a no-op under the turns shim.
Also: repaired the two code_agent_run tests broken by the earlier cwd
existence check (they used a nonexistent /repo), and added coverage for
feed broadcast, batch coalescing, partial-batch-on-failure, bridge
durability routing, and pending-permission derivation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback: drop the model-layer scheduler (per-call-site
interactive/classifier/background priorities, local-provider hostname
heuristic) in favor of app-level orchestration:
- deferBackgroundTasks flag in models.json, surfaced as a settings
toggle; auto-enabled once (UI logic) when the user connects Ollama
- ChatActivity counter marked by both chat runtimes (sessions layer and
legacy AgentRuntime.trigger)
- startWhenPossible/runWhenPossible wrappers around the headless agent
runner; all background invocations (knowledge pipeline, live notes,
background tasks, scheduled + prebuilt agents) go through them and
wait for chat-idle when the flag is set
createLanguageModel keeps only the Ollama context-window middleware;
the LM Studio capability probe now keys off the provider flavor instead
of hostname sniffing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
code_agent_run failed with a misleading "spawn <Electron> ENOENT" that
read as "Codex isn't installed". Two stacked causes:
1. cwd wasn't expanded/validated: a `~` or non-existent path was passed
straight to child_process.spawn, which reports ENOENT against the
command (Electron) rather than the missing directory. Now expandHome +
resolve + existence-check with a clear error.
2. Apple revoked the signing cert on the pinned @openai/codex@0.128.0, so
macOS Gatekeeper trashed the binary on launch and the ACP adapter died
mid-handshake. Bump the ACP stack off the revoked build:
codex-acp 0.0.44 -> 1.1.0, claude-agent-acp 0.39 -> 0.55, sdk 0.22 ->
1.1, regen engine-manifest (codex 0.142.5 / claude 0.3.198). Removed the
two now-obsolete patches (contextCompaction upstreamed; the codex
windowsHide patch targeted bin/codex.js, which we bypass via CODEX_PATH).
Bump fallout handled:
- client.ts setModel: sdk 1.x dropped unstable_setSessionModel; model is now
a config option -> setSessionConfigOption({configId:'model'}).
- engine-provisioner: codex 0.142 moved its binary (codex/ -> bin/) and rg
(path/ -> codex-path/); probe both layouts.
- pnpm override vscode-jsonrpc to 8.2.0: codex-acp 1.1 pulls jsonrpc 9.x
whose restrictive exports break langium's deep import in the renderer
(blank screen). codex-acp is the only 9.x consumer and works on 8.x
(handshake verified), so pin the workspace to 8.x.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigating 'apps blank while a bg task runs': measured serving latency
through a real scheduled bg-task run — 1-3ms on both loopbacks throughout,
so the server is not the bottleneck. Found and fixed the likely renderer-era
culprit plus made the failure observable:
- agent sync (apps:list, polled every 4s) rewrote each bundled agent's
task.yaml unconditionally — racing the bg runner's own patches mid-run and
spamming watcher events; now writes only when the definition changed
- AppFrame load watchdog: if the iframe hasn't loaded in 6s, show a visible
'taking too long' state with Retry instead of a silent blank pane
- main event-loop lag monitor: stalls >300ms are logged so future blank
reports can be tied to the offending work
- checkCapability choke point (D7): tools/llm/copilot access requires the
capability in the app's manifest; 403 capability_not_declared otherwise
- POST /_rowboat/tools/search|execute: Composio pass-through with the same
request shape as the builtin, typed error mapping
- POST /_rowboat/fetch: SSRF-guarded proxy (loopback/RFC1918/link-local/ULA/
*.localhost rejection re-checked per redirect hop, 5MB cap + truncated flag,
30s timeout, Host/Cookie stripped)
- POST /_rowboat/llm/generate: default-model plumbing, override validated
against the allowed set, token/concurrency caps, usage attributed
app_llm_generate/<folder>
- POST /_rowboat/copilot/run: headless run (bg-task tool profile, no shell),
app-attributed audit turn with origin context, 10-min timeout, 1-per-app cap
- registered M2 routes are POST-only (GETs are D17-exempt and must not reach them)
- bundled agents (§8): strict AppAgentDefinitionSchema, deterministic
app--<folder>--<name> slugs materialized disabled with sourceApp, manifest
removals deactivate, app delete removes owned tasks; sourceApp in summaries
- new use cases app_llm_generate / app_copilot_run in runs + analytics enums