very-happy-cli 0.2.91 → 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-GqveWg8u.cjs → AcpBackend-BP7id8L6.cjs} +1 -1
- package/dist/{AcpBackend-B5RRlzLU.mjs → AcpBackend-DDEsgc1c.mjs} +1 -1
- package/dist/{AcpSessionManager-Ij1pI5Ex.mjs → AcpSessionManager-C29t6wb_.mjs} +1 -1
- package/dist/{AcpSessionManager-YcrSZwvq.cjs → AcpSessionManager-Cys1Hsb8.cjs} +1 -1
- package/dist/{config-C4NCw7Dz.cjs → config-B77vhdwT.cjs} +2 -2
- package/dist/{config-ra8iKxdU.mjs → config-CJQw0z1o.mjs} +2 -2
- package/dist/{index-UCYD5Wk3.mjs → index-8LDXVYWi.mjs} +337 -35
- package/dist/{index-FkXJNwiy.cjs → index-B63Vjt4W.cjs} +353 -51
- package/dist/{index-CB5H7YI6.mjs → index-BP9pfzdZ.mjs} +5 -5
- package/dist/{index-C1HIm7NY.cjs → index-L3o5Gz-2.cjs} +4 -4
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{installTerminalHooks-CoK5wNcA.cjs → installTerminalHooks-BBc2nij4.cjs} +2 -2
- package/dist/{installTerminalHooks-TkkGzzbI.mjs → installTerminalHooks-DDRrUG_n.mjs} +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +69 -3
- package/dist/lib.d.mts +69 -3
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-C3MiqObi.mjs → mcp-CYJAl5pk.mjs} +2 -2
- package/dist/{mcp-DcRvswVI.cjs → mcp-DbYXyw1U.cjs} +2 -2
- package/dist/{runGemini-Dnvd2gWq.cjs → runGemini-BDSWSID6.cjs} +4 -4
- package/dist/{runGemini-BbC0uwUj.mjs → runGemini-DHYs_Beq.mjs} +4 -4
- package/dist/{runOpenClaw-CnuTBNQW.cjs → runOpenClaw-DC9gb1bd.cjs} +3 -3
- package/dist/{runOpenClaw--nbqTW3K.mjs → runOpenClaw-DTaXxvNd.mjs} +3 -3
- package/dist/{send-DkKOPatx.cjs → send-BEdGwmA-.cjs} +2 -2
- package/dist/{send-9nJxn09Y.mjs → send-BQSaVz9r.mjs} +2 -2
- package/dist/{spawn-DE7WhS8Y.cjs → spawn-CK1ma_qc.cjs} +2 -2
- package/dist/{spawn-CRW4ioms.mjs → spawn-DS-Uz_Hw.mjs} +2 -2
- package/dist/{types-BKyjG23_.mjs → types-B_8X_rZk.mjs} +328 -85
- package/dist/{types-ClDADuxP.cjs → types-CjlB_fKT.cjs} +329 -85
- 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:*",
|
|
@@ -880,7 +880,11 @@ const UpdateSessionBodySchema = z__namespace.object({
|
|
|
880
880
|
t: z__namespace.literal("update-session"),
|
|
881
881
|
id: z__namespace.string(),
|
|
882
882
|
metadata: VersionedEncryptedValueSchema.nullish(),
|
|
883
|
-
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()
|
|
884
888
|
});
|
|
885
889
|
const VersionedMachineEncryptedValueSchema = z__namespace.object({
|
|
886
890
|
version: z__namespace.number(),
|
|
@@ -1019,7 +1023,9 @@ const WebTerminalListItemSchema = z.z.object({
|
|
|
1019
1023
|
mirrorSessionId: z.z.string().optional(),
|
|
1020
1024
|
/** B-150: auto-restored after a restart (ms epoch) — the directory and the
|
|
1021
1025
|
* conversation carried over, the processes did not. Cleared once opened. */
|
|
1022
|
-
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()
|
|
1023
1029
|
});
|
|
1024
1030
|
const ClosedTerminalRecordSchema = z.z.object({
|
|
1025
1031
|
id: z.z.string(),
|
|
@@ -1033,6 +1039,9 @@ const ClosedTerminalRecordSchema = z.z.object({
|
|
|
1033
1039
|
/** B-149: 'daemon-gap' = died while no daemon was watching (restart/reboot);
|
|
1034
1040
|
* absent means an ordinary observed close. */
|
|
1035
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(),
|
|
1036
1045
|
closedAt: z.z.number()
|
|
1037
1046
|
});
|
|
1038
1047
|
z.z.object({
|
|
@@ -1077,6 +1086,15 @@ z.z.object({
|
|
|
1077
1086
|
* ignore the field; old daemons never write it (web renders nothing).
|
|
1078
1087
|
*/
|
|
1079
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(),
|
|
1080
1098
|
/** Relay-owned CLI compatibility/update policy last checked by this daemon. */
|
|
1081
1099
|
cliUpdate: CliUpdateStateSchema.optional()
|
|
1082
1100
|
});
|
|
@@ -1527,7 +1545,7 @@ class RpcHandlerManager {
|
|
|
1527
1545
|
}
|
|
1528
1546
|
}
|
|
1529
1547
|
|
|
1530
|
-
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))));
|
|
1531
1549
|
function projectPath() {
|
|
1532
1550
|
const path = path$1.resolve(__dirname$1, "..");
|
|
1533
1551
|
return path;
|
|
@@ -3103,10 +3121,19 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3103
3121
|
directInboundLocalIds = /* @__PURE__ */ new Set();
|
|
3104
3122
|
routedInboundLocalIds = /* @__PURE__ */ new Set();
|
|
3105
3123
|
handoverInFlight = null;
|
|
3106
|
-
|
|
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) {
|
|
3107
3130
|
super();
|
|
3108
3131
|
this.token = token;
|
|
3109
3132
|
this.sessionId = session.id;
|
|
3133
|
+
if (opts?.initialSeq !== void 0 && opts.initialSeq >= 0) {
|
|
3134
|
+
this.lastSeq = opts.initialSeq;
|
|
3135
|
+
this.awaitingInitialFetch = true;
|
|
3136
|
+
}
|
|
3110
3137
|
this.metadata = session.metadata;
|
|
3111
3138
|
this.metadataVersion = session.metadataVersion;
|
|
3112
3139
|
this.agentState = session.agentState;
|
|
@@ -3166,7 +3193,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3166
3193
|
}
|
|
3167
3194
|
if (data.body.t === "new-message") {
|
|
3168
3195
|
const messageSeq = data.body.message?.seq;
|
|
3169
|
-
if (this.lastSeq === 0) {
|
|
3196
|
+
if (this.lastSeq === 0 || this.awaitingInitialFetch) {
|
|
3170
3197
|
this.receiveSync.invalidate();
|
|
3171
3198
|
return;
|
|
3172
3199
|
}
|
|
@@ -3462,6 +3489,9 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3462
3489
|
if (message.localId && this.directInboundLocalIds.delete(message.localId)) {
|
|
3463
3490
|
continue;
|
|
3464
3491
|
}
|
|
3492
|
+
if (message.localId && this.routedInboundLocalIds.has(message.localId)) {
|
|
3493
|
+
continue;
|
|
3494
|
+
}
|
|
3465
3495
|
if (message.content?.t !== "encrypted") {
|
|
3466
3496
|
continue;
|
|
3467
3497
|
}
|
|
@@ -3491,6 +3521,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3491
3521
|
break;
|
|
3492
3522
|
}
|
|
3493
3523
|
}
|
|
3524
|
+
this.awaitingInitialFetch = false;
|
|
3494
3525
|
}
|
|
3495
3526
|
static MAX_OUTBOX_BATCH_SIZE = 50;
|
|
3496
3527
|
async flushOutbox() {
|
|
@@ -4738,6 +4769,101 @@ function registerTodoHandlers(manager) {
|
|
|
4738
4769
|
});
|
|
4739
4770
|
}
|
|
4740
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
|
+
|
|
4741
4867
|
var xtermHeadless = {};
|
|
4742
4868
|
|
|
4743
4869
|
var hasRequiredXtermHeadless;
|
|
@@ -9788,63 +9914,6 @@ function sendTerminalNotification(opts) {
|
|
|
9788
9914
|
});
|
|
9789
9915
|
}
|
|
9790
9916
|
|
|
9791
|
-
const LIVE_SNAPSHOT_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
9792
|
-
const LIVE_SNAPSHOT_MAX = 100;
|
|
9793
|
-
function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE_SNAPSHOT_MAX) {
|
|
9794
|
-
const out = /* @__PURE__ */ new Map();
|
|
9795
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
|
9796
|
-
const rows = [];
|
|
9797
|
-
for (const [id, value] of Object.entries(raw)) {
|
|
9798
|
-
if (!id || !value || typeof value !== "object") continue;
|
|
9799
|
-
const { title, cwd, seenAt } = value;
|
|
9800
|
-
if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
|
|
9801
|
-
if (now - seenAt >= ttlMs) continue;
|
|
9802
|
-
rows.push([id, {
|
|
9803
|
-
title: typeof title === "string" && title.trim() ? title : void 0,
|
|
9804
|
-
cwd: typeof cwd === "string" && cwd ? cwd : void 0,
|
|
9805
|
-
seenAt
|
|
9806
|
-
}]);
|
|
9807
|
-
}
|
|
9808
|
-
rows.sort((a, b) => b[1].seenAt - a[1].seenAt);
|
|
9809
|
-
for (const [id, info] of rows.slice(0, max)) out.set(id, info);
|
|
9810
|
-
return out;
|
|
9811
|
-
}
|
|
9812
|
-
function serializeLiveSnapshot(map, max = LIVE_SNAPSHOT_MAX) {
|
|
9813
|
-
const rows = [...map.entries()].sort((a, b) => b[1].seenAt - a[1].seenAt).slice(0, max);
|
|
9814
|
-
const out = {};
|
|
9815
|
-
for (const [id, info] of rows) out[id] = info;
|
|
9816
|
-
return out;
|
|
9817
|
-
}
|
|
9818
|
-
function liveSnapshotChanged(prev, next) {
|
|
9819
|
-
if (prev.size !== next.size) return true;
|
|
9820
|
-
for (const [id, info] of next) {
|
|
9821
|
-
const before = prev.get(id);
|
|
9822
|
-
if (!before) return true;
|
|
9823
|
-
if (before.title !== info.title || before.cwd !== info.cwd) return true;
|
|
9824
|
-
}
|
|
9825
|
-
return false;
|
|
9826
|
-
}
|
|
9827
|
-
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}$/;
|
|
9828
|
-
function isClaudeSessionId(value) {
|
|
9829
|
-
return typeof value === "string" && CLAUDE_SESSION_ID_RE.test(value);
|
|
9830
|
-
}
|
|
9831
|
-
function pickMirrorForTerminal(sessions, terminalId) {
|
|
9832
|
-
if (!terminalId) return null;
|
|
9833
|
-
let best = null;
|
|
9834
|
-
for (const [id, session] of Object.entries(sessions || {})) {
|
|
9835
|
-
const meta = session?.metadata;
|
|
9836
|
-
if (!meta || meta.terminalId !== terminalId) continue;
|
|
9837
|
-
const savedAt = typeof session.savedAt === "number" ? session.savedAt : 0;
|
|
9838
|
-
if (best && savedAt <= best.savedAt) continue;
|
|
9839
|
-
best = {
|
|
9840
|
-
id,
|
|
9841
|
-
savedAt,
|
|
9842
|
-
claude: isClaudeSessionId(meta.claudeSessionId) ? meta.claudeSessionId : void 0
|
|
9843
|
-
};
|
|
9844
|
-
}
|
|
9845
|
-
return best ? { sessionId: best.id, claudeSessionId: best.claude } : null;
|
|
9846
|
-
}
|
|
9847
|
-
|
|
9848
9917
|
const AUTO_RESTORE_DEFAULT_MAX = 6;
|
|
9849
9918
|
const AUTO_RESTORE_DEFAULT_WINDOW_HOURS = 24;
|
|
9850
9919
|
const AUTO_RESTORE_HARD_MAX = 20;
|
|
@@ -9903,6 +9972,8 @@ function selectAutoRestore(candidates, opts) {
|
|
|
9903
9972
|
}
|
|
9904
9973
|
plans.push({
|
|
9905
9974
|
terminalId: c.id,
|
|
9975
|
+
...c.tags !== void 0 ? { tags: c.tags } : {},
|
|
9976
|
+
...c.manual ? { manual: true } : {},
|
|
9906
9977
|
cwd: c.cwd,
|
|
9907
9978
|
title: c.title,
|
|
9908
9979
|
claudeSessionId: c.claudeSessionId,
|
|
@@ -9951,10 +10022,11 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
9951
10022
|
const out = [];
|
|
9952
10023
|
for (const item of raw) {
|
|
9953
10024
|
if (!item || typeof item !== "object") continue;
|
|
9954
|
-
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt } = item;
|
|
10025
|
+
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual } = item;
|
|
9955
10026
|
if (typeof id !== "string" || id.length === 0 || typeof closedAt !== "number") continue;
|
|
9956
10027
|
if (seen.has(id)) continue;
|
|
9957
10028
|
seen.add(id);
|
|
10029
|
+
const tagList = sanitizeTagList(tags);
|
|
9958
10030
|
out.push({
|
|
9959
10031
|
id,
|
|
9960
10032
|
title: typeof title === "string" ? title : void 0,
|
|
@@ -9962,6 +10034,8 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
9962
10034
|
mirrorSessionId: typeof mirrorSessionId === "string" ? mirrorSessionId : void 0,
|
|
9963
10035
|
claudeSessionId: typeof claudeSessionId === "string" ? claudeSessionId : void 0,
|
|
9964
10036
|
reason: reason === "daemon-gap" || reason === "closed" ? reason : void 0,
|
|
10037
|
+
...tagList !== void 0 ? { tags: tagList } : {},
|
|
10038
|
+
...manual === true ? { manual: true } : {},
|
|
9965
10039
|
closedAt
|
|
9966
10040
|
});
|
|
9967
10041
|
}
|
|
@@ -10085,6 +10159,9 @@ function startupInjectionArgs(tmuxSession, command) {
|
|
|
10085
10159
|
["send-keys", "-t", `=${tmuxSession}:`, "Enter"]
|
|
10086
10160
|
];
|
|
10087
10161
|
}
|
|
10162
|
+
function seenInfoOf(t) {
|
|
10163
|
+
return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual };
|
|
10164
|
+
}
|
|
10088
10165
|
function terminalListSignature(items) {
|
|
10089
10166
|
const canon = [...items].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((t) => [
|
|
10090
10167
|
t.id,
|
|
@@ -10721,7 +10798,7 @@ class WebTerminalManager {
|
|
|
10721
10798
|
}
|
|
10722
10799
|
/** Refresh the per-id {title, cwd} cache from an observed live list. */
|
|
10723
10800
|
noteSeen(list) {
|
|
10724
|
-
this.lastSeenInfo = new Map(list.map((t) => [t.id,
|
|
10801
|
+
this.lastSeenInfo = new Map(list.map((t) => [t.id, seenInfoOf(t)]));
|
|
10725
10802
|
}
|
|
10726
10803
|
/** Record one terminal as closed (dedupe/cap in the pure module) and
|
|
10727
10804
|
* persist. The confirming daemonState push rides the list refresh that
|
|
@@ -10795,7 +10872,7 @@ class WebTerminalManager {
|
|
|
10795
10872
|
const liveIds = new Set(list.map((t) => t.id));
|
|
10796
10873
|
this.reconcileRestoredSnapshot(liveIds);
|
|
10797
10874
|
const next = new Map(
|
|
10798
|
-
list.map((t) => [t.id,
|
|
10875
|
+
list.map((t) => [t.id, seenInfoOf(t)])
|
|
10799
10876
|
);
|
|
10800
10877
|
let changed = false;
|
|
10801
10878
|
const now = Date.now();
|
|
@@ -10812,6 +10889,8 @@ class WebTerminalManager {
|
|
|
10812
10889
|
id,
|
|
10813
10890
|
title: info.title,
|
|
10814
10891
|
cwd: info.cwd,
|
|
10892
|
+
tags: info.tags,
|
|
10893
|
+
manual: info.manual,
|
|
10815
10894
|
// Live binding first (authoritative while it exists), persisted
|
|
10816
10895
|
// metadata as the fallback once the mirror has been torn down.
|
|
10817
10896
|
mirrorSessionId: this.mirrorResolver?.(id) ?? mirror?.sessionId,
|
|
@@ -10862,6 +10941,8 @@ class WebTerminalManager {
|
|
|
10862
10941
|
id,
|
|
10863
10942
|
title: info.title,
|
|
10864
10943
|
cwd: info.cwd,
|
|
10944
|
+
tags: info.tags,
|
|
10945
|
+
manual: info.manual,
|
|
10865
10946
|
mirrorSessionId: mirror?.sessionId,
|
|
10866
10947
|
claudeSessionId: mirror?.claudeSessionId,
|
|
10867
10948
|
reason: "daemon-gap",
|
|
@@ -10872,6 +10953,8 @@ class WebTerminalManager {
|
|
|
10872
10953
|
id,
|
|
10873
10954
|
title: info.title,
|
|
10874
10955
|
cwd: info.cwd,
|
|
10956
|
+
tags: info.tags,
|
|
10957
|
+
manual: info.manual,
|
|
10875
10958
|
seenAt: info.seenAt,
|
|
10876
10959
|
claudeSessionId: mirror?.claudeSessionId
|
|
10877
10960
|
});
|
|
@@ -10947,6 +11030,51 @@ class WebTerminalManager {
|
|
|
10947
11030
|
* exactly like a terminal whose pty was reaped.
|
|
10948
11031
|
*/
|
|
10949
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) {
|
|
10950
11078
|
const env = ptyEnv();
|
|
10951
11079
|
const name = `vh-${plan.terminalId}`;
|
|
10952
11080
|
const exists = node_child_process.spawnSync(
|
|
@@ -10983,18 +11111,35 @@ class WebTerminalManager {
|
|
|
10983
11111
|
tmuxArgs(["set-option", "-t", `=${name}:`, "@vh_title", plan.title]),
|
|
10984
11112
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
10985
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
|
+
);
|
|
10986
11129
|
}
|
|
10987
|
-
|
|
10988
|
-
|
|
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
|
+
}
|
|
10989
11134
|
}
|
|
10990
|
-
logger.info(`[WEB TERMINAL]
|
|
11135
|
+
logger.info(`[WEB TERMINAL] cold-created ${plan.terminalId} in ${plan.cwd}`);
|
|
10991
11136
|
return true;
|
|
10992
11137
|
}
|
|
10993
11138
|
/** Mirror the in-memory cache to disk, but only when something a restart
|
|
10994
11139
|
* would care about changed (see liveSnapshotChanged). */
|
|
10995
11140
|
persistLiveSnapshot(live, now) {
|
|
10996
11141
|
const candidate = /* @__PURE__ */ new Map();
|
|
10997
|
-
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 });
|
|
10998
11143
|
if (!liveSnapshotChanged(this.lastPersistedSnapshot, candidate)) return;
|
|
10999
11144
|
this.lastPersistedSnapshot = candidate;
|
|
11000
11145
|
saveLiveSnapshot(candidate);
|
|
@@ -11569,6 +11714,8 @@ class WebTerminalManager {
|
|
|
11569
11714
|
id: terminalId,
|
|
11570
11715
|
title: info.title,
|
|
11571
11716
|
cwd: info.cwd,
|
|
11717
|
+
tags: info.tags,
|
|
11718
|
+
manual: info.manual,
|
|
11572
11719
|
mirrorSessionId: this.mirrorResolver?.(terminalId) ?? mirror?.sessionId,
|
|
11573
11720
|
claudeSessionId: mirror?.claudeSessionId,
|
|
11574
11721
|
reason: "closed",
|
|
@@ -11652,7 +11799,8 @@ class WebTerminalManager {
|
|
|
11652
11799
|
// tmux last-activity (epoch s) → ms; optional so old daemons
|
|
11653
11800
|
// simply omit it and web clients fall back to createdAt.
|
|
11654
11801
|
activityAt: s.activity,
|
|
11655
|
-
agentState: this.probeAgentState(s.name, s.paneCurrentCommand)
|
|
11802
|
+
agentState: this.probeAgentState(s.name, s.paneCurrentCommand),
|
|
11803
|
+
manual: s.manual
|
|
11656
11804
|
});
|
|
11657
11805
|
}
|
|
11658
11806
|
return out;
|
|
@@ -12102,12 +12250,6 @@ function detectResumeSupport(happyHomeDir = configuration.happyHomeDir) {
|
|
|
12102
12250
|
};
|
|
12103
12251
|
}
|
|
12104
12252
|
|
|
12105
|
-
function getProjectPath(workingDirectory) {
|
|
12106
|
-
const projectId = path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
12107
|
-
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
12108
|
-
return path.join(claudeConfigDir, "projects", projectId);
|
|
12109
|
-
}
|
|
12110
|
-
|
|
12111
12253
|
class ForkTruncateUuidNotFoundError extends Error {
|
|
12112
12254
|
constructor(cutAfterUuid, sourcePath) {
|
|
12113
12255
|
super(`Truncation UUID ${cutAfterUuid} not found in ${sourcePath}`);
|
|
@@ -13612,6 +13754,7 @@ class ApiMachineClient {
|
|
|
13612
13754
|
cliUpdatePushChain = Promise.resolve();
|
|
13613
13755
|
rpcHandlerManager;
|
|
13614
13756
|
resumeSessionHandler = null;
|
|
13757
|
+
restartSessionHandler = null;
|
|
13615
13758
|
stopSessionHandler = null;
|
|
13616
13759
|
listTrackedSessionIds = null;
|
|
13617
13760
|
reconnectInterval = null;
|
|
@@ -13678,11 +13821,13 @@ class ApiMachineClient {
|
|
|
13678
13821
|
setRPCHandlers({
|
|
13679
13822
|
spawnSession,
|
|
13680
13823
|
resumeSession,
|
|
13824
|
+
restartSession,
|
|
13681
13825
|
stopSession,
|
|
13682
13826
|
listTrackedSessionIds,
|
|
13683
13827
|
requestShutdown
|
|
13684
13828
|
}) {
|
|
13685
13829
|
this.resumeSessionHandler = resumeSession ?? null;
|
|
13830
|
+
this.restartSessionHandler = restartSession ?? null;
|
|
13686
13831
|
this.stopSessionHandler = stopSession;
|
|
13687
13832
|
this.listTrackedSessionIds = listTrackedSessionIds ?? null;
|
|
13688
13833
|
this.rpcHandlerManager.registerHandler("spawn-happy-session", async (params) => {
|
|
@@ -13704,6 +13849,7 @@ class ApiMachineClient {
|
|
|
13704
13849
|
}
|
|
13705
13850
|
});
|
|
13706
13851
|
this.syncResumeSessionRpcRegistration();
|
|
13852
|
+
this.syncRestartSessionRpcRegistration();
|
|
13707
13853
|
this.rpcHandlerManager.registerHandler("open-terminal", async (params) => {
|
|
13708
13854
|
const { terminalId, cols, rows, cwd, fromSeq, encStream, startupCommand, resub, attachOnly, streamMode } = params || {};
|
|
13709
13855
|
const result = await this.webTerminal.open({
|
|
@@ -13779,6 +13925,14 @@ class ApiMachineClient {
|
|
|
13779
13925
|
this.encTerminals.delete(terminalId);
|
|
13780
13926
|
return { type: "success" };
|
|
13781
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
|
+
});
|
|
13782
13936
|
this.rpcHandlerManager.registerHandler("list-terminals", async () => {
|
|
13783
13937
|
return { type: "success", terminals: this.webTerminal.buildTerminalList() };
|
|
13784
13938
|
});
|
|
@@ -13930,6 +14084,40 @@ class ApiMachineClient {
|
|
|
13930
14084
|
return { message: "Daemon stop request acknowledged, starting shutdown sequence..." };
|
|
13931
14085
|
});
|
|
13932
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
|
+
}
|
|
13933
14121
|
syncResumeSessionRpcRegistration() {
|
|
13934
14122
|
const method = "resume-happy-session";
|
|
13935
14123
|
if (this.resumeSessionHandler) {
|
|
@@ -14137,6 +14325,9 @@ class ApiMachineClient {
|
|
|
14137
14325
|
httpPort: this.machine.daemonState?.httpPort,
|
|
14138
14326
|
startedAt: now,
|
|
14139
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 },
|
|
14140
14331
|
// B-084: closed records survive daemon restarts (persisted
|
|
14141
14332
|
// in closed-terminals.json), so the connect snapshot ships
|
|
14142
14333
|
// them too — not just the incremental pushes.
|
|
@@ -14146,6 +14337,7 @@ class ApiMachineClient {
|
|
|
14146
14337
|
this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
|
|
14147
14338
|
if (!rpcAlreadyRegistered) this.rpcHandlerManager.onSocketConnect(socket);
|
|
14148
14339
|
this.syncResumeSessionRpcRegistration();
|
|
14340
|
+
this.syncRestartSessionRpcRegistration();
|
|
14149
14341
|
this.startKeepAlive();
|
|
14150
14342
|
void this.reconcileArchivedSessions();
|
|
14151
14343
|
void this.refreshRelayConnection();
|
|
@@ -14381,6 +14573,7 @@ class ApiMachineClient {
|
|
|
14381
14573
|
if (this.relaySocket !== relaySocket) return;
|
|
14382
14574
|
this.rpcHandlerManager.onSocketConnect(relaySocket);
|
|
14383
14575
|
this.syncResumeSessionRpcRegistration();
|
|
14576
|
+
this.syncRestartSessionRpcRegistration();
|
|
14384
14577
|
logger.debug(`[API MACHINE] Connected to regional relay ${selected.assignment.relayId}`);
|
|
14385
14578
|
});
|
|
14386
14579
|
relaySocket.on("disconnect", () => {
|
|
@@ -15155,8 +15348,8 @@ class ApiClient {
|
|
|
15155
15348
|
throw error;
|
|
15156
15349
|
}
|
|
15157
15350
|
}
|
|
15158
|
-
sessionSyncClient(session) {
|
|
15159
|
-
return new ApiSessionClient(this.credential.token, session);
|
|
15351
|
+
sessionSyncClient(session, opts) {
|
|
15352
|
+
return new ApiSessionClient(this.credential.token, session, opts);
|
|
15160
15353
|
}
|
|
15161
15354
|
machineSyncClient(machine) {
|
|
15162
15355
|
return new ApiMachineClient(this.credential.token, machine);
|
|
@@ -15332,6 +15525,56 @@ class ApiClient {
|
|
|
15332
15525
|
return false;
|
|
15333
15526
|
}
|
|
15334
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
|
+
}
|
|
15335
15578
|
/** Clear the server-owned archive tombstone before an intentional resume.
|
|
15336
15579
|
* Older servers return 404 because the endpoint does not exist; that is a
|
|
15337
15580
|
* compatible no-op because those servers also have no durable tombstone. */
|
|
@@ -15468,6 +15711,7 @@ exports.getProjectPath = getProjectPath;
|
|
|
15468
15711
|
exports.hardenPrivateFile = hardenPrivateFile;
|
|
15469
15712
|
exports.hardenPrivateFileSync = hardenPrivateFileSync;
|
|
15470
15713
|
exports.initializeSandbox = initializeSandbox;
|
|
15714
|
+
exports.isClaudeSessionId = isClaudeSessionId;
|
|
15471
15715
|
exports.isValidSessionId = isValidSessionId;
|
|
15472
15716
|
exports.isValidTerminalId = isValidTerminalId;
|
|
15473
15717
|
exports.listVhTerminals = listVhTerminals;
|