Compare commits

..
Author SHA1 Message Date
CodeWhale BotandClaude Sonnet 5.5 c0c80ffa07 fix(web): Plugins page names what each permission mode does with an agent-written plugin and the real skill command (CW-8087 PL-04 repair 1)
Auto-Review's reviewer model judges and Plan refuses, per the recorded ruling, so the
page no longer says approval is always asked outside Full Access. /skill install is
the command that adds a skill. The coming lead says the rest of the page is the
released version, and the meta description no longer carries a sentence that outlives
the 0.10.3 release.

Checks: vitest 92 files / 1303 tests passed, tsc exit 0, eslint exit 0, check:facts exit 0,
next build exit 0; built /en and /zh plugins pages fetched and show the new text.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:43 -08:00
CodeWhale BotandClaude Sonnet 5.5 66987f4fb5 feat(web): the Plugins page says plugin for what a person adds and labels what 0.10.3 brings as coming (CW-8087 PL-04)
The page described "everything is a plugin" as if the released terminal
already listed skills, connectors and hooks as plugins. It does not: a plugin
bundle carries them, and /skills, /mcp and /hooks add them one at a time. The
title, lead and meta now say that, and the heading "One format for every
extension" becomes "What a plugin can add", since one format is not released.

What 0.10.3 adds goes in a new section, "Coming in 0.10.3, not in the current
release": one Plugins place with kind labels, Standard and Creator mode per
conversation, the approval rule for a plugin the agent wrote, and the old
Plan/Work control renamed Permissions. The section renders only while the
published release is older than 0.10.3 (pluginsComingApplies) and is in the
site search under the same condition. English and Simplified Chinese carry the
text; the other 16 languages show the English page body through the existing
fallback, and no dictionary key was added or removed, so the 18-language
parity checks are unchanged.

Also: the authoring button opens the site's own plugin-authoring page (written
from the released tag) instead of the main-branch document; the DeepSeek
Harness compatibility link says it describes current source, because that
document is not in the v0.10.2 tag; the desktop app page names Plugins as one
noun with skills, connectors and hooks inside it; docs map descriptions say a
connector is an MCP server, a hook is added, and a skill can come inside a
plugin; the unrendered product copy says a skill is shared inside a plugin.

Checks run in web/: tsc --noEmit, eslint, check:facts, check:locales,
check:docs, vitest (91 files, 1301 tests plus 2 new), next build; the built
/en, /zh and /de plugins pages were fetched and the coming section checked.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:42 -08:00
CodeWhale BotandClaude Sonnet 5.5 bd1d61630a docs(plugins): repair round 2 for review of CW-8071
plugin-creator step 7 says to write the Full Access plugin in an ordinary
project folder, because install refuses a name already used by the bundle
step 1 creates. English RUNTIME_API gets the same after-0.10.2 label as zh.
Agent docs regenerated; preflight --check OK.

At landing: the skills and plugins rows in docs/features.toml are
shortened to the registry's 120-character summary limit (they were 122
and 153, which failed feature_registry_matches_code); features JSON
regenerated.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:40 -08:00
CodeWhale BotandClaude Sonnet 5.5 45794614da docs(plugins): repair round 1 for review of CW-8071
Docs no longer claim a single Plugins list exists: hand-made skills, servers
and hooks are plugins of a kind, still managed in Extensions and Customize,
and the one list is stated as planned for 0.10.3. plugin-creator step 7 now
gives the install, trust with review token and enable steps that the Full
Access exception accepts. Drop the generation 22 comment. zh RUNTIME_API says
ACP plugin and MCP tools are main after 0.10.2.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:38 -08:00
CodeWhale BotandClaude Sonnet 5.5 3dbb084a9e docs(plugins): the docs say plugin, in English and Simplified Chinese (CW-8071)
PLUGINS, SKILLS, MCP and HOOKS open by saying everything a person adds is a plugin;
a skill, an MCP connector and a hook are each a plugin or part of one and keep their
kind as a label, with their files, trust rules and commands unchanged. Connection
stays the word for a model provider or another agent and nothing else. PLUGINS no
longer says DSH plugin UI is unsupported. features.toml names the three kinds as
plugin kinds (ids unchanged); agent docs and feature data regenerated.

Checks: scripts/preflight.sh --check (see the review packet).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:37 -08:00
CodeWhale BotandClaude Sonnet 5.5 50b4d5b45d docs(plugins): correct the stale creator texts of ruling R8 (CW-8071)
plugin-creator no longer tells an agent never to automate trust: in Full Access an
agent may trust and enable a plugin it wrote and installed from its own project,
through the session's approval, recorded as the agent's (section 142.4), and code or
screen plugins point at cordis-plugin-development instead of a second format. The
0.10.2 body (generation 19) is retained and listed by hash so untouched copies
upgrade; the bundled generation is 22. DSH_PLUGIN_COMPAT now says imported client
UIs are served on the admitted seats instead of unsupported. The Simplified Chinese
EXTENSIONS and RUNTIME_API say ACP sessions get the extension host, plugin tools and
configured MCP servers, as the English does. The vocabulary script says /pod answers
Unknown command and codewhale pod is prompt text.

Checks: codewhale-skills nextest 188 passed 0 skipped; clippy -D warnings clean;
scripts/check-tui-product-vocabulary.sh exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:36 -08:00
CodeWhale BotandClaude Sonnet 5.5 7b60b85ac3 fix(extension-host): cordisInspect commands.register states the alias and refusal rules the registry enforces (CR-06a, CW-8073)
A built-in clash gets plugin-<plugin-slug>-<command> and is refused if invalid or taken; a name another plugin registered is refused. Catalog regenerated; --check exit 0; inspect tests 17/17 on Node and Bun; typecheck clean.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:34 -08:00
CodeWhale BotandClaude Sonnet 5.5 f94b43012a fix(extension-host): cordisInspect catalog states the persona section and activation result contracts the host enforces (CR-06a, CW-8073)
systemPrompt.section is named deployment:persona-prefix with the order getSectionOrder returns; subagents result is a getter resolving to stopReason and output blocks. Command, avatar and hook limits corrected; non-JSON provider output is refused as DSH does. The method-existence test now checks each method on the class that serves it; a renamed shim method fails it.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:33 -08:00
CodeWhale BotandClaude Sonnet 5.5 34b0631492 feat(extension-host): cordisInspect data that describes this host (CR-06a, CW-8073)
A dependency-light cordisInspect service with list() and query() in the
shape DSH's cordis_inspect_list/query tools call. Host providers: Service,
Event, Config (Tool only when a source is supplied); client providers:
Slots and Theme. The catalog is generated from compatibility.json, the
host's refused-event list in root.ts, the shell-hook event map and the
Engine seat table, plus one hand-kept method table; manifests pass DSH's
own supported-schema check and inputs its validator. Not wired to the live
host (CR-06b); dist/ is not regenerated.

Checks: node --test test/inspect.test.mjs 15 pass 0 fail; bun test 15 pass
0 fail; npm run typecheck clean; generator --check clean; preflight OK.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:32 -08:00
CodeWhale BotandClaude Sonnet 5.5 3d4158cf07 test(tui): real-terminal thinking stream, stop, restore and sign-in notice at four widths (CW-7876)
Adds a PTY scenario to the existing cucumber harness, compiled only on unix
with long-running-tests: stream thinking, stop, and restore at four terminal
widths, the one-time sign-in notice with Esc dismissal, and a saved choice to
hide thinking surviving the new default.

Checks: farm run 20261112T073916Z-ENG-TUI-rebased-pty4-b3fc94, 3 tests run,
3 passed (1 slow), 60 skipped, exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:30 -08:00
CodeWhale BotandClaude Sonnet 5.5 4bd3e15373 fix(tui): a paused stream shows the thinking it already received (CW-7876)
When the provider paused mid-stream, reasoning already received stayed
unrendered until the next token arrived. The owed revision now renders on a
later display beat (render_owed_revision), so the transcript shows what has
arrived. Covered by a_paused_stream_renders_the_reasoning_it_already_received
and the PTY scenario.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0; clippy run 20261112T061950Z-ENG-TUI-rebased-clippy-50d460
clean, exit 0; PTY run 20261112T073916Z-ENG-TUI-rebased-pty4-b3fc94, 3 of 3
passed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:29 -08:00
CodeWhale BotandClaude Sonnet 5.5 192a7306ed fix(tui): the footer clock says thinking, like the transcript (CW-7876)
The footer phase word and clock said reasoning while the transcript header
said thinking. Both now say thinking, in the 12 locale packs that carried the
old word and the localization library; uk and ru keep their own wording. The
ui tests assert the new clock text.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0; locale parity PASS; vocabulary check exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:28 -08:00
CodeWhale BotandClaude Sonnet 5.5 4ec3c45b47 feat(settings): model thinking shows by default; a saved choice is kept (CW-7876)
Thinking traces are first-class: show_thinking now defaults to true in the
runtime Settings and the config schema, while thinking_default_expanded stays
false (live while streaming, collapsed once the answer starts). Docs, the
features.toml row, the zh-Hans docs, the config panel goldens and the derived
web json follow. A show_thinking value already present in settings.toml is
read as an explicit choice and kept. Note that earlier saves wrote the old
default into that file, so only installs without the key get the new default;
whether to migrate those is a founder decision recorded in the review packet.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0, including feature_registry (2 passed); clippy run
20261112T061950Z-ENG-TUI-rebased-clippy-50d460 clean, exit 0; check-facts OK.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:26 -08:00
CodeWhale BotandClaude Sonnet 5.5 cf6e4c702e fix(tui): the sign-in notice is a one-time footer notice that Esc dismisses (CW-7876)
The "account preferences not loaded" notice was pushed into the transcript
on every refresh, so it repeated and scrolled away with the conversation.
It is now a standing footer notice shown once per sign-in
(account_preferences_notice_shown), and Esc dismisses it while idle. The
message names the remedy within 40 columns and carries meaning in words, not
colour. New string added to all 15 locale packs and the settings schema.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0; clippy run 20261112T061950Z-ENG-TUI-rebased-clippy-50d460
clean, exit 0; PTY run 20261112T073916Z-ENG-TUI-rebased-pty4-b3fc94, 3 of 3
passed; locale parity PASS; vocabulary check exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:24 -08:00
CodeWhale BotandClaude Opus 5.5 d82eeaf592 docs(sandbox): Fleet workers keep the managed variable; named files fail closed; serve --mcp refuses shell (CW-8052)
CHANGELOG: the Sandbox bullet's managed-configuration sentence now says what
the four CW-8052 commits do.

Checks: round 0 nextest 987/987 passed and clippy clean on runtime, engine,
app-server and TUI; repair round on the rebased tip nextest 1058/1058 passed
(runtime lib 922, engine fleet::host 26, TUI config_stay 110); preflight OK.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:23 -08:00
CodeWhale BotandClaude Opus 5.5 5aed666083 fix(mcp): serve --mcp does not expose shell tools under a tool launcher (CW-8052)
The stdio MCP server builds its own ToolContext, whose shell manager has
no launcher, deny list or prefer_bwrap. When the loaded config carries a
managed tool_launcher, run_mcp_server now removes every expose_tools entry
that maps to exec_shell (shell, exec_shell) before the registry is built,
so no shell tool is registered, and names each one on stderr. Wiring the
launcher would also need the deny list and read guard; refusing is the
closed form the review allowed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:22 -08:00
CodeWhale BotandClaude Opus 5.5 1396698607 fix(fleet): local workers keep the parent's managed config variable (CW-8052)
A Fleet local worker is spawned with a cleaned environment rebuilt from
the child allow-list, which has no CODEWHALE_* names, so it lost
CODEWHALE_MANAGED_CONFIG_PATH and read the user config's
managed_config_path instead. base_env_from now carries both managed-config
names from the parent for every local worker (not only when a sessions
directory is set), and worker_env drops them from the request so an
allow-listed request cannot replace or add them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:20 -08:00
CodeWhale BotandClaude Opus 5.5 72e5bcdb39 fix(config): a named managed file that cannot be read is a load error (CW-8052)
One helper, managed_config_file, now resolves the managed path for the
loader and for the three "who controls this" readers: the variable first
(an empty CODEWHALE_MANAGED_CONFIG_PATH defers to the legacy alias instead
of hiding it), then the config file's managed_config_path, then the
default path. The variable is read through codewhale_env_var, under the
test environment lock.

A file named by the variable or by managed_config_path that is missing,
a dangling link, a directory or unreadable used to load with no managed
layer; it now fails the load. Only a missing file at the default path
means no managed layer, and a config file's managed_config_path can no
longer point away from an existing default-path file. The readers report
Ambiguous where the loader would fail.

A provider that only a plugin declares used to fail under a managed file
with the saved-session message; it now says that plugin providers are not
usable under a managed configuration and what to do.

Tests that isolated themselves with an absent managed file now name an
empty one (no_managed_config writes it). New test covers missing,
directory, config-key and blank-variable cases; the late-provider test
now pins the error instead of passing on any Err.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:19 -08:00
CodeWhale BotandClaude Sonnet 5.5 e6183ad966 fix(usage-reporting): installers force the kill switch for their own run; doctor names a saved opt-out as config (CW-8033)
The installers (web/public/install.sh, scripts/release/install.sh,
scripts/release/install.ps1) now run their closing `doctor --installs --brief`
with CODEWHALE_TELEMETRY=0 and CODEWHALE_NO_UPDATE_CHECK=1 for that command
only, and print one line saying usage reporting is on by default and how to
turn it off. v0.10.2 already honours both variables, so nothing needs the next
release. `doctor` is classed read-only and does not arm reporting at v0.10.2 or
main, so this is a guard that does not depend on that classification.

The dispatcher hands the child CODEWHALE_TELEMETRY with the resolved value, so
a saved opt-out read as an environment kill switch and doctor printed "(env)".
The dispatcher now also passes CODEWHALE_TELEMETRY_SOURCE; the child uses it
only to label the source, only next to the dispatcher's floor statement, and
never to change the decision. A hand-set CODEWHALE_TELEMETRY=0 still reads
"(env)".

Wording: the two clap help strings and the English TUI strings say "usage
reporting".

Checks: node install-safety.test.js 74 passed; farm nextest of the four new or
touched tests 4 passed, and a wider telemetry sweep (config, cli, tui,
localization) 206 passed, 1 failed, the failure being a test expectation fixed
in this commit.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:17 -08:00
CodeWhale BotandClaude Sonnet 5.5 7ed8f92649 fix(engine): withhold tools whose encoded name exceeds the route limit; make decode exact (CW-8041)
to_api_tool_name expands every character outside [A-Za-z0-9_] (a dot is
nine characters, a hyphen two), so a long qualified MCP or plugin tool name
could encode past the provider limit and fail every retry with a 400 on
tools.N.name. A shortened name could not be decoded back, so no shortening
is attempted: the encoder stays injective and nothing is truncated.

Policy, applied in the one encoder module (client/wire.rs) and used by the
chat, responses and Anthropic builders:
- A tool whose encoded name exceeds the limit is withheld through the
  existing omitted-tools path. Forcing it with tool_choice fails with an
  error giving the tool name, its encoded length, the limit and what to
  change.
- A tool name already present in message history that exceeds a documented
  limit (Anthropic, OpenAI and Codex, DeepSeek) refuses the request with
  its own message: the tool was called earlier in the session, so switch to
  a route with a larger limit or start a new session. Routes with no
  documented limit send history unchanged. History is never rewritten.
- The omitted-tools warning (core/events.rs, one message) names the tools
  and states the limits and what to do. The exact encoded length is not in
  it: the warning carries names already cut to 64 characters; adding it
  needs a new field on the warning event.

Limit rule, in tool_name_limit(provider, wire): where a provider documents
its own limit it is used; otherwise the default is the limit of the wire
the route speaks, 128 on the Anthropic Messages wire and 64 on the OpenAI
chat and responses wires, except Ollama, Ollama Cloud, vLLM, SGLang and
custom routes, which have no name limit and use 128.
- Documented, read 2026-11-10: Anthropic 128 (define-tools page,
  ^[a-zA-Z0-9_-]{1,128}$); DeepSeek 128 on its chat and Anthropic wires
  (chat-completion API reference); OpenAI 64 (OpenAPI spec, function name
  description).
- Wire default, no documented limit found: MiniMax and ModelStudio on the
  Anthropic wire (128; their Anthropic-compatible references, read
  2026-11-10, state no tool-name limit), every other Anthropic-wire route
  (128), and every other chat or responses provider including Gemini (64;
  a wire default, not a documented provider limit).

Decode: from_api_tool_name ran a lenient second pass that rewrote bare
x00002E-style text, so a tool literally named ax00002Eb decoded to a.b, a
different tool. A name that is the canonical encoding of its own exact
decode now returns that decode and skips the lenient pass, so a legal tool
name can no longer be turned into another. The cost: a model that strips
both dashes from an encoded name is no longer recovered; the old
foo_x00002Ebar expectation is changed accordingly.

Tests: encoding collision and round trip over hostile names, limit counting,
limit per wire and provider, a 100-character name offered on the
MiniMax Anthropic wire and withheld on a chat wire, over-long MCP-style
names withheld per provider in the Anthropic, chat and responses builders,
forced-tool and history refusals, and a 65 to 128 character history name sent unchanged on Ollama, Custom and MiniMax chat routes, and the warning wording. Farm: nextest -p codewhale-engine client:: and core::events:: 664 run, 664 passed;
workspace clippy (ci.yml flags) clean.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:16 -08:00
CodeWhale BotandClaude Sonnet 5.5 a937eaccbc fix(engine): encode the tool name in a chat function-object tool_choice (CW-8040)
map_tool_choice_for_chat encoded only the internal {"type":"tool","name"}
shape. A choice already in chat-completions form,
{"type":"function","function":{"name":"web.run"}}, fell into the
passthrough arm and named a tool the request never declared, because the
declaration is sent as web-x00002E-run. The same held for the flat
{"type":"function","name":...} form and for a bare tool-name string, both
of which forced_tool_name already treats as a forced tool.

All of these now go out as {"type":"function","function":{"name":
to_api_tool_name(name)}}. Reserved strings (auto, none, required) and a
function choice with no name are unchanged.

Shape per OpenAI's published OpenAPI spec (ChatCompletionNamedToolChoice),
read 2026-11-10. The regression test covers web.run in all four shapes and
checks the built request body against the declared tool name.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:14 -08:00
CodeWhale BotandClaude Sonnet 5.5 a7d9986382 fix(app-server): match a gone store spelled through a symlink or another letter case when retiring a dead owner receipt (CW-8054)
The dead-owner discard compared the lexically normalised selected store with the
receipt's data_dir, so a home spelled through a symlinked ancestor (for example
/tmp on macOS) or in another letter case never matched and startup stayed
refused. Both sides now resolve the deepest existing ancestor and re-append the
missing tail, and on macOS and Windows the final comparison folds letter case.
Adds a symlinked-home test (unix) and a case-spelling test (macOS).

The one new canonicalize is admitted to the blocking-calls budget; it runs
inside owner_work, off the async runtime.

Checks: nextest -p codewhale-app-server --test daemon_socket 18/18 passed;
clippy -p codewhale-app-server --all-targets --all-features 0 warnings;
blocking-calls budget within budget; fmt clean.

Squashed from bf3c88bde3 and 6cdf29d72c.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:13 -08:00
CodeWhale BotandClaude Sonnet 5.5 665fb506ff test(engine): settle the queue fixture and bound idle reads by measured time (CW-8070)
The settled-nap test waited a fixed 2.3s for a fresh queue file to age and
bounded fingerprint reads by the nominal 3s window. On a slow runner the queue
could still be unsettled when counting began, or the sleep could overshoot and
fit more 1s naps than the bound allowed. Settle the stamp through the existing
fixture, wait for every worker to report a settled schedule, and derive the
bound from measured elapsed time. Product code is unchanged.

nextest stress 20/20 passed (farm run 20261112T061942Z-nap-stress-90898e);
clippy -p codewhale-engine --all-targets --all-features clean.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:11 -08:00
CodeWhale BotandClaude Opus 5.5 5e2366a1c4 fix(subagent): remove the workspace-wide delegated coordination lock (CW-7966)
A stale or suspended Codewhale session in a folder held
`.codewhale/state/subagents.v1.lock`, and every later session there
failed the exclusive v1->v3 cutover acquire, fell back to private or
read-only sub-agent state for the life of the process, and repeated
"Runtime coordination was opened without its writer lock" on each
sidebar refresh.

The v3 store already owns its concurrency control: the bounded
per-operation `subagents.v3.transaction.lock` and per-writer boot
leases, with per-writer snapshot merge, record ownership, terminal
receipts and stale/foreign record rejection in `merge_publish`. This
removes `CoordinationProcessLock` and everything that only it used: the
process-lock slot and its required/exclusive/read-only flags, the
session-private fallback, the cutover-blocked marker, the per-admitted-
workspace writer locks, and the lock-only constructor flag. The v1->v3
cutover now runs under the transaction lock only, as the architecture
doc already allows. Store admission that fails is retried on the next
delegated write (`admitted_store_authority`) instead of staying refused
for the life of the process; a corrupt store still fails every write
because each transaction validates the image it reads.

A Runtime that admits a selected workspace other than its own state root
used to take an exclusive flock there; it now refuses shared-workspace
writers while any session holds a live boot lease in that workspace's own
store, treating only NotFound as "no session there". The state
directory's `.gitignore` is written by store admission after the cutover
and the image read, so a refused admission through a linked `.codewhale`
writes no new file there. `cleanup_due` is false while a state path
exists and the store is not admitted, so a refused store shows its
refusal once rather than on every sidebar refresh.
`new_exclusive_runtime_subagent_manager` stays as an alias of the shared
Runtime constructor for its app-server caller.

Also carries the out-of-ownership edits the change requires: the
coordination-lock wording in engine.rs, coord.rs and coord/ledger.rs
comments, the runtime_threads test comment, the six TUI conformance
goldens (the engine no longer creates subagents.v1.lock), the two docs,
and the blocking-calls budget for subagent/mod.rs (std_fs 3 to 2).

Regression: `a_stale_session_never_makes_a_new_session_read_only`, a
two-process check; it fails on main.
`a_linked_codewhale_admission_writes_no_gitignore_through_it` replaces the deleted linked-lock test.

Squashed from a5a2dade1c and c1e7863392 plus the review's requested
patch; see the review packet for the evidence.

Checks: subagent filter: 824 run, 824 passed (nextest, farm
20261112T062100Z-ref04land-nextest-81c0b0); TUI conformance::events 4/4
(20261112T062102Z-ref04land-golden-bb9851); fmt clean; blocking-calls
budget within budget.

CHANGELOG: the three 0.10.3 coordination-lock entries are replaced by one
entry that says what the code now does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:09 -08:00
60 changed files with 1371 additions and 1161 deletions

No files matched your search

