very-happy-cli 0.2.90 → 0.2.92
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/dist/{AcpBackend-CnYZFdd0.cjs → AcpBackend-BP7id8L6.cjs} +1 -1
- package/dist/{AcpBackend-BV4wSnJ3.mjs → AcpBackend-DDEsgc1c.mjs} +1 -1
- package/dist/{AcpSessionManager-Cyne6Rmg.mjs → AcpSessionManager-C29t6wb_.mjs} +1 -1
- package/dist/{AcpSessionManager-BPBdrrME.cjs → AcpSessionManager-Cys1Hsb8.cjs} +1 -1
- package/dist/{config-CgXitj0f.cjs → config-B77vhdwT.cjs} +2 -2
- package/dist/{config-BEdyyciP.mjs → config-CJQw0z1o.mjs} +2 -2
- package/dist/{index-an09rejO.mjs → index-8LDXVYWi.mjs} +409 -51
- package/dist/{index-4YmVbx7j.cjs → index-B63Vjt4W.cjs} +425 -67
- package/dist/{index-Dfu5uvjk.mjs → index-BP9pfzdZ.mjs} +5 -5
- package/dist/{index-B1wTVXjt.cjs → index-L3o5Gz-2.cjs} +4 -4
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{installTerminalHooks-DDpVxk8-.cjs → installTerminalHooks-BBc2nij4.cjs} +2 -2
- package/dist/{installTerminalHooks-C-OgZr5k.mjs → installTerminalHooks-DDRrUG_n.mjs} +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +103 -3
- package/dist/lib.d.mts +103 -3
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-Du-N0qlK.mjs → mcp-CYJAl5pk.mjs} +2 -2
- package/dist/{mcp-B6Nq-RUL.cjs → mcp-DbYXyw1U.cjs} +2 -2
- package/dist/{runGemini-ChG7cGMC.cjs → runGemini-BDSWSID6.cjs} +4 -4
- package/dist/{runGemini-DBr_TiTL.mjs → runGemini-DHYs_Beq.mjs} +4 -4
- package/dist/{runOpenClaw-D2jtVgpC.cjs → runOpenClaw-DC9gb1bd.cjs} +3 -3
- package/dist/{runOpenClaw-Cbde8t92.mjs → runOpenClaw-DTaXxvNd.mjs} +3 -3
- package/dist/{send-Bjen1TUE.cjs → send-BEdGwmA-.cjs} +2 -2
- package/dist/{send-BHliX59m.mjs → send-BQSaVz9r.mjs} +2 -2
- package/dist/{spawn-CH1Rq-LW.cjs → spawn-CK1ma_qc.cjs} +2 -2
- package/dist/{spawn-JxaPHAoP.mjs → spawn-DS-Uz_Hw.mjs} +2 -2
- package/dist/{types-DS3i_5LV.mjs → types-B_8X_rZk.mjs} +572 -93
- package/dist/{types-BNGeiP1C.cjs → types-CjlB_fKT.cjs} +573 -93
- package/package.json +7 -7
|
@@ -51,7 +51,7 @@ var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
|
51
51
|
var pty__namespace = /*#__PURE__*/_interopNamespaceDefault(pty);
|
|
52
52
|
|
|
53
53
|
var name = "very-happy-cli";
|
|
54
|
-
var version = "0.2.
|
|
54
|
+
var version = "0.2.92";
|
|
55
55
|
var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
|
|
56
56
|
var author = "Kirill Dubovitskiy";
|
|
57
57
|
var contributors = [
|
|
@@ -164,12 +164,12 @@ var dependencies = {
|
|
|
164
164
|
zod: "^4.0.0"
|
|
165
165
|
};
|
|
166
166
|
var optionalDependencies = {
|
|
167
|
-
"very-happy-tools-arm64-darwin": "0.2.
|
|
168
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
169
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
170
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
171
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
172
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
167
|
+
"very-happy-tools-arm64-darwin": "0.2.92",
|
|
168
|
+
"very-happy-tools-x64-darwin": "0.2.92",
|
|
169
|
+
"very-happy-tools-arm64-linux": "0.2.92",
|
|
170
|
+
"very-happy-tools-x64-linux": "0.2.92",
|
|
171
|
+
"very-happy-tools-arm64-win32": "0.2.92",
|
|
172
|
+
"very-happy-tools-x64-win32": "0.2.92"
|
|
173
173
|
};
|
|
174
174
|
var devDependencies = {
|
|
175
175
|
"@slopus/happy-wire": "workspace:*",
|
|
@@ -661,9 +661,27 @@ const sessionToolCallStartEventSchema = z__namespace.object({
|
|
|
661
661
|
description: z__namespace.string(),
|
|
662
662
|
args: z__namespace.record(z__namespace.string(), z__namespace.unknown())
|
|
663
663
|
});
|
|
664
|
+
const sessionSubagentResultSchema = z__namespace.object({
|
|
665
|
+
text: z__namespace.string(),
|
|
666
|
+
truncated: z__namespace.boolean().optional()
|
|
667
|
+
});
|
|
668
|
+
const sessionSubagentUsageSchema = z__namespace.object({
|
|
669
|
+
toolUses: z__namespace.number().optional(),
|
|
670
|
+
totalTokens: z__namespace.number().optional(),
|
|
671
|
+
durationMs: z__namespace.number().optional()
|
|
672
|
+
});
|
|
664
673
|
const sessionToolCallEndEventSchema = z__namespace.object({
|
|
665
674
|
t: z__namespace.literal("tool-call-end"),
|
|
666
|
-
call: z__namespace.string()
|
|
675
|
+
call: z__namespace.string(),
|
|
676
|
+
// Foreground Agent/Task completion: the sub-agent's final report + run
|
|
677
|
+
// totals (SDK `tool_use_result`). Absent for ordinary tools and for the
|
|
678
|
+
// background stub ("Async agent launched").
|
|
679
|
+
result: sessionSubagentResultSchema.extend({
|
|
680
|
+
isError: z__namespace.boolean().optional(),
|
|
681
|
+
stats: sessionSubagentUsageSchema.extend({
|
|
682
|
+
toolStats: z__namespace.record(z__namespace.string(), z__namespace.unknown()).optional()
|
|
683
|
+
}).optional()
|
|
684
|
+
}).optional()
|
|
667
685
|
});
|
|
668
686
|
const sessionFileEventSchema = z__namespace.object({
|
|
669
687
|
t: z__namespace.literal("file"),
|
|
@@ -682,7 +700,10 @@ const sessionTurnStartEventSchema = z__namespace.object({
|
|
|
682
700
|
});
|
|
683
701
|
const sessionStartEventSchema = z__namespace.object({
|
|
684
702
|
t: z__namespace.literal("start"),
|
|
685
|
-
title: z__namespace.string().optional()
|
|
703
|
+
title: z__namespace.string().optional(),
|
|
704
|
+
// B-260-P2: identity from SDK task_started / Agent tool input.
|
|
705
|
+
description: z__namespace.string().optional(),
|
|
706
|
+
subagentType: z__namespace.string().optional()
|
|
686
707
|
});
|
|
687
708
|
const sessionTurnEndStatusSchema = z__namespace.enum(["completed", "failed", "cancelled"]);
|
|
688
709
|
const sessionTurnUsageSchema = z__namespace.object({
|
|
@@ -705,7 +726,21 @@ const sessionTurnEndEventSchema = z__namespace.object({
|
|
|
705
726
|
usage: sessionTurnUsageSchema.optional()
|
|
706
727
|
});
|
|
707
728
|
const sessionStopEventSchema = z__namespace.object({
|
|
708
|
-
t: z__namespace.literal("stop")
|
|
729
|
+
t: z__namespace.literal("stop"),
|
|
730
|
+
// B-260-P2: from SDK task_notification. A second `stop` for the same
|
|
731
|
+
// subagent may follow carrying `result` once the notification user message
|
|
732
|
+
// (which holds the <result> block) arrives; consumers fold by subagent id.
|
|
733
|
+
status: z__namespace.enum(["completed", "failed", "stopped"]).optional(),
|
|
734
|
+
result: sessionSubagentResultSchema.optional(),
|
|
735
|
+
usage: sessionSubagentUsageSchema.optional()
|
|
736
|
+
});
|
|
737
|
+
const sessionProgressEventSchema = z__namespace.object({
|
|
738
|
+
t: z__namespace.literal("progress"),
|
|
739
|
+
toolUses: z__namespace.number(),
|
|
740
|
+
lastTool: z__namespace.string().optional(),
|
|
741
|
+
totalTokens: z__namespace.number().optional(),
|
|
742
|
+
durationMs: z__namespace.number().optional(),
|
|
743
|
+
summary: z__namespace.string().optional()
|
|
709
744
|
});
|
|
710
745
|
const sessionEventSchema = z__namespace.discriminatedUnion("t", [
|
|
711
746
|
sessionTextEventSchema,
|
|
@@ -716,7 +751,8 @@ const sessionEventSchema = z__namespace.discriminatedUnion("t", [
|
|
|
716
751
|
sessionTurnStartEventSchema,
|
|
717
752
|
sessionStartEventSchema,
|
|
718
753
|
sessionTurnEndEventSchema,
|
|
719
|
-
sessionStopEventSchema
|
|
754
|
+
sessionStopEventSchema,
|
|
755
|
+
sessionProgressEventSchema
|
|
720
756
|
]);
|
|
721
757
|
const sessionEnvelopeSchema = z__namespace.object({
|
|
722
758
|
id: z__namespace.string(),
|
|
@@ -747,7 +783,14 @@ const sessionEnvelopeSchema = z__namespace.object({
|
|
|
747
783
|
path: ["role"]
|
|
748
784
|
});
|
|
749
785
|
}
|
|
750
|
-
if (
|
|
786
|
+
if (envelope.ev.t === "progress" && !envelope.subagent) {
|
|
787
|
+
ctx.addIssue({
|
|
788
|
+
code: z__namespace.ZodIssueCode.custom,
|
|
789
|
+
message: "progress events must carry a subagent",
|
|
790
|
+
path: ["subagent"]
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
if ((envelope.ev.t === "start" || envelope.ev.t === "stop" || envelope.ev.t === "progress") && envelope.role !== "agent") {
|
|
751
794
|
ctx.addIssue({
|
|
752
795
|
code: z__namespace.ZodIssueCode.custom,
|
|
753
796
|
message: `${envelope.ev.t} events must use role "agent"`,
|
|
@@ -837,7 +880,11 @@ const UpdateSessionBodySchema = z__namespace.object({
|
|
|
837
880
|
t: z__namespace.literal("update-session"),
|
|
838
881
|
id: z__namespace.string(),
|
|
839
882
|
metadata: VersionedEncryptedValueSchema.nullish(),
|
|
840
|
-
agentState: VersionedNullableEncryptedValueSchema.nullish()
|
|
883
|
+
agentState: VersionedNullableEncryptedValueSchema.nullish(),
|
|
884
|
+
/** B-265: server-owned archive intent (ms epoch, null = not archived).
|
|
885
|
+
* Present only on the archive / unarchive transitions; absent elsewhere.
|
|
886
|
+
* Old clients strip it (default z.object) and fall back to `active`. */
|
|
887
|
+
archivedAt: z__namespace.number().nullable().optional()
|
|
841
888
|
});
|
|
842
889
|
const VersionedMachineEncryptedValueSchema = z__namespace.object({
|
|
843
890
|
version: z__namespace.number(),
|
|
@@ -976,7 +1023,9 @@ const WebTerminalListItemSchema = z.z.object({
|
|
|
976
1023
|
mirrorSessionId: z.z.string().optional(),
|
|
977
1024
|
/** B-150: auto-restored after a restart (ms epoch) — the directory and the
|
|
978
1025
|
* conversation carried over, the processes did not. Cleared once opened. */
|
|
979
|
-
restoredAt: z.z.number().optional()
|
|
1026
|
+
restoredAt: z.z.number().optional(),
|
|
1027
|
+
/** B-265: @vh_title_manual is set (user renamed it). */
|
|
1028
|
+
manual: z.z.boolean().optional()
|
|
980
1029
|
});
|
|
981
1030
|
const ClosedTerminalRecordSchema = z.z.object({
|
|
982
1031
|
id: z.z.string(),
|
|
@@ -990,6 +1039,9 @@ const ClosedTerminalRecordSchema = z.z.object({
|
|
|
990
1039
|
/** B-149: 'daemon-gap' = died while no daemon was watching (restart/reboot);
|
|
991
1040
|
* absent means an ordinary observed close. */
|
|
992
1041
|
reason: z.z.enum(["closed", "daemon-gap"]).optional(),
|
|
1042
|
+
/** B-265: tags + manual-rename flag at close time (restored verbatim). */
|
|
1043
|
+
tags: z.z.array(z.z.string()).optional(),
|
|
1044
|
+
manual: z.z.boolean().optional(),
|
|
993
1045
|
closedAt: z.z.number()
|
|
994
1046
|
});
|
|
995
1047
|
z.z.object({
|
|
@@ -1034,6 +1086,15 @@ z.z.object({
|
|
|
1034
1086
|
* ignore the field; old daemons never write it (web renders nothing).
|
|
1035
1087
|
*/
|
|
1036
1088
|
closedTerminals: z.z.array(ClosedTerminalRecordSchema).optional(),
|
|
1089
|
+
/**
|
|
1090
|
+
* B-265 capability flag: this daemon answers `restore-terminal`. Same trust
|
|
1091
|
+
* rule as webTerminals — only `detectedAt >= startedAt` counts, because a
|
|
1092
|
+
* downgraded daemon spreads the stale field forward on connect.
|
|
1093
|
+
*/
|
|
1094
|
+
terminalRestore: z.z.object({
|
|
1095
|
+
rpcAvailable: z.z.boolean(),
|
|
1096
|
+
detectedAt: z.z.number()
|
|
1097
|
+
}).optional(),
|
|
1037
1098
|
/** Relay-owned CLI compatibility/update policy last checked by this daemon. */
|
|
1038
1099
|
cliUpdate: CliUpdateStateSchema.optional()
|
|
1039
1100
|
});
|
|
@@ -1484,7 +1545,7 @@ class RpcHandlerManager {
|
|
|
1484
1545
|
}
|
|
1485
1546
|
}
|
|
1486
1547
|
|
|
1487
|
-
const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-
|
|
1548
|
+
const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CjlB_fKT.cjs', document.baseURI).href))));
|
|
1488
1549
|
function projectPath() {
|
|
1489
1550
|
const path = path$1.resolve(__dirname$1, "..");
|
|
1490
1551
|
return path;
|
|
@@ -2171,6 +2232,69 @@ function shouldReconnect() {
|
|
|
2171
2232
|
return true;
|
|
2172
2233
|
}
|
|
2173
2234
|
|
|
2235
|
+
const SUBAGENT_RESULT_MAX_CHARS = 16384;
|
|
2236
|
+
const SUBAGENT_PROGRESS_MIN_INTERVAL_MS = 5e3;
|
|
2237
|
+
function capSubagentText(text, max = SUBAGENT_RESULT_MAX_CHARS) {
|
|
2238
|
+
const trimmed = text.trim();
|
|
2239
|
+
if (trimmed.length <= max) return { text: trimmed };
|
|
2240
|
+
return { text: trimmed.slice(0, max), truncated: true };
|
|
2241
|
+
}
|
|
2242
|
+
function toolUseResultToText(toolUseResult) {
|
|
2243
|
+
if (!toolUseResult || typeof toolUseResult !== "object") return null;
|
|
2244
|
+
const content = toolUseResult.content;
|
|
2245
|
+
if (typeof content === "string") return content;
|
|
2246
|
+
if (!Array.isArray(content)) return null;
|
|
2247
|
+
const parts = [];
|
|
2248
|
+
for (const block of content) {
|
|
2249
|
+
if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
|
|
2250
|
+
parts.push(block.text);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
2254
|
+
}
|
|
2255
|
+
function toolUseResultStatus(toolUseResult) {
|
|
2256
|
+
if (!toolUseResult || typeof toolUseResult !== "object") return "unknown";
|
|
2257
|
+
const status = toolUseResult.status;
|
|
2258
|
+
if (status === "async_launched") return "async_launched";
|
|
2259
|
+
if (status === "completed") return "completed";
|
|
2260
|
+
return "unknown";
|
|
2261
|
+
}
|
|
2262
|
+
function toolUseResultStats(toolUseResult) {
|
|
2263
|
+
if (!toolUseResult || typeof toolUseResult !== "object") return void 0;
|
|
2264
|
+
const r = toolUseResult;
|
|
2265
|
+
const stats = {};
|
|
2266
|
+
if (typeof r.totalToolUseCount === "number") stats.toolUses = r.totalToolUseCount;
|
|
2267
|
+
if (typeof r.totalTokens === "number") stats.totalTokens = r.totalTokens;
|
|
2268
|
+
if (typeof r.totalDurationMs === "number") stats.durationMs = r.totalDurationMs;
|
|
2269
|
+
if (r.toolStats && typeof r.toolStats === "object") stats.toolStats = r.toolStats;
|
|
2270
|
+
return Object.keys(stats).length > 0 ? stats : void 0;
|
|
2271
|
+
}
|
|
2272
|
+
function parseTaskNotificationText(text) {
|
|
2273
|
+
if (!/<task-notification>/i.test(text)) return null;
|
|
2274
|
+
const pick = (tag) => text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "i"))?.[1] ?? null;
|
|
2275
|
+
const rawStatus = pick("status")?.trim().toLowerCase() ?? null;
|
|
2276
|
+
const status = rawStatus === "completed" || rawStatus === "failed" || rawStatus === "stopped" ? rawStatus : null;
|
|
2277
|
+
let resultText = pick("result");
|
|
2278
|
+
let result = null;
|
|
2279
|
+
if (resultText !== null) {
|
|
2280
|
+
resultText = resultText.replace(/^\s*\[harness:[^\]]*\]\s*/i, "").replace(/<usage>[\s\S]*?<\/usage>/gi, "").trim();
|
|
2281
|
+
if (resultText.length > 0) result = capSubagentText(resultText);
|
|
2282
|
+
}
|
|
2283
|
+
return {
|
|
2284
|
+
toolUseId: pick("tool-use-id")?.trim() || null,
|
|
2285
|
+
taskId: pick("task-id")?.trim() || null,
|
|
2286
|
+
status,
|
|
2287
|
+
summary: pick("summary")?.trim() || null,
|
|
2288
|
+
result
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
function shouldEmitProgress(previous, toolUses, now) {
|
|
2292
|
+
if (!previous) return true;
|
|
2293
|
+
if (previous.lastToolUses === toolUses) return false;
|
|
2294
|
+
return now - previous.lastAt >= SUBAGENT_PROGRESS_MIN_INTERVAL_MS;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
const SUBAGENT_BUFFER_MAX_MESSAGES = 100;
|
|
2174
2298
|
function isSubagentTool(name) {
|
|
2175
2299
|
return name === "Task" || name === "Agent";
|
|
2176
2300
|
}
|
|
@@ -2239,6 +2363,7 @@ function bufferSubagentMessage(state, subagent, message) {
|
|
|
2239
2363
|
const buffer = getBufferedSubagentMessages(state);
|
|
2240
2364
|
const queue = buffer.get(subagent) ?? [];
|
|
2241
2365
|
queue.push(message);
|
|
2366
|
+
while (queue.length > SUBAGENT_BUFFER_MAX_MESSAGES) queue.shift();
|
|
2242
2367
|
buffer.set(subagent, queue);
|
|
2243
2368
|
}
|
|
2244
2369
|
function consumeBufferedSubagentMessages(state, subagent) {
|
|
@@ -2259,6 +2384,31 @@ function getActiveSubagents(state) {
|
|
|
2259
2384
|
}
|
|
2260
2385
|
return state.activeSubagents;
|
|
2261
2386
|
}
|
|
2387
|
+
function getAgentToolCalls(state) {
|
|
2388
|
+
if (!state.agentToolCalls) state.agentToolCalls = /* @__PURE__ */ new Set();
|
|
2389
|
+
return state.agentToolCalls;
|
|
2390
|
+
}
|
|
2391
|
+
function getSubagentMeta(state) {
|
|
2392
|
+
if (!state.subagentMeta) state.subagentMeta = /* @__PURE__ */ new Map();
|
|
2393
|
+
return state.subagentMeta;
|
|
2394
|
+
}
|
|
2395
|
+
function getStubSubagents(state) {
|
|
2396
|
+
if (!state.stubSubagents) state.stubSubagents = /* @__PURE__ */ new Set();
|
|
2397
|
+
return state.stubSubagents;
|
|
2398
|
+
}
|
|
2399
|
+
function getProgressThrottle(state) {
|
|
2400
|
+
if (!state.progressThrottle) state.progressThrottle = /* @__PURE__ */ new Map();
|
|
2401
|
+
return state.progressThrottle;
|
|
2402
|
+
}
|
|
2403
|
+
function nowOf(state) {
|
|
2404
|
+
return state.now ? state.now() : Date.now();
|
|
2405
|
+
}
|
|
2406
|
+
function rememberSubagentMeta(state, subagent, meta) {
|
|
2407
|
+
const existing = getSubagentMeta(state).get(subagent) ?? {};
|
|
2408
|
+
const description = typeof meta.description === "string" && meta.description.trim() ? meta.description.trim() : existing.description;
|
|
2409
|
+
const subagentType = typeof meta.subagentType === "string" && meta.subagentType.trim() ? meta.subagentType.trim() : existing.subagentType;
|
|
2410
|
+
getSubagentMeta(state).set(subagent, { ...description ? { description } : {}, ...subagentType ? { subagentType } : {} });
|
|
2411
|
+
}
|
|
2262
2412
|
function pickAssistantUsage(message) {
|
|
2263
2413
|
if (message.type !== "assistant") return void 0;
|
|
2264
2414
|
const usage = message.message?.usage;
|
|
@@ -2435,13 +2585,26 @@ function maybeEmitSubagentStart(state, turn, subagent, envelopes) {
|
|
|
2435
2585
|
return;
|
|
2436
2586
|
}
|
|
2437
2587
|
const title = getSubagentTitles(state).get(subagent);
|
|
2588
|
+
const meta = getSubagentMeta(state).get(subagent);
|
|
2438
2589
|
envelopes.push(createEnvelope("agent", {
|
|
2439
2590
|
t: "start",
|
|
2440
|
-
...title ? { title } : {}
|
|
2591
|
+
...title ? { title } : {},
|
|
2592
|
+
...meta?.description ? { description: meta.description } : {},
|
|
2593
|
+
...meta?.subagentType ? { subagentType: meta.subagentType } : {}
|
|
2441
2594
|
}, { turn, subagent }));
|
|
2442
2595
|
started.add(subagent);
|
|
2443
2596
|
getActiveSubagents(state).add(subagent);
|
|
2444
2597
|
}
|
|
2598
|
+
function emitSubagentStopWithStatus(state, turn, subagent, envelopes, payload) {
|
|
2599
|
+
envelopes.push(createEnvelope("agent", {
|
|
2600
|
+
t: "stop",
|
|
2601
|
+
...payload.status ? { status: payload.status } : {},
|
|
2602
|
+
...payload.result ? { result: payload.result } : {},
|
|
2603
|
+
...payload.usage ? { usage: payload.usage } : {}
|
|
2604
|
+
}, { ...turn ? { turn } : {}, subagent }));
|
|
2605
|
+
getActiveSubagents(state).delete(subagent);
|
|
2606
|
+
getStartedSubagents(state).delete(subagent);
|
|
2607
|
+
}
|
|
2445
2608
|
function maybeEmitSubagentStop(state, turn, subagent, envelopes) {
|
|
2446
2609
|
const active = getActiveSubagents(state);
|
|
2447
2610
|
if (!active.has(subagent)) {
|
|
@@ -2453,8 +2616,6 @@ function maybeEmitSubagentStop(state, turn, subagent, envelopes) {
|
|
|
2453
2616
|
function clearSubagentTracking(state) {
|
|
2454
2617
|
getUuidToProviderSubagent(state).clear();
|
|
2455
2618
|
getTaskPromptToSubagents(state).clear();
|
|
2456
|
-
getProviderSubagentToSessionSubagent(state).clear();
|
|
2457
|
-
getSubagentTitles(state).clear();
|
|
2458
2619
|
getBufferedSubagentMessages(state).clear();
|
|
2459
2620
|
getHiddenParentToolCalls(state).clear();
|
|
2460
2621
|
getStartedSubagents(state).clear();
|
|
@@ -2515,6 +2676,58 @@ function closeClaudeTurnWithStatus(state, status, meta) {
|
|
|
2515
2676
|
function mapClaudeLogMessageToSessionEnvelopes(message, state) {
|
|
2516
2677
|
return mapClaudeLogMessageToSessionEnvelopesInternal(message, state);
|
|
2517
2678
|
}
|
|
2679
|
+
function mapTaskLifecycleSystemMessage(message, state, envelopes) {
|
|
2680
|
+
const raw = message;
|
|
2681
|
+
const subtype = typeof raw.subtype === "string" ? raw.subtype : "";
|
|
2682
|
+
if (!subtype.startsWith("task_")) return;
|
|
2683
|
+
if (raw.skip_transcript === true) return;
|
|
2684
|
+
const toolUseId = typeof raw.tool_use_id === "string" && raw.tool_use_id.length > 0 ? raw.tool_use_id : null;
|
|
2685
|
+
if (!toolUseId || !getAgentToolCalls(state).has(toolUseId)) return;
|
|
2686
|
+
const subagent = ensureSessionSubagentIdForProviderSubagent(state, toolUseId);
|
|
2687
|
+
const usage = raw.usage && typeof raw.usage === "object" ? {
|
|
2688
|
+
...typeof raw.usage.tool_uses === "number" ? { toolUses: raw.usage.tool_uses } : {},
|
|
2689
|
+
...typeof raw.usage.total_tokens === "number" ? { totalTokens: raw.usage.total_tokens } : {},
|
|
2690
|
+
...typeof raw.usage.duration_ms === "number" ? { durationMs: raw.usage.duration_ms } : {}
|
|
2691
|
+
} : void 0;
|
|
2692
|
+
if (subtype === "task_started") {
|
|
2693
|
+
rememberSubagentMeta(state, subagent, { description: raw.description, subagentType: raw.subagent_type });
|
|
2694
|
+
const turn = ensureTurn(state, envelopes);
|
|
2695
|
+
getStartedSubagents(state).delete(subagent);
|
|
2696
|
+
maybeEmitSubagentStart(state, turn, subagent, envelopes);
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
if (subtype === "task_progress") {
|
|
2700
|
+
const toolUses = typeof raw.usage?.tool_uses === "number" ? raw.usage.tool_uses : 0;
|
|
2701
|
+
const throttle = getProgressThrottle(state);
|
|
2702
|
+
const now = nowOf(state);
|
|
2703
|
+
if (!shouldEmitProgress(throttle.get(subagent), toolUses, now)) return;
|
|
2704
|
+
throttle.set(subagent, { lastAt: now, lastToolUses: toolUses });
|
|
2705
|
+
const turn = ensureTurn(state, envelopes);
|
|
2706
|
+
maybeEmitSubagentStart(state, turn, subagent, envelopes);
|
|
2707
|
+
envelopes.push(createEnvelope("agent", {
|
|
2708
|
+
t: "progress",
|
|
2709
|
+
toolUses,
|
|
2710
|
+
...typeof raw.last_tool_name === "string" && raw.last_tool_name ? { lastTool: raw.last_tool_name } : {},
|
|
2711
|
+
...usage?.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {},
|
|
2712
|
+
...usage?.durationMs !== void 0 ? { durationMs: usage.durationMs } : {},
|
|
2713
|
+
...typeof raw.summary === "string" && raw.summary.trim() ? { summary: raw.summary.trim() } : {}
|
|
2714
|
+
}, { turn, subagent }));
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
if (subtype === "task_notification") {
|
|
2718
|
+
const status = raw.status === "completed" || raw.status === "failed" || raw.status === "stopped" ? raw.status : "completed";
|
|
2719
|
+
emitSubagentStopWithStatus(state, state.currentTurnId ?? void 0, subagent, envelopes, {
|
|
2720
|
+
status,
|
|
2721
|
+
...usage ? { usage } : {}
|
|
2722
|
+
});
|
|
2723
|
+
getStubSubagents(state).delete(subagent);
|
|
2724
|
+
getProgressThrottle(state).delete(subagent);
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (subtype === "task_updated" && raw.patch?.is_backgrounded === true) {
|
|
2728
|
+
getStubSubagents(state).add(subagent);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2518
2731
|
function mapClaudeLogMessageToSessionEnvelopesInternal(message, state) {
|
|
2519
2732
|
const envelopes = [];
|
|
2520
2733
|
const claudeUuid = pickUuid(message);
|
|
@@ -2535,6 +2748,7 @@ function mapClaudeLogMessageToSessionEnvelopesInternal(message, state) {
|
|
|
2535
2748
|
};
|
|
2536
2749
|
}
|
|
2537
2750
|
if (message.type === "system") {
|
|
2751
|
+
mapTaskLifecycleSystemMessage(message, state, envelopes);
|
|
2538
2752
|
return {
|
|
2539
2753
|
currentTurnId: state.currentTurnId,
|
|
2540
2754
|
envelopes
|
|
@@ -2576,6 +2790,9 @@ function mapClaudeLogMessageToSessionEnvelopesInternal(message, state) {
|
|
|
2576
2790
|
queueTaskPromptSubagent(state, prompt, call);
|
|
2577
2791
|
}
|
|
2578
2792
|
setSubagentTitle(state, sessionSubagentForCall, pickTaskTitle(block.input) ?? prompt);
|
|
2793
|
+
getAgentToolCalls(state).add(call);
|
|
2794
|
+
const input = block.input ?? {};
|
|
2795
|
+
rememberSubagentMeta(state, sessionSubagentForCall, { description: input.description, subagentType: input.subagent_type });
|
|
2579
2796
|
}
|
|
2580
2797
|
if (shouldHideParentToolCall(name)) {
|
|
2581
2798
|
getHiddenParentToolCalls(state).add(call);
|
|
@@ -2620,6 +2837,18 @@ function mapClaudeLogMessageToSessionEnvelopesInternal(message, state) {
|
|
|
2620
2837
|
maybeEmitSubagentStart(state, turnId2, subagent, envelopes);
|
|
2621
2838
|
envelopes.push(createEnvelope("agent", { t: "text", text: message.message.content }, { turn: turnId2, subagent, claudeUuid }));
|
|
2622
2839
|
} else {
|
|
2840
|
+
const origin = message.origin;
|
|
2841
|
+
if (origin?.kind === "task-notification" || /<task-notification>/i.test(message.message.content)) {
|
|
2842
|
+
const fields = parseTaskNotificationText(message.message.content);
|
|
2843
|
+
const providerId = fields?.toolUseId ?? null;
|
|
2844
|
+
const subagentForNotification = providerId && getAgentToolCalls(state).has(providerId) ? getSessionSubagentIdForProviderSubagent(state, providerId) : void 0;
|
|
2845
|
+
if (fields && subagentForNotification && (fields.result || fields.status)) {
|
|
2846
|
+
emitSubagentStopWithStatus(state, state.currentTurnId ?? void 0, subagentForNotification, envelopes, {
|
|
2847
|
+
...fields.status ? { status: fields.status } : {},
|
|
2848
|
+
...fields.result ? { result: fields.result } : {}
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2623
2852
|
closeTurn(state, "completed", envelopes);
|
|
2624
2853
|
envelopes.push(createEnvelope("user", { t: "text", text: message.message.content }, { claudeUuid }));
|
|
2625
2854
|
}
|
|
@@ -2650,6 +2879,31 @@ function mapClaudeLogMessageToSessionEnvelopesInternal(message, state) {
|
|
|
2650
2879
|
getHiddenParentToolCalls(state).delete(block.tool_use_id);
|
|
2651
2880
|
continue;
|
|
2652
2881
|
}
|
|
2882
|
+
if (sessionSubagentForToolResult && getAgentToolCalls(state).has(block.tool_use_id)) {
|
|
2883
|
+
const toolUseResult = message.tool_use_result;
|
|
2884
|
+
const resultStatus = toolUseResultStatus(toolUseResult);
|
|
2885
|
+
if (resultStatus === "async_launched") {
|
|
2886
|
+
getStubSubagents(state).add(sessionSubagentForToolResult);
|
|
2887
|
+
envelopes.push(createEnvelope("agent", { t: "tool-call-end", call: block.tool_use_id }, { turn: turnId, subagent }));
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2890
|
+
if (resultStatus === "completed") {
|
|
2891
|
+
const text = toolUseResultToText(toolUseResult);
|
|
2892
|
+
const stats = toolUseResultStats(toolUseResult);
|
|
2893
|
+
const result = text ? capSubagentText(text) : void 0;
|
|
2894
|
+
emitSubagentStopWithStatus(state, turnId, sessionSubagentForToolResult, envelopes, {
|
|
2895
|
+
status: "completed",
|
|
2896
|
+
...result ? { result } : {},
|
|
2897
|
+
...stats ? { usage: { toolUses: stats.toolUses, totalTokens: stats.totalTokens, durationMs: stats.durationMs } } : {}
|
|
2898
|
+
});
|
|
2899
|
+
envelopes.push(createEnvelope("agent", {
|
|
2900
|
+
t: "tool-call-end",
|
|
2901
|
+
call: block.tool_use_id,
|
|
2902
|
+
...result ? { result: { ...result, ...stats ? { stats } : {} } } : {}
|
|
2903
|
+
}, { turn: turnId, subagent }));
|
|
2904
|
+
continue;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2653
2907
|
if (sessionSubagentForToolResult) {
|
|
2654
2908
|
maybeEmitSubagentStop(state, turnId, sessionSubagentForToolResult, envelopes);
|
|
2655
2909
|
}
|
|
@@ -2867,10 +3121,19 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
2867
3121
|
directInboundLocalIds = /* @__PURE__ */ new Set();
|
|
2868
3122
|
routedInboundLocalIds = /* @__PURE__ */ new Set();
|
|
2869
3123
|
handoverInFlight = null;
|
|
2870
|
-
|
|
3124
|
+
/** B-265 reconnect: the message cursor was seeded from the server, so
|
|
3125
|
+
* nothing before it is history to replay — but socket fast-path routing
|
|
3126
|
+
* must wait for the first fetch, otherwise a message that lands between
|
|
3127
|
+
* the seed and the first fetch is routed twice (socket + fetch). */
|
|
3128
|
+
awaitingInitialFetch = false;
|
|
3129
|
+
constructor(token, session, opts) {
|
|
2871
3130
|
super();
|
|
2872
3131
|
this.token = token;
|
|
2873
3132
|
this.sessionId = session.id;
|
|
3133
|
+
if (opts?.initialSeq !== void 0 && opts.initialSeq >= 0) {
|
|
3134
|
+
this.lastSeq = opts.initialSeq;
|
|
3135
|
+
this.awaitingInitialFetch = true;
|
|
3136
|
+
}
|
|
2874
3137
|
this.metadata = session.metadata;
|
|
2875
3138
|
this.metadataVersion = session.metadataVersion;
|
|
2876
3139
|
this.agentState = session.agentState;
|
|
@@ -2930,7 +3193,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
2930
3193
|
}
|
|
2931
3194
|
if (data.body.t === "new-message") {
|
|
2932
3195
|
const messageSeq = data.body.message?.seq;
|
|
2933
|
-
if (this.lastSeq === 0) {
|
|
3196
|
+
if (this.lastSeq === 0 || this.awaitingInitialFetch) {
|
|
2934
3197
|
this.receiveSync.invalidate();
|
|
2935
3198
|
return;
|
|
2936
3199
|
}
|
|
@@ -3226,6 +3489,9 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3226
3489
|
if (message.localId && this.directInboundLocalIds.delete(message.localId)) {
|
|
3227
3490
|
continue;
|
|
3228
3491
|
}
|
|
3492
|
+
if (message.localId && this.routedInboundLocalIds.has(message.localId)) {
|
|
3493
|
+
continue;
|
|
3494
|
+
}
|
|
3229
3495
|
if (message.content?.t !== "encrypted") {
|
|
3230
3496
|
continue;
|
|
3231
3497
|
}
|
|
@@ -3255,6 +3521,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3255
3521
|
break;
|
|
3256
3522
|
}
|
|
3257
3523
|
}
|
|
3524
|
+
this.awaitingInitialFetch = false;
|
|
3258
3525
|
}
|
|
3259
3526
|
static MAX_OUTBOX_BATCH_SIZE = 50;
|
|
3260
3527
|
async flushOutbox() {
|
|
@@ -4502,6 +4769,101 @@ function registerTodoHandlers(manager) {
|
|
|
4502
4769
|
});
|
|
4503
4770
|
}
|
|
4504
4771
|
|
|
4772
|
+
function sanitizeTagList(value) {
|
|
4773
|
+
if (!Array.isArray(value)) return void 0;
|
|
4774
|
+
const out = [];
|
|
4775
|
+
for (const tag of value) {
|
|
4776
|
+
if (typeof tag !== "string") continue;
|
|
4777
|
+
const clean = tag.trim();
|
|
4778
|
+
if (clean && !out.includes(clean)) out.push(clean);
|
|
4779
|
+
if (out.length >= 64) break;
|
|
4780
|
+
}
|
|
4781
|
+
return out;
|
|
4782
|
+
}
|
|
4783
|
+
const LIVE_SNAPSHOT_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
4784
|
+
const LIVE_SNAPSHOT_MAX = 100;
|
|
4785
|
+
function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE_SNAPSHOT_MAX) {
|
|
4786
|
+
const out = /* @__PURE__ */ new Map();
|
|
4787
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
|
4788
|
+
const rows = [];
|
|
4789
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
4790
|
+
if (!id || !value || typeof value !== "object") continue;
|
|
4791
|
+
const { title, cwd, seenAt, tags, manual } = value;
|
|
4792
|
+
if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
|
|
4793
|
+
if (now - seenAt >= ttlMs) continue;
|
|
4794
|
+
const tagList = sanitizeTagList(tags);
|
|
4795
|
+
rows.push([id, {
|
|
4796
|
+
title: typeof title === "string" && title.trim() ? title : void 0,
|
|
4797
|
+
cwd: typeof cwd === "string" && cwd ? cwd : void 0,
|
|
4798
|
+
...tagList !== void 0 ? { tags: tagList } : {},
|
|
4799
|
+
...manual === true ? { manual: true } : {},
|
|
4800
|
+
seenAt
|
|
4801
|
+
}]);
|
|
4802
|
+
}
|
|
4803
|
+
rows.sort((a, b) => b[1].seenAt - a[1].seenAt);
|
|
4804
|
+
for (const [id, info] of rows.slice(0, max)) out.set(id, info);
|
|
4805
|
+
return out;
|
|
4806
|
+
}
|
|
4807
|
+
function serializeLiveSnapshot(map, max = LIVE_SNAPSHOT_MAX) {
|
|
4808
|
+
const rows = [...map.entries()].sort((a, b) => b[1].seenAt - a[1].seenAt).slice(0, max);
|
|
4809
|
+
const out = {};
|
|
4810
|
+
for (const [id, info] of rows) out[id] = info;
|
|
4811
|
+
return out;
|
|
4812
|
+
}
|
|
4813
|
+
function liveSnapshotChanged(prev, next) {
|
|
4814
|
+
if (prev.size !== next.size) return true;
|
|
4815
|
+
for (const [id, info] of next) {
|
|
4816
|
+
const before = prev.get(id);
|
|
4817
|
+
if (!before) return true;
|
|
4818
|
+
if (before.title !== info.title || before.cwd !== info.cwd) return true;
|
|
4819
|
+
if (!!before.manual !== !!info.manual) return true;
|
|
4820
|
+
if ((before.tags ?? []).join("\0") !== (info.tags ?? []).join("\0")) return true;
|
|
4821
|
+
}
|
|
4822
|
+
return false;
|
|
4823
|
+
}
|
|
4824
|
+
const CLAUDE_SESSION_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
4825
|
+
function isClaudeSessionId(value) {
|
|
4826
|
+
return typeof value === "string" && CLAUDE_SESSION_ID_RE.test(value);
|
|
4827
|
+
}
|
|
4828
|
+
function pickMirrorForTerminal(sessions, terminalId) {
|
|
4829
|
+
if (!terminalId) return null;
|
|
4830
|
+
let best = null;
|
|
4831
|
+
for (const [id, session] of Object.entries(sessions || {})) {
|
|
4832
|
+
const meta = session?.metadata;
|
|
4833
|
+
if (!meta || meta.terminalId !== terminalId) continue;
|
|
4834
|
+
const savedAt = typeof session.savedAt === "number" ? session.savedAt : 0;
|
|
4835
|
+
if (best && savedAt <= best.savedAt) continue;
|
|
4836
|
+
best = {
|
|
4837
|
+
id,
|
|
4838
|
+
savedAt,
|
|
4839
|
+
claude: isClaudeSessionId(meta.claudeSessionId) ? meta.claudeSessionId : void 0
|
|
4840
|
+
};
|
|
4841
|
+
}
|
|
4842
|
+
return best ? { sessionId: best.id, claudeSessionId: best.claude } : null;
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
const TERMINAL_ID_RE$1 = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
4846
|
+
function planTerminalRestore(record, facts) {
|
|
4847
|
+
if (facts.tmuxAlive) return { kind: "already-live" };
|
|
4848
|
+
if (!record.cwd || !facts.cwdExists(record.cwd)) return { kind: "error", reason: "missing-cwd" };
|
|
4849
|
+
const resumable = isClaudeSessionId(record.claudeSessionId) && facts.conversationExists(record.cwd, record.claudeSessionId);
|
|
4850
|
+
return {
|
|
4851
|
+
kind: "create",
|
|
4852
|
+
terminalId: record.id,
|
|
4853
|
+
cwd: record.cwd,
|
|
4854
|
+
title: record.title,
|
|
4855
|
+
manual: record.manual === true,
|
|
4856
|
+
tags: record.tags,
|
|
4857
|
+
...resumable ? { command: `claude --resume ${record.claudeSessionId}` } : {}
|
|
4858
|
+
};
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
function getProjectPath(workingDirectory) {
|
|
4862
|
+
const projectId = path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
4863
|
+
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
4864
|
+
return path.join(claudeConfigDir, "projects", projectId);
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4505
4867
|
var xtermHeadless = {};
|
|
4506
4868
|
|
|
4507
4869
|
var hasRequiredXtermHeadless;
|
|
@@ -9552,63 +9914,6 @@ function sendTerminalNotification(opts) {
|
|
|
9552
9914
|
});
|
|
9553
9915
|
}
|
|
9554
9916
|
|
|
9555
|
-
const LIVE_SNAPSHOT_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
9556
|
-
const LIVE_SNAPSHOT_MAX = 100;
|
|
9557
|
-
function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE_SNAPSHOT_MAX) {
|
|
9558
|
-
const out = /* @__PURE__ */ new Map();
|
|
9559
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
|
9560
|
-
const rows = [];
|
|
9561
|
-
for (const [id, value] of Object.entries(raw)) {
|
|
9562
|
-
if (!id || !value || typeof value !== "object") continue;
|
|
9563
|
-
const { title, cwd, seenAt } = value;
|
|
9564
|
-
if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
|
|
9565
|
-
if (now - seenAt >= ttlMs) continue;
|
|
9566
|
-
rows.push([id, {
|
|
9567
|
-
title: typeof title === "string" && title.trim() ? title : void 0,
|
|
9568
|
-
cwd: typeof cwd === "string" && cwd ? cwd : void 0,
|
|
9569
|
-
seenAt
|
|
9570
|
-
}]);
|
|
9571
|
-
}
|
|
9572
|
-
rows.sort((a, b) => b[1].seenAt - a[1].seenAt);
|
|
9573
|
-
for (const [id, info] of rows.slice(0, max)) out.set(id, info);
|
|
9574
|
-
return out;
|
|
9575
|
-
}
|
|
9576
|
-
function serializeLiveSnapshot(map, max = LIVE_SNAPSHOT_MAX) {
|
|
9577
|
-
const rows = [...map.entries()].sort((a, b) => b[1].seenAt - a[1].seenAt).slice(0, max);
|
|
9578
|
-
const out = {};
|
|
9579
|
-
for (const [id, info] of rows) out[id] = info;
|
|
9580
|
-
return out;
|
|
9581
|
-
}
|
|
9582
|
-
function liveSnapshotChanged(prev, next) {
|
|
9583
|
-
if (prev.size !== next.size) return true;
|
|
9584
|
-
for (const [id, info] of next) {
|
|
9585
|
-
const before = prev.get(id);
|
|
9586
|
-
if (!before) return true;
|
|
9587
|
-
if (before.title !== info.title || before.cwd !== info.cwd) return true;
|
|
9588
|
-
}
|
|
9589
|
-
return false;
|
|
9590
|
-
}
|
|
9591
|
-
const CLAUDE_SESSION_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
9592
|
-
function isClaudeSessionId(value) {
|
|
9593
|
-
return typeof value === "string" && CLAUDE_SESSION_ID_RE.test(value);
|
|
9594
|
-
}
|
|
9595
|
-
function pickMirrorForTerminal(sessions, terminalId) {
|
|
9596
|
-
if (!terminalId) return null;
|
|
9597
|
-
let best = null;
|
|
9598
|
-
for (const [id, session] of Object.entries(sessions || {})) {
|
|
9599
|
-
const meta = session?.metadata;
|
|
9600
|
-
if (!meta || meta.terminalId !== terminalId) continue;
|
|
9601
|
-
const savedAt = typeof session.savedAt === "number" ? session.savedAt : 0;
|
|
9602
|
-
if (best && savedAt <= best.savedAt) continue;
|
|
9603
|
-
best = {
|
|
9604
|
-
id,
|
|
9605
|
-
savedAt,
|
|
9606
|
-
claude: isClaudeSessionId(meta.claudeSessionId) ? meta.claudeSessionId : void 0
|
|
9607
|
-
};
|
|
9608
|
-
}
|
|
9609
|
-
return best ? { sessionId: best.id, claudeSessionId: best.claude } : null;
|
|
9610
|
-
}
|
|
9611
|
-
|
|
9612
9917
|
const AUTO_RESTORE_DEFAULT_MAX = 6;
|
|
9613
9918
|
const AUTO_RESTORE_DEFAULT_WINDOW_HOURS = 24;
|
|
9614
9919
|
const AUTO_RESTORE_HARD_MAX = 20;
|
|
@@ -9667,6 +9972,8 @@ function selectAutoRestore(candidates, opts) {
|
|
|
9667
9972
|
}
|
|
9668
9973
|
plans.push({
|
|
9669
9974
|
terminalId: c.id,
|
|
9975
|
+
...c.tags !== void 0 ? { tags: c.tags } : {},
|
|
9976
|
+
...c.manual ? { manual: true } : {},
|
|
9670
9977
|
cwd: c.cwd,
|
|
9671
9978
|
title: c.title,
|
|
9672
9979
|
claudeSessionId: c.claudeSessionId,
|
|
@@ -9715,10 +10022,11 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
9715
10022
|
const out = [];
|
|
9716
10023
|
for (const item of raw) {
|
|
9717
10024
|
if (!item || typeof item !== "object") continue;
|
|
9718
|
-
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt } = item;
|
|
10025
|
+
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual } = item;
|
|
9719
10026
|
if (typeof id !== "string" || id.length === 0 || typeof closedAt !== "number") continue;
|
|
9720
10027
|
if (seen.has(id)) continue;
|
|
9721
10028
|
seen.add(id);
|
|
10029
|
+
const tagList = sanitizeTagList(tags);
|
|
9722
10030
|
out.push({
|
|
9723
10031
|
id,
|
|
9724
10032
|
title: typeof title === "string" ? title : void 0,
|
|
@@ -9726,6 +10034,8 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
9726
10034
|
mirrorSessionId: typeof mirrorSessionId === "string" ? mirrorSessionId : void 0,
|
|
9727
10035
|
claudeSessionId: typeof claudeSessionId === "string" ? claudeSessionId : void 0,
|
|
9728
10036
|
reason: reason === "daemon-gap" || reason === "closed" ? reason : void 0,
|
|
10037
|
+
...tagList !== void 0 ? { tags: tagList } : {},
|
|
10038
|
+
...manual === true ? { manual: true } : {},
|
|
9729
10039
|
closedAt
|
|
9730
10040
|
});
|
|
9731
10041
|
}
|
|
@@ -9849,6 +10159,9 @@ function startupInjectionArgs(tmuxSession, command) {
|
|
|
9849
10159
|
["send-keys", "-t", `=${tmuxSession}:`, "Enter"]
|
|
9850
10160
|
];
|
|
9851
10161
|
}
|
|
10162
|
+
function seenInfoOf(t) {
|
|
10163
|
+
return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual };
|
|
10164
|
+
}
|
|
9852
10165
|
function terminalListSignature(items) {
|
|
9853
10166
|
const canon = [...items].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((t) => [
|
|
9854
10167
|
t.id,
|
|
@@ -10485,7 +10798,7 @@ class WebTerminalManager {
|
|
|
10485
10798
|
}
|
|
10486
10799
|
/** Refresh the per-id {title, cwd} cache from an observed live list. */
|
|
10487
10800
|
noteSeen(list) {
|
|
10488
|
-
this.lastSeenInfo = new Map(list.map((t) => [t.id,
|
|
10801
|
+
this.lastSeenInfo = new Map(list.map((t) => [t.id, seenInfoOf(t)]));
|
|
10489
10802
|
}
|
|
10490
10803
|
/** Record one terminal as closed (dedupe/cap in the pure module) and
|
|
10491
10804
|
* persist. The confirming daemonState push rides the list refresh that
|
|
@@ -10559,7 +10872,7 @@ class WebTerminalManager {
|
|
|
10559
10872
|
const liveIds = new Set(list.map((t) => t.id));
|
|
10560
10873
|
this.reconcileRestoredSnapshot(liveIds);
|
|
10561
10874
|
const next = new Map(
|
|
10562
|
-
list.map((t) => [t.id,
|
|
10875
|
+
list.map((t) => [t.id, seenInfoOf(t)])
|
|
10563
10876
|
);
|
|
10564
10877
|
let changed = false;
|
|
10565
10878
|
const now = Date.now();
|
|
@@ -10576,6 +10889,8 @@ class WebTerminalManager {
|
|
|
10576
10889
|
id,
|
|
10577
10890
|
title: info.title,
|
|
10578
10891
|
cwd: info.cwd,
|
|
10892
|
+
tags: info.tags,
|
|
10893
|
+
manual: info.manual,
|
|
10579
10894
|
// Live binding first (authoritative while it exists), persisted
|
|
10580
10895
|
// metadata as the fallback once the mirror has been torn down.
|
|
10581
10896
|
mirrorSessionId: this.mirrorResolver?.(id) ?? mirror?.sessionId,
|
|
@@ -10626,6 +10941,8 @@ class WebTerminalManager {
|
|
|
10626
10941
|
id,
|
|
10627
10942
|
title: info.title,
|
|
10628
10943
|
cwd: info.cwd,
|
|
10944
|
+
tags: info.tags,
|
|
10945
|
+
manual: info.manual,
|
|
10629
10946
|
mirrorSessionId: mirror?.sessionId,
|
|
10630
10947
|
claudeSessionId: mirror?.claudeSessionId,
|
|
10631
10948
|
reason: "daemon-gap",
|
|
@@ -10636,6 +10953,8 @@ class WebTerminalManager {
|
|
|
10636
10953
|
id,
|
|
10637
10954
|
title: info.title,
|
|
10638
10955
|
cwd: info.cwd,
|
|
10956
|
+
tags: info.tags,
|
|
10957
|
+
manual: info.manual,
|
|
10639
10958
|
seenAt: info.seenAt,
|
|
10640
10959
|
claudeSessionId: mirror?.claudeSessionId
|
|
10641
10960
|
});
|
|
@@ -10711,6 +11030,51 @@ class WebTerminalManager {
|
|
|
10711
11030
|
* exactly like a terminal whose pty was reaped.
|
|
10712
11031
|
*/
|
|
10713
11032
|
restoreOneTerminal(plan) {
|
|
11033
|
+
return this.createDetachedTerminal(plan);
|
|
11034
|
+
}
|
|
11035
|
+
/**
|
|
11036
|
+
* B-265: `restore-terminal` — bring ONE closed terminal back with its
|
|
11037
|
+
* original id / cwd / title (+ manual flag) / tags and, when the record
|
|
11038
|
+
* knows a claude conversation whose JSONL is still on disk, `claude
|
|
11039
|
+
* --resume` injected. Idempotent: an id whose tmux session already exists
|
|
11040
|
+
* is a success. The closed record is left to `pruneClosedAgainstLive` (the
|
|
11041
|
+
* next tick sees the id live again); the tombstone is deliberately kept —
|
|
11042
|
+
* a live tmux session attaches regardless, and dropping it would re-open
|
|
11043
|
+
* the stale-client resurrection hole B-149 closed.
|
|
11044
|
+
*/
|
|
11045
|
+
restoreClosedTerminal(terminalId) {
|
|
11046
|
+
if (typeof terminalId !== "string" || !TERMINAL_ID_RE$1.test(terminalId)) return { type: "error", reason: "invalid-id" };
|
|
11047
|
+
if (!isTmuxAvailable()) return { type: "error", reason: "no-tmux" };
|
|
11048
|
+
const record = this.closedTerminals.find((r) => r.id === terminalId);
|
|
11049
|
+
if (!record) return { type: "error", reason: "no-record" };
|
|
11050
|
+
const env = ptyEnv();
|
|
11051
|
+
const alive = node_child_process.spawnSync(
|
|
11052
|
+
"tmux",
|
|
11053
|
+
tmuxArgs(["has-session", "-t", `=vh-${terminalId}:`]),
|
|
11054
|
+
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11055
|
+
).status === 0;
|
|
11056
|
+
const plan = planTerminalRestore(record, {
|
|
11057
|
+
tmuxAlive: alive,
|
|
11058
|
+
cwdExists: (cwd) => {
|
|
11059
|
+
try {
|
|
11060
|
+
return node_fs.existsSync(cwd) && node_fs.statSync(cwd).isDirectory();
|
|
11061
|
+
} catch {
|
|
11062
|
+
return false;
|
|
11063
|
+
}
|
|
11064
|
+
},
|
|
11065
|
+
conversationExists: (cwd, claudeSessionId) => node_fs.existsSync(path.join(getProjectPath(cwd), `${claudeSessionId}.jsonl`))
|
|
11066
|
+
});
|
|
11067
|
+
if (plan.kind === "already-live") return { type: "success", terminalId };
|
|
11068
|
+
if (plan.kind === "error") return { type: "error", reason: plan.reason };
|
|
11069
|
+
if (!this.createDetachedTerminal(plan)) return { type: "error", reason: "create-failed" };
|
|
11070
|
+
logger.info(`[WEB TERMINAL] restored closed terminal ${terminalId} in ${plan.cwd}${plan.command ? " (conversation resumed)" : ""}`);
|
|
11071
|
+
this.kickListRefresh();
|
|
11072
|
+
return { type: "success", terminalId };
|
|
11073
|
+
}
|
|
11074
|
+
/** Shared cold-create for auto-restore (B-150) and restore-terminal
|
|
11075
|
+
* (B-265): same create-only env as the interactive path, title/tags
|
|
11076
|
+
* written back, optional startup command injected. */
|
|
11077
|
+
createDetachedTerminal(plan) {
|
|
10714
11078
|
const env = ptyEnv();
|
|
10715
11079
|
const name = `vh-${plan.terminalId}`;
|
|
10716
11080
|
const exists = node_child_process.spawnSync(
|
|
@@ -10747,18 +11111,35 @@ class WebTerminalManager {
|
|
|
10747
11111
|
tmuxArgs(["set-option", "-t", `=${name}:`, "@vh_title", plan.title]),
|
|
10748
11112
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
10749
11113
|
);
|
|
11114
|
+
if (plan.manual) {
|
|
11115
|
+
node_child_process.spawnSync(
|
|
11116
|
+
"tmux",
|
|
11117
|
+
tmuxArgs(["set-option", "-t", `=${name}:`, "@vh_title_manual", "1"]),
|
|
11118
|
+
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11119
|
+
);
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
11122
|
+
const tags = plan.tags !== void 0 ? validateTerminalTags(plan.tags) : void 0;
|
|
11123
|
+
if (tags && tags.length > 0) {
|
|
11124
|
+
node_child_process.spawnSync(
|
|
11125
|
+
"tmux",
|
|
11126
|
+
tmuxArgs(["set-option", "-t", `=${name}:`, "@vh_tags", JSON.stringify(tags)]),
|
|
11127
|
+
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11128
|
+
);
|
|
10750
11129
|
}
|
|
10751
|
-
|
|
10752
|
-
|
|
11130
|
+
if (plan.command) {
|
|
11131
|
+
for (const argv of startupInjectionArgs(name, plan.command)) {
|
|
11132
|
+
node_child_process.spawnSync("tmux", tmuxArgs(argv), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
11133
|
+
}
|
|
10753
11134
|
}
|
|
10754
|
-
logger.info(`[WEB TERMINAL]
|
|
11135
|
+
logger.info(`[WEB TERMINAL] cold-created ${plan.terminalId} in ${plan.cwd}`);
|
|
10755
11136
|
return true;
|
|
10756
11137
|
}
|
|
10757
11138
|
/** Mirror the in-memory cache to disk, but only when something a restart
|
|
10758
11139
|
* would care about changed (see liveSnapshotChanged). */
|
|
10759
11140
|
persistLiveSnapshot(live, now) {
|
|
10760
11141
|
const candidate = /* @__PURE__ */ new Map();
|
|
10761
|
-
for (const [id, info] of live) candidate.set(id, { title: info.title, cwd: info.cwd, seenAt: now });
|
|
11142
|
+
for (const [id, info] of live) candidate.set(id, { title: info.title, cwd: info.cwd, tags: info.tags, manual: info.manual, seenAt: now });
|
|
10762
11143
|
if (!liveSnapshotChanged(this.lastPersistedSnapshot, candidate)) return;
|
|
10763
11144
|
this.lastPersistedSnapshot = candidate;
|
|
10764
11145
|
saveLiveSnapshot(candidate);
|
|
@@ -11333,6 +11714,8 @@ class WebTerminalManager {
|
|
|
11333
11714
|
id: terminalId,
|
|
11334
11715
|
title: info.title,
|
|
11335
11716
|
cwd: info.cwd,
|
|
11717
|
+
tags: info.tags,
|
|
11718
|
+
manual: info.manual,
|
|
11336
11719
|
mirrorSessionId: this.mirrorResolver?.(terminalId) ?? mirror?.sessionId,
|
|
11337
11720
|
claudeSessionId: mirror?.claudeSessionId,
|
|
11338
11721
|
reason: "closed",
|
|
@@ -11416,7 +11799,8 @@ class WebTerminalManager {
|
|
|
11416
11799
|
// tmux last-activity (epoch s) → ms; optional so old daemons
|
|
11417
11800
|
// simply omit it and web clients fall back to createdAt.
|
|
11418
11801
|
activityAt: s.activity,
|
|
11419
|
-
agentState: this.probeAgentState(s.name, s.paneCurrentCommand)
|
|
11802
|
+
agentState: this.probeAgentState(s.name, s.paneCurrentCommand),
|
|
11803
|
+
manual: s.manual
|
|
11420
11804
|
});
|
|
11421
11805
|
}
|
|
11422
11806
|
return out;
|
|
@@ -11866,12 +12250,6 @@ function detectResumeSupport(happyHomeDir = configuration.happyHomeDir) {
|
|
|
11866
12250
|
};
|
|
11867
12251
|
}
|
|
11868
12252
|
|
|
11869
|
-
function getProjectPath(workingDirectory) {
|
|
11870
|
-
const projectId = path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
11871
|
-
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
11872
|
-
return path.join(claudeConfigDir, "projects", projectId);
|
|
11873
|
-
}
|
|
11874
|
-
|
|
11875
12253
|
class ForkTruncateUuidNotFoundError extends Error {
|
|
11876
12254
|
constructor(cutAfterUuid, sourcePath) {
|
|
11877
12255
|
super(`Truncation UUID ${cutAfterUuid} not found in ${sourcePath}`);
|
|
@@ -13376,6 +13754,7 @@ class ApiMachineClient {
|
|
|
13376
13754
|
cliUpdatePushChain = Promise.resolve();
|
|
13377
13755
|
rpcHandlerManager;
|
|
13378
13756
|
resumeSessionHandler = null;
|
|
13757
|
+
restartSessionHandler = null;
|
|
13379
13758
|
stopSessionHandler = null;
|
|
13380
13759
|
listTrackedSessionIds = null;
|
|
13381
13760
|
reconnectInterval = null;
|
|
@@ -13442,11 +13821,13 @@ class ApiMachineClient {
|
|
|
13442
13821
|
setRPCHandlers({
|
|
13443
13822
|
spawnSession,
|
|
13444
13823
|
resumeSession,
|
|
13824
|
+
restartSession,
|
|
13445
13825
|
stopSession,
|
|
13446
13826
|
listTrackedSessionIds,
|
|
13447
13827
|
requestShutdown
|
|
13448
13828
|
}) {
|
|
13449
13829
|
this.resumeSessionHandler = resumeSession ?? null;
|
|
13830
|
+
this.restartSessionHandler = restartSession ?? null;
|
|
13450
13831
|
this.stopSessionHandler = stopSession;
|
|
13451
13832
|
this.listTrackedSessionIds = listTrackedSessionIds ?? null;
|
|
13452
13833
|
this.rpcHandlerManager.registerHandler("spawn-happy-session", async (params) => {
|
|
@@ -13468,6 +13849,7 @@ class ApiMachineClient {
|
|
|
13468
13849
|
}
|
|
13469
13850
|
});
|
|
13470
13851
|
this.syncResumeSessionRpcRegistration();
|
|
13852
|
+
this.syncRestartSessionRpcRegistration();
|
|
13471
13853
|
this.rpcHandlerManager.registerHandler("open-terminal", async (params) => {
|
|
13472
13854
|
const { terminalId, cols, rows, cwd, fromSeq, encStream, startupCommand, resub, attachOnly, streamMode } = params || {};
|
|
13473
13855
|
const result = await this.webTerminal.open({
|
|
@@ -13543,6 +13925,14 @@ class ApiMachineClient {
|
|
|
13543
13925
|
this.encTerminals.delete(terminalId);
|
|
13544
13926
|
return { type: "success" };
|
|
13545
13927
|
});
|
|
13928
|
+
this.rpcHandlerManager.registerHandler("restore-terminal", async (params) => {
|
|
13929
|
+
const { terminalId } = params || {};
|
|
13930
|
+
const result = this.webTerminal.restoreClosedTerminal(terminalId);
|
|
13931
|
+
if (result.type === "error") {
|
|
13932
|
+
return { type: "error", reason: result.reason, errorMessage: `restore-terminal: ${result.reason}` };
|
|
13933
|
+
}
|
|
13934
|
+
return { type: "success", terminalId: result.terminalId };
|
|
13935
|
+
});
|
|
13546
13936
|
this.rpcHandlerManager.registerHandler("list-terminals", async () => {
|
|
13547
13937
|
return { type: "success", terminals: this.webTerminal.buildTerminalList() };
|
|
13548
13938
|
});
|
|
@@ -13694,6 +14084,40 @@ class ApiMachineClient {
|
|
|
13694
14084
|
return { message: "Daemon stop request acknowledged, starting shutdown sequence..." };
|
|
13695
14085
|
});
|
|
13696
14086
|
}
|
|
14087
|
+
// B-264: symmetric to resume, but the handler stops a live-but-broken
|
|
14088
|
+
// wrapper first. Old web never calls it; new web calling an old daemon that
|
|
14089
|
+
// lacks the handler gets "method not found" and shows an upgrade hint —
|
|
14090
|
+
// never a double-spawn.
|
|
14091
|
+
syncRestartSessionRpcRegistration() {
|
|
14092
|
+
const method = "restart-session";
|
|
14093
|
+
if (this.restartSessionHandler) {
|
|
14094
|
+
if (!this.rpcHandlerManager.hasHandler(method)) {
|
|
14095
|
+
this.rpcHandlerManager.registerHandler(method, async (params) => {
|
|
14096
|
+
const { sessionId, model, permissionMode } = params || {};
|
|
14097
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
14098
|
+
throw new Error("Session ID is required");
|
|
14099
|
+
}
|
|
14100
|
+
const handler = this.restartSessionHandler;
|
|
14101
|
+
if (!handler) {
|
|
14102
|
+
throw new Error("Restart session handler not available");
|
|
14103
|
+
}
|
|
14104
|
+
const result = await handler(sessionId, { model, permissionMode });
|
|
14105
|
+
switch (result.type) {
|
|
14106
|
+
case "success":
|
|
14107
|
+
return { type: "success", sessionId: result.sessionId };
|
|
14108
|
+
case "requestToApproveDirectoryCreation":
|
|
14109
|
+
return result;
|
|
14110
|
+
case "error":
|
|
14111
|
+
throw new Error(result.errorMessage);
|
|
14112
|
+
}
|
|
14113
|
+
});
|
|
14114
|
+
}
|
|
14115
|
+
return;
|
|
14116
|
+
}
|
|
14117
|
+
if (this.rpcHandlerManager.hasHandler(method)) {
|
|
14118
|
+
this.rpcHandlerManager.unregisterHandler(method);
|
|
14119
|
+
}
|
|
14120
|
+
}
|
|
13697
14121
|
syncResumeSessionRpcRegistration() {
|
|
13698
14122
|
const method = "resume-happy-session";
|
|
13699
14123
|
if (this.resumeSessionHandler) {
|
|
@@ -13901,6 +14325,9 @@ class ApiMachineClient {
|
|
|
13901
14325
|
httpPort: this.machine.daemonState?.httpPort,
|
|
13902
14326
|
startedAt: now,
|
|
13903
14327
|
webTerminals: { updatedAt: now, terminals: initialTerminals },
|
|
14328
|
+
// B-265 capability flag; restamped every connect so the
|
|
14329
|
+
// web's `detectedAt >= startedAt` trust rule holds.
|
|
14330
|
+
terminalRestore: { rpcAvailable: true, detectedAt: now },
|
|
13904
14331
|
// B-084: closed records survive daemon restarts (persisted
|
|
13905
14332
|
// in closed-terminals.json), so the connect snapshot ships
|
|
13906
14333
|
// them too — not just the incremental pushes.
|
|
@@ -13910,6 +14337,7 @@ class ApiMachineClient {
|
|
|
13910
14337
|
this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
|
|
13911
14338
|
if (!rpcAlreadyRegistered) this.rpcHandlerManager.onSocketConnect(socket);
|
|
13912
14339
|
this.syncResumeSessionRpcRegistration();
|
|
14340
|
+
this.syncRestartSessionRpcRegistration();
|
|
13913
14341
|
this.startKeepAlive();
|
|
13914
14342
|
void this.reconcileArchivedSessions();
|
|
13915
14343
|
void this.refreshRelayConnection();
|
|
@@ -14145,6 +14573,7 @@ class ApiMachineClient {
|
|
|
14145
14573
|
if (this.relaySocket !== relaySocket) return;
|
|
14146
14574
|
this.rpcHandlerManager.onSocketConnect(relaySocket);
|
|
14147
14575
|
this.syncResumeSessionRpcRegistration();
|
|
14576
|
+
this.syncRestartSessionRpcRegistration();
|
|
14148
14577
|
logger.debug(`[API MACHINE] Connected to regional relay ${selected.assignment.relayId}`);
|
|
14149
14578
|
});
|
|
14150
14579
|
relaySocket.on("disconnect", () => {
|
|
@@ -14919,8 +15348,8 @@ class ApiClient {
|
|
|
14919
15348
|
throw error;
|
|
14920
15349
|
}
|
|
14921
15350
|
}
|
|
14922
|
-
sessionSyncClient(session) {
|
|
14923
|
-
return new ApiSessionClient(this.credential.token, session);
|
|
15351
|
+
sessionSyncClient(session, opts) {
|
|
15352
|
+
return new ApiSessionClient(this.credential.token, session, opts);
|
|
14924
15353
|
}
|
|
14925
15354
|
machineSyncClient(machine) {
|
|
14926
15355
|
return new ApiMachineClient(this.credential.token, machine);
|
|
@@ -15096,6 +15525,56 @@ class ApiClient {
|
|
|
15096
15525
|
return false;
|
|
15097
15526
|
}
|
|
15098
15527
|
}
|
|
15528
|
+
/**
|
|
15529
|
+
* B-265: one session by id — the message cursor (`seq`), metadata and
|
|
15530
|
+
* agent state with their versions, decrypted with the session key the
|
|
15531
|
+
* reconnecting process already holds. `unsupported` = the server has no
|
|
15532
|
+
* such route (or the row is gone): callers fall back to the legacy
|
|
15533
|
+
* skip-all-history reconnect. Transient failures are retried a few times
|
|
15534
|
+
* first, because falling back silently discards the very message a restore
|
|
15535
|
+
* is meant to deliver.
|
|
15536
|
+
*/
|
|
15537
|
+
async getSession(sessionId, encryptionKey, encryptionVariant, opts) {
|
|
15538
|
+
const attempts = opts?.attempts ?? 3;
|
|
15539
|
+
const delays = opts?.delaysMs ?? [500, 1500];
|
|
15540
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
15541
|
+
try {
|
|
15542
|
+
const response = await axios.get(
|
|
15543
|
+
`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
|
|
15544
|
+
{
|
|
15545
|
+
headers: {
|
|
15546
|
+
"Authorization": `Bearer ${this.credential.token}`,
|
|
15547
|
+
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15548
|
+
},
|
|
15549
|
+
timeout: opts?.timeoutMs ?? 4e3,
|
|
15550
|
+
validateStatus: () => true
|
|
15551
|
+
}
|
|
15552
|
+
);
|
|
15553
|
+
if (response.status === 404 || response.status === 401 || response.status === 403) {
|
|
15554
|
+
return { ok: false, reason: "unsupported" };
|
|
15555
|
+
}
|
|
15556
|
+
if (response.status >= 200 && response.status < 300) {
|
|
15557
|
+
const row = response.data.session;
|
|
15558
|
+
if (!row || typeof row.seq !== "number" || typeof row.metadata !== "string") {
|
|
15559
|
+
return { ok: false, reason: "unsupported" };
|
|
15560
|
+
}
|
|
15561
|
+
const metadata = decrypt(encryptionKey, encryptionVariant, decodeBase64(row.metadata));
|
|
15562
|
+
if (!metadata) return { ok: false, reason: "unavailable" };
|
|
15563
|
+
const agentState = row.agentState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(row.agentState)) : null;
|
|
15564
|
+
return {
|
|
15565
|
+
ok: true,
|
|
15566
|
+
session: { seq: row.seq, metadata, metadataVersion: row.metadataVersion, agentState, agentStateVersion: row.agentStateVersion }
|
|
15567
|
+
};
|
|
15568
|
+
}
|
|
15569
|
+
logger.debug(`[API] getSession ${sessionId}: status ${response.status} (attempt ${attempt + 1}/${attempts})`);
|
|
15570
|
+
} catch (error) {
|
|
15571
|
+
logger.debug(`[API] getSession ${sessionId} failed (attempt ${attempt + 1}/${attempts}):`, error);
|
|
15572
|
+
}
|
|
15573
|
+
const delayMs = delays[Math.min(attempt, delays.length - 1)];
|
|
15574
|
+
if (attempt < attempts - 1 && delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
15575
|
+
}
|
|
15576
|
+
return { ok: false, reason: "unavailable" };
|
|
15577
|
+
}
|
|
15099
15578
|
/** Clear the server-owned archive tombstone before an intentional resume.
|
|
15100
15579
|
* Older servers return 404 because the endpoint does not exist; that is a
|
|
15101
15580
|
* compatible no-op because those servers also have no durable tombstone. */
|
|
@@ -15232,6 +15711,7 @@ exports.getProjectPath = getProjectPath;
|
|
|
15232
15711
|
exports.hardenPrivateFile = hardenPrivateFile;
|
|
15233
15712
|
exports.hardenPrivateFileSync = hardenPrivateFileSync;
|
|
15234
15713
|
exports.initializeSandbox = initializeSandbox;
|
|
15714
|
+
exports.isClaudeSessionId = isClaudeSessionId;
|
|
15235
15715
|
exports.isValidSessionId = isValidSessionId;
|
|
15236
15716
|
exports.isValidTerminalId = isValidTerminalId;
|
|
15237
15717
|
exports.listVhTerminals = listVhTerminals;
|