chainlesschain 0.162.169 → 0.162.171

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 (170) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-DCeNRK8z.js → AIOps-Cf8yb18C.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-ZfD0HgVe.js → ActionButton-CsFT6FMt.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-Ccn40f9E.js → Analytics-DQe_hXMA.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-DyXs4nSt.js → AppLayout-Cl9Pp7jk.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-CYjMh5KR.js → Artifacts-CBEzLVjF.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-DZIhvx88.js → Audit-BS2Un14C.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-C1XJsa2d.js → BackgroundAgents-DmcDH8OK.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-CMXC0oUN.js → Backup-DvzjTkkn.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CRVmIH1t.js → BaseInput-DnbpuyME.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-acBud5Yj.js → Chat-BJRRCm2P.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CTjqFhdD.js → ChatBubbleRenderer-Bgpxyqd_.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-CZem825o.js → Checkbox-DFOrWmvm.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-B9UjZsww.js → Codegen-BIQ2kU6Q.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Dtu6Frtj.js → Col-BI3R-N4I.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-9bV55Huv.js → Community-CPiAwsHT.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-g-kQ4c84.js → Compact-Bv4cslSK.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CQjHzBO1.js → Compliance-CSlDG1NZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-D1Qm_Xhi.js → Cowork-CFcRumXf.js} +4 -4
  20. package/src/assets/web-panel/assets/{Cron-992qo8zX.js → Cron-DKYKMmOn.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-4TiqEwuc.js → Crosschain-CxPMByp2.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BbsXzXCb.js → DID-DJqiYw1C.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Dx5aO9d3.js → Dashboard-D2dLMxqy.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-BhzW6i6i.js → Dropdown-BnJc384A.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-qEib4yiq.js → EmailListRenderer-DctFvm-J.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BZ3N-u3h.js → FamilyGuardDashboard-J_xg-Qop.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CcOqwzHY.js → Federation-BckL07Uw.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-dbi8mohd.js → FormItemContext-B8s5ccSO.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-57dqulUe.js → GenericCardRenderer-hPMBK2No.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DtSV7C5g.js → Git-CfMpYLo7.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CLnmH-RD.js → Governance-Bt3OcrC7.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-wXiLeDnk.js → Inference-BlKc336N.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-NGdIX2Gx.js → KnowledgeGraph-B6p7og2c.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-Dz0HQn69.js → Logs-uv5Re6YR.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-CId5JWjX.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-DpPDmY3W.js → Marketplace-CfNbntHt.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-UMysoRQm.js → McpTools-YRXt74Yk.js} +5 -5
  38. package/src/assets/web-panel/assets/{Memory-DsnBpQnY.js → Memory-BfQQs-Jd.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-DweooAfG.js → MobileBridge-BuGC-4S2.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-UdLfZOVA.js → MobileProjects-Czieqw1m.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-oXWhJNgC.js → Mtc-DHWHOVCO.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-CXTD7qrm.js → MtcAudit-DWSEbMW-.js} +4 -4
  43. package/src/assets/web-panel/assets/{Multisig-BKt5mJ9O.js → Multisig-DrhEv1vq.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-C-zcYzje.js → NLProgramming-D8wKCu7E.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-CGaZ_jep.js → Notes-AFeqq0I_.js} +4 -4
  46. package/src/assets/web-panel/assets/{NotificationSettings-BwX-tDbq.js → NotificationSettings-D6qenYUo.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-Cc9Se8H-.js → OrderTableRenderer-CF0X4fyB.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-8BXdC7Im.js → Organization-DdfbNqf6.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-zCisFuGU.js → Overflow-BFEvnLfd.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-BehHH9Ro.js → P2P-DtL6wbkz.js} +2 -2
  51. package/src/assets/web-panel/assets/PdhVaultBrowser-CQPriTud.js +7 -0
  52. package/src/assets/web-panel/assets/{Permissions-BVDATm3p.js → Permissions-Bj25ANmR.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-C43nnYoi.js → PersonalDataHub-BNj2Oux0.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-D9E7aSVD.js → Pipeline-Dyj8UEaG.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-CRbIchdL.js → Privacy-BBUrWRFz.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-CTsy8lhL.js → ProjectInit-BdX7uQn6.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-CPEKWWJ5.js → ProjectSettings-BghtW1zr.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-CNnW0v5N.js → Projects-9VTquZCz.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-BeZnDL9Y.js → Providers-D-0K_Eza.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CaydwuIU.js → QrScannerModal-DDaN30_D.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-qYWbu7kw.js → QuickAsk-DfHlifOj.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-RV_ogHrM.js → Recommend-CgLIvYE2.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-Mah1i41_.js → RemoteSession-DpOPrwE4.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-xZQhGktc.js → Reputation-wMexlbCE.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-B6cVbPbJ.js → Row-D9aD_nrc.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-C6hKjDPz.js → RssFeed-Drg7l6zu.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-0jti_-Pe.js → Search-BKO1nrwN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-Cy48feGz.js → Security-G3Kyk-TG.js} +3 -3
  69. package/src/assets/web-panel/assets/{Services-BW3ObFxH.js → Services-xJReTOna.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-CfrZRvAM.js → Skeleton-ldVDVTPG.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-yoccdk3t.js → Skills-emwNYV0j.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-AKBBdri6.js → Sla-CZYwGp1K.js} +1 -1
  73. package/src/assets/web-panel/assets/SpeechSettings-CyaPrzOF.js +1 -0
  74. package/src/assets/web-panel/assets/{SyncSettings-DUuO-ZSw.js → SyncSettings-Dcs5Hhj6.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-ZTUfOzMZ.js → Tasks-CerFAo26.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-shPrQ4NE.js → Templates-BdZUbOg3.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-C_inPunR.js → Tenant-B4c_7Bsw.js} +1 -1
  78. package/src/assets/web-panel/assets/Terminal-VVFLS3Ol.js +3 -0
  79. package/src/assets/web-panel/assets/{TimelineRenderer-Bzp_T3Xc.js → TimelineRenderer-tc6sp0TV.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-kUFRLdtp.js → Tokens-Bw_vkMJq.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-BsTU2blQ.js → Trigger-DkbJnpvz.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DEKkuAsi.js → Trust-CIf9kqN1.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-D1KEEgbK.js → UkeySign-Cjwllnea.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-C2yYkm9W.js → VideoEditing-kgfkY7AN.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-BKKod4qb.js → Wallet-CYWXt4u9.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-XXUgED6d.js → WebAuthn-QgPwyWCO.js} +3 -3
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DnlEyYTi.js → WorkflowEditor-Co0lha18.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-BaVjI6nu.js → chat-CI__Ox-r.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-DQ0yx43H.js → colors-Csv0eYYd.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-DEyWDp0v.js → compact-item-e53UCdHy.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-SuPOfylZ.js → createContext-BMPGRDhV.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-BIulobq7.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-CQz1JNtC.js → hasIn-Dz68qIAq.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BJEIjYhV.js → index--7tHEadk.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BIgWgye3.js → index-53ljQo_x.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-C_fcNCoV.js → index-5iicv5af.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BffmUosH.js → index-B1s1B8Iy.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CgvnudM_.js → index-B4lKPJy1.js} +1 -1
  99. package/src/assets/web-panel/assets/index-BGkiYnET.js +1 -0
  100. package/src/assets/web-panel/assets/{index-COsmMpAO.js → index-BHkDRNA5.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-57GdL9ro.js → index-BOpz2qS1.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DjZ6szGM.js → index-BT3CV4Nh.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DpPv8zeU.js → index-BW2dImbg.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CpR6M7eQ.js → index-C-NbcAbP.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Dfh6OJKz.js → index-C53o-MdJ.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CfVW-cd_.js → index-C5hLtxVz.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-ByGF9ev7.js → index-CNtZMPKV.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-ZKOZ-YFJ.js → index-CXuON9OG.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CGu-b0RE.js → index-CYUxs6J1.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-3xcB2i6I.js → index-CoGSbW5J.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DB8AJ6T-.js → index-CpTzqJ0Z.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-XjQfQBXH.js → index-CrsoSnQF.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CZrARoXd.js → index-CvLBQk_M.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-DWIuZ9bI.js → index-CyLUaP2i.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CEfLMvKf.js → index-D0bg74e1.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-ZMgCYtI7.js → index-DCl4Xb4N.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DXczrVij.js → index-DJqQL6so.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-fGgx2f1y.js → index-DVCd2aMX.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BHJhyrFr.js → index-DctYrVMq.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-v8PYYw05.js → index-Dk9Gi8xd.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-fcA6tfBA.js → index-DnXm3Qsh.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C3Vlo45C.js → index-Dtjm1K1a.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-wBsuJZcz.js → index-DyqcHiL1.js} +1 -1
  124. package/src/assets/web-panel/assets/index-Eyhrr_3u.js +1 -0
  125. package/src/assets/web-panel/assets/{index-CnhJU5To.js → index-HbhQRrM2.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D5SY6Lwu.js → index-K5E_wjut.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DhBeUGCp.js → index-N9DkR1i7.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CI5jlfyn.js → index-_LmPlyeV.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DNYVGZzG.js → index-ajz0E0pK.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-C2k8eHyq.js → index-t6f80uqL.js} +17 -17
  131. package/src/assets/web-panel/assets/{index-C-IKjQD3.js → index-wcCouSNq.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D-gXgEsU.js → index-ygfUVVFv.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-KetDXPXx.js → initDefaultProps-DVCeQJrT.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BfAtwo2m.js → motion-chBCR_8v.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-BnJvzHgx.js → move-BziPlrh6.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-DZxDclhM.js → mtc-parser-Cenk0OD7.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-CmqfX2P_.js → omit-D-AiRsei.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-B2B2ahzC.js → pickAttrs-CHw3Fvqt.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-C1SIgWRj.js → placementArrow-g8I-rCad.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-Dy_adDTl.js → responsiveObserve-BWffAr9k.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-hDO9sBPk.js → slide-BmUQqhkb.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-DlpTjRpg.js → statusUtils-B89gCO-y.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-DAv9eo6h.js → styleChecker-x1bnCF73.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-vfoXaeH8.js → useFlexGapSupport-B-k1_Ggw.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-Dzji-S7v.js → useFs-Bc3PUqQk.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-jKcLN_Gj.js → usePersonalDataHub-D8qFDX1I.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-DAWM5GgY.js → vnode-UaL5-WGg.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-q3XgSOUd.js → zoom-Dv7bsbLs.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agent.js +39 -14
  151. package/src/commands/background-session.js +12 -1
  152. package/src/data/changelog.json +7 -2
  153. package/src/gateways/ws/ws-agent-handler.js +26 -1
  154. package/src/gateways/ws/ws-approval-gate.js +131 -0
  155. package/src/lib/agent-worktree.js +173 -5
  156. package/src/lib/background-agent-supervisor.js +234 -14
  157. package/src/lib/interaction-adapter.js +26 -5
  158. package/src/lib/packer/smoke-runner.js +5 -0
  159. package/src/lib/repl-rewind.js +62 -14
  160. package/src/lib/turn-binding.js +134 -0
  161. package/src/repl/agent-repl.js +75 -3
  162. package/src/repl/repl-turn-binding.js +91 -0
  163. package/src/runtime/headless-runner.js +59 -0
  164. package/src/assets/web-panel/assets/MarkdownRenderer-CSRZ9da3.js +0 -1
  165. package/src/assets/web-panel/assets/PdhVaultBrowser-DVuXYKAR.js +0 -7
  166. package/src/assets/web-panel/assets/SpeechSettings-DH87GbkH.js +0 -1
  167. package/src/assets/web-panel/assets/Terminal-CvVtx6ie.js +0 -3
  168. package/src/assets/web-panel/assets/devWarning-C_RK48j-.js +0 -1
  169. package/src/assets/web-panel/assets/index-CDqhisfz.js +0 -1
  170. package/src/assets/web-panel/assets/index-CKUoX6Yi.js +0 -1
@@ -6,16 +6,21 @@ import {
6
6
  openSync,
7
7
  readFileSync,
8
8
  readdirSync,
9
+ realpathSync,
9
10
  renameSync,
10
11
  rmSync,
11
12
  statSync,
12
13
  writeFileSync,
13
14
  } from "node:fs";
14
15
  import { randomBytes } from "node:crypto";
15
- import { join } from "node:path";
16
+ import { join, resolve } from "node:path";
16
17
  import { fileURLToPath } from "node:url";
17
18
  import { getHomeDir } from "./paths.js";
18
19
  import { deriveSessionState } from "./session-lifecycle.js";
20
+ import {
21
+ finishAgentWorktree,
22
+ validateAgentWorktree,
23
+ } from "./agent-worktree.js";
19
24
 
20
25
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
21
26
  export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
@@ -135,19 +140,28 @@ function defaultReadProcessStartTimeMs(pid) {
135
140
  function defaultKillProcessTree(pid, signal = "SIGKILL") {
136
141
  const target = Number(pid);
137
142
  if (!Number.isInteger(target) || target <= 0) return false;
143
+ // Never signal ourselves: no legitimate background-agent record can point at
144
+ // the process performing the reclaim, so a matching pid is a corrupt/stale
145
+ // record — killing it would take down the caller (a `cc` CLI process, or a
146
+ // vitest worker when a test drives this path with a live-looking fixture).
147
+ if (target === process.pid) return false;
138
148
  try {
139
149
  if (process.platform === "win32") {
140
- const r = spawnSync("taskkill", ["/PID", String(target), "/T", "/F"], {
141
- windowsHide: true,
142
- encoding: "utf8",
143
- });
150
+ const r = _deps.spawnSync(
151
+ "taskkill",
152
+ ["/PID", String(target), "/T", "/F"],
153
+ {
154
+ windowsHide: true,
155
+ encoding: "utf8",
156
+ },
157
+ );
144
158
  return !r.error && r.status === 0;
145
159
  }
146
160
  try {
147
- process.kill(-target, signal);
161
+ _deps.kill(-target, signal);
148
162
  return true;
149
163
  } catch {
150
- process.kill(target, signal);
164
+ _deps.kill(target, signal);
151
165
  return true;
152
166
  }
153
167
  } catch {
@@ -158,6 +172,12 @@ function defaultKillProcessTree(pid, signal = "SIGKILL") {
158
172
  export const _deps = {
159
173
  spawn,
160
174
  spawnSync,
175
+ // Single POSIX signal seam: every kill this module issues goes through here
176
+ // so tests can stub it — a bare `process.kill` is NOT interceptable and a
177
+ // fixture recording a live pid would eat a REAL signal (the shard-2/4
178
+ // "worker-death" CI flake: a stop-flow test recorded pid=process.pid and
179
+ // SIGTERMed its own vitest worker).
180
+ kill: (pid, signal) => process.kill(pid, signal),
161
181
  readProcessStartTimeMs: defaultReadProcessStartTimeMs,
162
182
  killProcessTree: defaultKillProcessTree,
163
183
  };
@@ -265,6 +285,20 @@ export function writeBackgroundAgentState(state) {
265
285
  if (Number(current.pinnedAt || 0) > Number(next.pinnedAt || 0)) {
266
286
  next = { ...next, pinned: current.pinned, pinnedAt: current.pinnedAt };
267
287
  }
288
+ // A background worktree is immutable session identity, just like the
289
+ // conversation id. Heartbeat/finalize/rename writers may race with a stale
290
+ // snapshot, but no same-id transition is allowed to move the run back to
291
+ // the main checkout or drop the cleanup metadata.
292
+ if (current.worktreePath) {
293
+ next = {
294
+ ...next,
295
+ cwd: current.worktreePath,
296
+ repoRoot: current.repoRoot,
297
+ worktreePath: current.worktreePath,
298
+ baseSha: current.baseSha,
299
+ branch: current.branch,
300
+ };
301
+ }
268
302
  }
269
303
  const tmp = `${target}.${process.pid}.tmp`;
270
304
  writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
@@ -485,7 +519,7 @@ export function renameBackgroundAgent(id, title, options = {}) {
485
519
  * conversation session is NOT touched — that's `cc session delete`.
486
520
  */
487
521
  export function removeBackgroundAgent(id, options = {}) {
488
- const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
522
+ let state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
489
523
  now: options.now,
490
524
  heartbeatStaleMs: options.heartbeatStaleMs,
491
525
  });
@@ -498,15 +532,28 @@ export function removeBackgroundAgent(id, options = {}) {
498
532
  }
499
533
  try {
500
534
  stopBackgroundAgent(id);
535
+ state =
536
+ effectiveBackgroundAgentState(readBackgroundAgentState(id), {
537
+ now: options.now,
538
+ heartbeatStaleMs: options.heartbeatStaleMs,
539
+ }) || state;
501
540
  } catch {
502
541
  /* best-effort — the record removal below is the point of rm --force */
503
542
  }
504
543
  }
544
+ const worktree = cleanupBackgroundAgentWorktree(state, {
545
+ keepWorktree: options.keepWorktree === true,
546
+ });
505
547
  rmSync(statePath(id), { force: true });
506
548
  if (options.keepLog !== true) {
507
549
  rmSync(logPath(id), { force: true });
508
550
  }
509
- return { id, removed: true, status: state.status };
551
+ return {
552
+ id,
553
+ removed: true,
554
+ status: state.status,
555
+ ...(worktree ? { worktree } : {}),
556
+ };
510
557
  }
511
558
 
512
559
  /**
@@ -583,6 +630,68 @@ export function buildFollowUpArgv(argv, opts = {}) {
583
630
  return out;
584
631
  }
585
632
 
633
+ function canonicalPath(value) {
634
+ const absolute = resolve(String(value || ""));
635
+ try {
636
+ const real = realpathSync.native
637
+ ? realpathSync.native(absolute)
638
+ : realpathSync(absolute);
639
+ return process.platform === "win32" ? real.toLowerCase() : real;
640
+ } catch {
641
+ return process.platform === "win32" ? absolute.toLowerCase() : absolute;
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Normalize and verify worktree metadata before it enters a background job.
647
+ * The metadata is persisted as top-level state fields for compatibility with
648
+ * status/IDE consumers: repoRoot/worktreePath/baseSha/branch.
649
+ */
650
+ export function normalizeBackgroundWorktree(worktree, cwd) {
651
+ if (!worktree) return null;
652
+ const normalized = {
653
+ repoRoot: resolve(String(worktree.repoRoot || "")),
654
+ worktreePath: resolve(String(worktree.worktreePath || worktree.path || "")),
655
+ baseSha: String(worktree.baseSha || ""),
656
+ branch: String(worktree.branch || ""),
657
+ };
658
+ if (
659
+ !normalized.repoRoot ||
660
+ !normalized.worktreePath ||
661
+ !normalized.baseSha ||
662
+ !normalized.branch
663
+ ) {
664
+ throw new Error(
665
+ "Cannot launch background agent: incomplete worktree metadata",
666
+ );
667
+ }
668
+ if (canonicalPath(cwd) !== canonicalPath(normalized.worktreePath)) {
669
+ throw new Error(
670
+ "Cannot launch background agent: cwd does not match the persisted worktree path",
671
+ );
672
+ }
673
+ const validation = validateAgentWorktree({
674
+ ...normalized,
675
+ path: normalized.worktreePath,
676
+ });
677
+ if (!validation.valid) {
678
+ throw new Error(
679
+ `Cannot launch background agent: worktree metadata is not usable (${validation.reason})`,
680
+ );
681
+ }
682
+ return normalized;
683
+ }
684
+
685
+ function persistedWorktree(state) {
686
+ if (!state?.worktreePath) return null;
687
+ return {
688
+ repoRoot: state.repoRoot,
689
+ worktreePath: state.worktreePath,
690
+ baseSha: state.baseSha,
691
+ branch: state.branch,
692
+ };
693
+ }
694
+
586
695
  /**
587
696
  * Continue a finished/crashed background session as a NEW background session
588
697
  * on the same conversation (`cc daemon resume <id> <prompt>`). The original
@@ -607,14 +716,27 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
607
716
  }
608
717
  const text = String(prompt || "").trim();
609
718
  if (!text) throw new Error("resume requires a prompt");
719
+ const worktree = persistedWorktree(state);
720
+ if (
721
+ worktree &&
722
+ options.cwd &&
723
+ canonicalPath(options.cwd) !== canonicalPath(worktree.worktreePath)
724
+ ) {
725
+ throw new Error(
726
+ `Background agent ${id} is bound to worktree "${worktree.worktreePath}" and cannot resume in a different cwd`,
727
+ );
728
+ }
610
729
  const argv = ["agent", "--session", state.sessionId, "-p", text];
611
730
  return launchBackgroundAgent({
612
731
  argv,
613
- cwd: options.cwd || state.cwd || process.cwd(),
732
+ // Worktree sessions fail closed: never fall back to process.cwd(), which
733
+ // could silently resume a write task in the main checkout.
734
+ cwd: worktree?.worktreePath || options.cwd || state.cwd || process.cwd(),
614
735
  sessionId: state.sessionId,
615
736
  title: options.title || state.title || text.slice(0, 100),
616
737
  cliEntry: options.cliEntry,
617
738
  followUpArgv: ["agent", "--session", state.sessionId],
739
+ worktree,
618
740
  });
619
741
  }
620
742
 
@@ -652,8 +774,10 @@ export function launchBackgroundAgent({
652
774
  title,
653
775
  cliEntry,
654
776
  followUpArgv,
777
+ worktree,
655
778
  }) {
656
779
  assertUsableCwd(cwd);
780
+ const worktreeState = normalizeBackgroundWorktree(worktree, cwd);
657
781
  const id = createBackgroundAgentId();
658
782
  const dir = backgroundAgentsDir();
659
783
  const jobFile = join(dir, `${id}.job.${process.pid}.json`);
@@ -668,6 +792,7 @@ export function launchBackgroundAgent({
668
792
  title: title || "Background agent",
669
793
  cliEntry: cliEntry || process.argv[1],
670
794
  logFile: logPath(id),
795
+ ...(worktreeState || {}),
671
796
  // Present = interactive attach can start follow-up turns; absent = the
672
797
  // transport rejects prompts (log-only session).
673
798
  ...(Array.isArray(followUpArgv) ? { followUpArgv } : {}),
@@ -691,6 +816,7 @@ export function launchBackgroundAgent({
691
816
  endedAt: null,
692
817
  exitCode: null,
693
818
  logFile: job.logFile,
819
+ ...(worktreeState || {}),
694
820
  };
695
821
  writeBackgroundAgentState(state);
696
822
  let child;
@@ -763,6 +889,21 @@ export function stopBackgroundAgent(id) {
763
889
  }
764
890
  return { ...state, stopped: false };
765
891
  }
892
+ // Self-pid guard: a worker record whose pid is the CURRENT process is
893
+ // impossible in legitimate operation (the stopper is never the worker) —
894
+ // it means a corrupt/hand-edited state file. Signalling it would SIGTERM /
895
+ // taskkill-tree the very process (and shell tree) running `cc daemon stop`.
896
+ // Treat as lost, kill nothing.
897
+ if (Number(state.pid) === process.pid) {
898
+ const lost = {
899
+ ...state,
900
+ status: "lost",
901
+ endedAt: Date.now(),
902
+ lostReason: "self-pid-corrupt-record",
903
+ };
904
+ writeBackgroundAgentState(lost);
905
+ return { ...lost, stopped: false };
906
+ }
766
907
  // Gap 1: last-instant identity re-check before the kill — never
767
908
  // taskkill/SIGTERM a pid the OS has re-assigned to an unrelated process.
768
909
  if (!isSameProcess(state.pid, state.startedAt)) {
@@ -796,9 +937,9 @@ export function stopBackgroundAgent(id) {
796
937
  }
797
938
  } else {
798
939
  try {
799
- process.kill(-Number(state.pid), "SIGTERM");
940
+ _deps.kill(-Number(state.pid), "SIGTERM");
800
941
  } catch {
801
- process.kill(Number(state.pid), "SIGTERM");
942
+ _deps.kill(Number(state.pid), "SIGTERM");
802
943
  }
803
944
  // The worker detaches the agent child into its OWN process group (so a
804
945
  // crashed worker's child can be group-killed) — which also means the
@@ -810,15 +951,16 @@ export function stopBackgroundAgent(id) {
810
951
  Number.isInteger(agentPid) &&
811
952
  agentPid > 0 &&
812
953
  agentPid !== Number(state.pid) &&
954
+ agentPid !== process.pid &&
813
955
  Number.isFinite(anchor) &&
814
956
  anchor > 0 &&
815
957
  isSameProcess(agentPid, anchor)
816
958
  ) {
817
959
  try {
818
- process.kill(-agentPid, "SIGTERM");
960
+ _deps.kill(-agentPid, "SIGTERM");
819
961
  } catch {
820
962
  try {
821
- process.kill(agentPid, "SIGTERM");
963
+ _deps.kill(agentPid, "SIGTERM");
822
964
  } catch {
823
965
  /* already gone */
824
966
  }
@@ -835,6 +977,84 @@ export function stopBackgroundAgent(id) {
835
977
  return { ...next, stopped: true };
836
978
  }
837
979
 
980
+ function activeWorktreeProcesses(state) {
981
+ const active = [];
982
+ if (
983
+ Number.isInteger(Number(state?.pid)) &&
984
+ Number(state.pid) > 0 &&
985
+ isSameProcess(state.pid, state.startedAt)
986
+ ) {
987
+ active.push("worker");
988
+ }
989
+ if (
990
+ Number.isInteger(Number(state?.agentPid)) &&
991
+ Number(state.agentPid) > 0 &&
992
+ isSameProcess(state.agentPid, state.agentStartedAt)
993
+ ) {
994
+ active.push("agent");
995
+ }
996
+ return active;
997
+ }
998
+
999
+ /**
1000
+ * Explicit background-record cleanup owns the worktree teardown. Normal
1001
+ * completion, kill, lost-worker reconciliation, attach and resume all retain
1002
+ * it so a later resume continues in the exact same isolated checkout.
1003
+ */
1004
+ export function cleanupBackgroundAgentWorktree(state, options = {}) {
1005
+ const worktree = persistedWorktree(state);
1006
+ if (!worktree) return null;
1007
+ if (options.keepWorktree === true) {
1008
+ return {
1009
+ removed: false,
1010
+ kept: true,
1011
+ reason: "kept by explicit request",
1012
+ path: worktree.worktreePath,
1013
+ };
1014
+ }
1015
+
1016
+ const blockers = [];
1017
+ const active = activeWorktreeProcesses(state);
1018
+ if (active.length) blockers.push(`active ${active.join("+")} process`);
1019
+ if (Number(state?.pendingApprovals || 0) > 0) {
1020
+ blockers.push("pending approval");
1021
+ }
1022
+ if (state?.pendingQuestion) blockers.push("pending user input");
1023
+ if (
1024
+ Number(state?.uncertainSideEffects || 0) > 0 ||
1025
+ state?.phase === "uncertain_side_effect" ||
1026
+ state?.phase === "uncertain-side-effect"
1027
+ ) {
1028
+ blockers.push("unfinished or uncertain side effect");
1029
+ }
1030
+ if (blockers.length) {
1031
+ throw new Error(
1032
+ `Refusing to clean background worktree "${worktree.worktreePath}": ${blockers.join(", ")}`,
1033
+ );
1034
+ }
1035
+
1036
+ if (!existsSync(worktree.worktreePath)) {
1037
+ return {
1038
+ removed: false,
1039
+ kept: false,
1040
+ reason: "worktree path already missing",
1041
+ path: worktree.worktreePath,
1042
+ };
1043
+ }
1044
+
1045
+ const result = finishAgentWorktree({
1046
+ ...worktree,
1047
+ path: worktree.worktreePath,
1048
+ });
1049
+ if (!result.removed) {
1050
+ throw new Error(
1051
+ `Refusing to remove background agent record while its worktree is kept (${result.reason}). ` +
1052
+ "Resolve or preserve the work first, or pass --keep-worktree to remove only the record.",
1053
+ );
1054
+ }
1055
+ return { ...result, path: worktree.worktreePath };
1056
+ }
1057
+
838
1058
  export function openBackgroundLogFile(id) {
839
1059
  const fd = openSync(logPath(id), "a", 0o600);
840
1060
  return { fd, close: () => closeSync(fd) };
@@ -187,7 +187,7 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
187
187
  });
188
188
  }
189
189
 
190
- _ask(questionType, question, extra = {}) {
190
+ _ask(questionType, question, extra = {}, options = {}) {
191
191
  return this._request(
192
192
  {
193
193
  type: "question",
@@ -195,7 +195,12 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
195
195
  question,
196
196
  ...extra,
197
197
  },
198
- { kind: "question" },
198
+ {
199
+ kind: "question",
200
+ // Approval binding pass-through: a permission-gate producer supplies
201
+ // it; plain questions never carry one (message stays byte-identical).
202
+ ...(options.binding ? { binding: options.binding } : {}),
203
+ },
199
204
  );
200
205
  }
201
206
 
@@ -207,10 +212,26 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
207
212
  return this._ask("select", question, { choices });
208
213
  }
209
214
 
215
+ /**
216
+ * @param {string} question
217
+ * @param {boolean|object} [defaultVal] — boolean (legacy, byte-identical) or
218
+ * an options object `{ default, binding, ...extra }`: `binding` rides out
219
+ * on the request so the client must echo it back on approve (verified in
220
+ * _resolvePending, mismatched approve → deny); any other keys merge into
221
+ * the question message (e.g. structured `approval` context for UIs).
222
+ */
210
223
  async askConfirm(question, defaultVal = true) {
211
- const answer = await this._ask("confirm", question, {
212
- default: defaultVal,
213
- });
224
+ const opts =
225
+ defaultVal !== null && typeof defaultVal === "object"
226
+ ? defaultVal
227
+ : { default: defaultVal };
228
+ const { binding = null, default: dflt = true, ...extra } = opts;
229
+ const answer = await this._ask(
230
+ "confirm",
231
+ question,
232
+ { default: dflt, ...extra },
233
+ binding ? { binding } : {},
234
+ );
214
235
  // Normalize to boolean
215
236
  if (typeof answer === "boolean") return answer;
216
237
  return answer === "true" || answer === "yes" || answer === "y";
@@ -111,6 +111,11 @@ export async function smokeTestExe(ctx) {
111
111
  // child.kill on Windows doesn't traverse the process tree; taskkill
112
112
  // does. We spawned a single exe with no subprocesses so plain kill
113
113
  // is enough in practice, but /T protects us if that ever changes.
114
+ // Skip when the child has ALREADY exited: Windows recycles pids
115
+ // within seconds, so taskkilling a dead child's pid can murder an
116
+ // unrelated process tree that inherited the number (on CI: another
117
+ // vitest worker or a test's helper child).
118
+ if (child.exitCode !== null || child.signalCode !== null) return;
114
119
  spawn("taskkill", ["/F", "/T", "/PID", String(child.pid)], {
115
120
  stdio: "ignore",
116
121
  }).unref();
@@ -232,18 +232,48 @@ export function restoreClearedConversation(messages, marks, cleared) {
232
232
  }
233
233
 
234
234
  /**
235
- * Coverage-aware restore plan for a rewound turn, derived from the REPL's
236
- * EXISTING implicit state (`listUserTurns()` output + `{atMessageCount,id,tool}`
237
- * marks) — no new agent-loop events required. Bridges those into the explicit
238
- * turn-binding table and asks `resolveRestorePlan` for the honest plan, so
239
- * `/rewind` can warn when a restore can't be fully promised (side-effects, a
240
- * missing checkpoint, or a conversation/files drift) instead of silently doing a
241
- * lossy rewind.
235
+ * Match a live-conversation turn to its record in a PERSISTED explicit
236
+ * turn-binding table (`loadTurnBindingLog(sessionId)` — produced by headless
237
+ * runs of the same session, which anchor each turn at
238
+ * `conversationOffset = messages.length` measured just AFTER that turn's user
239
+ * message was appended). So the record for the user message at live index
240
+ * `turnIndex` is the one with `conversationOffset === turnIndex + 1`. Exact
241
+ * match only — anything else falls back to the marks-derived table rather than
242
+ * guessing (the offsets align because both runs rebuild the same persisted
243
+ * message sequence under a single leading system prompt).
244
+ *
245
+ * @param {import("./turn-binding.js").TurnBindingLog|null} persistedLog
246
+ * @param {number} turnIndex message index of the target user turn
247
+ * @returns {object|null} a turn view (with computed coverage), or null
248
+ */
249
+ export function pickPersistedTurn(persistedLog, turnIndex) {
250
+ if (!persistedLog || typeof persistedLog.list !== "function") return null;
251
+ const want = Number(turnIndex) + 1;
252
+ if (!Number.isFinite(want)) return null;
253
+ for (const t of persistedLog.list()) {
254
+ if (Number(t.conversationOffset) === want) return t;
255
+ }
256
+ return null;
257
+ }
258
+
259
+ /**
260
+ * Coverage-aware restore plan for a rewound turn. Prefers the PERSISTED
261
+ * explicit turn-binding table when the session has one for the target turn
262
+ * (cross-process honesty: a `--resume`d session's pre-resume turns have NO
263
+ * process-local marks, but their headless run persisted checkpoint ids, shell/
264
+ * side-effect flags and child agents — the explicit table answers where marks
265
+ * cannot). Falls back to the REPL's implicit state (`listUserTurns()` output +
266
+ * `{atMessageCount,id,tool}` marks) bridged into the same table shape. Either
267
+ * way `resolveRestorePlan` produces the honest plan, so `/rewind` can warn
268
+ * when a restore can't be fully promised (side-effects, a missing checkpoint,
269
+ * or a conversation/files drift) instead of silently doing a lossy rewind.
242
270
  *
243
271
  * @param {Array} messages live conversation (for listUserTurns)
244
272
  * @param {Array<{atMessageCount:number,id:string,tool?:string}>} marks
245
273
  * @param {number} turnIndex message index returned by rewindToTurn()
246
274
  * @param {string} [scope] one of RESTORE_SCOPE (default BOTH)
275
+ * @param {{persistedLog?:object|null}} [opts] explicit table from
276
+ * `loadTurnBindingLog(sessionId)` (advisory; null → marks only)
247
277
  * @returns {{scope, coverage, conversation, files, warnings:string[]}}
248
278
  */
249
279
  export function buildRewindPlan(
@@ -251,7 +281,10 @@ export function buildRewindPlan(
251
281
  marks,
252
282
  turnIndex,
253
283
  scope = RESTORE_SCOPE.BOTH,
284
+ { persistedLog = null } = {},
254
285
  ) {
286
+ const persisted = pickPersistedTurn(persistedLog, turnIndex);
287
+ if (persisted) return resolveRestorePlan(persisted, scope);
255
288
  const turns = listUserTurns(messages, { limit: 1000 });
256
289
  const log = buildTurnBindingFromMarks(turns, marks);
257
290
  const turn = log.get(`turn-${Number(turnIndex)}`);
@@ -288,18 +321,29 @@ export function renderRewindWarnings(plan) {
288
321
  * @param {Array} messages live conversation (for listUserTurns)
289
322
  * @param {Array<{atMessageCount:number,id:string,tool?:string}>} marks
290
323
  * @param {number} turnIndex message index of the branch-point user turn
291
- * @param {{parentSessionId?:string, seq?:number, writeIntent?:boolean}} [opts]
324
+ * @param {{parentSessionId?:string, seq?:number, writeIntent?:boolean,
325
+ * persistedLog?:object|null}} [opts] persistedLog: explicit table
326
+ * from `loadTurnBindingLog(sessionId)`, preferred over marks (same
327
+ * cross-process rationale as buildRewindPlan)
292
328
  * @returns {object} the plan from planSessionBranch()
293
329
  */
294
330
  export function buildBranchPlan(
295
331
  messages,
296
332
  marks,
297
333
  turnIndex,
298
- { parentSessionId = null, seq = 0, writeIntent = false } = {},
334
+ {
335
+ parentSessionId = null,
336
+ seq = 0,
337
+ writeIntent = false,
338
+ persistedLog = null,
339
+ } = {},
299
340
  ) {
300
- const turns = listUserTurns(messages, { limit: 1000 });
301
- const log = buildTurnBindingFromMarks(turns, marks);
302
- const turn = log.get(`turn-${Number(turnIndex)}`);
341
+ let turn = pickPersistedTurn(persistedLog, turnIndex);
342
+ if (!turn) {
343
+ const turns = listUserTurns(messages, { limit: 1000 });
344
+ const log = buildTurnBindingFromMarks(turns, marks);
345
+ turn = log.get(`turn-${Number(turnIndex)}`);
346
+ }
303
347
  return planSessionBranch({ parentSessionId, turn, seq, writeIntent });
304
348
  }
305
349
 
@@ -336,6 +380,7 @@ export function renderBranchPlan(plan) {
336
380
  * /rewind --files 3 → { command: "turn", n: 3, scope: FILES }
337
381
  * /rewind 3 --branch → { command: "branch", n: 3, writeIntent: false }
338
382
  * /rewind 3 --branch --write → { command: "branch", n: 3, writeIntent: true }
383
+ * /rewind 3 --both --yes → non-interactive file confirmation (IDE)
339
384
  *
340
385
  * An unknown flag is ignored (kept forgiving); a non-numeric, flag-less arg with
341
386
  * no turn number falls back to `list` so a typo can't silently rewind — and a
@@ -354,6 +399,7 @@ export function parseRewindArg(arg) {
354
399
  let scope = RESTORE_SCOPE.BOTH;
355
400
  let branch = false;
356
401
  let writeIntent = false;
402
+ let confirmed = false;
357
403
  for (const t of tokens) {
358
404
  if (t === "--files" || t === "--files-only") scope = RESTORE_SCOPE.FILES;
359
405
  else if (
@@ -365,11 +411,13 @@ export function parseRewindArg(arg) {
365
411
  else if (t === "--both") scope = RESTORE_SCOPE.BOTH;
366
412
  else if (t === "--branch" || t === "--fork") branch = true;
367
413
  else if (t === "--write" || t === "--write-intent") writeIntent = true;
414
+ else if (t === "--yes" || t === "-y") confirmed = true;
368
415
  else if (n === null && /^\d+$/.test(t)) n = Number(t);
369
416
  }
370
- if (branch && n !== null) return { command: "branch", n, scope, writeIntent };
417
+ if (branch && n !== null)
418
+ return { command: "branch", n, scope, writeIntent, confirmed };
371
419
  if (n === null) return { command: "list", scope };
372
- return { command: "turn", n, scope };
420
+ return { command: "turn", n, scope, confirmed };
373
421
  }
374
422
 
375
423
  /** Render the picker list (shared by /rewind and double-Esc). */