Skip to content

feat(runtime-hooks): P5 async/async_rewake(internal-only)+ run 内存通知队列 + ephemeral 注入 (#493)#544

Merged
pionxe merged 10 commits into1024XEngineer:mainfrom
Cai-Tang-www:feat/hook-p5
May 3, 2026
Merged

feat(runtime-hooks): P5 async/async_rewake(internal-only)+ run 内存通知队列 + ephemeral 注入 (#493)#544
pionxe merged 10 commits into1024XEngineer:mainfrom
Cai-Tang-www:feat/hook-p5

Conversation

@Cai-Tang-www
Copy link
Copy Markdown
Collaborator

Summary

本 PR 落地 #493(P5)最小闭环:在不改 FinalDecider/acceptance 终态裁决的前提下,新增 internal hooks 的 async/async_rewake 执行能力,并实现“run 内存通知队列 + 下一轮 provider 请求前 ephemeral 注入”。

Background

当前 hooks 仅支持 sync,对长耗时观察型逻辑和“异步提醒下一轮处理”场景支持不足。
同时,通知回灌如果落到 session/db 或直接变成对话消息,会污染会话历史并引入状态歧义。

Problem

当前行为:

  • hooks 执行以同步为主,无法安全表达后台观察与下一轮提醒;
  • 没有 run 内存态通知队列,缺少受控的异步回灌路径;
  • TUI/Gateway 缺少 hook_notification 可观测事件。

期望行为:

  • internal hooks 支持 async/async_rewake
  • 异步结果只在 run 生命周期内入队;
  • 下一轮请求前以 system hint 临时注入(ephemeral),不落历史不落库;
  • 通知事件仅可观测,不改变主链阻断/stop/terminal。

影响:

  • 提升运行中“异步观察 -> 下一轮提示”的能力;
  • 避免历史污染和状态机副作用;
  • 为后续 P5/P6 扩展保留稳定边界。

User Scenario

当某个 internal hook 在工具后置观察阶段检测到异常(或需要下一轮补动作)时:

  1. 当前轮不阻塞主链;
  2. runtime 记录一条内存通知;
  3. 下一轮 provider 请求前注入临时 system hint;
  4. TUI Activity 可看到 hook_notification 日志,但不会把它当 user/assistant 消息展示。

Goals

  • 支持 HookModeAsync / HookModeAsyncRewake(internal 可用)。
  • 新增 run 内存通知队列(不写 session/db,不跨 run)。
  • 下一轮请求前 drain 队列并注入 ephemeral system hint。
  • hook_notification 事件贯通 runtime -> gateway/tui(仅可观测)。
  • 异步执行具备 timeout/panic/cancel 防护且无 in-flight 泄漏。
  • user/repo 仍保持 sync-only(含核心层防绕过校验)。

Non-Goals

  • 不重构 FinalDecider/acceptance 主链。
  • 不引入 session 持久化通知或外置队列。
  • 不开放 user/repo async hook 执行。
  • 不把通知转换成对话 transcript 消息。

Proposed Design

1) hooks 核心执行语义

  • sync:保持现状(可阻断、可短路)。
  • async:后台执行,不影响当前 RunOutput.Blocked
  • async_rewake:后台执行;满足条件时产出通知信号。
  • HookResult.Metadata 新增结构化回灌字段:
    • rewake
    • rewake_reason
    • rewake_summary
  • 触发条件仅看结构化状态:failed / block / metadata.rewake=true,不做 message 文本匹配。

2) run 内存通知队列

  • 挂载到 runState,仅 run 生命周期有效:
    • hookNotifications
    • hookNotificationSeen(dedupe)
    • hookNotificationOmitted
  • 限制策略:
    • MaxNotificationsPerRun
    • MaxNotificationChars
    • NotificationTTL
    • MaxInjectedNotificationsPerTurn
    • 超限聚合:N more notifications omitted

3) ephemeral 注入

  • 在 provider 请求构建前 drain 通知队列;
  • 生成 system hint 注入本次 request messages
  • 注入后消费;
  • 不写 session history,不写 session.db,不转成 user/assistant 消息。

