Skip to content

feat(ch12): phase 6 — hook event emission stream - #36

Merged
agentforce314 merged 1 commit into
feat/ch12/phase5from
feat/ch12/phase6
May 9, 2026
Merged

feat(ch12): phase 6 — hook event emission stream#36
agentforce314 merged 1 commit into
feat/ch12/phase5from
feat/ch12/phase6

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

Summary

Phase 6 of ch12 — hook event emission stream for SDK / TUI / telemetry consumers. Three event types, idempotent subscriber API, exception-isolated dispatch.

What's new in this phase (vs Phase 5)

  • New src/hooks/events.py with three event types:
    • hook_started — pre-execution, per-hook
    • hook_response — post-execution, per-hook (carries hook's own decision, NOT aggregated)
    • hook_aggregated — once per _run_hooks_for_event invocation, carries AggregatedHookResult with contributing_reasons
  • Subscriber API: register_hook_event_handler(handler) returns idempotent deregister closure. set_all_hook_events_enabled(False) short-circuit for sessions without listeners.
  • Error isolation: subscriber raising does NOT break executor or other subscribers. Logged at WARNING. Tested at three levels (cross-subscriber, cross-event, end-to-end via executor).
  • Concurrency: threading.Lock guards subscriber list; dispatch iterates a snapshot so handlers can deregister mid-iteration safely.
  • Emission seams wired into _run_hooks_for_event:
    • emit_hook_started before each _execute_command_hook
    • emit_hook_response after (per-hook decision)
    • emit_hook_aggregated once at the end with AggregatedHookResult
  • 14 new tests covering subscriber lifecycle, error isolation, ordering, and idempotent unregister.

Test plan

🤖 Generated with Claude Code

Phase 6 of ch12 extensibility refactor (plan §10). Closes gap #9 (no
observability of hook execution for UI/SDK/telemetry subscribers).

WI-6.1 — Hook event emission stream. New ``src/hooks/events.py``
module + emission seams in ``_run_hooks_for_event``. Mirrors TS
``typescript/src/utils/hooks/hookEvents.ts``.

Three event types fire per hook execution:

  * ``hook_started`` — before each hook executes (subprocess/HTTP/LLM
    call). Carries hook_id, event, hook_type, command, source,
    tool_use_id. Lets subscribers show "hook running" spinners.
  * ``hook_response`` — after each hook returns. Carries the per-hook
    decision: exit_code, duration_ms, blocking_error,
    permission_behavior, command. NOT the aggregated decision —
    subscribers wanting that listen for hook_aggregated.
  * ``hook_aggregated`` — once per ``_run_hooks_for_event`` invocation,
    AFTER Phase-4 aggregation. Carries the AggregatedHookResult so
    subscribers see the final decision + full ``contributing_reasons``
    attribution without re-deriving from individual responses.
    Skipped when no hooks fired.

Subscriber API (mirrors hookEvents.ts):

  * ``register_hook_event_handler(handler) -> deregister_fn``: returns
    an idempotent deregister callable (calling twice is a no-op,
    matching the context-manager-cleanup pattern subscribers use).
  * ``set_all_hook_events_enabled(bool)``: global enable flag for
    runtime configuration / performance-sensitive code paths.
  * ``clear_hook_event_state()``: reset between tests.

Concurrency: subscriber list guarded by ``threading.Lock``. Dispatch
iterates a snapshot so a handler can deregister itself or others
mid-iteration without racing the iteration.

Error isolation contract (chapter requirement): a subscriber that
raises must NOT break the executor or other subscribers. Each handler
is called inside try/except; failures logged at WARNING (not exception
— avoids massive tracebacks for buggy subscribers). Tested explicitly:

  * ``test_handler_exception_does_not_break_dispatch``: 2 handlers,
    first crashes, second still receives the event.
  * ``test_handler_exception_isolated_per_event``: 3 successive emits
    with a crashing handler; all 3 dispatches complete.
  * ``test_subscriber_crash_does_not_break_executor``: end-to-end —
    register a crashing handler, drive ``_run_hooks_for_event``,
    executor still completes and yields its messages.

Tests — 14 new in tests/test_hook_event_emission.py:

  * Subscription: each event type round-trips; deregister stops
    delivery; idempotent unregister.
  * Exception isolation (3 tests, see above).
  * Global enable flag: suppress + re-enable.
  * clear_hook_event_state.
  * End-to-end via ``_run_hooks_for_event``: full event sequence with
    ordering pinned (started < response < aggregated); aggregated
    payload carries the AggregatedHookResult; no events when no hooks
    fire; subscriber crashes don't break the executor.

Verification (S3 baseline-diff discipline):
  Phase 5 (5fa75d0 + 317aed2) failures saved at /tmp/phase5_failures.txt
  Phase 6 (this commit) failures saved at /tmp/phase6_failures.txt
  diff returns empty → identical 27-failure set, matching Phase 0
  baseline (62a307c).

Test counts: 3859 passed, 27 failed (Phase 5 was 3851 with the
follow-up commit; +14 new in this commit). Hook+skill surface:
331/331 (Phase 5 was 317; +14).

