chainlesschain 0.162.93 → 0.162.95
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/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-BB5mADdi.js → AIOps-DLvvkb9x.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BcIs1DhT.js → ActionButton-BxhQTwqd.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CNhfaUbF.js → Analytics-C4oUXzhG.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-D1T07CY4.js → AppLayout-CjzKJdv3.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-Et29_1F4.js → Audit-B-MrugUs.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-B0ggFwOy.js → Backup-ecIfEpIZ.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-ChTsFhxU.js → BaseInput-BUcSfWnx.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-H5DMwE1E.js → Chat-DIc1n_Lk.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CBQYVCUs.js → ChatBubbleRenderer-BGzg7oVA.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DBsH3Plc.js → Checkbox-BQ3Ueff-.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-gGtw6sXf.js → Codegen-CvSGLnqE.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Df317HUo.js → Col-Br9IXZcE.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CSERdOno.js → Community-tPuLOf0r.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-D31wUdGx.js → Compact-vMWH73BE.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-92p7-fAe.js → Compliance-Ct9z699x.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B_HJ_3fc.js → Cowork-Bxs1kL1y.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DIAgns-d.js → Cron-QEpr2k4U.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain--pT2fZrx.js → Crosschain-u4xbUw6O.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BR_apx67.js → DID-DXTEvyrW.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CIv6YbX3.js → Dashboard-C4pZgzkg.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BByKNFqx.js → Dropdown-LRuqREGe.js} +1 -1
- package/src/assets/web-panel/assets/EmailListRenderer-D7XI1yBM.js +1 -0
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CA9WH0R8.js → FamilyGuardDashboard-3y_SuAMQ.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CnOtOrN0.js → Federation-DbjbJJCW.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CW4kzO3J.js → FormItemContext-WPpMZcUw.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CUVA-qrZ.js → GenericCardRenderer-nNT9Tn4E.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Dl9HG9Yo.js → Git-DqDaVzwV.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BgieRKC3.js → Governance-BquBZpcg.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-55hVpHm_.js → Inference-RcPPYijs.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BCSm8nT7.js → KnowledgeGraph-ByXfZV13.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DPR2qyoe.js → Logs-CHT3DUcq.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-DSfYHaLe.js → Marketplace-Bi0HJ9pN.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DvP_EW02.js → McpTools-QCBxPRRc.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-vpNLjkQh.js → Memory-D4RlxGXn.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DXT2hsiu.js → MobileBridge-CU_PV7e-.js} +2 -2
- package/src/assets/web-panel/assets/{MobileProjects-wh6wAgGH.js → MobileProjects-c1CMFD8z.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-hLUm4hEi.js → Mtc-BezDB1-s.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-DapBAJjs.js → MtcAudit-CgLpiHpE.js} +6 -6
- package/src/assets/web-panel/assets/{Multisig-hQs0FE-E.js → Multisig-vx6bz-bA.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-JHOgd9cA.js → NLProgramming-CGOagg3-.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CRF8S0xK.js → Notes-BcuDGYRS.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-VYaLm62h.js → NotificationSettings-ZAGCDUpc.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-PNYP-hFh.js → OrderTableRenderer-B5onE0eX.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DnrXiOjr.js → Organization-y_y20-5Y.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-y0VkBEmD.js → Overflow-BEICS31D.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-jK93QK2m.js → P2P-D_WRMzh4.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-6u2dj6z7.js → Permissions-3aqCc_4f.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-E0j19ibV.js → PersonalDataHub-DT3R9g9Y.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-2YSzaqHA.js → Pipeline-MiMWIhw0.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-jhYDq4eK.js → Privacy-C3JsLuDa.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C4UBpmgK.js → ProjectInit-EhHVi1ff.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-B-BYxXeT.js → ProjectSettings-D_dNEA37.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Dn-wcIfk.js → Projects-CXydUXp8.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-B9oU5w2Y.js → Providers-CPEhi2iZ.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BSEAjKKg.js → QuickAsk-BFQpMleX.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-B2G2qN0o.js → Recommend-DzbovRC8.js} +1 -1
- package/src/assets/web-panel/assets/Reputation-DoBeV3TD.js +1 -0
- package/src/assets/web-panel/assets/{Row-CfFRxFir.js → Row-DthhwX_B.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Cr7P2tEV.js → RssFeed-Dsf5U0lP.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DU23Ncv0.js → Search-CJPB8kKl.js} +1 -1
- package/src/assets/web-panel/assets/{Security-XlP_XKyJ.js → Security-DI9MfpdD.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CvmT7pHE.js → Services-BdG0TFGf.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-r38WJidJ.js → Skeleton-DSYazY_B.js} +1 -1
- package/src/assets/web-panel/assets/Skills-BD309VTT.js +1 -0
- package/src/assets/web-panel/assets/{Sla-D4EOWYSw.js → Sla-CaUlw4_6.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-COVO-nuO.js → SpeechSettings-CZAPkR7H.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CFworQPJ.js → SyncSettings-CFRSowN3.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-Di8EOluc.js +1 -0
- package/src/assets/web-panel/assets/{Templates-CXcol1Sm.js → Templates-B5FTziwi.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DthNkjQO.js → Tenant-Dj3XYfBg.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-CznH_rJT.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-Dd9CmQ-l.js → TimelineRenderer-DTHzaXPt.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DoV9Eaxw.js → Tokens-Bo56nuML.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-t0b7L2h7.js → Trigger-pyVUVBwb.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-fXDFZevl.js → Trust-Cp7jMkKN.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-c7zmwDuv.js → UkeySign-aPcZCdpz.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-AxUsMrpY.js → VideoEditing-DAv5j--w.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BB6Hkf9D.js → Wallet-DRhK7IDR.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-JBl4_qBv.js → WebAuthn-B2yQJuq8.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-BMOyglO4.js → WorkflowEditor-D98DhR54.js} +1 -1
- package/src/assets/web-panel/assets/{chat-ClEx1qbW.js → chat-BXrRNi8C.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DBawfCLx.js → colors-jQ94T_qn.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BZff7uOC.js → compact-item-KqUJLlF2.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-pWYC3gCE.js → createContext-DxM3zuQW.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-lX6sY5PY.js → hasIn-BGm946KV.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dm_BVdZG.js → index-1P0sXNhG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdXLG_Lh.js → index-B-2HpfWo.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRhurdyB.js → index-B4b7KjRq.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2m7AQfe.js → index-BAPk6ntc.js} +1 -1
- package/src/assets/web-panel/assets/index-BCU6dVQ-.js +1 -0
- package/src/assets/web-panel/assets/{index-OYqduj3d.js → index-BGjCjsf1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdKd25BC.js → index-BMp41Q1Y.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dt3yAuCK.js → index-BXMZLfd8.js} +1 -1
- package/src/assets/web-panel/assets/{index-5ER-NP_g.js → index-BaU_fSK7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYgqlm3l.js → index-BbS0cBU9.js} +1 -1
- package/src/assets/web-panel/assets/{index-oETPgXwU.js → index-BnEZfi5D.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6QoSIPt.js → index-C5NQai7O.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkUBHiXd.js → index-C7Z2m28C.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0xNvq3Z.js → index-C7sBLech.js} +1 -1
- package/src/assets/web-panel/assets/{index-DH58L6nn.js → index-CbQvgx2e.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd3KxxFO.js → index-Ce-YoL4x.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dj-c7GTQ.js → index-CfE3jxrT.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE0RpNJf.js → index-CghRL1dh.js} +1 -1
- package/src/assets/web-panel/assets/{index-Oirvb-h0.js → index-CreXAr5Y.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0cwx_ly.js → index-Crw76vIF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXBf3t5D.js → index-Cz_SFSix.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7xkvbtG.js → index-D1VWL6Lc.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bxy05j6q.js → index-DOYcemym.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bvcasgi2.js → index-DRzsaWQy.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bp4RaoTi.js → index-DSzb9VOG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQTfwMek.js → index-DTUs9t5F.js} +1 -1
- package/src/assets/web-panel/assets/{index-oK90a36q.js → index-DWcd5zDG.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cewjdty4.js → index-DZMzj4ay.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4tHr9Y9.js → index-DZpVYD4j.js} +1 -1
- package/src/assets/web-panel/assets/{index-E7dpMseg.js → index-DboUBf6R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEc3MwP8.js → index-Dgzw6eKr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXBvWyps.js → index-Dib0FsTT.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPXnMie3.js → index-FSYz5aww.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdXWNqWZ.js → index-LWTZAud0.js} +1 -1
- package/src/assets/web-panel/assets/index-N_oOFphx.js +1 -0
- package/src/assets/web-panel/assets/{index-mBBrUDzF.js → index-T4UJiTi3.js} +1 -1
- package/src/assets/web-panel/assets/{index-F4__gs9X.js → index-aOLXuHpG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CI6hB8lg.js → index-mZNLT2SP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLqR3qaE.js → index-yavsyHif.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BTUpF0QI.js → initDefaultProps-BDoZJI7g.js} +1 -1
- package/src/assets/web-panel/assets/{motion-2WeVnnEL.js → motion-sceLjgp-.js} +1 -1
- package/src/assets/web-panel/assets/{move-WjKqLduO.js → move-CTWQpFa7.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DnFKewM5.js → omit-dr-NCrHZ.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-C6Y6tV6C.js → pickAttrs-DK-GX_Z2.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-5GZUx8Lr.js → placementArrow-BIPf--18.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DGmwHltc.js → responsiveObserve-BzrWT5tV.js} +1 -1
- package/src/assets/web-panel/assets/{slide-RbVe2IlW.js → slide-D5XhxlET.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CqyCtZXh.js → statusUtils-6DI-k-9E.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-D4lnP6aI.js → styleChecker-DMw-eyn9.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DiMKO4Xv.js → useFlexGapSupport-D4oVdliM.js} +1 -1
- package/src/assets/web-panel/assets/{useFs--HqUmdpU.js → useFs-D3PPZnvy.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-4h3Apiwr.js → usePersonalDataHub-Gh5ba4KP.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BKpoqKhe.js → vnode-CVEM1u4x.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D1RKjeeJ.js → zoom-ClT_-9RZ.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/pdh.js +162 -0
- package/src/harness/mcp-client.js +19 -2
- package/src/harness/mock-llm-provider.js +14 -0
- package/src/index.js +2 -0
- package/src/lib/claude-code-bridge.js +14 -2
- package/src/lib/file-checkpoint.js +30 -4
- package/src/lib/pdh-bridge.js +266 -0
- package/src/lib/repl-completer.js +5 -1
- package/src/lib/session-tail.js +6 -1
- package/src/lib/skill-mcp.js +10 -4
- package/src/repl/agent-repl.js +13 -6
- package/src/runtime/agent-core.js +25 -0
- package/src/runtime/headless-runner.js +39 -5
- package/src/runtime/headless-stream.js +92 -1
- package/src/runtime/mcp-config.js +104 -3
- package/src/assets/web-panel/assets/EmailListRenderer-DKCmhw1W.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-BITZbGtU.js +0 -7
- package/src/assets/web-panel/assets/Reputation-cdXZrYdH.js +0 -1
- package/src/assets/web-panel/assets/Skills-Bd49FHct.js +0 -1
- package/src/assets/web-panel/assets/Tasks-mWQgh-Xq.js +0 -1
- package/src/assets/web-panel/assets/Terminal-BuX3TG_s.js +0 -3
- package/src/assets/web-panel/assets/devWarning-B1b-Rehg.js +0 -1
- package/src/assets/web-panel/assets/index-Bv1uJXTd.js +0 -1
- package/src/assets/web-panel/assets/index-DJYUttUZ.js +0 -1
|
@@ -43,6 +43,33 @@ function ensureDir(dir) {
|
|
|
43
43
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Write a file atomically: write to a unique temp sibling, then rename over the
|
|
48
|
+
* target. rename() is atomic within a filesystem, so a crash mid-write leaves
|
|
49
|
+
* the previous file (or nothing) intact — never a half-written manifest/blob.
|
|
50
|
+
* Without this, a checkpoint manifest truncated by a crash makes the whole
|
|
51
|
+
* checkpoint unrecoverable (getCheckpoint's JSON.parse fails → null) while its
|
|
52
|
+
* blobs are orphaned on disk. The temp lives in the SAME dir as the target so
|
|
53
|
+
* the rename stays on one filesystem; on failure the temp is best-effort
|
|
54
|
+
* removed. Temp names end in `.tmp` (never `.json`), so a leftover from a hard
|
|
55
|
+
* crash is ignored by listCheckpoints.
|
|
56
|
+
*/
|
|
57
|
+
function atomicWriteFileSync(filePath, data) {
|
|
58
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
59
|
+
const tmp = `${filePath}.${process.pid}.${rand}.tmp`;
|
|
60
|
+
try {
|
|
61
|
+
fs.writeFileSync(tmp, data);
|
|
62
|
+
fs.renameSync(tmp, filePath);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
try {
|
|
65
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
|
66
|
+
} catch {
|
|
67
|
+
/* best-effort temp cleanup */
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
function sha256(buf) {
|
|
47
74
|
return createHash("sha256").update(buf).digest("hex");
|
|
48
75
|
}
|
|
@@ -127,7 +154,7 @@ export function createCheckpoint(paths, opts = {}) {
|
|
|
127
154
|
const buf = fs.readFileSync(abs);
|
|
128
155
|
const hash = sha256(buf);
|
|
129
156
|
const blobPath = path.join(blobDir, hash);
|
|
130
|
-
if (!fs.existsSync(blobPath))
|
|
157
|
+
if (!fs.existsSync(blobPath)) atomicWriteFileSync(blobPath, buf);
|
|
131
158
|
files.push({
|
|
132
159
|
rel: path.relative(cwd, abs) || path.basename(abs),
|
|
133
160
|
abs,
|
|
@@ -145,10 +172,9 @@ export function createCheckpoint(paths, opts = {}) {
|
|
|
145
172
|
files,
|
|
146
173
|
};
|
|
147
174
|
ensureDir(root);
|
|
148
|
-
|
|
175
|
+
atomicWriteFileSync(
|
|
149
176
|
path.join(root, `${id}.json`),
|
|
150
177
|
JSON.stringify(manifest, null, 2),
|
|
151
|
-
"utf-8",
|
|
152
178
|
);
|
|
153
179
|
return manifest;
|
|
154
180
|
}
|
|
@@ -275,7 +301,7 @@ export function restoreCheckpoint(id, opts = {}) {
|
|
|
275
301
|
|
|
276
302
|
for (const w of toWrite) {
|
|
277
303
|
ensureDir(path.dirname(w.abs));
|
|
278
|
-
|
|
304
|
+
atomicWriteFileSync(w.abs, w.blob);
|
|
279
305
|
restored.push(w.rel);
|
|
280
306
|
}
|
|
281
307
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDH bridge discovery (Phase 0) — find a first-party "device capability" MCP
|
|
3
|
+
* server that the Android app advertises via a lockfile, and turn it into an
|
|
4
|
+
* MCP server config the agent loop can connect to.
|
|
5
|
+
*
|
|
6
|
+
* This is the cc-side (CLI) half of the Personal-Data IDE / PDH Bridge
|
|
7
|
+
* (design: docs/design/modules/101_个人数据IDE桥接方案.md). It mirrors the IDE
|
|
8
|
+
* bridge (ide-bridge.js, design module 98): the app runs a localhost MCP server
|
|
9
|
+
* exposing collect / query / file / task tools, and writes
|
|
10
|
+
* <appFiles>/.chainlesschain/pdh-bridge/<port>.json (file 0600, dir 0700)
|
|
11
|
+
* The app ALSO injects CHAINLESSCHAIN_PDH_PORT (+ optional _TOKEN) into the env
|
|
12
|
+
* of the in-APK cc it spawns, so cc locks onto the exact instance without
|
|
13
|
+
* scanning ("path A"). External agents (e.g. desktop over a forwarded port)
|
|
14
|
+
* fall back to a lockfile scan ("path B").
|
|
15
|
+
*
|
|
16
|
+
* Unlike the IDE bridge there is no multi-root workspace concept: a device has
|
|
17
|
+
* a single PDH server, so discovery is env-port → else the newest live lock.
|
|
18
|
+
*
|
|
19
|
+
* Pure CLI, no Android dependency, fully unit-testable through `_deps`. The
|
|
20
|
+
* connect path reuses the existing MCP client (HTTP) via mcp-config.js
|
|
21
|
+
* `loadPdhMcp`. Reserved server name `pdh` → tools `mcp__pdh__*`.
|
|
22
|
+
*
|
|
23
|
+
* NOTE: the discovery mechanics (localhost / transport / stale filtering,
|
|
24
|
+
* env fast-path, bearer headers) duplicate ide-bridge.js by design — keeping
|
|
25
|
+
* the two bridges decoupled for Phase 0. A later refactor may extract a shared
|
|
26
|
+
* device-bridge core; until then this file is self-contained on purpose.
|
|
27
|
+
*/
|
|
28
|
+
import fs from "fs";
|
|
29
|
+
import path from "path";
|
|
30
|
+
import os from "os";
|
|
31
|
+
|
|
32
|
+
/** A lock whose pid is dead and whose file is older than this = stale. */
|
|
33
|
+
const STALE_TTL_MS = 30_000;
|
|
34
|
+
|
|
35
|
+
/** Transports the CLI's MCP client can actually talk (see mcp-client.js). */
|
|
36
|
+
const SUPPORTED_TRANSPORTS = new Set(["http", "https", "sse"]);
|
|
37
|
+
|
|
38
|
+
/** Lock `kind` we accept (absent is tolerated; the dir already scopes us). */
|
|
39
|
+
const EXPECTED_KIND = "pdh-bridge";
|
|
40
|
+
|
|
41
|
+
/** Liveness probe via signal 0 — cross-platform (works on Windows too). */
|
|
42
|
+
function defaultProcessAlive(pid) {
|
|
43
|
+
if (!pid || typeof pid !== "number") return false;
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// EPERM: the process exists but we may not signal it → still alive.
|
|
49
|
+
return !!(err && err.code === "EPERM");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const _deps = {
|
|
54
|
+
readDir: (d) => fs.readdirSync(d),
|
|
55
|
+
readFile: (p) => fs.readFileSync(p, "utf-8"),
|
|
56
|
+
statMtimeMs: (p) => fs.statSync(p).mtimeMs,
|
|
57
|
+
homedir: () => os.homedir(),
|
|
58
|
+
now: () => Date.now(),
|
|
59
|
+
processAlive: defaultProcessAlive,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** `~/.chainlesschain/pdh-bridge/`. */
|
|
63
|
+
export function pdhLockDir(deps = _deps) {
|
|
64
|
+
return path.join(deps.homedir(), ".chainlesschain", "pdh-bridge");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isLocalhostUrl(url) {
|
|
68
|
+
try {
|
|
69
|
+
const u = new URL(url);
|
|
70
|
+
return u.hostname === "127.0.0.1" || u.hostname === "::1";
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inferTransportFromUrl(url) {
|
|
77
|
+
if (/\/sse(\b|\/|$)/i.test(url)) return "sse";
|
|
78
|
+
if (/^https:/i.test(url)) return "https";
|
|
79
|
+
if (/^http:/i.test(url)) return "http";
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Parse + validate one lock JSON. Returns a normalized lock or null. */
|
|
84
|
+
function parseLock(raw, filePath) {
|
|
85
|
+
let lock;
|
|
86
|
+
try {
|
|
87
|
+
lock = JSON.parse(raw);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (!lock || typeof lock !== "object") return null;
|
|
92
|
+
// Reject a foreign lock kind if present; absent kind is tolerated (the
|
|
93
|
+
// pdh-bridge/ dir already scopes us, but a stray file gets rejected here).
|
|
94
|
+
if (lock.kind != null && String(lock.kind) !== EXPECTED_KIND) return null;
|
|
95
|
+
if (!lock.url || !isLocalhostUrl(lock.url)) return null;
|
|
96
|
+
|
|
97
|
+
const transport = String(
|
|
98
|
+
lock.transport || inferTransportFromUrl(lock.url) || "",
|
|
99
|
+
).toLowerCase();
|
|
100
|
+
if (!SUPPORTED_TRANSPORTS.has(transport)) return null; // e.g. ws — not yet
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
kind: EXPECTED_KIND,
|
|
104
|
+
device: typeof lock.device === "string" ? lock.device : "unknown",
|
|
105
|
+
appUid: typeof lock.appUid === "number" ? lock.appUid : null,
|
|
106
|
+
transport,
|
|
107
|
+
url: lock.url,
|
|
108
|
+
port: lock.port,
|
|
109
|
+
token: typeof lock.token === "string" ? lock.token : null,
|
|
110
|
+
pid: typeof lock.pid === "number" ? lock.pid : null,
|
|
111
|
+
startedAt: typeof lock.started_at === "number" ? lock.started_at : 0,
|
|
112
|
+
_file: filePath,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** True when a lock is dead: pid gone AND file older than the TTL. */
|
|
117
|
+
function isStale(lock, deps) {
|
|
118
|
+
if (lock.pid && deps.processAlive(lock.pid)) return false;
|
|
119
|
+
let mtime;
|
|
120
|
+
try {
|
|
121
|
+
mtime = deps.statMtimeMs(lock._file);
|
|
122
|
+
} catch {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return deps.now() - mtime > STALE_TTL_MS;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Scan the lock dir → normalized, live, localhost, supported-transport locks.
|
|
130
|
+
* Missing dir / unreadable files are silently skipped (best-effort).
|
|
131
|
+
*/
|
|
132
|
+
export function readPdhLocks(deps = _deps) {
|
|
133
|
+
const dir = pdhLockDir(deps);
|
|
134
|
+
let names;
|
|
135
|
+
try {
|
|
136
|
+
names = deps.readDir(dir);
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const name of names || []) {
|
|
142
|
+
if (!String(name).endsWith(".json")) continue;
|
|
143
|
+
const fp = path.join(dir, name);
|
|
144
|
+
let raw;
|
|
145
|
+
try {
|
|
146
|
+
raw = deps.readFile(fp);
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const lock = parseLock(raw, fp);
|
|
151
|
+
if (!lock) continue;
|
|
152
|
+
if (isStale(lock, deps)) continue;
|
|
153
|
+
out.push(lock);
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Is the env wired to a PDH bridge (app-spawned cc, or explicit opt-in)? */
|
|
159
|
+
export function isInPdhTerminal(env = process.env) {
|
|
160
|
+
if (!env) return false;
|
|
161
|
+
if (env.CHAINLESSCHAIN_PDH_PORT) return true;
|
|
162
|
+
if (env.CHAINLESSCHAIN_PDH) return true;
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Pick the PDH server to connect to, or null.
|
|
168
|
+
*
|
|
169
|
+
* Path A (deterministic): if `CHAINLESSCHAIN_PDH_PORT` is set and a live lock
|
|
170
|
+
* has that port, use it (token may come from the lock or the env).
|
|
171
|
+
* Path B (scan): otherwise the newest live lock (a device has one PDH server;
|
|
172
|
+
* no workspace concept to match on, unlike the IDE bridge).
|
|
173
|
+
*
|
|
174
|
+
* @param {object} opts { env?, force? } (force kept for API symmetry)
|
|
175
|
+
* @param {object} [deps]
|
|
176
|
+
*/
|
|
177
|
+
export function discoverPdhServer(
|
|
178
|
+
{ env = process.env, force = false } = {},
|
|
179
|
+
deps = _deps,
|
|
180
|
+
) {
|
|
181
|
+
void force; // no workspace gating for PDH; param kept for call-site symmetry
|
|
182
|
+
const locks = readPdhLocks(deps);
|
|
183
|
+
if (!locks.length) return null;
|
|
184
|
+
|
|
185
|
+
// Path A — env fast-path.
|
|
186
|
+
const envPort = env && env.CHAINLESSCHAIN_PDH_PORT;
|
|
187
|
+
if (envPort) {
|
|
188
|
+
const hit = locks.find((l) => String(l.port) === String(envPort));
|
|
189
|
+
if (hit) {
|
|
190
|
+
if (!hit.token && env.CHAINLESSCHAIN_PDH_TOKEN) {
|
|
191
|
+
hit.token = env.CHAINLESSCHAIN_PDH_TOKEN;
|
|
192
|
+
}
|
|
193
|
+
return hit;
|
|
194
|
+
}
|
|
195
|
+
// env named a port with no live lock — fall through to newest.
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Path B — newest live lock.
|
|
199
|
+
return locks.slice().sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A discovered lock → an MCP server config row for `setupMcpFromConfig`.
|
|
204
|
+
* `longRunning` flags the server as exempt from the agent loop's per-call
|
|
205
|
+
* timeout: PDH collect tools can block on `assist_required` (the user does a
|
|
206
|
+
* step in the target app, then resumes) — the same blocking-tool contract as
|
|
207
|
+
* the IDE bridge's `openDiff`.
|
|
208
|
+
*/
|
|
209
|
+
export function pdhServerToMcpConfig(lock) {
|
|
210
|
+
if (!lock || !lock.url) return null;
|
|
211
|
+
const headers = {};
|
|
212
|
+
if (lock.token) headers.Authorization = `Bearer ${lock.token}`;
|
|
213
|
+
return {
|
|
214
|
+
url: lock.url,
|
|
215
|
+
transport: lock.transport,
|
|
216
|
+
headers,
|
|
217
|
+
longRunning: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Human-readable explanation of why discovery did/didn't find a server — backs
|
|
223
|
+
* `cc pdh doctor`. Returns { inPdhTerminal, lockDir, locks:[...], chosen, reason }.
|
|
224
|
+
*/
|
|
225
|
+
export function diagnosePdh(
|
|
226
|
+
{ env = process.env, force = false } = {},
|
|
227
|
+
deps = _deps,
|
|
228
|
+
) {
|
|
229
|
+
const inPdhTerminal = isInPdhTerminal(env);
|
|
230
|
+
const locks = readPdhLocks(deps);
|
|
231
|
+
const chosen = discoverPdhServer({ env, force }, deps);
|
|
232
|
+
|
|
233
|
+
let reason;
|
|
234
|
+
if (chosen) {
|
|
235
|
+
reason =
|
|
236
|
+
env &&
|
|
237
|
+
env.CHAINLESSCHAIN_PDH_PORT &&
|
|
238
|
+
String(chosen.port) === String(env.CHAINLESSCHAIN_PDH_PORT)
|
|
239
|
+
? "matched CHAINLESSCHAIN_PDH_PORT (env fast-path)"
|
|
240
|
+
: "newest live lock";
|
|
241
|
+
} else if (!locks.length) {
|
|
242
|
+
reason =
|
|
243
|
+
"no live PDH lockfiles in " +
|
|
244
|
+
pdhLockDir(deps) +
|
|
245
|
+
" (is the app's PDH bridge server running? are locks stale?)";
|
|
246
|
+
} else {
|
|
247
|
+
reason = "lockfiles present but none usable";
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
inPdhTerminal,
|
|
251
|
+
lockDir: pdhLockDir(deps),
|
|
252
|
+
locks: locks.map((l) => ({
|
|
253
|
+
device: l.device,
|
|
254
|
+
appUid: l.appUid,
|
|
255
|
+
port: l.port,
|
|
256
|
+
transport: l.transport,
|
|
257
|
+
url: l.url,
|
|
258
|
+
hasToken: !!l.token,
|
|
259
|
+
pid: l.pid,
|
|
260
|
+
})),
|
|
261
|
+
chosen: chosen
|
|
262
|
+
? { device: chosen.device, port: chosen.port, url: chosen.url }
|
|
263
|
+
: null,
|
|
264
|
+
reason,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
@@ -128,8 +128,12 @@ export function makeAtCompleter(opts = {}) {
|
|
|
128
128
|
});
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
+
// De-duplicate before sorting so a drifted/aliased entry in the hand-
|
|
132
|
+
// maintained command list never shows twice in the autocomplete (mirrors the
|
|
133
|
+
// @-path's `new Set` merge below; Claude-Code 2.1.183 fixed the same class of
|
|
134
|
+
// duplicate-in-slash-autocomplete bug).
|
|
131
135
|
const slashCommands = Array.isArray(opts.slashCommands)
|
|
132
|
-
? [...opts.slashCommands].sort()
|
|
136
|
+
? [...new Set(opts.slashCommands)].sort()
|
|
133
137
|
: [];
|
|
134
138
|
|
|
135
139
|
const completer = (line) => {
|
package/src/lib/session-tail.js
CHANGED
|
@@ -89,6 +89,10 @@ export async function* followSession(sessionId, options = {}) {
|
|
|
89
89
|
? fromOffset
|
|
90
90
|
: initialOffset(sessionId, { fromStart });
|
|
91
91
|
let buffer = "";
|
|
92
|
+
// Streaming decoder persisted across polls: when the file grows by a partial
|
|
93
|
+
// multi-byte UTF-8 character between reads (the rest arrives next poll), the
|
|
94
|
+
// held bytes are reassembled instead of corrupted into U+FFFD.
|
|
95
|
+
let decoder = new TextDecoder("utf-8");
|
|
92
96
|
|
|
93
97
|
// eslint-disable-next-line no-constant-condition
|
|
94
98
|
while (true) {
|
|
@@ -100,6 +104,7 @@ export async function* followSession(sessionId, options = {}) {
|
|
|
100
104
|
// File was truncated / rotated — restart from beginning
|
|
101
105
|
offset = 0;
|
|
102
106
|
buffer = "";
|
|
107
|
+
decoder = new TextDecoder("utf-8"); // drop any held partial-char bytes
|
|
103
108
|
}
|
|
104
109
|
if (stat.size > offset) {
|
|
105
110
|
const fd = await fsp.open(filePath, "r");
|
|
@@ -108,7 +113,7 @@ export async function* followSession(sessionId, options = {}) {
|
|
|
108
113
|
const buf = Buffer.alloc(length);
|
|
109
114
|
await fd.read(buf, 0, length, offset);
|
|
110
115
|
offset = stat.size;
|
|
111
|
-
buffer +=
|
|
116
|
+
buffer += decoder.decode(buf, { stream: true });
|
|
112
117
|
} finally {
|
|
113
118
|
await fd.close();
|
|
114
119
|
}
|
package/src/lib/skill-mcp.js
CHANGED
|
@@ -118,10 +118,16 @@ export async function mountSkillMcpServers(mcpClient, skill, opts = {}) {
|
|
|
118
118
|
for (const server of declared) {
|
|
119
119
|
const normalized = validateMcpServerConfig(server);
|
|
120
120
|
if (!normalized) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
const name = server?.name || "(invalid)";
|
|
122
|
+
skipped.push({ name, error: "invalid config" });
|
|
123
|
+
// Surface invalid configs too — previously only connect FAILURES warned,
|
|
124
|
+
// so a malformed server config was dropped silently and the skill author
|
|
125
|
+
// never learned why their MCP server didn't mount.
|
|
126
|
+
if (typeof opts.onWarn === "function") {
|
|
127
|
+
opts.onWarn(
|
|
128
|
+
`[skill-mcp] Skipped "${name}" for skill "${skill?.id || skill?.name}": invalid config`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
125
131
|
continue;
|
|
126
132
|
}
|
|
127
133
|
try {
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -1946,6 +1946,11 @@ export async function startAgentRepl(options = {}) {
|
|
|
1946
1946
|
messages.length = 1; // keep system prompt
|
|
1947
1947
|
messages.push(...rebuilt.filter((m) => m.role !== "system"));
|
|
1948
1948
|
sessionId = resumeId;
|
|
1949
|
+
// The prior session's checkpoint marks (turn-index → checkpoint id)
|
|
1950
|
+
// no longer map to this swapped-in history; keeping them would make
|
|
1951
|
+
// a later /rewind restore files from the WRONG session's snapshot.
|
|
1952
|
+
// Same reset /clear does on a context swap.
|
|
1953
|
+
_checkpointMarks.length = 0;
|
|
1949
1954
|
logger.info(
|
|
1950
1955
|
`Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
|
|
1951
1956
|
);
|
|
@@ -1959,6 +1964,8 @@ export async function startAgentRepl(options = {}) {
|
|
|
1959
1964
|
messages.length = 1; // keep system prompt
|
|
1960
1965
|
messages.push(...parsed.filter((m) => m.role !== "system"));
|
|
1961
1966
|
sessionId = existing.id;
|
|
1967
|
+
// Drop the prior session's checkpoint marks (see JSONL branch).
|
|
1968
|
+
_checkpointMarks.length = 0;
|
|
1962
1969
|
logger.info(
|
|
1963
1970
|
`Resumed session ${sessionId} (${parsed.length} messages)`,
|
|
1964
1971
|
);
|
|
@@ -2660,7 +2667,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
2660
2667
|
|
|
2661
2668
|
// `/sessions` — list recent RESUMABLE conversations (read-only; the ids
|
|
2662
2669
|
// work with `cc agent --resume <id>`). `/session` shows the current one.
|
|
2663
|
-
if (trimmed === "/sessions" || trimmed
|
|
2670
|
+
if (trimmed === "/sessions" || trimmed.startsWith("/sessions ")) {
|
|
2664
2671
|
try {
|
|
2665
2672
|
const { listRecentSessions } = await import("../lib/recent-session.js");
|
|
2666
2673
|
const { renderRecentSessions } = await import("./recent-sessions.js");
|
|
@@ -2676,7 +2683,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
2676
2683
|
// `/memory` — project-memory files auto-loaded into the system prompt
|
|
2677
2684
|
// (cc.md hierarchy + imports + path-scoped rules). Distinct from `#` (add
|
|
2678
2685
|
// a note) and `cc memory recall` (scoped store).
|
|
2679
|
-
if (trimmed === "/memory" || trimmed
|
|
2686
|
+
if (trimmed === "/memory" || trimmed.startsWith("/memory ")) {
|
|
2680
2687
|
try {
|
|
2681
2688
|
const { loadProjectInstructions } =
|
|
2682
2689
|
await import("../lib/project-instructions.js");
|
|
@@ -2695,7 +2702,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
2695
2702
|
}
|
|
2696
2703
|
|
|
2697
2704
|
// `/mcp` — overview of connected MCP servers' resources + prompts.
|
|
2698
|
-
if (trimmed === "/mcp" || trimmed
|
|
2705
|
+
if (trimmed === "/mcp" || trimmed.startsWith("/mcp ")) {
|
|
2699
2706
|
const mcpClient = _adhocMcp?.mcpClient || _bundleMcpClient;
|
|
2700
2707
|
logger.log(renderMcpSurface(mcpClient));
|
|
2701
2708
|
prompt();
|
|
@@ -2753,7 +2760,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
2753
2760
|
|
|
2754
2761
|
// `/doctor` — consolidated session-health readout (Claude-Code parity):
|
|
2755
2762
|
// provider/key/IDE/MCP/permissions/hooks in one pass-or-warn view.
|
|
2756
|
-
if (trimmed === "/doctor" || trimmed
|
|
2763
|
+
if (trimmed === "/doctor" || trimmed.startsWith("/doctor ")) {
|
|
2757
2764
|
let config = {};
|
|
2758
2765
|
try {
|
|
2759
2766
|
config = (await import("../lib/config-manager.js")).loadConfig() || {};
|
|
@@ -2860,7 +2867,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
2860
2867
|
|
|
2861
2868
|
// `/cost` — running token spend + estimated $ for this session (Claude-Code
|
|
2862
2869
|
// parity). In-memory accumulation, so it works without session persistence.
|
|
2863
|
-
if (trimmed === "/cost" || trimmed
|
|
2870
|
+
if (trimmed === "/cost" || trimmed.startsWith("/cost ")) {
|
|
2864
2871
|
let overrides;
|
|
2865
2872
|
let visionModel;
|
|
2866
2873
|
try {
|
|
@@ -2937,7 +2944,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
2937
2944
|
|
|
2938
2945
|
// `/ide` — IDE bridge connection status (Claude-Code parity): which editor
|
|
2939
2946
|
// is connected, its tools, or why discovery came up empty.
|
|
2940
|
-
if (trimmed === "/ide" || trimmed
|
|
2947
|
+
if (trimmed === "/ide" || trimmed.startsWith("/ide ")) {
|
|
2941
2948
|
let diag = null;
|
|
2942
2949
|
try {
|
|
2943
2950
|
const { diagnoseIde } = await import("../lib/ide-bridge.js");
|
|
@@ -3838,6 +3838,11 @@ export async function* agentLoop(messages, options) {
|
|
|
3838
3838
|
// True once a Stop hook has forced a continuation — passed to the next Stop
|
|
3839
3839
|
// hook as `stop_hook_active` so a well-behaved hook won't block forever.
|
|
3840
3840
|
let stopHookActive = false;
|
|
3841
|
+
// True once we have already re-prompted a thinking-only turn (a turn that
|
|
3842
|
+
// produced extended-thinking but no visible text and no tool calls). The
|
|
3843
|
+
// one-shot guard means a model that keeps returning empty turns completes
|
|
3844
|
+
// rather than looping forever (the iteration budget is the hard backstop).
|
|
3845
|
+
let emptyThinkingReprompted = false;
|
|
3841
3846
|
|
|
3842
3847
|
while (budget.hasRemaining()) {
|
|
3843
3848
|
budget.consume();
|
|
@@ -4036,6 +4041,26 @@ export async function* agentLoop(messages, options) {
|
|
|
4036
4041
|
.join("")
|
|
4037
4042
|
.trim()
|
|
4038
4043
|
: "";
|
|
4044
|
+
// Claude Code 2.1.183 parity: a turn that produced ONLY extended-thinking
|
|
4045
|
+
// (no visible text, no tool calls) would otherwise complete silently with
|
|
4046
|
+
// an empty answer — the user sees nothing. Re-prompt the model ONCE to
|
|
4047
|
+
// surface its actual response. Mirrors the Stop-hook continuation path
|
|
4048
|
+
// below (push assistant turn + a user nudge, then continue). Scoped to the
|
|
4049
|
+
// thinking-only case so a genuinely empty completion (no thinking) still
|
|
4050
|
+
// ends instead of looping.
|
|
4051
|
+
const _contentEmpty = !String(msg.content || "").trim();
|
|
4052
|
+
if (_contentEmpty && _thinking && !emptyThinkingReprompted) {
|
|
4053
|
+
emptyThinkingReprompted = true;
|
|
4054
|
+
messages.push({ role: "assistant", content: "" });
|
|
4055
|
+
messages.push({
|
|
4056
|
+
role: "user",
|
|
4057
|
+
content:
|
|
4058
|
+
"You ended your turn with only internal reasoning and no visible " +
|
|
4059
|
+
"response. Please provide your actual answer now.",
|
|
4060
|
+
});
|
|
4061
|
+
yield { type: "empty-thinking-reprompt", runId };
|
|
4062
|
+
continue;
|
|
4063
|
+
}
|
|
4039
4064
|
yield {
|
|
4040
4065
|
type: "response-complete",
|
|
4041
4066
|
content: msg.content || "",
|
|
@@ -414,6 +414,40 @@ export async function runAgentHeadless(options = {}, deps = {}) {
|
|
|
414
414
|
writeErr("No previous session to continue; starting a new one.\n");
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
// Machine-readable modes (`--output-format json` / `stream-json`) must ALWAYS
|
|
418
|
+
// end with a terminal result envelope — a consumer parsing stdout otherwise
|
|
419
|
+
// gets nothing and can't tell success from failure. Several setup-phase error
|
|
420
|
+
// paths below early-return BEFORE the main loop's try/catch envelope (and
|
|
421
|
+
// before `system/init`), so route them through this so stdout is never empty.
|
|
422
|
+
// No-op in text mode (those paths already writeErr a human message).
|
|
423
|
+
const emitHeadlessError = (resultMsg) => {
|
|
424
|
+
if (isStream) {
|
|
425
|
+
writeOut(
|
|
426
|
+
JSON.stringify({
|
|
427
|
+
type: "result",
|
|
428
|
+
subtype: "error",
|
|
429
|
+
is_error: true,
|
|
430
|
+
error: resultMsg,
|
|
431
|
+
}) + "\n",
|
|
432
|
+
);
|
|
433
|
+
} else if (isJson) {
|
|
434
|
+
writeOut(
|
|
435
|
+
JSON.stringify(
|
|
436
|
+
buildResultEnvelope({
|
|
437
|
+
subtype: "error",
|
|
438
|
+
isError: true,
|
|
439
|
+
result: resultMsg,
|
|
440
|
+
sessionId,
|
|
441
|
+
toolCalls: [],
|
|
442
|
+
usage: {},
|
|
443
|
+
numTurns: 0,
|
|
444
|
+
durationMs: 0,
|
|
445
|
+
}),
|
|
446
|
+
) + "\n",
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
417
451
|
// Load prior conversation when resuming an existing session. The fresh
|
|
418
452
|
// system prompt always leads; we drop any persisted system turns so it is
|
|
419
453
|
// never duplicated.
|
|
@@ -489,11 +523,9 @@ export async function runAgentHeadless(options = {}, deps = {}) {
|
|
|
489
523
|
writeErr(
|
|
490
524
|
`[hook] prompt blocked${ups.reason ? ": " + ups.reason : ""}\n`,
|
|
491
525
|
);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
isError: true,
|
|
496
|
-
};
|
|
526
|
+
const reason = ups.reason || "blocked by UserPromptSubmit hook";
|
|
527
|
+
emitHeadlessError(reason);
|
|
528
|
+
return { exitCode: 2, result: reason, isError: true };
|
|
497
529
|
}
|
|
498
530
|
if (ups.additionalContext) {
|
|
499
531
|
userContent += `\n\n[hook context]\n${ups.additionalContext}`;
|
|
@@ -593,6 +625,7 @@ export async function runAgentHeadless(options = {}, deps = {}) {
|
|
|
593
625
|
}
|
|
594
626
|
} catch (err) {
|
|
595
627
|
writeErr(`Error: ${err.message}\n`);
|
|
628
|
+
emitHeadlessError(err.message);
|
|
596
629
|
return { exitCode: 1, result: err.message, isError: true };
|
|
597
630
|
}
|
|
598
631
|
}
|
|
@@ -635,6 +668,7 @@ export async function runAgentHeadless(options = {}, deps = {}) {
|
|
|
635
668
|
} catch (err) {
|
|
636
669
|
writeErr(`Error: ${err.message}\n`);
|
|
637
670
|
if (mcp?.mcpClient) await mcp.mcpClient.disconnectAll().catch(() => {});
|
|
671
|
+
emitHeadlessError(err.message);
|
|
638
672
|
return { exitCode: 1, result: err.message, isError: true };
|
|
639
673
|
}
|
|
640
674
|
if (approvalGate && typeof approvalGate.setConfirmer === "function") {
|