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.
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-DCeNRK8z.js → AIOps-Cf8yb18C.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-ZfD0HgVe.js → ActionButton-CsFT6FMt.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Ccn40f9E.js → Analytics-DQe_hXMA.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DyXs4nSt.js → AppLayout-Cl9Pp7jk.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CYjMh5KR.js → Artifacts-CBEzLVjF.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DZIhvx88.js → Audit-BS2Un14C.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C1XJsa2d.js → BackgroundAgents-DmcDH8OK.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CMXC0oUN.js → Backup-DvzjTkkn.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CRVmIH1t.js → BaseInput-DnbpuyME.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-acBud5Yj.js → Chat-BJRRCm2P.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CTjqFhdD.js → ChatBubbleRenderer-Bgpxyqd_.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CZem825o.js → Checkbox-DFOrWmvm.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B9UjZsww.js → Codegen-BIQ2kU6Q.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Dtu6Frtj.js → Col-BI3R-N4I.js} +1 -1
- package/src/assets/web-panel/assets/{Community-9bV55Huv.js → Community-CPiAwsHT.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-g-kQ4c84.js → Compact-Bv4cslSK.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CQjHzBO1.js → Compliance-CSlDG1NZ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-D1Qm_Xhi.js → Cowork-CFcRumXf.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-992qo8zX.js → Cron-DKYKMmOn.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-4TiqEwuc.js → Crosschain-CxPMByp2.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BbsXzXCb.js → DID-DJqiYw1C.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Dx5aO9d3.js → Dashboard-D2dLMxqy.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BhzW6i6i.js → Dropdown-BnJc384A.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-qEib4yiq.js → EmailListRenderer-DctFvm-J.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BZ3N-u3h.js → FamilyGuardDashboard-J_xg-Qop.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CcOqwzHY.js → Federation-BckL07Uw.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-dbi8mohd.js → FormItemContext-B8s5ccSO.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-57dqulUe.js → GenericCardRenderer-hPMBK2No.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DtSV7C5g.js → Git-CfMpYLo7.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CLnmH-RD.js → Governance-Bt3OcrC7.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-wXiLeDnk.js → Inference-BlKc336N.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-NGdIX2Gx.js → KnowledgeGraph-B6p7og2c.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Dz0HQn69.js → Logs-uv5Re6YR.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-CId5JWjX.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-DpPDmY3W.js → Marketplace-CfNbntHt.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-UMysoRQm.js → McpTools-YRXt74Yk.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-DsnBpQnY.js → Memory-BfQQs-Jd.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DweooAfG.js → MobileBridge-BuGC-4S2.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-UdLfZOVA.js → MobileProjects-Czieqw1m.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-oXWhJNgC.js → Mtc-DHWHOVCO.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CXTD7qrm.js → MtcAudit-DWSEbMW-.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-BKt5mJ9O.js → Multisig-DrhEv1vq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-C-zcYzje.js → NLProgramming-D8wKCu7E.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CGaZ_jep.js → Notes-AFeqq0I_.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-BwX-tDbq.js → NotificationSettings-D6qenYUo.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Cc9Se8H-.js → OrderTableRenderer-CF0X4fyB.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-8BXdC7Im.js → Organization-DdfbNqf6.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-zCisFuGU.js → Overflow-BFEvnLfd.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BehHH9Ro.js → P2P-DtL6wbkz.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-CQPriTud.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-BVDATm3p.js → Permissions-Bj25ANmR.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-C43nnYoi.js → PersonalDataHub-BNj2Oux0.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-D9E7aSVD.js → Pipeline-Dyj8UEaG.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CRbIchdL.js → Privacy-BBUrWRFz.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CTsy8lhL.js → ProjectInit-BdX7uQn6.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CPEKWWJ5.js → ProjectSettings-BghtW1zr.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CNnW0v5N.js → Projects-9VTquZCz.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BeZnDL9Y.js → Providers-D-0K_Eza.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CaydwuIU.js → QrScannerModal-DDaN30_D.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-qYWbu7kw.js → QuickAsk-DfHlifOj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-RV_ogHrM.js → Recommend-CgLIvYE2.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Mah1i41_.js → RemoteSession-DpOPrwE4.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-xZQhGktc.js → Reputation-wMexlbCE.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B6cVbPbJ.js → Row-D9aD_nrc.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C6hKjDPz.js → RssFeed-Drg7l6zu.js} +3 -3
- package/src/assets/web-panel/assets/{Search-0jti_-Pe.js → Search-BKO1nrwN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Cy48feGz.js → Security-G3Kyk-TG.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BW3ObFxH.js → Services-xJReTOna.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CfrZRvAM.js → Skeleton-ldVDVTPG.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-yoccdk3t.js → Skills-emwNYV0j.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-AKBBdri6.js → Sla-CZYwGp1K.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-CyaPrzOF.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-DUuO-ZSw.js → SyncSettings-Dcs5Hhj6.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-ZTUfOzMZ.js → Tasks-CerFAo26.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-shPrQ4NE.js → Templates-BdZUbOg3.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-C_inPunR.js → Tenant-B4c_7Bsw.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-VVFLS3Ol.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-Bzp_T3Xc.js → TimelineRenderer-tc6sp0TV.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-kUFRLdtp.js → Tokens-Bw_vkMJq.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BsTU2blQ.js → Trigger-DkbJnpvz.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DEKkuAsi.js → Trust-CIf9kqN1.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-D1KEEgbK.js → UkeySign-Cjwllnea.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C2yYkm9W.js → VideoEditing-kgfkY7AN.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BKKod4qb.js → Wallet-CYWXt4u9.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-XXUgED6d.js → WebAuthn-QgPwyWCO.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-DnlEyYTi.js → WorkflowEditor-Co0lha18.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BaVjI6nu.js → chat-CI__Ox-r.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DQ0yx43H.js → colors-Csv0eYYd.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DEyWDp0v.js → compact-item-e53UCdHy.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-SuPOfylZ.js → createContext-BMPGRDhV.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BIulobq7.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CQz1JNtC.js → hasIn-Dz68qIAq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJEIjYhV.js → index--7tHEadk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIgWgye3.js → index-53ljQo_x.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_fcNCoV.js → index-5iicv5af.js} +1 -1
- package/src/assets/web-panel/assets/{index-BffmUosH.js → index-B1s1B8Iy.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgvnudM_.js → index-B4lKPJy1.js} +1 -1
- package/src/assets/web-panel/assets/index-BGkiYnET.js +1 -0
- package/src/assets/web-panel/assets/{index-COsmMpAO.js → index-BHkDRNA5.js} +1 -1
- package/src/assets/web-panel/assets/{index-57GdL9ro.js → index-BOpz2qS1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjZ6szGM.js → index-BT3CV4Nh.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpPv8zeU.js → index-BW2dImbg.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpR6M7eQ.js → index-C-NbcAbP.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dfh6OJKz.js → index-C53o-MdJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfVW-cd_.js → index-C5hLtxVz.js} +1 -1
- package/src/assets/web-panel/assets/{index-ByGF9ev7.js → index-CNtZMPKV.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZKOZ-YFJ.js → index-CXuON9OG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGu-b0RE.js → index-CYUxs6J1.js} +1 -1
- package/src/assets/web-panel/assets/{index-3xcB2i6I.js → index-CoGSbW5J.js} +1 -1
- package/src/assets/web-panel/assets/{index-DB8AJ6T-.js → index-CpTzqJ0Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-XjQfQBXH.js → index-CrsoSnQF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZrARoXd.js → index-CvLBQk_M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWIuZ9bI.js → index-CyLUaP2i.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEfLMvKf.js → index-D0bg74e1.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZMgCYtI7.js → index-DCl4Xb4N.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXczrVij.js → index-DJqQL6so.js} +1 -1
- package/src/assets/web-panel/assets/{index-fGgx2f1y.js → index-DVCd2aMX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHJhyrFr.js → index-DctYrVMq.js} +1 -1
- package/src/assets/web-panel/assets/{index-v8PYYw05.js → index-Dk9Gi8xd.js} +1 -1
- package/src/assets/web-panel/assets/{index-fcA6tfBA.js → index-DnXm3Qsh.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3Vlo45C.js → index-Dtjm1K1a.js} +1 -1
- package/src/assets/web-panel/assets/{index-wBsuJZcz.js → index-DyqcHiL1.js} +1 -1
- package/src/assets/web-panel/assets/index-Eyhrr_3u.js +1 -0
- package/src/assets/web-panel/assets/{index-CnhJU5To.js → index-HbhQRrM2.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5SY6Lwu.js → index-K5E_wjut.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhBeUGCp.js → index-N9DkR1i7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CI5jlfyn.js → index-_LmPlyeV.js} +1 -1
- package/src/assets/web-panel/assets/{index-DNYVGZzG.js → index-ajz0E0pK.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2k8eHyq.js → index-t6f80uqL.js} +17 -17
- package/src/assets/web-panel/assets/{index-C-IKjQD3.js → index-wcCouSNq.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-gXgEsU.js → index-ygfUVVFv.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-KetDXPXx.js → initDefaultProps-DVCeQJrT.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BfAtwo2m.js → motion-chBCR_8v.js} +1 -1
- package/src/assets/web-panel/assets/{move-BnJvzHgx.js → move-BziPlrh6.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DZxDclhM.js → mtc-parser-Cenk0OD7.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CmqfX2P_.js → omit-D-AiRsei.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-B2B2ahzC.js → pickAttrs-CHw3Fvqt.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-C1SIgWRj.js → placementArrow-g8I-rCad.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Dy_adDTl.js → responsiveObserve-BWffAr9k.js} +1 -1
- package/src/assets/web-panel/assets/{slide-hDO9sBPk.js → slide-BmUQqhkb.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DlpTjRpg.js → statusUtils-B89gCO-y.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DAv9eo6h.js → styleChecker-x1bnCF73.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-vfoXaeH8.js → useFlexGapSupport-B-k1_Ggw.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Dzji-S7v.js → useFs-Bc3PUqQk.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-jKcLN_Gj.js → usePersonalDataHub-D8qFDX1I.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DAWM5GgY.js → vnode-UaL5-WGg.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-q3XgSOUd.js → zoom-Dv7bsbLs.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +39 -14
- package/src/commands/background-session.js +12 -1
- package/src/data/changelog.json +7 -2
- package/src/gateways/ws/ws-agent-handler.js +26 -1
- package/src/gateways/ws/ws-approval-gate.js +131 -0
- package/src/lib/agent-worktree.js +173 -5
- package/src/lib/background-agent-supervisor.js +234 -14
- package/src/lib/interaction-adapter.js +26 -5
- package/src/lib/packer/smoke-runner.js +5 -0
- package/src/lib/repl-rewind.js +62 -14
- package/src/lib/turn-binding.js +134 -0
- package/src/repl/agent-repl.js +75 -3
- package/src/repl/repl-turn-binding.js +91 -0
- package/src/runtime/headless-runner.js +59 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-CSRZ9da3.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-DVuXYKAR.js +0 -7
- package/src/assets/web-panel/assets/SpeechSettings-DH87GbkH.js +0 -1
- package/src/assets/web-panel/assets/Terminal-CvVtx6ie.js +0 -3
- package/src/assets/web-panel/assets/devWarning-C_RK48j-.js +0 -1
- package/src/assets/web-panel/assets/index-CDqhisfz.js +0 -1
- 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(
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
161
|
+
_deps.kill(-target, signal);
|
|
148
162
|
return true;
|
|
149
163
|
} catch {
|
|
150
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
940
|
+
_deps.kill(-Number(state.pid), "SIGTERM");
|
|
800
941
|
} catch {
|
|
801
|
-
|
|
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
|
-
|
|
960
|
+
_deps.kill(-agentPid, "SIGTERM");
|
|
819
961
|
} catch {
|
|
820
962
|
try {
|
|
821
|
-
|
|
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
|
-
{
|
|
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
|
|
212
|
-
|
|
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();
|
package/src/lib/repl-rewind.js
CHANGED
|
@@ -232,18 +232,48 @@ export function restoreClearedConversation(messages, marks, cleared) {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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
|
|
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
|
-
{
|
|
334
|
+
{
|
|
335
|
+
parentSessionId = null,
|
|
336
|
+
seq = 0,
|
|
337
|
+
writeIntent = false,
|
|
338
|
+
persistedLog = null,
|
|
339
|
+
} = {},
|
|
299
340
|
) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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)
|
|
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). */
|