fix: fix i18n readme - #2
Merged
Merged
Conversation
This was referenced May 9, 2026
ericleepi314
pushed a commit
that referenced
this pull request
May 9, 2026
…registrars + Stop→SubagentStop conversion Phase 3 of ch12 extensibility refactor. Closes gap #4 (no session-hook API) and gap #11's registration-time half (skill-frontmatter Stop→ SubagentStop conversion). Brings the chapter's "skill with `once: true` PreToolUse" worked example end-to-end live for the first time. A9 rename: src/hooks/session_hooks.py → src/hooks/lifecycle_routers.py (the lifecycle router functions — run_session_start_hooks, etc. — keep their public signatures at the new path). The freed name is now used for the Phase-3 registration API. WI-3.1 — SessionHookRegistry. New src/hooks/session_hooks.py with: * SessionHookEntry dataclass (config + event + matcher + on_success callback + source_session_id). * SessionHookRegistry — async API with asyncio.Lock per N2. Mutator contract per A10: bodies are sync (just dict mutation); only the lock acquisition is async. * Module-level helpers: add_session_hook, remove_session_hook, get_session_hooks, clear_session_hooks. Mirror TS sessionHooks.ts. * `once: true` removal scheduled via asyncio.create_task in the on_success callback — fire-and-forget so the executor's main loop doesn't await the lock acquisition. ToolContext gains session_hook_registry: Any | None field. WI-3.2 — register_skill_hooks. Forwards skill frontmatter ``hooks`` verbatim (no Stop→SubagentStop conversion per B1). Each registered hook is tagged with HookSource.SESSION_HOOK; ``once: true`` hooks are wired with on_success removers that schedule remove_session_hook via asyncio.create_task on the running loop. WI-3.3 — register_frontmatter_hooks. The general entry point. **This is the home of the Stop→SubagentStop conversion** (B1 correction — register_skill_hooks does NOT do it). Conversion gated on ``is_agent: bool`` parameter; mirrors registerFrontmatterHooks.ts:37-45. Wire-in: _run_markdown_skill calls register_skill_hooks when the skill has frontmatter hooks AND a registry is mounted on context. The function stays sync (the I3-async migration is deferred — 30+ existing sync skill tests bypass the dispatcher and call SkillTool.call directly, keeping it sync minimizes churn). Async registration is fire-and-forget via _schedule_skill_hook_registration: loop.create_task on the running loop, asyncio.run fallback when no loop. Failures logged at ERROR; never break skill invocation. The fire-and-forget approach sidesteps I3's "asyncio.run inside running loop raises RuntimeError" without requiring the async migration — the registration completes in microseconds while the model's next tool call is seconds away. I2 contract — Phase 3 owns Collect: new _collect_hooks_for_event in hook_executor.py merges snapshot + session-registry entries, applies trust gate and matcher filter, returns list[SessionHookEntry] for the executor to drive. Phase 4 will own Aggregate (deny>ask>allow precedence across results); the per-hook yield shape stays unchanged for now. When a hook fires successfully (exit 0, no blocking_error, no permission deny), the executor calls entry.on_success() — for once=True hooks this schedules removal via asyncio.create_task. on_success is None for snapshot-tier hooks (once=True is session-hook-only per chapter). Tests: 25 new across 4 files. test_session_hook_registry.py — add/get/remove/clear, session isolation, skill_root propagation, concurrent-add and concurrent-remove safety under asyncio.Lock. test_register_skill_hooks.py — basic registration, source tagging, on_success wiring for once=True, **and the B1 regression test: Stop hook in skill frontmatter does NOT become SubagentStop**. test_register_frontmatter_hooks.py — Stop→SubagentStop conversion only fires when is_agent=True; SubagentStop itself stays SubagentStop in both modes; non-Stop events untouched. test_skill_once_true_e2e.py — the headline acceptance gate. Chapter example #2 end-to-end: register → first PreToolUse fires → asyncio.create_task removal lands → second PreToolUse finds nothing. Plus test_once_true_does_not_remove_on_blocking_error (audit hooks don't auto-uninstall on detected violations) and a once-true race regression with two concurrent firings. Verification (per S3 baseline-diff discipline): Phase 2 (e72b92f) failures saved at /tmp/phase2_failures.txt Phase 3 (this) failures saved at /tmp/phase3_failures.txt diff returns empty → identical 27-failure set, matching Phase 0 baseline (62a307c) exactly. Sweep counts: 3789 passed, 27 failed (Phase 2 was 3764 passed; +25 new Phase 3 tests). Same 27 pre-existing API-key env failures; zero regressions. Hook surface: 261/261 (Phase 2 was 209; +52 covers the new session-registry, registrars, and once-true E2E paths). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ericleepi314
pushed a commit
that referenced
this pull request
May 9, 2026
…registrars + Stop→SubagentStop conversion Phase 3 of ch12 extensibility refactor. Closes gap #4 (no session-hook API) and gap #11's registration-time half (skill-frontmatter Stop→ SubagentStop conversion). Brings the chapter's "skill with `once: true` PreToolUse" worked example end-to-end live for the first time. A9 rename: src/hooks/session_hooks.py → src/hooks/lifecycle_routers.py (the lifecycle router functions — run_session_start_hooks, etc. — keep their public signatures at the new path). The freed name is now used for the Phase-3 registration API. WI-3.1 — SessionHookRegistry. New src/hooks/session_hooks.py with: * SessionHookEntry dataclass (config + event + matcher + on_success callback + source_session_id). * SessionHookRegistry — async API with asyncio.Lock per N2. Mutator contract per A10: bodies are sync (just dict mutation); only the lock acquisition is async. * Module-level helpers: add_session_hook, remove_session_hook, get_session_hooks, clear_session_hooks. Mirror TS sessionHooks.ts. * `once: true` removal scheduled via asyncio.create_task in the on_success callback — fire-and-forget so the executor's main loop doesn't await the lock acquisition. ToolContext gains session_hook_registry: Any | None field. WI-3.2 — register_skill_hooks. Forwards skill frontmatter ``hooks`` verbatim (no Stop→SubagentStop conversion per B1). Each registered hook is tagged with HookSource.SESSION_HOOK; ``once: true`` hooks are wired with on_success removers that schedule remove_session_hook via asyncio.create_task on the running loop. WI-3.3 — register_frontmatter_hooks. The general entry point. **This is the home of the Stop→SubagentStop conversion** (B1 correction — register_skill_hooks does NOT do it). Conversion gated on ``is_agent: bool`` parameter; mirrors registerFrontmatterHooks.ts:37-45. Wire-in: _run_markdown_skill calls register_skill_hooks when the skill has frontmatter hooks AND a registry is mounted on context. The function stays sync (the I3-async migration is deferred — 30+ existing sync skill tests bypass the dispatcher and call SkillTool.call directly, keeping it sync minimizes churn). Async registration is fire-and-forget via _schedule_skill_hook_registration: loop.create_task on the running loop, asyncio.run fallback when no loop. Failures logged at ERROR; never break skill invocation. The fire-and-forget approach sidesteps I3's "asyncio.run inside running loop raises RuntimeError" without requiring the async migration — the registration completes in microseconds while the model's next tool call is seconds away. I2 contract — Phase 3 owns Collect: new _collect_hooks_for_event in hook_executor.py merges snapshot + session-registry entries, applies trust gate and matcher filter, returns list[SessionHookEntry] for the executor to drive. Phase 4 will own Aggregate (deny>ask>allow precedence across results); the per-hook yield shape stays unchanged for now. When a hook fires successfully (exit 0, no blocking_error, no permission deny), the executor calls entry.on_success() — for once=True hooks this schedules removal via asyncio.create_task. on_success is None for snapshot-tier hooks (once=True is session-hook-only per chapter). Tests: 25 new across 4 files. test_session_hook_registry.py — add/get/remove/clear, session isolation, skill_root propagation, concurrent-add and concurrent-remove safety under asyncio.Lock. test_register_skill_hooks.py — basic registration, source tagging, on_success wiring for once=True, **and the B1 regression test: Stop hook in skill frontmatter does NOT become SubagentStop**. test_register_frontmatter_hooks.py — Stop→SubagentStop conversion only fires when is_agent=True; SubagentStop itself stays SubagentStop in both modes; non-Stop events untouched. test_skill_once_true_e2e.py — the headline acceptance gate. Chapter example #2 end-to-end: register → first PreToolUse fires → asyncio.create_task removal lands → second PreToolUse finds nothing. Plus test_once_true_does_not_remove_on_blocking_error (audit hooks don't auto-uninstall on detected violations) and a once-true race regression with two concurrent firings. Verification (per S3 baseline-diff discipline): Phase 2 (e72b92f) failures saved at /tmp/phase2_failures.txt Phase 3 (this) failures saved at /tmp/phase3_failures.txt diff returns empty → identical 27-failure set, matching Phase 0 baseline (62a307c) exactly. Sweep counts: 3789 passed, 27 failed (Phase 2 was 3764 passed; +25 new Phase 3 tests). Same 27 pre-existing API-key env failures; zero regressions. Hook surface: 261/261 (Phase 2 was 209; +52 covers the new session-registry, registrars, and once-true E2E paths). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 10, 2026
ericleepi314
added a commit
that referenced
this pull request
May 11, 2026
ch03 state #2: bootstrap state core expansion
This was referenced May 24, 2026
peroxider
pushed a commit
to peroxider/clawcodex
that referenced
this pull request
Jun 15, 2026
…14#1, agentforce314#2) - /workflows modal (src/tui/screens/workflow_dialog.py): a list of runs and a per-run phases->agents detail view; `x` stops a run/agent, `r` retries an agent (wired to kill/skip/retry_workflow_agent). Pure render helper format_workflow_detail is unit-tested; the screens via the Textual pilot. - /workflows command wired into the TUI (LOCAL_BUILTINS + app opener). - StatusLine shows "N background workflows" from runtime_tasks (live pill). Remaining: `p` (pause) / `s` (save) affordances — see port-plan §10. Full workflow + TUI suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
peroxider
pushed a commit
to peroxider/clawcodex
that referenced
this pull request
Jun 15, 2026
feat(f37): PR Review 自动修复闭环 — review_feedback 采集、follow-up、幂等、回复 Created-by: qq_49552963 Commit-by: wukong;qq_49552963 Merged-by: chadwweng Description: feat(f37): PR Review 自动修复闭环 — review_feedback 采集、follow-up、幂等、回复 测试配置 # workflow-click-f37.yaml --- version: "1" tracker: kind: gitcode owner: qq_49552963 repo: click api_url: https://gitcode.com/api/v5 active_states: [open, progressing] agent: provider: deepseek model: deepseek-v4-pro max_turns: 15 permission_mode: bypassPermissions review_feedback: enabled: true mode: auto max_followup_attempts_per_pr: 10 max_feedback_items_per_run: 2 ignore_authors: [] bot_login: clawcodex-bot reply_to_comments: true include_ci_failures: false workspace: root: /tmp/click-f37-workspaces repo_clone_url: https://user:token@gitcode.com/qq_49552963/click.git polling: interval_seconds: 30 --- Scene 1: Issue → Agent → PR 创建 Step 1: 创建 Issue agentforce314#2 并添加 progressing label   Step 2: Agent 运行 orchestrator 检测到 Issue agentforce314#2 → 启动 DeepSeek agent → 3 turns / 21 tool calls # orchestrator 日志 [dashboard] session start: agentforce314#2 (2) Starting agent run issue_id=2 identifier=agentforce314#2 Agent run completed issue_id=2 turns=3/15 tools=21 Step 3: 创建分支并推送 自动创建 clawcodex/2-add-type-hints-to-click.testing-module 分支并 push:  Step 4: 创建 PR agentforce314#2  Scene 2: Review Comment → Follow-up Commit Step 1: 发布 review 评论 评论 ID 175732234: "Please add a CONTRIBUTING.md file to the repository root..."  Step 2: Feedback 检测 _filter_pending() 识别为新 feedback → mark_feedback_pending() 标记 Step 3: Follow-up Agent 运行 1 turn / 16 tool calls — Agent 分析评论并修改代码 Step 4: 追加 Commit + 推送   Step 5: 自动回复 "Handled"  Step 6: Follow-up Run Summary  Step 7: Follow-up Feedback List  Scene 3: 幂等性验证 重启 orchestrator → 重新加 registry → 已处理评论 175732234 在 processed_feedback_ids 中 → 不再触发 Scene 4: 第二轮 Follow-up 第二条 review 评论 175741951 触发第二次 follow-up。 followup_attempt_count: 1 → 2 processed_feedback_ids 同时包含两条 ID   Scene 5: max_followup_attempts_per_pr 上限 配置 max=2,followup_attempt_count=2 后发第三条评论 175742570。 ✅ can_follow_up() 返回 False ✅ 日志只有 GET /issues 轮询,无 feedback fetch # cap 生效后的轮询 GET /issues?state=open&labels=progressing → 200 OK # 仅 issue 轮询,无 agentforce314/issues/2/comments → cap 阻止 ✅ Scene 6: ignore_authors A/B 对照 Phase 配置 同一条评论 175742570 结果 A ignore_authors: [qq_49552963] 被 _filter_pending() 过滤 跳过 ✅ B ignore_authors: [] 被拾取并触发 follow-up Agent 运行 2 turns / 29 tools ✅ Scene 7: 多 PR 并发隔离 Step 1: Issue agentforce314#4 → PR agentforce314#4 创建     Step 2: 同时发评论 Issue agentforce314#2: 175746587 ("[Multi-PR Test] For Issue agentforce314#2 PR...") Issue agentforce314#4: 175746589 ("[Multi-PR Test] For Issue agentforce314#4 PR...") Step 3: 同一轮询周期并发触发 2026-06-15 17:13:05,051 [dashboard] session start: agentforce314#2 (2) 2026-06-15 17:13:07,206 [dashboard] session start: agentforce314#4 (4) ← 仅隔 2 秒 2026-06-15 17:14:07,377 Agent run completed issue_id=2 turns=1/15 tools=21 2026-06-15 17:15:48,924 Agent run completed issue_id=4 turns=1/15 tools=26 Step 4: Issue agentforce314#4 Follow-up Summary   反馈不交叉 PR agentforce314#2 processed_feedback_ids 含 175746587,不含 175746589 PR agentforce314#4 processed_feedback_ids 含 175746589,不含 175746587 Scene 8: GitCode Inline PR Code Comment  步骤 结果 创建 inline PR comment Comment ID: 175749629, path: CONTRIBUTING.md F-37 拾取 inline_review:175749629 出现在 pending Follow-up Summary [inline_review] inline_review:175749629: [Inline Test]... ✅ GitCode 的 /pulls/X/comments API 支持 inline code comments,F-37 正确识别 inline_review source。 Scene 9: Batch + max_feedback_items_per_run 截断 配置 max_feedback_items_per_run: 2,一次性在 Issue agentforce314#4 发 3 条评论。 第一次 follow-up 处理 2 条 → processed: [175754829, 175754826] 第 3 条留在 pending_feedback_ids: [175754820] ✅ 截断生效 同时在 Issue agentforce314#2 上之前卡住的 3 条 pending 评论也在并行 follow-up 中消费了 2 条。 See merge request: chadwweng/clawcodex!6
peroxider
pushed a commit
to peroxider/clawcodex
that referenced
this pull request
Jun 16, 2026
fix(orchestrator): F-37 pending_feedback 超时自动清理 Created-by: qq_49552963 Commit-by: 叶蕴瑶;wukong;qq_49552963 Merged-by: chadwweng Description: ## 问题描述 当 `ReviewFeedbackService.collect_followups()` 在单次轮询中检测到 N 条待处理的 review feedback 时,受 `max_feedback_items_per_run` 限制仅选取前 M 条(M < N)提交给 Agent 处理。被选中的 M 条与未选中的 (N-M) 条均被写入 `pending_feedback_ids`。 Agent 运行结束后,`mark_feedback_processed()` 将已处理的 M 条迁移至 `processed_feedback_ids`,但剩余 (N-M) 条仍驻留在 `pending_feedback_ids` 中。在后续轮询中,`_filter_pending()` 会跳过所有已存在于 `pending_feedback_ids` 的条目,导致这些未处理的 feedback **永久滞留**——既无法被重新检测,也不会被标记为已处理。 **影响范围**:当单次轮询的 feedback 数量超过 `max_feedback_items_per_run` 阈值,或 Agent 运行异常仅消费部分 feedback 时,未处理的 review 评论将被静默丢弃,不会产生任何日志告警。 ## 修复方案 引入基于时间戳的超时清理机制,在每轮 `collect_followups` 执行前检测并清理滞留的 pending 条目,使其重新进入检测流程。 ### 改动文件 | 文件 | 改动说明 | |------|----------| | `extensions/orchestrator/issue_registry.py` | 新增 `pending_feedback_since` 字段记录 pending 队列首次非空的时间戳;新增 `clear_stale_pending()` 方法,当滞留时间超过阈值时清空 pending 队列并重置时间戳 | | `extensions/orchestrator/review_feedback.py` | 在 `collect_followups` 循环体开头调用 `clear_stale_pending()`,超时条目被清理后可被 `_filter_pending()` 重新识别 | | `extensions/orchestrator/config/schema.py` | `ReviewFeedbackConfig` 新增 `pending_feedback_timeout_seconds` 配置项(默认 600 秒),支持通过 workflow YAML 自定义超时阈值 | ### 设计要点 - **不影响正常流程**:正常 Agent 运行在数分钟内即完成 `mark_feedback_processed` 调用,远早于默认 600 秒超时 - **向后兼容**:旧版 registry JSON 无 `pending_feedback_since` 字段时默认为 `None`,`clear_stale_pending()` 检测到 `None` 直接返回 0,不影响存量数据 - **幂等安全**:多次调用不会产生副作用,空队列直接短路返回 ## 验证 在 h144 测试环境(gitcode.com/qq_49552963/click 仓库)设置 `pending_feedback_timeout_seconds: 60`,对真实 registry 数据执行验证: - Issue agentforce314#2: `conversation:175749937` 滞留 3125 秒 → 超过 60 秒阈值 → 成功清理并触发重新检测 - Issue agentforce314#4: `pending_feedback_since=None`(历史数据无时间戳)→ 正确跳过,不误清 验证 (timeout=60s): 19:43:31,625 INFO Cleared 1 stale pending feedback item(s) for issue 2 — will re-detect 19:43:35,186 INFO [dashboard] session start: agentforce314#2 (2) 2 分钟前的 pending 条目 → poll 时自动清掉 → 重新检测 → 触发 follow-up ✅ h144 实际运行输出 (2026-06-15 20:35, 对 /tmp/click-f37-workspaces/.clawcodex_issue_registry.json 执行 clear_stale_pending): Issue agentforce314#2: conversation:175749937 卡了 3125s → 超过 60s timeout → 已清理 Issue agentforce314#4: pending_feedback_since=None(旧代码未设时间戳)→ 正确跳过 See merge request: chadwweng/clawcodex!11
peroxider
pushed a commit
to peroxider/clawcodex
that referenced
this pull request
Jun 23, 2026
…+ P108-B) Layer 0 quick fix for the 3 highest-severity freeze risk points identified in F-108 §十八 audit: * agentforce314#2 TUI Permission modal hang (CRITICAL) * agentforce314#3 TUI AskUserQuestion modal hang (CRITICAL) * agentforce314#5 headless query future never completes (HIGH) - clawcodex_ext/tui/agent_bridge.py: bind _permission_handler / _ask_user_handler waits to _PERMISSION_TIMEOUT_S (30s). On timeout Permission auto-denies (False, False) and AskUser returns {}, both matching existing Esc-cancel semantics. timeout=0 preserves the legacy unbounded wait (F-108 design decision agentforce314#5). - extensions/api/query.py: move the wall-clock budget inside the polling loop. asyncio.wait_for cannot cancel executor futures, so the budget check fires from loop_started_at and surfaces SessionComplete(reason="exit_code=124") + a query_runner.timeout NDJSON event for postmortem. 0 src/ files modified (Decoupling Mandate golden rule agentforce314#1). 12 new unit tests; 483 orchestrator regression tests unaffected. Co-Authored-By: MiniMax-M3 <MiniMax-AI@claude-code-best.win>
peroxider
pushed a commit
to peroxider/clawcodex
that referenced
this pull request
Jun 26, 2026
Migrate src/transports/remote_io.py (13174B) to clawcodex_ext/transports/remote_io.py with a 9-line sys.modules facade, eliminating 1 b_internal (src->src) ref. Importers rerouted: - src/transports/__init__.py (RemoteIO) Internal imports remain src.* (allowed per Golden Rule agentforce314#2): - src.cli_core.ndjson (already facaded) for ndjson_safe_dumps - src.utils.session_ingress_auth for auth headers/token Pattern unchanged: impl -> clawcodex_ext/<path>.py, src/<path>.py -> 9-line sys.modules facade, src importers -> `from clawcodex_ext.<path>`. Verified: 262/262 stability gate stages 1-5 PASS, identity check (src.X is clawcodex_ext.X) green, src.transports aggregator re-export still resolves. Co-Authored-By: MiniMax-M3 <MiniMax-AI@claude-code-best.win>
peroxider
pushed a commit
to peroxider/clawcodex
that referenced
this pull request
Jun 26, 2026
Migrate 5 remaining src.bridge.* leaf modules to clawcodex_ext with sys.modules facades, eliminating 14 b_internal (src->src) refs from src/bridge/__init__.py. Targets (4 pure leaves + 1 with allowed src.* deps): - bridge_config (2872B) — 4 auth/URL accessors - bridge_permission_callbacks (3558B) — TypedDict + type guard + Protocol - capacity_wake (4654B) — CapacityWake / CapacitySignal / create_capacity_wake - env_less_bridge_config (6053B) — Pydantic config model + defaults - repl_bridge_handle (3819B) — process-global REPL bridge handle pointer (keeps `from src.bridge.types import ReplBridgeHandle` per Golden Rule agentforce314#2) Importers rerouted: - src/bridge/__init__.py (all 14 refs) Pattern unchanged: impl -> clawcodex_ext/<path>.py, src/<path>.py -> 9-line sys.modules facade, src importers -> `from clawcodex_ext.<path>`. Verified: 262/262 stability gate stages 1-5 PASS, all 5 facade identity checks green, capacity_wake short-circuit path works, aggregator re-exports resolve. Co-Authored-By: MiniMax-M3 <MiniMax-AI@claude-code-best.win>
peroxider
pushed a commit
to peroxider/clawcodex
that referenced
this pull request
Jun 26, 2026
…atch H) Migrate 2 coordinator leaf modules to clawcodex_ext with sys.modules facades, eliminating 8 b_internal (src->src) refs from src/coordinator/__init__.py. Targets: - coordinator/mode.py (8991B) — is_coordinator_mode, match_session_mode, INTERNAL_WORKER_TOOLS, filter_coordinator_tools, filter_worker_tools, get_coordinator_user_context. Keeps TYPE_CHECKING import of src.tool_system.build_tool:Tool per Golden Rule agentforce314#2. - coordinator/worker_agent.py (2099B) — WORKER_AGENT, get_coordinator_agents Importers rerouted: - src/coordinator/__init__.py (all 8 refs) Pattern unchanged: impl -> clawcodex_ext/<path>.py, src/<path>.py -> 9-line sys.modules facade, src importers -> `from clawcodex_ext.<path>`. Verified: 262/262 stability gate stages 1-5 PASS, identity checks (src.X is clawcodex_ext.X) green, coordinator aggregator re-exports resolve, WORKER_AGENT.agent_type == 'worker'. Co-Authored-By: MiniMax-M3 <MiniMax-AI@claude-code-best.win>
ericleepi314
added a commit
that referenced
this pull request
Jul 3, 2026
The round-4 LLM memory-relevance recall (#594) ships default-off because flipping it on is a latency+cost+correctness regression. This fixes the four blockers the round-4 critic flagged. Still default-off — zero behavior change on merge. - #2 selector cost (provider-gated pin): the recall side-query ran on the SESSION model (an Opus turn paid full price every turn). TS pins it to a small default (getDefaultSonnetModel). The port wires the small_fast_model setting, but its shipped default is an Anthropic id (claude-3-5-haiku-20241022) valid only on the first-party Anthropic endpoint. So _resolve_recall_model(provider) applies the pin ONLY when the session provider is a first-party AnthropicProvider; Minimax (Anthropic SDK, own endpoint), DeepSeek, OpenAI, OpenRouter fall back to the session model. Without the gate the Anthropic default id 400s on a non-Anthropic endpoint and — since recall swallows errors — silently kills recall every turn (critic M1). small_fast_model_provider pairing for non-Anthropic cheap-recall is deferred. - #3 de-dup reset on compaction: the recall reminder is ephemeral but _memory_surfaced was monotonic — once surfaced, a memory was never re-surfaced. After /compact drops the earlier context, the memory stayed suppressed. _do_compact now clears _memory_surfaced on success (idle-gated — no race with the turn worker). - #4 aggregate turn byte-cap: per-file was 4 KB but 5 selections could inject ~20 KB/turn. build_relevant_memory_reminder_with_paths caps at MAX_TOTAL_SURFACE_CHARS=12 KB and returns the paths ACTUALLY surfaced; get_relevant_memory_reminder de-dups only those, so a cap-trimmed memory stays eligible (the old mark-all was a latent suppress-forever bug the cap would have activated). Back-compat string wrapper retained. - #5 docstring: _maybe_recall_memories said "prepend"; the code appends after the user turn (correct — normalize_messages merges consecutive user messages). Fixed. Deferred #1 (synchronous side-query): the port has only this one prefetch, so there is no concurrent prefetch to overlap without a larger prompt-submit refactor — one small-model call per turn (fast Haiku on the default path). Documented. Docs: my-docs/port-improvement-round-5/r5-1-ch11-memory-enable-blockers.md + critic verdicts (REVISE->APPROVE). Tests: tests/test_r5_ch11_memory_enable_blockers.py (8) incl. the M1 regression guard (non-Anthropic never pins even when the Anthropic-default id is set). Existing memory suite (466) green via the back-compat wrapper. Full suite at the pre-existing baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 3, 2026
The round-4 LLM memory-relevance recall (#594) ships default-off because flipping it on is a latency+cost+correctness regression. This fixes the four blockers the round-4 critic flagged. Still default-off — zero behavior change on merge. - #2 selector cost (provider-gated pin): the recall side-query ran on the SESSION model (an Opus turn paid full price every turn). TS pins it to a small default (getDefaultSonnetModel). The port wires the small_fast_model setting, but its shipped default is an Anthropic id (claude-3-5-haiku-20241022) valid only on the first-party Anthropic endpoint. So _resolve_recall_model(provider) applies the pin ONLY when the session provider is a first-party AnthropicProvider; Minimax (Anthropic SDK, own endpoint), DeepSeek, OpenAI, OpenRouter fall back to the session model. Without the gate the Anthropic default id 400s on a non-Anthropic endpoint and — since recall swallows errors — silently kills recall every turn (critic M1). small_fast_model_provider pairing for non-Anthropic cheap-recall is deferred. - #3 de-dup reset on compaction: the recall reminder is ephemeral but _memory_surfaced was monotonic — once surfaced, a memory was never re-surfaced. After /compact drops the earlier context, the memory stayed suppressed. _do_compact now clears _memory_surfaced on success (idle-gated — no race with the turn worker). - #4 aggregate turn byte-cap: per-file was 4 KB but 5 selections could inject ~20 KB/turn. build_relevant_memory_reminder_with_paths caps at MAX_TOTAL_SURFACE_CHARS=12 KB and returns the paths ACTUALLY surfaced; get_relevant_memory_reminder de-dups only those, so a cap-trimmed memory stays eligible (the old mark-all was a latent suppress-forever bug the cap would have activated). Back-compat string wrapper retained. - #5 docstring: _maybe_recall_memories said "prepend"; the code appends after the user turn (correct — normalize_messages merges consecutive user messages). Fixed. Deferred #1 (synchronous side-query): the port has only this one prefetch, so there is no concurrent prefetch to overlap without a larger prompt-submit refactor — one small-model call per turn (fast Haiku on the default path). Documented. Docs: my-docs/port-improvement-round-5/r5-1-ch11-memory-enable-blockers.md + critic verdicts (REVISE->APPROVE). Tests: tests/test_r5_ch11_memory_enable_blockers.py (8) incl. the M1 regression guard (non-Anthropic never pins even when the Anthropic-default id is set). Existing memory suite (466) green via the back-compat wrapper. Full suite at the pre-existing baseline. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 3, 2026
LocalAgentTaskState.abort_event was declared (local_agent.py:77) but NEVER
assigned — only read — so kill_async_agent's abort_event.set() was guarded
out and killing a background Agent only flipped the registry status to
"killed" while the live run kept going to NATURAL completion (burning model
tokens). Directly counter to "run long coding tasks reliably": a user who
stops a runaway background agent expects it to stop.
The async run already had an abort mechanism the port never exposed:
run_agent mints a fresh AbortController for async agents (run_agent.py:291),
wires it into the subagent context, and query() polls signal.aborted at
every yield point (query.py:1433/1520/1571/2046). This makes it REACHABLE:
- LocalAgentTaskState.abort_controller field + register_async_agent param.
- _launch_async_agent mints a dedicated AbortController, stores it on the
task state AND sets run_params.abort_controller, so run_agent uses THIS
one (run_agent.py:286-287) instead of its unreachable internal one. FRESH
(not the parent turn's) → preserves async isolation.
- kill_async_agent calls abort_controller.abort("killed by user") → the
run's query() loop halts at the next poll.
Cross-thread safe: the kill runs on the kill thread, the run polls on the
background loop; AbortController.abort() is a GIL-atomic flag set + sync
listeners and signal.aborted is an atomic read — the same cross-thread
pattern the turn-interrupt path already uses. .abort() is placed outside
the registry lock (matches the abort_event.set() rationale). Legacy
abort_event.set() retained (harmless defense).
Docs: my-docs/port-improvement-round-6/r6-1-kill-background-agent.md +
critic verdict (APPROVE).
Tests: tests/test_r6_kill_background_agent.py (4): unit (kill aborts the
stored controller + flips killed; kill w/o controller is safe); END-TO-END
(a long-running bg agent that polls the abort signal STOPS advancing after a
cross-thread kill — NON-VACUOUS: neg-control advances ~34 steps post-kill
without the fix, <=2 with it) + two ISOLATION assertions (killing the bg
agent doesn't abort the parent; aborting the parent doesn't stop the bg
agent). 1812 agent/async/task tests green; full suite at the 9-failure
baseline.
Deferred (critic minor #2): the resume path must mirror this wire when it
gains a live run loop, else resumed agents are un-killable (benign today —
resume doesn't drive a run).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 3, 2026
LocalAgentTaskState.abort_event was declared (local_agent.py:77) but NEVER
assigned — only read — so kill_async_agent's abort_event.set() was guarded
out and killing a background Agent only flipped the registry status to
"killed" while the live run kept going to NATURAL completion (burning model
tokens). Directly counter to "run long coding tasks reliably": a user who
stops a runaway background agent expects it to stop.
The async run already had an abort mechanism the port never exposed:
run_agent mints a fresh AbortController for async agents (run_agent.py:291),
wires it into the subagent context, and query() polls signal.aborted at
every yield point (query.py:1433/1520/1571/2046). This makes it REACHABLE:
- LocalAgentTaskState.abort_controller field + register_async_agent param.
- _launch_async_agent mints a dedicated AbortController, stores it on the
task state AND sets run_params.abort_controller, so run_agent uses THIS
one (run_agent.py:286-287) instead of its unreachable internal one. FRESH
(not the parent turn's) → preserves async isolation.
- kill_async_agent calls abort_controller.abort("killed by user") → the
run's query() loop halts at the next poll.
Cross-thread safe: the kill runs on the kill thread, the run polls on the
background loop; AbortController.abort() is a GIL-atomic flag set + sync
listeners and signal.aborted is an atomic read — the same cross-thread
pattern the turn-interrupt path already uses. .abort() is placed outside
the registry lock (matches the abort_event.set() rationale). Legacy
abort_event.set() retained (harmless defense).
Docs: my-docs/port-improvement-round-6/r6-1-kill-background-agent.md +
critic verdict (APPROVE).
Tests: tests/test_r6_kill_background_agent.py (4): unit (kill aborts the
stored controller + flips killed; kill w/o controller is safe); END-TO-END
(a long-running bg agent that polls the abort signal STOPS advancing after a
cross-thread kill — NON-VACUOUS: neg-control advances ~34 steps post-kill
without the fix, <=2 with it) + two ISOLATION assertions (killing the bg
agent doesn't abort the parent; aborting the parent doesn't stop the bg
agent). 1812 agent/async/task tests green; full suite at the 9-failure
baseline.
Deferred (critic minor #2): the resume path must mirror this wire when it
gains a live run loop, else resumed agents are un-killable (benign today —
resume doesn't drive a run).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
peroxider
pushed a commit
to peroxider/clawcodex
that referenced
this pull request
Jul 3, 2026
…gressive summarization 修复 /goal 系统四个关键问题: agentforce314#1 — _pending_injection 排空(P0 阻断性 Bug) - repl/app.py: chat() finally 中 drain 注入并 enqueue 到 _queued_prompts - chat() 开始时 drain set_new_goal() 遗留的 objective_updated 注入 - TUI 路径 (tui/screens/repl.py): 在 on_agent_run_finished 中 drain 注入 并追加到 app_state.queued_prompts + 投递 QueuedPromptReady agentforce314#2 — <active-goal> 动态截断(原硬编码 240 字) - _dynamic_objective_chars() 根据 token 预算比例动态调优: >60% → 600, 30-60% → 360, 10-30% → 240, <10% → 160, 无预算 → 480 agentforce314#3 — 压缩管道 goal-aware 优先级 - PipelineConfig.goal_active: bool 字段 - goal_active 时 autocompact 阈值 +10%(上限 0.95) - _is_goal_steering_message() 保护含 <goal-steering 的消息不被压缩 - query/engine.py: _check_goal_active() 查询 GoalStateRegistry agentforce314#4 — 渐进式摘要(Progressive Summarization) - GoalState.milestones: list[dict] + MILESTONE_TURN_INTERVAL=15 - state_machine: add_milestone() 纯函数 + 各转换保留 milestones - controller: on_assistant_turn_complete() 每 15 轮自动记录里程碑 - prompts: continuation_prompt 在里程碑边界注入结构化进度要求 - <active-goal> 上下文块嵌入最近 5 个里程碑的 XML 摘要 Tests: 86/86 goal + 145/145 compact + 68/69 stage5 (1 pre-existing CI circular import) all pass. Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 3, 2026
The expand-result feature (#623) delegated verbose-block truncation to boundedLiveRenderText, which keeps the TAIL with a 'showing live tail' label — correct for the live stream, wrong for a persisted ctrl+o expansion where the reader wants the START of Grep matches / Bash output / an error. The comment had also gone stale (claimed 'NOT the 16KB budget' when the cap is now exactly 16k, and 'expanded by default' when it's behind ctrl+o). - New headTruncate: keeps the first VERBOSE_TRAIL_MAX_LINES/_CHARS with a trailing '… +N lines/chars omitted' marker (no live-tail label). - verboseToolBlock uses it; comment corrected to match the behavior. - boundedLiveRenderText stays for the genuine live-stream tails (streaming assistant text, thinking) — unchanged. Test: a 500-line Grep result keeps line 0-199, drops line 200 and the 499 tail, shows '… +N lines omitted', and no longer says 'showing live tail' — distinguishing head from tail (the old 'A'.repeat test couldn't). Reconciles the plan, the comment, the label, and the code (critic Finding B, required change #2 from the expand-E implementation review — missed in the first fix round, which only closed Finding A). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 4, 2026
…S-2) (#645) * feat(services): autoFix config + hook + runner (SERVICES-2 W1-W3; wiring next) Port of typescript/src/services/autoFix/ — the settings-opt-in feature that runs the user's lint/test command after a file edit and injects <auto_fix_feedback> so the model self-fixes. Python had only the /autofix slash shell; the runtime was absent. - config.py (autoFixConfig.ts): get_auto_fix_config(raw) → AutoFixConfig|None — dict + enabled + at-least-one-of-lint/test (the zod .refine) + numeric bounds (maxRetries 0..10 default 3, timeout 1000..300000 default 30000); None (safeParse-null posture) on any failure. load_auto_fix_config reads settings.extra["autoFix"] (verified: no typed field — lands in extra, like the plugins round's enabledPlugins). - hook.py (autoFixHook.ts): AUTO_FIX_TOOLS = the port's file-mutation registry names (Write/Edit/MultiEdit/NotebookEdit, the file_edit/file_write analog); should_run_auto_fix; build_auto_fix_context (the <auto_fix_feedback> block VERBATIM) + build_max_retries_context (verbatim). - runner.py (autoFixRunner.ts): run_auto_fix_check — lint first, test only if lint passed; each an asyncio.create_subprocess_shell bounded by timeout, killing the process GROUP on timeout (start_new_session + os.killpg, the detached/killTree analog) and reaping to avoid zombies; _build_error_summary verbatim; never-raises (spawn failure → has_errors=False). Verified: config parse/refine/bounds, hook fire + verbatim context, runner lint-fail-skips-test / lint-pass-test-fail / both-clean / timeout-kill (sleep 30 @ 800ms → returns 0.81s, group killed, timed_out) / abort. REMAINING: W4 wiring — a NEW sibling run_auto_fix_step at the orchestrator (tool_execution.py, after run_post_tool_use_hooks), NOT inside that function (it early-returns when no user PostToolUse hooks configured — the teammate-hooks split-gate class; autoFix must run regardless). Retry cap by query_tracking.chain_id. Then tests + suite. Awaiting autofix-critic on the plan's W4 placement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(services): autoFix wiring + all critic findings (SERVICES-2 complete) The autofix-critic caught that TS autoFix is DEAD CODE — AUTO_FIX_TOOLS = {'file_edit','file_write'} never matches the real tool names (Edit/Write, FileEditTool/constants.ts:2 FILE_EDIT_TOOL_NAME='Edit'), verified first-hand. So this port activates the author's INTENT ({Edit,Write}) as a DOCUMENTED, opt-in-gated DIVERGENCE (fixes the reference's dead tool-name set; inert unless a user writes settings.autoFix.enabled). All findings: - B1 (split-gate): run_auto_fix_step is a NEW SIBLING at the orchestrator (tool_execution.py, after run_post_tool_use_hooks) — NOT inside that function, which early-returns when no user PostToolUse hook is configured (the common autoFix case). Pinned by test_fires_with_zero_posttool_hooks. - B2 (accessor): config via get_settings().extra.get("autoFix") (ToolContext has no settings; global read = the per-process equivalent). - M1 (reject-not-clamp): out-of-range/non-int → whole config None (zod .min/.max rejects); .default only when key absent. - M2 (camelCase): reads raw["maxRetries"]/raw["timeout"]. - M3 (reset-on-success): the retry counter is DELETED on a clean run (toolHooks.ts:247), not just incremented on error. - M4 (tool set): {"Edit","Write"} — the author's intent, not the 4-element _FILE_EDIT_TOOLS (NotebookEdit excluded, MultiEdit is a phantom). - M5 (None-guard): chain key handles query_tracking=None → "default". - M6 (10k cap): each stream sliced to 10 000 before combine. - minors: SIGTERM (not SIGKILL) + reap + ProcessLookupError race; mid-flight abort (kills the group, degrades to no-errors); cwd from tool_use_context; the post-hook attachment yield shape (list-wrapped content). tests/services/test_autofix.py (26): config parse/refine/reject/camelCase, hook fire + verbatim contexts + {Edit,Write} intent, runner (lint-skips-test / test-fail / clean / timeout-SIGTERM-kill / 10k cap / pre-set abort / mid-flight abort / no-commands), step (ZERO-hooks regression, non-file skip, retry cap, reset-on-clean, None-query-tracking). Full suite at the 6-failure baseline (7857 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(services): autoFix config zod-strictness (critic minor #1) enabled must be a strict bool (a string "false" is truthy in Python — must not turn autoFix ON against intent); a bad-type lint/test rejects the whole config → None, matching zod z.boolean()/z.string() safeParse (not the prior silent coerce/drop). 3 new tests; consistent with the already-strict int path. (Minors #2 retry-map-lifecycle and #3 proxy-coverage accepted as faithful-to-TS / adequate.) Full suite at the 6-failure baseline (7860). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 6, 2026
* feat(bash): notify on background-bash completion (C5 Part 1 — enqueueShellNotification) TS notifies when a run_in_background bash task reaches a terminal state (utils/task/framework.ts:289 enqueuePendingNotification; BashTool/prompt.ts: 285-287 "you will be notified when it completes — do not poll"). The port marked the terminal state notified=True WITHOUT sending — the documented forward-trap in background.py (ch10 WI-2), so a bg bash finishing while the model was away went unannounced (silently polled via TaskOutput instead). - task_notification.py: enqueue_shell_notification — mirrors enqueue_agent_notification (the atomic check-and-set duplicate-delivery guard + build_task_notification_xml + enqueue_pending_notification) but gated on LocalShellTaskState instead of LocalAgentTaskState. - background.py: REMOVED the forward-trap notified=True from the terminal replace() (per its own comment's instruction), and call enqueue_shell_notification after the terminal update. The eviction sweeper's notify-before-evict guard (eviction.py:97) now correctly keeps the entry until the notification is delivered, then reclaims it — instead of the old mark-notified-without-sending. Best-effort (never breaks reaping); duplicate-safe (a TaskStop that already notified makes it a no-op). tests/test_shell_completion_notification.py (3): enqueues + marks notified on terminal, duplicate-safe (already-notified → no second), wrong-task-type no-op. 44 background/eviction/notification tests green (the notified=False terminal state is compatible with the eviction guard). Part 2 (the Monitor tool + backpressure) follows separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bash): C5 Part 1 — fallback-mark-notified on enqueue failure (no leak) Pre-empting the eviction-leak risk: enqueue_shell_notification is best-effort (except: pass), but with the forward-trap removed the terminal state is notified=False — so if the enqueue raised, the sweeper (which keeps un-notified terminal tasks, eviction.py:97) would leak the task in /tasks forever. Added a fallback: on enqueue failure, force notified=True so the task stays evictable, degrading to the OLD evictable-but-unsent behavior rather than a leak (still strictly better than the pre-C5 silent-drop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bash): C5 Part 1 critic — shell-specific summary, XML escaping, kill path The c5p1-critic found the model-facing payload was wrong (the mechanism was right). Real TS analog: enqueueShellNotification (LocalShellTask.tsx:105-172). #1 BLOCKING — wrong summary: reused the AGENT builder, so every bg-bash completion was announced as `Agent "..." completed`, the exit code was dropped, and the failed case said "Unknown error". Added build_shell_notification_xml + _build_shell_summary + BACKGROUND_BASH_SUMMARY_PREFIX ("Background command ") — `Background command "<desc>" completed (exit code N)` / `... failed with exit code N` / `... was stopped`, exit code included only when known. Threaded exit_code + tool_use_id through enqueue_shell_notification ← background.py's rc. #2 MAJOR — no XML escaping: a bg command routinely has < > & (and _reap uses `description or command`), so a raw summary produced a MALFORMED envelope. The shell builder XML-escapes the summary (TS LocalShellTask.tsx:164 escapeXml); the agent builder stays raw (LocalAgentTask.tsx:246-256 is raw — that claim was only true for agents). #3 MAJOR — kill path: TS killTask (killShellTasks.ts:38-44) atomically sets status='killed' + notified=True, which SUPPRESSES the reaper notification (no notification on a user kill). The port sent SIGTERM only → the reaper saw rc≠0 → sent a spurious `failed`. stop_background_bash now sets status='killed' + notified=True; _patch preserves a 'killed' status (doesn't reclassify from the SIGTERM exit code). The duplicate-guard's "TaskStop did it" premise is now actually wired. #4 tests — the old tests never asserted summary CONTENT (passed with the wrong "Agent" text). Added: full-summary snapshots (prefix + exit code + not-"Agent"), metachar XML-escaping, a REAL spawn→reap integration (exit 3 → the "failed with exit code 3" notification delivered), and kill-suppression (kill → status=killed + notified + NO notification). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bash): C5 Part 1 #3 — mark killed in LocalShellTask.kill before the signal The c5p1-critic reproduced the spurious kill notification 8/8 through the REAL path (TaskStop tool → stop_task → impl.kill = LocalShellTask.kill). My prior mark lived only in stop_background_bash, which stop_task doesn't call — so nothing marked killed/notified before the reaper, which sent "failed with exit code -15". Fix (the critic's exact direction): move the status='killed' + notified=True mutation into LocalShellTask.kill, BEFORE os.killpg, gated on the process still being alive (TS killTask's status==='running' gate, killShellTasks.ts:38-44). This is the production kill path (TaskStop → stop_task → this), so the mark lives here; marking BEFORE the signal closes the reaper-vs-killer race (the reaper is blocked in proc.wait() and can't wake until the signal lands, by which point notified=True is committed → the reaper's enqueue no-ops). The stop_background_bash mark stays as defense-in-depth, also moved before its killpg. _patch preserves a 'killed' status. Test rewritten to drive the REAL path: test_stop_task_kill_suppresses_notification runs stop_task (what TaskStop uses) on a real bg task → asserts status=killed + notified + NO notification (the critic's repro, now 0). + a direct stop_background_bash defense-in-depth test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 7, 2026
The expand-result feature (#623) delegated verbose-block truncation to boundedLiveRenderText, which keeps the TAIL with a 'showing live tail' label — correct for the live stream, wrong for a persisted ctrl+o expansion where the reader wants the START of Grep matches / Bash output / an error. The comment had also gone stale (claimed 'NOT the 16KB budget' when the cap is now exactly 16k, and 'expanded by default' when it's behind ctrl+o). - New headTruncate: keeps the first VERBOSE_TRAIL_MAX_LINES/_CHARS with a trailing '… +N lines/chars omitted' marker (no live-tail label). - verboseToolBlock uses it; comment corrected to match the behavior. - boundedLiveRenderText stays for the genuine live-stream tails (streaming assistant text, thinking) — unchanged. Test: a 500-line Grep result keeps line 0-199, drops line 200 and the 499 tail, shows '… +N lines omitted', and no longer says 'showing live tail' — distinguishing head from tail (the old 'A'.repeat test couldn't). Reconciles the plan, the comment, the label, and the code (critic Finding B, required change #2 from the expand-E implementation review — missed in the first fix round, which only closed Finding A). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 7, 2026
…S-2) (#645) * feat(services): autoFix config + hook + runner (SERVICES-2 W1-W3; wiring next) Port of typescript/src/services/autoFix/ — the settings-opt-in feature that runs the user's lint/test command after a file edit and injects <auto_fix_feedback> so the model self-fixes. Python had only the /autofix slash shell; the runtime was absent. - config.py (autoFixConfig.ts): get_auto_fix_config(raw) → AutoFixConfig|None — dict + enabled + at-least-one-of-lint/test (the zod .refine) + numeric bounds (maxRetries 0..10 default 3, timeout 1000..300000 default 30000); None (safeParse-null posture) on any failure. load_auto_fix_config reads settings.extra["autoFix"] (verified: no typed field — lands in extra, like the plugins round's enabledPlugins). - hook.py (autoFixHook.ts): AUTO_FIX_TOOLS = the port's file-mutation registry names (Write/Edit/MultiEdit/NotebookEdit, the file_edit/file_write analog); should_run_auto_fix; build_auto_fix_context (the <auto_fix_feedback> block VERBATIM) + build_max_retries_context (verbatim). - runner.py (autoFixRunner.ts): run_auto_fix_check — lint first, test only if lint passed; each an asyncio.create_subprocess_shell bounded by timeout, killing the process GROUP on timeout (start_new_session + os.killpg, the detached/killTree analog) and reaping to avoid zombies; _build_error_summary verbatim; never-raises (spawn failure → has_errors=False). Verified: config parse/refine/bounds, hook fire + verbatim context, runner lint-fail-skips-test / lint-pass-test-fail / both-clean / timeout-kill (sleep 30 @ 800ms → returns 0.81s, group killed, timed_out) / abort. REMAINING: W4 wiring — a NEW sibling run_auto_fix_step at the orchestrator (tool_execution.py, after run_post_tool_use_hooks), NOT inside that function (it early-returns when no user PostToolUse hooks configured — the teammate-hooks split-gate class; autoFix must run regardless). Retry cap by query_tracking.chain_id. Then tests + suite. Awaiting autofix-critic on the plan's W4 placement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(services): autoFix wiring + all critic findings (SERVICES-2 complete) The autofix-critic caught that TS autoFix is DEAD CODE — AUTO_FIX_TOOLS = {'file_edit','file_write'} never matches the real tool names (Edit/Write, FileEditTool/constants.ts:2 FILE_EDIT_TOOL_NAME='Edit'), verified first-hand. So this port activates the author's INTENT ({Edit,Write}) as a DOCUMENTED, opt-in-gated DIVERGENCE (fixes the reference's dead tool-name set; inert unless a user writes settings.autoFix.enabled). All findings: - B1 (split-gate): run_auto_fix_step is a NEW SIBLING at the orchestrator (tool_execution.py, after run_post_tool_use_hooks) — NOT inside that function, which early-returns when no user PostToolUse hook is configured (the common autoFix case). Pinned by test_fires_with_zero_posttool_hooks. - B2 (accessor): config via get_settings().extra.get("autoFix") (ToolContext has no settings; global read = the per-process equivalent). - M1 (reject-not-clamp): out-of-range/non-int → whole config None (zod .min/.max rejects); .default only when key absent. - M2 (camelCase): reads raw["maxRetries"]/raw["timeout"]. - M3 (reset-on-success): the retry counter is DELETED on a clean run (toolHooks.ts:247), not just incremented on error. - M4 (tool set): {"Edit","Write"} — the author's intent, not the 4-element _FILE_EDIT_TOOLS (NotebookEdit excluded, MultiEdit is a phantom). - M5 (None-guard): chain key handles query_tracking=None → "default". - M6 (10k cap): each stream sliced to 10 000 before combine. - minors: SIGTERM (not SIGKILL) + reap + ProcessLookupError race; mid-flight abort (kills the group, degrades to no-errors); cwd from tool_use_context; the post-hook attachment yield shape (list-wrapped content). tests/services/test_autofix.py (26): config parse/refine/reject/camelCase, hook fire + verbatim contexts + {Edit,Write} intent, runner (lint-skips-test / test-fail / clean / timeout-SIGTERM-kill / 10k cap / pre-set abort / mid-flight abort / no-commands), step (ZERO-hooks regression, non-file skip, retry cap, reset-on-clean, None-query-tracking). Full suite at the 6-failure baseline (7857 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(services): autoFix config zod-strictness (critic minor #1) enabled must be a strict bool (a string "false" is truthy in Python — must not turn autoFix ON against intent); a bad-type lint/test rejects the whole config → None, matching zod z.boolean()/z.string() safeParse (not the prior silent coerce/drop). 3 new tests; consistent with the already-strict int path. (Minors #2 retry-map-lifecycle and #3 proxy-coverage accepted as faithful-to-TS / adequate.) Full suite at the 6-failure baseline (7860). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 7, 2026
* feat(bash): notify on background-bash completion (C5 Part 1 — enqueueShellNotification) TS notifies when a run_in_background bash task reaches a terminal state (utils/task/framework.ts:289 enqueuePendingNotification; BashTool/prompt.ts: 285-287 "you will be notified when it completes — do not poll"). The port marked the terminal state notified=True WITHOUT sending — the documented forward-trap in background.py (ch10 WI-2), so a bg bash finishing while the model was away went unannounced (silently polled via TaskOutput instead). - task_notification.py: enqueue_shell_notification — mirrors enqueue_agent_notification (the atomic check-and-set duplicate-delivery guard + build_task_notification_xml + enqueue_pending_notification) but gated on LocalShellTaskState instead of LocalAgentTaskState. - background.py: REMOVED the forward-trap notified=True from the terminal replace() (per its own comment's instruction), and call enqueue_shell_notification after the terminal update. The eviction sweeper's notify-before-evict guard (eviction.py:97) now correctly keeps the entry until the notification is delivered, then reclaims it — instead of the old mark-notified-without-sending. Best-effort (never breaks reaping); duplicate-safe (a TaskStop that already notified makes it a no-op). tests/test_shell_completion_notification.py (3): enqueues + marks notified on terminal, duplicate-safe (already-notified → no second), wrong-task-type no-op. 44 background/eviction/notification tests green (the notified=False terminal state is compatible with the eviction guard). Part 2 (the Monitor tool + backpressure) follows separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bash): C5 Part 1 — fallback-mark-notified on enqueue failure (no leak) Pre-empting the eviction-leak risk: enqueue_shell_notification is best-effort (except: pass), but with the forward-trap removed the terminal state is notified=False — so if the enqueue raised, the sweeper (which keeps un-notified terminal tasks, eviction.py:97) would leak the task in /tasks forever. Added a fallback: on enqueue failure, force notified=True so the task stays evictable, degrading to the OLD evictable-but-unsent behavior rather than a leak (still strictly better than the pre-C5 silent-drop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bash): C5 Part 1 critic — shell-specific summary, XML escaping, kill path The c5p1-critic found the model-facing payload was wrong (the mechanism was right). Real TS analog: enqueueShellNotification (LocalShellTask.tsx:105-172). #1 BLOCKING — wrong summary: reused the AGENT builder, so every bg-bash completion was announced as `Agent "..." completed`, the exit code was dropped, and the failed case said "Unknown error". Added build_shell_notification_xml + _build_shell_summary + BACKGROUND_BASH_SUMMARY_PREFIX ("Background command ") — `Background command "<desc>" completed (exit code N)` / `... failed with exit code N` / `... was stopped`, exit code included only when known. Threaded exit_code + tool_use_id through enqueue_shell_notification ← background.py's rc. #2 MAJOR — no XML escaping: a bg command routinely has < > & (and _reap uses `description or command`), so a raw summary produced a MALFORMED envelope. The shell builder XML-escapes the summary (TS LocalShellTask.tsx:164 escapeXml); the agent builder stays raw (LocalAgentTask.tsx:246-256 is raw — that claim was only true for agents). #3 MAJOR — kill path: TS killTask (killShellTasks.ts:38-44) atomically sets status='killed' + notified=True, which SUPPRESSES the reaper notification (no notification on a user kill). The port sent SIGTERM only → the reaper saw rc≠0 → sent a spurious `failed`. stop_background_bash now sets status='killed' + notified=True; _patch preserves a 'killed' status (doesn't reclassify from the SIGTERM exit code). The duplicate-guard's "TaskStop did it" premise is now actually wired. #4 tests — the old tests never asserted summary CONTENT (passed with the wrong "Agent" text). Added: full-summary snapshots (prefix + exit code + not-"Agent"), metachar XML-escaping, a REAL spawn→reap integration (exit 3 → the "failed with exit code 3" notification delivered), and kill-suppression (kill → status=killed + notified + NO notification). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bash): C5 Part 1 #3 — mark killed in LocalShellTask.kill before the signal The c5p1-critic reproduced the spurious kill notification 8/8 through the REAL path (TaskStop tool → stop_task → impl.kill = LocalShellTask.kill). My prior mark lived only in stop_background_bash, which stop_task doesn't call — so nothing marked killed/notified before the reaper, which sent "failed with exit code -15". Fix (the critic's exact direction): move the status='killed' + notified=True mutation into LocalShellTask.kill, BEFORE os.killpg, gated on the process still being alive (TS killTask's status==='running' gate, killShellTasks.ts:38-44). This is the production kill path (TaskStop → stop_task → this), so the mark lives here; marking BEFORE the signal closes the reaper-vs-killer race (the reaper is blocked in proc.wait() and can't wake until the signal lands, by which point notified=True is committed → the reaper's enqueue no-ops). The stop_background_bash mark stays as defense-in-depth, also moved before its killpg. _patch preserves a 'killed' status. Test rewritten to drive the REAL path: test_stop_task_kill_suppresses_notification runs stop_task (what TaskStop uses) on a real bg task → asserts status=killed + notified + NO notification (the critic's repro, now 0). + a direct stop_background_bash defense-in-depth test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
singlaamitesh
pushed a commit
to singlaamitesh/clawcodex
that referenced
this pull request
Jul 7, 2026
fix: fix i18n readme
singlaamitesh
pushed a commit
to singlaamitesh/clawcodex
that referenced
this pull request
Jul 7, 2026
… hyperlinks + frame metrics + thinking widget + per-tool permission specialization + output styles frontmatter + placeholder Resume/Doctor screens Bundle of standalone module additions across ch13 phases 4-12 that share no integration surface with each other and don't touch ``app.py``, ``screens/repl.py``, ``agent_bridge.py``, or ``agent_loop.py``. Each module ships with unit tests; cross-cutting production wiring (where present) lives in a follow-up PR so this change merges cleanly into ``main`` regardless of order. Phase 4 vim foundation (gap agentforce314#4 wave-1+2): - ``src/tui/vim_buffer.py`` — multi-line ``VimBuffer`` + ``Cursor`` + ``Range`` - ``src/tui/vim_text_objects.py`` — ``find_text_object`` for ``iw``/``aw``/``ip``/``ap``/quotes/braces - ``src/tui/vim_operators.py`` — ``parse_operator_motion`` (``d2w``, ``ciw``, ``y$``); operator dispatch d/c/y/>/< - ``src/tui/vim_visual.py`` — Visual / Visual-Line / Visual-Block selection model - ``src/tui/vim_search.py`` — ``/`` / ``?`` regex search with ``n``/``N`` repeat - 70 tests (``test_vim_multiline.py`` + ``test_vim_wave2.py``) Phase 5 transcript search module (gap agentforce314#2): - ``src/tui/widgets/transcript_search.py`` — ``TranscriptSearch`` ModalScreen + ``find_matches`` helper - 16 tests (``test_transcript_search.py``) Phase 6 IME declared cursor module (gap agentforce314#3): - ``src/tui/declared_cursor.py`` — refcounted-set registry + CSI emission - 16 tests (``test_declared_cursor.py``) Phase 7 specialized permission dialogs (gap agentforce314#8): - ``src/tui/screens/permission_modal.py`` ``_TOOL_RENDERERS`` dispatcher + per-tool renderers (Bash, Edit, Write, Read) - 16 tests (``test_permission_modal_specialization.py``) Phase 8 placeholder Resume/Doctor screens (gap agentforce314#9): - ``src/tui/screens/resume_conversation.py`` — placeholder with empty-state until persistence wiring lands - ``src/tui/screens/doctor.py`` — read-only diagnostics (env/hyperlinks/frame-metrics/storage) - 6 tests (``test_resume_doctor_screens.py``) Phase 9 output styles frontmatter (gap agentforce314#7): - ``src/outputStyles/{loader,styles}.py`` — YAML frontmatter parsing via existing ``parse_frontmatter``; auto-discovery of ``~/.claude/outputStyles/`` - 11 tests (``test_output_styles_frontmatter.py``) + parity test (``test_output_styles_parity.py``) Phase 10 OSC 8 hyperlinks (gap agentforce314#15): - ``src/tui/hyperlinks.py`` — capability detection (FORCE_HYPERLINK / TERM_PROGRAM / VTE_VERSION / truecolor) - ``src/tui/widgets/messages/tool_result.py`` wraps file paths with the negative-lookbehind regex (avoids eating sentence punctuation) - 38 tests (``test_hyperlinks.py`` + ``test_tool_result_hyperlinks.py``) Phase 11 frame-event observability (gap agentforce314#10): - ``src/tui/frame_metrics.py`` — ``FrameEvent`` shape + ``register_frame_observer`` + no-op fast path when ``CLAWCODEX_DEBUG_REPAINTS`` unset - 17 tests (``test_frame_metrics.py``) Phase 12 assistant-thinking widget (gap agentforce314#16 sub-item): - ``src/tui/widgets/messages/assistant_thinking.py`` — italic-dim styled distinct row; redacted variant - ``src/tui/widgets/transcript_view.py`` adds ``append_thinking_chunk``/``append_thinking`` shells with symmetric guard for ``append_assistant_chunk`` (Critic-flagged: prevents future thinking↔assistant mis-routing once dispatch wiring lands) - 11 tests (``test_assistant_thinking.py`` + ``test_transcript_thinking_transitions.py``) Test surface: 521 cumulative tests pass (``tests/tui/`` + ``tests/parity/test_output_styles_parity.py``). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
singlaamitesh
pushed a commit
to singlaamitesh/clawcodex
that referenced
this pull request
Jul 7, 2026
…ync I/O Six blocking issues identified in the post-rebase Critic review: 1. Thread auth_provider through connect_to_server / manager BEFORE client.connect, so the NeedsAuth fast-path + auth-header injection take effect on the first attempt. Previously wired after-the-fact in connection_manager, making OAuth inert through the agent boot path. 2. Enforce HTTPS on authServerMetadataUrl escape hatch in discover_oauth_metadata — RFC 8414 §2 mandates TLS, and an http:// override from a project-scoped .mcp.json would let an attacker steal the access_token. Mirrors TS auth.ts:332-334. 3. html.escape the reason text in oauth_callback_server's error body. Previously reflected attacker-controllable OAuth error params and path segments verbatim — a malicious redirect could execute JS in the user's browser against http://localhost:PORT/callback. 4. Use http://localhost:PORT/callback (not 127.0.0.1) for redirect_uri in auth_provider + xaa_idp_login. Real OAuth providers match the redirect_uri string literally; sending 127.0.0.1 when a provider has localhost on file produces redirect_uri_mismatch. Plan A5 + TS. 5. Replace synchronous urllib.urlopen with httpx.AsyncClient in auth.py exchange_code / refresh_token. urlopen blocks the event loop for the whole round-trip, stalling the OAuth callback listener that is waiting on the same loop. Plumbed normalize_oauth_error_body so Slack-style 200+error responses raise rather than store garbage. 6. (Major, paired with agentforce314#2): toggle_mcp_server now runs flip + reconnect under one lock acquisition — previously released the lock between the flip and the reconnect, leaving a race window. 15 regression tests added (tests/test_mcp_critic_blockers.py) covering each fix concretely. Full MCP suite: 307 passed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
singlaamitesh
pushed a commit
to singlaamitesh/clawcodex
that referenced
this pull request
Jul 7, 2026
…e-2-bootstrap ch03 state agentforce314#2: bootstrap state core expansion
singlaamitesh
pushed a commit
to singlaamitesh/clawcodex
that referenced
this pull request
Jul 7, 2026
…14#1, agentforce314#2) - /workflows modal (src/tui/screens/workflow_dialog.py): a list of runs and a per-run phases->agents detail view; `x` stops a run/agent, `r` retries an agent (wired to kill/skip/retry_workflow_agent). Pure render helper format_workflow_detail is unit-tested; the screens via the Textual pilot. - /workflows command wired into the TUI (LOCAL_BUILTINS + app opener). - StatusLine shows "N background workflows" from runtime_tasks (live pill). Remaining: `p` (pause) / `s` (save) affordances — see port-plan §10. Full workflow + TUI suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
singlaamitesh
pushed a commit
to singlaamitesh/clawcodex
that referenced
this pull request
Jul 7, 2026
…#601) The round-4 LLM memory-relevance recall (agentforce314#594) ships default-off because flipping it on is a latency+cost+correctness regression. This fixes the four blockers the round-4 critic flagged. Still default-off — zero behavior change on merge. - agentforce314#2 selector cost (provider-gated pin): the recall side-query ran on the SESSION model (an Opus turn paid full price every turn). TS pins it to a small default (getDefaultSonnetModel). The port wires the small_fast_model setting, but its shipped default is an Anthropic id (claude-3-5-haiku-20241022) valid only on the first-party Anthropic endpoint. So _resolve_recall_model(provider) applies the pin ONLY when the session provider is a first-party AnthropicProvider; Minimax (Anthropic SDK, own endpoint), DeepSeek, OpenAI, OpenRouter fall back to the session model. Without the gate the Anthropic default id 400s on a non-Anthropic endpoint and — since recall swallows errors — silently kills recall every turn (critic M1). small_fast_model_provider pairing for non-Anthropic cheap-recall is deferred. - agentforce314#3 de-dup reset on compaction: the recall reminder is ephemeral but _memory_surfaced was monotonic — once surfaced, a memory was never re-surfaced. After /compact drops the earlier context, the memory stayed suppressed. _do_compact now clears _memory_surfaced on success (idle-gated — no race with the turn worker). - agentforce314#4 aggregate turn byte-cap: per-file was 4 KB but 5 selections could inject ~20 KB/turn. build_relevant_memory_reminder_with_paths caps at MAX_TOTAL_SURFACE_CHARS=12 KB and returns the paths ACTUALLY surfaced; get_relevant_memory_reminder de-dups only those, so a cap-trimmed memory stays eligible (the old mark-all was a latent suppress-forever bug the cap would have activated). Back-compat string wrapper retained. - agentforce314#5 docstring: _maybe_recall_memories said "prepend"; the code appends after the user turn (correct — normalize_messages merges consecutive user messages). Fixed. Deferred agentforce314#1 (synchronous side-query): the port has only this one prefetch, so there is no concurrent prefetch to overlap without a larger prompt-submit refactor — one small-model call per turn (fast Haiku on the default path). Documented. Docs: my-docs/port-improvement-round-5/r5-1-ch11-memory-enable-blockers.md + critic verdicts (REVISE->APPROVE). Tests: tests/test_r5_ch11_memory_enable_blockers.py (8) incl. the M1 regression guard (non-Anthropic never pins even when the Anthropic-default id is set). Existing memory suite (466) green via the back-compat wrapper. Full suite at the pre-existing baseline. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
singlaamitesh
pushed a commit
to singlaamitesh/clawcodex
that referenced
this pull request
Jul 7, 2026
…rce314#606) LocalAgentTaskState.abort_event was declared (local_agent.py:77) but NEVER assigned — only read — so kill_async_agent's abort_event.set() was guarded out and killing a background Agent only flipped the registry status to "killed" while the live run kept going to NATURAL completion (burning model tokens). Directly counter to "run long coding tasks reliably": a user who stops a runaway background agent expects it to stop. The async run already had an abort mechanism the port never exposed: run_agent mints a fresh AbortController for async agents (run_agent.py:291), wires it into the subagent context, and query() polls signal.aborted at every yield point (query.py:1433/1520/1571/2046). This makes it REACHABLE: - LocalAgentTaskState.abort_controller field + register_async_agent param. - _launch_async_agent mints a dedicated AbortController, stores it on the task state AND sets run_params.abort_controller, so run_agent uses THIS one (run_agent.py:286-287) instead of its unreachable internal one. FRESH (not the parent turn's) → preserves async isolation. - kill_async_agent calls abort_controller.abort("killed by user") → the run's query() loop halts at the next poll. Cross-thread safe: the kill runs on the kill thread, the run polls on the background loop; AbortController.abort() is a GIL-atomic flag set + sync listeners and signal.aborted is an atomic read — the same cross-thread pattern the turn-interrupt path already uses. .abort() is placed outside the registry lock (matches the abort_event.set() rationale). Legacy abort_event.set() retained (harmless defense). Docs: my-docs/port-improvement-round-6/r6-1-kill-background-agent.md + critic verdict (APPROVE). Tests: tests/test_r6_kill_background_agent.py (4): unit (kill aborts the stored controller + flips killed; kill w/o controller is safe); END-TO-END (a long-running bg agent that polls the abort signal STOPS advancing after a cross-thread kill — NON-VACUOUS: neg-control advances ~34 steps post-kill without the fix, <=2 with it) + two ISOLATION assertions (killing the bg agent doesn't abort the parent; aborting the parent doesn't stop the bg agent). 1812 agent/async/task tests green; full suite at the 9-failure baseline. Deferred (critic minor agentforce314#2): the resume path must mirror this wire when it gains a live run loop, else resumed agents are un-killable (benign today — resume doesn't drive a run). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
singlaamitesh
pushed a commit
to singlaamitesh/clawcodex
that referenced
this pull request
Jul 7, 2026
The expand-result feature (agentforce314#623) delegated verbose-block truncation to boundedLiveRenderText, which keeps the TAIL with a 'showing live tail' label — correct for the live stream, wrong for a persisted ctrl+o expansion where the reader wants the START of Grep matches / Bash output / an error. The comment had also gone stale (claimed 'NOT the 16KB budget' when the cap is now exactly 16k, and 'expanded by default' when it's behind ctrl+o). - New headTruncate: keeps the first VERBOSE_TRAIL_MAX_LINES/_CHARS with a trailing '… +N lines/chars omitted' marker (no live-tail label). - verboseToolBlock uses it; comment corrected to match the behavior. - boundedLiveRenderText stays for the genuine live-stream tails (streaming assistant text, thinking) — unchanged. Test: a 500-line Grep result keeps line 0-199, drops line 200 and the 499 tail, shows '… +N lines omitted', and no longer says 'showing live tail' — distinguishing head from tail (the old 'A'.repeat test couldn't). Reconciles the plan, the comment, the label, and the code (critic Finding B, required change agentforce314#2 from the expand-E implementation review — missed in the first fix round, which only closed Finding A). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 10, 2026
…ckground band (#691) Past user prompts rendered as bare '❯ text' — visually identical in weight to assistant prose, so earlier inputs were hard to find when scrolling a multi-turn transcript. The original Claude Code draws every past user input (and slash echo) on a full-row background band; port that: - theme.ts: new userMessageBackground token — dark rgb(55,55,55) / light rgb(240,240,240), the exact original utils/theme.ts values; skinnable via user_message_bg with a DEFAULT_THEME fallback. - messageLine.tsx: transcriptRowBand(msg, t) paints the inner row Box for user rows and slash echoes (UserPromptMessage.tsx:76 / UserCommandMessage.tsx:62 parity). Slash echoes borrow the user pointer and text color instead of the muted system dot, and the user glyph drops its non-original bold — the band, not bold text, carries the emphasis (HighlightedThinkingText renders the pointer un-bolded). - appLayout/useMainApp/virtualHeights: the '───' inter-turn dash above non-first user rows becomes a monochrome-only fallback. It existed as a crutch for exactly this findability problem; with color available the band replaces it. On NO_COLOR / FORCE_COLOR=0 / TERM=dumb terminals the band emits nothing (chalk level 0), so the textual separator returns — gated by domain/blockLayout.ts::showsInterTurnSeparator fed from config/env.ts::TRANSCRIPT_COLOR (process.stdout.hasColors), with the render gate and the height estimator sharing the same predicate. [Codex adversarial-review catch: the first cut removed the separator unconditionally, leaving no-color terminals with no turn boundary.] - tests: pin band routing (transcriptRowBand), slash pointer swap, theme values incl. skin fallback/override, separator gate matrix, band-mode same-height + monochrome +2 heights; refresh the stale compound-prompt expectation (glyph is the fixed '❯' since the re-theme; the compound brand prompt only widens the gutter). - theme.ts selectionBg: ported the original selection blues — dark rgb(38,79,120) / light rgb(180,213,255) (utils/theme.ts). The previous dark #373737 was the exact band color, so selecting text on a past user row painted band-on-band and vanished. [Critic catch #2.] - diffView.ts structuredDiffSupported(): gate the raw-ANSI ColorDiff path on TRANSCRIPT_COLOR as well as NO_COLOR. ColorDiff builds SGRs itself (not through chalk), so a FORCE_COLOR=0 / TERM=dumb session previously kept colored diff blocks inside an otherwise monochrome transcript; it now falls back to the chalk-suppressed markdown ```diff path on the same signals as transcript chrome. [Codex verification-pass catch.] Verified with the pyte harness (fake NDJSON agent-server): banded rows carry bg 373737 across the row for single-line, multi-line (both lines + indent), and slash echoes; assistant/system rows and the composer stay bandless; with NO_COLOR=1 the '───' separator renders above non-first user turns. ui-tui vitest: 8 failed vs 9 on main baseline (one stale test fixed, no new failures); tsc + eslint clean. - lib/forceTruecolor.ts: the bundled chalk predates NO_COLOR support (it emitted full color regardless — verified level 2 on a PTY with NO_COLOR=1), so the pre-chalk bootstrap now translates NO_COLOR into FORCE_COLOR=0, the channel chalk does honor, unless the user set FORCE_COLOR explicitly. This keeps TRANSCRIPT_COLOR (stdout.hasColors, which honors NO_COLOR) agreeing with what the renderer emits — critic caught the divergence: NO_COLOR previously showed the band AND the fallback separator. diffView already gated on NO_COLOR, so the app is now consistent. [Critic catch: hasColors vs chalk signal divergence under NO_COLOR.] Known pre-existing (NOT introduced here, repro'd on the unmodified build): the inline growth repaint can leave stale right-edge cells from shifted footer/rule rows (e.g. a stray '─' at 100x30 after 3 turns on main). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 21, 2026
The round-4 LLM memory-relevance recall (#594) ships default-off because flipping it on is a latency+cost+correctness regression. This fixes the four blockers the round-4 critic flagged. Still default-off — zero behavior change on merge. - #2 selector cost (provider-gated pin): the recall side-query ran on the SESSION model (an Opus turn paid full price every turn). TS pins it to a small default (getDefaultSonnetModel). The port wires the small_fast_model setting, but its shipped default is an Anthropic id (claude-3-5-haiku-20241022) valid only on the first-party Anthropic endpoint. So _resolve_recall_model(provider) applies the pin ONLY when the session provider is a first-party AnthropicProvider; Minimax (Anthropic SDK, own endpoint), DeepSeek, OpenAI, OpenRouter fall back to the session model. Without the gate the Anthropic default id 400s on a non-Anthropic endpoint and — since recall swallows errors — silently kills recall every turn (critic M1). small_fast_model_provider pairing for non-Anthropic cheap-recall is deferred. - #3 de-dup reset on compaction: the recall reminder is ephemeral but _memory_surfaced was monotonic — once surfaced, a memory was never re-surfaced. After /compact drops the earlier context, the memory stayed suppressed. _do_compact now clears _memory_surfaced on success (idle-gated — no race with the turn worker). - #4 aggregate turn byte-cap: per-file was 4 KB but 5 selections could inject ~20 KB/turn. build_relevant_memory_reminder_with_paths caps at MAX_TOTAL_SURFACE_CHARS=12 KB and returns the paths ACTUALLY surfaced; get_relevant_memory_reminder de-dups only those, so a cap-trimmed memory stays eligible (the old mark-all was a latent suppress-forever bug the cap would have activated). Back-compat string wrapper retained. - #5 docstring: _maybe_recall_memories said "prepend"; the code appends after the user turn (correct — normalize_messages merges consecutive user messages). Fixed. Deferred #1 (synchronous side-query): the port has only this one prefetch, so there is no concurrent prefetch to overlap without a larger prompt-submit refactor — one small-model call per turn (fast Haiku on the default path). Documented. Docs: my-docs/port-improvement-round-5/r5-1-ch11-memory-enable-blockers.md + critic verdicts (REVISE->APPROVE). Tests: tests/test_r5_ch11_memory_enable_blockers.py (8) incl. the M1 regression guard (non-Anthropic never pins even when the Anthropic-default id is set). Existing memory suite (466) green via the back-compat wrapper. Full suite at the pre-existing baseline. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 21, 2026
LocalAgentTaskState.abort_event was declared (local_agent.py:77) but NEVER
assigned — only read — so kill_async_agent's abort_event.set() was guarded
out and killing a background Agent only flipped the registry status to
"killed" while the live run kept going to NATURAL completion (burning model
tokens). Directly counter to "run long coding tasks reliably": a user who
stops a runaway background agent expects it to stop.
The async run already had an abort mechanism the port never exposed:
run_agent mints a fresh AbortController for async agents (run_agent.py:291),
wires it into the subagent context, and query() polls signal.aborted at
every yield point (query.py:1433/1520/1571/2046). This makes it REACHABLE:
- LocalAgentTaskState.abort_controller field + register_async_agent param.
- _launch_async_agent mints a dedicated AbortController, stores it on the
task state AND sets run_params.abort_controller, so run_agent uses THIS
one (run_agent.py:286-287) instead of its unreachable internal one. FRESH
(not the parent turn's) → preserves async isolation.
- kill_async_agent calls abort_controller.abort("killed by user") → the
run's query() loop halts at the next poll.
Cross-thread safe: the kill runs on the kill thread, the run polls on the
background loop; AbortController.abort() is a GIL-atomic flag set + sync
listeners and signal.aborted is an atomic read — the same cross-thread
pattern the turn-interrupt path already uses. .abort() is placed outside
the registry lock (matches the abort_event.set() rationale). Legacy
abort_event.set() retained (harmless defense).
Docs: my-docs/port-improvement-round-6/r6-1-kill-background-agent.md +
critic verdict (APPROVE).
Tests: tests/test_r6_kill_background_agent.py (4): unit (kill aborts the
stored controller + flips killed; kill w/o controller is safe); END-TO-END
(a long-running bg agent that polls the abort signal STOPS advancing after a
cross-thread kill — NON-VACUOUS: neg-control advances ~34 steps post-kill
without the fix, <=2 with it) + two ISOLATION assertions (killing the bg
agent doesn't abort the parent; aborting the parent doesn't stop the bg
agent). 1812 agent/async/task tests green; full suite at the 9-failure
baseline.
Deferred (critic minor #2): the resume path must mirror this wire when it
gains a live run loop, else resumed agents are un-killable (benign today —
resume doesn't drive a run).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 21, 2026
The expand-result feature (#623) delegated verbose-block truncation to boundedLiveRenderText, which keeps the TAIL with a 'showing live tail' label — correct for the live stream, wrong for a persisted ctrl+o expansion where the reader wants the START of Grep matches / Bash output / an error. The comment had also gone stale (claimed 'NOT the 16KB budget' when the cap is now exactly 16k, and 'expanded by default' when it's behind ctrl+o). - New headTruncate: keeps the first VERBOSE_TRAIL_MAX_LINES/_CHARS with a trailing '… +N lines/chars omitted' marker (no live-tail label). - verboseToolBlock uses it; comment corrected to match the behavior. - boundedLiveRenderText stays for the genuine live-stream tails (streaming assistant text, thinking) — unchanged. Test: a 500-line Grep result keeps line 0-199, drops line 200 and the 499 tail, shows '… +N lines omitted', and no longer says 'showing live tail' — distinguishing head from tail (the old 'A'.repeat test couldn't). Reconciles the plan, the comment, the label, and the code (critic Finding B, required change #2 from the expand-E implementation review — missed in the first fix round, which only closed Finding A). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 21, 2026
…S-2) (#645) * feat(services): autoFix config + hook + runner (SERVICES-2 W1-W3; wiring next) Port of typescript/src/services/autoFix/ — the settings-opt-in feature that runs the user's lint/test command after a file edit and injects <auto_fix_feedback> so the model self-fixes. Python had only the /autofix slash shell; the runtime was absent. - config.py (autoFixConfig.ts): get_auto_fix_config(raw) → AutoFixConfig|None — dict + enabled + at-least-one-of-lint/test (the zod .refine) + numeric bounds (maxRetries 0..10 default 3, timeout 1000..300000 default 30000); None (safeParse-null posture) on any failure. load_auto_fix_config reads settings.extra["autoFix"] (verified: no typed field — lands in extra, like the plugins round's enabledPlugins). - hook.py (autoFixHook.ts): AUTO_FIX_TOOLS = the port's file-mutation registry names (Write/Edit/MultiEdit/NotebookEdit, the file_edit/file_write analog); should_run_auto_fix; build_auto_fix_context (the <auto_fix_feedback> block VERBATIM) + build_max_retries_context (verbatim). - runner.py (autoFixRunner.ts): run_auto_fix_check — lint first, test only if lint passed; each an asyncio.create_subprocess_shell bounded by timeout, killing the process GROUP on timeout (start_new_session + os.killpg, the detached/killTree analog) and reaping to avoid zombies; _build_error_summary verbatim; never-raises (spawn failure → has_errors=False). Verified: config parse/refine/bounds, hook fire + verbatim context, runner lint-fail-skips-test / lint-pass-test-fail / both-clean / timeout-kill (sleep 30 @ 800ms → returns 0.81s, group killed, timed_out) / abort. REMAINING: W4 wiring — a NEW sibling run_auto_fix_step at the orchestrator (tool_execution.py, after run_post_tool_use_hooks), NOT inside that function (it early-returns when no user PostToolUse hooks configured — the teammate-hooks split-gate class; autoFix must run regardless). Retry cap by query_tracking.chain_id. Then tests + suite. Awaiting autofix-critic on the plan's W4 placement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(services): autoFix wiring + all critic findings (SERVICES-2 complete) The autofix-critic caught that TS autoFix is DEAD CODE — AUTO_FIX_TOOLS = {'file_edit','file_write'} never matches the real tool names (Edit/Write, FileEditTool/constants.ts:2 FILE_EDIT_TOOL_NAME='Edit'), verified first-hand. So this port activates the author's INTENT ({Edit,Write}) as a DOCUMENTED, opt-in-gated DIVERGENCE (fixes the reference's dead tool-name set; inert unless a user writes settings.autoFix.enabled). All findings: - B1 (split-gate): run_auto_fix_step is a NEW SIBLING at the orchestrator (tool_execution.py, after run_post_tool_use_hooks) — NOT inside that function, which early-returns when no user PostToolUse hook is configured (the common autoFix case). Pinned by test_fires_with_zero_posttool_hooks. - B2 (accessor): config via get_settings().extra.get("autoFix") (ToolContext has no settings; global read = the per-process equivalent). - M1 (reject-not-clamp): out-of-range/non-int → whole config None (zod .min/.max rejects); .default only when key absent. - M2 (camelCase): reads raw["maxRetries"]/raw["timeout"]. - M3 (reset-on-success): the retry counter is DELETED on a clean run (toolHooks.ts:247), not just incremented on error. - M4 (tool set): {"Edit","Write"} — the author's intent, not the 4-element _FILE_EDIT_TOOLS (NotebookEdit excluded, MultiEdit is a phantom). - M5 (None-guard): chain key handles query_tracking=None → "default". - M6 (10k cap): each stream sliced to 10 000 before combine. - minors: SIGTERM (not SIGKILL) + reap + ProcessLookupError race; mid-flight abort (kills the group, degrades to no-errors); cwd from tool_use_context; the post-hook attachment yield shape (list-wrapped content). tests/services/test_autofix.py (26): config parse/refine/reject/camelCase, hook fire + verbatim contexts + {Edit,Write} intent, runner (lint-skips-test / test-fail / clean / timeout-SIGTERM-kill / 10k cap / pre-set abort / mid-flight abort / no-commands), step (ZERO-hooks regression, non-file skip, retry cap, reset-on-clean, None-query-tracking). Full suite at the 6-failure baseline (7857 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(services): autoFix config zod-strictness (critic minor #1) enabled must be a strict bool (a string "false" is truthy in Python — must not turn autoFix ON against intent); a bad-type lint/test rejects the whole config → None, matching zod z.boolean()/z.string() safeParse (not the prior silent coerce/drop). 3 new tests; consistent with the already-strict int path. (Minors #2 retry-map-lifecycle and #3 proxy-coverage accepted as faithful-to-TS / adequate.) Full suite at the 6-failure baseline (7860). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 21, 2026
* feat(bash): notify on background-bash completion (C5 Part 1 — enqueueShellNotification) TS notifies when a run_in_background bash task reaches a terminal state (utils/task/framework.ts:289 enqueuePendingNotification; BashTool/prompt.ts: 285-287 "you will be notified when it completes — do not poll"). The port marked the terminal state notified=True WITHOUT sending — the documented forward-trap in background.py (ch10 WI-2), so a bg bash finishing while the model was away went unannounced (silently polled via TaskOutput instead). - task_notification.py: enqueue_shell_notification — mirrors enqueue_agent_notification (the atomic check-and-set duplicate-delivery guard + build_task_notification_xml + enqueue_pending_notification) but gated on LocalShellTaskState instead of LocalAgentTaskState. - background.py: REMOVED the forward-trap notified=True from the terminal replace() (per its own comment's instruction), and call enqueue_shell_notification after the terminal update. The eviction sweeper's notify-before-evict guard (eviction.py:97) now correctly keeps the entry until the notification is delivered, then reclaims it — instead of the old mark-notified-without-sending. Best-effort (never breaks reaping); duplicate-safe (a TaskStop that already notified makes it a no-op). tests/test_shell_completion_notification.py (3): enqueues + marks notified on terminal, duplicate-safe (already-notified → no second), wrong-task-type no-op. 44 background/eviction/notification tests green (the notified=False terminal state is compatible with the eviction guard). Part 2 (the Monitor tool + backpressure) follows separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bash): C5 Part 1 — fallback-mark-notified on enqueue failure (no leak) Pre-empting the eviction-leak risk: enqueue_shell_notification is best-effort (except: pass), but with the forward-trap removed the terminal state is notified=False — so if the enqueue raised, the sweeper (which keeps un-notified terminal tasks, eviction.py:97) would leak the task in /tasks forever. Added a fallback: on enqueue failure, force notified=True so the task stays evictable, degrading to the OLD evictable-but-unsent behavior rather than a leak (still strictly better than the pre-C5 silent-drop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bash): C5 Part 1 critic — shell-specific summary, XML escaping, kill path The c5p1-critic found the model-facing payload was wrong (the mechanism was right). Real TS analog: enqueueShellNotification (LocalShellTask.tsx:105-172). #1 BLOCKING — wrong summary: reused the AGENT builder, so every bg-bash completion was announced as `Agent "..." completed`, the exit code was dropped, and the failed case said "Unknown error". Added build_shell_notification_xml + _build_shell_summary + BACKGROUND_BASH_SUMMARY_PREFIX ("Background command ") — `Background command "<desc>" completed (exit code N)` / `... failed with exit code N` / `... was stopped`, exit code included only when known. Threaded exit_code + tool_use_id through enqueue_shell_notification ← background.py's rc. #2 MAJOR — no XML escaping: a bg command routinely has < > & (and _reap uses `description or command`), so a raw summary produced a MALFORMED envelope. The shell builder XML-escapes the summary (TS LocalShellTask.tsx:164 escapeXml); the agent builder stays raw (LocalAgentTask.tsx:246-256 is raw — that claim was only true for agents). #3 MAJOR — kill path: TS killTask (killShellTasks.ts:38-44) atomically sets status='killed' + notified=True, which SUPPRESSES the reaper notification (no notification on a user kill). The port sent SIGTERM only → the reaper saw rc≠0 → sent a spurious `failed`. stop_background_bash now sets status='killed' + notified=True; _patch preserves a 'killed' status (doesn't reclassify from the SIGTERM exit code). The duplicate-guard's "TaskStop did it" premise is now actually wired. #4 tests — the old tests never asserted summary CONTENT (passed with the wrong "Agent" text). Added: full-summary snapshots (prefix + exit code + not-"Agent"), metachar XML-escaping, a REAL spawn→reap integration (exit 3 → the "failed with exit code 3" notification delivered), and kill-suppression (kill → status=killed + notified + NO notification). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bash): C5 Part 1 #3 — mark killed in LocalShellTask.kill before the signal The c5p1-critic reproduced the spurious kill notification 8/8 through the REAL path (TaskStop tool → stop_task → impl.kill = LocalShellTask.kill). My prior mark lived only in stop_background_bash, which stop_task doesn't call — so nothing marked killed/notified before the reaper, which sent "failed with exit code -15". Fix (the critic's exact direction): move the status='killed' + notified=True mutation into LocalShellTask.kill, BEFORE os.killpg, gated on the process still being alive (TS killTask's status==='running' gate, killShellTasks.ts:38-44). This is the production kill path (TaskStop → stop_task → this), so the mark lives here; marking BEFORE the signal closes the reaper-vs-killer race (the reaper is blocked in proc.wait() and can't wake until the signal lands, by which point notified=True is committed → the reaper's enqueue no-ops). The stop_background_bash mark stays as defense-in-depth, also moved before its killpg. _patch preserves a 'killed' status. Test rewritten to drive the REAL path: test_stop_task_kill_suppresses_notification runs stop_task (what TaskStop uses) on a real bg task → asserts status=killed + notified + NO notification (the critic's repro, now 0). + a direct stop_background_bash defense-in-depth test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 21, 2026
…ckground band (#691) Past user prompts rendered as bare '❯ text' — visually identical in weight to assistant prose, so earlier inputs were hard to find when scrolling a multi-turn transcript. The original Claude Code draws every past user input (and slash echo) on a full-row background band; port that: - theme.ts: new userMessageBackground token — dark rgb(55,55,55) / light rgb(240,240,240), the exact original utils/theme.ts values; skinnable via user_message_bg with a DEFAULT_THEME fallback. - messageLine.tsx: transcriptRowBand(msg, t) paints the inner row Box for user rows and slash echoes (UserPromptMessage.tsx:76 / UserCommandMessage.tsx:62 parity). Slash echoes borrow the user pointer and text color instead of the muted system dot, and the user glyph drops its non-original bold — the band, not bold text, carries the emphasis (HighlightedThinkingText renders the pointer un-bolded). - appLayout/useMainApp/virtualHeights: the '───' inter-turn dash above non-first user rows becomes a monochrome-only fallback. It existed as a crutch for exactly this findability problem; with color available the band replaces it. On NO_COLOR / FORCE_COLOR=0 / TERM=dumb terminals the band emits nothing (chalk level 0), so the textual separator returns — gated by domain/blockLayout.ts::showsInterTurnSeparator fed from config/env.ts::TRANSCRIPT_COLOR (process.stdout.hasColors), with the render gate and the height estimator sharing the same predicate. [Codex adversarial-review catch: the first cut removed the separator unconditionally, leaving no-color terminals with no turn boundary.] - tests: pin band routing (transcriptRowBand), slash pointer swap, theme values incl. skin fallback/override, separator gate matrix, band-mode same-height + monochrome +2 heights; refresh the stale compound-prompt expectation (glyph is the fixed '❯' since the re-theme; the compound brand prompt only widens the gutter). - theme.ts selectionBg: ported the original selection blues — dark rgb(38,79,120) / light rgb(180,213,255) (utils/theme.ts). The previous dark #373737 was the exact band color, so selecting text on a past user row painted band-on-band and vanished. [Critic catch #2.] - diffView.ts structuredDiffSupported(): gate the raw-ANSI ColorDiff path on TRANSCRIPT_COLOR as well as NO_COLOR. ColorDiff builds SGRs itself (not through chalk), so a FORCE_COLOR=0 / TERM=dumb session previously kept colored diff blocks inside an otherwise monochrome transcript; it now falls back to the chalk-suppressed markdown ```diff path on the same signals as transcript chrome. [Codex verification-pass catch.] Verified with the pyte harness (fake NDJSON agent-server): banded rows carry bg 373737 across the row for single-line, multi-line (both lines + indent), and slash echoes; assistant/system rows and the composer stay bandless; with NO_COLOR=1 the '───' separator renders above non-first user turns. ui-tui vitest: 8 failed vs 9 on main baseline (one stale test fixed, no new failures); tsc + eslint clean. - lib/forceTruecolor.ts: the bundled chalk predates NO_COLOR support (it emitted full color regardless — verified level 2 on a PTY with NO_COLOR=1), so the pre-chalk bootstrap now translates NO_COLOR into FORCE_COLOR=0, the channel chalk does honor, unless the user set FORCE_COLOR explicitly. This keeps TRANSCRIPT_COLOR (stdout.hasColors, which honors NO_COLOR) agreeing with what the renderer emits — critic caught the divergence: NO_COLOR previously showed the band AND the fallback separator. diffView already gated on NO_COLOR, so the app is now consistent. [Critic catch: hasColors vs chalk signal divergence under NO_COLOR.] Known pre-existing (NOT introduced here, repro'd on the unmodified build): the inline growth repaint can leave stale right-edge cells from shifted footer/rule rows (e.g. a stray '─' at 100x30 after 3 turns on main). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
peroxider
pushed a commit
to peroxider/clawcodex
that referenced
this pull request
Jul 21, 2026
feat(SOP):resource catalog,composite tools与SDK隔离实现
Created-by: gkawuiq8
Commit-by: gkawuiq8;root
Merged-by: chadwweng
Description: ## 背景
SOP Converter 将 SDK 接口转换为独立 Tool 后,创建型工具产生的内存对象无法跨 Tool 调用复用,导致“创建 Agent → 按 ID 调用”的生命周期链路断裂。同时,复合宏工具与 SDK 原子工具存在语义竞争,Agent 即使能够执行宏,也不一定能稳定检索并选择宏。
本 PR 串联 F-55、F-56、F-57 和 F-157,补齐从资源创建、持久化、恢复、复合执行到 ToolSearch 分层选择的完整链路。
## 主要改动
### F-55:补齐工具生命周期依赖
让「创建 → 保存 → 再调用」变成系统认的固定套路,而不是靠模型自己猜下一步该调哪个工具。
- 为 create/invoke 工具建立 `resource_type`、`produces`、`consumes` 和 `resource_ref` 契约。
- 创建成功后写入可恢复记录,并通过 `created_persisted` 等稳定字段区分创建与持久化状态。
- 为 invoke/run 工具增加 catalog fallback,资源不在当前进程时可自动查找并恢复。
- 生成 `.clawcodex/tool-dependencies.yaml`,记录 create → persist → materialize → invoke 依赖链。
- 将生命周期依赖写入 Skill frontmatter、Task Guide 和系统提示。
- ToolSearch 接入生命周期排序与 `lifecycle-chain:` 查询,减少相似 SDK 工具之间的误选和空转。
### F-56:实现通用 SOP Resource Catalog
给「创建出来的 Agent / 资源」做一个可跨会话复用的通讯录——按名字或 ID 能查到,敏感信息只记环境变量引用,不落明文 Key。
- 新增 `ResourceRecord`、`ResourceCatalog` 和 `CatalogExecutionContext`。
- 支持 bundle-local/user-local catalog、原子写入、幂等 upsert 和 legacy AgentCatalog 兼容。
- 敏感配置仅保存 `env:` 引用,避免 API Key 明文落盘。
- 新增 `ResourceHandler` 注册表,以 `resource_type` 作为扩展点;Agent 是首个生产级实现。
- 支持按 ID、名称和 alias 查找资源,歧义时拒绝猜测。
- 实现 Agent 的 materialize/invoke 恢复路径,并统一缺失、歧义、secret 缺失、版本不兼容等错误码。
- 支持 `.clawcodex/resources.yaml` sidecar 显式覆盖资源类型和句柄字段。
### F-57:可执行复合工作流 + 手写宏
把「多步流水线」做成真正可调用的一种 Tool(`workflow`),并支持你在仓库里手写 YAML 宏(例如文本/图像/多模态处理流水线);Agent 一次调用宏,系统按步骤顺序跑完,而不是让模型自己拼一串原子工具。
#### 复合工作流运行时
- `workflow` 成为一等 Tool 调用类型,由主进程 `ToolRegistry` 调度。
- 支持顺序 step、输入输出绑定、资源绑定、递归深度限制、延迟工具激活,以及结构化 `trace`(哪一步成功/失败一目了然)。
- trusted private lane:Agent 实例等不能 JSON 化的对象不进入公开 Tool 输出。
- 内置宏 `invoke-existing-agent` 固定三步:`load_agent_record → materialize_agent → invoke_agent`(对应「从目录取出 → 在本进程恢复 → 真正调用」)。
- 完善 output schema、类型重建与 JSON-safe 输出,保证宏返回值可被后续步骤和下一次 Tool 消费。
#### 手写宏
除了 convert 自动生成的工具,你还可以在 `sop-macros/` 里手写「业务宏」YAML;convert 时装进 bundle,运行时能被 ToolSearch 召回并一键执行。
- 支持从源码树 `sop-macros/`、`--macros-dir` 或 `--macro-manifest` 加载手写宏定义。
- convert 阶段完成 schema 校验、原子写入 bundle(如 `.clawcodex/macros/`),并注册为 `call_type=workflow` 的可调工具。
- 宏可声明对外入参(如 `input_path` / `output_path`)、内部 step 编排(如调用 `execute-pipeline`),以及对外输出契约(如非空 `session_id` + `summary`)。
- 与 MacroCatalog / MacroRoute 打通:自然语言或 `select:宏名` 可稳定命中手写宏,而不是落到裸原子工具。
- 验收侧典型路径:AscendDataForge 文本/图像/多模态手写宏 → ToolSearch 命中 → 一次 `tool_use` 跑完整流水线 → 产物落盘。
### F-157:实现宏工具/原子工具分层检索
搜索工具时「能走整包宏就不要拆零件」——有合适宏就优先(甚至独占)展示宏;宏不可用时再把原子工具放回来,避免 Agent 被一堆相似小工具带偏。
- 宏路由增加 `intent_key`、`covered_tools`、`unavailable_policy`,标明「这个宏覆盖了哪些原子工具」。
- `sop convert` 生成 `.clawcodex/tool-retrieval.yaml`,显式记录 macro / atomic / neutral 层级与覆盖关系。
- `RetrievalPlan` 支持 verified exclusive、prefer、普通检索三种策略。
- exclusive 命中且预检通过后,从搜索结果和当前可用工具里隐藏被覆盖的原子工具。
- 宏预检失败时,同一次 ToolSearch 内撤销隐藏并恢复原子候选,避免「宏挂了又搜不到退路」。
- 若仍去调已被宏盖住的陈旧原子工具,返回 `tool_shadowed_by_macro`,引导改调推荐宏。
- 评分接近时优先同意图宏,同时保留「用户精确点名原子工具」时的优先级。
- ToolSearch 结果带上 intent、selection、被抑制工具、preflight、reason codes,方便对照验收。
### SDK 依赖隔离与转换健壮性
convert 时给每个 bundle 准备独立 venv 并装好 SDK 依赖;真正跑 Tool 时不切换进程解释器(避免把 Agent/REPL 进程 exec 掉),而是把 bundle 的 site-packages 挂进当前进程,形成「软隔离」。
- 为转换后的 bundle 建立独立虚拟环境并解析 SDK requirements。
- 支持 Windows/WSL 路径、模块导入、wrapper 重入和 bundle 上下文传播。
- 完善 CLI handler、异步接口、复杂参数 schema 和 SDK 类型恢复。
## 核心链路
```text
create resource
→ ResourceCatalog upsert
→ ToolSearch 召回复合宏
→ catalog lookup
→ materialize
→ invoke
→ 返回 JSON-safe output + workflow trace
```
## 验证
聚焦回归结果:
164 passed, 8 subtests passed
覆盖范围包括:
- F-55 生命周期排序、catalog fallback 和 create → invoke E2E。
- F-56 catalog、secret 引用、资源类型扩展及 Agent 重建。
- F-57 workflow dispatch、宏装载、参数绑定、trace 和 output schema。
- F-157 exclusive suppression、preflight 回滚、shadow guard、结构偏置和索引校验。
手工验证已确认 Agent 创建后写入 catalog,以及 invoke-existing-agent 完成
load → materialize → invoke 三步调用。F-157 自然语言选择效果仍需按手工验收清单复验。
## 兼容性与边界
- 保留 legacy AgentCatalog 双写及读取 fallback。
- 历史未分类工具默认为 neutral,不会因名称相似被隐藏。
- 当前生产级资源恢复保证以 Agent 为主;第二种真实 SDK 资源和通用 resume-resource 宏不在本 PR 范围。
- Session 宏注册、trace-to-macro 和宏提升属于后续可选能力。
- 外部 Ray/worker 是否使用 bundle 解释器仍受目标 SDK 运行环境影响,需单独进行端到端验收。
## 一、F-56,F-57特性验证
会话根目录:
`\\wsl$\Ubuntu-18.04\root\.clawcodex\sessions\3efca96f-4e92-4580-9506-8f479371b255\`
落盘 catalog:
`D:\projects\clawcodex\.clawcodex\JiuwenAgent_v7.18\.clawcodex\resource-catalog.json`
### 1) F-56:创建并写入 catalog
**文件:** `subagents\agent-ark8z37bg.jsonl`
| 行号(约) | 流程 | 详情 |
|-----------|--------|----------|
| L0 | `tool_use` Skill | `core_merged-skill` |
| L4 | `tool_use` ToolSearch | `select:openjiuwen-core-application-llm-agent-create-llm-agent` |
| L7 | `tool_use` create 工具 | `openjiuwen-core-application-llm-agent-create-llm-agent`,`agent_config.id=verify-bot` |
| **L9** | **`tool_result`(核心)** | 见下表字段 |
**L9 `tool_result` 里的 F-56 字段:**
```text
created_persisted = true
resource_catalog_reason = "f56_resource_catalog"
resource_catalog_path = ".../JiuwenAgent_v7.18/.clawcodex/resource-catalog.json"
catalog_reason = "bundle-local"
agent_id = "verify-bot"
resource_ref = "verify-bot"
callable_by_agent_id / callable_by_resource_ref = true
*_call_contract = "catalog_persisted"
```

**落盘对照:** 打开 `resource-catalog.json`,`records` 中有 key 含 `verify-bot`,且:
- `resource_id`: `verify-bot`
- `source_tool`: `openjiuwen-core-application-llm-agent-create-llm-agent`
- `materializer` 含 `init_kwargs.agent_config`(api_key 应是 `env:DEEPSEEK_API_KEY`,不是明文)

### 2) F-57:宏召回 + catalog→materialize→invoke
**文件:** `subagents\agent-a6d96va9i.jsonl`
| 行号(约) | 流程 | 详情 |
|-----------|--------|----------|
| L0 | Skill | `core_merged-skill` |
| L4 | ToolSearch | `select:invoke-existing-agent` |
| **L6** | ToolSearch 结果 | `matches: ["invoke-existing-agent"]`(宏被召回,不是原子 `llmagent-invoke`) |
| L7 | 调宏 | `invoke-existing-agent`,入参 `agent_ref=verify-bot`, `query=ping` |
| **L9** | **宏结果(核心)** | workflow `steps` 三步全 success |



**L9 `steps`(F-57 主路径):**
```text
load_agent_record kind=catalog status=success
materialize_agent kind=python status=success
invoke_agent kind=python status=success
agent_id = verify-bot
```

父会话 `transcript.jsonl` 只能看到 overview 委派了 `Agent(core_merged-agent)`;**F-56/57 细节只在上述两个 subagent jsonl + resource-catalog.json**。
---
## 二、F-56 / F-57 验收对话用例
前置:`--agent` 指向已 convert 的 JiuwenAgent bundle;`DEEPSEEK_API_KEY` 已设置;每次新开 session 更清晰。
### A. 主路径(必过)
| ID | 用户输入 | 期望工具链 | 验收点(日志/文件) |
|----|----------|------------|-------------------|
| A1 | 用 JiuwenAgent SDK 创建名为 `verify-bot` 的 LLMAgent;provider=deepseek;api key 用 `env:DEEPSEEK_API_KEY`;model=`deepseek-v4-flash` | Skill → ToolSearch → `create-llm-agent` | `created_persisted=true`;`resource_catalog_reason=f56_resource_catalog`;catalog 文件有 `verify-bot`;密钥非明文 |
| A2 | 用 `verify-bot` 回复 `请原样输出:PING_OK_56`,把原文返回给我 | Skill → ToolSearch(`select:invoke-existing-agent` 或自然语言命中宏) → `invoke-existing-agent` | ToolSearch `matches` 含宏;steps=`load_agent_record→materialize_agent→invoke_agent` 全 success;output 含 `PING_OK_56` |
| A3 | (新开同 bundle 的新 session,不重建)只用已有 `verify-bot` 回复 `PING_CROSS_SESSION` 原文返回 | 同上,**禁止**再 create | 证明跨会话读 catalog(F-56§9 agentforce314#2) |


### B. 引用形态(F-56 契约 / F-57 兼容入参)
| ID | 用户输入 | 期望 | 验收点 |
|----|----------|------|--------|
| B1 | 用 resource_ref=`verify-bot` 调用,消息=`REF_OK`,原文返回 | `invoke-existing-agent`,`agent_ref` 或等价字段 | steps 全 success;output 含 `REF_OK` |
| B2 | 用 agent_id=`verify-bot` 调用,消息=`ID_OK`,原文返回 | 同上(legacy 兼容) | 成功,不落到 `llmagent-invoke` |


### C. 幂等 / 更新(F-56§9 agentforce314#4)
| ID | 用户输入 | 期望 | 验收点 |
|----|----------|------|--------|
| C1 | 再次创建同名 `verify-bot`(同配置) | create 成功或明确 upsert | catalog 仍一条主记录;可再 invoke |
| C2 | 创建后立刻 invoke 一次 | 宏成功 | 说明 upsert 未破坏 materializer |


### D. 负向错误码(F-56§9 agentforce314#5/agentforce314#6 + F-57 透传)
| ID | 用户输入 | 期望 | 验收点 |
|----|----------|------|--------|
| D1 | 用 `no-such-agent-xyz` 回复 `ping` | `invoke-existing-agent` 失败 | `error_code` 含 `resource_catalog_missing`(或等价);steps 在 catalog 步失败 |
| D2 | (若环境可构造重名)按模糊名调用 | 拒绝猜测 | `resource_catalog_ambiguous` |
| D3 | 临时去掉 `DEEPSEEK_API_KEY` 再 invoke | materialize/invoke 失败 | `resource_secret_missing` 或明确 secret 错误,**日志无明文 key** |

### E. 宏 vs 原子工具(F-57 路由,不含 F-157 隐藏验收)
| ID | 用户输入 | 期望 | 验收点 |
|----|----------|------|--------|
| E1 | 调用已经创建的 Agent(例如让 verify-bot 回复 ping) | 优先 `invoke-existing-agent` | ToolSearch/实际调用是宏,不是 `llmagent-in
See merge request: chadwweng/clawcodex!107
ericleepi314
added a commit
that referenced
this pull request
Jul 29, 2026
…em (#762) Follow-up to #761. Two user-reported gaps. 1. A pasted image left no trace in the input box. The composer now shows the reference's chip: > what this image is about? [Image #2] The chip is not decoration. The backend assigns the id, and at submit it DROPS any pending image whose [Image #N] is no longer in the text, so deleting the chip un-attaches the image. That is the reference's rule (handlePromptSubmit.ts:225) and it closes the un-attach gap #761 shipped with: previously the only way to undo an accidental attach was /clear, which destroys the conversation. The chip text stays in the prompt the model sees -- the reference leaves image refs inline and sends the bytes as separate blocks (history.ts:79). _pending_images entries become (image_id, PastedImage, expects_placeholder) behind the existing _queue_image choke point, with a monotonic per-session _image_seq. Session-wide rather than per-prompt on purpose: ids are never reused and the drain is destructive, so a stale chip recalled from history or /resume matches nothing. A per-prompt reset would be worse than the reference -- a recalled [Image #1] would keep a freshly-pasted image #1 alive after its real chip was deleted. 2. Cmd+V did nothing on macOS. A third unwired RPC: clipboard.paste. macOS terminals own Cmd+V and, with an image-only clipboard, deliver an EMPTY bracketed paste -- which lands in the composer's empty-paste branch, calls onClipboardPaste, and hit gatewayClient's "Unhandled RPC (Phase 2)" default. Resolved {}, silently, because that call passes quiet=true. The keybinding was never the missing piece. hermes already binds Cmd+V (textInput.tsx `isMac && isActionMod(k) && inp === 'v'`, with super+v reserved in platform.ts) and clawcodex inherited it, so Cmd+V works as a keypress wherever the terminal reports Cmd, and through the empty-paste route where it does not. opencode reaches the same place from the other direction: ctrl+v plus a re-dispatch on an empty bracketed paste. `placeholder` is a CALL-SITE property, not an RPC property. Whether an [Image #N] chip gets rendered depends on the caller, so the flag has to come from one. Six sites reach these four RPCs and only three render a chip; hardcoding placeholder:true at the RPC layer made the backend drop the images of the other three -- /image, the CLAWCODEX_TUI_IMAGE startup image, and typed-path submit -- at submit time, AFTER their UI had already printed "Attached image". Both of those worked when #761 shipped. Default false is fail-open, so headless -p and the VS Code bridge keep sending their images. Tests pin the cross-layer invariant in both directions, which is what was missing when a green suite hid the above: the RPC forwards true from a chip-rendering caller, defaults to false, and refuses a truthy-ish value; the real clipboard and dropped-path handlers honour the flag and drop on a deleted chip. Live: clipboard image -> [Image #1] -> claude-opus-5 read "**PURPLE-ELEPHANT-42**" (finish_reason end_turn); with the chip deleted the prompt went out as plain text with the queue drained. Python 9007 passed / 3 skipped / 0 failed. ui-tui 1545 passed, same 8 pre-existing baseline failures. Follow-ups, not in this change: an atomic chip (one backspace deletes the whole pill, Cursor.ts:346-370 -- today it takes 10, and a partial "[Image #1" silently un-attaches), and a chip for /image. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR formats readme.