+8 -49
View File
@@ -4,12 +4,11 @@
# alias = Display Name <[email protected]>
#
# The right-hand side must use GitHub's numeric noreply address so a harvested
# commit authored with it, or a Co-authored-by trailer in the commit's final
# trailer block, links to the contributor's account. GitHub resolves the
# address by its numeric id, so the id must be the contributor's own
# (`gh api users/<login> --jq .id`). The left-hand side may be a GitHub login,
# old-style noreply address, raw email from a contributor commit, or local
# machine email seen in older harvested history.
# commit authored with it links to the contributor's account. Only commit
# authorship puts someone in the repository's contributor list; a co-author
# trailer shows on their profile but not there. The left-hand side may be a
# GitHub login, old-style noreply address, raw email from a contributor commit,
# or local machine email seen in older harvested history.
hmbown = Hmbown <[email protected]>
[email protected] = Hmbown <[email protected]>
@@ -51,7 +50,6 @@ Oliver-ZPLiu = Oliver-ZPLiu <[email protected]>
idling11 = idling11 <[email protected]>
Hanmiao Li = idling11 <[email protected]>
[email protected] = idling11 <[email protected]>
894876246 = idling11 <[email protected]>
angziii = angziii <[email protected]>
aboimpinto = aboimpinto <[email protected]>
Paulo Aboim Pinto = aboimpinto <[email protected]>
@@ -81,24 +79,19 @@ Vince = Liu-Vince <[email protected]>
[email protected] = Liu-Vince <[email protected]>
LI-Jialu = LI-Jialu <[email protected]>
JiarenWang = JiarenWang <[email protected]>
wdw8276 = kitty <3972439+mars-base@users.noreply.github.com>
mars-base = kitty <[email protected]>
[email protected] = kitty <[email protected]>
wdw8276 = wdw8276 <3972439+wdw8276@users.noreply.github.com>
pengyou200902 = pengyou200902 <[email protected]>
linzhiqin2003 = linzhiqin2003 <[email protected]>
LING71671 = LING71671 <[email protected]>
[email protected] = LING71671 <[email protected]>
JasonOA888 = JasonOA888 <[email protected]>
Inference1 = Inference1 <[email protected]>
hongqitai = hongqitai <[email protected]>
gordonlu = gordonlu <[email protected]>
angus-guo = gus <217034332+im[email protected]>
imgusguo = gus <217034332+im[email protected]>
[email protected] = gus <[email protected]>
angus-guo = gus <217034332+angus-[email protected]>
gus.guo@tec-do.com = gus <217034332+angus-[email protected]>
gaord = gaord <[email protected]>
Ben Gao = gaord <[email protected]>
[email protected] = gaord <[email protected]>
bengao168 = gaord <[email protected]>
MXAntian = MXAntian <[email protected]>
[email protected] = MXAntian <[email protected]>
DarrellThomas = Darrell Thomas <[email protected]>
@@ -254,7 +247,6 @@ [email protected] = zhuowp <[email protected]>
[email protected] = 秋月凉梦 <[email protected]>
asto18089 = asto18089 <[email protected]>
[email protected] = asto18089 <[email protected]>
xiechimon = Xmon Dai <[email protected]>
yrk111222 = Rongkun Yan <[email protected]>
VincentCorleone = Vincent Corleone <[email protected]>
@@ -286,36 +278,3 @@ Felixkw12 = Felix Wunderlich <[email protected]>
[email protected] = Felix Wunderlich <[email protected]>
Felix Wunderlich = Felix Wunderlich <[email protected]>
[email protected] = luochen211 <[email protected]>
AiurArtanis = AiurArtanis <[email protected]>
[email protected] = AiurArtanis <[email protected]>
Apeiron0w0 = Apeiron <[email protected]>
[email protected] = Apeiron <[email protected]>
[email protected] = ChaceLyee2101 <[email protected]>
AmosLeeeeeeee = ChaceLyee2101 <[email protected]>
ddlw = ddlw <[email protected]>
mo-vic = movic <[email protected]>
[email protected] = movic <[email protected]>
[email protected] = merchloubna70-dot <[email protected]>
[email protected] = merchloubna70-dot <[email protected]>
xulongzhe = luola <[email protected]>
[email protected] = luola <[email protected]>
ZhulongNT = SONG ZHENRUI <[email protected]>
[email protected] = SONG ZHENRUI <[email protected]>
xiluoduyu = xiluoduyu <[email protected]>
[email protected] = xiluoduyu <[email protected]>
wlon = Stephen Xu <[email protected]>
[email protected] = Stephen Xu <[email protected]>
krisclarkdev = Kristopher Clark <[email protected]>
THatch26 = THatch26 <[email protected]>
jeoor = Kayro <[email protected]>
harvey2011888 = harvey2011888 <[email protected]>
duanchao-lab = DC <[email protected]>
dumbjack = dumbjack <[email protected]>
[email protected] = dumbjack <[email protected]>
AquaZhang = IceTear <[email protected]>
disappear00 = idontknow <[email protected]>
namezzb = sailorZ <[email protected]>
alphapku-ux = Alpha Wu <[email protected]>
BX166 = BX166 <[email protected]>
c020627 = c020627 <[email protected]>
-14
View File
@@ -14,17 +14,3 @@ Hunter Bown <[email protected]> Cursor Agent <[email protected]>
Hunter Bown <[email protected]> cursoragent <[email protected]>
Hunter Bown <[email protected]> gemini-code-assist[bot] <gemini-code-assist[bot]@users.noreply.github.com>
Hunter Bown <[email protected]> gemini-code-assist[bot] <956858+gemini-code-assist[bot]@users.noreply.github.com>
ddlw <[email protected]> <[email protected]>
dmitri-0 <[email protected]> <[email protected]>
Paulo Aboim Pinto <[email protected]> <[email protected]>
Paulo Aboim Pinto <[email protected]> <[email protected]>
Paulo Aboim Pinto <[email protected]> <[email protected]>
AiurArtanis <[email protected]> <[email protected]>
Apeiron <[email protected]> <[email protected]>
ChaceLyee2101 <[email protected]> <[email protected]>
movic <[email protected]> <[email protected]>
merchloubna70-dot <[email protected]> <[email protected]>
merchloubna70-dot <[email protected]> <[email protected]>
luola <[email protected]> <[email protected]>
SONG ZHENRUI <[email protected]> <[email protected]>
xiluoduyu <[email protected]> <[email protected]>
+3 -10
View File
@@ -259,16 +259,9 @@ prefer merging the contributor's PR itself). When a contributor's work
lands as our commit, the contributor is that commit's **author** (`git commit
--author` with their GitHub-linked address), it carries `Harvested from PR #N by
@handle`, and maintainers or agents go in `Co-authored-by`. GitHub's contributor
count takes commit authors and the `Co-authored-by` trailers in a commit's final
trailer block, and only addresses linked to an account: a trailer cut off by a
blank line, an unlinked email, or a noreply address carrying someone else's
numeric id credits nobody or the wrong person. Harvested commits authored by
maintainer identities without such a trailer left their contributors off it,
which is why `docs/CONTRIBUTORS.md` carries the full wall
(`scripts/contributor-wall.py`). Credit lost in history that cannot change is
restored by a later commit with one trailer per person, naming each pull
request; `scripts/check-contributor-credit.py` fails the release window when a
harvest, a trailer or a noreply id would leave someone uncounted.
list counts commit authors only: about 200 harvested commits authored by
maintainer identities left their contributors off it, which is why
`docs/CONTRIBUTORS.md` carries the full wall (`scripts/contributor-wall.py`).
A maintainer closes their PR by hand with a thank-you naming the commit;
nothing closes a PR automatically. Canonical human identities come from
`.github/AUTHOR_MAP`.
+1 -13
View File
@@ -669,7 +669,7 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
- **Windows uninstall removes the receipt and update leftovers.** The installer's uninstaller also deletes `.codewhale-install.sha256` and `codewhale.old-*`/`codew.old-*` left by a self-update, so `bin` can be removed. Refs #7091.
- **Windows `/undo` matches recorded file paths regardless of case.** A write recorded under a different drive-letter or directory case, or a `\\?\` prefix, is no longer dropped from the restore plan. Refs #6970.
- **Windows: Fleet and metrics read live files again.** Replaying a Fleet ledger and reading audit or receipt records no longer fail with a sharing violation while their writer is still open; the same regular-file, link and identity checks apply. Plugin enablement now says which snapshot check refused (stage root, content identity or final check) instead of one generic message. Generated protocol and SDK exports keep LF line endings in Windows checkouts.
- **A held coordination lock no longer makes a new session read-only.** When another session owns the delegated-agent store, the new session starts on private state and says so, instead of refusing every sub-agent with “Failed to clean up sub-agents”.
- **Sessions in one folder no longer leave each other's sub-agents read-only or private:** a second session works beside another one, stale or suspended included, the older sub-agent store upgrades right away, and a store that cannot be opened is retried on the next sub-agent change instead of needing a restart. While another Codewhale session, a suspended one included, is open in a folder the desktop app or `serve` was asked to work in (other than the folder it started in), that Runtime still refuses sub-agents that would edit the folder directly; isolated worktrees are unaffected. A sub-agent store that cannot be opened shows its refusal once instead of repeating cleanup warnings on every sidebar refresh. Restart any session started with Codewhale 0.10.2 or older: once the folder upgrades it can no longer save sub-agent state (Refs #7232).
- **A dev server can outlive the turn in an interactive session.** `persist:true` now works in the TUI and desktop when the thread has Full Access; restricted postures and Fleet/sub-agents still refuse it.
- **`codewhale exec --add-dir <path>`** (repeatable) and `addDirs` on `POST /v1/threads` let the file tools read and write a sibling directory, a second checkout or a shared scratch root without turning on Full Access.
- **Unsettled usage is persisted in production, not only in tests.** The interactive sink now records a legacy usage receipt when a provider response has no settlement, so children and turns stop reporting settlement failure.
@@ -706,12 +706,6 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
composer. Real Pinyin input on macOS and Windows is not yet confirmed.
Thanks [@cmdcorp6534](https://github.com/cmdcorp6534) for the report
(Refs #2323).
- **Sub-agents beside an older session:** while a Codewhale 0.10.2 session
still uses a folder's older delegated-agent store, a 0.10.3 session in the
same folder keeps working sub-agents with private state that is not saved,
instead of refusing every sub-agent action. The older session's files are
left untouched, and the store upgrades when a session starts after the older
one exits (Refs #7232).
- **Fleet members:** a replayed member start now matches only within its own
session, so two sessions that reuse an operation key no longer receive each
other's agent.
@@ -740,12 +734,6 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
bare self-package exports resolve inside the selected package directory.
Invalid or incompatible ranges and paths outside the reviewed package still
refuse. Real published-plugin activation remains unqualified.
- **Coordination lock diagnostics (#7232):** stale lock-file contents no longer
identify an owner PID. A refused legacy admission keeps its reason in Work
and shows one notice per cause instead of repeating cleanup warnings on
every sidebar refresh. Legacy migration still requires the actual holder to
exit and the refused owning Runtime to restart; locks and retained state
remain protected.
- **A local proxy that stops mid-session no longer strands the session (#7231):** the model, catalog, search, vision, finance, MCP-registry, profile and remote-control clients watch a loopback proxy port and go direct while it refuses connections, then resume proxying when it returns. Only a definite refusal from every resolved address changes the route; remote proxies, `NO_PROXY` matches, credentials and the existing precedence are unchanged. Set `CODEWHALE_LOCAL_PROXY_FALLBACK=0` to keep the old build-time snapshot.
- **Proxy diagnostic privacy ([#7233](https://github.com/codewhale-hq/codewhale/issues/7233)):** malformed MCP proxy warnings omit configured values; local-proxy log labels mask userinfo and omit URL paths, queries and fragments. Routing is unchanged. All 23 existing proxy tests passed on macOS; runtime log capture and other platforms remain unqualified. Thanks [@SparkofSpike](https://github.com/SparkofSpike) for the report.
- **Fork, undo and retry work after a workspace drift notice (#7234):** the history-boundary walks now treat a whole `<context_update>` envelope as engine-owned, so a session that received one can be cut at every saved turn. A person quoting a notice or a truncated envelope still counts as their own prompt.
+7 -1
View File
@@ -605,7 +605,7 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
def(
"show_thinking",
SettingKind::Bool(SHOW_THINKING),
"false",
"true",
ui(
TAB_APPEARANCE,
"display",
@@ -1546,6 +1546,12 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
"false",
None,
),
def(
"account_preferences_notice_shown",
SettingKind::String,
"",
None,
),
// Round 3 work-bar placement migration ran once (`top` → `bottom`).
def(
"work_surface_bottom_migrated",
+2 -2
View File
@@ -314,8 +314,8 @@ pub struct EngineConfig {
pub session_id: Option<String>,
/// Optional host-owned root for delegated-agent runtime state.
///
/// When unset, the worker ledger, complete transcript artifacts and
/// coordination lock retain their historical location under
/// When unset, the worker ledger and complete transcript artifacts
/// retain their historical location under
/// `workspace/.codewhale/state`. Embedders may set a session-scoped root
/// to separate that control-plane state from the execution workspace.
/// Child cwd and file authority still derive from `workspace`; hosts using
+1 -2
View File
@@ -11615,10 +11615,9 @@ async fn compact_thread_receipt_keeps_exact_named_custom_identity() -> Result<()
#[tokio::test]
async fn compact_thread_with_real_engine_reaches_terminal_status() -> Result<()> {
// A real engine takes the workspace coordination lock exclusively, and
// nextest runs tests in parallel processes with this crate as their working
// directory. A `"."` workspace would therefore be the crate directory
// itself, and two exclusive holders of it cannot both start.
// itself, shared by every test process.
let workspace = tempfile::tempdir()?;
let manager = RuntimeThreadManager::open(
{
+1 -1
View File
@@ -1721,7 +1721,7 @@ mod tests {
.expect_err("mutation must fail when its receipt cannot persist")
.to_string();
assert!(
error.contains("Runtime coordination was opened without its writer lock"),
error.contains("delegated durable store admission failed"),
"{error}"
);
assert!(
@@ -326,9 +326,10 @@ pub struct CoordinationDetailProjection {
pub metrics: CoordinationDetailMetrics,
pub bounded: bool,
pub limit: usize,
/// Whether this process currently holds the workspace coordination flock.
/// When false, durable ledger writes are skipped and the UI must say so —
/// a counter must never tick on a turn the engine has already settled.
/// Whether this manager's durable store is admitted. When false, durable
/// ledger writes are refused until admission is retried and the UI must
/// say so — a counter must never tick on a turn the engine has already
/// settled.
#[serde(default = "default_process_lock_held")]
pub process_lock_held: bool,
/// Human-readable reason when [`Self::process_lock_held`] is false.
+134 -435
View File
@@ -499,7 +499,6 @@ const SUBAGENT_TRANSCRIPT_ARTIFACT_DIR: &str = "subagent-transcripts";
const SUBAGENT_STATE_SCHEMA_VERSION: u32 = 1;
const SUBAGENT_STATE_FILE: &str = "subagents.v3.json";
const LEGACY_SUBAGENT_STATE_FILE: &str = "subagents.v1.json";
const SUBAGENT_STATE_LOCK_FILE: &str = "subagents.v1.lock";
const SUBAGENT_RESTART_REASON: &str = "Interrupted by process restart";
const SUBAGENT_SESSION_CLOSED_REASON: &str = "Interrupted: parent session closed";
#[cfg(test)]
@@ -3933,12 +3932,6 @@ impl SubAgent {
}
}
/// Manager for active sub-agents.
struct CoordinationProcessLock {
release: Option<std::sync::mpsc::SyncSender<()>>,
thread: Option<std::thread::JoinHandle<()>>,
}
/// Marker prefix retained for presentation compatibility with same-process
/// handover notes from earlier hosts. It does not control lock acquisition
/// or retry.
@@ -3954,123 +3947,6 @@ pub const COORDINATION_LOCK_TIMEOUT_MARKER: &str =
pub const SESSION_PRIVATE_COORDINATION: &str = "another Codewhale session still uses this folder's older delegated-agent store, so its upgrade waits; sub-agents in this session keep private state that other sessions do not see and that is not saved, and the store upgrades when a session starts after the other one exits";
#[derive(Debug)]
struct SubAgentCutoverBlocked(anyhow::Error);
impl std::fmt::Display for SubAgentCutoverBlocked {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for SubAgentCutoverBlocked {}
impl CoordinationProcessLock {
fn acquire(state_root: &Path, exclusive: bool) -> Result<Self> {
let requested_root = normalize_subagent_workspace(state_root);
// Root and path for the lock. A non-home workspace keeps itself as the
// root, so a repository-supplied `.codewhale` link is walked and
// refused before `create_dir_all` could follow it into the target. A
// home workspace resolves its own junction once and still walks
// everything below it.
let (walk_root, lock_path) = codewhale_util::files::workspace_state_path(
&requested_root,
&Path::new("state").join(SUBAGENT_STATE_LOCK_FILE),
);
let lock_dir = lock_path
.parent()
.map_or_else(|| walk_root.clone(), Path::to_path_buf);
// Refuse a linked `.codewhale` or `state` before creating anything:
// `create_dir_all` follows links and would populate the target.
reject_root_relative_symlinks(&walk_root, &lock_path)?;
fs::create_dir_all(&lock_dir)?;
let ignore = lock_dir.join(".gitignore");
let ignored = reject_root_relative_symlinks(&walk_root, &ignore).and_then(|()| {
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&ignore)
{
Ok(mut file) => file.write_all(b"*\n").map_err(Into::into),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(error) => Err(error.into()),
}
});
if let Err(error) = ignored {
tracing::warn!(target: "subagent", %error, "could not write {}", ignore.display());
}
// Creating a missing root can change its canonical spelling on
// Windows (for example by adding a `\\?\` prefix). Re-resolve both
// sides before checking containment.
let state_root = normalize_subagent_workspace(state_root);
let (walk_root, lock_path) = codewhale_util::files::workspace_state_path(
&state_root,
&Path::new("state").join(SUBAGENT_STATE_LOCK_FILE),
);
reject_root_relative_symlinks(&walk_root, &lock_path)?;
let lock_file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&lock_path)?;
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
let (release_tx, release_rx) = std::sync::mpsc::sync_channel(0);
let thread = std::thread::spawn(move || {
let mut lock = fd_lock::RwLock::new(lock_file);
let acquired = if exclusive {
lock.try_write().map(|guard| {
let _ = ready_tx.send(Ok::<(), String>(()));
let _guard = guard;
let _ = release_rx.recv();
})
} else {
lock.try_read().map(|guard| {
let _ = ready_tx.send(Ok::<(), String>(()));
let _guard = guard;
let _ = release_rx.recv();
})
};
match acquired {
Ok(()) => {}
Err(error) => {
let _ = ready_tx.send(Err(error.to_string()));
}
}
});
match ready_rx.recv_timeout(Duration::from_secs(5)) {
Ok(Ok(())) => Ok(Self {
release: Some(release_tx),
thread: Some(thread),
}),
Ok(Err(error)) => {
let _ = thread.join();
Err(anyhow!(
"delegated coordination lock is unavailable for {}: {error}; close any other running or suspended Codewhale session using this workspace, then restart this Runtime if admission was refused",
state_root.display()
))
}
Err(error) => {
drop(release_tx);
let _ = thread.join();
Err(anyhow!(
"{COORDINATION_LOCK_TIMEOUT_MARKER} for {}: {error}",
state_root.display()
))
}
}
}
}
impl Drop for CoordinationProcessLock {
fn drop(&mut self) {
self.release.take();
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
/// Only the captured Runtime owner admits roots. Held handles are not serialized;
/// a restored attributed claim must be re-admitted before it can authorize work.
struct AdmittedCoordinationRoot {
@@ -4106,19 +3982,12 @@ pub struct SubAgentManager {
worker_origins: HashMap<String, WorkerOrigin>,
worker_origin_schema: u32,
workspace: PathBuf,
/// Root that owns the delegated-agent ledger, complete transcript
/// artifacts and coordination lock. Defaults to `workspace`; embedders may
/// Root that owns the delegated-agent ledger and complete transcript
/// artifacts. Defaults to `workspace`; embedders may
/// isolate it without changing child execution cwd or file authority.
state_root: PathBuf,
state_path: Option<PathBuf>,
state_unreachable_warned: bool,
coordination_process_lock: std::sync::Mutex<Option<Arc<CoordinationProcessLock>>>,
coordination_process_lock_required: bool,
coordination_exclusive: bool,
coordination_snapshot_read_only: bool,
store_cutover_blocked: bool,
session_private: bool,
workspace_writer_locks: HashMap<PathBuf, Arc<CoordinationProcessLock>>,
/// Configured default per-child model-turn budget (`[subagents]
/// default_max_steps`, #5324). `None` keeps the unbounded Fleet default;
/// an explicit spawn `max_steps` still wins. Zero means unbounded.
@@ -4759,13 +4628,6 @@ impl SubAgentManager {
state_root,
state_path: None,
state_unreachable_warned: false,
coordination_process_lock: std::sync::Mutex::new(None),
coordination_process_lock_required: false,
coordination_exclusive: false,
coordination_snapshot_read_only: false,
store_cutover_blocked: false,
session_private: false,
workspace_writer_locks: HashMap::new(),
max_steps: None,
wall_time: None,
max_agents,
@@ -4831,22 +4693,6 @@ impl SubAgentManager {
return Err("coordination workspace identity is unsupported on this platform".into());
#[cfg(any(unix, windows))]
{
if self.coordination_exclusive
&& !self.coordination_snapshot_read_only
&& normalize_subagent_workspace(&lexical)
!= normalize_subagent_workspace(&self.state_root)
&& !self.workspace_writer_locks.contains_key(&canonical_root)
{
match CoordinationProcessLock::acquire(&lexical, true) {
Ok(lock) => {
self.workspace_writer_locks
.insert(canonical_root.clone(), Arc::new(lock));
}
Err(error) => {
tracing::warn!(target: "subagent", %error, "admitted coordination workspace without its writer lock; shared-workspace writers there stay refused until it is free");
}
}
}
let receipt = CoordinationClaimScope {
canonical_root,
platform: platform.into(),
@@ -4866,44 +4712,61 @@ impl SubAgentManager {
}
}
fn ensure_origin_writer_lock(&mut self, origin: &WorkerOrigin) -> Result<(), String> {
let (Some(selected), Some(scope)) = (&origin.selected_workspace, &origin.scope) else {
fn refuse_live_selected_store_writers(&self, origin: &WorkerOrigin) -> Result<(), String> {
let (Some(selected), Some(_)) = (&origin.selected_workspace, &origin.scope) else {
return Ok(());
};
if !self.coordination_exclusive
|| normalize_subagent_workspace(selected)
== normalize_subagent_workspace(&self.state_root)
|| self
.workspace_writer_locks
.contains_key(&scope.canonical_root)
if normalize_subagent_workspace(selected) == normalize_subagent_workspace(&self.state_root)
{
return Ok(());
}
let lock =
CoordinationProcessLock::acquire(selected, true).map_err(|error| error.to_string())?;
self.workspace_writer_locks
.insert(scope.canonical_root.clone(), Arc::new(lock));
let path = default_state_path(selected).map_err(|error| error.to_string())?;
let unreadable = |error: std::io::Error| {
format!(
"cannot check {} for another open Codewhale session ({error}); shared-workspace writers there stay refused, or launch the builder in an isolated worktree",
selected.display()
)
};
let directory = path
.parent()
.ok_or_else(|| "delegated store has no parent".to_string())?;
let entries = match fs::read_dir(directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(unreadable(error)),
};
for entry in entries {
let name = entry.map_err(unreadable)?.file_name();
let Some(writer) = name
.to_str()
.and_then(|name| name.strip_prefix("subagents.v3.writer."))
.and_then(|name| name.strip_suffix(".lock"))
else {
continue;
};
if valid_store_writer(writer) && store_writer_is_live(&path, writer) {
return Err(format!(
"another Codewhale session ({writer}) has {} open; shared-workspace writers there stay refused until it exits, or launch the builder in an isolated worktree",
selected.display()
));
}
}
Ok(())
}
fn ensure_worker_coordination_locks(
&mut self,
fn admit_worker_shared_writes(
&self,
spec: &AgentWorkerSpec,
origin: &WorkerOrigin,
) -> Result<(), String> {
let claim = worker_coordination_claim(spec)?;
if claim.as_ref().is_some_and(|(_, isolated)| *isolated) {
return Ok(());
match worker_coordination_claim(spec)? {
Some((_, false)) => self.refuse_live_selected_store_writers(origin),
_ => Ok(()),
}
self.ensure_coordination_process_lock()?;
if claim.is_some() {
self.ensure_origin_writer_lock(origin)?;
}
Ok(())
}
pub fn ensure_registered_worker_coordination_locks(
&mut self,
&self,
worker_id: &str,
) -> Result<(), String> {
let spec = self
@@ -4918,7 +4781,7 @@ impl SubAgentManager {
.cloned()
.ok_or_else(|| "worker origin is missing from the owner journal".to_string())?;
self.validate_worker_origin(&origin)?;
self.ensure_worker_coordination_locks(&spec, &origin)
self.admit_worker_shared_writes(&spec, &origin)
}
fn admitted_coordination_root(&self, workspace: &Path) -> Option<&AdmittedCoordinationRoot> {
@@ -5316,7 +5179,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
if let Some(root) = self.admitted_coordination_root(workspace) {
root.validate()?;
}
@@ -5395,7 +5257,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
self.coordination
.update_decision_status(decision_id, status, owner, expected_version)
}
@@ -5442,7 +5303,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
let expected_owner = self.nearest_common_fan_in_owner(&input_decisions)?;
if owner != expected_owner {
return Err(format!(
@@ -5714,19 +5574,13 @@ impl SubAgentManager {
},
bounded: true,
limit,
process_lock_held: !self.session_private && {
// Retry acquire on each projection so a session that lost the
// flock at construction recovers once the previous owner exits.
let _ = self.coordination_process_lock_status();
self.holds_coordination_process_lock()
},
process_lock_note: if self.session_private {
Some(SESSION_PRIVATE_COORDINATION.to_string())
} else if self.holds_coordination_process_lock() {
None
} else {
self.coordination_process_lock_status().err()
},
process_lock_held: self.state_path.is_none() || self.store_authority.is_some(),
process_lock_note: (self.state_path.is_some() && self.store_authority.is_none()).then(
|| {
"delegated store admission failed; it is retried on the next delegated write"
.to_string()
},
),
}
}
@@ -5861,7 +5715,6 @@ impl SubAgentManager {
.and_then(|outcome| outcome);
}
self.ensure_coordination_process_lock()?;
let Some(existing) = self
.coordination
.write_claims
@@ -5948,7 +5801,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
let active = self.active_coordination_owners();
let released = self
.coordination
@@ -6073,109 +5925,12 @@ impl SubAgentManager {
match SubAgentStoreAuthority::open(&self.state_root, &path, &self.current_session_boot_id) {
Ok(authority) => self.store_authority = Some(authority),
Err(error) => {
self.coordination_snapshot_read_only = true;
self.store_cutover_blocked = error
.chain()
.any(|cause| cause.is::<SubAgentCutoverBlocked>());
tracing::warn!(target: "subagent", %error, "delegated store admission failed; writable state remains refused");
tracing::warn!(target: "subagent", %error, "delegated store admission failed; it is retried on the next delegated write");
}
}
self
}
fn into_session_private(mut self) -> Self {
self.state_path = None;
self.store_authority = None;
self.coordination_snapshot_read_only = false;
self.coordination_process_lock_required = false;
self.session_private = true;
tracing::warn!(target: "subagent", "{SESSION_PRIVATE_COORDINATION}");
self
}
fn require_coordination_process_lock(mut self) -> Self {
self.coordination_process_lock_required = true;
if self.store_authority.is_none()
&& let Some(path) = self.state_path.as_ref()
{
match SubAgentStoreAuthority::open(
&self.state_root,
path,
&self.current_session_boot_id,
) {
Ok(authority) => self.store_authority = Some(authority),
Err(error) => {
self.coordination_snapshot_read_only = true;
tracing::warn!(target: "subagent", %error, "delegated store migration or admission failed; no durable writes are admitted");
return self;
}
}
}
let acquired =
CoordinationProcessLock::acquire(&self.state_root, self.coordination_exclusive)
.ok()
.map(Arc::new);
if acquired.is_none() {
return self.into_session_private();
}
*self
.coordination_process_lock
.get_mut()
.expect("coordination lock slot poisoned") = acquired;
self
}
/// Terminal sessions retry failed acquisition when the previous owner
/// exits. A Runtime loaded without its writer lock must restart rather
/// than publish its stale snapshot. The flock is held on an open fd —
/// deleting the lock file does not release it (#5036).
fn ensure_coordination_process_lock(&self) -> Result<(), String> {
if self.coordination_snapshot_read_only {
return Err(
"Runtime coordination was opened without its writer lock; stop and restart this Runtime process after the other session exits"
.into(),
);
}
if !self.coordination_process_lock_required {
return Ok(());
}
let mut slot = self
.coordination_process_lock
.lock()
.expect("coordination lock slot poisoned");
if slot.is_some() {
return Ok(());
}
match CoordinationProcessLock::acquire(&self.state_root, self.coordination_exclusive) {
Ok(lock) => {
*slot = Some(Arc::new(lock));
Ok(())
}
Err(error) => Err(error.to_string()),
}
}
/// Whether this process currently owns the workspace coordination flock.
/// Read-only inspection of the in-process slot — does not attempt acquire.
#[must_use]
pub fn holds_coordination_process_lock(&self) -> bool {
if !self.coordination_process_lock_required {
// Tests and non-durable managers do not require the flock.
return true;
}
self.coordination_process_lock
.lock()
.expect("coordination lock slot poisoned")
.is_some()
}
/// Probe lock ownership for UI/inspect surfaces. Retries acquire when the
/// slot is empty so a terminal session can recover after the previous
/// owner exits (#2.6 / #5036); read-only Runtime snapshots require restart.
pub fn coordination_process_lock_status(&self) -> Result<(), String> {
self.ensure_coordination_process_lock()
}
#[must_use]
pub fn with_running_heartbeat_timeout(mut self, timeout: Duration) -> Self {
self.running_heartbeat_timeout = if timeout.is_zero() {
@@ -6223,18 +5978,31 @@ impl SubAgentManager {
.is_some_and(|authority| authority.owned.lock().contains(id))
}
fn admitted_store_authority(&mut self) -> Result<Option<Arc<SubAgentStoreAuthority>>> {
let Some(path) = self.state_path.as_ref() else {
return Ok(None);
};
if let Some(authority) = self.store_authority.as_ref() {
return Ok(Some(authority.clone()));
}
let authority =
SubAgentStoreAuthority::open(&self.state_root, path, &self.current_session_boot_id)
.map_err(|error| {
anyhow!(
"delegated durable store admission failed ({error:#}); it is retried on the next delegated write"
)
})?;
self.store_authority = Some(authority.clone());
Ok(Some(authority))
}
pub(crate) fn with_store_transaction<T>(
&mut self,
operation: impl FnOnce(&mut Self) -> Result<T>,
) -> Result<T> {
if self.state_path.is_none() {
let Some(authority) = self.admitted_store_authority()? else {
return operation(self);
}
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
let authority = self.store_authority.clone().ok_or_else(|| {
anyhow!("delegated durable store admission failed; inspect before retrying")
})?;
};
if authority.is_current_transaction() {
return operation(self);
}
@@ -6435,8 +6203,6 @@ impl SubAgentManager {
worker_id: &str,
payload: PersistedSubAgentState,
) -> Result<Option<AgentWorkerRecord>> {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
if self.state_path.is_none() {
return Ok(None);
}
@@ -6534,8 +6300,6 @@ impl SubAgentManager {
/// completes. Callers may `.join()` for synchronous semantics or drop it
/// for fire-and-forget.
fn persist_state(&self) -> Result<std::thread::JoinHandle<()>> {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
let Some((_, payload)) = self.build_persist_payload()? else {
return Ok(std::thread::spawn(|| {}));
};
@@ -6543,16 +6307,7 @@ impl SubAgentManager {
.store_authority
.clone()
.ok_or_else(|| anyhow!("delegated store authority is unavailable"))?;
let writer_locks = self
.coordination_process_lock
.lock()
.expect("coordination lock slot poisoned")
.iter()
.cloned()
.chain(self.workspace_writer_locks.values().cloned())
.collect::<Vec<_>>();
Ok(std::thread::spawn(move || {
let _writer_locks = writer_locks;
if let Err(error) = authority.publish(&payload, None, None) {
tracing::warn!(target: "subagent", %error, "failed to persist sub-agent checkpoint");
}
@@ -6560,8 +6315,6 @@ impl SubAgentManager {
}
fn persist_state_synchronously(&self) -> Result<()> {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
let Some((_, payload)) = self.build_persist_payload()? else {
return Ok(());
};
@@ -6624,8 +6377,6 @@ impl SubAgentManager {
/// Unlike `persist_state`, this performs disk I/O **synchronously** to
/// guarantee data is flushed before the process exits.
pub fn flush_pending_persist(&mut self) -> Result<()> {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
if self.persist_pending {
self.persist_state_synchronously()?;
self.last_persist_at = Some(Instant::now());
@@ -6848,8 +6599,7 @@ impl SubAgentManager {
}
pub fn finalize_session_close_for_session(&mut self, active_session_id: &str) -> Result<usize> {
if !self.coordination_snapshot_read_only
&& (self.state_path.is_none() || self.store_authority.is_some())
if (self.state_path.is_none() || self.store_authority.is_some())
&& !self.agents.values().any(|agent| {
self.owns_store_record(&agent.id)
&& self.agent_is_owned_by_session(agent, active_session_id)
@@ -7108,10 +6858,9 @@ impl SubAgentManager {
let Some((claim, isolated_worktree)) = worker_coordination_claim(spec)? else {
return Ok(());
};
// Isolated-worktree builders mutate their own checkout, not the
// shared workspace, so they must not contend for the per-workspace
// coordination process lock (#5036).
self.ensure_worker_coordination_locks(spec, origin)?;
if !isolated_worktree {
self.refuse_live_selected_store_writers(origin)?;
}
let claim = self.namespace_write_claim(&origin_workspace, isolated_worktree, claim)?;
let scope = if isolated_worktree {
None
@@ -7191,11 +6940,7 @@ impl SubAgentManager {
let previous_origins = self.worker_origins.clone();
let previous_schema = self.worker_origin_schema;
let claim = worker_coordination_claim(&spec)?;
// Same isolation rule as `preflight_worker_coordination` (#5036).
let isolated_worktree = claim
.as_ref()
.is_some_and(|(_, isolated_worktree)| *isolated_worktree);
self.ensure_worker_coordination_locks(&spec, &origin)?;
self.admit_worker_shared_writes(&spec, &origin)?;
let previous_worker_records = self.worker_records.clone();
let previous_coordination = self.coordination.clone();
let persisted_claim = claim
@@ -7247,18 +6992,6 @@ impl SubAgentManager {
let worker_id_for_log = spec.worker_id.clone();
self.register_worker_for_session(spec, &origin.owner_session_id, None);
self.install_worker_origin(&worker_id_for_log, origin);
if isolated_worktree && self.ensure_coordination_process_lock().is_err() {
// This process does not own the durable coordination state and an
// isolated-worktree builder must not be blocked on it: register
// in memory only and leave the durable ledger to the lock owner.
// The persist-layer lock check stays intact (#5036).
tracing::warn!(
target: "subagent",
worker = %worker_id_for_log,
"registered isolated-worktree worker in memory only; delegated coordination lock is owned elsewhere"
);
return Ok(());
}
if let Err(error) = self.persist_state_synchronously() {
self.worker_records = previous_worker_records;
self.coordination = previous_coordination;
@@ -7684,7 +7417,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
let worker_id = spec.worker_id.clone();
let previous = self
.worker_records
@@ -10327,20 +10059,9 @@ impl SubAgentManager {
));
}
}
if write_capable {
// Isolated-worktree children mutate their own checkout, so they
// do not contend for the shared-workspace process lock (#5036).
if !options.isolated_worktree {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
self.ensure_origin_writer_lock(&origin)
.map_err(anyhow::Error::msg)?;
}
if self.coordination_process_lock_required && self.state_path.is_none() {
return Err(anyhow!(
"write-capable sub-agent launch requires a durable coordination state path"
));
}
if write_capable && !options.isolated_worktree {
self.refuse_live_selected_store_writers(&origin)
.map_err(anyhow::Error::msg)?;
}
let runtime_origin_turn_id = if runtime.context.runtime.active_thread_id.as_deref()
== Some(runtime.context.state_namespace.as_str())
@@ -10574,7 +10295,9 @@ impl SubAgentManager {
self.resume_targets.insert(source.clone(), agent_id.clone());
}
self.agents.insert(agent_id.clone(), agent);
let persist_result = self.persist_state_synchronously();
let persist_result = self
.admitted_store_authority()
.and_then(|_| self.persist_state_synchronously());
agent = self
.agents
.remove(&agent_id)
@@ -10887,8 +10610,6 @@ impl SubAgentManager {
identity: &ChildActivationIdentity,
) -> Result<impl FnOnce() -> Result<()> + Send + 'static + use<>> {
self.exact_activation(identity)?;
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
let (path, payload) = self
.build_persist_payload()?
.ok_or_else(|| anyhow!("child activation has no durable worker store"))?;
@@ -10913,16 +10634,7 @@ impl SubAgentManager {
.clone()
.ok_or_else(|| anyhow!("child activation has no store authority"))?;
let identity = identity.clone();
let writer_locks = self
.coordination_process_lock
.lock()
.expect("coordination lock slot poisoned")
.iter()
.cloned()
.chain(self.workspace_writer_locks.values().cloned())
.collect::<Vec<_>>();
Ok(move || {
let _writer_locks = writer_locks;
store_authority.publish(&payload, None, None)?;
let persisted = read_durable_subagent_store(&state_root, &path)?.state;
let worker = persisted
@@ -11509,13 +11221,10 @@ impl SubAgentManager {
}
}
// Deliberately NOT here: a pass that terminalized every Running agent
// without a live task handle whenever this process lacked the
// coordination flock (#2.6). Two Codewhale sessions in one workspace is
// ordinary usage, and failure to append to a shared bookkeeping ledger
// is not evidence that live work has stopped. Liveness is decided by
// the heartbeat above — which is actual evidence — never by who owns
// the flock (owner report, 2026-08-04). See `docs/architecture/
// delegated-coordination.md` for what lock loss legitimately costs.
// without a live task handle because another session shares the
// workspace (#2.6). Two Codewhale sessions in one workspace is ordinary
// usage, and liveness is decided by the heartbeat above — which is
// actual evidence (owner report, 2026-08-04).
// The retained lineage owns continuation lookup and descendant
// control. Keep the agents projection while live work references it.
@@ -11645,11 +11354,12 @@ impl SubAgentManager {
}
/// #3803: whether the next sidebar refresh should attempt cleanup.
/// Read-only managers without a process lock only render their refusal.
/// Other managers use the last attempt's interval to bound cleanup locking.
/// A manager whose durable store is not admitted only renders its refusal;
/// admission is retried on the next delegated write. Other managers use the
/// last attempt's interval to bound cleanup locking.
#[must_use]
pub fn cleanup_due(&self, min_interval: Duration) -> bool {
(!self.coordination_snapshot_read_only || self.holds_coordination_process_lock())
(self.state_path.is_none() || self.store_authority.is_some())
&& self
.last_cleanup_at
.is_none_or(|last| last.elapsed() >= min_interval)
@@ -13790,22 +13500,6 @@ fn ensure_subagent_store_cutover(root: &Path, path: &Path) -> Result<()> {
let _ = codewhale_config::private_directory::PrivateDirectory::inspect(&legacy)?;
return Ok(());
}
let _migration = if legacy.is_dir() {
None
} else {
Some(
CoordinationProcessLock::acquire(root, true)
.map_err(|error| anyhow::Error::new(SubAgentCutoverBlocked(error)))?,
)
};
if path.exists() {
read_durable_subagent_store(root, path)?;
anyhow::ensure!(
legacy.is_dir(),
"delegated migration did not retain its legacy-write tombstone"
);
return Ok(());
}
let staging = parent.join("subagents.v1.cutover");
let backup_name = "legacy.json";
let original_name = "original.json";
@@ -13882,6 +13576,20 @@ fn ensure_subagent_store_cutover(root: &Path, path: &Path) -> Result<()> {
write_json_atomic(root, path, &DurableSubAgentStore::from_legacy(state))
}
fn write_state_gitignore(root: &Path, path: &Path) -> Result<()> {
let ignore = checked_subagent_state_path(root, &path.with_file_name(".gitignore"))?;
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&ignore)
{
Ok(mut file) => file.write_all(b"*\n")?,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error.into()),
}
Ok(())
}
fn store_writer_is_live(path: &Path, writer: &str) -> bool {
if !valid_store_writer(writer) {
return true;
@@ -13954,6 +13662,9 @@ impl SubAgentStoreAuthority {
let _opening = acquire_store_transaction(root, path)?;
ensure_subagent_store_cutover(root, path)?;
let image = read_durable_subagent_store(root, path)?;
if let Err(error) = write_state_gitignore(root, path) {
tracing::warn!(target: "subagent", %error, "could not ignore the delegated state directory");
}
sync_subagent_store_directory(
path.parent()
.ok_or_else(|| anyhow!("delegated store has no parent"))?,
@@ -14494,24 +14205,16 @@ pub fn new_shared_subagent_manager_with_timeout(
)
}
pub fn new_shared_runtime_subagent_manager(
workspace: PathBuf,
workers: usize,
) -> Result<SharedSubAgentManager> {
new_runtime_subagent_manager(workspace, workers, false)
}
pub fn new_exclusive_runtime_subagent_manager(
workspace: PathBuf,
workers: usize,
) -> Result<SharedSubAgentManager> {
new_runtime_subagent_manager(workspace, workers, true)
new_shared_runtime_subagent_manager(workspace, workers)
}
fn new_runtime_subagent_manager(
pub fn new_shared_runtime_subagent_manager(
workspace: PathBuf,
workers: usize,
exclusive: bool,
) -> Result<SharedSubAgentManager> {
type RuntimeManagerRegistry =
parking_lot::Mutex<HashMap<PathBuf, (usize, std::sync::Weak<RwLock<SubAgentManager>>)>>;
@@ -14536,12 +14239,6 @@ fn new_runtime_subagent_manager(
.with_admission_limit(workers)
.with_launch_concurrency(workers)
.with_state_path(state_path);
manager.coordination_exclusive = exclusive;
manager = manager.require_coordination_process_lock();
if let Err(error) = manager.ensure_coordination_process_lock() {
manager.coordination_snapshot_read_only = true;
tracing::warn!(target: "subagent", %error, "Runtime coordination is read-only; stop and restart this Runtime process after the other session exits to load current state and regain delegated writes");
}
manager.load_state()?;
let manager = Arc::new(RwLock::new(manager));
managers.insert(key, (workers, Arc::downgrade(&manager)));
@@ -14551,7 +14248,7 @@ fn new_runtime_subagent_manager(
/// Create a shared sub-agent manager with an explicit control-plane state root.
///
/// `workspace` remains the child execution and file-authority root. The worker
/// ledger, complete transcript artifacts and coordination lock live under
/// ledger and complete transcript artifacts live under
/// `state_root/.codewhale/state`. Distinct state roots intentionally do not
/// share write claims; an embedding host that runs them against the same
/// workspace must provide any required cross-session write coordination.
@@ -14594,25 +14291,8 @@ pub fn new_shared_subagent_manager_with_state_root_and_timeout(
if let Some(state_path) = state_path {
manager = manager.with_state_path(state_path);
}
manager = if manager.store_cutover_blocked {
manager.into_session_private()
} else {
manager.require_coordination_process_lock()
};
if let Err(error) = manager.ensure_coordination_process_lock() {
tracing::warn!(target: "subagent", %error, "this session cannot append to the shared coordination ledger");
}
// Loading is a read. It was previously gated behind the *write* flock,
// which left a second session in the same workspace blind to every
// decision and write claim already recorded — and holding a default,
// empty ledger that would be written straight over the real one the
// moment the first session exited and the flock became acquirable
// (owner report, 2026-08-04). Read unconditionally; the write gate on
// `persist_state`/`persist_state_synchronously` still checks this
// manager's coordinator admission before publishing.
if let Err(err) = manager.load_state() {
manager.coordination_snapshot_read_only = true;
tracing::warn!(target: "subagent", ?err, "failed to load sub-agent state; durable writes remain refused");
tracing::warn!(target: "subagent", ?err, "failed to load sub-agent state");
}
Arc::new(RwLock::new(manager))
}
@@ -23153,10 +22833,29 @@ mod owner_scoped_coordination_tests {
let mut manager = authority.try_write().unwrap();
admit(&mut manager, &original);
admit(&mut manager, &selected);
let independent =
SubAgentManager::new(selected.clone(), 2).require_coordination_process_lock();
assert!(independent.session_private);
assert!(independent.ensure_coordination_process_lock().is_ok());
let independent = SubAgentManager::new(selected.clone(), 2)
.with_state_path(default_state_path(&selected).unwrap());
assert!(independent.store_authority.is_some());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let state_directory = default_state_path(&selected)
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let mode = fs::metadata(&state_directory).unwrap().permissions().mode();
fs::set_permissions(&state_directory, fs::Permissions::from_mode(0o000)).unwrap();
let unreadable =
manager.preflight_worker_coordination(&writer("selected", &selected, "src"));
fs::set_permissions(&state_directory, fs::Permissions::from_mode(mode)).unwrap();
assert!(unreadable.is_err(), "an unlistable store must fail closed");
}
let refused = manager
.preflight_worker_coordination(&writer("selected", &selected, "src"))
.unwrap_err();
assert!(refused.contains("another Codewhale session"), "{refused}");
drop(independent);
manager
.preflight_worker_coordination(&writer("original", &original, "src"))
.unwrap();
+236 -272
View File
@@ -1223,21 +1223,18 @@ fn coordination_hot_paths_count_only_active_authoritative_owners() {
}
#[test]
fn runtime_mount_coordination_lock_is_shared_with_parallel_sessions() {
fn runtime_mount_and_parallel_sessions_both_append_durably() {
let tmp = tempdir().expect("tempdir");
let runtime_authority = new_shared_runtime_subagent_manager(tmp.path().to_path_buf(), 8)
.expect("admit TUI runtime writer");
let session =
SubAgentManager::new(tmp.path().to_path_buf(), 8).require_coordination_process_lock();
session
.ensure_coordination_process_lock()
.expect("a second session appends beside the runtime");
assert!(!session.coordination_snapshot_read_only);
let session = SubAgentManager::new(tmp.path().to_path_buf(), 8)
.with_state_path(default_state_path(tmp.path()).expect("state path"));
assert!(session.store_authority.is_some());
session
.persist_state_synchronously()
.expect("session append");
let runtime = runtime_authority.try_write().expect("runtime lock");
assert!(!runtime.coordination_snapshot_read_only);
assert!(runtime.store_authority.is_some());
runtime
.persist_state_synchronously()
.expect("runtime append");
@@ -1363,10 +1360,9 @@ fn headless_worker_registration_enforces_live_claims_and_projects_context() {
.iter()
.any(|record| record.owner_session_id == "owner-worker-c")
);
let independent =
SubAgentManager::new(tmp.path().to_path_buf(), 8).require_coordination_process_lock();
assert!(independent.session_private);
assert!(independent.ensure_coordination_process_lock().is_ok());
let independent = SubAgentManager::new(tmp.path().to_path_buf(), 8)
.with_state_path(default_state_path(tmp.path()).expect("state path"));
assert!(independent.store_authority.is_some());
drop(independent);
drop(authority_a);
drop(authority_b);
@@ -1575,25 +1571,21 @@ fn prior_session_owners_never_count_as_active_coordination_owners() {
}
#[test]
fn isolated_worktree_workers_skip_the_coordination_process_lock() {
fn parallel_sessions_admit_shared_and_isolated_writers_durably() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let state_path = default_state_path(&workspace).expect("state path");
let holder = SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
holder
.ensure_coordination_process_lock()
.expect("holder owns the lock");
let holder = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path.clone());
assert!(holder.store_authority.is_some());
let mut contender =
SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
contender
.ensure_coordination_process_lock()
.expect("second manager now also owns shared lock (coexistence)");
let mut contender = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path);
assert!(contender.store_authority.is_some());
let shared = make_write_worker_spec("shared-writer", workspace.clone(), "src/shared");
contender
.preflight_worker_coordination(&shared)
.expect("shared-workspace writer proceeds with shared lock");
.expect("shared-workspace writer proceeds beside another session");
let mut isolated = make_write_worker_spec("isolated-writer", workspace.clone(), "src/iso");
isolated
@@ -1603,69 +1595,64 @@ fn isolated_worktree_workers_skip_the_coordination_process_lock() {
.worktree = true;
contender
.preflight_worker_coordination(&isolated)
.expect("isolated-worktree writer preflights without the lock");
.expect("isolated-worktree writer preflights beside another session");
contender
.register_worker_with_coordination(isolated)
.expect("isolated-worktree writer registers without the lock");
// Once the previous owner exits, a manager that failed earlier must
// acquire the lock on retry instead of replaying a memoized failure.
.expect("isolated-worktree writer registers durably");
drop(holder);
contender
.ensure_coordination_process_lock()
.expect("lock acquisition retries after the holder exits");
contender
.preflight_worker_coordination(&shared)
.expect("shared-workspace writer proceeds once the lock is held");
.expect("shared-workspace writer proceeds after the other session exits");
}
#[test]
fn coordination_detail_projection_reports_process_lock_ownership() {
fn coordination_detail_projection_reports_store_admission_and_its_retry() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let state_path = default_state_path(&workspace).expect("state path");
let holder = SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
holder
.ensure_coordination_process_lock()
.expect("holder owns the lock");
let holder = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path.clone());
let held = holder.coordination_detail_projection(None, 8);
assert!(
held.process_lock_held,
"holder projection must report lock held"
);
assert!(held.process_lock_held);
assert!(held.process_lock_note.is_none());
let contender = SubAgentManager::new(workspace, 4).require_coordination_process_lock();
let contender = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path.clone());
let shared = contender.coordination_detail_projection(None, 8);
// With shared locks both holders coexist — contender reports held and no note.
assert!(shared.process_lock_held);
assert!(shared.process_lock_note.is_none());
let committed = std::fs::read(&state_path).expect("committed store");
std::fs::write(&state_path, b"{").expect("corrupt store");
let mut refused = SubAgentManager::new(workspace, 4).with_state_path(state_path.clone());
let projection = refused.coordination_detail_projection(None, 8);
assert!(!projection.process_lock_held);
assert!(projection.process_lock_note.is_some());
assert!(!refused.cleanup_due(Duration::ZERO));
assert!(refused.release_stale_write_claims(None).is_err());
assert!(!refused.cleanup_due(Duration::ZERO));
std::fs::write(&state_path, committed).expect("restore store");
refused
.release_stale_write_claims(None)
.expect("admission is retried once the store is readable");
assert!(
shared.process_lock_held,
"contender projection must report lock held with shared locks"
);
assert!(
shared.process_lock_note.is_none(),
"shared projection carries no note"
refused
.coordination_detail_projection(None, 8)
.process_lock_held
);
assert!(refused.cleanup_due(Duration::ZERO));
}
/// A second Codewhale session in the same workspace is ordinary usage. Losing
/// the coordination flock means "I cannot append to the shared ledger" — it
/// must never mean "my agents are dead". Bookkeeping failure and liveness are
/// separate concerns (owner report, 2026-08-04).
/// A second Codewhale session in the same workspace is ordinary usage and
/// must never mean "my agents are dead". Bookkeeping and liveness are separate
/// concerns (owner report, 2026-08-04).
#[test]
fn a_second_session_without_the_lock_keeps_its_running_agents_live() {
fn a_second_session_keeps_its_running_agents_live() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let holder = SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
holder
.ensure_coordination_process_lock()
.expect("holder owns the lock");
let mut contender = SubAgentManager::new(workspace, 4).require_coordination_process_lock();
contender
.ensure_coordination_process_lock()
.expect("contender now also owns shared lock");
let _first = SubAgentManager::new(workspace.clone(), 4);
let mut contender = SubAgentManager::new(workspace, 4);
let (input_tx, _input_rx) = mpsc::unbounded_channel();
let mut agent = SubAgent::new(
@@ -1680,9 +1667,6 @@ fn a_second_session_without_the_lock_keeps_its_running_agents_live() {
PathBuf::from("."),
contender.session_boot_id().to_string(),
);
// Fresh, well inside the heartbeat window: nothing about this agent is
// stale. The only thing "wrong" with it is that this process cannot write
// the shared ledger.
agent.last_activity_at = Instant::now();
let agent_id = agent.id.clone();
contender.agents.insert(agent_id.clone(), agent);
@@ -1692,7 +1676,7 @@ fn a_second_session_without_the_lock_keeps_its_running_agents_live() {
.expect("durable worker operation");
assert_eq!(
cancelled, 0,
"lock loss alone must not terminalize anything"
"another session alone must not terminalize anything"
);
let snap = contender
.get_result(&agent_id)
@@ -1700,26 +1684,21 @@ fn a_second_session_without_the_lock_keeps_its_running_agents_live() {
assert_eq!(
snap.status,
SubAgentStatus::Running,
"a live agent stays Running when this session cannot write the ledger: {:?}",
"a live agent stays Running beside another session: {:?}",
snap.status
);
}
/// A second session shares the workspace coordination read-lock and must load
/// the existing ledger before it persists any of its own state. That preserves
/// prior decisions instead of replacing them with an empty ledger.
/// A second session must load the existing ledger before it persists any of
/// its own state. That preserves prior decisions instead of replacing them
/// with an empty ledger.
#[test]
fn a_second_session_with_the_shared_lock_loads_the_workspace_ledger() {
fn a_second_session_loads_the_workspace_ledger() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let state_path = default_state_path(&workspace).expect("state path");
let mut first = SubAgentManager::new(workspace.clone(), 4)
.with_state_path(state_path.clone())
.require_coordination_process_lock();
first
.ensure_coordination_process_lock()
.expect("first session owns the lock");
let mut first = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path.clone());
first
.record_coordination_decision(DecisionRecord {
decision_id: "shared-decision".to_string(),
@@ -1737,20 +1716,15 @@ fn a_second_session_with_the_shared_lock_loads_the_workspace_ledger() {
.persist_state_synchronously()
.expect("first session persists the ledger");
// Second session in the same workspace while the first still holds the
// shared flock. It coexists and must see the workspace ledger.
let second = new_shared_subagent_manager(workspace.clone(), 4);
{
let guard = second.blocking_read();
assert!(
guard.holds_coordination_process_lock(),
"test premise: the second session joins the shared flock"
);
assert!(guard.store_authority.is_some());
let ledger = guard.coordination_snapshot();
assert_eq!(
ledger.decisions.len(),
1,
"second session must load the workspace ledger it cannot write"
"second session must load the workspace ledger"
);
assert_eq!(ledger.decisions[0].decision_id, "shared-decision");
}
@@ -1759,12 +1733,9 @@ fn a_second_session_with_the_shared_lock_loads_the_workspace_ledger() {
drop(first);
{
let guard = second.blocking_write();
guard
.ensure_coordination_process_lock()
.expect("shared lock remains available after the first session exits");
guard
.persist_state_synchronously()
.expect("second session persists once it owns the lock");
.expect("second session persists after the first exits");
}
let mut replayed = SubAgentManager::new(workspace, 4).with_state_path(state_path);
replayed.load_state().expect("reload the workspace ledger");
@@ -1776,23 +1747,15 @@ fn a_second_session_with_the_shared_lock_loads_the_workspace_ledger() {
}
/// The complement of the test above: an orphan still terminalizes, but only on
/// heartbeat evidence. Lock ownership is not part of the decision either way.
/// heartbeat evidence. Another session is not part of the decision either way.
#[test]
fn cleanup_terminalizes_orphans_on_heartbeat_evidence_not_on_lock_ownership() {
fn cleanup_terminalizes_orphans_on_heartbeat_evidence_not_on_other_sessions() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let holder = SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
holder
.ensure_coordination_process_lock()
.expect("holder owns the lock");
let mut contender = SubAgentManager::new(workspace, 4)
.with_running_heartbeat_timeout(Duration::from_secs(30))
.require_coordination_process_lock();
contender
.ensure_coordination_process_lock()
.expect("contender now also owns shared lock");
let _first = SubAgentManager::new(workspace.clone(), 4);
let mut contender =
SubAgentManager::new(workspace, 4).with_running_heartbeat_timeout(Duration::from_secs(30));
let insert_orphan = |manager: &mut SubAgentManager, name: &str, idle: Duration| {
let (input_tx, _input_rx) = mpsc::unbounded_channel();
@@ -1832,7 +1795,7 @@ fn cleanup_terminalizes_orphans_on_heartbeat_evidence_not_on_lock_ownership() {
.expect("fresh orphan still listed")
.status,
SubAgentStatus::Running,
"a fresh orphan is not terminalized just because the flock is elsewhere"
"a fresh orphan is not terminalized just because another session is open"
);
assert!(
matches!(
@@ -11351,7 +11314,7 @@ async fn write_capable_agent_does_not_launch_when_durable_registration_fails() {
.expect_err("writer must fail before spawn when its durable claim cannot commit")
.to_string();
assert!(
error.contains("durably register") || error.contains("writer lock"),
error.contains("durably register") || error.contains("store admission failed"),
"{error}"
);
@@ -12230,7 +12193,10 @@ fn persist_state_rejects_state_path_outside_state_root() {
.persist_state()
.expect_err("outside state path should fail");
assert!(format!("{err:#}").contains("without its writer lock"));
assert!(
format!("{err:#}").contains("must stay within state root"),
"{err:#}"
);
}
/// A user who keeps application state on another volume makes the workspace's
@@ -12405,9 +12371,7 @@ fn explicit_state_roots_isolate_managers_for_the_same_execution_workspace() {
let mut manager = manager.try_write().expect("manager write lock");
assert_eq!(manager.workspace.as_path(), workspace.as_path());
assert_eq!(manager.state_root.as_path(), expected_root.as_path());
manager
.ensure_coordination_process_lock()
.expect("independent state-root coordination lock");
assert!(manager.store_authority.is_some());
manager.register_worker(make_worker_spec(worker_id, workspace.clone()));
manager
.persist_state_synchronously()
@@ -12462,31 +12426,21 @@ fn persist_state_rejects_symlinked_state_directory() {
);
}
/// A linked `.codewhale` must be refused before anything is created: the
/// coordination lock used to `create_dir_all` through the link first.
#[cfg(unix)]
#[test]
fn coordination_lock_refuses_linked_codewhale_without_writing_through_it() {
fn a_linked_codewhale_admission_writes_no_gitignore_through_it() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().join("workspace");
let outside = tmp.path().join("outside");
let root = tmp.path().canonicalize().expect("canonical root");
let workspace = root.join("workspace");
let outside = root.join("outside");
std::fs::create_dir_all(&workspace).expect("mkdir workspace");
std::fs::create_dir_all(&outside).expect("mkdir outside");
std::os::unix::fs::symlink(&outside, workspace.join(".codewhale")).expect("link .codewhale");
let Err(err) = CoordinationProcessLock::acquire(&workspace, false) else {
panic!("a linked .codewhale must be refused");
};
let message = format!("{err:#}");
let manager = new_shared_subagent_manager(workspace, 4);
assert!(manager.blocking_read().store_authority.is_none());
assert!(
message.contains("must not traverse symlinks")
|| message.contains("must stay within state root"),
"{message}"
);
assert_eq!(
std::fs::read_dir(&outside).expect("read outside").count(),
0,
"nothing may be created through the link"
!outside.join("state").join(".gitignore").exists(),
"a refused admission writes no .gitignore through the link"
);
}
@@ -20874,24 +20828,66 @@ fn write_json_atomic_survives_concurrent_writers() {
assert!(leftover.is_empty(), "temp files leaked: {leftover:?}");
}
fn two_process_decision(id: &str) -> DecisionRecord {
DecisionRecord {
decision_id: id.to_string(),
subject: id.to_string(),
status: DecisionStatus::Accepted,
owner: "root".to_string(),
scope: vec!["src".to_string()],
constraints: Vec::new(),
evidence_handles: Vec::new(),
version: 1,
sequence: 0,
}
}
fn wait_for_two_process_file(path: &Path, child: &mut std::process::Child, what: &str) {
let deadline = Instant::now() + Duration::from_secs(30);
while !path.exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
if !path.exists() {
let _ = child.kill();
panic!("{what} never became ready");
}
}
#[test]
fn coordination_process_lock_rejects_second_process() {
const ROLE_ENV: &str = "CODEWHALE_TEST_COORDINATION_LOCK_ROLE";
const WORKSPACE_ENV: &str = "CODEWHALE_TEST_COORDINATION_LOCK_WORKSPACE";
fn a_stale_session_never_makes_a_new_session_read_only() {
const ROLE_ENV: &str = "CODEWHALE_TEST_COORDINATION_ROLE";
const WORKSPACE_ENV: &str = "CODEWHALE_TEST_COORDINATION_WORKSPACE";
const TEST_NAME: &str =
"tools::subagent::tests::coordination_process_lock_rejects_second_process";
"tools::subagent::tests::a_stale_session_never_makes_a_new_session_read_only";
if let Some(role) = std::env::var_os(ROLE_ENV) {
let workspace = PathBuf::from(std::env::var_os(WORKSPACE_ENV).expect("workspace env"));
if role == "legacy-holder" {
let _legacy_session = CoordinationProcessLock::acquire(&workspace, false)
.expect("an older session holds the shared lock");
std::fs::write(workspace.join("holder.ready"), b"ready").unwrap();
let deadline = Instant::now() + Duration::from_secs(10);
let wait_for_release = || {
let deadline = Instant::now() + Duration::from_secs(60);
while !workspace.join("holder.release").exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
assert!(workspace.join("holder.release").exists(), "release timeout");
};
if role == "legacy-holder" {
let lock_file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(
workspace
.join(".codewhale")
.join("state")
.join("subagents.v1.lock"),
)
.expect("open legacy lock");
let legacy = fd_lock::RwLock::new(lock_file);
let _guard = legacy
.try_read()
.expect("an older session holds the legacy lock");
std::fs::write(workspace.join("holder.ready"), b"ready").unwrap();
wait_for_release();
return;
}
let manager = new_shared_subagent_manager_with_timeout(
@@ -20901,78 +20897,82 @@ fn coordination_process_lock_rejects_second_process() {
Duration::from_secs(30),
4,
);
let mut guard = manager.try_write().unwrap();
assert!(guard.store_authority.is_some(), "{role:?} store admitted");
assert!(
guard
.coordination_detail_projection(None, 8)
.process_lock_held
);
let name = role.to_string_lossy().into_owned();
guard
.record_coordination_decision(two_process_decision(&format!("{name}-decision")))
.expect("record a decision beside the other session");
guard.register_worker(make_worker_spec(
&format!("{name}-worker"),
workspace.clone(),
));
guard
.persist_state_synchronously()
.expect("publish beside the other session");
if role == "holder" {
manager
.try_read()
.unwrap()
.ensure_coordination_process_lock()
.expect("holder owns lock");
std::fs::write(workspace.join("holder.ready"), b"ready").unwrap();
let deadline = Instant::now() + Duration::from_secs(10);
while !workspace.join("holder.release").exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
assert!(workspace.join("holder.release").exists(), "release timeout");
wait_for_release();
} else {
manager
.try_read()
.unwrap()
.ensure_coordination_process_lock()
.expect("second process now also succeeds with shared lock");
assert!(
guard
.coordination_snapshot()
.decisions
.iter()
.any(|decision| decision.decision_id == "holder-decision"),
"the new session merges the open session's ledger"
);
}
return;
}
let dir = tempdir().expect("tempdir");
let workspace = dir.path().canonicalize().expect("canonical workspace");
let test_binary = std::env::current_exe().expect("test binary");
let mut holder = std::process::Command::new(&test_binary)
.arg("--exact")
.arg(TEST_NAME)
.arg("--nocapture")
.env(ROLE_ENV, "holder")
.env(WORKSPACE_ENV, &workspace)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn lock holder");
let deadline = Instant::now() + Duration::from_secs(5);
while !workspace.join("holder.ready").exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
if !workspace.join("holder.ready").exists() {
let _ = holder.kill();
let output = holder.wait_with_output().expect("holder output");
panic!(
"holder never acquired lock:\nstdout:\n{}\nstderr:\n{}",
let spawn = |role: &str, workspace: &Path| {
std::process::Command::new(&test_binary)
.arg("--exact")
.arg(TEST_NAME)
.arg("--nocapture")
.env(ROLE_ENV, role)
.env(WORKSPACE_ENV, workspace)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn session process")
};
let finish = |child: std::process::Child, what: &str| {
let output = child.wait_with_output().expect("session output");
assert!(
output.status.success(),
"{what} failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let contender = std::process::Command::new(&test_binary)
.arg("--exact")
.arg(TEST_NAME)
.arg("--nocapture")
.env(ROLE_ENV, "contender")
.env(WORKSPACE_ENV, &workspace)
.output()
.expect("spawn lock contender");
assert!(
contender.status.success(),
"contender failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&contender.stdout),
String::from_utf8_lossy(&contender.stderr)
);
};
let dir = tempdir().expect("tempdir");
let workspace = dir.path().canonicalize().expect("canonical workspace");
let mut holder = spawn("holder", &workspace);
wait_for_two_process_file(&workspace.join("holder.ready"), &mut holder, "open session");
finish(spawn("contender", &workspace), "new session");
std::fs::write(workspace.join("holder.release"), b"release").unwrap();
let output = holder.wait_with_output().expect("holder output");
assert!(
output.status.success(),
"holder failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
finish(holder, "open session");
let workers = load_persisted_agent_worker_records_with_state_root(&workspace, &workspace)
.expect("read merged store");
for id in ["holder-worker", "contender-worker"] {
assert!(
workers.iter().any(|record| record.spec.worker_id == id),
"{id} survives the other session's publication"
);
}
let mut replayed = SubAgentManager::new(workspace.clone(), 4)
.with_state_path(default_state_path(&workspace).unwrap());
replayed.load_state().expect("reload merged ledger");
assert_eq!(replayed.coordination.decisions.len(), 2);
let legacy_dir = tempdir().expect("tempdir");
let legacy_workspace = legacy_dir
@@ -20988,75 +20988,42 @@ fn coordination_process_lock_rejects_second_process() {
)
.unwrap();
let legacy_bytes = std::fs::read(&legacy_file).unwrap();
let mut legacy_holder = std::process::Command::new(&test_binary)
.arg("--exact")
.arg(TEST_NAME)
.arg("--nocapture")
.env(ROLE_ENV, "legacy-holder")
.env(WORKSPACE_ENV, &legacy_workspace)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn legacy lock holder");
let deadline = Instant::now() + Duration::from_secs(5);
while !legacy_workspace.join("holder.ready").exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
if !legacy_workspace.join("holder.ready").exists() {
let _ = legacy_holder.kill();
let output = legacy_holder
.wait_with_output()
.expect("legacy holder output");
panic!(
"legacy holder never acquired lock:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let private = new_shared_subagent_manager(legacy_workspace.clone(), 4);
let mut legacy_holder = spawn("legacy-holder", &legacy_workspace);
wait_for_two_process_file(
&legacy_workspace.join("holder.ready"),
&mut legacy_holder,
"older session",
);
let upgraded = new_shared_subagent_manager(legacy_workspace.clone(), 4);
{
let guard = private.blocking_read();
let mut guard = upgraded.blocking_write();
assert!(
guard.ensure_coordination_process_lock().is_ok(),
"a session beside an older one keeps working sub-agents"
guard.store_authority.is_some(),
"a stale older session must not leave the new one read-only"
);
let projection = guard.coordination_detail_projection(None, 8);
assert!(!projection.process_lock_held);
assert!(
projection
.process_lock_note
.as_deref()
.is_some_and(|note| note.starts_with(SESSION_PRIVATE_COORDINATION)),
projection.process_lock_held,
"{:?}",
projection.process_lock_note
);
assert!(projection.process_lock_note.is_none());
guard
.record_coordination_decision(two_process_decision("upgraded-decision"))
.expect("the new session writes while the older one is still open");
assert!(guard.cleanup_due(Duration::from_secs(3600)));
}
assert_eq!(
std::fs::read(&legacy_file).unwrap(),
legacy_bytes,
"the older session's store stays untouched"
);
assert!(
!state_dir.join(SUBAGENT_STATE_FILE).exists(),
"no forced cutover while the older session runs"
);
std::fs::write(legacy_workspace.join("holder.release"), b"release").unwrap();
let output = legacy_holder
.wait_with_output()
.expect("legacy holder output");
assert!(
output.status.success(),
"legacy holder failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let upgraded = new_shared_subagent_manager(legacy_workspace, 4);
assert!(
state_dir.join(SUBAGENT_STATE_FILE).exists() && legacy_file.is_dir(),
"the next session upgrades the store once the older one exits"
"the store upgrades without waiting for the older session to exit"
);
assert!(upgraded.blocking_read().holds_coordination_process_lock());
drop(private);
assert_eq!(
std::fs::read(legacy_file.join("legacy.json")).unwrap(),
legacy_bytes,
"the older store is retained byte for byte"
);
std::fs::write(legacy_workspace.join("holder.release"), b"release").unwrap();
finish(legacy_holder, "older session");
}
// === agent(action="wait") + peek throttling (#4097) ===
@@ -24171,35 +24138,32 @@ fn validate_resume_from_source(
}
/// Owner report 2026-08-04: a model/provider switch spawns the new engine's
/// manager while the old engine still holds the workspace coordination flock
/// in this same process. That losing acquisition must classify itself as a
/// same-process handover (not "another Codewhale process"), and must
/// self-heal via the projection retry once the previous owner drops.
/// manager while the old engine's manager is still open in this same process.
/// Both stay admitted, and the new one keeps writing after the old one drops.
#[test]
fn coordination_lock_loss_to_own_process_reads_as_handover_and_self_heals() {
fn a_same_process_engine_handover_keeps_both_managers_admitted() {
let tmp = tempdir().expect("tempdir");
let state_path = default_state_path(tmp.path()).expect("state path");
let first =
SubAgentManager::new(tmp.path().to_path_buf(), 1).require_coordination_process_lock();
SubAgentManager::new(tmp.path().to_path_buf(), 1).with_state_path(state_path.clone());
let second = SubAgentManager::new(tmp.path().to_path_buf(), 1).with_state_path(state_path);
assert!(
first.holds_coordination_process_lock(),
"first manager must own the flock"
first
.coordination_detail_projection(None, 8)
.process_lock_held
);
let second =
SubAgentManager::new(tmp.path().to_path_buf(), 1).require_coordination_process_lock();
assert!(
second.holds_coordination_process_lock(),
"second manager now also holds shared flock (coexistence)"
second
.coordination_detail_projection(None, 8)
.process_lock_held
);
let note = second.coordination_process_lock_status();
assert!(note.is_ok(), "shared lock should not error: {note:?}");
first
.persist_state_synchronously()
.expect("old engine publishes");
drop(first);
assert!(
second.coordination_process_lock_status().is_ok(),
"retry must acquire once the previous engine dropped the flock"
);
assert!(second.holds_coordination_process_lock());
second
.persist_state_synchronously()
.expect("new engine publishes after the old one drops");
}
// -- resume-from-checkpoint tests (checkpoint-based continuation) --
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "actiu",
"ExtensionPromptUnavailable": "Les instruccions de les extensions no estan disponibles; consulta /plugin show.",
"ProfileConstitutionUnavailableLocal": "No s'han pogut carregar les preferències del compte de Codewhale; aquest torn fa servir la configuració local. La clau del proveïdor continua funcionant; executa codewhale login per recuperar les preferències del compte.",
"AccountPreferencesNotLoaded": "Compte no carregat. Executa {command}. La clau del proveïdor continua funcionant.",
"ComposerOversizedSubmitHeld": "No s’ha enviat: el missatge supera els {limit} caràcters i no s’ha pogut desar en un fitxer de text enganxat ({error}). El text complet continua al camp d’entrada: escurça’l o habilita l’escriptura a .codewhale/pastes i torna a prémer Enter.",
"ComposerOversizedSubmitHeldShort": "No s’ha enviat: supera els {limit} caràcters; no s’ha desat el fitxer d’enganxat.",
"CmdPluginOwnerReport": "Amfitrió d’extensions:\n Estat: {state}\n Eines actives ({count}): {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "inactiu",
"PhaseDraft": "esborrany",
"PhaseWorking": "treballant",
"PhaseReasoning": "raonant",
"PhaseReasoning": "pensant",
"PhaseReading": "llegint",
"PhaseUsingTool": "usant una eina",
"PhaseSubagents": "subagents en execució",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "aktiv",
"ExtensionPromptUnavailable": "Anweisungen von Erweiterungen sind nicht verfügbar; prüfe /plugin show.",
"ProfileConstitutionUnavailableLocal": "Die Codewhale-Kontoeinstellungen konnten nicht geladen werden; dieser Durchlauf nutzt deine lokalen Einstellungen. Dein Anbieter-Schlüssel funktioniert weiter; führe codewhale login aus, um die Kontoeinstellungen wiederherzustellen.",
"AccountPreferencesNotLoaded": "Konto nicht geladen. Führe {command} aus. Dein Anbieter-Schlüssel funktioniert weiter.",
"ComposerOversizedSubmitHeld": "Nicht gesendet: Diese Nachricht enthält mehr als {limit} Zeichen und konnte nicht als Einfügedatei gespeichert werden ({error}). Der vollständige Text steht noch im Eingabefeld: Kürze ihn oder mache .codewhale/pastes beschreibbar und drücke dann erneut Enter.",
"ComposerOversizedSubmitHeldShort": "Nicht gesendet: über {limit} Zeichen; Einfügedatei nicht gespeichert.",
"CmdPluginOwnerReport": "Erweiterungshost:\n Status: {state}\n Aktive Werkzeuge ({count}): {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "ruhend",
"PhaseDraft": "entwurf",
"PhaseWorking": "arbeitet",
"PhaseReasoning": "überlegt",
"PhaseReasoning": "denkt nach",
"PhaseReading": "liest",
"PhaseUsingTool": "nutzt Tool",
"PhaseSubagents": "Subagenten laufen",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "live",
"ExtensionPromptUnavailable": "Extension instructions are unavailable; inspect /plugin show.",
"ProfileConstitutionUnavailableLocal": "Codewhale account preferences could not be loaded, so this turn uses your local settings. Your provider key still works; run codewhale login to restore account preferences.",
"AccountPreferencesNotLoaded": "Account not loaded. Run {command}. Your provider key still works.",
"ComposerOversizedSubmitHeld": "Not sent: this message is over {limit} characters and could not be saved as a paste file ({error}). The full text is still in the composer: shorten it, or make .codewhale/pastes writable, then press Enter again.",
"ComposerOversizedSubmitHeldShort": "Not sent: over {limit} characters; paste file not saved.",
"CmdPluginOwnerReport": "Extension host:\n State: {state}\n Live tools ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "idle",
"PhaseDraft": "draft",
"PhaseWorking": "working",
"PhaseReasoning": "reasoning",
"PhaseReasoning": "thinking",
"PhaseReading": "reading",
"PhaseUsingTool": "using tool",
"PhaseSubagents": "agents underway",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "activo",
"ExtensionPromptUnavailable": "Las instrucciones de las extensiones no están disponibles; revisa /plugin show.",
"ProfileConstitutionUnavailableLocal": "No se pudieron cargar las preferencias de la cuenta de Codewhale; este turno usa tu configuración local. Tu clave del proveedor sigue funcionando; ejecuta codewhale login para restaurar las preferencias de la cuenta.",
"AccountPreferencesNotLoaded": "Cuenta no cargada. Ejecuta {command}. Tu clave del proveedor sigue funcionando.",
"ComposerOversizedSubmitHeld": "No se envió: este mensaje supera los {limit} caracteres y no se pudo guardar en un archivo de texto pegado ({error}). El texto completo sigue en el campo de entrada: acórtalo o habilita la escritura en .codewhale/pastes y vuelve a presionar Enter.",
"ComposerOversizedSubmitHeldShort": "No se envió: supera los {limit} caracteres; no se guardó el archivo pegado.",
"CmdPluginOwnerReport": "Host de extensiones:\n Estado: {state}\n Herramientas activas ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "inactivo",
"PhaseDraft": "borrador",
"PhaseWorking": "trabajando",
"PhaseReasoning": "razonando",
"PhaseReasoning": "pensando",
"PhaseReading": "leyendo",
"PhaseUsingTool": "usando herramienta",
"PhaseSubagents": "subagentes en curso",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "actif",
"ExtensionPromptUnavailable": "Les instructions des extensions sont indisponibles ; consultez /plugin show.",
"ProfileConstitutionUnavailableLocal": "Les préférences du compte Codewhale n'ont pas pu être chargées ; ce tour utilise vos réglages locaux. Votre clé de fournisseur fonctionne toujours ; lancez codewhale login pour rétablir les préférences du compte.",
"AccountPreferencesNotLoaded": "Compte non chargé. Lancez {command}. Votre clé de fournisseur fonctionne toujours.",
"ComposerOversizedSubmitHeld": "Non envoyé : ce message dépasse {limit} caractères et n’a pas pu être enregistré dans un fichier de texte collé ({error}). Le texte intégral reste dans la zone de saisie : raccourcissez-le ou autorisez l’écriture dans .codewhale/pastes, puis appuyez à nouveau sur Enter.",
"ComposerOversizedSubmitHeldShort": "Non envoyé : plus de {limit} caractères ; fichier collé non enregistré.",
"CmdPluginOwnerReport": "Hôte des extensions :\n État : {state}\n Outils actifs ({count}) : {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "inactif",
"PhaseDraft": "brouillon",
"PhaseWorking": "travail en cours",
"PhaseReasoning": "raisonnement",
"PhaseReasoning": "réflexion",
"PhaseReading": "lecture",
"PhaseUsingTool": "utilisation d'outil",
"PhaseSubagents": "sous-agents en cours",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "सक्रिय",
"ExtensionPromptUnavailable": "एक्सटेंशन के निर्देश उपलब्ध नहीं हैं; /plugin show से जाँचें।",
"ProfileConstitutionUnavailableLocal": "Codewhale खाते की प्राथमिकताएँ लोड नहीं हो सकीं, इसलिए यह टर्न आपकी स्थानीय सेटिंग्स का उपयोग करता है। आपकी प्रदाता कुंजी काम करती रहेगी; खाते की प्राथमिकताएँ वापस लाने के लिए codewhale login चलाएँ।",
"AccountPreferencesNotLoaded": "खाता लोड नहीं हुआ। {command} चलाएँ। आपकी प्रदाता कुंजी काम करती रहेगी।",
"ComposerOversizedSubmitHeld": "नहीं भेजा गया: इस संदेश में {limit} से अधिक वर्ण हैं और इसे पेस्ट फ़ाइल के रूप में सहेजा नहीं जा सका ({error})। पूरा पाठ अभी भी इनपुट बॉक्स में है: इसे छोटा करें या .codewhale/pastes में लिखने की अनुमति दें, फिर Enter दबाएँ।",
"ComposerOversizedSubmitHeldShort": "नहीं भेजा गया: {limit} से अधिक वर्ण; पेस्ट फ़ाइल सहेजी नहीं गई।",
"CmdPluginOwnerReport": "एक्सटेंशन होस्ट:\n स्थिति: {state}\n सक्रिय टूल ({count}): {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "निष्क्रिय",
"PhaseDraft": "मसौदा",
"PhaseWorking": "काम चल रहा",
"PhaseReasoning": "तर्क चल रहा",
"PhaseReasoning": "सोच रहा",
"PhaseReading": "पढ़ रहा",
"PhaseUsingTool": "टूल इस्तेमाल हो रहा",
"PhaseSubagents": "सबएजेंट चल रहे हैं",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "aktif",
"ExtensionPromptUnavailable": "Instruksi ekstensi tidak tersedia; periksa /plugin show.",
"ProfileConstitutionUnavailableLocal": "Preferensi akun Codewhale tidak dapat dimuat, jadi giliran ini memakai pengaturan lokal. Kunci penyedia tetap berfungsi; jalankan codewhale login untuk memulihkan preferensi akun.",
"AccountPreferencesNotLoaded": "Akun tidak dimuat. Jalankan {command}. Kunci penyedia tetap berfungsi.",
"ComposerOversizedSubmitHeld": "Belum terkirim: pesan ini melebihi {limit} karakter dan tidak dapat disimpan sebagai berkas teks tempelan ({error}). Teks lengkap masih ada di kolom input: persingkat teks atau izinkan penulisan ke .codewhale/pastes, lalu tekan Enter lagi.",
"ComposerOversizedSubmitHeldShort": "Belum terkirim: melebihi {limit} karakter; berkas tempelan tidak tersimpan.",
"CmdPluginOwnerReport": "Host ekstensi:\n Status: {state}\n Alat aktif ({count}): {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "diam",
"PhaseDraft": "draf",
"PhaseWorking": "bekerja",
"PhaseReasoning": "menalar",
"PhaseReasoning": "berpikir",
"PhaseReading": "membaca",
"PhaseUsingTool": "memakai tool",
"PhaseSubagents": "subagen berjalan",
+1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "稼働中",
"ExtensionPromptUnavailable": "拡張機能の指示を利用できません。/plugin show で確認してください。",
"ProfileConstitutionUnavailableLocal": "Codewhale アカウントの設定を読み込めなかったため、このターンはローカル設定を使います。プロバイダーキーはそのまま使えます。アカウント設定を戻すには codewhale login を実行してください。",
"AccountPreferencesNotLoaded": "アカウントを読み込めませんでした。{command} を実行してください。プロバイダーキーはそのまま使えます。",
"ComposerOversizedSubmitHeld": "未送信:メッセージが {limit} 文字を超えており、貼り付けファイルに保存できませんでした({error})。全文は入力欄に残っています。短くするか .codewhale/pastes を書き込み可能にしてから、もう一度 Enter を押してください。",
"ComposerOversizedSubmitHeldShort": "未送信:{limit} 文字超。貼り付けファイルは未保存です。",
"CmdPluginOwnerReport": "拡張機能ホスト:\n 状態: {state}\n 有効なツール ({count}): {tools}",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "실행 중",
"ExtensionPromptUnavailable": "확장 기능의 지침을 사용할 수 없습니다. /plugin show에서 확인하세요.",
"ProfileConstitutionUnavailableLocal": "Codewhale 계정 설정을 불러오지 못해 이번 턴은 로컬 설정을 사용합니다. 공급자 키는 계속 사용할 수 있으며, 계정 설정을 복원하려면 codewhale login을 실행하세요.",
"AccountPreferencesNotLoaded": "계정을 불러오지 못했습니다. {command}을 실행하세요. 공급자 키는 계속 사용할 수 있습니다.",
"ComposerOversizedSubmitHeld": "전송하지 못했습니다. 메시지가 {limit}자를 초과하며 붙여넣기 파일로 저장할 수 없습니다({error}). 전체 텍스트는 입력란에 그대로 있습니다. 내용을 줄이거나 .codewhale/pastes에 쓰기 권한을 부여한 뒤 Enter를 다시 누르세요.",
"ComposerOversizedSubmitHeldShort": "전송 안 됨: {limit}자 초과, 붙여넣기 파일 저장 실패.",
"CmdPluginOwnerReport": "확장 호스트:\n 상태: {state}\n 활성 도구 ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "유휴",
"PhaseDraft": "초안",
"PhaseWorking": "작업 중",
"PhaseReasoning": "추론 중",
"PhaseReasoning": "생각 중",
"PhaseReading": "읽는 중",
"PhaseUsingTool": "도구 사용 중",
"PhaseSubagents": "서브에이전트 실행 중",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "ativo",
"ExtensionPromptUnavailable": "As instruções das extensões não estão disponíveis; confira /plugin show.",
"ProfileConstitutionUnavailableLocal": "Não foi possível carregar as preferências da conta Codewhale; este turno usa suas configurações locais. Sua chave do provedor continua funcionando; execute codewhale login para restaurar as preferências da conta.",
"AccountPreferencesNotLoaded": "Conta não carregada. Execute {command}. Sua chave do provedor continua funcionando.",
"ComposerOversizedSubmitHeld": "Não enviado: esta mensagem excede {limit} caracteres e não pôde ser salva em um arquivo de texto colado ({error}). O texto completo continua no campo de entrada: reduza-o ou permita a gravação em .codewhale/pastes e pressione Enter novamente.",
"ComposerOversizedSubmitHeldShort": "Não enviado: acima de {limit} caracteres; arquivo colado não salvo.",
"CmdPluginOwnerReport": "Host de extensões:\n Estado: {state}\n Ferramentas ativas ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "ocioso",
"PhaseDraft": "rascunho",
"PhaseWorking": "trabalhando",
"PhaseReasoning": "raciocinando",
"PhaseReasoning": "pensando",
"PhaseReading": "lendo",
"PhaseUsingTool": "usando ferramenta",
"PhaseSubagents": "subagentes em andamento",
+1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "активен",
"ExtensionPromptUnavailable": "Инструкции расширений недоступны; проверьте /plugin show.",
"ProfileConstitutionUnavailableLocal": "Не удалось загрузить настройки аккаунта Codewhale, поэтому этот ход использует локальные настройки. Ключ провайдера продолжает работать; выполните codewhale login, чтобы вернуть настройки аккаунта.",
"AccountPreferencesNotLoaded": "Аккаунт не загружен. Выполните {command}. Ключ провайдера продолжает работать.",
"ComposerOversizedSubmitHeld": "Не отправлено: сообщение длиннее {limit} символов, и его не удалось сохранить в файл вставленного текста ({error}). Полный текст остался в поле ввода: сократите его или разрешите запись в .codewhale/pastes, затем снова нажмите Enter.",
"ComposerOversizedSubmitHeldShort": "Не отправлено: больше {limit} символов; файл вставки не сохранён.",
"CmdPluginOwnerReport": "Хост расширений:\n Состояние: {state}\n Активные инструменты ({count}): {tools}",
+1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "активний",
"ExtensionPromptUnavailable": "Інструкції розширень недоступні; перевірте /plugin show.",
"ProfileConstitutionUnavailableLocal": "Не вдалося завантажити налаштування облікового запису Codewhale, тому цей хід використовує локальні налаштування. Ключ провайдера й далі працює; виконайте codewhale login, щоб повернути налаштування облікового запису.",
"AccountPreferencesNotLoaded": "Обліковий запис не завантажено. Виконайте {command}. Ключ провайдера й далі працює.",
"ComposerOversizedSubmitHeld": "Не надіслано: повідомлення перевищує {limit} символів, і його не вдалося зберегти у файл вставленого тексту ({error}). Повний текст залишається в полі введення: скоротіть його або дозвольте запис у .codewhale/pastes, а потім знову натисніть Enter.",
"ComposerOversizedSubmitHeldShort": "Не надіслано: понад {limit} символів; файл вставки не збережено.",
"CmdPluginOwnerReport": "Хост розширень:\n Стан: {state}\n Активні інструменти ({count}): {tools}",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "đang chạy",
"ExtensionPromptUnavailable": "Không thể tải hướng dẫn của tiện ích mở rộng; kiểm tra /plugin show.",
"ProfileConstitutionUnavailableLocal": "Không thể tải tùy chọn tài khoản Codewhale nên lượt này dùng cài đặt cục bộ. Khóa nhà cung cấp của bạn vẫn hoạt động; chạy codewhale login để khôi phục tùy chọn tài khoản.",
"AccountPreferencesNotLoaded": "Chưa tải được tài khoản. Chạy {command}. Khóa nhà cung cấp của bạn vẫn hoạt động.",
"ComposerOversizedSubmitHeld": "Chưa gửi: tin nhắn vượt quá {limit} ký tự và không thể lưu thành tệp văn bản đã dán ({error}). Toàn bộ nội dung vẫn còn trong ô nhập: hãy rút ngắn hoặc cho phép ghi vào .codewhale/pastes, rồi nhấn Enter lần nữa.",
"ComposerOversizedSubmitHeldShort": "Chưa gửi: vượt quá {limit} ký tự; chưa lưu tệp dán.",
"CmdPluginOwnerReport": "Máy chủ tiện ích mở rộng:\n Trạng thái: {state}\n Công cụ đang hoạt động ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "rảnh",
"PhaseDraft": "nháp",
"PhaseWorking": "đang xử lý",
"PhaseReasoning": "đang suy luận",
"PhaseReasoning": "đang suy nghĩ",
"PhaseReading": "đang đọc",
"PhaseUsingTool": "đang dùng công cụ",
"PhaseSubagents": "đang chạy tác tử con",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "运行中",
"ExtensionPromptUnavailable": "扩展指令不可用;请通过 /plugin show 查看。",
"ProfileConstitutionUnavailableLocal": "无法加载 Codewhale 账号偏好,本轮改用本地设置。你的提供商密钥仍可正常使用;运行 codewhale login 可恢复账号偏好。",
"AccountPreferencesNotLoaded": "账号未加载。请运行 {command}。你的提供商密钥仍可正常使用。",
"ComposerOversizedSubmitHeld": "未发送:消息超过 {limit} 个字符,无法保存为粘贴文本文件({error})。完整文本仍保留在输入框中:请缩短内容,或允许写入 .codewhale/pastes,然后再次按 Enter。",
"ComposerOversizedSubmitHeldShort": "未发送:超过 {limit} 个字符;粘贴文件未保存。",
"CmdPluginOwnerReport": "扩展宿主:\n 状态:{state}\n 活动工具({count}):{tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "空闲",
"PhaseDraft": "草稿",
"PhaseWorking": "工作中",
"PhaseReasoning": "推理中",
"PhaseReasoning": "思考中",
"PhaseReading": "读取中",
"PhaseUsingTool": "使用工具中",
"PhaseSubagents": "智能体工作中",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "執行中",
"ExtensionPromptUnavailable": "擴充功能指令無法使用;請透過 /plugin show 查看。",
"ProfileConstitutionUnavailableLocal": "無法載入 Codewhale 帳號偏好,本輪改用本機設定。你的服務商金鑰仍可正常使用;執行 codewhale login 可恢復帳號偏好。",
"AccountPreferencesNotLoaded": "帳號未載入。請執行 {command}。你的服務商金鑰仍可正常使用。",
"ComposerOversizedSubmitHeld": "未傳送:訊息超過 {limit} 個字元,無法儲存為貼上文字檔案({error})。完整文字仍保留在輸入框中:請縮短內容,或允許寫入 .codewhale/pastes,然後再次按 Enter。",
"ComposerOversizedSubmitHeldShort": "未傳送:超過 {limit} 個字元;貼上檔案未儲存。",
"CmdPluginOwnerReport": "擴充功能主機:\n 狀態:{state}\n 活動工具({count}):{tools}",
@@ -1688,7 +1689,7 @@
"PhaseIdle": "閒置",
"PhaseDraft": "草稿",
"PhaseWorking": "工作中",
"PhaseReasoning": "推理中",
"PhaseReasoning": "思考中",
"PhaseReading": "讀取中",
"PhaseUsingTool": "使用工具中",
"PhaseSubagents": "代理程式工作中",
+2
View File
@@ -714,6 +714,7 @@ pub enum MessageId {
ExtensionsInventorySkills,
ExtensionPromptUnavailable,
ProfileConstitutionUnavailableLocal,
AccountPreferencesNotLoaded,
ExtensionsMarketplaceDetail,
ExtensionsMarketplaceUnavailable,
ExtensionsMcpEmpty,
@@ -3651,6 +3652,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ExtensionsInventorySkills,
MessageId::ExtensionPromptUnavailable,
MessageId::ProfileConstitutionUnavailableLocal,
MessageId::AccountPreferencesNotLoaded,
MessageId::ExtensionsMarketplaceDetail,
MessageId::ExtensionsMarketplaceUnavailable,
MessageId::ExtensionsMcpEmpty,
+37 -6
View File
@@ -492,6 +492,8 @@ pub struct Settings {
/// One-time Operate-retirement toast has been shown. Same suppression
/// contract as `yolo_deprecation_shown`.
pub operate_deprecation_shown: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_preferences_notice_shown: Option<String>,
/// Round 3 (2026-09-01) moved the work bar under the composer. Every
/// settings.toml saved before that carries `work_surface_placement =
/// "top"` — the old default, persisted verbatim by ordinary saves, not a
@@ -650,9 +652,7 @@ impl Default for Settings {
mention_menu_limit: 128,
mention_walk_depth: 10,
mention_menu_behavior: "fuzzy".to_string(),
// Reasoning is useful when explicitly requested, but it should
// never displace the actual conversation in the default TUI.
show_thinking: false,
show_thinking: true,
thinking_default_expanded: false,
thinking_preview_lines: default_thinking_preview_lines(),
thinking_highlight: true,
@@ -694,6 +694,7 @@ impl Default for Settings {
feature_intro_shown: false,
yolo_deprecation_shown: false,
operate_deprecation_shown: false,
account_preferences_notice_shown: None,
legacy_operate_default: false,
work_surface_bottom_migrated: false,
behavioral_tip_impressions: std::collections::BTreeMap::new(),
@@ -3747,6 +3748,36 @@ mod tests {
assert_eq!(reloaded.permission_posture.as_deref(), Some("ask"));
}
#[test]
fn thinking_shows_by_default_and_a_saved_choice_survives_reload_and_other_saves() {
let _g = config_path_test_guard();
let tmp = tempfile::tempdir().expect("tempdir");
let settings_path = tmp.path().join("settings.toml");
let _config_override =
EnvVarRestore::set("DEEPSEEK_CONFIG_PATH", tmp.path().join("config.toml"));
let absent = Settings::load().expect("load without a settings file");
assert!(absent.show_thinking);
assert!(!absent.thinking_default_expanded);
assert!(!absent.is_set("show_thinking"));
std::fs::write(&settings_path, "theme = \"light\"\n").expect("settings without the key");
let unnamed = Settings::load().expect("load settings without the key");
assert!(unnamed.show_thinking);
assert!(!unnamed.is_set("show_thinking"));
for saved in [false, true] {
std::fs::write(&settings_path, format!("show_thinking = {saved}\n"))
.expect("saved choice");
let loaded = Settings::load().expect("load the saved choice");
assert_eq!(loaded.show_thinking, saved);
assert!(loaded.is_set("show_thinking"));
Settings::transact(|settings| settings.set("locale", "en")).expect("another save");
let reloaded = Settings::load().expect("reload after another save");
assert_eq!(reloaded.show_thinking, saved);
}
}
#[test]
fn settings_load_keeps_top_placement_chosen_after_the_bottom_migration() {
let _g = config_path_test_guard();
@@ -3886,7 +3917,7 @@ mod tests {
let mut settings = Settings::default();
// Density is calm by default; motion is an independent axis.
assert!(settings.calm_mode);
assert!(!settings.show_thinking);
settings.show_thinking = false;
let changed = settings.apply_preset("CALM").expect("calm preset applies");
assert_eq!(
@@ -3916,8 +3947,8 @@ mod tests {
assert!(settings.fancy_animations);
assert_eq!(settings.transcript_spacing, "spacious");
assert_eq!(settings.tool_collapse_mode, "compact");
// Thinking is opt-in so the transcript stays focused on the chat.
assert!(!settings.show_thinking);
assert!(settings.show_thinking);
assert!(!settings.thinking_default_expanded);
}
#[test]
+1 -13
View File
@@ -669,7 +669,7 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
- **Windows uninstall removes the receipt and update leftovers.** The installer's uninstaller also deletes `.codewhale-install.sha256` and `codewhale.old-*`/`codew.old-*` left by a self-update, so `bin` can be removed. Refs #7091.
- **Windows `/undo` matches recorded file paths regardless of case.** A write recorded under a different drive-letter or directory case, or a `\\?\` prefix, is no longer dropped from the restore plan. Refs #6970.
- **Windows: Fleet and metrics read live files again.** Replaying a Fleet ledger and reading audit or receipt records no longer fail with a sharing violation while their writer is still open; the same regular-file, link and identity checks apply. Plugin enablement now says which snapshot check refused (stage root, content identity or final check) instead of one generic message. Generated protocol and SDK exports keep LF line endings in Windows checkouts.
- **A held coordination lock no longer makes a new session read-only.** When another session owns the delegated-agent store, the new session starts on private state and says so, instead of refusing every sub-agent with “Failed to clean up sub-agents”.
- **Sessions in one folder no longer leave each other's sub-agents read-only or private:** a second session works beside another one, stale or suspended included, the older sub-agent store upgrades right away, and a store that cannot be opened is retried on the next sub-agent change instead of needing a restart. While another Codewhale session, a suspended one included, is open in a folder the desktop app or `serve` was asked to work in (other than the folder it started in), that Runtime still refuses sub-agents that would edit the folder directly; isolated worktrees are unaffected. A sub-agent store that cannot be opened shows its refusal once instead of repeating cleanup warnings on every sidebar refresh. Restart any session started with Codewhale 0.10.2 or older: once the folder upgrades it can no longer save sub-agent state (Refs #7232).
- **A dev server can outlive the turn in an interactive session.** `persist:true` now works in the TUI and desktop when the thread has Full Access; restricted postures and Fleet/sub-agents still refuse it.
- **`codewhale exec --add-dir <path>`** (repeatable) and `addDirs` on `POST /v1/threads` let the file tools read and write a sibling directory, a second checkout or a shared scratch root without turning on Full Access.
- **Unsettled usage is persisted in production, not only in tests.** The interactive sink now records a legacy usage receipt when a provider response has no settlement, so children and turns stop reporting settlement failure.
@@ -706,12 +706,6 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
composer. Real Pinyin input on macOS and Windows is not yet confirmed.
Thanks [@cmdcorp6534](https://github.com/cmdcorp6534) for the report
(Refs #2323).
- **Sub-agents beside an older session:** while a Codewhale 0.10.2 session
still uses a folder's older delegated-agent store, a 0.10.3 session in the
same folder keeps working sub-agents with private state that is not saved,
instead of refusing every sub-agent action. The older session's files are
left untouched, and the store upgrades when a session starts after the older
one exits (Refs #7232).
- **Fleet members:** a replayed member start now matches only within its own
session, so two sessions that reuse an operation key no longer receive each
other's agent.
@@ -740,12 +734,6 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
bare self-package exports resolve inside the selected package directory.
Invalid or incompatible ranges and paths outside the reviewed package still
refuse. Real published-plugin activation remains unqualified.
- **Coordination lock diagnostics (#7232):** stale lock-file contents no longer
identify an owner PID. A refused legacy admission keeps its reason in Work
and shows one notice per cause instead of repeating cleanup warnings on
every sidebar refresh. Legacy migration still requires the actual holder to
exit and the refused owning Runtime to restart; locks and retained state
remain protected.
- **A local proxy that stops mid-session no longer strands the session (#7231):** the model, catalog, search, vision, finance, MCP-registry, profile and remote-control clients watch a loopback proxy port and go direct while it refuses connections, then resume proxying when it returns. Only a definite refusal from every resolved address changes the route; remote proxies, `NO_PROXY` matches, credentials and the existing precedence are unchanged. Set `CODEWHALE_LOCAL_PROXY_FALLBACK=0` to keep the old build-time snapshot.
- **Proxy diagnostic privacy ([#7233](https://github.com/codewhale-hq/codewhale/issues/7233)):** malformed MCP proxy warnings omit configured values; local-proxy log labels mask userinfo and omit URL paths, queries and fragments. Routing is unchanged. All 23 existing proxy tests passed on macOS; runtime log capture and other platforms remain unqualified. Thanks [@SparkofSpike](https://github.com/SparkofSpike) for the report.
- **Fork, undo and retry work after a workspace drift notice (#7234):** the history-boundary walks now treat a whole `<context_update>` envelope as engine-owned, so a session that received one can be cut at every saved turn. A person quoting a notice or a truncated envelope still counts as their own prompt.
+54
View File
@@ -87,6 +87,27 @@ pub(crate) use types::{
WORKFLOW_DRAFT_INSTRUCTION_PREFIX,
};
const ACCOUNT_PREFERENCES_NOTICE: &str = "account-preferences-notice";
fn account_sign_in_marker(profile: Option<&str>) -> String {
use sha2::{Digest, Sha256};
let api_base = codewhale_engine::profile_constitution::runtime_account_api_base();
let session = codewhale_secrets::account::AccountSessionStore::new(
codewhale_secrets::Secrets::file_backed_read_only(),
profile,
&api_base,
)
.runtime_info_at(Utc::now())
.ok()
.and_then(|info| info.session_id)
.unwrap_or_default();
Sha256::digest(session.as_bytes())
.iter()
.take(8)
.map(|byte| format!("{byte:02x}"))
.collect()
}
// === Types ===
/// One login owns one mailbox. A cancelled task can only write its abandoned
@@ -2448,6 +2469,7 @@ pub struct App {
/// re-render per character. `None` means "no bump since the last
/// finalize" so the first chunk of a block always renders immediately.
pub thinking_revision_last_bump_at: Option<Instant>,
pub(crate) thinking_revision_owed: bool,
/// Newline-gated streaming collector state.
pub streaming_state: StreamingState,
/// Live approximate output tokens for the current assistant stream.
@@ -3791,6 +3813,38 @@ impl App {
);
}
pub(crate) fn notify_account_preferences_not_loaded(&mut self) {
let sign_in = account_sign_in_marker(self.config_profile.as_deref());
self.notify_account_preferences_not_loaded_for(&sign_in);
}
fn notify_account_preferences_not_loaded_for(&mut self, sign_in: &str) {
let first_for_sign_in = Settings::transact_opt(|settings| {
if settings.account_preferences_notice_shown.as_deref() == Some(sign_in) {
return Ok(None);
}
settings.account_preferences_notice_shown = Some(sign_in.to_string());
Ok(Some(()))
});
if matches!(first_for_sign_in, Ok(None)) {
return;
}
let text = self
.tr(MessageId::AccountPreferencesNotLoaded)
.replace("{command}", "codewhale login");
self.push_status_toast_record(StatusToast::standing(
text,
StatusToastLevel::Warning,
ACCOUNT_PREFERENCES_NOTICE,
));
}
pub(crate) fn dismiss_account_preferences_notice(&mut self) -> bool {
let before = self.status_toasts.len();
self.retire_event_notices(ACCOUNT_PREFERENCES_NOTICE);
self.status_toasts.len() != before
}
/// One-release migration notice for the Shift+Tab/Ctrl+T rebinding: users
/// pressing Shift+Tab expecting the old thinking cycle land here first.
fn notify_keybinding_migration_once(&mut self) {
+1
View File
@@ -1142,6 +1142,7 @@ impl App {
streaming_thinking_active_entry: None,
pending_thinking_translation_cells: HashMap::new(),
thinking_revision_last_bump_at: None,
thinking_revision_owed: false,
streaming_state: StreamingState::new(),
streaming_output_token_estimate: 0,
reasoning_buffer: String::new(),
+40
View File
@@ -184,6 +184,46 @@ fn feature_intro_shows_once_persists_then_is_idempotent() {
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn account_preferences_notice_shows_once_per_sign_in_and_can_be_dismissed() {
let _env_lock = lock_test_env();
let tmp = tempfile::tempdir().unwrap();
let config_path = tmp.path().join("config.toml");
let _env = EnvVarGuard::set(
"DEEPSEEK_CONFIG_PATH",
config_path.to_string_lossy().as_ref(),
);
let shown = |app: &App| {
let notice = app
.tr(MessageId::AccountPreferencesNotLoaded)
.replace("{command}", "codewhale login");
app.status_toasts
.iter()
.filter(|toast| toast.text == notice && toast.kind == StatusToastKind::Standing)
.count()
};
let mut app = App::new(test_options(false), &Config::default());
app.notify_account_preferences_not_loaded_for("first-sign-in");
app.notify_account_preferences_not_loaded_for("first-sign-in");
assert_eq!(shown(&app), 1);
assert_eq!(
Settings::load()
.expect("settings should load")
.account_preferences_notice_shown
.as_deref(),
Some("first-sign-in")
);
assert!(app.dismiss_account_preferences_notice());
assert_eq!(shown(&app), 0);
let mut later_session = App::new(test_options(false), &Config::default());
later_session.notify_account_preferences_not_loaded_for("first-sign-in");
assert_eq!(shown(&later_session), 0, "the same sign-in is told once");
later_session.notify_account_preferences_not_loaded_for("second-sign-in");
assert_eq!(shown(&later_session), 1, "a later sign-in is told again");
}
#[test]
fn initial_input_scenario() {
// Scenario consolidation of: initial_input_prefill_waits_for_manual_submit, initial_input_submit_marks_startup_dispatch
@@ -9,7 +9,7 @@
│ Background (default) ✎ │current terminal
│ Pet mode Off [ ] │saved terminal
│ Quiet transcript On [x] │startup terminal
│ Model reasoning in chat Off [ ] │source settings.toml
│ Model reasoning in chat On [x] │source settings.toml
│ Thinking Default Expanded Off [ ] │scope SAVED
│ Thinking Preview Lines 2 ✎ │apply applies on save
│ Thinking background highlight On [x] │kind choice
@@ -9,7 +9,7 @@
Background (default) ✎ SAVED █
Pet mode Off [ ] SAVED █
Quiet transcript On [x] SAVED █
Model reasoning in chat Off [ ] SAVED █
Model reasoning in chat On [x] SAVED █
Thinking Default Expanded Off [ ] SAVED █
Thinking Preview Lines 2 ✎ SAVED │
Thinking background highlight On [x] SAVED │
+46 -6
View File
@@ -30,15 +30,17 @@ use crate::tui::history::HistoryCell;
/// frame cap (see `frame_rate_limiter`) yet imperceptible as lag.
///
/// Correctness: this only skips *intermediate* repaints. Appended content is
/// never dropped — it lands in the cell immediately — and finalize always
/// forces a bump so the final reasoning text is fully rendered.
/// never dropped — it lands in the cell immediately — the display beat renders
/// a skipped bump once the window has passed, and finalize always forces a
/// bump so the final reasoning text is fully rendered.
const THINKING_REVISION_THROTTLE: Duration = Duration::from_millis(100);
/// Bump the active-cell revision for a streaming thinking mutation, but at
/// most once per [`THINKING_REVISION_THROTTLE`] window. Returns whether a bump
/// was actually emitted. Skipped bumps coalesce into the next one (or into the
/// forced finalize bump), so no content is ever lost — only redundant
/// intermediate re-wraps are dropped.
/// was actually emitted. Skipped bumps coalesce into the next one, into
/// [`render_owed_revision`] on a later display beat, or into the forced
/// finalize bump, so no content is ever lost — only redundant intermediate
/// re-wraps are dropped.
fn bump_thinking_revision_throttled(app: &mut App, now: Instant) -> bool {
let due = app
.thinking_revision_last_bump_at
@@ -47,9 +49,17 @@ fn bump_thinking_revision_throttled(app: &mut App, now: Instant) -> bool {
app.thinking_revision_last_bump_at = Some(now);
app.bump_active_cell_revision();
}
app.thinking_revision_owed = !due;
due
}
pub(super) fn render_owed_revision(app: &mut App, now: Instant) -> bool {
if app.streaming_thinking_active_entry.is_none() {
app.thinking_revision_owed = false;
}
app.thinking_revision_owed && bump_thinking_revision_throttled(app, now)
}
/// Ensure an in-flight Thinking entry exists in `active_cell` and return its
/// entry index. If no thinking entry is currently streaming, push a fresh one.
/// P2.3: thinking shares the active cell with subsequent tool calls so the
@@ -76,7 +86,8 @@ pub(super) fn ensure_active_entry(app: &mut App) -> usize {
/// committed to the cell immediately; the active-cell revision bump that
/// triggers a re-wrap of the live tail is debounced to at most one per
/// [`THINKING_REVISION_THROTTLE`] window (#1620). Skipped bumps coalesce into
/// the next append or the forced finalize bump, so no content is ever lost.
/// the next append, a later display beat or the forced finalize bump, so no
/// content is ever lost.
pub(super) fn append(app: &mut App, entry_idx: usize, text: &str) {
append_at(app, entry_idx, text, Instant::now());
}
@@ -308,6 +319,7 @@ pub(super) fn finalize_active_entry(app: &mut App, duration: Option<f32>, remain
// the debounce window so the next thinking block's first chunk renders
// immediately rather than being coalesced into a stale window.
app.thinking_revision_last_bump_at = None;
app.thinking_revision_owed = false;
app.bump_active_cell_revision();
true
}
@@ -426,6 +438,34 @@ mod tests {
);
}
#[test]
fn a_paused_stream_renders_the_reasoning_it_already_received() {
let mut app = test_app();
let entry = ensure_active_entry(&mut app);
app.thinking_revision_last_bump_at = None;
let t0 = Instant::now();
append_at(&mut app, entry, "first slice ", t0);
let shown = app.active_cell_revision;
append_at(&mut app, entry, "held back", t0 + Duration::from_millis(16));
assert_eq!(app.active_cell_revision, shown);
assert!(!render_owed_revision(
&mut app,
t0 + Duration::from_millis(32)
));
assert_eq!(app.active_cell_revision, shown);
assert!(render_owed_revision(
&mut app,
t0 + THINKING_REVISION_THROTTLE
));
assert_eq!(app.active_cell_revision, shown.wrapping_add(1));
assert!(!render_owed_revision(
&mut app,
t0 + THINKING_REVISION_THROTTLE * 3
));
assert_eq!(app.active_cell_revision, shown.wrapping_add(1));
}
/// #7123: a reasoning block that opens while the display beat still holds
/// an uncommitted slice of prose must not drop that slice. `start_block`
/// resets `StreamingState`, so the tail has to be delivered first.
+9 -11
View File
@@ -7232,14 +7232,16 @@ pub(crate) async fn run_event_loop(
continue;
}
// An idle operator can dismiss the persistent context warning
// without affecting vim or attachment handling. While a turn
// is active Esc retains its cancellation meaning.
// or the account preferences notice without affecting vim or
// attachment handling. While a turn is active Esc retains its
// cancellation meaning.
KeyCode::Esc
if !app.is_loading
&& app.input.is_empty()
&& !slash_menu_open
&& !mention_menu_open
&& app.dismiss_context_pressure_warning() =>
&& (app.dismiss_context_pressure_warning()
|| app.dismiss_account_preferences_notice()) =>
{
continue;
}
@@ -9326,14 +9328,10 @@ pub(super) fn apply_engine_status(app: &mut App, message: String) -> bool {
app.add_message(HistoryCell::System { content: message });
return true;
}
// The account-profile fallback notice is sent once per engine; keep it in
// the transcript so the next turn's status line cannot erase it.
let retain = app.tr(MessageId::ProfileConstitutionUnavailableLocal) == message.as_str();
if retain {
app.add_message(HistoryCell::System {
content: message.clone(),
});
if app.tr(MessageId::ProfileConstitutionUnavailableLocal) == message.as_str() {
app.notify_account_preferences_not_loaded();
return false;
}
app.status_message = Some(message);
retain
false
}
+4 -1
View File
@@ -1555,7 +1555,10 @@ pub(crate) fn commit_streaming_display_tick(
updated = true;
}
if app.streaming_state.has_pending_stream_text(0) {
if streaming_thinking::render_owed_revision(app, now) {
updated = true;
}
if app.streaming_state.has_pending_stream_text(0) || app.thinking_revision_owed {
stream_display_clock.note_delta(now);
}
+26 -8
View File
@@ -27343,7 +27343,7 @@ fn turn_inspector_switches_isolated_full_turn_pages_with_tagged_reasoning_and_ou
let latest = pager.body_text();
assert!(latest.contains("SECOND-PROMPT"), "{latest}");
assert!(latest.contains("[∿ reasoning 1/1 · running]"), "{latest}");
assert!(latest.contains("[∿ thinking 1/1 · running]"), "{latest}");
assert!(latest.contains("SECOND-THOUGHT"), "{latest}");
assert!(latest.contains("[◆ · running]"), "{latest}");
assert!(latest.contains("SECOND-ANSWER"), "{latest}");
@@ -27355,7 +27355,7 @@ fn turn_inspector_switches_isolated_full_turn_pages_with_tagged_reasoning_and_ou
for expected in [
"FIRST-PROMPT",
"keep this input line",
"[∿ reasoning 1/1 · Done]",
"[∿ thinking 1/1 · Done]",
"FIRST-THOUGHT",
"full reasoning tail",
"[⚙ using tool]",
@@ -35488,23 +35488,41 @@ fn extension_prompt_origin_keeps_denials_and_other_policy_holds() {
}
#[test]
fn account_profile_fallback_notice_stays_in_the_transcript() {
fn account_profile_fallback_notice_is_a_standing_notice_outside_the_transcript() {
let _home = SettingsHomeGuard::new();
let mut app = App::new(create_test_options(), &Config::default());
let notice = app
let engine_notice = app
.tr(MessageId::ProfileConstitutionUnavailableLocal)
.to_string();
let before = app.history.len();
assert!(super::event_loop::apply_engine_status(
let status_before = app.status_message.clone();
assert!(!super::event_loop::apply_engine_status(
&mut app,
notice.clone()
engine_notice
));
assert_eq!(app.history.len(), before);
assert_eq!(app.status_message, status_before);
assert!(!super::event_loop::apply_engine_status(
&mut app,
"Executing tools sequentially".into()
));
assert_eq!(app.history.len(), before + 1);
assert!(matches!(&app.history[before], HistoryCell::System { content } if *content == notice));
assert_eq!(app.history.len(), before);
let notice = app
.tr(MessageId::AccountPreferencesNotLoaded)
.replace("{command}", "codewhale login");
let standing = |app: &App| {
app.status_toasts
.iter()
.find(|toast| toast.text == notice)
.cloned()
};
app.sync_status_message_to_toasts();
let shown = standing(&app).expect("a later routine status does not erase the notice");
assert_eq!(shown.level, StatusToastLevel::Warning);
assert_eq!(shown.ttl_ms, None);
assert!(app.dismiss_account_preferences_notice());
assert!(standing(&app).is_none());
assert!(!app.dismiss_account_preferences_notice());
}
#[test]
+2
View File
@@ -32,6 +32,8 @@ mod plugin_e2e_acceptance;
mod screen_mode_inline_pty;
#[cfg(all(unix, feature = "long-running-tests"))]
mod search_text_pty;
#[cfg(all(unix, feature = "long-running-tests"))]
mod thinking_stream_pty;
mod tool_lifecycle_acceptance;
#[cfg(all(unix, feature = "long-running-tests"))]
mod work_bar_keys_pty;
@@ -0,0 +1,585 @@
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::time::{Duration, Instant};
use super::qa_harness::{
harness::{Harness, SealedWorkspace, make_sealed_workspace},
keys,
};
const WAIT: Duration = Duration::from_secs(20);
const MODEL: &str = "deepseek-v4-pro";
const PROMPTS: [&str; 2] = ["first slow fixture prompt", "second slow fixture prompt"];
const ANSWER: &str = "Fixture answer after thinking.";
const REASONING_LINES: usize = 6;
const NOTICE: &str = "Account not loaded. Run codewhale login.";
const RETIRED_TRANSCRIPT_NOTICE: &str = "could not be loaded";
fn reasoning_line(index: usize) -> String {
format!("thinking fixture line {index} of {REASONING_LINES}")
}
struct PacedProvider {
address: SocketAddr,
gate: mpsc::Sender<()>,
stop: Arc<AtomicBool>,
turns: Arc<AtomicUsize>,
profile_requests: Arc<AtomicUsize>,
acceptor: Option<std::thread::JoinHandle<()>>,
}
struct Served {
gates: Mutex<mpsc::Receiver<()>>,
stop: Arc<AtomicBool>,
turns: Arc<AtomicUsize>,
profile_requests: Arc<AtomicUsize>,
}
impl PacedProvider {
fn start() -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("loopback provider");
let address = listener.local_addr().expect("loopback address");
let (gate, gates) = mpsc::channel();
let stop = Arc::new(AtomicBool::new(false));
let turns = Arc::new(AtomicUsize::new(0));
let profile_requests = Arc::new(AtomicUsize::new(0));
let served = Arc::new(Served {
gates: Mutex::new(gates),
stop: Arc::clone(&stop),
turns: Arc::clone(&turns),
profile_requests: Arc::clone(&profile_requests),
});
let acceptor = std::thread::spawn(move || {
for stream in listener.incoming() {
if served.stop.load(Ordering::Acquire) {
break;
}
let Ok(stream) = stream else { continue };
let served = Arc::clone(&served);
std::thread::spawn(move || {
let _ = serve(stream, &served);
});
}
});
Self {
address,
gate,
stop,
turns,
profile_requests,
acceptor: Some(acceptor),
}
}
fn model_base(&self) -> String {
format!("http://{}/v1", self.address)
}
fn account_base(&self) -> String {
format!("http://{}", self.address)
}
fn release(&self, gates: usize) {
for _ in 0..gates {
self.gate.send(()).expect("provider gate");
}
}
fn turns(&self) -> usize {
self.turns.load(Ordering::Acquire)
}
}
impl Drop for PacedProvider {
fn drop(&mut self) {
self.stop.store(true, Ordering::Release);
let _ = TcpStream::connect(self.address);
if let Some(acceptor) = self.acceptor.take() {
let _ = acceptor.join();
}
}
}
fn serve(stream: TcpStream, served: &Served) -> std::io::Result<()> {
stream.set_read_timeout(Some(Duration::from_secs(10)))?;
let mut reader = BufReader::new(stream.try_clone()?);
let mut stream = stream;
let mut request_line = String::new();
if reader.read_line(&mut request_line)? == 0 {
return Ok(());
}
let mut content_length = 0usize;
loop {
let mut header = String::new();
if reader.read_line(&mut header)? == 0 {
break;
}
let header = header.trim_end();
if header.is_empty() {
break;
}
if let Some((name, value)) = header.split_once(':')
&& name.eq_ignore_ascii_case("content-length")
{
content_length = value.trim().parse().unwrap_or(0);
}
}
let mut body = vec![0u8; content_length.min(8 * 1024 * 1024)];
reader.read_exact(&mut body)?;
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or_default();
let path = parts.next().unwrap_or_default();
if method == "GET" && path.ends_with("/models") {
return respond_json(
&mut stream,
"200 OK",
&serde_json::json!({"object":"list","data":[{"id":MODEL,"object":"model"}]}),
);
}
if path.starts_with("/api/") {
served.profile_requests.fetch_add(1, Ordering::AcqRel);
return respond_json(
&mut stream,
"401 Unauthorized",
&serde_json::json!({"error":"loopback fixture account is signed out"}),
);
}
if method != "POST" {
return respond_json(
&mut stream,
"404 Not Found",
&serde_json::json!({"error":"not served by the loopback fixture"}),
);
}
let request: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
let streaming = request["stream"].as_bool().unwrap_or(false);
let with_tools = request["tools"]
.as_array()
.is_some_and(|tools| !tools.is_empty());
let text = String::from_utf8_lossy(&body);
let turn = PROMPTS
.iter()
.rposition(|prompt| text.contains(prompt))
.map(|index| index + 1);
let paced = streaming
&& with_tools
&& turn.is_some_and(|turn| {
served
.turns
.compare_exchange(turn - 1, turn, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
});
if !streaming {
return respond_json(
&mut stream,
"200 OK",
&serde_json::json!({
"id":"loopback","object":"chat.completion","model":MODEL,
"choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],
"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}
}),
);
}
stream.write_all(
b"HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\ncache-control: no-cache\r\nconnection: close\r\n\r\n",
)?;
stream.flush()?;
if paced {
wait_for_gate(served)?;
for index in 1..=REASONING_LINES {
send_delta(
&mut stream,
serde_json::json!({"reasoning_content": format!("{}\n", reasoning_line(index))}),
)?;
}
wait_for_gate(served)?;
send_delta(&mut stream, serde_json::json!({"content": ANSWER}))?;
wait_for_gate(served)?;
} else {
send_delta(&mut stream, serde_json::json!({"content": "ok"}))?;
}
send_chunk(
&mut stream,
&serde_json::json!({
"id":"loopback","object":"chat.completion.chunk","model":MODEL,
"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],
"usage":{"prompt_tokens":8,"completion_tokens":4,"total_tokens":12}
}),
)?;
stream.write_all(b"data: [DONE]\n\n")?;
stream.flush()
}
fn wait_for_gate(served: &Served) -> std::io::Result<()> {
let gates = served.gates.lock().expect("provider gates");
loop {
if served.stop.load(Ordering::Acquire) {
return Err(std::io::ErrorKind::Interrupted.into());
}
match gates.recv_timeout(Duration::from_millis(100)) {
Ok(()) => return Ok(()),
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => {
return Err(std::io::ErrorKind::BrokenPipe.into());
}
}
}
}
fn send_delta(stream: &mut TcpStream, delta: serde_json::Value) -> std::io::Result<()> {
send_chunk(
stream,
&serde_json::json!({
"id":"loopback","object":"chat.completion.chunk","model":MODEL,
"choices":[{"index":0,"delta":delta,"finish_reason":null}]
}),
)
}
fn send_chunk(stream: &mut TcpStream, chunk: &serde_json::Value) -> std::io::Result<()> {
stream.write_all(format!("data: {chunk}\n\n").as_bytes())?;
stream.flush()
}
fn respond_json(
stream: &mut TcpStream,
status: &str,
body: &serde_json::Value,
) -> std::io::Result<()> {
let body = body.to_string();
stream.write_all(
format!(
"HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
body.len()
)
.as_bytes(),
)?;
stream.flush()
}
fn prepare(workspace: &SealedWorkspace, provider: &PacedProvider, settings: Option<&str>) {
let state = workspace.home().join(".codewhale");
std::fs::write(state.join(".onboarded"), "").unwrap();
std::fs::write(
state.join("config.toml"),
format!(
"provider = \"deepseek\"\n[providers.deepseek]\nbase_url = {:?}\napi_key = \"loopback-only\"\nmodel = {MODEL:?}\n[notifications]\nmethod = \"off\"\ncompletion_sound = \"off\"\n",
provider.model_base()
),
)
.unwrap();
if let Some(settings) = settings {
std::fs::write(state.join("settings.toml"), settings).unwrap();
}
let trust = workspace.workspace().join(".deepseek");
std::fs::create_dir_all(&trust).unwrap();
std::fs::write(trust.join("trusted"), "").unwrap();
}
fn launch(
workspace: &SealedWorkspace,
provider: &PacedProvider,
(rows, cols): (u16, u16),
session: &str,
animated: bool,
) -> Harness {
let mut tui = Harness::builder(Harness::codewhale_binary())
.cwd(workspace.workspace())
.clear_env()
.seal_home(workspace.home())
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
.env("SHELL", "/bin/sh")
.env("CODEWHALE_CLOUD_API_BASE", provider.account_base())
.env("CODEWHALE_DISABLE_MODELS_DEV_FETCH", "1")
.env("CODEWHALE_NO_UPDATE_CHECK", "1")
.env("NO_ANIMATIONS", if animated { "0" } else { "1" })
.args([
"--workspace",
workspace.workspace().to_str().unwrap(),
"--no-project-config",
"--skip-onboarding",
session,
])
.size(rows, cols)
.spawn()
.unwrap();
tui.wait_for_text("accept edits", WAIT)
.unwrap_or_else(|error| panic!("the dial chip never painted: {error}"));
tui
}
fn expect(tui: &mut Harness, what: &str, predicate: impl FnMut(&super::qa_harness::Frame) -> bool) {
if let Err(error) = tui.wait_for(predicate, WAIT) {
panic!("waiting for {what}: {error}");
}
}
fn settle(tui: &mut Harness) {
let deadline = Instant::now() + Duration::from_secs(2);
let mut quiet_since = Instant::now();
while Instant::now() < deadline && quiet_since.elapsed() < Duration::from_millis(300) {
if tui.pump() {
quiet_since = Instant::now();
}
std::thread::sleep(Duration::from_millis(20));
}
}
fn report(tui: &mut Harness, label: &str) {
let frame = tui.frame();
println!(
"--- {label} at {}x{} ---\n{}",
frame.cols(),
frame.rows(),
frame.debug_dump()
);
}
fn footer_row(tui: &mut Harness) -> (u16, String) {
let frame = tui.frame();
(0..frame.rows())
.rev()
.map(|row| (row, frame.row(row)))
.find(|(_, text)| text.contains("accept edits"))
.unwrap_or_else(|| panic!("no dial chip on screen:\n{}", frame.debug_dump()))
}
fn assert_status_and_water(tui: &mut Harness, label: &str) {
let (footer, status) = footer_row(tui);
let frame = tui.frame();
for retired in ["operate", "Operate", "Work bar"] {
assert!(
!frame.contains(retired),
"{label}: `{retired}` is on screen:\n{}",
frame.debug_dump()
);
}
assert!(status.contains("accept edits"), "{label}: {status}");
for row in 0..footer.saturating_sub(3) {
let text = frame.row(row);
let cells: Vec<char> = text.chars().collect();
let school = cells.windows(2).any(|pair| {
pair.iter()
.all(|cell| ('\u{2801}'..='\u{28FF}').contains(cell))
});
assert!(
!school && !text.contains(['˚', '°', '≈', '~']),
"{label}: ambient art on transcript row {row} `{text}`:\n{}",
frame.debug_dump()
);
}
}
fn thinking_rows(tui: &mut Harness) -> usize {
let frame = tui.frame();
(0..frame.rows())
.filter(|&row| frame.row(row).contains("Thinking…"))
.count()
}
fn stream_first_turn(tui: &mut Harness, provider: &PacedProvider, thinking_shown: bool) {
tui.type_line(PROMPTS[0]).unwrap();
expect(tui, "the first request and its interrupt hint", |frame| {
provider.turns() == 1 && frame.contains("Esc to interrupt")
});
std::thread::sleep(Duration::from_millis(900));
report(tui, "loading");
assert_status_and_water(tui, "loading");
provider.release(1);
let tail = reasoning_line(REASONING_LINES);
if thinking_shown {
expect(tui, "the live thinking block", |frame| {
frame.contains("Thinking…") && frame.contains(&tail)
});
} else {
expect(tui, "the thinking activity line", |frame| {
frame.contains("Thinking…")
});
}
std::thread::sleep(Duration::from_millis(1300));
report(tui, "thinking");
assert_status_and_water(tui, "thinking");
assert_eq!(thinking_rows(tui), 1, "one Thinking line");
let (_, status) = footer_row(tui);
assert!(
status.contains("thinking ") && !status.contains("reasoning"),
"the clock names the phase: {status}"
);
let frame = tui.frame();
assert!(!frame.contains("reasoning..."), "{}", frame.debug_dump());
assert_eq!(frame.contains(&tail), thinking_shown);
assert!(
!frame.contains(&reasoning_line(1)),
"a long trace stays inside its window:\n{}",
frame.debug_dump()
);
provider.release(1);
expect(tui, "the streamed answer", |frame| frame.contains(ANSWER));
report(tui, "answer");
let frame = tui.frame();
assert!(!frame.contains("Thinking…"), "{}", frame.debug_dump());
assert_eq!(frame.contains("Thought for "), thinking_shown);
assert!(!frame.contains(&tail), "{}", frame.debug_dump());
provider.release(1);
expect(tui, "the settled turn", |frame| {
!frame.contains("Esc to interrupt")
});
settle(tui);
report(tui, "settled");
assert_status_and_water(tui, "settled");
}
#[test]
fn thinking_streams_stops_and_restores_at_the_shipped_widths() {
for size in [(24, 80), (32, 100), (32, 120), (40, 140)] {
let workspace = make_sealed_workspace().unwrap();
let provider = PacedProvider::start();
prepare(&workspace, &provider, None);
let mut tui = launch(&workspace, &provider, size, "--fresh", true);
stream_first_turn(&mut tui, &provider, true);
tui.type_line(PROMPTS[1]).unwrap();
expect(&mut tui, "the second request", |_| provider.turns() == 2);
provider.release(1);
let tail = reasoning_line(REASONING_LINES);
expect(&mut tui, "thinking before the stop", |frame| {
frame.contains("Thinking…") && frame.contains(&tail)
});
tui.send(keys::key::esc()).unwrap();
expect(&mut tui, "the stopped turn", |frame| {
!frame.contains("Esc to interrupt") && !frame.contains("Thinking…")
});
settle(&mut tui);
report(&mut tui, "stopped");
assert_status_and_water(&mut tui, "stopped");
provider.release(2);
tui.send(keys::key::ctrl('u')).unwrap();
settle(&mut tui);
tui.send(keys::key::ctrl_d()).unwrap();
tui.wait_for_exit(Duration::from_secs(10));
tui.shutdown();
let mut tui = launch(&workspace, &provider, size, "--continue", false);
expect(&mut tui, "the restored conversation", |frame| {
frame.contains(ANSWER)
});
settle(&mut tui);
report(&mut tui, "restored");
assert_status_and_water(&mut tui, "restored");
let frame = tui.frame();
assert!(!frame.contains("Thinking…"), "{}", frame.debug_dump());
tui.shutdown();
}
}
#[test]
fn a_saved_choice_to_hide_thinking_survives_the_new_default() {
let workspace = make_sealed_workspace().unwrap();
let provider = PacedProvider::start();
prepare(&workspace, &provider, Some("show_thinking = false\n"));
let mut tui = launch(&workspace, &provider, (24, 80), "--fresh", false);
stream_first_turn(&mut tui, &provider, false);
let frame = tui.frame();
assert!(!frame.contains("Thought"), "{}", frame.debug_dump());
tui.shutdown();
let saved: toml::Value = toml::from_str(
&std::fs::read_to_string(workspace.home().join(".codewhale/settings.toml")).unwrap(),
)
.unwrap();
assert_eq!(saved["show_thinking"].as_bool(), Some(false));
}
fn sign_in(workspace: &SealedWorkspace, provider: &PacedProvider, session: &str) {
let path = workspace.home().join(".codewhale/secrets/secrets.json");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let secrets =
codewhale_secrets::Secrets::new(Arc::new(codewhale_secrets::FileKeyringStore::new(path)));
let bundle = serde_json::from_value(serde_json::json!({
"tokenType": "Bearer",
"accessToken": "loopback-access",
"refreshToken": "loopback-refresh",
"session": {"id": session},
"user": {"id": "loopback-account"}
}))
.unwrap();
codewhale_secrets::account::AccountSessionStore::new(secrets, None, &provider.account_base())
.save(bundle)
.unwrap();
}
fn run_ungated_turn(tui: &mut Harness, provider: &PacedProvider) {
provider.release(3);
tui.type_line(PROMPTS[0]).unwrap();
expect(tui, "the answer", |frame| frame.contains(ANSWER));
expect(tui, "the settled turn", |frame| {
!frame.contains("Esc to interrupt")
});
}
#[test]
fn account_preferences_notice_is_shown_once_per_sign_in_and_esc_dismisses_it() {
let workspace = make_sealed_workspace().unwrap();
let provider = PacedProvider::start();
prepare(&workspace, &provider, None);
sign_in(&workspace, &provider, "first-sign-in");
for cols in [80, 120] {
let workspace = make_sealed_workspace().unwrap();
let provider = PacedProvider::start();
prepare(&workspace, &provider, None);
sign_in(&workspace, &provider, "first-sign-in");
let mut tui = launch(&workspace, &provider, (32, cols), "--fresh", false);
run_ungated_turn(&mut tui, &provider);
expect(&mut tui, "the standing notice", |frame| {
frame.contains(NOTICE)
});
report(&mut tui, "account notice");
assert!(provider.profile_requests.load(Ordering::Acquire) > 0);
let (_, status) = footer_row(&mut tui);
assert!(
status.contains(NOTICE),
"the notice is footer chrome: {status}"
);
assert!(!tui.frame().contains(RETIRED_TRANSCRIPT_NOTICE));
tui.send(keys::key::esc()).unwrap();
expect(&mut tui, "the dismissed notice", |frame| {
!frame.contains(NOTICE)
});
report(&mut tui, "account notice dismissed");
tui.shutdown();
}
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
run_ungated_turn(&mut tui, &provider);
expect(&mut tui, "the first notice", |frame| frame.contains(NOTICE));
tui.shutdown();
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
provider.turns.store(0, Ordering::Release);
run_ungated_turn(&mut tui, &provider);
tui.wait_for_idle(Duration::from_secs(6), Duration::from_secs(30))
.unwrap();
report(&mut tui, "same sign-in, later session");
assert!(
!tui.frame().contains(NOTICE),
"the same sign-in is told once"
);
assert!(!tui.frame().contains(RETIRED_TRANSCRIPT_NOTICE));
tui.shutdown();
sign_in(&workspace, &provider, "second-sign-in");
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
provider.turns.store(0, Ordering::Release);
run_ungated_turn(&mut tui, &provider);
expect(&mut tui, "the notice for a later sign-in", |frame| {
frame.contains(NOTICE)
});
report(&mut tui, "later sign-in");
tui.shutdown();
}
@@ -24,4 +24,4 @@
{"engine_session_id":"conformance-session","event":"session_updated","instruction_assembly":{"budgets":{"claude_fallback":false,"file_max_bytes":8388608,"global_tokens":100000,"policy":"trim","project_tokens":100000,"static_guard_tokens":667571,"usable_window_tokens":834464},"constitution_source_path":null,"constitution_tokens":0,"global_tokens":0,"notices":[{"action":"Kept","bytes_dropped":0,"bytes_kept":498,"detail":null,"fallback_selected":null,"limit":"None","origin":"Instructions","path":null,"scope":"Project","tokens_dropped":0,"tokens_kept":166}],"project_tokens":166,"retained_block_tokens":188,"source_path":null,"warnings":[]},"messages":[{"content":[{"text":"Create denied.txt with touch.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\nEstimated conversation input: 1973 / 1000000 context tokens (0.2%). Automatic compaction is enabled.\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"<codewhale:runtime_event kind=\"project_instructions\" visibility=\"internal\">\n<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions. Instructions from: AGENTS.md\n\n<project_instructions source=\"project\">\n# Project Context (Auto-generated, ephemeral)\n\n> This context was generated in memory by Codewhale.\n> No .codewhale/instructions.md file was written.\n\n## Bounded Project Overview\n\n```json\n{\n \"project_name\": \"workspace\",\n \"directory_structure\": [\n \"README.md\"\n ],\n \"readme\": {\n \"path\": \"README.md\",\n \"excerpt\": \"conformance fixture\"\n },\n \"config_files\": [],\n \"key_source_files\": [],\n \"counts\": {\n \"config_files\": 0,\n \"directory_entries\": 1,\n \"key_source_files\": 0\n }\n}\n```\n</project_instructions>\n</system-reminder>\n</codewhale:runtime_event>","type":"text"},{"text":"<turn_meta>\nInput provenance: runtime (non-authoritative)\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"execution_id":"<uuid:2>","id":"call_touch","input":{"command":"touch denied.txt"},"name":"Bash","type":"tool_use"}],"role":"assistant"},{"content":[{"content":"Error: Tool 'Bash' denied by user — the call was not approved. Do not retry the same call; present what you intended and wait for the user's approval or new instructions.","execution_id":"<uuid:2>","is_error":true,"tool_use_id":"call_touch","type":"tool_result"}],"role":"user"},{"content":[{"text":"Understood, I did not create it.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":1,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":200,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":2,"model_step_index":1,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesysteLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":350,"output_tokens":21},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":350,"output_tokens":21}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","README.md":"41f49bffb38de5ba"}}}
@@ -15,4 +15,4 @@
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":0,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":null,"last_reported_input_tokens":120,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":null,"model_requests_started":1,"model_step_index":0,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"interrupted","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"interrupted","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound networkLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":0},"routed_usage_dropped_records":0,"status":"interrupted","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":120,"output_tokens":0}}
{"event":"status","message":"Turn interrupted."}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
@@ -25,4 +25,4 @@
{"engine_session_id":"conformance-session","event":"session_updated","instruction_assembly":{"budgets":{"claude_fallback":false,"file_max_bytes":8388608,"global_tokens":100000,"policy":"trim","project_tokens":100000,"static_guard_tokens":667571,"usable_window_tokens":834464},"constitution_source_path":null,"constitution_tokens":0,"global_tokens":0,"notices":[{"action":"Kept","bytes_dropped":0,"bytes_kept":514,"detail":null,"fallback_selected":null,"limit":"None","origin":"Instructions","path":null,"scope":"Project","tokens_dropped":0,"tokens_kept":172}],"project_tokens":172,"retained_block_tokens":193,"source_path":null,"warnings":[]},"messages":[{"content":[{"text":"What does README.md say?","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\nEstimated conversation input: 1972 / 1000000 context tokens (0.2%). Automatic compaction is enabled.\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"<codewhale:runtime_event kind=\"project_instructions\" visibility=\"internal\">\n<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions. Instructions from: AGENTS.md\n\n<project_instructions source=\"project\">\n# Project Context (Auto-generated, ephemeral)\n\n> This context was generated in memory by Codewhale.\n> No .codewhale/instructions.md file was written.\n\n## Bounded Project Overview\n\n```json\n{\n \"project_name\": \"workspace\",\n \"directory_structure\": [\n \"README.md\",\n \"NOTES.md\"\n ],\n \"readme\": {\n \"path\": \"README.md\",\n \"excerpt\": \"conformance fixture\"\n },\n \"config_files\": [],\n \"key_source_files\": [],\n \"counts\": {\n \"config_files\": 0,\n \"directory_entries\": 2,\n \"key_source_files\": 0\n }\n}\n```\n</project_instructions>\n</system-reminder>\n</codewhale:runtime_event>","type":"text"},{"text":"<turn_meta>\nInput provenance: runtime (non-authoritative)\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"execution_id":"<uuid:2>","id":"call_read_readme","input":{"action":"read","path":"README.md"},"name":"File","type":"tool_use"}],"role":"assistant"},{"content":[{"content":"content_hash=\"sha256:41f49bffb38de5ba88945f09121ffc590f9d7f6ff80fdb9ea8ad10224fecea61\"\nconformance fixture","execution_id":"<uuid:2>","tool_use_id":"call_read_readme","type":"tool_result"}],"role":"user"},{"content":[{"text":"It says: conformance fixture.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":1,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":200,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":2,"model_step_index":1,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesysteLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":350,"output_tokens":26},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":350,"output_tokens":26}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
@@ -31,4 +31,4 @@
{"engine_session_id":"conformance-session","event":"session_updated","instruction_assembly":{"budgets":{"claude_fallback":false,"file_max_bytes":8388608,"global_tokens":100000,"policy":"trim","project_tokens":100000,"static_guard_tokens":667571,"usable_window_tokens":834464},"constitution_source_path":null,"constitution_tokens":0,"global_tokens":0,"notices":[{"action":"Kept","bytes_dropped":0,"bytes_kept":514,"detail":null,"fallback_selected":null,"limit":"None","origin":"Instructions","path":null,"scope":"Project","tokens_dropped":0,"tokens_kept":172}],"project_tokens":172,"retained_block_tokens":193,"source_path":null,"warnings":[]},"messages":[{"content":[{"text":"Read README.md and NOTES.md.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\nEstimated conversation input: 1973 / 1000000 context tokens (0.2%). Automatic compaction is enabled.\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- NOTES.md (file)\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"<codewhale:runtime_event kind=\"project_instructions\" visibility=\"internal\">\n<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions. Instructions from: AGENTS.md\n\n<project_instructions source=\"project\">\n# Project Context (Auto-generated, ephemeral)\n\n> This context was generated in memory by Codewhale.\n> No .codewhale/instructions.md file was written.\n\n## Bounded Project Overview\n\n```json\n{\n \"project_name\": \"workspace\",\n \"directory_structure\": [\n \"README.md\",\n \"NOTES.md\"\n ],\n \"readme\": {\n \"path\": \"README.md\",\n \"excerpt\": \"conformance fixture\"\n },\n \"config_files\": [],\n \"key_source_files\": [],\n \"counts\": {\n \"config_files\": 0,\n \"directory_entries\": 2,\n \"key_source_files\": 0\n }\n}\n```\n</project_instructions>\n</system-reminder>\n</codewhale:runtime_event>","type":"text"},{"text":"<turn_meta>\nInput provenance: runtime (non-authoritative)\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"execution_id":"<uuid:2>","id":"call_read_a","input":{"action":"read","path":"README.md"},"name":"File","type":"tool_use"},{"execution_id":"<uuid:3>","id":"call_read_b","input":{"action":"read","path":"NOTES.md"},"name":"File","type":"tool_use"}],"role":"assistant"},{"content":[{"content":"content_hash=\"sha256:41f49bffb38de5ba88945f09121ffc590f9d7f6ff80fdb9ea8ad10224fecea61\"\nconformance fixture","execution_id":"<uuid:2>","tool_use_id":"call_read_a","type":"tool_result"}],"role":"user"},{"content":[{"content":"content_hash=\"sha256:f957b19529906961933c5c30f8713c500a9bb5d9d0695c40d48c97a26a3594ec\"\nsecond file","execution_id":"<uuid:3>","tool_use_id":"call_read_b","type":"tool_result"}],"role":"user"},{"content":[{"text":"Both files read.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":1,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":200,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":2,"model_step_index":1,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesysteLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":360,"output_tokens":46},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":360,"output_tokens":46}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
@@ -15,4 +15,4 @@
{"engine_session_id":"conformance-session","event":"session_updated","instruction_assembly":{"budgets":{"claude_fallback":false,"file_max_bytes":8388608,"global_tokens":100000,"policy":"trim","project_tokens":100000,"static_guard_tokens":667571,"usable_window_tokens":834464},"constitution_source_path":null,"constitution_tokens":0,"global_tokens":0,"notices":[{"action":"Kept","bytes_dropped":0,"bytes_kept":514,"detail":null,"fallback_selected":null,"limit":"None","origin":"Instructions","path":null,"scope":"Project","tokens_dropped":0,"tokens_kept":172}],"project_tokens":172,"retained_block_tokens":193,"source_path":null,"warnings":[]},"messages":[{"content":[{"text":"Say hello.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\nEstimated conversation input: 1968 / 1000000 context tokens (0.2%). Automatic compaction is enabled.\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"<codewhale:runtime_event kind=\"project_instructions\" visibility=\"internal\">\n<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions. Instructions from: AGENTS.md\n\n<project_instructions source=\"project\">\n# Project Context (Auto-generated, ephemeral)\n\n> This context was generated in memory by Codewhale.\n> No .codewhale/instructions.md file was written.\n\n## Bounded Project Overview\n\n```json\n{\n \"project_name\": \"workspace\",\n \"directory_structure\": [\n \"README.md\",\n \"NOTES.md\"\n ],\n \"readme\": {\n \"path\": \"README.md\",\n \"excerpt\": \"conformance fixture\"\n },\n \"config_files\": [],\n \"key_source_files\": [],\n \"counts\": {\n \"config_files\": 0,\n \"directory_entries\": 2,\n \"key_source_files\": 0\n }\n}\n```\n</project_instructions>\n</system-reminder>\n</codewhale:runtime_event>","type":"text"},{"text":"<turn_meta>\nInput provenance: runtime (non-authoritative)\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"Hello there.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":0,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":120,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":1,"model_step_index":0,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesysteLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":4},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":120,"output_tokens":4}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
@@ -13,4 +13,4 @@
{"event":"tool_call_complete","result":{"content":"Not executed: the provider stream failed before the model response completed (Model not exist.).","metadata":{"error_category":"model_stream_failed","model_output_incomplete":true,"side_effect_status":"not_started"},"outcome":"ok","success":false},"tool_call_id":"<uuid:2>","tool_name":"File"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":0,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":null,"last_reported_input_tokens":120,"last_response_tool_calls":1,"last_response_tool_calls_suppressed":1,"model_requests_started":1,"model_step_index":0,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"failed","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"failed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only iLine truncated
{"error":"Model not exist.","event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":0},"routed_usage_dropped_records":0,"status":"failed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":120,"output_tokens":0}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
+4 -1
View File
@@ -2230,7 +2230,10 @@ Common settings keys:
`@`-mention completions are populated. `fuzzy` searches the workspace and
applies mention frecency. `browser` lists only the immediate children of the
currently typed directory segment in deterministic alphabetical order.
- `show_thinking` (on/off)
- `show_thinking` (on/off, default on): shows model thinking in the transcript,
live while it streams and collapsed under a `Thought for …` header once the
answer starts. Off hides it; answers and tools stay visible. A value already
saved in `settings.toml` is kept.
- `thinking_default_expanded` (on/off, default off): renders thinking blocks
expanded initially when `show_thinking` is enabled. Space still toggles the
selected block, and it decides only the blocks you have not touched: a block
@@ -68,8 +68,8 @@ ids to customers.
## Subagent state
Subagent state lives in the workspace, under `.codewhale/state/`, in
`subagents.v1.json` (the manager's `SUBAGENT_STATE_FILE`; the lock is
`subagents.v1.lock`), loaded by `load_state` and written by
`subagents.v3.json` (the manager's `SUBAGENT_STATE_FILE`), loaded by
`load_state` and written by
`persist_state*` in `crates/engine/src/tools/subagent/mod.rs`. A legacy
`.deepseek/state/` copy is still read for migration.
-11
View File
@@ -161,17 +161,6 @@ separately from the #6953 reporter-observed exports; the x64 profile now allows
468 pairs. The candidate still has to pass the import check against its final
executable bytes, and neither addition is a 14393 or installer run.
Release candidate run 43186389246 on source `f5a3dda975` then stopped the same
check on three more pairs, `GetSidIdentifierAuthority`, `GetSidSubAuthority` and
`GetSidSubAuthorityCount` from advapi32.dll, on both 64-bit targets. Probe run
43197611105 (Windows 11 ARM64 build 26200.9725) resolved all 447 imports of
that candidate's ARM64 executables, and the 3 pairs were added. For x64 the 3
pairs are documented Microsoft API contracts (Windows XP minimum on each
function's reference page), labelled apart from the #6953 reporter-observed
exports; the x64 profile now allows 471 pairs and the ARM64 profile 473. The
same limit applies: the candidate still has to pass the import check against
its final executable bytes, and neither addition is a 14393 or installer run.
At this snapshot, startup/installation of the final release bytes on an actual
1607 machine and the candidate's generated uninstaller were still unqualified.
The final frozen source, release import dumps, archive/native/npm installation,
+30 -12
View File
@@ -29,7 +29,7 @@ resolved values are persisted in their launch manifest.
By default, delegated control-plane state remains workspace-scoped at
`<workspace>/.codewhale/state`: the worker ledger, complete transcript
artifacts, and coordination lock share that root. An embedding host may set
artifacts, and store locks share that root. An embedding host may set
`EngineConfig::subagent_state_root` to keep those files under a session-owned
root without changing child cwd, tool path authority, or the execution
workspace recorded in receipts.
@@ -124,19 +124,32 @@ cannot replace a newer one, and a terminal receipt cannot be resurrected as
Running by a checkpoint. An explicit terminal retry accepts only the same
winning outcome; authoritative newer accounting metadata is retained.
Normal interactive/exec/Fleet/background sessions continue to share the
existing admission flock. The explicit exclusive Runtime constructor retains
its host admission policy; it is not the mechanism that conserves shared
snapshots. No manager holds the transaction lock while a child executes or
while it waits for a provider or channel capacity. Conflicting lock contention
returns a bounded retry error. Unsupported locking or failed store admission
refuses durable writes rather than silently creating an empty writable ledger.
There is no workspace-wide session lock. Interactive, exec, Fleet, background
and Runtime managers in one folder are ordinary neighbours: each holds only its
own boot lease and takes the transaction lock per operation. The Runtime
constructor keeps one manager per state path inside its process. No manager
holds the transaction lock while a child executes or while it waits for a
provider or channel capacity. Conflicting lock contention returns a bounded
retry error. Unsupported locking or failed store admission refuses durable
writes rather than silently creating an empty writable ledger, and admission is
retried on the next delegated write instead of staying refused for the life of
the process.
A Runtime that admits a selected workspace other than its own state root keeps
that workspace's claims in its own store. Before it admits a shared-workspace
writer there, and each time it leases such a writer, it refuses while any
session holds a live boot lease in the selected workspace's own store, and it
refuses when that store's directory cannot be listed. Only sessions that hold
a boot lease are seen: a Codewhale 0.10.2 or older session holds none. A
session that opens in that workspace later does not see the Runtime's claims,
and its lease refuses the Runtime's next shared-workspace writer there; give
writers isolated worktrees when both are expected.
Each writer holds its own boot lease. Other managers preserve its live records
and use its active claims during admission. An existing unlocked lease can
establish that its writer ended; a missing, invalid or unreadable lease is
uncertain and fails closed. Local task handles and a failed admission flock do
not establish that another process's work stopped. Parked and headless
uncertain and fails closed. Local task handles do not establish that another
process's work stopped. Parked and headless
WaitingForUser claim rules retain their existing lifecycle meaning; they do not
transfer a still-live writer's record ownership to a different boot.
@@ -216,10 +229,15 @@ terminal receipt or claim that a remote effect was undone.
### Legacy cutover and recovery
The first current-format admission takes the legacy admission lock exclusively
only for migration, after previous holders have left. It retains the original
The first current-format admission migrates under the short transaction lock.
It does not wait for older sessions that still hold the retired
`subagents.v1.lock`. It retains the original
legacy bytes and a validated recovery copy, then leaves
`subagents.v1.json` as a private directory containing that recovery material.
An older session still open in that folder can no longer save its delegated
state once the directory replaces its file; restart it. If it publishes its
file between the two cutover renames, the cutover stops at a retained-file
refusal until a person inspects both files; nothing is deleted.
The current format3 file is published under the same short transaction lock.
Interrupted cutover states are validated and resumed without replacing a later
current-format revision with the old backup. Future or malformed envelopes
+1 -1
View File
@@ -805,7 +805,7 @@ owner = "crates/tui/src/commands/groups/session/export.rs"
[[feature]]
id = "live-thinking"
name = "Live thinking"
summary = "/verbose toggles live model thinking in the transcript."
summary = "Model thinking shows live in the transcript, then collapses under a Thought header; /verbose shows it in full."
status = "stable"
surfaces = { tui = "stable" }
owner = "crates/tui/src/commands/groups/config/mod.rs"
+2 -7
View File
@@ -97,13 +97,8 @@ Stage 5 of the loop: [cw-orient](../cw-orient/SKILL.md) →
Co-authored-by: <maintainer or agent identity>
```
GitHub's contributor count takes authors and `Co-authored-by` trailers in
the final trailer block (no blank line between trailers) whose address is
linked to the account, and `check-contributor-credit.py` flags a harvested
handle who is not credited, a trailer GitHub cannot parse, and a noreply id
that belongs to someone else. It accepts credit only from a noreply address
whose id matches `.github/AUTHOR_MAP`, or from a raw email listed there, so
add the contributor's line to `AUTHOR_MAP` when it is missing.
GitHub's contributor list counts authors only, and
`check-contributor-credit.py` flags a harvested handle who is not credited.
Resolve the address from `.github/AUTHOR_MAP`, else
`gh api users/<handle> --jq '"\(.id)+\(.login)@users.noreply.github.com"'`.
A clean cherry-pick keeps the author by itself. This step is the single
+1 -1
View File
@@ -1152,7 +1152,7 @@ codewhale 还把用户偏好存储在:
- `mention_menu_limit`(整数,默认 `128`):输入区渲染可见窗口之前保留的 `@`-提及弹出候选最大数。可见行仍取决于终端高度。
- `mention_walk_depth`(整数,默认 `10`):`@`-提及补全遍历的最大工作区深度。在深度嵌套的工作区设为 `0` 表示无限深度;在非常大的仓库保持默认,除非需要。
- `mention_menu_behavior`(`fuzzy`、`browser`;默认 `fuzzy`):控制 `@`-提及补全如何填充。`fuzzy` 搜索工作区并应用提及 frecency。`browser` 只按确定性字母顺序列出当前输入目录段的直接子项。
- `show_thinking`(on/off)
- `show_thinking`(on/off,默认 on):在对话记录中显示模型思考,流式输出时实时显示,回答开始后折叠到“思考用时 …”标题下。设为 off 则隐藏思考;回答和工具仍然可见。`settings.toml` 中已保存的值保持不变。
- `thinking_default_expanded`(on/off,默认 off):启用 `show_thinking` 时,让思考块初始以展开状态渲染。Space 仍可切换所选块,并且它只决定你尚未动过的块:你自己展开或折叠过的块,即使之后更改这个设置,也会保持该状态。这在 Space 绑定可能被拦截的 SSH/tmux 环境中很有用。
- `thinking_preview_lines`(整数,默认 `2`):**折叠**的已完成思考仍显示多少正文行。`0` 仅头部;`10` 是较旧的完整显示。实时流式预览不变。用 Space 展开块,或设置 `thinking_default_expanded` 打开每个块。
- `help_expand_groups`(on/off,默认 off):让帮助/快捷键从每个组展开开始。默认折叠长尾(Grok 风格);输入过滤仍展开匹配。
-5
View File
@@ -127,11 +127,6 @@ DLL 的身份一致,[该 API 的文档前提是 Vista](https://learn.microsoft
x64 没有导出探测,因此这 12 对作为低于 14393 下限的微软文档 API 契约单独记录,与 #6953 报告者实测导出分开标注;x64 允许列表现为 468 对。
候选仍须以最终可执行文件通过导入检查,两项新增都不是 14393 或安装器运行。
源 `f5a3dda975` 的发布候选运行 43186389246 随后在两个 64 位目标上又停止于三对导入:advapi32.dll 的 `GetSidIdentifierAuthority`、`GetSidSubAuthority` 和 `GetSidSubAuthorityCount`。
探测运行 43197611105(Windows 11 ARM64 内部版本 26200.9725)解析了该候选 ARM64 程序的全部 447 个导入,新增的 3 对已加入基线。
x64 的这 3 对作为微软文档 API 契约记录(各函数参考页的最低客户端为 Windows XP),与 #6953 报告者实测导出分开标注;x64 允许列表现为 471 对,ARM64 为 473 对。
限制不变:候选仍须以最终可执行文件通过导入检查,两项新增都不是 14393 或安装器运行。
此快照中,最终发布文件在真实 1607 上启动/安装及候选程序生成的卸载器仍未验收。
最终冻结源码、发布导入记录、各安装途径、打包/原生应用崩溃恢复和最低 OS 回执应
进入发布验收记录,见[英文发布检查单](../RELEASE_CHECKLIST.md)。
+1 -1
View File
@@ -257,7 +257,7 @@
},
"crates/engine/src/tools/subagent/mod.rs": {
"path_canonicalize": 4,
"std_fs": 3,
"std_fs": 2,
"thread_sleep": 1
},
"crates/engine/src/tools/subagent/worktree.rs": {
+20 -91
View File
@@ -17,7 +17,7 @@ live in the commit bodies, so no network call is needed.
Usage: scripts/check-contributor-credit.py [<since-rev>]
"""
from __future__ import annotations
import functools, re, subprocess, sys
import re, subprocess, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
@@ -35,7 +35,6 @@ def sh(*args: str) -> str:
return subprocess.run(args, cwd=ROOT, capture_output=True, text=True, check=True).stdout
@functools.cache
def author_map() -> dict[str, str]:
"""alias -> github handle, from .github/AUTHOR_MAP."""
out: dict[str, str] = {}
@@ -53,43 +52,19 @@ def author_map() -> dict[str, str]:
return out
NOREPLY = re.compile(r"\d+\+([^@<>\s]+)@users\.noreply\.github\.com")
def not_a_person(name: str, email: str) -> bool:
low, addr = name.strip().lower(), email.lower()
return any(low.startswith(skip) for skip in SKIP) or "[bot]" in addr \
or addr.endswith(("[email protected]", "[email protected]"))
def maintainer(name: str, email: str) -> bool:
handle = author_map().get(email.lower()) or author_map().get(name.strip().lower()) or name.strip()
return any(h.lower().startswith(skip) for skip in SKIP for h in (name.strip(), handle))
def restoring(subject: str, name: str, email: str) -> bool:
return subject.startswith("chore(credit):") and maintainer(name, email)
def noreply_ids() -> dict[str, str]:
out: dict[str, str] = {}
path = ROOT / ".github" / "AUTHOR_MAP"
if path.exists():
for login_id, login in re.findall(r"(\d+)\+([^@<>\s]+)@users\.noreply\.github\.com", path.read_text()):
out[login.lower()] = login_id
return out
def expected(since: str) -> dict[str, str]:
"""handle -> why we think they are owed credit."""
amap = author_map()
found: dict[str, str] = {}
def add(name: str, email: str, why: str) -> None:
low = name.strip().lower()
# Model co-authorship ("Claude Opus 5 (1M context)") and bot addresses
# are not people owed credit; match on prefix since the model name and
# its context window change release to release.
if not_a_person(name, email):
if any(low.startswith(skip) for skip in SKIP):
return
if "[bot]" in email.lower() or email.lower().endswith(("[email protected]", "[email protected]")):
return
handle = amap.get(email.lower()) or amap.get(name.strip().lower())
if not handle:
@@ -99,17 +74,16 @@ def expected(since: str) -> dict[str, str]:
return
found.setdefault(handle, why)
log = sh("git", "log", f"{since}..HEAD", "--format=%an%x1f%ae%x1f%s%x1f%b%x00")
work = [parts for parts in (entry.lstrip("\n").split("\x1f", 3) for entry in log.split("\0"))
if len(parts) == 4 and not restoring(parts[2], parts[0], parts[1])]
for author, email, _, _ in work:
add(author, email, "commit author")
for _, _, _, body in work:
for name, addr in re.findall(r"(?im)^co-authored-by:\s*(.+?)\s*<([^>]+)>", body):
add(name, addr, "co-author trailer")
for pr, handle in re.findall(r"(?i)harvested from PR #(\d+) by @([A-Za-z0-9-]+)", body):
if handle.lower() not in SKIP:
found.setdefault(handle, f"harvested PR #{pr}")
for line in sh("git", "log", f"{since}..HEAD", "--format=%an\t%ae").splitlines():
name, _, email = line.partition("\t")
add(name, email, "commit author")
body = sh("git", "log", f"{since}..HEAD", "--format=%b")
for name, email in re.findall(r"(?im)^co-authored-by:\s*(.+?)\s*<([^>]+)>", body):
add(name, email, "co-author trailer")
for pr, handle in re.findall(r"(?i)harvested from PR #(\d+) by @([A-Za-z0-9-]+)", body):
if handle.lower() not in SKIP:
found.setdefault(handle, f"harvested PR #{pr}")
return found
@@ -134,50 +108,17 @@ def main() -> int:
print(f"Contributor credit since {since}: {len(owed)} contributor(s) found.")
amap = author_map()
ids = noreply_ids()
lost = []
broken: list[tuple[str, str]] = []
credited: set[str] = set()
def login(name: str, addr: str) -> str:
nm = NOREPLY.search(addr)
return (amap.get(addr.lower()) or amap.get(name.strip().lower()) or (nm.group(1) if nm else name.strip())).lower()
def who(name: str, addr: str) -> str:
nm = re.search(r"(\d+)\+([^@<>\s]+)@users\.noreply\.github\.com", addr)
if not nm:
return amap.get(addr.lower(), "").lower() if "@" in addr else ""
known = ids.get(nm.group(2).lower())
if known is not None and known != nm.group(1):
broken.append((nm.group(2).lower(), f"uses {addr}, but @{nm.group(2)} is id {known} "
f"in AUTHOR_MAP; GitHub credits whoever owns id {nm.group(1)}"))
return nm.group(2).lower() if known == nm.group(1) else ""
log = sh("git", "log", f"{since}..HEAD",
"--format=%h%x1f%an%x1f%ae%x1f%(trailers:key=Co-authored-by,unfold)%x1f%B%x00")
for entry in log.split("\0"):
parts = entry.strip().split("\x1f", 4)
if len(parts) < 5:
for entry in sh("git", "log", f"{since}..HEAD", "--format=%h\t%an\t%ae\t%B%x00").split("\0"):
parts = entry.strip().split("\t", 3)
if len(parts) < 4:
continue
sha, name, email, parsed, body = parts
sha, name, email, body = parts
m = re.search(r"\d+\+([^@<>\s]+)@users\.noreply\.github\.com", email)
author = (amap.get(email.lower()) or amap.get(name.lower()) or (m.group(1) if m else name)).lower()
trailers = re.findall(r"(?im)^co-authored-by:\s*(.+?)\s*<([^>]+)>", parsed)
mark = len(broken)
credited.add(who(name, email))
credited.update(who(n, e) for n, e in trailers)
broken[mark:] = [(login, f"{sha} {why}") for login, why in broken[mark:]]
for co_name, co_email in re.findall(r"(?im)^\s*co-authored-by:\s*(.+?)\s*<([^>]+)>", body):
if (co_name, co_email) not in trailers and not not_a_person(co_name, co_email):
broken.append((login(co_name, co_email),
f"{sha} has 'Co-authored-by: {co_name}' outside the final trailer block; GitHub ignores it"))
if restoring(body.split("\n", 1)[0], name, email):
continue
for handle in re.findall(r"(?i)harvested from PR #\d+ by @([A-Za-z0-9-]+)", body):
if handle.lower() != author:
lost.append(f" {sha} harvested @{handle} but is authored by {name}")
broken.append((amap.get(handle.lower(), handle).lower(),
f"{sha} harvested @{handle} with neither authorship nor a Co-authored-by trailer for them"))
if any(author.startswith(skip) for skip in SKIP):
for co_name, co_email in re.findall(r"(?im)^co-authored-by:\s*(.+?)\s*<([^>]+)>", body):
cm = re.search(r"\d+\+([^@<>\s]+)@users\.noreply\.github\.com", co_email)
@@ -187,18 +128,8 @@ def main() -> int:
and "[bot]" not in co_email.lower() and not co_email.lower().endswith(("[email protected]", "[email protected]")):
lost.append(f" {sha} credits @{human} only as a co-author; authored by {name}")
if lost:
print("\nCommits whose human contributor is not the commit author (GitHub counts them only through a\n"
"well-formed Co-authored-by trailer whose address is linked to their account):\n")
print("\nCommits whose human contributor is not the commit author (GitHub's contributor list will miss them):\n")
print("\n".join(lost))
broken = [(login, why) for login, why in broken if login.lower() not in credited]
if broken:
print("\nCredit GitHub cannot see:\n")
print("\n".join(f" {why}" for _, why in broken))
print("\nPut each contributor in the final trailer block as\n"
"'Co-authored-by: Name <[email protected]>', with no blank line between trailers,\n"
"using the id from `gh api users/<login>`. A later commit can carry the trailer when history cannot change.\n"
"Credit is taken only from such an address, or from a raw email listed in .github/AUTHOR_MAP; add the\n"
"email a contributor commits with there once it is linked to their account.")
if missing:
print("\nUncredited work landed in this release:\n")
print("\n".join(missing))
@@ -208,8 +139,6 @@ def main() -> int:
"is still their work -- see AGENTS.md 'Landing other people's work'."
)
return 1
if broken:
return 1
print("Every contributor in the window is credited on all three surfaces.")
return 0
+6 -6
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""Everyone whose work landed in or is credited by Codewhale, on one wall.
GitHub's contributor count takes only commit authors and co-author trailers
whose email is linked to an account. Harvested pull requests without a trailer,
unlinked commit emails and credited bug reports never reach it, so it shows
about half of the people who built Codewhale.
GitHub's contributor list counts only commit authors whose email is linked to
an account. Harvested pull requests, co-author trailers, unlinked commit
emails and credited bug reports never reach it, so it shows about half of the
people who built Codewhale.
refresh rebuild docs/contributors.json from git history, the CHANGELOG,
docs/CONTRIBUTORS.md and GitHub (needs `gh`)
@@ -202,8 +202,8 @@ def render() -> None:
"<!-- contributor-wall:start -->\n## Everyone\n\n"
f"[![{total} people built Codewhale](assets/contributors.png)](#everyone)\n\n"
f"{total} people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. "
"GitHub's contributor count includes only commit authors and co-authors whose email is linked to their "
"account, so it misses many of them. If your commits are missing there, add your commit email to your "
"GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts "
f"{data['github_contributor_list']} of them. If your commits are missing there, add your commit email to your "
"GitHub account and they will link.\n\n"
f"{links}\n\nAlso with us, from commits whose email is not linked to an account: {' · '.join(extra)}.\n"
"<!-- contributor-wall:end -->\n"
+53 -144
View File
@@ -6,7 +6,7 @@
"osArchitecture": "x64",
"qualification": {
"result": "PASS",
"kind": "reporter exact-OS export resolution plus 15 documented Microsoft named API minima",
"kind": "reporter exact-OS export resolution plus 12 documented Microsoft named API minima",
"evidence": {
"issue": "https://github.com/codewhale-hq/Codewhale/issues/6953",
"binary": "https://github.com/codewhale-hq/Codewhale/releases/download/v0.10.1/codewhale-windows-x64.exe",
@@ -16,7 +16,7 @@
"previous_sha256": "f7b35a1e928f67884637f36b01fc72854f50690130f191bac3bcf62ece3c1d84"
},
"runtimeAcceptance": "Final 0.10.2 release bytes on 14393 not run",
"qualifiedPairs": 471,
"qualifiedPairs": 468,
"namedApiContracts": [
{
"dll": "advapi32.dll",
@@ -89,24 +89,6 @@
"symbol": "expf",
"minimumClient": "Exact API-set pair introduced in 10.0.10240",
"source": "https://learn.microsoft.com/en-us/uwp/win32-and-com/win32-extension-apis"
},
{
"dll": "advapi32.dll",
"symbol": "GetSidIdentifierAuthority",
"minimumClient": "Windows XP",
"source": "https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-getsididentifierauthority"
},
{
"dll": "advapi32.dll",
"symbol": "GetSidSubAuthority",
"minimumClient": "Windows XP",
"source": "https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-getsidsubauthority"
},
{
"dll": "advapi32.dll",
"symbol": "GetSidSubAuthorityCount",
"minimumClient": "Windows XP",
"source": "https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-getsidsubauthoritycount"
}
],
"namedApiContractEvidence": {
@@ -115,21 +97,7 @@
"binarySha256": "cd5a1f9d4b53eb0f2ac1e29fbbedc71f8845d63ab85115192f60b78cf197e2fa",
"importReceiptSha256": "a872374cb767467732c5f36e4b315d58fd424107a9a725800673b9181a714f02",
"ucrtPlatformSource": "https://learn.microsoft.com/en-us/cpp/porting/upgrade-your-code-to-the-universal-crt?view=msvc-170",
"scope": "Documentation-backed static API contracts below the unchanged 14393 floor; not reporter-observed exports, a native x64 export probe, executable startup, installer or ordinary-user acceptance",
"additionalCandidates": [
{
"candidateRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43186389246",
"source": "f5a3dda9755170aeafecd0841347a5578a540290",
"binarySha256": "6fa43dc8c8680cf3e30a38ea950154b28e30aa97fe373fe82fae6086aa58e1c1",
"importReceiptSha256": "9cc807d09bee35101a926773934937427a53214e6e1a54fb85e622b3f18a6ae3",
"pairs": [
"advapi32.dll!GetSidIdentifierAuthority",
"advapi32.dll!GetSidSubAuthority",
"advapi32.dll!GetSidSubAuthorityCount"
],
"scope": "Three pairs from crates/config private_directory.rs; documentation-backed static contracts, same limits as above"
}
]
"scope": "Documentation-backed static API contracts below the unchanged 14393 floor; not reporter-observed exports, a native x64 export probe, executable startup, installer or ordinary-user acceptance"
}
},
"imports": {
@@ -153,9 +121,6 @@
"GetSecurityDescriptorControl",
"GetSecurityDescriptorDacl",
"GetSecurityInfo",
"GetSidIdentifierAuthority",
"GetSidSubAuthority",
"GetSidSubAuthorityCount",
"GetTokenInformation",
"ImpersonateAnonymousToken",
"InitializeAcl",
@@ -666,25 +631,25 @@
"qualification": {
"result": "PASS",
"kind": "exact native system export resolution",
"time": "2026-11-12T10:08:11.6545236Z",
"time": "2026-11-07T16:01:13.7658879Z",
"osVersion": "10.0.26200.0",
"osBuild": "26200.9725",
"processArchitecture": "ARM64",
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
"source": "f5a3dda9755170aeafecd0841347a5578a540290",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/f5a3dda9755170aeafecd0841347a5578a540290",
"binarySha256": "c8c6f97d0ccf20e392b2457cb970187e79a29caa28d091dbfefe3a1393b99040",
"source": "549ff01ebb78856408164058fcb52db1e639ee0d",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/549ff01ebb78856408164058fcb52db1e639ee0d",
"binarySha256": "38558ab2b4dc54590503543a0a584c5a82da6f078dd616b5b68e301bf09621b0",
"binaries": [
"codewhale-windows-arm64.exe",
"codew-windows-arm64.exe",
"codewhale-tui-windows-arm64.exe"
],
"resolved": 447,
"resolved": 444,
"unresolved": 0,
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43186389246",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43197611105",
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42381818665",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42389141087",
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 43186389246",
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 42381818665",
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
@@ -718,119 +683,66 @@
"supersedes": {
"result": "PASS",
"kind": "exact native system export resolution",
"time": "2026-11-07T16:01:13.7658879Z",
"time": "2026-10-16T10:57:53.0754367Z",
"osVersion": "10.0.26200.0",
"osBuild": "26200.9725",
"osBuild": "26200.9457",
"processArchitecture": "ARM64",
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
"source": "549ff01ebb78856408164058fcb52db1e639ee0d",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/549ff01ebb78856408164058fcb52db1e639ee0d",
"binarySha256": "38558ab2b4dc54590503543a0a584c5a82da6f078dd616b5b68e301bf09621b0",
"source": "db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
"binarySha256": "a9a2a521d8d0c971b9d49c2c463cdb49d0c7a4eb700b73e6fed2c743e688eebb",
"binaries": [
"codewhale-windows-arm64.exe",
"codew-windows-arm64.exe",
"codewhale-tui-windows-arm64.exe"
],
"resolved": 444,
"resolved": 445,
"unresolved": 0,
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42381818665",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42389141087",
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38867608774",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38890634267",
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 42381818665",
"profile": "release, built by release-artifacts.yml (windows-arm64 job) in release candidate run 38867608774",
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
"systemModuleHashes": {
"advapi32.dll": "31a87f532c0e99521de26fb25d1ec33cfbb92359952580c88350891602b6cf35",
"api-ms-win-core-synch-l1-2-0.dll": "06bf19bcd0b4af9e78c46fc81fe8fb1bf950fb5f295d44de889c5028129b5b56",
"api-ms-win-crt-convert-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-heap-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-locale-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-math-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-runtime-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-stdio-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-string-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-time-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-utility-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"bcrypt.dll": "2a016e6f29683a7e6be564424d286d0885ff4846fff87cefab4d4e5df47a8d4a",
"bcryptprimitives.dll": "6626bf08ddb38e3c37804d7a000c9684977fb0f5f7ab8eb1c0a91a654ec975fc",
"combase.dll": "6082ef654c1a0978e127a1e045cb3078b1e80f8f7108b5c6cb4bc601c69b7f51",
"crypt32.dll": "3f53b3351e75cb8847b87ce40349d48279cdddab4b19c4c6c9ad38eef6aa6baf",
"kernel32.dll": "905c5e00f64ddf9e3439a617b25d4dd1bd0fe54143ade689422b89ccb324ba3a",
"ntdll.dll": "0a7201420df35a67e76aa2f2f4adb793b0c0b4fbc927d7da112268a7beb74930",
"oleaut32.dll": "61cd70f21624866fb46102644d5af24563a30c85921ed8deed89139cab340f0b",
"shell32.dll": "b9a118ff822ab1e950c7a1009de223ecfea5d5f0a23b8a969d58ff19ce28d9cf",
"shlwapi.dll": "b4df8c76ea7999912ddf12ba026c5f4a55b683fe9234878f506080575481779a",
"user32.dll": "b95f8cce02f51a7cbf77f17582ad3eee8abc577c324963d0026e8dbb977fa097",
"userenv.dll": "e9b94fa6123048d535265214322a6a09e450dd85941292bf5258bf39d5b86f9e",
"advapi32.dll": "021452c20f89c50d64c304391ec18169f95a9fa14762f76cfd2dded627dbf0bb",
"api-ms-win-core-synch-l1-2-0.dll": "26be64c26bac33368a650372cb5fb1e3235f1bd7883471bd3291dbf6372ae274",
"api-ms-win-crt-convert-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-heap-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-locale-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-math-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-runtime-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-stdio-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-string-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-time-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-utility-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"bcrypt.dll": "12e3562488e5ca3a26884408d99d81898237c8ca3c5c3c9e40ca6b8f5ed361cd",
"bcryptprimitives.dll": "d3da1097e9e1fc16c910350e40c4405ef52f36f134f9bf165fbe02194c1eaa82",
"combase.dll": "850b43094ae61a5d3b8f40f0cddf89b4a90190bf5fc57c5242e0bf5b06644b2f",
"crypt32.dll": "e3e800f1905658674ce4af1d3efb31d722b9255aaae4fa46c54302d329ba4ab6",
"kernel32.dll": "6a7bd4daff9c0c4cc8b9478ecd9828c3f19be257a4a18db9559cae929197fffa",
"ntdll.dll": "60ef561645b50dce71ad0919dbbef3ad334a0489432cde718e75b73b558c7997",
"oleaut32.dll": "5ebb761b3f96b227006e79d6a92cf5d98ac5a5fd3c136d35dd05391bed867753",
"shell32.dll": "ef0fbd71a46fe4b35bff893ea65241cd08ea5231de704c106e39920c03698fe9",
"shlwapi.dll": "67f713491bc18614d6ea29b920e3c609cabf9eecf054990e030476a2e9f76a2c",
"user32.dll": "f2fc06e7e5016a15ad7f7f4ee250154b7d41d0a0706bf14927a3bbd4c42727dd",
"userenv.dll": "18bd1660ab7cacd90789f1bebe35bc6a5730b1e75e5582f53575473fae3db530",
"vcruntime140.dll": "3c56f4167e2b3d8e6338497731e6aae8cd7ec46bd6789f9423a8d9cf9a630310",
"winmm.dll": "4fd628e0a495b1fa7e7653b562a4f5484592c9cfe733184f9808dfce18e26680",
"ws2_32.dll": "d10c37755ef07b55829406cfaa22c623ae6f86cce5a5fed42d77d75c29ae824a"
"winmm.dll": "2ebb728e5c510492df8fde4ec81b8a8554f2f33d1686b837d22c962b7d2fa9b7",
"ws2_32.dll": "e1266872f796f68b4be9c20640ca940a7a96e1ba98c741d472ae305fccf87ca9"
},
"supersedes": {
"result": "PASS",
"kind": "exact native system export resolution",
"time": "2026-10-16T10:57:53.0754367Z",
"osVersion": "10.0.26200.0",
"osBuild": "26200.9457",
"processArchitecture": "ARM64",
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
"source": "db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
"binarySha256": "a9a2a521d8d0c971b9d49c2c463cdb49d0c7a4eb700b73e6fed2c743e688eebb",
"binaries": [
"codewhale-windows-arm64.exe",
"codew-windows-arm64.exe",
"codewhale-tui-windows-arm64.exe"
],
"resolved": 445,
"time": "2026-10-13T10:43:07.5742747Z",
"osVersion": "10.0.26300",
"osBuild": "26300",
"source": "cf7199b52ecb9147e4670ae7874924ffc30a6a67",
"binarySha256": "ca51f91562a0569ee43855715856e1cfee74e8a834bd6206183a3175373fdb2d",
"resolved": 453,
"unresolved": 0,
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38867608774",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38890634267",
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
"profile": "release, built by release-artifacts.yml (windows-arm64 job) in release candidate run 38867608774",
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
"systemModuleHashes": {
"advapi32.dll": "021452c20f89c50d64c304391ec18169f95a9fa14762f76cfd2dded627dbf0bb",
"api-ms-win-core-synch-l1-2-0.dll": "26be64c26bac33368a650372cb5fb1e3235f1bd7883471bd3291dbf6372ae274",
"api-ms-win-crt-convert-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-heap-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-locale-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-math-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-runtime-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-stdio-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-string-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-time-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-utility-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"bcrypt.dll": "12e3562488e5ca3a26884408d99d81898237c8ca3c5c3c9e40ca6b8f5ed361cd",
"bcryptprimitives.dll": "d3da1097e9e1fc16c910350e40c4405ef52f36f134f9bf165fbe02194c1eaa82",
"combase.dll": "850b43094ae61a5d3b8f40f0cddf89b4a90190bf5fc57c5242e0bf5b06644b2f",
"crypt32.dll": "e3e800f1905658674ce4af1d3efb31d722b9255aaae4fa46c54302d329ba4ab6",
"kernel32.dll": "6a7bd4daff9c0c4cc8b9478ecd9828c3f19be257a4a18db9559cae929197fffa",
"ntdll.dll": "60ef561645b50dce71ad0919dbbef3ad334a0489432cde718e75b73b558c7997",
"oleaut32.dll": "5ebb761b3f96b227006e79d6a92cf5d98ac5a5fd3c136d35dd05391bed867753",
"shell32.dll": "ef0fbd71a46fe4b35bff893ea65241cd08ea5231de704c106e39920c03698fe9",
"shlwapi.dll": "67f713491bc18614d6ea29b920e3c609cabf9eecf054990e030476a2e9f76a2c",
"user32.dll": "f2fc06e7e5016a15ad7f7f4ee250154b7d41d0a0706bf14927a3bbd4c42727dd",
"userenv.dll": "18bd1660ab7cacd90789f1bebe35bc6a5730b1e75e5582f53575473fae3db530",
"vcruntime140.dll": "3c56f4167e2b3d8e6338497731e6aae8cd7ec46bd6789f9423a8d9cf9a630310",
"winmm.dll": "2ebb728e5c510492df8fde4ec81b8a8554f2f33d1686b837d22c962b7d2fa9b7",
"ws2_32.dll": "e1266872f796f68b4be9c20640ca940a7a96e1ba98c741d472ae305fccf87ca9"
},
"supersedes": {
"time": "2026-10-13T10:43:07.5742747Z",
"osVersion": "10.0.26300",
"osBuild": "26300",
"source": "cf7199b52ecb9147e4670ae7874924ffc30a6a67",
"binarySha256": "ca51f91562a0569ee43855715856e1cfee74e8a834bd6206183a3175373fdb2d",
"resolved": 453,
"unresolved": 0,
"buildCommand": "cargo build --locked --target aarch64-pc-windows-msvc -p codewhale-cli --bin codewhale",
"profile": "debug, default features",
"runtimeAcceptance": "Only cf7199b52e ARM64 debug application acceptance; final packaged release and older OS unproven"
}
"buildCommand": "cargo build --locked --target aarch64-pc-windows-msvc -p codewhale-cli --bin codewhale",
"profile": "debug, default features",
"runtimeAcceptance": "Only cf7199b52e ARM64 debug application acceptance; final packaged release and older OS unproven"
}
}
},
@@ -855,9 +767,6 @@
"GetSecurityDescriptorControl",
"GetSecurityDescriptorDacl",
"GetSecurityInfo",
"GetSidIdentifierAuthority",
"GetSidSubAuthority",
"GetSidSubAuthorityCount",
"GetTokenInformation",
"ImpersonateAnonymousToken",
"InitializeAcl",
+10 -10
View File
@@ -58,8 +58,8 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
},
{
"path": "docs/PROVIDERS.md",
@@ -90,8 +90,8 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
},
{
"path": "docs/LEGACY_PATHS.md",
@@ -122,8 +122,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
}
]
},
@@ -265,8 +265,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
}
]
},
@@ -297,8 +297,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
}
]
},
+2 -2
View File
@@ -2,7 +2,7 @@
"format": "codewhale.features.v1",
"source": {
"path": "docs/features.toml",
"sha256": "4c4dfe17cea716328253c687d9d0665e1b24fc3b59d00f2a81cf1157dfea7072"
"sha256": "a63042dc25a658f626735cc40e7a7cf4c67eaa51bfbb037dc2820d083d10fe3a"
},
"features": [
{
@@ -1070,7 +1070,7 @@
{
"id": "live-thinking",
"name": "Live thinking",
"summary": "/verbose toggles live model thinking in the transcript.",
"summary": "Model thinking shows live in the transcript, then collapses under a Thought header; /verbose shows it in full.",
"status": "stable",
"surfaces": {
"tui": "stable"