4) 生命周期与安全边界

  • 异步回灌前检查 run 是否 active(token + active map);
  • run cancel/stop/timeout 后,通知不再入队;
  • SetHookExecutor 增加 sink 绑定兜底,避免替换执行器后回灌失效;
  • hooks 核心层新增约束:scope=user/repo 仅支持 sync,禁止 async/async_rewake(防绕过配置层)。

Scope

涉及模块:

  • internal/runtime/hooks
  • internal/runtime
  • internal/tui/services
  • internal/tui/core/app

主要文件:

  • internal/runtime/hooks/executor.go
  • internal/runtime/hooks/types.go
  • internal/runtime/hooks/result.go
  • internal/runtime/hooks/events.go
  • internal/runtime/hook_notifications.go(新增)
  • internal/runtime/run.go
  • internal/runtime/runtime.go
  • internal/runtime/hooks_integration.go
  • internal/runtime/state.go
  • internal/runtime/session_scheduler.go
  • internal/runtime/events.go
  • internal/tui/services/runtime_contract.go
  • internal/tui/services/gateway_stream_client.go
  • internal/tui/core/app/update.go

Acceptance Criteria

  • async hook 不阻塞当前执行链。
  • async 失败不影响主链阻断/stop reason。
  • async_rewake 满足条件可入队通知。
  • 下一轮 provider 请求可收到 ephemeral 注入。
  • 注入内容不写入 session history / session.db。
  • run 结束后通知不会继续入队。
  • 重复通知可去重,超长通知被裁剪。
  • 队列超限时有 omitted 聚合。
  • user/repo async 在核心校验层被拒绝。
  • TUI/Gateway 可观测 hook_notification,但不转对话消息。

Test Plan

已执行:

  • go test ./internal/runtime/hooks/...
  • go test ./internal/runtime/...
  • go test ./internal/tui/services ./internal/tui/core/app
  • go test ./internal/config/...

新增/更新覆盖点:

  • hooks executor async/async_rewake 行为测试;
  • runtime 通知队列:入队、去重、TTL、裁剪、限流、drain 注入;
  • run inactive/canceled 后不入队;
  • ephemeral 注入不持久化;
  • TUI hook_notification handler 注册与渲染测试;
  • user/repo scope async 拒绝校验测试。

Risks / Compatibility

风险:

  • 异步执行引入 goroutine 生命周期管理复杂度。
  • 通知注入可能放大 prompt(已通过 per-turn/char 限制控制)。

兼容策略:

  • 保持 sync 路径不变;
  • hook_notification 为新增可选事件,不破坏旧事件消费;
  • user/repo async 默认拒绝,避免权限争议扩散。

Follow-ups

  • 后续可拆分 FinalDecider / NextActionPlanner 责任(本 PR 不扩范围)。
  • 如需开放 user/repo async,需先补独立安全模型与审批边界。

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 2, 2026

Copy link
Copy Markdown

@fennoai fennoai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One functional issue and one documentation issue.

  • docs/runtime-hooks-design.md still says async / async_rewake are unimplemented and does not list hook_notification in the observable event set.
  • docs/guides/configuration.md still describes hooks as sync-only without clarifying that the new async modes are internal-only.

The inline comment below covers the code path that currently breaks provider-agnostic delivery of the new ephemeral notifications.

Comment thread internal/runtime/run.go Outdated
@pionxe
Copy link
Copy Markdown
Collaborator

pionxe commented May 3, 2026

@xgopilot 你能详细、有逻辑且使用通俗易懂的话语解答我下述问题吗?

  1. 当前HOOK机制在项目中起到什么作用?实现了什么?作为系统内部扩展机制要达成的目的是?是“在不修改原有核心执行链路(主函数/主循环)的前提下,拦截、观察或篡改系统的行为”?
  2. 我看当前项目中HOOK机制是有基础底座了,实现的状况怎么样?
  3. 当前pr要实现的异步执行,我的理解是相当于有一个临时队列存储异步通知,最后会被临时作为伪造成系统提示词动态传给LLM,我的理解对吗?

@fennoai
Copy link
Copy Markdown

