Skip to content

feat(hooks): P55 kernel hook system slices A-D with slice C closure (scope B) - #34

Open
hellochica wants to merge 18 commits into
AlchemistCxC:mainfrom
hellochica:Chica/p55
Open

feat(hooks): P55 kernel hook system slices A-D with slice C closure (scope B)#34
hellochica wants to merge 18 commits into
AlchemistCxC:mainfrom
hellochica:Chica/p55

Conversation

@hellochica

Copy link
Copy Markdown
Collaborator

Summary

P55 kernel hook system — slices A/B/C/D complete, slice C closed under scope B (core hook embedding done; E/F deferred to a follow-up task, per user decision 2026-09-09).

Branch: Chica/p55 (based on Chica/dev, fork hellochica/Pylon-co-works). Build-resource files (build.rs, icons/manifest.rc) are content-identical to this repo's main (merged via #33); their appearance in the three-dot diff is relative to the merge-base only and introduces no conflict.

Delivered (vs main)

Slice Content Status
A (D1) Bridge infra + send-chain + platform-inbound anchors done
B (D2) permission/interaction anchors wired into dispatcher (spawn-dispatch, zero await in main loop) done
C (D3) turn.completed/failed/cancelled + tool.beforeCall/afterCall/failed observe dispatch (payloads carry startedAtMs/elapsedMs), replay guarded (C11); RunningSegment segment-state machine kept as a contract-ready artifact (unit-tested) done, closed
D (D4) Registry negotiation + dangerousHooks + trigger (depth-2 hard reject) done
  • 10 of the 16 event anchors are fully triggerable by plugins; message.sealed / agent.status / context.threshold deferred by user decision (kept out of HOOK_NAMES vocabulary, same precedent as feat(gateway): [I12-A-BE-01] 冻结 adapter catalog/instance/route domain contract #14). The sealed-segment decision logic remains in-tree, unit-pinned, ready to wire without re-research.
  • Frontend: event schema/normalizer extended for turn.cancelled tri-state; HOOK_NAMES extended for turn/tool anchors.

Gates

  • cargo test --lib: 926 passed / 0 failed / 4 ignored (baseline 916 + 10 new; 3 transient wire-matrix flakes observed once in parallel mode — serial rerun 4/4 pass, second parallel run full green)
  • vitest (6 domains): 881 passed; tsc 0 errors; lint 0 errors; check:boundaries green

Follow-ups (out of scope for this PR)

  • Slice E (D5): displayHint render-derivation layer
  • Slice F (D6): GUI merge + docs section 6.2 + CLI hook-list kernel domain

Notes for reviewers

  • Only the claimed files were touched per construction rules; no behavior tests were deleted/modified outside the exception-1 list recorded in the issue ledger.
  • dispatcher/mod.rs RunningSegment doc comment carries the final deferred-anchor decision and the future wiring path.

MiyakiKumo and others added 18 commits September 6, 2026 19:19
build.rs panicked with 'windres: program not found': the MinGW tool is
gone from this machine and the MSVC-only toolchain decision leaves no
reason to keep it. Route resource compilation through embed_resource
(already a build-dependency), which locates Windows SDK rc.exe the same
way tauri-winres does, and emit a plain rustc-link-arg so bins, tests,
examples and benches all keep the comctl32 v6 manifest.

icons/icon.rc becomes icons/manifest.rc carrying the manifest only:
with the SDK present tauri-winres embeds the window icon (id 32512),
and a second .rc holding an .ico collides on the internal RT_ICON
entries rc.exe numbers from 1 (CVT1100 duplicate resource, observed).

Ledger: docs Pylon-co-works issue AlchemistCxC#1. cargo build --release and
bun run build:release (exit 0, twice) verified; pylon.exe carries
RT_MANIFEST and the icon group.
D2 code surface ~90%: hook_bridge gains HOOK_PERMISSION_REQUEST /
HOOK_INTERACTION_REQUEST anchors, permission_request_hook_outcome and
interaction_request_hook_outcome seam dispatchers (fail-open to the
legacy flow), dispatcher gains permission_post_normalize_flow /
handle_interaction_request extraction with spawn-off hook tasks (main
loop never awaits the bridge), AppStateHandles carries the bridge Arc,
frontend HOOK_NAMES extends the two anchor names. tsc -b green.

Remaining: 1-2 closure-ownership compile errors in dispatcher/mod.rs
(handle_crash closure moving hook_bridge), D2 tests (auto-allow skip
pending table, no-answer byte-identical regression, interaction
respond/cancel), gates, then slices C/E/F. Not pushed yet (network
down); resume point documented in spec §9.2.
Three repairs to the committed D1 slice, without which no Rust or
boundary gate can run:

1. prompt.rs before_send_hook_transform test deadlocked the whole test
   binary: the responder task blocked on std mpsc recv and pinned the
   single-threaded current_thread runtime, so the ACP reader was never
   polled (session/new response never read) and even the RPC timeout
   timer could not fire. Convert the responder channel to a tokio
   unbounded_channel so recv actually awaits.

2. hookBridgeDispatcher fail-closed test used 'agent.chunk' as the
   out-of-vocabulary example, but D4 (7472b73) added that anchor to
   HOOK_NAMES; the premise is stale. Use a genuinely unknown name.

3. check-runtime-boundaries failed: the D1 bridge dispatcher performs
   direct invoke by design (same pattern as pylonCliBridge) but was
   never allowlisted. Register it with a P55-D1 comment.

Gates after repair: cargo test --lib 914 passed / 0 failed / 4 ignored
(first fully green run since D1 landed); dispatcher hooks suite 6/6.
P55-D2 (spec slice B). Kernel hook coverage for the two gating anchors:

- hook_bridge: HOOK_PERMISSION_REQUEST / HOOK_INTERACTION_REQUEST
  constants, permission_request_hook_outcome and
  interaction_request_hook_outcome seam dispatchers with fail-open
  mapping back to the legacy flow. Permission envelope per contract
  10.2 ({provider, agentId, requestId, payload{title,prompt,options}});
  interaction receives the raw JSON-RPC envelope per 10.3.
- dispatcher: permission.request hook inserted before the bypass/auto
  mode check (hooks must see auto-mode requests); allow/deny pick the
  allow/deny-semantic option and answer via send_agent_response without
  entering the pending table - the hook answer IS the permission
  answer, no second pending map. Legacy flow extracted unchanged into
  permission_post_normalize_flow for the continue/no-answer fallback.
  interaction.request hook runs before the legacy reject: respond
  writes the result with the original request id, cancel maps to a
  JSON-RPC -32600 error, continue keeps the byte-identical reject.
  Both seams spawn off the hook task; the dispatcher main loop never
  awaits the bridge (B1).
- AppStateHandles carries the hook_bridge Arc (dispatcher + crash
  closure paths); frontend HOOK_NAMES extends permission.request /
  interaction.request.
- E0283 in trigger-depth test fixed by typing the None emitter.
- Remaining spawned responders converted from blocking std mpsc recv to
  tokio unbounded channels (same deadlock class as the D1 repair).
- 8 dispatcher d2_hooks tests: allow/deny skip the pending table,
  continue falls back byte-identically, zero registration takes the
  direct legacy path, interaction respond/cancel/continue, missing id
  never reaches the bridge. Verified via python trace-agent roundtrip.

Spec: docs Pylon-co-works P55 slice B (D2). Full gates: cargo test --lib
914/0/4; vitest plugin-runtime 286, hooks 6, application+workbench 491;
tsc 0; eslint 0; check:boundaries green.
…xits

P55 slice C (D3) part 1: turn.cancelled event chain.

- error.rs: add PylonError::PromptCancelled structured variant so stop
  reason cancelled no longer hides inside Protocol prose.
- prompt.rs: PromptFailureMetadata gains outcome (Failed/Cancelled);
  exit 1 (finalize_response stopReason=cancelled) raises
  PromptCancelled, exit 2 (CancelledAfterTimeout) marks failure
  Cancelled; publish_prompt_failure commits sessionUpdate:cancelled
  instead of error when outcome is Cancelled (SESSION_ERROR frame and
  channel terminal unchanged).
- event_repo.rs: normalize sessionUpdate:cancelled -> turn.cancelled;
  code/error extracted for cancelled like error (diagnostics parity).
- frontend: eventSchema CANONICAL_EVENT_TYPES gains turn.cancelled;
  canonicalTurnDuration treats cancelled as terminal (3-state);
  canonicalNormalizer maps wire cancelled -> turn.cancelled.

Tests: event_repo cancelled normalization; prompt exit-1 cross-layer
(fake ACP stopReason=cancelled -> journal turn.cancelled row); failure
metadata outcome serialization; frontend duration/terminal + normalizer
table. Gate: cargo lib 916/0/4, events 98, workbench/hooks/app 783,
tsc 0, lint 0 errors, boundaries green.
Slice C round 2 (D3-2/3/4): wire kernel observe anchors fire-and-forget.

- hook_bridge: six anchor consts (turn.completed/failed/cancelled,
  tool.beforeCall/afterCall/failed) + dispatch_observe helper (result
  discarded; callers spawn, B1 keeps dispatcher main loop await-free)
- prompt.rs: turn.completed spawned after done journal row in
  finalize_response; turn.failed/turn.cancelled spawned from
  publish_prompt_failure keyed on the same outcome discriminator as the
  journal row (mutually exclusive, no double dispatch); profile sessions
  only, matching journal persistence semantics
- dispatcher: tool_call -> tool.beforeCall + startedAt memory record;
  tool_call_update completed/failed -> tool.afterCall/tool.failed with
  startedAtMs+elapsedMs payload (D3-4); replay events skipped (C11)
- dispatcher: RunningSegment/segment_seal_signal/segment_advance -
  message.sealed segment boundary state machine (D3-3), pure functions
  with four-signal unit tests; wire wiring deferred pending frontend
  HOOK_NAMES adjudication (message.sealed absent from the vocabulary,
  see ledger upgrade entry)
- tests: 10 new (2 turn observe cross-layer in prompt.rs, 3 tool
  observe + 1 slow-hook backpressure + 5 segment-state in dispatcher)

Gates: cargo lib 926/0/4, vitest six domains 881, tsc 0, lint 0 errors,
check:boundaries green.
@AlchemistCxC

Copy link
Copy Markdown
Owner

冲突分析结论:需 rebase 重做(不直接合并)

经完整三方分析(main / Chica/p55 / merge-base f679f81),本 PR 不能直接合并——不是普通文件冲突,而是架构代差:

根本问题:分支落后 main 260 commits,基于 P60 改造前的旧 ACP 架构

证据 Chica/p55 main
acp/transport.rs / jsonrpc.rs / request_id.rs 存在(P60 A1c 明令删除) 已删除
acp/engine.rs(SDK 引擎) 不存在 存在
dispatcher 引用旧架构符号(write_tx/transport/jsonrpc/Channel::) 53 处
dispatcher 使用 P60 新 API(engine::/SentRequest/responder) 0 处 全量

P55 的功能增量(observe spawn 派发、tool.afterCall 耗时载荷、段状态机契约件)全部写在旧 ACP 骨架(write_tx / channel bridge)上,而 main 已整体切换 SDK 引擎。强行解冲突 = 产出引用已删模块的代码,cargo 无法编译。

语义裁决(用户拍板 2026-09-10):canonical 保持单终态失败族

冲突中最关键的语义分歧已裁决——cancelled → turn.failed + stopReason=cancelled(main/P60 方向),不引入第二终态 turn.cancelled:

  • Rust event_repo / 前端 canonicalNormalizer / eventSchema:cancelled 归一 turn.failed(P60 aa955ff 语义保留)
  • hook 锚点词表(HOOK_TURN_CANCELLED)是插件观察层,与 canonical 解耦——rebase 后应保留 P55 的 hook 观察锚点(payload 带 outcome=cancelled/failed),这是 P55 的真实价值
  • P55 prompt.rs 的 journal 落 sessionUpdate:"cancelled"(wire 层)正确,无需改

建议路线(主任 21:40 板报选项①,现为唯一可行项)

以最新 main 为基底 cherry-pick / 重写 P55 增量,产出干净 PR:

  1. hook_bridge.rs:P55 完整实现是超集(PermissionHookDecision / InteractionHookDecision / permission_request_hook_outcome / interaction_request_hook_outcome / dispatch_observe),main 仅 interpret_interaction_hook_response(已被 P55 内联取代)→ 以 P55 hook_bridge 为准移植
  2. dispatcher / prompt:把 observe 派发缝、tool 耗时载荷、RunningSegment 段状态机按 P60 SDK API(responder/SentRequest)重写接线
  3. 前端:HOOK_NAMES 词表 + turn/tool 锚点保留,canonical 层按上面裁决

已确认可保留的设计(避免重做时返工)

  • P55 hook 锚点 payload 契约:{source, outcome: cancelled|failed, code, error}
  • RunningSegment 段状态机为契约就绪件(单测锚定,接线路径注释已留 dispatcher)
  • E0283 修法实证:None::<&tauri::Window<tauri::test::MockRuntime>>(非 App)

关联:台账 #6(三选项分析)已登记同结论。此 PR 保持 open 标记 needs-rebase,由后续移植 PR 承接。

@AlchemistCxC AlchemistCxC added the needs-rebase 需 rebase 到最新 main 后重做(架构代差,不能直接合并) label Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase 需 rebase 到最新 main 后重做(架构代差,不能直接合并)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants