chainlesschain 0.162.167 → 0.162.168

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-BZJrchIA.js → AIOps-CDzzuWZV.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Bhlrylt7.js → ActionButton-Dy-8MmQi.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-pHtXVQYs.js → Analytics-BtJ_VWkx.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-ORpuri4R.js → AppLayout-DkjiI4VS.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-eC77yr1_.js → Artifacts-h8PyDO86.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-B2dZIiqz.js → Audit-DiDV0C6l.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-COZiTiwP.js → BackgroundAgents-DTw_aOaf.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-vY_Srl5d.js → Backup-CihFuUO9.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-Dili5BdM.js → BaseInput-CqP2QNoR.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-EzKBf0XL.js → Chat-tm-ygX-p.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-D1CtRAyy.js → ChatBubbleRenderer-BVKEhj_y.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-Beh6BmuW.js → Checkbox-BFMyRN3Q.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-C3fA6kBU.js → Codegen-B1gTeoAS.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CefMqONH.js → Col-1GP8MvKL.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CBAxFJtB.js → Community-BLY938F6.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact--flHc-sM.js → Compact-DVt_p36I.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CPeNLvA6.js → Compliance-DxqEBXVG.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bv5yv4BU.js → Cowork-CyWrM-T8.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-C22yMEas.js → Cron-Domu6aI9.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-Dsb2bcpu.js → Crosschain-C8LKpjHJ.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DAxei4w0.js → DID-XBy15RZ5.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-DEUwuVZ8.js → Dashboard-DDAyPD-V.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-BVp4si2j.js → Dropdown-BHV0MZ13.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-CyFkybOG.js → EmailListRenderer-DmFGW4PX.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BWmmsbVx.js → FamilyGuardDashboard-CqYdbKvd.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-Cxw27A5d.js → Federation-YP5-Puv5.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Q9XthzL1.js → FormItemContext-BOmcIcTe.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DS5DIdsW.js → GenericCardRenderer-CvTZdoc4.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-BOliHfxP.js → Git-Q2oxua0L.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CD4oxa-W.js → Governance-D-WpyWEU.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CMlcivo8.js → Inference-NUtsYHQo.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-BwhERcC4.js → KnowledgeGraph-BHQLa8fr.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BjVcZR5P.js → Logs-6WsR5Q1G.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-BfRhXdyG.js → MarkdownRenderer-DuWoDwMx.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-DQLU3E2w.js → Marketplace-CigohRRW.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-CeK0XkpB.js → McpTools-jcp6jVLf.js} +5 -5
  38. package/src/assets/web-panel/assets/{Memory-DLFkgwaz.js → Memory-B00NU1ph.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-Bh6ymXyj.js → MobileBridge-Cs_p3PzU.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-BFs2WljZ.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-Bx5x9_2m.js → Mtc-CSEflwzw.js} +6 -6
  42. package/src/assets/web-panel/assets/{MtcAudit-a1-Fexsq.js → MtcAudit-dxSiEZ96.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-eaw0X8X1.js → Multisig-CDAFEAyq.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-BaNsPV4k.js → NLProgramming-HoFpZNiB.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-C7pevN2X.js → Notes-tAoQvVcE.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-HDqeoP0r.js → NotificationSettings-Czo2M9F4.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-DvzVl5T3.js → OrderTableRenderer-CM1kNTp6.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-D_xHDlGW.js → Organization-Cudr0VOC.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-B5sRnueL.js → Overflow-BSXeDKiN.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-C-IRjOPW.js → P2P-Cqv_g-F9.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-BqgUPlcU.js → PdhVaultBrowser-7rjprPh8.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-Bm_T1uWw.js → Permissions-97pIDQKE.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DujDllye.js → PersonalDataHub-bparK9eQ.js} +4 -4
  54. package/src/assets/web-panel/assets/{Pipeline-DyNPOLIG.js → Pipeline-CJgerM09.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-CYKTW3iK.js → Privacy-JqFS3PN7.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-CGf03uZ0.js → ProjectInit-DMITd-2c.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-DIec4bTI.js → ProjectSettings-1UkdJ8Ii.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-BY43-bmi.js → Projects-CYZqegqw.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-RrnUmgPk.js → Providers-ChfXz3U0.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-BBWfqd4y.js → QrScannerModal-Rzkka-mf.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-BsICSrFp.js → QuickAsk-DEz9HaON.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-B8y0b4ys.js → Recommend-EFGP8AVu.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-0QgdB1Oe.js → RemoteSession-6um0lGGy.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-DBJGM0YO.js → Reputation-CA1BXTup.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-DtWQ5otv.js → Row-stTnDJ9y.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-2zBxHd5H.js → RssFeed-B10qCCoG.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-BofliSkZ.js → Search-DSJroZ6p.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-ockRBBEL.js → Security-BpXkwiMW.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-QyvG7Vd0.js → Services-BPQxjwK6.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-DwbOndX3.js → Skeleton-CfbKAMul.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-cHWa63sZ.js → Skills-CttYTfX0.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-BHnTEydQ.js → Sla-ByluHich.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-DE2hrrLb.js → SpeechSettings-BdZXKsfV.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-CZ9vnbJH.js → SyncSettings-xCR7iDzy.js} +2 -2
  75. package/src/assets/web-panel/assets/Tasks-qTko_INn.js +1 -0
  76. package/src/assets/web-panel/assets/{Templates-DiR3ad4X.js → Templates-CkIzhgDJ.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-NqcW3frV.js → Tenant-BIaCaaK8.js} +1 -1
  78. package/src/assets/web-panel/assets/Terminal-B2Qq38XP.js +3 -0
  79. package/src/assets/web-panel/assets/{TimelineRenderer-C5GOoxFz.js → TimelineRenderer-D5eQDnEF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-82dz0XdU.js → Tokens-DmeJ8NMQ.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-B3JCNrOi.js → Trigger-C5sgb3C7.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-BehFYsN_.js → Trust-H2vOb3Rb.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BfOxJjVf.js → UkeySign-NmPtHK2u.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DdITCCPz.js → VideoEditing-DHv-Ti7K.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-B0EhkCmD.js → Wallet-BYZ3-PRP.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-DJ4i1f3-.js → WebAuthn-BaSlP7om.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-Dr9XTqIT.js → WorkflowEditor-CZ7IHuIi.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-Dc2nFyOj.js → chat-Dd9kY8gn.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-Drw9djBR.js → colors-yhzo6QSV.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-BdLXbU52.js → compact-item-C1JneVdM.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-BpQMZiWY.js → createContext-C_waVCg2.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DaDlyVDY.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-CbYwawHf.js → hasIn-CkVg0Xnh.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-aHrUfrv1.js → index-1VYhIr6D.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CpmlTfpv.js → index-BTMiVIBI.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-B3uRI8_y.js → index-BYOyb5X2.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DHMeEu-P.js → index-BjDnI7oM.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-COoIIMbO.js → index-BlJNvg-2.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Bu8Vy-mj.js → index-BlzbSUK9.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-XYe6kBmi.js → index-BrN2hAeE.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DO7LD_1t.js → index-BsSsurcn.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BtsLExcc.js +1 -0
  103. package/src/assets/web-panel/assets/{index-BIcEW07n.js → index-Bwuu3Bf3.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BBueKSTR.js → index-Bx3A6X4J.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DWUN8c01.js → index-C-Sscgnv.js} +1 -1
  106. package/src/assets/web-panel/assets/index-C3qDvvIb.js +1 -0
  107. package/src/assets/web-panel/assets/{index-D_lkTnZ0.js → index-C4HNdMCt.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BWUoc9-s.js → index-CBwmwTNa.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-C1eo1Qkd.js → index-CErewFeT.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BZxzdAI0.js → index-CGtoL5mR.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Bo_KwJTF.js → index-CUDjGt_V.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D7Ipfs4F.js → index-C_qqYZT7.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-VIyNlXPX.js → index-C_riAa6d.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-UX_Mfyfv.js → index-CfEMjbhu.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CKmz3Is-.js → index-ClQhh6v3.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Blv7c80V.js → index-CmiLmG0i.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-GG_MXjSL.js → index-D1xIbAxQ.js} +3 -3
  118. package/src/assets/web-panel/assets/{index-_vVucYqC.js → index-D6UXDH2D.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CJQiSQw7.js → index-D7kQ_4uL.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DTeqWdAS.js → index-DPkqQ2Pr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-B9_u4Qhe.js → index-DUA96wEj.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DyU21xVi.js → index-DXHk9Vof.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-IG3__pGj.js → index-DapftLUr.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C3Alz2QD.js → index-Dez7TUgG.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CBwQwtsE.js → index-DhZgULsY.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BAq-4KqY.js → index-Diquc7-l.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BiGDrEt4.js → index-DuGtLiTW.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DSmZjrFy.js → index-U_nLXGIx.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-B1ANzVc6.js → index-_M8El5iz.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-B5KboakZ.js → index-_VmzCYFn.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DKQlF7Pw.js → index-_rZxgFtc.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-BmQlot_u.js → index-ewb0-LoH.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-DybC85E4.js → initDefaultProps-DNN6qtGa.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BNNuyDg7.js → motion-BcM9exeo.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-Bm8gSjEN.js → move-DsKJ-YvA.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-CuHQvzzr.js → mtc-parser-glgGkxaK.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-lv4NO7Ng.js → omit-BwHCU50S.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-BoSMfjHq.js → pickAttrs-BVp-bLt7.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BOMDSvgz.js → placementArrow-CZNe0r-w.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-C064Ubmu.js → responsiveObserve-DUafj9pU.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-CeqhbKoG.js → slide-H4NqXQxu.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-CunFoYww.js → statusUtils-UjMzzGZ5.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-Cxz3wPL7.js → styleChecker-qO5aCjyk.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DJsIaU2S.js → useFlexGapSupport-ChecYwC8.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DnAygceJ.js → useFs-BR0QvwO3.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CrEUY7ta.js → usePersonalDataHub-DitK2Qgz.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-Cq3rN2eT.js → vnode-dxSb40dY.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-TMzcOx__.js → zoom-ekJk8YB2.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/data/changelog.json +5 -4
  151. package/src/harness/worktree-isolator.js +10 -2
  152. package/src/lib/agent-core.js +1 -0
  153. package/src/lib/background-phase-reporter.js +116 -0
  154. package/src/lib/hook-runner.cjs +248 -36
  155. package/src/lib/install-command-policy.js +11 -1
  156. package/src/lib/lsp/code-intelligence.js +5 -1
  157. package/src/lib/lsp/workspace-roots.js +115 -0
  158. package/src/lib/settings-hook-events.cjs +16 -3
  159. package/src/lib/sub-agent-context.js +13 -0
  160. package/src/runtime/agent-core.js +411 -82
  161. package/src/runtime/headless-runner.js +42 -8
  162. package/src/runtime/headless-stream.js +17 -0
  163. package/src/workers/background-agent-worker.js +8 -0
  164. package/src/assets/web-panel/assets/MobileProjects-D950tUIe.js +0 -1
  165. package/src/assets/web-panel/assets/Tasks-DWgoAHvM.js +0 -1
  166. package/src/assets/web-panel/assets/Terminal-iYeqRQZ0.js +0 -3
  167. package/src/assets/web-panel/assets/devWarning-CnfpDzMr.js +0 -1
  168. package/src/assets/web-panel/assets/index-CYTEB0tb.js +0 -1
  169. package/src/assets/web-panel/assets/index-CgsEp5bm.js +0 -1
@@ -163,9 +163,17 @@ export function pruneWorktrees(repoDir) {
163
163
  return before - after;
164
164
  }
165
165
 
166
- export async function isolateTask(repoDir, taskId, fn) {
166
+ export async function isolateTask(repoDir, taskId, fn, options = {}) {
167
167
  const branchName = `agent/${taskId}`;
168
- const { path: worktreePath } = createWorktree(repoDir, branchName);
168
+ // options (large-monorepo): { sparsePaths, symlinkDirectories } forwarded to
169
+ // createWorktree so an isolated task only materializes the packages it needs
170
+ // and reuses approved dep dirs. Empty/absent → full checkout (byte-identical).
171
+ const { path: worktreePath } = createWorktree(
172
+ repoDir,
173
+ branchName,
174
+ undefined,
175
+ options || {},
176
+ );
169
177
 
170
178
  try {
171
179
  const result = await fn(worktreePath);
@@ -46,4 +46,5 @@ export {
46
46
  _resetFileFreshness,
47
47
  _getSharedCodeIntel,
48
48
  disposeSharedCodeIntel,
49
+ _resetSubtreeInstructionLoaders,
49
50
  } from "../runtime/agent-core.js";
@@ -0,0 +1,116 @@
1
+ /**
2
+ * background-phase-reporter — the missing PRODUCER for the background-agent
3
+ * `waiting_permission` phase (P0 state-machine slice,
4
+ * CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"后台 Agent 状态机").
5
+ *
6
+ * The consuming contract has existed for a while: `background-agent-phase.js`
7
+ * normalizes `waiting_permission` and `pendingApprovals > 0` into the
8
+ * dashboard's "Needs input" group, and `session-lifecycle.js` folds them into
9
+ * `waitingApproval`. But nothing ever WROTE those fields — the worker only
10
+ * emits `turn`/`idle`, so a background agent blocked on a human approval
11
+ * (`--permission-prompt-tool`, `--remote-control`) showed as "Working"
12
+ * indefinitely while a decision was actually pending.
13
+ *
14
+ * The production seam is the agent CHILD process, not the worker: the worker
15
+ * spawns each turn with `CC_BACKGROUND_AGENT_ID` in the environment, and the
16
+ * only places a plain headless run genuinely blocks on a human are the
17
+ * confirmer overrides installed on the ApprovalGate. This module wraps those
18
+ * confirmers: while one is awaiting a decision it patches the shared state
19
+ * file with `phase: "waiting_permission"` + a live `pendingApprovals` count,
20
+ * and restores `phase: "turn"` when every pending approval has settled.
21
+ *
22
+ * Safety properties:
23
+ * - Disabled (no `CC_BACKGROUND_AGENT_ID`) → `wrapConfirmer` returns the
24
+ * SAME function object; zero reads, zero writes, byte-identical behavior
25
+ * for every non-background run.
26
+ * - Never resurrects a terminal session: writes are skipped when the current
27
+ * on-disk status is anything other than absent/"running" (mirrors the
28
+ * worker's own writeHeartbeat guard).
29
+ * - Best-effort throughout: a state-file hiccup must never break an approval.
30
+ * - IO is injectable so tests never touch the real user data dir.
31
+ */
32
+
33
+ import {
34
+ readBackgroundAgentState,
35
+ writeBackgroundAgentState,
36
+ } from "./background-agent-supervisor.js";
37
+
38
+ /**
39
+ * Create a phase reporter for the current process.
40
+ *
41
+ * @param {object} [opts]
42
+ * @param {string|null} [opts.agentId] background session id; defaults to
43
+ * `process.env.CC_BACKGROUND_AGENT_ID`. Falsy → disabled no-op reporter.
44
+ * @param {(id:string)=>object|null} [opts.readState] injectable state reader
45
+ * @param {(state:object)=>void} [opts.writeState] injectable state writer
46
+ */
47
+ export function createBackgroundPhaseReporter(opts = {}) {
48
+ const agentId =
49
+ opts.agentId !== undefined
50
+ ? opts.agentId
51
+ : process.env.CC_BACKGROUND_AGENT_ID;
52
+ const readState = opts.readState || readBackgroundAgentState;
53
+ const writeState = opts.writeState || writeBackgroundAgentState;
54
+ const enabled = typeof agentId === "string" && agentId.trim() !== "";
55
+ let pending = 0;
56
+
57
+ function patch(fields) {
58
+ if (!enabled) return false;
59
+ try {
60
+ const current = readState(agentId) || { id: agentId };
61
+ // Terminal states are owned by the worker/stopper — never write a
62
+ // "waiting" phase over a completed/failed/stopped record.
63
+ if (current.status && current.status !== "running") return false;
64
+ writeState({ ...current, id: agentId, ...fields });
65
+ return true;
66
+ } catch {
67
+ return false; // state persistence must never break the approval itself
68
+ }
69
+ }
70
+
71
+ return {
72
+ enabled,
73
+ /** Current in-process pending-approval count (for tests/introspection). */
74
+ pendingCount() {
75
+ return pending;
76
+ },
77
+ /** A human approval just started blocking progress. */
78
+ beginApproval() {
79
+ if (!enabled) return;
80
+ pending++;
81
+ patch({
82
+ phase: "waiting_permission",
83
+ pendingApprovals: pending,
84
+ });
85
+ },
86
+ /** A previously reported approval settled (approved, denied, or failed). */
87
+ endApproval() {
88
+ if (!enabled) return;
89
+ pending = Math.max(0, pending - 1);
90
+ if (pending === 0) {
91
+ // Back to a live turn — the worker owns the turn/idle transition, we
92
+ // only hand the phase back to its steady in-turn value.
93
+ patch({ phase: "turn", pendingApprovals: 0 });
94
+ } else {
95
+ patch({ pendingApprovals: pending });
96
+ }
97
+ },
98
+ /**
99
+ * Wrap a (potentially human-blocking) confirmer so the pending window is
100
+ * visible in the background state. Disabled → returns `confirmer`
101
+ * unchanged (same object identity, zero overhead).
102
+ */
103
+ wrapConfirmer(confirmer) {
104
+ if (!enabled || typeof confirmer !== "function") return confirmer;
105
+ const self = this;
106
+ return async function phaseReportingConfirmer(...args) {
107
+ self.beginApproval();
108
+ try {
109
+ return await confirmer(...args);
110
+ } finally {
111
+ self.endApproval();
112
+ }
113
+ };
114
+ },
115
+ };
116
+ }
@@ -26,8 +26,9 @@ const {
26
26
  buildPowershellArgv,
27
27
  loadShellConfig,
28
28
  } = require("./shell-selector.cjs");
29
+ const { mergeHookDecisions } = require("./hook-event-bus.cjs");
29
30
 
30
- const _deps = { spawnSync: cpDefault.spawnSync };
31
+ const _deps = { spawnSync: cpDefault.spawnSync, spawn: cpDefault.spawn };
31
32
 
32
33
  const HOOK_DECISIONS = Object.freeze({
33
34
  BLOCK: "block",
@@ -116,43 +117,65 @@ function tryParseDecision(stdout) {
116
117
  * stdout?:string, stderr?:string, additionalContext?:string,
117
118
  * nonBlockingError?:boolean, error?:string }}
118
119
  */
120
+ // Circuit breaker (gap 2026-07-11 P2 "Hooks 硬化"): N consecutive non-blocking
121
+ // failures (spawn error / timeout / non-zero non-2 exit) of the SAME command
122
+ // trip an open state — further runs are skipped for a cooldown, then one
123
+ // half-open trial runs; success (or a real block/ask decision) closes it.
124
+ // CC_HOOK_BREAKER_THRESHOLD=0 disables. Extracted so the sync + async runners
125
+ // share ONE breaker implementation.
126
+ function _breakerShortCircuit(command, breaker) {
127
+ if (breaker.threshold <= 0) return null;
128
+ const st = _breaker.get(command);
129
+ if (st && st.fails >= breaker.threshold) {
130
+ const now = Date.now();
131
+ if (now - st.openedAt < breaker.cooldownMs) {
132
+ return {
133
+ decision: HOOK_DECISIONS.CONTINUE,
134
+ reason: `hook circuit breaker open (${st.fails} consecutive failures) — skipped for cooldown`,
135
+ exitCode: null,
136
+ skipped: true,
137
+ breakerOpen: true,
138
+ };
139
+ }
140
+ // half-open: let this one trial through; the outcome records below.
141
+ }
142
+ return null;
143
+ }
144
+
145
+ function _breakerRecord(command, breaker, result) {
146
+ if (breaker.threshold <= 0) return;
147
+ if (result.nonBlockingError) {
148
+ const st = _breaker.get(command) || { fails: 0, openedAt: 0 };
149
+ st.fails += 1;
150
+ if (st.fails >= breaker.threshold) st.openedAt = Date.now();
151
+ _breaker.set(command, st);
152
+ } else {
153
+ _breaker.delete(command); // success / block / ask closes the breaker
154
+ }
155
+ }
156
+
119
157
  function runCommandHook(command, input = {}, opts = {}) {
120
158
  if (!command) {
121
159
  return { decision: HOOK_DECISIONS.CONTINUE, reason: null, exitCode: 0 };
122
160
  }
123
- // Circuit breaker (gap 2026-07-11 P2 "Hooks 硬化"): N consecutive
124
- // non-blocking failures (spawn error / timeout / non-zero non-2 exit) of
125
- // the SAME command trip an open state — further runs are skipped for a
126
- // cooldown, then one half-open trial runs; success (or a real block/ask
127
- // decision) closes it. CC_HOOK_BREAKER_THRESHOLD=0 disables.
128
161
  const breaker = hookBreakerConfig();
129
- if (breaker.threshold > 0) {
130
- const st = _breaker.get(command);
131
- if (st && st.fails >= breaker.threshold) {
132
- const now = Date.now();
133
- if (now - st.openedAt < breaker.cooldownMs) {
134
- return {
135
- decision: HOOK_DECISIONS.CONTINUE,
136
- reason: `hook circuit breaker open (${st.fails} consecutive failures) — skipped for cooldown`,
137
- exitCode: null,
138
- skipped: true,
139
- breakerOpen: true,
140
- };
141
- }
142
- // half-open: let this one trial through; the outcome below decides.
143
- }
144
- }
162
+ const shortCircuit = _breakerShortCircuit(command, breaker);
163
+ if (shortCircuit) return shortCircuit;
145
164
  const result = _runCommandHookInner(command, input, opts);
146
- if (breaker.threshold > 0) {
147
- if (result.nonBlockingError) {
148
- const st = _breaker.get(command) || { fails: 0, openedAt: 0 };
149
- st.fails += 1;
150
- if (st.fails >= breaker.threshold) st.openedAt = Date.now();
151
- _breaker.set(command, st);
152
- } else {
153
- _breaker.delete(command); // success / block / ask closes the breaker
154
- }
165
+ _breakerRecord(command, breaker, result);
166
+ return result;
167
+ }
168
+
169
+ /** Async counterpart of runCommandHook (same breaker + protocol). */
170
+ async function runCommandHookAsync(command, input = {}, opts = {}) {
171
+ if (!command) {
172
+ return { decision: HOOK_DECISIONS.CONTINUE, reason: null, exitCode: 0 };
155
173
  }
174
+ const breaker = hookBreakerConfig();
175
+ const shortCircuit = _breakerShortCircuit(command, breaker);
176
+ if (shortCircuit) return shortCircuit;
177
+ const result = await _runCommandHookInnerAsync(command, input, opts);
178
+ _breakerRecord(command, breaker, result);
156
179
  return result;
157
180
  }
158
181
 
@@ -230,7 +253,19 @@ function _runCommandHookInner(command, input = {}, opts = {}) {
230
253
  nonBlockingError: true,
231
254
  };
232
255
  }
233
- // spawnSync surfaces timeout/ENOENT on res.error (status null) — non-blocking.
256
+ return interpretHookOutcome(res);
257
+ }
258
+
259
+ /**
260
+ * Interpret a finished hook process — a spawnSync result OR the collected output
261
+ * of an async spawn — into a normalized decision. The exit-code / JSON-stdout
262
+ * protocol lives in ONE place so the sync and async runners can never drift.
263
+ *
264
+ * @param {{error?:Error|null, status:number|null, stdout?:string, stderr?:string}} res
265
+ */
266
+ function interpretHookOutcome(res) {
267
+ // A surfaced spawn error (timeout / ENOENT, status null) is non-blocking — a
268
+ // broken hook must never wedge the agent; only an explicit block blocks.
234
269
  if (res.error) {
235
270
  return {
236
271
  decision: HOOK_DECISIONS.CONTINUE,
@@ -287,19 +322,157 @@ function _runCommandHookInner(command, input = {}, opts = {}) {
287
322
  }
288
323
 
289
324
  /**
290
- * Run a list of command hooks in order; first BLOCK (or ASK) short-circuits.
291
- * @returns {{ decision, reason, hook?, results }}
325
+ * Async single-hook runner — mirrors _runCommandHookInner but uses async spawn
326
+ * so a batch can run CONCURRENTLY (see runHooksParallel). stdin gets the JSON
327
+ * payload; stdout/stderr are collected (bounded); a hard timeout SIGKILLs a hung
328
+ * hook and reports it as a non-blocking error. Interpretation is shared via
329
+ * interpretHookOutcome so the exit-code protocol matches the sync path exactly.
330
+ */
331
+ async function _runCommandHookInnerAsync(command, input = {}, opts = {}) {
332
+ const { cwd, event } = opts;
333
+ const _rawTimeout = Number(opts.timeout);
334
+ const timeout =
335
+ Number.isFinite(_rawTimeout) && _rawTimeout > 0
336
+ ? Math.min(_rawTimeout, 600000)
337
+ : 60000;
338
+ const shellKind = normalizeShellKind(opts.shell);
339
+ const usePowershell = shellKind === "powershell" || shellKind === "pwsh";
340
+ const wd = cwd || process.cwd();
341
+ const inputJson = JSON.stringify({
342
+ ...input,
343
+ schema_version: input.schema_version ?? HOOK_PAYLOAD_SCHEMA_VERSION,
344
+ });
345
+ const spawnEnv = {
346
+ ...process.env,
347
+ CLAUDE_HOOK_EVENT: event || input.hook_event_name || "",
348
+ };
349
+
350
+ let child;
351
+ try {
352
+ if (usePowershell) {
353
+ const { executionPolicy } = loadShellConfig({ cwd: wd });
354
+ const inv = buildPowershellArgv(command, shellKind, { executionPolicy });
355
+ child = _deps.spawn(inv.file, inv.argv, { cwd: wd, env: spawnEnv });
356
+ } else {
357
+ child = _deps.spawn(command, { cwd: wd, env: spawnEnv, shell: true });
358
+ }
359
+ } catch (err) {
360
+ return {
361
+ decision: HOOK_DECISIONS.CONTINUE,
362
+ reason: `hook spawn failed: ${err.message}`,
363
+ exitCode: null,
364
+ error: err.message,
365
+ nonBlockingError: true,
366
+ };
367
+ }
368
+
369
+ return await new Promise((resolve) => {
370
+ const MAXB = 8 * 1024 * 1024;
371
+ let stdout = "";
372
+ let stderr = "";
373
+ let settled = false;
374
+ const done = (res) => {
375
+ if (settled) return;
376
+ settled = true;
377
+ clearTimeout(timer);
378
+ resolve(interpretHookOutcome(res));
379
+ };
380
+ const timer = setTimeout(() => {
381
+ try {
382
+ child.kill("SIGKILL");
383
+ } catch {
384
+ /* already exited */
385
+ }
386
+ done({
387
+ error: new Error(`hook timed out after ${timeout}ms`),
388
+ status: null,
389
+ stdout,
390
+ stderr,
391
+ });
392
+ }, timeout);
393
+ if (typeof timer.unref === "function") timer.unref();
394
+
395
+ child.on("error", (err) =>
396
+ done({ error: err, status: null, stdout, stderr }),
397
+ );
398
+ if (child.stdout) {
399
+ child.stdout.on("data", (d) => {
400
+ if (stdout.length < MAXB) stdout += d.toString("utf8");
401
+ });
402
+ }
403
+ if (child.stderr) {
404
+ child.stderr.on("data", (d) => {
405
+ if (stderr.length < MAXB) stderr += d.toString("utf8");
406
+ });
407
+ }
408
+ child.on("close", (code) =>
409
+ done({ error: null, status: code, stdout, stderr }),
410
+ );
411
+ // Feed the payload to the hook's stdin. A fast-exiting hook may close its
412
+ // end first (EPIPE) — that is harmless, the exit code still governs.
413
+ try {
414
+ if (child.stdin) {
415
+ child.stdin.on("error", () => {});
416
+ child.stdin.write(inputJson);
417
+ child.stdin.end();
418
+ }
419
+ } catch {
420
+ /* stdin already closed */
421
+ }
422
+ });
423
+ }
424
+
425
+ /**
426
+ * Run a list of command hooks and reduce to one decision.
427
+ *
428
+ * Default (`opts.mergeStrict` falsy): in-order, the first BLOCK **or** ASK
429
+ * short-circuits — byte-identical to the historical behavior.
430
+ *
431
+ * `opts.mergeStrict: true`: run EVERY matching hook (no short-circuit) then take
432
+ * the STRICTEST decision (block > ask > allow > continue) via mergeHookDecisions.
433
+ * This closes the safety gap where an earlier hook's `ask` masks a LATER hook's
434
+ * `block` (the short-circuit path returns `ask` and never runs the blocker).
435
+ * Claude-Code parity: all matching hooks contribute to the merged decision.
436
+ * spawnSync is synchronous so execution is sequential — but the MERGE is
437
+ * order-independent, so the outcome matches a parallel run.
438
+ *
439
+ * @returns {{ decision, reason, hook?, results, contributing? }}
292
440
  */
293
441
  function runHooks(commandHooks, input = {}, opts = {}) {
294
442
  const results = [];
295
- for (const h of commandHooks || []) {
296
- const r = runCommandHook(h.command, input, {
443
+ const hooks = commandHooks || [];
444
+ const runOne = (h) =>
445
+ runCommandHook(h.command, input, {
297
446
  ...opts,
298
447
  timeout: h.timeout != null ? h.timeout * 1000 : opts.timeout,
299
448
  // per-hook shell selection (P1 #8): `{ "type":"command", "command":…,
300
449
  // "shell":"powershell" }` in the settings hook entry
301
450
  shell: h.shell != null ? h.shell : opts.shell,
302
451
  });
452
+
453
+ if (opts.mergeStrict) {
454
+ const decisions = [];
455
+ for (const h of hooks) {
456
+ const r = runOne(h);
457
+ results.push({ command: h.command, ...r });
458
+ decisions.push({
459
+ decision: r.decision,
460
+ reason: r.reason,
461
+ hook: h.command,
462
+ });
463
+ }
464
+ const merged = mergeHookDecisions(decisions);
465
+ return {
466
+ decision: merged.decision,
467
+ reason: merged.reason,
468
+ hook: merged.hook,
469
+ results,
470
+ contributing: merged.contributing,
471
+ };
472
+ }
473
+
474
+ for (const h of hooks) {
475
+ const r = runOne(h);
303
476
  results.push({ command: h.command, ...r });
304
477
  if (
305
478
  r.decision === HOOK_DECISIONS.BLOCK ||
@@ -316,9 +489,48 @@ function runHooks(commandHooks, input = {}, opts = {}) {
316
489
  return { decision: HOOK_DECISIONS.CONTINUE, reason: null, results };
317
490
  }
318
491
 
492
+ /**
493
+ * Run every matching hook CONCURRENTLY (async spawn) and reduce to the strictest
494
+ * decision (block > ask > allow > continue) via mergeHookDecisions — the true-
495
+ * parallel form of runHooks({mergeStrict}). Wall-clock is the slowest single hook
496
+ * rather than their sum; the merge is order-independent so the outcome matches
497
+ * the sequential strict merge exactly. Same per-hook timeout/shell resolution as
498
+ * runHooks. Returns the same shape ({decision, reason, hook, results,
499
+ * contributing}) so the decision consumers are unchanged.
500
+ */
501
+ async function runHooksParallel(commandHooks, input = {}, opts = {}) {
502
+ const hooks = commandHooks || [];
503
+ const settled = await Promise.all(
504
+ hooks.map((h) =>
505
+ runCommandHookAsync(h.command, input, {
506
+ ...opts,
507
+ timeout: h.timeout != null ? h.timeout * 1000 : opts.timeout,
508
+ shell: h.shell != null ? h.shell : opts.shell,
509
+ }).then((r) => ({ h, r })),
510
+ ),
511
+ );
512
+ const results = settled.map(({ h, r }) => ({ command: h.command, ...r }));
513
+ const decisions = settled.map(({ h, r }) => ({
514
+ decision: r.decision,
515
+ reason: r.reason,
516
+ hook: h.command,
517
+ }));
518
+ const merged = mergeHookDecisions(decisions);
519
+ return {
520
+ decision: merged.decision,
521
+ reason: merged.reason,
522
+ hook: merged.hook,
523
+ results,
524
+ contributing: merged.contributing,
525
+ };
526
+ }
527
+
319
528
  module.exports = {
320
529
  runCommandHook,
530
+ runCommandHookAsync,
321
531
  runHooks,
532
+ runHooksParallel,
533
+ interpretHookOutcome,
322
534
  tryParseDecision,
323
535
  HOOK_DECISIONS,
324
536
  HOOK_PAYLOAD_SCHEMA_VERSION,
@@ -128,8 +128,18 @@ function unwrapArgv(tokens) {
128
128
  * @returns {{manager:string, subcommand:string, packages:string[], global:boolean}|null}
129
129
  */
130
130
  export function classifyInstallSegment(segment) {
131
- const argv = unwrapArgv(tokenizeShellCommand(segment));
131
+ let argv = unwrapArgv(tokenizeShellCommand(segment));
132
132
  if (argv.length === 0) return null;
133
+ // Module invocation: `python -m pip install …` (and `py -m pip …`) is the
134
+ // canonical way run_code's auto-install — and plenty of humans — invoke pip;
135
+ // treat the module as the binary so it classifies like a direct `pip`.
136
+ if (
137
+ /^(python[0-9.]*|py)$/.test(normalizeBin(argv[0])) &&
138
+ argv[1] === "-m" &&
139
+ argv.length > 2
140
+ ) {
141
+ argv = argv.slice(2);
142
+ }
133
143
  const bin = normalizeBin(argv[0]);
134
144
  const mgr = MANAGERS.find((m) => m.bin.test(bin));
135
145
  if (!mgr) return null;
@@ -64,7 +64,11 @@ export class CodeIntelligence {
64
64
  /* plugin LSP contributions are best-effort */
65
65
  }
66
66
  this.manager =
67
- opts.manager || new LSPManager({ projectRoot: opts.projectRoot });
67
+ opts.manager ||
68
+ new LSPManager({
69
+ projectRoot: opts.projectRoot,
70
+ ...(opts.lspOptions || {}),
71
+ });
68
72
  this._ownsManager = !opts.manager;
69
73
  // One-shot callers (the `cc code-intel` CLI) spawn a cold server per run and
70
74
  // must wait for the project to load before querying; the long-lived agent
@@ -0,0 +1,115 @@
1
+ /**
2
+ * workspace-roots — pure helpers for multi-root LSP workspaces (P2 LSP slice,
3
+ * CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"LSP 与 Code Review").
4
+ *
5
+ * A session started with `--add-dir` works across several project roots, but
6
+ * `code_intelligence` always keyed its shared language-server pool on the cwd
7
+ * alone — a query for a file living in an additional root spawned (or reused)
8
+ * the WRONG project's server, and `workspace_symbols` could only ever see the
9
+ * cwd project. These helpers make root selection explicit and testable:
10
+ *
11
+ * - `workspaceRootsFor(cwd, additionalDirectories)` — the deduped, resolved
12
+ * root list (cwd first, mirroring the roots the system prompt advertises).
13
+ * - `pickRootForFile(file, roots)` — the root that CONTAINS the file
14
+ * (deepest match wins so nested roots resolve to the more specific
15
+ * project); falls back to the first root (cwd) for out-of-root files,
16
+ * which is the pre-multi-root behavior.
17
+ * - `mergeWorkspaceSymbolResults(results, roots)` — fold per-root
18
+ * `workspaceSymbols` outcomes into one: available when ANY root answered,
19
+ * symbols concatenated in root order and stamped with their `root` so
20
+ * same-named symbols from different projects stay unambiguous.
21
+ *
22
+ * Pure + IO-free (path math only) so agent-core can consume them without any
23
+ * new export surface of its own.
24
+ */
25
+
26
+ import path from "node:path";
27
+
28
+ /** Case-fold for prefix comparison on case-insensitive filesystems. */
29
+ function fold(p) {
30
+ return process.platform === "win32" ? p.toLowerCase() : p;
31
+ }
32
+
33
+ /**
34
+ * Resolved, deduped workspace roots — cwd first, then each additional
35
+ * directory. Non-string/empty entries are dropped; never returns empty (falls
36
+ * back to process.cwd()).
37
+ */
38
+ export function workspaceRootsFor(cwd, additionalDirectories) {
39
+ const out = [];
40
+ const seen = new Set();
41
+ const push = (dir) => {
42
+ if (typeof dir !== "string" || dir.trim() === "") return;
43
+ const resolved = path.resolve(dir);
44
+ const key = fold(resolved);
45
+ if (seen.has(key)) return;
46
+ seen.add(key);
47
+ out.push(resolved);
48
+ };
49
+ push(cwd || process.cwd());
50
+ for (const d of Array.isArray(additionalDirectories)
51
+ ? additionalDirectories
52
+ : []) {
53
+ push(d);
54
+ }
55
+ if (out.length === 0) out.push(path.resolve(process.cwd()));
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * The workspace root that contains `file` — deepest containing root wins
61
+ * (nested roots resolve to the more specific project). Files outside every
62
+ * root fall back to the FIRST root (cwd), preserving the single-root
63
+ * behavior byte-for-byte.
64
+ */
65
+ export function pickRootForFile(file, roots) {
66
+ const list = Array.isArray(roots) && roots.length > 0 ? roots : null;
67
+ if (!list) return path.resolve(process.cwd());
68
+ if (typeof file !== "string" || file.trim() === "") return list[0];
69
+ const f = fold(path.resolve(file));
70
+ let best = null;
71
+ for (const root of list) {
72
+ const r = fold(path.resolve(root));
73
+ if (f === r || f.startsWith(r + path.sep)) {
74
+ if (best === null || r.length > fold(best).length) {
75
+ best = path.resolve(root);
76
+ }
77
+ }
78
+ }
79
+ return best || list[0];
80
+ }
81
+
82
+ /**
83
+ * Merge per-root `workspaceSymbols` results (same order as `roots`).
84
+ * Any available root makes the merged result available; each symbol is
85
+ * stamped with its `root`. All-unavailable folds the reasons together so the
86
+ * caller still sees WHY (first reason wins the summary, all are listed).
87
+ */
88
+ export function mergeWorkspaceSymbolResults(results, roots = []) {
89
+ const list = Array.isArray(results) ? results : [];
90
+ const symbols = [];
91
+ const reasons = [];
92
+ let anyAvailable = false;
93
+ for (let i = 0; i < list.length; i++) {
94
+ const r = list[i];
95
+ if (r && r.available) {
96
+ anyAvailable = true;
97
+ for (const s of Array.isArray(r.symbols) ? r.symbols : []) {
98
+ symbols.push(roots[i] ? { ...s, root: roots[i] } : s);
99
+ }
100
+ } else if (r) {
101
+ reasons.push(
102
+ roots[i]
103
+ ? `${roots[i]}: ${r.reason || "unavailable"}`
104
+ : r.reason || "unavailable",
105
+ );
106
+ }
107
+ }
108
+ if (!anyAvailable) {
109
+ return {
110
+ available: false,
111
+ reason: reasons.join("; ") || "language server unavailable",
112
+ };
113
+ }
114
+ return { available: true, symbols };
115
+ }
@@ -47,7 +47,15 @@ function withDeliveryId(event, payload, { sessionId, traceId, parentId } = {}) {
47
47
  /* best-effort — recording must not break the turn */
48
48
  }
49
49
  }
50
- return { ...payload, event_id: env.event_id };
50
+ // trace_id/parent_id are stamped onto the DELIVERED payload only when the
51
+ // caller actually threaded them (agent-core context) — absent they are
52
+ // omitted entirely, keeping every legacy payload byte-identical.
53
+ return {
54
+ ...payload,
55
+ event_id: env.event_id,
56
+ ...(env.trace_id ? { trace_id: env.trace_id } : {}),
57
+ ...(env.parent_id ? { parent_id: env.parent_id } : {}),
58
+ };
51
59
  }
52
60
 
53
61
  /**
@@ -343,7 +351,7 @@ function runObserveHooks(
343
351
  settingsHooks,
344
352
  event,
345
353
  payload = {},
346
- { cwd, matchTarget } = {},
354
+ { cwd, matchTarget, traceId, parentId } = {},
347
355
  ) {
348
356
  if (!settingsHooks) return { decision: "continue", results: [] };
349
357
  const matched = collectHooks(settingsHooks, event, matchTarget || "");
@@ -351,12 +359,17 @@ function runObserveHooks(
351
359
  if (sync.length === 0) return { decision: "continue", results: [] };
352
360
  return runHooks(
353
361
  sync,
354
- withDeliveryId(event, { hook_event_name: event, cwd, ...payload }),
362
+ withDeliveryId(
363
+ event,
364
+ { hook_event_name: event, cwd, ...payload },
365
+ { traceId, parentId },
366
+ ),
355
367
  { cwd, event },
356
368
  );
357
369
  }
358
370
 
359
371
  module.exports = {
372
+ withDeliveryId,
360
373
  runUserPromptSubmitHooks,
361
374
  runSessionStartHooks,
362
375
  runCwdChangedHooks,