fennoai Bot commented May 3, 2026

结论先说

你的理解大方向是对的,但有两个地方需要收紧一下:

  1. 当前 HOOK 机制的目标,不是“任意篡改系统行为”,而是把有限的、可控的扩展点插在 runtime 主链路的关键节点上,让系统可以在这些节点做“观察、补充判断、在少数允许的点位阻断”,同时不把逻辑散落到主循环各处。
  2. 这次 PR 的异步能力,也不是把通知正式写成对话历史,而是:异步 hook 产出 run 内存态通知,下一轮请求前临时拼到 system prompt 里,仅影响这一次 provider request,用完就消费掉,不入库、不进 transcript。

1. 当前 HOOK 机制到底在做什么

可以把它理解成:runtime 的“受控扩展插槽”

它不是在代码里随便找地方加回调,而是先定义一组固定挂点,再由 executor 在这些挂点统一调度。挂点定义在 internal/runtime/hooks/types.go,现在已经有这些主要点位:

  • before_tool_call
  • after_tool_result
  • after_tool_failure
  • before_permission_decision
  • before_completion_decision
  • session_start
  • session_end
  • user_prompt_submit
  • pre_compact
  • post_compact
  • subagent_start
  • subagent_stop

这些点位不是“都能改流程”。项目还专门做了一个能力矩阵,也就是每个挂点允许做什么,定义也在 internal/runtime/hooks/types.go

  • 有的点位允许 block,比如 before_tool_calluser_prompt_submitpre_compact
  • 有的点位只允许观察/注释,不允许真正阻断,比如 after_tool_resultbefore_completion_decision
  • 有的点位用户/仓库级 hook 不能碰,比如 before_permission_decisionpre_compactsubagent_start

所以更准确地说,它想达到的目的不是“在不改主循环前提下随意拦截、观察或篡改行为”,而是:

  • 把扩展逻辑从主链路里剥离出去
  • 允许在预定义边界上插入附加判断和观测
  • 把哪些点能阻断、哪些点只能观察,提前收口成规则
  • 避免 runtime/tui/provider 到处散落 if/else 特判

如果再通俗一点说,HOOK 在这里更像“预留的检查岗”和“观测岗”,不是“任意改线的后门”。

2. 当前项目里的 HOOK 底座做到什么程度了

我看下来,底座已经不是空壳,已经有一条比较完整的基础链路了。

2.1 已经具备的核心能力

internal/runtime/hooks/registry.gointernal/runtime/hooks/executor.gointernal/runtime/hooks_integration.go 看,当前已经有:

  • HookSpec 抽象
    • 包含 ID / Point / Scope / Source / Kind / Mode / Priority / Timeout / FailurePolicy / Handler
  • 注册表 Registry
    • 支持注册、按点位解析、按优先级排序
  • 统一执行器 Executor
    • 在某个挂点运行一组 hook
    • 支持超时、panic 防护、失败策略、事件发射
  • runtime 集成层
    • runHookPoint(...) 负责把 run_id / session_id / turn / phase 等运行态信息灌进 hook context
  • 事件桥接
    • hook 执行会发出 hook_started / hook_finished / hook_failed 这类 runtime 事件,TUI 可以消费

2.2 已经接到主链的哪些地方

它不是“只定义了类型没接线”,而是已经接到 runtime 关键路径里了:

  • 工具执行前:internal/runtime/toolexec.go
  • 工具结果后:internal/runtime/toolexec.go
  • 工具失败后:internal/runtime/toolexec.go
  • 权限决策前:internal/runtime/permission.go
  • completion 判断前:internal/runtime/run.go
  • compact 前后:internal/runtime/compact.go
  • session start/end:internal/runtime/run.gointernal/runtime/run_lifecycle.go
  • subagent start/stop:internal/runtime/subagent_run.go

这说明它已经是真实参与主链的内部扩展机制,不是概念层设计。

2.3 现在还处在什么阶段

虽然底座很完整,但当前仍然是**“受控、偏保守”的阶段**,不是完全开放式 hook 平台。

主要体现在:

  • Kind 目前实际上只支持 functioncommand/http/prompt/agent 还是后续预留位,见 internal/runtime/hooks/types.go
  • 原来主路径基本是 sync
  • user/repo hook 目前被限制得很严,internal/runtime/user_hooks.gointernal/runtime/repo_hooks.go 里都在走同步、安全收口的路线
  • 核心层现在还明确禁止 scope=user/repo 使用 async,校验在 internal/runtime/hooks/types.go

所以第二问的准确答案是:

有基础底座,而且已经打进 runtime 主链了;但它目前还是“内部优先、同步优先、能力边界明确”的状态,不是一个完全开放、任意扩展的 hook 系统。

3. 这次 PR 的异步执行,你的理解对不对

你的理解大方向是对的,我把它改成更精确一点的话:

是的,它本质上就是一个 run 生命周期内的内存通知队列,然后在下一轮 provider 请求前,把这些通知临时注入到 system prompt 里,作为这一次请求的附加提示。

但有 4 个关键限定,最好一起记住:

3.1 只有 async_rewake 才会走“通知回灌”

internal/runtime/hooks/executor.go 里现在把模式分成三类:

  • sync:同步执行,保持原有语义
  • async:后台执行,但只是不阻塞主链,不负责回灌
  • async_rewake:后台执行,满足条件时可以产出通知

真正把结果塞进 runtime 通知队列的是 hookAsyncResultSink.HandleAsyncHookResult(...),在 internal/runtime/hook_notifications.go

也就是说:

  • async ≠ 自动通知 LLM
  • async_rewake 才是“异步执行 + 下一轮提醒”

3.2 通知不是正式消息,而是 ephemeral hint

这点非常重要。

internal/runtime/run.go 里,provider 请求构建前会先:

  • drainHookNotificationsForTurn(state)
  • mergeEphemeralHookNotificationIntoSystemPrompt(systemPrompt, notificationHint)

这表示它注入的是:

  • 当前这一轮 request 的 SystemPrompt
  • 不是 session message
  • 不是 user message
  • 不是 assistant message
  • 不是持久化 transcript

所以你说“伪造成系统提示词”这个说法,从效果上可以这么理解;但从实现语义上更准确的说法是:

它是 runtime 在发请求前,临时把一段内部提示拼到 system prompt 中,作为 ephemeral 注入。

3.3 通知队列是 run 内存态,不跨 run

internal/runtime/hook_notifications.gointernal/runtime/state.go 的设计很明确:

  • 通知挂在 runState
  • 有 dedupe
  • 有 TTL
  • 有每次 run 的数量上限
  • 有每轮注入上限
  • run 结束后也不会再接受这次 run 的异步回灌

这就是为什么它要通过 runID + token + activeRunStates 做校验,避免 goroutine 晚到后把过期通知塞回去。

3.4 这个机制本质上是在“提醒下一轮”,不是强行改裁决

PR 描述里一直强调“不改 FinalDecider/acceptance 终态裁决”,代码也确实是这么做的。

异步通知的作用更像:

  • “刚才后台观察到了一个异常”
  • “下一轮你生成时顺便考虑一下这个情况”
  • “它是 guidance,不是硬阻断”

drainHookNotificationsForTurn(...) 生成的提示文本里甚至直接写了:

Use them as guidance only.

所以它不是偷偷改 stop reason,也不是直接改 acceptance,而是给下一轮模型一个内部提醒。

最后给一个最短版本

如果只用几句话总结:

  • 当前 HOOK 机制是 runtime 的受控扩展点系统,目的是在固定挂点做观察、补充判断、有限阻断,而不是开放式篡改主链。
  • 这个底座已经接到工具调用、权限、completion、compact、session 生命周期、subagent 生命周期等关键路径,已经可用,不是半成品概念稿。
  • 这次 PR 的异步能力,本质上是 async_rewake -> run 内存通知队列 -> 下一轮请求前临时 system hint 注入;它只影响当前 request,不写历史、不落库、不改变终态裁决主链。

这次我只做了代码与 PR 语义分析,没有改动代码。

View job run

@pionxe pionxe merged commit 7709343 into 1024XEngineer:main May 3, 2026
3 checks passed
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