* docs(automation): rename scheduled-tasks feature wording to Automations
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhJ8EiMXue6ADLmHfb7FL6
* docs: regenerate docs map and add Automations glossary entries
* docs(templates): follow renamed automations-vs-heartbeat anchor
* docs(automation): fix markdown formatting drift
* docs(automation): teach the canonical automations tool and sync the copied heartbeat default
Review follow-ups: normal instructions use the automations tool with cron as
an explicit compatibility alias; every verbatim copy of the default heartbeat
prompt matches the new shipped text from the strings PR.
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(harness): report copilot code-mode engagement on the attempt result
* test(copilot): prove code-mode engagement through the production tool bridge
* docs: describe the normalized codeModeEngaged value for native harnesses
* fix(agents): stop apply_patch from silently overwriting existing files
An "*** Add File:" hunk wrote its target unconditionally. When the path
already existed, apply_patch replaced the entire file, returned Success,
and listed the path under "added", so neither the model nor the UI got
any signal that existing content had been destroyed. The "*** Move to:"
destination of an update hunk had the same gap and reported the clobbered
path as merely modified.
The add and move-to branches now check the destination through the patch
file ops before writing and fail closed when it exists. Routing the check
through fileOps keeps it correct on all three backends (workspace-scoped
fs-safe root, raw fs, sandbox bridge). The check runs per hunk in patch
order, so deleting a path earlier in the same patch and recreating it
still works.
* fix(agents): make apply_patch destination creation atomic
The previous guard checked that an add or move-to destination was absent
and then wrote it. A competing writer could create the path in that gap,
after which the write still replaced it, so the no-clobber guarantee did
not hold under contention.
Destination creation now goes through a single exclusive create-if-absent
operation on every patch backend: Root.create for the workspace-scoped
default, an O_EXCL write for the raw filesystem, and a new pinned create
operation in the sandbox mutation helper that opens the target with
O_CREAT|O_EXCL and reports a reserved exit code when it already exists.
PatchFileOps drops its separate existence check.
Resolving the host ops behind an early return removes the repeated
workspaceOnly branch inside each operation and the optional-call dance
that let a missing root silently skip a write.
* fix(agents): complete atomic apply-patch creation
* fix(agents): preserve raced create replacements
* fix(agents): handle fs-safe patch collisions
* fix(agents): publish sandbox creates atomically
* test(agents): cover exclusive create provenance rollback
* fix(agents): use typed exclusive-create signal
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* docs: add local speech TTS setup
* docs: clarify local HTTP release requirement
* docs: require Speech Swift v0.0.23
* docs: use released speech-core Windows package
* docs: use current TTS config in local recipes
Co-authored-by: ivan-digital <root@ivan.digital>
---------
Co-authored-by: ivan-digital <root@ivan.digital>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Aggregators republish first-party models under a namespaced id and varying
case, so the shared-model check could not group novita/moonshotai/kimi-k3 with
moonshot/kimi-k3. Matching now ignores one leading namespace segment and case,
which catches that whole class automatically instead of needing an
upstreamModel marker per row.
Declares the tier on the 18 rows this surfaces across baseten, deepinfra, gmi,
novita, nvidia, and together. All are "capable", matching what the runtime
already applied for an absent flag, so behavior is unchanged.
* feat(plugins): catch code-mode drift between catalogs sharing one model
Adds a contract test that groups bundled catalog rows by shared upstream
model and requires every row in a group to declare compat.codeMode once any
sibling does. Rows sharing a model id group automatically; rows under
different ids opt in with the new manifest-only `upstreamModel` marker.
Moves the kimi catalog into its manifest so the scan can see it, and records
the tier reseller catalogs were silently missing as explicit "capable".
* docs: regenerate docs map for the shared-model code-mode section
navigate and act results that changed the page document now include the
loaded page's compact snapshot inline (honoring browser.snapshotDefaults
and standard caps/wrapping), removing the follow-up snapshot round-trip.
Snapshot execution moves to browser-tool.snapshot.ts to keep action
executors under the file size cap.
The moonshot provider already declares compat.codeMode: "preferred" for the
same model (kimi-k3), so tools.codeMode: "auto" engaged code mode on the API
surface but silently skipped the Kimi Code subscription surface (kimi/k3,
kimi/k3-256k). Live proof on kimi/k3: "auto" reported codeModeEngaged=false
while true reported codeModeEngaged=true with "code-mode: cataloged 50 tools
behind exec/wait" and 2 bridge tool calls.
K2.7 (kimi-for-coding*) stays unflagged, matching moonshot's kimi-k2.7-code.
* feat(agents): add per-run stats to embedded agent run meta
Adds codeModeEngaged, assistantTurns, bridgeCalls, and costUsd to
EmbeddedAgentRunMeta.agentMeta and mirrors them on the agent exec --json
envelope. Code-mode engagement is stamped from the tool-surface truth,
round trips accumulate across attempts beside usage, bridge counts come
from the run's tool-search catalog counters, and cost reuses the shared
model pricing helpers (cache tiers included, omitted without cost data).
* fix(agents): accumulate bridge call counts across run attempts
Attempt cleanup clears the per-attempt tool-search catalog, so retries and
fallbacks discarded earlier bridge counts. Fold each attempt's bridgeCalls
into the run accumulator beside assistantTurns and stamp the cumulative
totals into agentMeta, matching the documented per-run contract.
* feat(tools): per-model code-mode capability flags and auto master-switch tier
* feat(anthropic): flag claude-sonnet-4-6 as code-mode preferred
* revert(anthropic): drop claude-sonnet-4-6 from the code-mode preferred set
* test(anthropic): type the manifest compat field in the catalog contract test
* fix(anthropic): carry catalog compat onto hand-built forward-compat model rows
Reject recognizable POSIX and Windows shell source before QuickJS execution while preserving valid JavaScript, TypeScript, syntax errors, standard globals, and real hoisted bindings. Add real-worker regression coverage and adversarial cross-platform stress proof. Fixes#113069.
* fix(ui): keep stable chat rows in insertion order and only sort live tool/stream items by timestamp
* fix(ui): keep live rows within current turn
* fix(ui): keep current work above queued turns
* fix(ui): keep streamed replies above queued turns
* fix(ui): preserve reconnecting run order
* fix(ui): preserve causal terminal ordering
* fix(ui): bound replay rows to owning turns
* fix(ui): keep question summaries in owning turns
* fix(ui): scope question run ownership to session
* fix(ui): restore reconnecting chat run identity
* fix(ui): remove unused chatItemTimestamp import in chat-thread-build.ts
* fix(ui): correlate question summaries with agent runs
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
* chore(i18n): refresh native source baseline
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
* fix(ui): remove unused chatItemTimestamp export and split tool-stream test file
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(cron): bind agent turns to current sessions
* feat(commands): add loop chat command
* test(cron): cover session defaults and loop commands
* docs(cron): document current defaults and loops
* fix(commands): scope /loop status and stop by conversation name tag
* fix(commands): widen /loop conversation tag to 48 bits
* fix(commands): include disabled jobs in /loop status and stop
* docs(cron): regenerate docs map
* test(cron): split session-target default tests to satisfy max-lines
* feat(agents): rank Tool Search with BM25 over names, descriptions, and parameters
Ranking was case-insensitive substring matching with hand-tuned weights, which
failed in ways that made tools unreachable rather than merely mis-ordered:
- "scheduling" found nothing against a tool described "Schedule a recurring
task" — no stemming.
- "read" ranked spreadsheet_open, because "sp-read-sheet" contains it.
- A non-English query tokenized to zero terms, and the scorer returned 1 for
every entry, so the model received an arbitrary alphabetical slice of the
catalog presented as a ranked result.
Replace it with Okapi BM25 over a tokenizer that splits on Unicode word
boundaries, drops stopwords, and collapses light English inflection. Index
parameter names and descriptions too, which Codex (bm25 crate) and the Claude
API tool-search tools both do; a query like "repository" now reaches a tool
whose description never says it. Underscore-joined names index as both the whole
name and its parts.
A small query-expansion table bridges intent to description vocabulary ("look up
the price" -> search/web), which pure lexical overlap cannot do. It holds only
generic capability words, never plugin or vendor names.
Empty queries now score nothing instead of everything, and both `tool_search`
and the code-mode bridge tell the model to query in English, so the degenerate
case is steered away from rather than silently mishandled.
Untrusted schemas are still never traversed: parameters are indexed only for
`openclaw` entries, matching the boundary compactToolSearchCatalogEntry already
enforces by reporting MCP and client inputs as "unknown".
* fix(agents): undouble inflected consonants and state the real English contract
Autoreview caught two defects in the new stemmer and its documentation.
"running" stripped to "runn", which can never meet "run", so a tool named
task_runner described "Running tasks" became unreachable for the query "run" —
a regression the substring scorer did not have. English doubles the final
consonant before -ing/-ed/-er, so undo that, while keeping doubles that belong
to the root (call, process, off, buzz).
The English-only claim was also stronger than the code: the tokenizer keeps
Unicode letters, so a non-English query yields terms and can match. Rejecting
them would make a catalog that legitimately names a tool in another script
permanently unreachable, so the behavior stays and the wording now matches it —
catalogs are written in English, so other languages usually match nothing, and
the model is asked to query in English for that reason rather than because the
input is filtered. The previous test only proved an unrelated document scored
zero, so it is replaced by one that asserts each half directly.
* fix(agents): split camelCase, discount expansions, and stop trigger collisions
Three defects in the new tokenizer, all found by autoreview and all reproduced
before fixing.
`splitWords` lowercased before looking for boundaries, so `readFile` produced
only `readfil` and the natural query `read file` could not meet it. MCP catalogs
commonly use camelCase, and the old substring scorer matched those. Split case
transitions before lowercasing.
Expansion terms carried the same BM25 weight as words the caller typed, so
`weather` — which expands to search/web — could rank a general web tool above
the exact weather tool by matching two terms instead of one. Expansions are a
guess about how the catalog words a capability, so they now score at 0.35, and a
term the caller actually wrote keeps full weight even when an expansion repeats
it.
Trigger matching ran the document stemmer, which collapsed unrelated vocabulary:
`news` became `new`, so "open a new issue" silently acquired a web-search intent.
Triggers now normalize by singularization only, which leaves `news` intact, and
the table lists `reminder` explicitly rather than relying on the stemmer to
reach it.
* fix(agents): normalize -ies plurals and tier literal matches above expansions
`repositories` stemmed to `repositori` while `repository` stayed put, so the two
never met; `-ies` now normalizes back to `-y` in both the document stemmer and
the expansion triggers, which also lets `directories` and `memories` reach their
intended groups instead of stalling at `directorie`.
The 0.35 expansion discount is not sufficient on its own to keep a literal match
ranked first: BM25 sums per term, so a common literal like `weather` carries
little IDF while a short document collecting two rare expansions can outscore
it, and a small result limit then drops every tool matching the typed word.
Literal overlap is now reported per hit and ranked as a tier ahead of score, so
the discount orders within a tier rather than trying to carry the invariant.
Verified by running the ranker over the failing inputs rather than reasoning
about them; the first attempt at the trigger fix silently did not apply because
the formatter had reflowed the function, which the matrix run caught.
* fix(agents): keep non-plural -s words out of the plural stem rule
"news" stemmed to "new", which literal-matched every "Create a new ..." tool;
because literal overlap now outranks expansion-only hits, a search for news
returned creation tools instead of the web tool the query meant.
The collision is not unique to news — "status", "canvas", and "alias" are all
ordinary tool vocabulary here and all lose their meaning under the same rule, so
the exemption covers that class rather than the single reported word.
* fix(agents): restore the exact-name tier and keep `get` searchable
Two signals the old substring scorer had and BM25 alone does not.
It gave an exact name/id match +20, which flattening everything into one
document removed: querying a known tool name could rank a shorter entry that
merely mentions the word above the tool itself, and the result limit would then
drop the tool asked for. Exact name/id is now a sort tier ahead of literal
overlap.
`get` was in the stopword list, but it names real operations in a tool catalog.
Discarding it made `search("get")` empty and reduced "get issue" to "issue",
where a shorter delete_issue or update_issue entry can win on length
normalization. Capability verbs stay indexed.
* fix(agents): keep acronyms whole, both -ies readings, and stopword-named tools
* refactor(agents): keep the ranking types and entry-text helper module-local
Knip's hard-zero unused-export gate flagged WeightedTerm, RankedDocument,
LexicalIndex, and toolSearchEntryText: nothing outside their own modules
imported them. Narrowing the surface is what the repo asks for anyway.
The untrusted-schema test now drives ToolSearchRuntime.search instead of
calling toolSearchEntryText directly, which proves the boundary through the
real entry point. Verified it still bites: removing the source gate makes it
fail with "client properties must remain deferred".
* refactor(infra): move exec approvals into the shared SQLite state DB
Delete the file-runtime exec-approvals store (exec-approvals.json + .lock
sidecar machinery) on both runtimes and make the reserved
exec_approvals_config singleton row canonical. Doctor owns the one-time
import with claim/verify/receipt discipline; runtime fails closed with a
doctor instruction while un-migrated legacy state exists. The wire CAS
contract, socket semantics, and gateway auth-token derivations are
unchanged. Kills the #113929 lock-contention bug class structurally and
nets around -2.9k lines.
* fix(infra): green CI gates and retire file-era exec approvals tests
Break the migration-type import cycle with a leaf contract, regenerate the
plugin-SDK API and native i18n baselines for the intentional surface change,
drop unused exports, and replace the macOS file-era approvals test suite with
SQLite-backed behavior coverage per the obsolete-internals test policy.
* chore: green max-lines ratchet, native i18n baseline, and unused-export scan