Local-import pattern (``from src.hooks.events import emit_hook_*``
inside the executor body) keeps the module dependency lazy — emitter
costs are only paid when the surrounding code path is exercised.
Zero-subscriber dispatch is a single ``_enabled`` short-circuit, so
the overhead for sessions without listeners is negligible.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ericleepi314
ericleepi314 changed the base branch from main to feat/ch12/phase5 May 9, 2026 06:14
@agentforce314
agentforce314 merged commit 43ef185 into feat/ch12/phase5 May 9, 2026
ericleepi314 pushed a commit that referenced this pull request Jul 7, 2026
feat(ch12): phase 6 — hook event emission stream
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jul 20, 2026
feat(orchestrator): F-118 task decomposition and F-124 issue clarifier MVP

Created-by: qq_49552963
Commit-by: wukong;yeyunyao
Merged-by: chadwweng
Description: ## 这次做了什么

### F-124:信息不够就先问,不再直接猜

- Issue 明确写了“细节未定 / 不要猜”时,先暂停派发并向作者追问。
- 只接受作者在追问之后的新回复;GitCode 评论按时间重新排序,避免把新回复当旧评论跳过。
- 机器人评论带隐藏 marker,即使机器人账号和 issue 作者是同一个,也不会把自己的追问当成作者回答。
- POST 没返回 comment ID 时不再靠相同正文猜游标;拿不到可靠游标就保持阻塞。
- GitCode token 改走 `Authorization: Bearer`,不再出现在 URL 和访问日志里。

### F-118:真的拆任务、派 worker,并检查证据

- complex issue 会生成 task graph 和 wave;`mode:swarm` 会启动 coordinator。
- headless 现在不仅过滤工具,还会注入 coordinator 角色提示和 worker 能力上下文,因此模型会调用 `Agent`,不会反复撞不存在的 `Bash/Write`。
- 每个 task 由 write-capable worker 写独立的 `.orchestrator_control/task_evidence/task-N.json`,并行任务不会争写同一个文件。
- validator 检查状态、非空证据和依赖顺序;执行顺序使用 h144 文件系统记录的 evidence mtime,不信任模型自报的时间。
- swarm 未闭环时不会被 workspace-idle 早停抢先判完成。

### 长工具调用不会再被误杀

- 内层 turn timeout 看到 `tool_use` 尚未对应 `tool_result` 时暂停。
- 外层 stream-stall watchdog 复用同一份 pending-tool 状态,不再把 worker 正在运行误判成 provider 卡死。
- 普通模型静默、单工具超时和总运行预算仍然保留。

## click 仓实测

### F-124:先追问,再继续实现

- Issue:[click agentforce314#30](https://gitcode.com/qq_49552963/click/issues/30)
- 实际 PR:[click !24](https://gitcode.com/qq_49552963/click/merge_requests/24)
- 页面上能看到来源 issue、实际分支、commit 和 `Verification: passed`。

![F-124 click 实测](https://raw.gitcode.com/qq_49552963/clawcodex_7420/raw/1f9363ce757b1a57afe08bb0d464dfacde4798ac/docs/assets/orchestrator-demo/f124-issue-clarification.png)

### F-118:四步 swarm 从 issue 跑到 PR

- Issue:[click agentforce314#36](https://gitcode.com/qq_49552963/click/issues/36)
- 实际 PR:[click !31](https://gitcode.com/qq_49552963/click/merge_requests/31)
- 实际生成 4 个 task / 4 个 wave、4 份 task evidence;最终 `tests/test_testing.py` 为 `44 passed, 1 skipped`。
- 为了主动压测误杀问题,h144 上把 turn timeout 临时降到 30 秒;运行中多次记录 timeout deferred,worker 继续工作,最后 session complete、validator 通过并创建 PR。

![F-118 click 实测](https://raw.gitcode.com/qq_49552963/clawcodex_7420/raw/1f9363ce757b1a57afe08bb0d464dfacde4798ac/docs/assets/orchestrator-demo/f118-task-decomposition.png)

## h144 验证

- 环境:openEuler Linux / aarch64,Python 3.11;没有使用 Windows pytest。
- 最终相关回归:`251 passed in 65.64s`。
- click agentforce314#36 实际任务:`44 passed, 1 skipped`,session complete,创建 click PR !31。
- 更大范围回归:`1649 passed, 2 skipped, 18 subtests passed`;另有 3 个与本 MR 无关的既有环境/隔离失败(Linux 登录名假设、临时 session 路径补丁、无测试仓库 regression-guard 期望)。
- Python 编译、`git diff --check`、GitCode push hooks 通过。h144 环境未安装 Ruff,因此不把 Ruff 写成已通过。

## 仍然保留的边界

- task 的状态和测试说明仍由 worker 写入;orchestrator 用独立文件、host mtime 和 fail-closed validator 复核,不把模型一句“完成了”当结果。
- 总运行预算和 per-tool timeout 仍会终止真正卡死的任务;本次只暂停会误伤健康 in-flight 工具的 stall/turn timeout。


See merge request: chadwweng/clawcodex!86
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants