mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
86b4ec357a28fceac896c556da795e7a873f69ff
3873 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
86b4ec357a | chore(release): cut 1.2.0-rc.3 ironclaw-v1.2.0-rc.3 | ||
|
|
bf495dddf1 |
fix(docker): install curl so orchestrator healthchecks can run (#7555)
* fix(docker): install curl so orchestrator healthchecks can run Forward-port of #7303 from release/1.1.0-rc.1 onto this release branch. The runtime stage installs only ca-certificates, postgresql-client and sqlite3 on top of debian:bookworm-slim, which ships no HTTP client. Hosted orchestrators probe the container with an in-container HTTP healthcheck -- the CrabShack worker template runs test: ["CMD-SHELL", "curl -fsS http://localhost:3000/ || exit 1"] so the probe could never execute. The container is never marked healthy, the deploy times out, and the instance is moved to `error` -- while the listener is serving 200s the entire time. The regression test is scoped to the runtime stage rather than the whole file: this branch's `railway_cli` build stage already installs curl for its own download, so the upstream whole-file substring assertion would pass here while the shipped image still had no HTTP client. Verified red before the Dockerfile change, green after. Also ports the other half of #7303's planner fix: `_root_test_partitions` globbed only `tests/reborn_*.rs`, so `tests/dockerfile_runtime_home.rs` (and `trace_format`, `trace_llm_tests`, the `e2e_trace_runtime_policy_*` pair) hit the planner's fail-closed arm -- touching this test in a PR failed `Detect Reborn test scope`. The inventory now covers every root test target cargo auto-discovers. This branch's separate decision to keep `Dockerfile`/`.dockerignore` as static control paths (#7084) is left intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ci): align the root-partition runner with the planner inventory Addresses both review findings on #7555. Codex P1 — the planner and `scripts/ci/run-reborn-root-partition.sh` both index a sorted list of root tests and take `index % partitions`, so the two inventories have to match exactly. Widening only the planner shifted every assignment: the planner scheduled `dockerfile_runtime_home` into partition 0 while the runner, still globbing `reborn_*.rs`, executed a different set and never ran it. The job passed anyway. The runner now discovers the same `tests/*.rs` set cargo auto-discovers. The new regression test drives the real script with a stubbed `cargo` and `timeout` and compares what it executes, per partition, against `_root_test_partitions()` — behavior, not script text. Verified it fails when the runner's glob is narrowed back. IronLoop medium — `Dockerfile` and `.dockerignore` were static control paths (#7084) on the premise that no Reborn Rust lane reads the image definition. That premise is false: `tests/dockerfile_runtime_home.rs` asserts the runtime packages, the entrypoint's behavior, and the seed configs. `platform-and-compat.yml` builds the image but never runs those assertions, so a Dockerfile-only PR that dropped `curl` would have passed exactly as 1.1.0 did. Those paths, plus the two seed configs the same test reads, now schedule that test's partition. The routing set is enumerated rather than globbed as `docker/**`: `config.production.toml` and `process-sandbox-entrypoint.sh` have no owning lane and must keep failing closed, which `test_sibling_container_inputs_still_require_a_decision` pins. `docker/reborn/entrypoint.sh` keeps its earlier static-control decision — it is matched before this branch is reached. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9e27b5b18a | chore(release): cut 1.2.0-rc.2 ironclaw-v1.2.0-rc.2 | ||
|
|
a51fc39003 | fix(windows): keep ACL output out of CLI JSON | ||
|
|
2da27c5564 | fix(release): preserve Windows identity in smoke environment | ||
|
|
b75bf24874 | fix(filesystem): publish absent writes atomically on Windows | ||
|
|
1b2f77fdbc | fix(release): isolate smoke workspace on Windows | ||
|
|
85f72046f8 |
fix: enable Railway shell for hosted volume profile (#7514)
* docs: design Railway shell profile override * docs: keep Railway guidance with sandbox owner * docs: plan Railway shell profile override * fix(cli): enable Railway shell for hosted volume profile * fix(runtime): describe Railway sandbox shell * fix: move Railway shell test seams * test: cover Railway shell override rejection * chore: exclude internal Railway planning docs * fix(ci): restore Reborn test stack headroom * fix(ci): rebaseline composition budgetironclaw-v1.2.0-rc.1 |
||
|
|
c163f4d334 | fix(threads): restore listability for unprojected thread index rows (#7507) | ||
|
|
bdf8aef022 | fix(migration): forward-port legacy state upgrade to 1.2 | ||
|
|
e12bbae4d2 | fix(migration): preserve legacy state on 1.2 startup | ||
|
|
59eae25748 | ci(release): gate 1.2 on stateful upgrades | ||
|
|
5966a89ca2 | chore(release): cut 1.2.0-rc.1 | ||
|
|
499394df4a |
fix(llm): prefer the authenticated NEAR AI session for default probes (#7492)
* fix(llm): reuse NEAR AI session for model probes * fix(llm): prefer runtime session for default probes * fix(llm): allow public NEAR AI model discovery * fix(llm): scope NEAR AI model auth by endpoint |
||
|
|
2b87cf53df |
Install the packages the catalog already publishes (#7442)
* Install the packages the catalog already publishes Skills publish a files list for the scripts and assets they ship, but the catalog entry never deserialized it and the install path passed an empty bundle, so only SKILL.md landed. Files now install alongside it, digest-verified through the same download path and bounded by the limits ironclaw_skills already enforces, and they feed the skill artifact digest while a skill with no files keeps the digest it has today. Tools using HTTP Basic could not publish an extension manifest, so they listed and failed at install; the new basic target carries only the username and the host owns the join and the base64, with a colon or control character rejected at the host boundary, at the channel descriptor, and again at injection. * fix(ironhub): address package install review findings (#7076) * fix(ironhub): address review round — header-collision rejection, constant-derived caps, egress contract tests (#7076) * refactor(skills): drop unused validate_install_bundle_relative_path wrapper (#7076) * fix(runtime): harden derived credential redaction (#7076) * fix(skills): reject bundle path collisions at domain boundary --------- Co-authored-by: neo-sky <brandon.m.henderson93@gmail.com> |
||
|
|
81045020dc | test(memory): cover bounded search edge cases (#7494) | ||
|
|
2938f24e07 |
fix(memory): bound native search result snippets (#7436)
* fix(memory): bound native search result snippets * fix(memory): preserve bounded exact-match excerpts * fix(memory): bound conventional search output at shared boundary * docs(memory): add Mintlify validation to output-bounding plans (#7436) |
||
|
|
ce67ddaff6 | test(ci): restore main coverage gates (#7493) | ||
|
|
6f1ae709d5 |
feat(tool-search): complete fair discovery and benchmark arms (#7410)
* test(tool-search): add large-catalog baseline * feat(tool-search): return and use bounded signatures * feat(tool-search): add fair discovery benchmark arms * test(tool-discovery): add live benchmark harness * feat(tool-discovery): default to namespace summaries * ci(tool-discovery): classify benchmark harness * feat(tool-discovery): use semantic namespaces * fix(tool-search): harden discovery benchmark and mode wiring * docs(tool-search): record corrected benchmark verdict * fix(tool-search): address review feedback |
||
|
|
ed8ac2743e |
chore(agents): refresh codebase knowledge graph (#7479)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> |
||
|
|
419807c919 |
feat(stress): add durable memory parity matrix (#7426)
* feat(stress): add durable memory parity matrix * refactor(stress): split native search behavior * fix(stress): fail closed on delayed retries and parity * fix(stress): keep delayed retries in-flight * fix(stress): address final review edge cases |
||
|
|
2d64363101 |
fix(qa): stop the agent asserting unverified state — automation status, per-caller extension auth, recalled memory (#7246, #7247, #7294) (#7474)
* fix(assistant): ground automation-status claims in actual state checks (#7246) The agent confidently reported a BTC-news-digest automation as running and delivering to Telegram while the Automations page showed "No automations yet" — status fabricated from conversation history instead of checked. The read path already exists: builtin.trigger_list is model-callable (PermissionMode::Allow), core-tier always-advertised, granted in the interactive policy, and deliberately retained for scheduled fires. The failure is grounding: its description said only "List scheduled triggers owned by the current caller scope" — no bridge from the user vocabulary ("automation", the Automations page; "routine") to the trigger capability, and no instruction to consult it before asserting status. Mechanism (mirrors the proven builtin__outbound_delivery_targets_list grounding pattern — description-level positive rule tied to the exact assertion the model must not fabricate): - trigger_management.rs: TRIGGER_LIST_DESCRIPTION now names the surface ("the automations shown on the Automations page"), declares the listing the authoritative current state, instructs calling it before answering which routines/automations exist or saying one is running, paused, already set up, delivering, or missing, forbids reporting status from conversation history or memory, and grounds the empty result as "the caller has no routines". - schemas.rs: trigger_list.input.v1.json gains a root description carrying the same authoritative-state framing into the model-visible schema. Regression test (red on the old description, green now): builtin_trigger_list_surface_grounds_automation_status_claims in first_party_builtin_tools.rs, driven through the production visible_capabilities surface assembly — not the constant — asserting the vocabulary bridge, the check-before-assert rule, the memory ban, the empty-state grounding, and the schema root description. Validated: ironclaw_host_runtime suite + clippy -D warnings green; ironclaw_loop_host, ironclaw_turn_runner, ironclaw_composition green; reborn_group_triggers integration group green, proving the new description survives the prompt-build descriptor validation chain (VerifiedCatalog surface, 4096-byte cap) under production wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory): frame recalled memory as recollection, not live state; pin cross-thread transcript containment (#7294) Investigation verdict: the reported "agent remembers a Telegram routine from another scope or thread" is NOT a retrieval leak. Every isolation seam holds: providers scope-filter (native retains only scope-equal results and excludes threads/ scratch from the long-term lane; mem0 partitions by the composed user namespace), and the host re-applies the ExpectedScope drop filter in ironclaw_host_runtime::memory_context. Durable memory crossing conversations for the same user is the contract's design. The defect is presentation: recalled snippets entered the prompt as bare "Untrusted memory content: ..." system messages, so the model read a recollection ("user asked for a BTC news routine") as verified current state ("you already have this set up"). Fix, at the prompt-assembly seam (InstructionBundleBuilder): whenever at least one memory snippet is admitted, the memory section now opens with a recall-framing system message (prompts/memory_recall_framing.md, include_str!) telling the model these are recollections from earlier turns/conversations that describe past state and must be verified with a tool before being asserted as currently configured. No rows are filtered, deleted, or re-scoped. Regression coverage making the scoping contract explicit (all green today; sabotage-verified to arm): - shared conformance suite (ironclaw_memory::test_support, runs for native + mem0): a recorded conversation transcript is invisible to another thread's short-term AND long-term lanes and to thread-less (trigger-shaped) long-term retrieval; durable memory written during one conversation stays retrievable from another (cross-thread recall is by design, not a leak). - instruction-bundle unit tests: framing precedes the snippets; absent when no snippets are admitted. - scenario_proactive_prompt_recall_libsql (production wiring, libSQL): the writer conversation's after-turn transcript (proven recorded via its own short-term lane) never surfaces in the reader conversation's prompt, and recalled durable memory arrives behind the framing. Consumer contract tests in ironclaw_turns / ironclaw_loop_host updated for the new memory-section header; tests/CLAUDE.md coverage row updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(assistant): communication context reports per-caller credential truth (#7247) The model-facing communication runtime context asserted connection state it never verified: RuntimeCommunicationContextProvider hard-coded `authenticated: true` for every host-Active channel-surface extension, and carried no per-caller credential truth at all for credentialed tools-only extensions (the GitHub repro — the model saw 49 github.* tools plus "installed and active" catalog state and told the user no further connection was required, right before the next call raised Authentication required). Mechanism: - ironclaw_assistant: the provider now takes the existing readiness ports — ExtensionCredentialSetupService (the same scope-gated credential_status the extensions card and the runtime auth gate resolve through) and ironclaw_auth::ChannelConnectionService — and classifies each Active installed extension with a new shared caller_extension_auth verdict in reborn_services/extensions.rs. The extensions card's channel-unconnected computation is extracted into caller_channel_connection and reused by both paths, so the card and the prompt can never diverge on "connected for this caller". - Channels: `authenticated` is now the per-caller truth. A channel needing a personal OAuth/pairing binding with no proof for this caller renders "unauthenticated"; a genuinely paired/connected channel (or one requiring no personal binding, e.g. admin-managed) still renders "authenticated" — the #6478 truthful positive is preserved and pinned. - ironclaw_loop_contracts: CommunicationRuntimeContext gains PendingExtensionAuthState; a bounded, sanitized render line names installed extensions the calling user has NOT authenticated and forbids the "already connected" claim. Unknown/empty render nothing. - Fail-closed: when a needed verdict is unknowable (ports unwired, lookup failed, budget expired) both states degrade to Unknown — the slice claims nothing in either direction. Tools stay visible; the auth gate still owns enforcement at dispatch. - ironclaw_composition: wires ProductAuthExtensionCredentialSetup and the generic channel-connection facade (assembly extracted into build_generic_channel_connection_facade, shared with the product surface) into the provider. - Architecture: ironclaw_loop_contracts size ceiling raised 13112 -> 13172 for the declaration/render vocabulary (reason recorded at the ceiling). Regression tests: communication_context::tests {active_channel_requiring_connection_is_not_claimed_authenticated_without_proof, credentialed_tool_extension_without_caller_credential_is_pending_auth, credentialed_tool_extension_with_expired_credential_is_pending_auth, oauth_channel_not_connected_by_caller_reads_unauthenticated, oauth_channel_connected_by_caller_reads_authenticated, credentialed_extension_without_credential_port_degrades_to_unknown} plus render pins in ironclaw_loop_contracts runtime_context/tests.rs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(arch): re-measure loop_contracts size ceiling for the #7247+#7294 union Each fix measured the ceiling alone against main (#7247 raised it to 13,172 for its additions); batching both crate-growing commits onto one branch requires the union measurement, 13,306 — read from the gate's own failure message, pinned exactly per the #7147 no-untracked-slack lesson. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(ci): raise composition arc_dyn ceiling 814 -> 816 for the #7247 context-provider ports Two genuine dyn seams (ExtensionCredentialSetupService + the channel- connection facade) wired into communication-context assembly; observed 831 sites, effective ceiling re-pinned to exactly that — no slack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: apply the 8 CodeRabbit findings (#7474) - memory_recall_framing: tool results are authoritative for tool-queryable current state; conversation text no longer outranks a fresh tool result. - trigger_list: limit 0 rejected (schema minimum: 1) so an empty result is always proof of absence; regression tests at the handler and the boundary suite (the old boundary test pinned the buggy empty-success). - runtime_context: worst-case SafeSummary fixture now saturates the pending-auth arm (fits within 4 KiB); byte-budget truncation test added. - agent_loop_host_contract: recall-framing filter keyed by content_ref. - communication_context: per-extension credential lookups run under bounded concurrency (8, matching the extensions card) instead of serially inside the 500 ms budget; account-backed unconnected test (expired / refresh-failed rows read unauthenticated). - composition-budget: arc_dyn_observed re-measured to 831 with the rationale corrected (observed == effective ceiling exactly). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ad7ecd1f69 |
docs(internal): doc-truth pipeline design record (doc-truth PR 5/5) (#7381)
* docs(internal): record the doc-truth pipeline design (issue #7317) The as-built design for keeping the public Mintlify docs in sync with released binaries: the problem evidence, the decisions of record (single doc tree deploying from a docs-live branch; deterministic gates only; human-curated changelog), the three enforcement layers (PR-time static gates, release-time cut/publish automation, human checklist), a surface-to-gate enforcement table, the deferred follow-ups (release-time live probes on the packaged binary, an openwiki-style non-blocking LLM fix-PR generator, flag-level CLI coverage, link integrity, zh freshness, contract-doc pinned-test re-verification), and the risk register. Part of #7317 (doc-truth pipeline, PR 5 of 5). Companions: #7375 #7376 #7378 #7379. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(internal): record review hardening in the doc-truth design Post-implementation review found five gaps; this records their resolutions where the design doc already claims the guarantees: planner routing so doc-fact tests run on docs-only PRs, the changelog entry landing on main before the Monday cut, a newest-stable-tag guard on publish-docs-live, the force-push allowance in the docs-live branch-protection shape, and the docs-hotfix repoint recipe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4fb4208693 |
fix(loop-host): dedup consumed steering replays (#7336)
* fix(loop-host): dedup consumed steering replays * test(loop-host): assert consumed replay bound |
||
|
|
0f646ed0de |
feat(channels): rich working indicator — reactions, failure states, progress nudges (#7446)
* feat(channels): vary the "working" notice per run
Replace the single "Ironclaw is thinking..." working indicator with a small
rotation of warm notices ("On it!", "Let me look into that…", …), picked
deterministically per run (by run id) so a shared channel with several
concurrent runs does not fill with identical lines while one run keeps a
single voice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ
* test(channels): assert working-indicator structure, not its varied copy
The varied per-run working notice broke three suites that pinned the exact
"Ironclaw is thinking..." literal. The literal is volatile content now;
pin the copy in one place (the prompts unit test) and assert *structure*
everywhere else — a distinct working indicator is posted, then retracted,
then the reply.
- run_delivery_contract.rs: a non-empty working notice distinct from the
final reply precedes it (3 sites).
- e2e_tests.rs (extension_host): the running turn posts a non-empty
working indicator (2 sites) + generalized a stale doc comment.
- extension_delivery.rs (root integration): select the working sendMessage
by the call, not the words — the model is paused so it is the only
/sendMessage before release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ
* feat(channels): rich working indicator — reactions, failure states, progress nudges
Batches the full shared-channel working-indicator UX onto the varied-copy PR:
- Reactions on the triggering message track the run: 👀 working → ✅ done,
→ ⚠️ when parked on an approval/auth prompt (and back to 👀 on resume),
→ ❌ on failure/timeout. New OutboundPart::React + neutral RunReaction /
ReactionAction in ironclaw_extension_contracts; Slack reactions.add/remove,
Telegram setMessageReaction (allowlist-mapped), egress updated in both
manifests; web-push reports unsupported; DeliveryIntent::Reaction (notice-class).
- Failure/timeout states now reach the channel: a terminal failed/cancelled run
retracts the stuck "thinking" indicator and posts a brief, diagnostic-free
failure notice (source-routed through the same reliable path) instead of going
silent; timeouts retract the indicator too.
- Progress nudges: a long run refreshes its indicator in place with escalating
"still working" copy — first at 30s, then each gap doubling — so it never
looks stalled.
The source message's vendor ref already rides ExternalConversationRef, so no new
ingress plumbing was needed. The reaction lifecycle is a small state machine
(set_source_reaction) in the delivery observer.
Also addresses prior review: auth-flow assertions now require the working
indicator distinct from the auth prompt too (CodeRabbit); reaction / failure /
needs-input / nudge lifecycles are covered through observe_ack at the adapter
seam (caller-level coverage).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ
* fix(channels): keep neutral code vendor-agnostic in reaction comments
The reaction doc/comments named Telegram and Slack in the neutral channel
contract and the delivery observer, tripping the
reborn_generic_code_names_no_concrete_extension architecture gate. Reword to
generic phrasing (vendor reaction APIs / the originating channel).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ
* test(channels): identify the working indicator structurally in the pairing race test
The unbound-Telegram pairing race test (merged from main via #7131) matched the
working indicator by the substring "is thinking", which the varied copy
removed. Identify it structurally instead — the race-chat message anchored to
618 that is not the final reply — so it survives the copy change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ
* test(channels): admit the new reaction egress paths in the manifest allowlist
The reaction feature added reactions.add/remove (Slack) and setMessageReaction
(Telegram) to the channel egress allowlists; the first-party manifest parity
tests pin those lists exactly (a security boundary — a new egress path must be
a reviewed change), so add the new paths to the expected sets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
7b8926cada |
fix(channels): shared channels invoke the bot only on explicit mention (#7445)
In a shared channel (Slack channel/thread, Telegram group) the bot was triggered by every thread reply, not just @mentions: the Slack adapter classifies any threaded `message` event as `ReplyToBot` and Telegram does the same for a reply to the bot, and both spawned a run. With ephemeral-per-ping (#7397) each event is its own run hydrated from thread history, so bystander chatter re-ran the same instruction once per follow-up message — observed as the bot sending the same DM three times and over-replying. Gate run-spawning on an explicit mention: in `build_channel_envelope`, a channel message whose trigger is `ReplyToBot` and which carries no reserved classification (gate resolution / command) is acked durably and dropped as a `NoOp`, never spawning a run. Explicit mentions (`BotMention`, their own event) and direct messages (`DirectChat`) still run; approve/deny replies and slash commands are classified upstream (`Some(_)`) and are unaffected, so gate resolution keeps working without a re-mention. Host-level and trigger-based, so it fixes Slack and Telegram together. Regression test: shared_channel_plain_thread_reply_is_dropped_but_mentions_dms_and_gates_run. Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
38f8de4486 |
fix(run_delivery): deliver triggered run failures to the creator (#6896) (#7131)
* fix(run_delivery): deliver triggered run failures to the creator (#6896)
Scheduled/triggered runs that ended in Failed, Cancelled, or
RecoveryRequired produced no user-visible notification: the triggered
delivery driver minted notifications only for Completed /
BlockedApproval / BlockedAuth and recorded every other terminal status
as Skipped. A run that timed out before reaching an actionable state
only logged a warn and recorded Failed, leaving the creator in silence.
Delivery:
- triggered_notification_for_state now mints a FinalReplyReady
notification for Failed and RecoveryRequired using the existing
per-category failure summaries (reborn_failure_summary_for_category)
over state.failure.category(), with a generic fallback when no
category is present.
- Cancelled mints the same notification, preferring a failure-category
summary when one is present and falling back to a fixed cancellation
notice otherwise.
- The RunWaitTimedOut branch with no prior blocked marker now delivers
the timeout notice as a terminal reply instead of recording Failed.
- The wildcard arm is replaced with explicit non-actionable statuses
(Queued, Running, CancelRequested, BlockedResource,
BlockedDependentRun, BlockedExternalTool) so a future status fails to
compile rather than silently skipping.
Observer:
- TriggerFireSettlementObserver gains on_failed_fire_settled as a
default no-op method, plus a TriggerFailedFireSettlement event
carrying tenant/trigger/fire-slot/run-id/history-status. Noop and
existing implementors keep compiling.
- The active-cleanup sweep fires on_failed_fire_settled when
clear_active_fire succeeds with TriggerRunHistoryStatus::Error, so
post-accept failures are observable for automation health. Ok,
Running, and already-cleared fires do not fire the hook.
Tests:
- run_delivery_contract: Failed+model_error, Failed without category,
Cancelled, and timeout-before-actionable all assert a Delivered
outcome with the expected notice text and footer.
- worker tests: a terminal-Error active fire fires exactly one
on_failed_fire_settled; a terminal-Ok active fire fires none.
The larger retry/redrive budget for failed post-accept fires
(retry_disposition has zero production callers) is intentionally left
for a follow-up; it is out of scope for this surgical delivery fix.
* style: cargo fmt the #6896 delivery fix
* fix(triggers): address terminal delivery review feedback
* fix(assistant): drop unused UserId import after merge
* fix(run_delivery): address multi-agent review findings
- Extract shared terminal-notice helpers (final_reply_notice,
outcome_for_delivery_failure, deliver_terminal_notice) so the
timeout, OAuth-backstop, and generic failure arms share one notice
shape and outcome taxonomy instead of a third hand-rolled copy.
- Add a bounded race-grace window after the wait backstop: a run that
crosses into a terminal state during the final wait (cancellation in
flight, failure landing after the last poll) now delivers the correct
terminal notice instead of the timeout copy.
- Cancelled runs always deliver the fixed cancellation notice; the
failure-category branch was unreachable in production and would have
mislabeled a host/operator cancel as a failure.
- Update the stale invariant doc, the five-output surface contract
count, and the exhaustiveness-only comment on the non-actionable arm.
- Document the cheap/non-blocking contract on
TriggerFireSettlementObserver (the worker awaits it inline in the
poller sweep) and note it at the active-cleanup call site.
- Add contract coverage for the timeout arm's delivery-failure outcome
(Failed) and a regression test proving the race-grace path delivers
the cancellation notice; the cancelled-with-category test now asserts
the cancellation notice wins.
* fix(run_delivery): address review comments and restore CI gates
Review fixes (CodeRabbit on 01e887f/f8af109):
- Grace loop fails loud: log the bound TurnError on state-poll failure and
the RunDeliveryError on terminal-notice build failure before falling back
to the timeout copy, with silent-ok markers on both intentional fallbacks.
- Hoist TriggeredReplyTargetAuthority, CodecChannelTargetResolver, and
TriggeredNotificationContext to one construction before the watcher loop;
the race-grace arm, timeout arm, and loop body now share it.
- Collapse the duplicated failure-summary expression into one closure and
name TurnStatus::Failed explicitly so future statuses are compiler-visible.
- Drop the stale "Only three states" count from the surface-contract doc.
- Test fixture: encode the late-terminal flip as one Option<(usize,
ScriptedRunState)> field instead of two correlated Options with an expect.
- Terminal-crossing test: document why flip_after=30 deterministically
outruns the wait poll budget and assert the grace loop issues no
cancellation (cancel_calls == 0).
CI:
- composition-budget: re-seed loc_ceiling 40432 -> 40593 (measured on the
merged tree; the #7131 settlement observer adds +161 governed LOC of
wiring) and move the arch-test record with it.
- trigger_poller: use the colon-form tracing target required by #7146.
* ci: re-trigger pull_request workflows for
|
||
|
|
0f771d4915 |
feat(channels): presence admission + ephemeral per-ping threads; delete owner-vs-actor (#7397)
Reworks how the bot handles being in a Slack channel or Telegram group, replacing the shared-thread model with ephemeral-per-ping threads, and removes the owner-vs-actor concept from run scope. - Presence is the admission (no per-extension allowlist). Each ping mints a fresh pinger-owned thread, event-idempotent (redeliveries replay, no orphan threads), pre-loaded with channel/thread history hydration. Reply placement via `can_reply_in_threads`; authority-bearing prompts (approval/auth) go privately to the pinger; unpaired users get an addressed-only nudge. DMs keep their persistent per-user thread. - Per-event reply targets: each run replies via its own reply target, so impersonation is blocked, authority-bearing payloads resolve only for the pinger, and a stale/cross-event reply ref is denied. - With owner == actor everywhere, the owner-vs-actor apparatus is deleted: `ResolvedBinding.owner_user_id`, the run_delivery two-scope split, the scattered actor-first keying patches, and the dead `ProcessGateOwnerMatch`. The run-user resolution machinery (the actor-first ladder, `TurnThreadOwner`, `TurnActor`) is kept and reframed — WebChat (=caller), triggers (=creator), subagents (=inherited), and system runs (=SYSTEM) resolve their user differently; the `Ownerless -> SYSTEM` host path is preserved. - Untrusted channel-context sanitizer strips bidi/zero-width `Cf` characters, not just `Cc` controls. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b230c326e9 |
fix(release): resolve candidates by package identity (#7433)
* fix(release): resolve candidates by package identity * fix(release): address bot reviews — validate workspace manifests (#7433) |
||
|
|
c5177861c1 |
fix(host-runtime): classify HTTP error responses as failures (#7342)
* fix(host-runtime): classify HTTP error responses as failures * fix(host-runtime): address review — bound HTTP error diagnostics and add status regression tests (#7330) * docs(host-runtime): correct saved-body retrieval sequence for failed HTTP calls (#7330) * fix(host-runtime): converge failure-diagnostic trim, scrub controls, cover header/base64 branches - share one budget-trim engine (fit_output_to_budget) between the success path and the failure diagnostic; the diagnostic trim now converges in a strict-progress loop instead of an 8-iteration cap, and the headers branch stays reachable when the inline body is empty or absent, so header-heavy 4xx/5xx diagnostics keep status/auth_hint/truncation instead of collapsing to the fallback verdict - scrub DEL/C1 control bytes (U+007F..U+009F) from the serialized diagnostic: serde_json does not escape them, and ModelDiagnostic validation would otherwise replace the whole diagnostic with the fixed fallback sentence at the resolution boundary - move the shaped output by value instead of cloning it per error response - add unit coverage for the empty-body/header trim, base64 alignment on the failure path, and control-char scrubbing; wire the redirect regression test into the architecture-runtime gate * fix(host-runtime): shape errors at diagnostic budget, attach wall clock, pin envelope size - shape 4xx/5xx responses at the 4 KiB diagnostic budget instead of the success inline limit so the discarded success-budget trim pass (and its serializations) no longer runs on the failure path - attach dispatch wall_clock_ms to the OperationFailed usage, matching the sibling first-party dispatches' failure-path accounting - pin the truncation envelope below its reserved budget with a unit test - correct the fit_output_to_budget doc to describe the incremental re-measure loop rather than a fixed three-serialization bound * fix(host-runtime): pin error-status predicate, failure usage, and fallback shape - single is_error_status predicate shared by the dispatch shape-limit selection and classify_status, so the 400..=599 boundary cannot drift - pin failure usage accounting: classify_status unit test asserts egress bytes + wall_clock_ms on the OperationFailed outcome; the 403 integration test asserts egress bytes reach the governor for failed calls - pin the fallback diagnostic payload shape for non-object output and note why the serde-failure branch is unreachable by construction * fix(host-runtime): migrate stale 5xx-success integration test to failed-outcome contract - reborn_integration_http_matcher asserted the pre-change contract that a scripted HTTP 500 surfaces as a successful tool result; the new classification makes it a recoverable OperationFailed outcome, so the test now asserts ToolErrorClass::Failed with the operation_failed kind (run still completes; docstring updated to the contract doc) - pin the post-fit fallback safety valve with an oversized untrimmable key test; correct the governor-accounting comment; dedupe the 400 boundary rationale onto is_error_status * docs(host-runtime): sync matcher guide to failed-outcome contract, document fence interaction - tests/integration/CLAUDE.md .with_status entry now states 4xx/5xx classify as a Failed tool outcome (operation_failed) with sanitized diagnostic context; other statuses remain Completed results - host-runtime contract doc records the loop-host injection-fence interaction: verdict semantics never depend on the fenced diagnostic surviving the observation bound (OperationFailed + safe summary always reach the model) - document the fit_output_to_budget convergence bound (<= 3 passes) * docs(host-runtime): correct serialize_diagnostic guard rationale serde_json serializes every Value string without revalidating UTF-8 (probed: even an unsafe lone-surrogate string serializes Ok), so the serde-failure arm is a pure defensive guard for future Value shapes, not a reachable lone-surrogate path. Correct the doc comment and the fallback test note to state the empirical fact. * fix(host-runtime): keep shape-stage truncation flags in failure-diagnostic envelope The re-inserted truncation envelope carried only the diagnostic-budget trim state, so a 4xx/5xx response whose shape stage had already marked headers or body as truncated (e.g. more than 32 headers) could end up with headers_truncated:true beside an envelope claiming headers:false. OR the surviving keys into the envelope and pin with a regression test; boundary doc now states the full complement (outside 400..=599 stays inspectable, including out-of-spec 600+). * docs(host-runtime): sync support-module doc to failed-outcome contract, fix trim rationale - tests/integration/support/http_matcher.rs module doc still claimed .with_status non-2xx stays Completed; now documents 4xx/5xx as a model-visible Failed operation_failed outcome - bounded_failure_diagnostic doc: head-keeping truncation cuts only the last-sorted keys (status, truncation envelope); auth_hint sorts first and survives * fix(host-runtime): address coderabbitai/ironloopai review — egress cap, fence headroom, saved-body fallback (#7342) - shape failed responses at the caller's response_body_limit again so egress-truncation accounting (body_was_truncated_by_egress) stays correct; the diagnostic display budget is applied separately. Pinned by builtin_http_error_diagnostic_preserves_egress_truncation_flag. - reserve MODEL_DIAGNOSTIC_FENCE_HEADROOM_BYTES so a diagnostic wrapped in the loop-host external-content fence still fits the observation budget; pinned by failure_diagnostic_stays_within_budget_when_fenced. - retain compact saved_body evidence (bounded path prefix + bytes_written) in the fallback verdict instead of dropping the save destination; pinned by failure_diagnostic_fallback_retains_saved_body_evidence. - rename failure_diagnostic_falls_back_on_unserializable_output to the non-object contract it actually exercises (Value::Null serializes). - host-runtime contract: correct the save-mode body-retention wording and document retry_after_ms semantics (None does not permit immediate retry). |
||
|
|
cf3226b3f7 |
feat(web-push): browser push notifications + PWA — the web app as a first-party notification channel (#7398)
* feat(web-push): browser push notifications + PWA — the web app as a first-party notification channel The web app becomes a real, selectable notification route for automations, at parity with Slack/Telegram: a bundled first-party channel extension (web-push) delivers W3C Web Push (RFC 8030/8291/8292) to the user's enrolled browsers through the existing catalog → notification-channel set → notifier → delivery coordinator → channel adapter → policy-enforced egress chain, with zero new routing machinery and the two-lane delivery contract untouched. The WebUI ships as an installable PWA (root-scope service worker with push + notification-click deep links; manifest and icons already existed), and the automations page's notification-channels panel replaces the always-on "web app" placeholder with a real toggleable row plus a per-browser enroll/disable flow. New crates: ironclaw_web_push (domain: subscription records + CAS store, RFC 8291 aes128gcm encryption pinned to the RFC's Appendix A vector, VAPID key-material generation, transport-free request planning, channel identity grammar, late-bound runtime slot) and ironclaw_web_push_extension (channel package: manifest with vapid_authorization egress injection, adapter with 404/410 pruning and honest Sent-without-ref evidence, personal-DM codec, owner-scoped catalog provider). One generic host addition: the RuntimeCredentialTarget::VapidAuthorization egress injection kind — the host signs the RFC 8292 ES256 JWT at the existing credential chokepoint with the audience derived from the request's own push-service origin; adapters never see key bytes. VAPID material is auto-generated and seeded at composition boot. Enrollment is an authenticated product surface (three new /api/webchat/v2/web-push routes) with descriptors declared in ironclaw_product_contracts::web_push per the transport/product boundary, and endpoints validate against the manifest-declared push-service hosts. Also fixes a boot bug the new integration tests exposed: DeploymentChannelBinding rejected outbound-only channels, which would have failed the runtime build at serve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web-push): address review — redact VAPID secret, byte-budget payload, account-scoped enrollment, sanitized store errors Triage of the CodeRabbit + IronLoop review on #7398: - Secret exposure (Critical): hand-written redacting `Debug` on `VapidCredentialMaterialV1` and `GeneratedVapidKeyMaterial` so the ES256 private key can never reach a log, panic, or `{:?}`. - VAPID material (High): composition now ensures-then-reads-back the canonical stored keypair so multi-replica cold-start converges on one signing key and the advertised applicationServerKey matches it; material shape is validated at boot and again before egress signing. - Payload budget (Medium): notification body is trimmed by serialized-JSON bytes, not character count, so multi-byte content can't blow the single-record push budget (+ regression test). - Error hygiene (Medium): `WebPushError::Store` is a fixed sanitized category; the backend cause is logged server-side, never rendered into the boundary error. - Account-scoped enrollment (Medium): status projects an `endpoint_digest` (SHA-256 hex) so a shared browser profile distinguishes "enrolled here" from "enrolled for another account" without the endpoint URL leaving the backend; the frontend correlates on it and only offers destructive disable when verified. - Deep-link/tag grammar enforced in the owning crate; SW validates same-origin before navigating; subject parsed via `url`; concurrent-writer CAS test; registry key derived from the extension-id constant; parse-cause preserved; doc/count corrections. The manifest keeps the `web_push_vapid` field (a channel egress credential handle must be declared in [admin_configuration]); it stays host-seeded and not operator-supplied, with the rotation caveat documented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web-push): keep SW registration out of the initial /chat bundle; raise the chat budget for the feature's i18n copy The merge-lane bundle-budget gate (skipped on the PR lane) flagged the initial /chat JS at 218.2 KB against the 217.0 KB gzip budget. `main.tsx` boot-imported `registerServiceWorker` from `lib/web-push.ts`, which dragged the enrollment lib's api-client and WebCrypto imports into the initial chunk. Extracted the dependency-free `lib/register-sw.ts` for boot; the enrollment API stays in `web-push.ts`, imported only by the already-lazy automations route. That recovered the eager-code weight (218.2 -> 217.4 KB); the residual is the feature's new `en.ts` fallback-pack strings, so the /chat budget is raised 217.0 -> 218.0 KB with rationale, matching how prior features (hosted MCP, Router 8) handled eager localized copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(web-push): register the browser channel in the journey coverage gate The merge-lane product-surface evidence gate (test_journey_coverage.py, skipped on the PR lane) requires every outbound channel manifest to name exact journey evidence. web-push declares `outbound = true`, so it entered the required delivery-target set with none. Added: - `JourneyDeliveryTarget.WEB_PUSH` and a `ProductJourneyCase` citing the existing `blocked_fire_pushes_web_push_notice_to_enrolled_browser` integration test, with unthreaded (`thread_anchor=None`) delivery-address evidence — browser push addresses a per-browser endpoint capability URL, not a conversation thread. - `assert_web_push_delivery_evidence` in delivery_user_journeys.rs, shaped as the gate's citability check requires (literal `expected_conversation_id` gating the count, `expected_thread_anchor = None`), called from that test. Verified: test_journey_coverage.py (68), test_product_surface_coverage.py + test_provider_capability_inventory.py (30), and the cited integration test all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(web-push): rustfmt the journey-evidence helper `cargo fmt --all -- --check` (the first step of the Fast deterministic checks lane, PR-lane-skipped) flagged the iterator chain in `assert_web_push_delivery_evidence`. Formatting only, no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web-push): present the channel as "Web UI" and hide it from the install catalog The web-app's browser-push channel is host infrastructure, not a browse-and-install integration, so: - Rename the extension/channel to "Web UI" (manifest name + channel display_name + first-party bundle label; description reworded). - Hide it from the install catalog (/extensions, /extensions/registry, and the Settings channels list) via an explicit built-in-host-surface classification in the product lifecycle projection — deliberately keyed by id, not inferred from channel direction, so it stays correct as the web-app channel later gains inbound/outbound. Its outbound notification target (/outbound/targets) is a separate registry and is unaffected, so it remains a selectable notification channel. Also finalizes the enrollment UI carried from this session: - Align the "This browser" device block with the channel cards (drop the stray left indent). - Lazy-mount the web-push device hook so its status query fires only when a web-push row is present, not on every automations view. - Rename the misleading webOnlyHelper i18n key to noSelectionHelper across all locale packs (the string already dropped the retired "stays in the web app" claim). - Manifest icons declare purpose "any maskable" so Chrome offers the PWA install prompt. Regression coverage extends the production web-push integration test to assert it is absent from the install endpoints yet present in /outbound/targets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(web-push): fix stale helper name in install-catalog filter comment CodeRabbit flagged that the filter comment still named the removed `is_host_managed_channel` helper and described the old outbound-only heuristic. Point it at `is_builtin_host_surface` and the id-based classification the code actually uses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(web-push): pin notification-helper i18n key as noSelectionHelper The P1 rename webOnlyHelper -> noSelectionHelper missed two test assertions that pin the i18n key, failing the affected-3 Reborn crate bucket (Tests (Reborn) rolls it up): - crates/app/ironclaw_composition/tests/webui_v2_serve.rs (served bundle) - crates/product/ironclaw_webui/src/webui_v2/static_assets/assets.rs (live source) Also renames the now-misleading test fn and corrects a stale bundle-budget comment. Test-only; no production behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(channels): notifications as a first-class channel capability Separates "notification target" from "final-reply / model-delivery target" so the web app is a notification channel (blocked-automation notices) without being a place the model or a run's final reply is delivered to. Folds the notifications-capability work into the web-push PR (#7398). Model / declarative: - ChannelDescriptor gains a `notifications` capability (serde default), so a manifest can independently declare inbound / outbound / notifications. The web-push manifest declares `notifications = true` (kept `outbound = true` for now: it is what registers the channel's delivery binding; outbound thread-creation is a later capability). Pinned by manifest_lockstep. Delivery authority (the load-bearing separation): - DeliveryTargetCapabilities gains a `notifications` bool, and a new `OutboundDeliveryTargetProvider::resolve_notification_target` resolves a target the caller may receive blocked-automation notices on, gated on `notifications` (not `final_replies`). Default impl + registry/mutable-registry overrides; the generic channel provider reuses its id-resolution. - web-push target caps flip to `final_replies: false, notifications: true`: browser push is where a run's reply already lands, never a destination the model/final-reply path delivers *to*. - The notification-channel picker (`/outbound/targets`) and the notifier + set-validation resolution now gate on the notifications capability; the model-facing delivery list (`builtin.outbound_delivery_targets_list`) narrows to `final_replies`, so a notification-only target (browser push) is invisible to the model until it gains outbound delivery. Why this shape: production blocked-automation notices run through TriggeredRunDeliveryDriver -> DeliveryCoordinator -> WebPushChannelAdapter and select targets by capability resolution; the `ThreadNotificationPolicy` / `progress` push-plan in ironclaw_outbound is dormant (no production writer or caller), so gating on `progress` would not have changed delivery. Slack and Telegram are both final-reply and notification targets and are unaffected. Regression coverage: web-push resolves as a notification target but NOT as a final-reply/model target (targets.rs); existing notifier + notification-channel integration journeys stay green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(channels): cargo fmt + drop vendor names from generic-code comments - cargo fmt reflow in notification_channel_resolution.rs. - Reword comments in ironclaw_composition/runtime.rs and ironclaw_extension_host/channel_outbound_targets.rs so generic code does not name Slack/Telegram (reborn_extension_specificity gate). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs+test(channels): fix notifications doc + serde-default coverage Addresses CodeRabbit on the notifications-capability commit: - channel.rs: reword ChannelDescriptor.notifications doc so it no longer claims the web app declines outbound (the manifest keeps outbound = true for the delivery binding). Net-zero line count (contracts size ceiling unchanged). - delivery_resolution.rs: assert notifications defaults false, and add a legacy-payload (omitted notifications) deserialize test for #[serde(default)]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(outbound): decouple notification-picker list from model-delivery list Addresses the CodeRabbit "Major": final_replies and notifications were coupled through one shared base filter. Now RebornOutboundDeliveryTargetCapabilities carries a wire-level `notifications`; list_outbound_delivery_targets returns the union (final_replies || notifications); the notification picker (build_outbound_delivery_targets_view) filters notifications and the model list (list_outbound_delivery_targets_for_model) filters final_replies — independently. A final-reply-only target is now visible to the model but not the picker; a notification-only target (web-push) shows in the picker but never to the model. Verified: assistant, ironclaw_webui, frontend (1208), delivery_user_journeys (25), web-push round-trip, architecture suite (41). Chose "in #7398" per the overnight directive to fix review comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(outbound): cover divergent notifications/final_replies at the filter seam Addresses CodeRabbit's follow-up: the unit capability-filter seam couldn't construct divergent targets (the fixture set notifications = final_replies). Adds `target_entry_with_caps` (independent notifications) and a divergent-combo test asserting the base list is the union — a final-reply-only target and a notification-only target both survive, a neither-capable target is excluded. Existing coincident-case tests unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5ba8892da4 |
fix(webui): make model selector searchable (#7424)
* fix(webui): make model selector searchable * fix: review comments * fix: review comment * fix(webui): preserve search input Home and End keys |
||
|
|
3f1faa66b2 |
fix(webui): keep failure bubbles with their prompts (#7301)
* fix(webui): keep failure bubbles with their prompts * fix(webui): address ironloop review — preserve request failure pairs (#7301) * fix(webui): keep stream failures at refresh boundary (#7301) |
||
|
|
72bcf876b8 |
fix(webui): clarify no-progress run failures (#7422)
* fix(webui): clarify no-progress run failures * test(webui): cover recovery-required run failures * fix(webui): preserve durable phases on run failure |
||
|
|
432d9d0522 |
feat(webui): replace Logs toolbar selects with shared SelectMenu (#7335)
* feat(webui): use shared log level select menu * test(e2e): cover logs level SelectMenu * fix(tests): address logs select review feedback |
||
|
|
9faa6cd2b1 |
refactor(webui): share native file-picker interaction (#7337)
* refactor(webui): share file picker interaction (#7333) * test(e2e): cover same-file picker reselection (#7333) * test(webui): assert native file input reset (#7333) * fix(webui): address file picker review feedback (#7333) |
||
|
|
2042380731 |
chore: remove IronLoop small-fix role instructions (#7415)
* chore: remove IronLoop small-fix role instructions * ci: classify IronLoop configuration changes |
||
|
|
226bd491dd |
ci(canary): remove provider-matrix lanes and zizmor scan (#7418)
* ci(canary): remove provider-matrix lanes and zizmor scan * fix(ci): classify nextest config as exhaustive-plan change .config/nextest.toml is read by every Tests (Reborn) lane, so the fail-closed planner arm raised 'unclassified pull-request path' on any PR touching it, skipping all downstream Reborn lanes. Widen it to the exhaustive plan like crate deletions. |
||
|
|
9dd228a62a |
fix(ci): clear inherited main check failures (#7425)
* fix(ci): scope POSIX trace test import to Unix * fix(ci): run Windows WebUI setup with Bash * test(ci): pin Bash for Windows WebUI setup * fix(ci): clear remaining main check failures --------- Co-authored-by: italic-jinxin <106428113+italic-jinxin@users.noreply.github.com> |
||
|
|
4e05a033d2 |
fix(webui): let a failed run's error bubble capture a trace (#7370)
A terminal run failure renders as a `role: "error"` chat message, which never carried a `turnRunId` and was excluded from the existing "download run artifact" action (gated on `role === assistant`). The action-button row itself was also gated on `showActions || showRetryAction`, neither true for an error bubble, so the button stayed hidden even once its role check was relaxed. Net effect: a failed run had no way to capture a trace for it (#7369). Thread `turnRunId` onto the run-failure message at creation, in-place update, and through the unknown-to-known-run-id promotion path, and let `showArtifactAction` fire for error messages that carry one. The existing download handler needed no changes — it was already keyed only on `threadId`/`turnRunId`, not role. Fixes #7369. Co-authored-by: Sergey <sergey@Mac.attlocal.net> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
4a5b3a6910 |
fix(webui): restore scoped attachment reads and SSE tests (#7341)
* fix(composition): read landed attachments through the per-caller workspace mount
/projects/workspace/tenants/{tenant}/users/{user}, but the loop-host
attachment_read_port still read through the shared read-only fixed view
(services.workspace_filesystem), which resolves the workspace root. A
landed image therefore came back NotFound at model-gateway time and was
silently dropped, so vision-capable model payloads lost every inline
image (the duration-4 Playwright attachment failure).
Wire the read port over the same per-caller scoped handle the WebUI
lander uses (runtime_mounts::read_write_workspace_filesystem), mirroring
what #7062 already did for the channel-host assembly. Under the Shared
policy the handle is byte-identical to the old fixed view; under
PerCaller it now resolves the caller's subtree.
* test(playwright): reconcile legacy WebUI v2 suites to the fetch-based SSE transport
event-source-plus (fetch/ReadableStream). The legacy suites still faked
window.EventSource, so the app never opened a stream and every
duration-1/duration-4 legacy test that emitted frames failed with "no
EventSource stream is open".
- Extract the smoke suite's proven fetch fake into
install_fake_v2_event_stream() in reborn_webui_harness, extended to
record request URLs and headers for reconnect assertions.
- Port all seven legacy scenario files onto it, updating cursor/token
assertions to the header contract (Authorization bearer,
Last-Event-ID) instead of the retired token/after_cursor query
params.
- legacy_skills delete: use the shared in-app confirmation dialog
instead of a native browser dialog.
- legacy_dom_resource_limits reconnect-timer: assert the pending
reconnect is cancelled when the tab hides (the fetch transport
schedules retries internally).
- legacy_rendering: assert no live onerror/iframe/img nodes instead of
substring-scanning escaped text.
- extensions_api: restore the #6520 wire contract (retired
authenticated/active/needs_setup/has_auth/onboarding_state booleans
must be absent).
- tool_execution truncated-tool test: expect model_output_truncated
failure per #6845's no-recovery contract instead of an assistant
recovery message.
- streaming_run_control_api: drop the stream=true assertion for the
OpenAI-compatible mock, which rides the buffered fallback since
#7120 (rig-core cannot distinguish a complete stream from a truncated
one).
|
||
|
|
fa6e72d7c0 | test(tool-search): add large-catalog baseline (#7409) | ||
|
|
1439e5b360 |
fix(webui): render gemoji shortcodes in chat markdown (#7404)
Co-authored-by: aiworkbot <220660587+aiworkbot@users.noreply.github.com> |
||
|
|
cd38d8834e |
chore(agents): refresh codebase knowledge graph (#7412)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> |
||
|
|
9fd1e638a6 |
feat(inspector): complete statistics, navigation, and localization (#7291)
* feat(inspector): add operator inspection API * docs(inspector): assign product service ownership * test(inspector): ratchet diagnostic contracts * feat(inspector): add debug panel shell * test(inspector): cover debug panel shell e2e * fix(inspector): stop diagnostics when panel closes * feat(inspector): add prompt inspection * fix(inspector): follow current webui ownership * feat(inspector): add model call statistics * test(inspector): cover model statistics e2e * fix(inspector): avoid uncollected tool metrics * test(inspector): cover prompt diagnostics e2e * test(inspector): align statistics e2e scope * fix(inspector): redact prompt metadata * fix(inspector): preserve per-call model identity * fix(inspector): classify prompt instruction sources * test(inspector): assert reported token usage * feat(inspector): add activity timeline and turn navigation * test(inspector): cover activity timeline in browser * fix(inspector): read current run before publishing activity * feat(inspector): add bounded tool execution details * test(inspector): cover bounded tool details in browser * fix(inspector): validate retained tool result sizes * test(inspector): add security and operator coverage * test(inspector): cover browser workflows end to end * fix(inspector): address review feedback * fix(inspector): retry transient snapshot failures * fix(inspector): address prompt diagnostic review findings * fix(inspector): follow debug query navigation * feat(inspector): complete frontend diagnostics * test(inspector): cover frontend parity in browser * fix(inspector): preserve stream terminal state * fix(inspector): capture full capability surface * fix(inspector): scope projection activity to its run * fix(inspector): harden activity diagnostics * fix(inspector): bound tool result diagnostic capture * fix(inspector): harden tool diagnostic pipeline * fix(llm): request usage for NEAR AI streams * fix(inspector): address prompt diagnostic review feedback * fix(webui): harden inspector stream coverage * fix(inspector): preserve debug session statistics * fix(inspector): keep diagnostics active while hidden * test(e2e): cover hidden inspector observation * fix inspector model call stats review findings * fix inspector refresh and truncation regressions * fix(inspector): address activity timeline review feedback * fix(inspector): harden activity lifecycle handling * fix(composition): move tool diagnostics to loop host * fix(inspector): keep a settled stream live and complete locale parity A live diagnostic update's debounced snapshot refresh was announcing LOADING, so an open, healthy stream read as "Connecting" indefinitely once a run settled — the settling stats update is the last one. That refresh is now a background read. Incomplete snapshot statistics no longer accumulate as real zeros, browser-session inspector state is namespaced by the authenticated caller, an evicted pinned run rejoins the latest turn instead of the oldest, tool status is localized, and the inspector strings now cover all ten locales. * test(inspector): put the inspector locale sidecar under the parity gate The inspector's English copy is registered from its lazy chunk instead of src/i18n/en.ts, so the all-locale parity test — which derives the required key set from en.ts — never covered those keys; a locale could drop one and fall back to English silently. The test now treats the English key set as the union of en.ts and a declared sidecar list. Keeping the copy in en.ts is not an option: measured, it puts /chat at 217.4 KB gzip against a 217.0 KB budget. * fix(inspector): reject malformed model breakdowns and correct locale copy A `calls_per_model` entry with a negative or non-integer `calls` passed the statistics decoder and was then coerced to zero during accumulation without marking the breakdown truncated, presenting a fabricated "0 calls" for a model. Every entry is now validated before a record is accepted. German turn navigation used "Zug" (a train, or a game move); it now reads "Runde", with the determiner agreement that noun requires. Spanish and Portuguese tool-status values were written feminine against a masculine "Estado"/"Status" label. * fix(inspector): bound the model breakdown before scanning and retaining it The statistics decoder validated every calls_per_model entry but never the array length, so an out-of-contract response was scanned in full and then retained by the accumulator for up to 128 runs. The host truncates this breakdown at MAX_MODELS_IN_STATS and reports it as truncated, so a longer array cannot conform; the client now mirrors that ceiling and rejects the record before the scan. * fix(inspector): align turn navigation with host diagnostic retention The browser offered 32 turns of navigation per thread while the host retained diagnostics for 2 runs per session, so every turn past the second rendered blank. Each layer was individually correct and the e2e scenario stopped at two turns, so nothing saw the dead zone. Retention moves to 4 and the navigation window mirrors it, pinned by a new architecture gate that reads both constants; the scenario now walks back two turns and asserts real activity. Retention is a ceiling as well as a default, and capture is unconditional, so 4 is a resident memory choice — roughly 80 MB worst case across the eight tracked sessions. * fix(composition): delimit the i18n bundle guard with an i18n-owned marker The guard sliced the concatenated chunk bundle from the i18n provider up to `QueryClient`, a symbol another module owns, so the segment's extent tracked Rollup's chunk boundaries. A split that merely folded react-query into the entry chunk removed that marker from everything appended after the provider and failed an i18n guard with no i18n change. It now ends on the AVAILABLE_LANGUAGES literal that follows the provider in the same module; string literals survive minification, and every existing assertion holds against the tighter segment. * fix(architecture): resolve the inspector gate's SPA path through crate_path The gate joined a family-nested literal onto the workspace root, the idiom crate_path exists to replace: a crate family move would have turned this into a read failure rather than a resolved path. It now names the SPA file in the logical flat spelling and resolves it, and the assertion reports the resolved path so the message still points at a file that exists. * test(inspector): follow a pinned turn explicitly when a new turn arrives The multi-turn scenario assumed the panel would jump to an arriving turn, but a selection the operator navigated to is deliberately sticky: the new turn widens the window without yanking them off the turn they are reading. The scenario now asserts that guarantee, then clicks Latest to follow, then walks back two turns as before. Verified by running the inspector scenarios locally rather than by reading, which is how this slipped through the first time. |
||
|
|
89285c8e70 |
fix(skills): one DB-backed tree for every skill mount, and make a skill's own commands runnable (closes #7168) (#7171)
* fix(reborn): inject skill bodies by default, not a one-line listing Reborn defaulted `SkillInjectionMode` to `Listing`, where a non-activated skill contributes only `- name: description` to context and its body loads only on an explicit `$name` mention or a `builtin.skill_activate` call. The intent was to save context budget. Benchmarking shows the model reads the menu and then never opens the skill. Over 30 runs with human-curated skills installed (SkillsBench/SkillLearnBench subset, `deepseek-v4-flash`, nearai/benchmarks#287): builtin.skill_list called in 30/30 runs builtin.skill_activate called in 3/30 runs a skill body actually read 0/30 runs So installed skills were effectively inert. Same 31 tasks, same skills, same model, varying only this default: no skills 78.5% curated skills, Listing 79.8% (+1.3pp -- skills bought almost nothing) curated skills, Full 85.6% (+7.1pp) For reference, harnesses that inject skill bodies unconditionally (Hermes, Claude Code) score 91.5% on these tasks with the same skills, so `Full` closes most but not all of that gap; the remainder is loop/verification behavior on a handful of multi-output tasks and is tracked separately. `Full` is already the library default in `SkillActivationSelectorConfig`; only the Reborn composition seam opted out. This restores it and adds a guard test so a revert is deliberate. `IRONCLAW_REBORN_SKILL_INJECTION=listing` still selects the previous behavior where context budget matters more than skills being used. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(skills): hot-swappable activation strategies so agent-authored skills are reusable Adds `skill.activation.v1`, a swappable-provider module in the shape of the memory-provider binding (`ironclaw_host_runtime::memory_binding`): named strategies, fail-closed resolution, behavior-preserving default, and a composition seam so nothing downstream names a concrete implementation. ## The bug it addresses `selector::score_skill` accumulates score ONLY from `activation.keywords` (+10/+5), `activation.tags` (+3) and `activation.patterns` (+20). A skill's `name` and `description` contribute nothing, and `select_skills` keeps a skill only `if score > 0`. That is fine for curated skills, which ship an `activation` block. It is fatal for skills an agent writes for itself: measured across the 31-task SkillsBench/SkillLearnBench subset in nearai/benchmarks#287, **0 of 30** agent-authored skills contained an `activation` block. Every one scored 0 and was permanently unselectable — the agent could create a skill via `builtin.skill_install` and then never reuse it, which makes self-improvement structurally impossible rather than merely weak. Claude Code has no such requirement: a skill is selectable from name and description alone. `ActivationStrategy::NameAndDescription` ports that contract. ## Design * `CriteriaOnly` (default) — today's rule, byte-identical. * `NameAndDescription` — whole-word name/description fallback, applied ONLY when the criteria pass scored 0, so a curated skill's explicit keywords always decide ordering and this can never reorder two skills that both declare metadata. `NAME_WORD_SCORE` (8) is deliberately below the selector's exact-keyword award (10). * `Disabled` — explicit mention / `skill_activate` only. * `ThirdParty { extension_id }` — production requires an admin override. Whole-word matching and a `MAX_FALLBACK_SCORE` cap keep it from over-selecting; over-selection is the failure mode that makes injecting an unrelated skill bank harmful (a whole-catalog injection took `xlsx_recover_data` 1.000 -> 0.271). ## Default stays behavior-preserving Reborn's default remains `CriteriaOnly`, opt in with `IRONCLAW_REBORN_SKILL_ACTIVATION=name_and_description`. Flipping the default changes three existing local-dev expectations (setup-marker suppression, the webui listing candidate, `skill_activate` context loading), so the strategy ships opt-in — the same discipline as the memory work, where the bundled native provider stays the default. ## Tests `cargo test -p ironclaw_skills --lib` — 239 passed, including: * `agent_authored_skill_unreachable_by_default_but_selected_under_name_strategy` — end-to-end via `prefilter_skills_with_options`: the same no-activation skill is dropped under `CriteriaOnly` and selected under `NameAndDescription`. * `name_strategy_does_not_select_an_irrelevant_skill` — no over-selection. * `name_hit_outranked_by_an_explicit_curated_keyword`, `whole_word_only_...`, `fallback_is_capped_...`, `stop_words_do_not_accumulate_score`. `cargo test -p ironclaw_first_party_extension_ports --lib` — 58 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(reborn): ship the Full skill-injection default as opt-in, not a flip The measurement in the previous commit stands: `Listing` leaves installed skills unread (`skill_list` 30/30 runs, a body actually opened 0/30) and `Full` is worth 79.8% -> 85.6% on the 31-task SkillsBench subset. But flipping the product default HANGS three existing local-dev tests, which drive a mock that expects the one-line listing candidate: * `local_dev_skill_activate_tool_loads_selected_skill_context` * `local_dev_webui_bundle_records_selectable_filesystem_skill_context` * `local_dev_runtime_wires_filesystem_skills_by_default_to_model_calls` Verified by bisect: all three hang on the previous commit alone, and pass with the default restored — the activation-strategy work is not implicated. Changing a documented product default in a way that turns CI red is a maintainer call, not something to force through, so `DEFAULT_SKILL_INJECTION_MODE` returns to `Listing` and `Full` ships as `IRONCLAW_REBORN_SKILL_INJECTION=full`. Both switches in this PR are now opt-in with the evidence attached, matching the memory-provider discipline where the bundled default is preserved. The guard test is retargeted to assert the current default, verify the opt-in path still resolves, and name the three tests that must be updated alongside a future flip. cargo test -p ironclaw_reborn_composition --lib -- skill_injection_mode \ local_dev_selector_config skill_activation # 14 passed, 0 failed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(threads): raise the result_read cap to 64 KiB, env-tunable A small per-request `result_read` cap turns one large file into a paging loop. On `manufacturing_equipment_maintenance` (nearai/benchmarks#287) reborn made 8 `read_file` calls and ZERO shell calls, hit the 24 KiB cap, then spent the whole turn paging — `result_read` at offset 24576, `handbook.pdf` at offsets 400/800/1200 — and never computed anything (`outputs_exist=0.00`). hermes, using shell to sample the same data, scored 0.522. * `TOOL_RESULT_RECORD_READ_MAX_BYTES` 24 KiB -> 64 KiB. This is the compile-time ceiling the model-observation envelope in `tool_result_reference.rs` is derived from (`* 2`, asserted at compile time), so 64 KiB here means a 128 KiB envelope — the reason not to go higher. * `TOOL_RESULT_RECORD_READ_DEFAULT_MAX_BYTES` = 64 KiB — the effective default. Enough that a typical data file or document page arrives in one read instead of a paging loop. * `IRONCLAW_TOOL_RESULT_READ_MAX_BYTES` overrides it, clamped to `[4, ceiling]`, so an override can never outgrow the envelope. Unparseable values fall back to the default rather than failing the run — a malformed tuning knob must not take down an agent. Unlike the skill-injection and skill-activation switches in this branch, this one does move the default: the paging loop is a silent capability loss rather than a behavior preference, and the knob exists for deployments that want the old size. cargo test -p ironclaw_threads --lib # 88 passed (85 existing + 3 new) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(skills): add always_available activation, Claude Code's actual contract `skill.activation.v1` gains a third binding, `always_available`: every installed skill is a candidate regardless of what it matches. This is what Claude Code and Hermes actually do. In both, a skill is a file in a directory the agent can read, so there is no gate for a correctly-installed skill to fail. Reborn's selector instead scores only `activation.keywords`/`tags`/ `patterns` and drops anything scoring 0 -- and `name_and_description` (this branch's earlier binding) only WIDENS that gate: it still needs a lexical hit, so an applicable skill phrased differently from the prompt is still discarded. The new test pins exactly that case -- a skill described as "cyclical component / growth path" against a prompt saying "hp filter" is dropped by both `criteria_only` AND `name_and_description`, and kept by `always_available`. Why it matters, measured on the 31-task SkillsBench/SkillLearnBench subset in nearai/benchmarks#287: 0 of 30 agent-authored skills contained an `activation` block, so under `criteria_only` a self-authored skill could never be selected again -- self-improvement was structurally impossible. Implementation is deliberately tiny: a `floor_score()` of 1 for this binding, applied via `.max()` in the selector's existing scoring loop. Ordering is untouched (a real keyword match still outranks a floor skill, so the context budget spends on the relevant skill first), and the existing budget -- not the score filter -- decides what is injected, which is also how Claude Code behaves. `floor_score()` is 0 for every other binding, so non-adopters are byte-identical. Default remains `criteria_only`; opt in with IRONCLAW_REBORN_SKILL_ACTIVATION=always_available. cargo test -p ironclaw_skills --lib # 241 passed (239 existing + 2 new) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(threads): drop the now-unused ceiling import Validation bounds against `contract::effective_tool_result_read_max_bytes()` (which applies the env override), so the compile-time ceiling is no longer referenced here. Removes an unused-import warning introduced by the 64 KiB cap commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * revert(threads): default result_read back to 24 KiB, keep the knob The raise to 64 KiB was never isolated: it shipped in a measurement arm alongside two other switches (skill activation, tool disclosure), so there is no evidence it changed anything. Defaulting it back keeps this crate byte-identical to pre-PR behavior. The paging trace that motivated it is real (`manufacturing_equipment_maintenance`, nearai/benchmarks#287: 8 `read_file` calls, zero shell calls, `result_read` at offset 24576, nothing computed) — but a real trace is not a measured fix, so the larger cap stays opt-in via IRONCLAW_TOOL_RESULT_READ_MAX_BYTES for whoever wants to measure it properly. The compile-time ceiling stays 64 KiB: it now bounds only how far the env override may reach, and still pins the derived model-observation envelope at 128 KiB. Net effect of this commit plus its parent: a new env knob, no default change. cargo test -p ironclaw_threads --lib # 88 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(skills): design for agent-authored multi-file skill bundles @henrypark133 pushed back on "move skills to the filesystem" as an overhaul that a single aggregate result did not justify. He was right, and stratifying the data shows why: the entire filesystem gain sits in skills that ship files besides SKILL.md. ships resource files (n=16): inject 81.0% -> files 94.2% (+13.2pp, CI [+0.3, +26.2]) SKILL.md-only (n=11): inject 91.5% -> files 84.7% (-6.9pp, CI [-20.4, +6.7]) So filesystem-for-everything is a REGRESSION on 13 of 31 tasks, paid to fix the other 18. The mechanism is not "models prefer filesystems": 81 of the resources are executable (you cannot run pasted Python -- citation_check scored 0.000 with the script absent, 0.833 with it present), and the text resources are too large to inline (exceltable_in_ppt would be ~262k tokens folded into SKILL.md). The design therefore keeps storage, discovery and selection exactly as they are and adds ONE extension holding the already-existing `/skills` read_write mount: skill_write_file / skill_read_file / skill_list_files. Discovery already lists from the same root that mount writes to, so nothing needs plumbing. Executing a bundled script copies that one file into `/workspace`, which the agent already mounts. Documents two things the implementation must not miss: SkillBundleDescriptor exposes only `skill_md_path`, so bundle resources are un-advertisable without skill_list_files; and `FilesystemSkillBundleRoot::user` marks bundles Trusted, so an agent that can write executable scripts there needs a distinct trust level -- the real open question. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(skills): state explicitly that creation, discovery and indexing are unchanged The crux of @henrypark133's objection. Spells out, per concern, that skill creation stays on the `skill_install` tool, discovery stays on the storage-agnostic `SkillBundleSource` trait with no new impl / trait method / descriptor change, and that there is no session-start index to migrate at all (selection is per-request; the only cache is a 5-minute TTL on catalog search). The single behavioral change remains the opt-in `always_available` selection predicate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(skills): the write tool needs an authoring prompt that asks for code skill_write_file makes multi-file skills possible; it does not elicit them. Measured: 6 of 31 tasks finished with ZERO skill_install calls despite 'Saving the skill is required', and the authoring request only ever asks for prose (method, conventions, output contract). An agent following it writes prose whether or not a write tool exists. Adds the elicitation requirement and a falsifiable success criterion: agent-authored bundles are currently 100% prose (0 of 27 ship a resource file) against 18 of 31 curated skills. If that ratio does not move once the tool ships, the bottleneck was elicitation rather than capability and the tool alone will not move scores. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(skills): let an agent install skill bundles, not just prose Agents could only ever author the PROSE half of a skill. Measured on the 31-task SkillsBench/SkillLearnBench subset (nearai/benchmarks#287): **0 of 27** agent-authored skills shipped a single file besides SKILL.md, against **18 of 31** human-curated ones (79 .py scripts, 78 .xsd schemas, 84 .md references). So every later run re-derived the method from prose and could re-make the same mistake -- lake_warming's self-authored skill described its regression procedure in prose, the next run recomputed it slightly differently and missed the grader's p<0.05 threshold. This was NOT a missing capability. `install_skill` has always taken `files: &[SkillInstallFile]`, and `parse_install_files` has always read an `input["files"]` array. Two things made it unreachable: 1. `schemas/builtin/skill_install.input.v1.json` advertised only `name`/`content`/`url` AND set `additionalProperties: false` -- so a model sending `files` was not merely uninformed, it was REJECTED. Across 112 observed skill_install calls, 111 used exactly `['content','name']`, which is what the schema permits. 2. The only encodings were `bytes_base64` and a JSON array of byte integers. A bundle file an agent writes is a script, a reference doc or a schema fragment -- all UTF-8. Making those go through base64 costs ~33% more tokens and turns one encoding slip into an InputEncode failure of the whole install. Changes: - `parse_install_files` accepts `text` (UTF-8) alongside `bytes_base64`/`bytes`. `text` takes precedence when both are given, matching the documented preference. Binary payloads are unaffected. - the schema advertises `files` with `path` + `text`/`bytes_base64`, and the description tells the model WHY to use it: put a reusable computation in a script rather than describing it in prose, and have SKILL.md name the files it relies on. That last part matters because `SkillBundleDescriptor` exposes only `skill_md_path`, so a bundle cannot advertise its own resources. - prose-only installs are untouched: no `files` key still parses to an empty vec. cargo test -p ironclaw_first_party_extensions --lib install_files_encoding # 4 passed cargo test -p ironclaw_host_runtime --test tool_surface_contract # 43 passed cargo test -p ironclaw_reborn_composition --test product_live_adapters skill_install # 1 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): stop rejecting an install that carries both content and files `skill_install_input` gated the direct-install arm on `!object.contains_key("files")`, so `content` + `files` matched NO arm and fell through to `_ => Err(InputEncode)`. An agent attaching a script had its ENTIRE install refused. `files` was reachable only on the URL-fetch arm, which builds the array itself. This was the third of three stacked gates hiding the same capability, and the one that actually bit. With the schema fixed to advertise `files` and a `text` encoding available, the model on the 31-task SkillsBench subset (nearai/benchmarks#287) immediately sent 18 correctly-shaped `{path, text}` entries across 9 calls -- `scripts/verify_bib.py`, `references/fake_patterns.json` -- and every one was rejected here. That is the real reason 0 of 27 agent-authored skills shipped a resource file while 18 of 31 human-curated ones do: not a missing capability, and not the model failing to try. `source`/`source_url` stay excluded from the direct arm: those record provenance and are set by the URL path, so an agent must not be able to forge them. cargo test -p ironclaw_host_runtime --lib skill_install_input # 4 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: rustfmt the skill-bundle and activation changes Test modules were appended programmatically without rustfmt, which is why Formatting, Code Style and Clippy all went red on this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(skills): rewrite to match what was measured, not the abandoned design The doc recommended a three-tool extension plus a resource-gate. Both are superseded: the tools turned out to be redundant (install_skill already accepted files -- three stacked gates were hiding it), and the gate MEASURED WORSE than always advertising a readable path (-25.7pp on self-creation, -40.6pp vs claude-code), because an agent-authored skill is usually SKILK.md-only so the gate suppresses the one route the selector had not already closed. Rewritten around the durable findings: the three gates and how each masked the next, the 0-of-27 vs 18-of-31 measurement, the SkillBundleDescriptor enumeration gap, and the trust question. The gate is kept in the doc as a recorded negative result, since its stratified justification is persuasive and will be proposed again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(skills): correct why always_available is not the default The previous note claimed the floor score overrides setup-marker suppression. It does not: `prefilter_skills_with_options` returns None for a satisfied marker BEFORE scoring, and the host-side filter in activation.rs already removed the candidate. What actually fails: all 32 bundled skills reach floor 1, so 3-4 unrelated ones land in plan.activations() in ActivationCriteria mode -- a mode that injects nothing under Listing. The defect exposed is that a criteria activation which injects no body is still recorded as an activation, so the count assertions stop being meaningful. Also records the sequencing against epic #6565 (Slice 0 first; Slice 5's bounded-shortlist rule constrains what an unbounded floor may do) and the measured detail that under Listing a zero-scoring skill is still listed -- the model just called skill_activate in only 3 of 30 runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): a floor-only skill is ranked, not activated Three defects, all surfaced by trying to make `always_available` the default. It failed 8 tests in ironclaw_reborn_composition; all 8 now pass with the flag on AND off. 1. A criteria selection that injects nothing was still recorded as an activation. Under `SkillInjectionMode::Listing` an `ActivationCriteria` entry contributes no body -- `body_eligible_bundle_ids` already ignores that mode -- so with a floor score every installed skill "activated" on every turn. Concretely: all 32 bundled skills reach floor 1, and 3 of them (6000 token budget / 2000 default per-skill cost) landed in each plan, chosen by descriptor order because the score sort is stable. `SelectionOutcome` now returns those separately as `ranked_only`, and the activation path does not iterate them. They still reach the model through the listing, which is where they belonged. 2. `AlwaysAvailable` also enabled the name/description fallback, which manufactured fake merit: a bundled skill whose description shares one word with the message scored above zero and was reported as a genuine activation. Under `AlwaysAvailable` the fallback adds no reach at all (the floor already admits everything), so it is now scoped to `NameAndDescription`, where widening the match is the entire point. This is what kept `local_dev_runtime_suppresses_explicit_setup_skill_when_workspace_marker_exists` failing after (1). 3. Raising TOOL_RESULT_RECORD_READ_MAX_BYTES to 64 KiB was NOT the no-op this PR claimed. `tool_result_reference.rs` derives MAX_MODEL_OBSERVATION_BYTES from it (* 2), so the observation envelope silently doubled 48 KiB -> 128 KiB and preview truncation changed for every caller. It broke three tests whose fixtures are sized against the envelope ("fixture must exceed the preview cap"), independently of any activation setting. The contract ceiling is back to 24 KiB and the env override is bounded by a new TOOL_RESULT_READ_ENV_CEILING_BYTES that nothing is derived from -- so the knob can raise a single read without moving anyone else's behavior. Correcting the record on an earlier comment in this PR: the failures were never the setup-marker interaction. Marker suppression returns None before scoring, so a floor score cannot revive a suppressed skill. cargo test -p ironclaw_reborn_composition --lib # 634 passed IRONCLAW_REBORN_SKILL_ACTIVATION=always_available cargo test -p ironclaw_reborn_composition --lib # 634 passed cargo test -p ironclaw_skills --lib # 241 cargo test -p ironclaw_threads --lib # 88 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * revert(skills): remove the always_available strategy, it bought nothing Verified against this branch: `AlwaysAvailable` was a no-op for everything the model can observe, and it carried a regression. Removing it rather than wiring a compensating half. Why it bought nothing. Listing membership is decided by VISIBILITY, not selection (extension_ports/activation.rs partitions candidates on body-eligibility, and everything not body-eligible still goes into the listing), so the model was ALREADY shown every visible skill before this strategy existed. The floor score never added reach -- pre-C1 its only effect was listing ORDER, and after C1 excluded floor-only skills from activations even the ordering effect was gone, because the ranking input is derived from the activation list. `SelectionOutcome::ranked_only` had no production reader at all: allocated, populated, returned, dropped. Under `Full` a floor-only skill could never be injected either, since `context_candidates_for_plan` renders only activated bundles. The regression. The floor-only bookkeeping ran for every non-merit entry BEFORE `try_select`, so under this strategy a chain-loaded companion got its own loop iteration, was recorded as floor-only, and was then partitioned OUT of `selected` -- i.e. `A requires B` activated only `A`, where `CriteriaOnly` activates both. Strictly worse than the default for any bundle with companions, and order-dependent. The comment claiming this could not happen was wrong. Also removed: ~29 "budget exhausted" notes per turn that reached `feedback` and fired a SkillActivation live-projection event with empty skill_names, because floor-only skills still ran the budget loop and `BudgetFull` continues rather than breaks. Kept: `NameAndDescription`, which has a real effect (matching on name/description, not only `activation.keywords`/`tags`/`patterns`), and the `skill.activation.v1` seam. Corrects the record in two places that argued the opposite: the runtime.rs doc comment and docs/skills/agent_authored_bundles.md. The measured reachability gap is elicitation, not filtering -- `builtin.skill_activate` was called in 3 of 30 runs and a body read in 0 of 30 -- so the next step is the listing header, not a scoring change. Note the parity numbers in nearai/benchmarks#327 never depended on this strategy: those arms ran with it off. cargo test -p ironclaw_reborn_composition --lib # 634 passed cargo test -p ironclaw_skills --lib # 240 passed cargo test -p ironclaw_threads --lib # 88 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): explain refusals, gate requirements, survive discovery limits Epic #6565, the missing/unusable half. Four causes it names, each of which was silent. ## A refusal now says why `select_named_skill_activations` reported a skill that exists but is not `Trusted` with the same string as a name that does not exist: "requested skill is not available". The two need opposite responses -- one means "try a different name", the other means "this needs promoting and no name will work" -- and the model got neither. This is the routine outcome of the model doing what it was told: the listing filters on visibility only while activation requires `Trusted`, and tenant-shared and URL-installed skills are `Installed`. So the listing advertises a skill and activation then refuses it. Deliberately does NOT enumerate alternatives, tempting as that was: `load_named_activation_candidate_set` scopes the candidate set to the requested names, so nothing else is loaded at that point and any "available: ..." list would be empty. I wrote that branch, found it could never fire, and removed it rather than ship a message that lies. Offering alternatives needs a wider descriptor load and belongs with #4428. ## Requirements are actually checked `requires.bins`, `requires.env` and `requires.config` were parsed into the manifest and never consulted. `check_requirements` exists, but its only callers are inside `SkillRegistry`, which has no consumers outside its own crate. A skill declaring a binary it needs was offered, activated cleanly, and failed later in the shell with nothing connecting the failure to the unmet requirement. Gated at ACTIVATION time, on both the explicit-mention and model-selected paths. Not at listing time: that would be three probes per visible skill on every prompt build and needs a caching design first. At activation it runs for the handful of skills being loaded, so the cost objection does not apply. Staying unusable is correct here -- the fix is that the reason reaches the model. ## One oversized root no longer erases itself `list_root` returned `BundleScanLimitExceeded` when a root held more than `max_bundles_per_root` directories, which removed EVERY skill in that root from the model's view. A catalog that grew past the cap lost all its skills at once, with no signal to the model and only a propagated error to the operator. Now it keeps the bundles that fit and warns about the truncation. ## Silent skips became warnings that name the reason Two `debug!` sites -- an invalid bundle directory name, and a manifest that fails validation (which covers the common authoring mistake of a directory name disagreeing with the manifest `name:`) -- meant a skill present on disk simply never appeared and nothing said why. Both are `warn!` with the error attached. ## Tests Three that pinned the old behaviour were rewritten rather than deleted, each with why: the two refusal-message assertions, and the scan-limit test that asserted total root loss. New coverage for the trust-vs-name distinction and for an unmet binary requirement being refused with the requirement named. `cargo test -p ironclaw_first_party_extension_ports -p ironclaw_loop_host` — 68 + 420 + 27 + 4 + 88 pass. fmt and clippy clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(skills): surface the refusal reason to the model, not just the projection The reason strings were the easy half. `skill_activate`'s result was built from `plan.selection.activations` only and **discarded `plan.selection.feedback` entirely**, so every reason the selector produces was constructed and thrown away. The model saw `{"activated":[],"count":0}` and had to guess whether it had used a bad name, hit a trust wall, or tripped an unmet requirement -- three situations that need three different responses. Caught by measurement rather than review: on the missing/unusable fixtures `usable` moved after the earlier commit but `diagnosed` stayed flat at 2/7, because improving the wording of a message nobody receives changes nothing. Adds `not_activated` alongside `activated`. Routine "activated after model selection" confirmations are filtered out -- next to `activated` they are noise and would dilute the refusals that matter. Output construction is extracted into `build_activation_output` so the contract is unit-testable in the same style as the rest of the module: a refusal carries its reason, a clean activation gains no empty field, and a mixed result reports both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(skills): the model decides, and the listing can never be hidden from it Epic #6941 criteria 1 and 8. Adopts @serrrfirat's position -- "I don't think we should statically regex match skill names to skill activation... we should show it to the model and it should decide" -- as the default rather than a flag. ## The default is now ExplicitOnly `SkillActivationSelectorConfig::default()` no longer runs the keyword/regex scorer. A profile that wants it must ask for `ExplicitAndCriteria` deliberately; nothing inherits it silently, which is how #5417 shipped. The scorer's own record is the argument for retiring it: * It produced #5417 -- `tech-debt-tracker` declares the keyword `hack`, so "search Hacker News for..." activated it. * Over 328 real prompts `coding` fired on ~220 through *legitimate whole-word* hits on `file`/`change`/`code`. No boundary rule or score threshold can fix that. * Measured against it, the model path made **zero** wrong selections across 28 tasks over an 88-skill catalog, at **94.8%** precision on what it did activate. The scorer is not deleted: it is still correct, still tested, and still reachable for a profile that opts in. It is simply no longer the thing that decides. ## And a trap that had to be fixed in the same commit In `Full` injection mode both context paths returned an EMPTY candidate set when nothing was active. That was survivable only while the scorer auto-activated something. With model-decides it would mean the model is never told a skill exists and therefore can never activate one -- flipping this default alone would have blinded the agent. Both paths now fall through to the listing. ## Tests Three new criterion tests: the default policy is pinned (so a silent revert fails), the listing survives with nothing activated in BOTH injection modes, and the listing stays inside a stated character budget at 200 skills -- with the scorer retired the listing IS the routing interface, so its size is a correctness property rather than a cosmetic one. Sixteen existing tests were updated rather than deleted, in two groups, each with its reason in the code: * Tests whose SUBJECT is the scorer now call `criteria_config()` to opt in. That is the point of the new default -- nothing gets the scorer by accident. Includes `prepare_grants_asset_reads_only_for_activated_bundles`, which uses criteria selection only as the mechanism to activate a bundle; I checked that one first because an asset-grant assertion failing could have been a real security regression rather than an expectation change. It was not. * Tests asserting `selected.is_empty()` now assert `assert_no_skill_body_disclosed(..)`. "No candidates" is no longer the right question; "no skill BODY reached the model" is what they were really protecting, and it is exact -- the listing is a *discoverable* candidate (`loaded_skill_md() == None`) while an activated skill is a *loaded* one. `cargo test -p ironclaw_first_party_extension_ports -p ironclaw_loop_host -p ironclaw_skills` -- 74 + 420 + 27 + 4 + 88 + 229 pass. fmt and clippy clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(skills): pin #5417 on the path that records the user message Epic #6941 criterion 2. Criteria selection needs a RECORDED user message (`take_message_for_run`), and the coordinator path never records one -- so a coordinator-path test passes vacuously and proves nothing. This records the message, which is what the product/WebUI surface does, and the issue itself reports "Run origin: WebUI chat". Asserts BOTH policies, including the uncomfortable one: * model-decides (the new default): no body is injected. Fixed. * criteria explicitly enabled: it STILL mis-activates on this branch. Asserted as a known residual rather than omitted. That second arm is the useful half. It shows the two changes are complementary rather than redundant: this PR removes the scorer from the decision, #6937's word-boundary matcher stops `hack` matching inside "Hacker" for any profile that opts the scorer back in. Neither alone closes #5417 on the criteria path, and pinning it here means a future reader cannot mistake model-decides for a complete fix. The assertion message says what to do when #6937 merges and the arm flips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(skills): a truncated skill listing must say so `MAX_LISTED_SKILLS` is 100 and the listing is source-then-name ordered, so past the cap whole alphabetical tails vanish -- with no signal to the model and none to the operator. Found by walking into it. Running Benchmark A against a 227-skill catalog, `pdf`, `pptx`, `xlsx` and `timeseries-detrending` all sorted past position 100, so three of the first four tasks could not reach their own expected skill and the arm was measuring nothing. Nothing anywhere reported it; I only caught it by diffing the rendered listing against each task's expected set. That is the failure mode this epic exists to remove, and it was hiding in the listing itself. Now the listing states how many skills are hidden, and the host logs a warning with listed/hidden/total. This does NOT make a large catalog usable -- that needs `skill_search` (#4428), and a 227-skill listing costs ~9k tokens of prompt besides. What it does is turn a silent, invisible failure into a stated one, so a benchmark or a user hitting the cap finds out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(skills): make "the model decides" reachable, and list every skill instead of the first 100 Two defects found by measuring this PR rather than reviewing it. Both made a claim the PR already made untrue in practice. **1. The ExplicitOnly default was dead code on the Reborn path.** `skill_activation_selector_config` pinned `ExplicitAndCriteria` at the call site, so changing the default in `activation.rs` could not affect any real Reborn user. The model-decides change looked like a behaviour change and was not one. No test in `ironclaw_first_party_extension_ports` could catch this, because those construct their own config; only a test on the value the composition layer actually returns can. `reborn_skill_selection_is_model_decided` is that test, and it fails if the mode is re-pinned. **2. The skill listing silently dropped everything past 100 skills.** `MAX_LISTED_SKILLS = 100` with source-then-name ordering meant whole alphabetical tails were rendered nowhere and logged nowhere. Measured on a 227-skill catalog: `pdf`, `pptx`, `xlsx` and `timeseries-detrending` all sorted past the cap, and three of the first four benchmark tasks could not reach their own expected skill. A skill the model cannot see is one it cannot activate, so this is indistinguishable from never having installed it — and with the scorer retired, the listing IS the routing interface, which makes its completeness a correctness property. The flat count cap becomes a character budget spent differently: every skill's name is listed, with per-entry descriptions shrinking as the catalog grows (250 chars at small sizes, 90 at 227), and entries are dropped only when even 60 chars will not fit — roughly past 380 skills. **This is not a context-size increase.** The budget is exactly what the old cap already permitted (`100 * (250 + 64)`). What changes is that it buys reachability for all skills rather than verbosity for the alphabetically lucky first hundred. When truncation does happen it is stated in the listing and warned with `listed`/`hidden`/`total`. `the_listing_stays_within_budget_at_two_hundred_skills` now asserts both that the listing fits its budget *and* that all 200 skills appear in it. The second assertion is the one the old cap violated: that test previously passed on budget alone while hiding half the catalog, which is how this survived. Beyond ~380 skills the answer is `skill_search` (#4428), not a bigger prompt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(skills): raise the per-root bundle scan cap so a real catalog enumerates whole Found by measuring: after replacing the listing's 100-skill cap with a character budget, a 227-skill root STILL only reached the model as 100 skills. The listing was never the binding limit. `DEFAULT_MAX_BUNDLES_PER_ROOT = 100` truncates one layer earlier, at enumeration: `skill bundle root exceeds the per-root scan limit ... limit=100 skipped=127`. The 127 skipped bundles were invisible to the selector, to the listing, and to the model — the same outcome as never installing them. Two caps, and only the lower one decides, so raising the listing budget alone accomplished nothing. Raised to 512. The cap exists to bound an unbounded directory walk, not to bound a catalog; 512 keeps that protection (a bundle is one directory read plus a manifest parse, cached per root) while leaving real catalogs whole. Past it, truncation is still partial-and-warned rather than fatal, and the answer is `skill_search` (#4428) rather than a larger number. `a_two_hundred_and_twenty_seven_skill_root_enumerates_whole` asserts the default cap does not truncate a real catalog. The existing test only proved truncation *degrades gracefully*, which is why a default too low to fit anything real passed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(skills): let the scorer rank the listing, while the model alone decides Retiring the criteria scorer from ACTIVATION is right and stays. Retiring it from ORDERING was a mistake, and the benchmark caught it. Measured on the 31-task routing benchmark, 88 candidates, same model, paired: | | criteria on | ExplicitOnly | |--------------------------|-------------|--------------| | >=1 correct skill | 57.1% | 28.6% | | recall over expected set | 42.9% | 25.0% | | correct skill REQUESTED | 75.0% | 35.7% | | never called skill_activate | 25.0% | 57.1% | The model was not being refused -- refusals were 0% in both arms. It stopped asking. The scorer was the only thing making a long listing legible: with it off, the listing collapses to source-then-name alphabetical order, and the relevant skill sits among dozens of equally-weighted lines with nothing marking it. At 227 candidates this is worse, not better. So the scorer keeps its useful job and loses its harmful one. Under `ExplicitOnly` the prefilter still runs, and its output populates `SkillActivationSelection::ranking_only`, which feeds `criteria_ranked_bundle_ids` and therefore listing order ONLY. It never appends to `activations`, and it deliberately does not extend `feedback` (those notes explain activation decisions, and nothing was activated). This is the distinction @serrrfirat's objection actually draws. "Don't statically regex match skill names to skill activation" is not "don't use scoring to rank what the model is shown". Ordering a menu is not choosing from it. The host recommends; the model decides; a wrong recommendation costs a listing line rather than the skill budget. `explicit_only_ranks_the_listing_without_activating_anything` pins both halves in one test, because they pull in opposite directions: the matched skill must LEAD the listing, and no body may be disclosed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Revert "fix(skills): let the scorer rank the listing, while the model alone decides" This reverts |
||
|
|
6384fa6c02 |
ci(nightly): grant actions: read to the reborn-tests call contract (#7323)
reborn-tests.yml's coverage-report job requests job-level `actions:
read` since #7018 (it fetches the base commit's merged-lcov artifact for
the changed-line coverage gate). GitHub validates called-workflow
permissions at trigger time, and the nightly caller grants only
`contents: read` + `pull-requests: write` — so every scheduled run
since 2026-08-03 (first run at
|
||
|
|
c7ec378f4e |
chore(agents): refresh codebase knowledge graph (#7399)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> |
||
|
|
0b5fcde996 |
feat!: a run acts as its invoker — remove shared-route subject binding (#7157 follow-ups) (#7377)
* feat: explicit channel delivery tool — two lanes, notification channels, delivery heuristics deleted Re-landed PR #7157 on current main ( |
||
|
|
30ae2d50f6 |
feat(stress): scripted tool-call workload with durable write read-back (#7360) (#7382)
* feat(stress): scripted tool-call workload with durable write read-back (#7360) Phase 1 of issue #7360: teach the stress harness to drive real builtin and memory tool calls through the production capability path and verify their durable side effects. The api-user-capacity mock LLM sidecar learns a deterministic scripted state machine: the driver embeds an `ironclaw-stress-tool` marker in the user message, the sidecar emits the scripted tool call for a tool advertised in the request, the server executes it through the real capability host, and the driver verifies the read-back verdict in the final assistant message. Verdicts: confirmed / contended (same-user CAS race, counted) / leak (cross-user isolation, hard failure) / missing (write lost, hard failure) / undisclosed (tool never advertised). Scripts: write_file_roundtrip (write_file + read_file of a unique workspace path), memory_roundtrip / memory_grow / memory_mixed (ironclaw.memory.write/replace-append + read of the shared stress/shared.md target — every run doubles as a same-relative-path isolation check). --api-scripted-doc-sizes cycles 4 KiB..1 MiB documents with per-size buckets and submit-to-tool-visible / submit-to-finalize stage latencies; --api-hot-writers spawns concurrent same-user writers for hot-document CAS contention. Gated tools are exercised through the per-user Tools auto-approve setting enabled during setup via the production settings API. Wired as a nightly leg in the hosted-single-tenant Postgres job (the existing server stays up; the leg rebinds the mock sidecar on the same port). Unit coverage: marker parsing, per-op step sequencing, tool-name resolution (encoded/dotted/bare), verdict computation incl. leak precedence, disclosure fallback, timeline helpers, per-size summary buckets, and flag validation. * fix(stress): hot writers on distinct user threads, size floor, CI server lifecycle Review fixes for the #7360 Phase 1 scripted workload: - Hot writers now run on distinct threads of the first user instead of sharing one thread, so concurrent operations exercise real per-user memory-document CAS contention rather than per-thread turn serialization. setup_users creates and records one extra thread per hot writer for user 0; run_hot_writer picks its own thread. - Scripted document sizes are floored at 4 KiB (the token-dominated region below is meaningless and the issue's workloads start there); enforced in marker parsing and --api-scripted-doc-sizes validation. - The CI scripted leg runs inside the server's run block so the trap does not kill the server before it starts; artifacts upload together. - Wire-shape tests: mock_tool_call_response deserializes as the rig OpenAI CompletionResponse (stringified arguments, finish_reason tool_calls) and streaming tool-call chunks carry indexed delta tool_calls. * fix(stress): hot-writer client action ids collide with the primary writer A hot writer and the first user's regular writer shared the same user label and operation index, so their client_action_id values were identical and the server rejected the second submit with a 409 duplicate conflict. Include the scripted op prefix (h{k}-) in the operation ref so concurrent writers always submit distinct action ids. Found by a full local E2E run of the scripted leg against a real hosted-single-tenant server: after the fix, memory_roundtrip with one hot writer runs 9/9 clean (6 confirmed + 3 contended, 0 leaks) and memory_grow runs 8/8 confirmed. * fix(stress): address coderabbit review — verdict integrity, op-scoped tool counts, typed script key (#7382) - compute_verdict: verdict comes from read steps only (write echoes can no longer mask missing/contended) - timeline tool evidence: count tool results by sequence above the op's baseline instead of subtracting page-limited absolute counts - timeline verdict match: delimit prefix by trailing space so op 1 cannot terminate on op 10's message; parse_result_verdict aligns on substring - background users namespace markers as b{index} so cross-cohort leaks cannot read back as their own token - hot writers drain in a dedicated JoinSet (no run_virtual_user refills) - fraction chunks derive from cumulative boundaries so split writes persist exactly the configured size (regression test at 4097) - ScriptKey derives clap::ValueEnum: CLI, marker wire format, and parsing share one string mapping; --api-hot-writers rejects write_file_roundtrip - parse_marker bounds identity grammar; poisoned mutex recovery; sorted stage latencies; single conversation parse per completion request; CLI-level scripted validation test and doc-size bound coverage * test(stress): cover --api-wait-for-assistant gate in CLI-level scripted test (#7382) |