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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
|
|
2
2
|
import os, { homedir } from 'node:os';
|
|
3
3
|
import { randomUUID, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
|
|
4
|
-
import { n as readDaemonState, l as logger, c as configuration, o as clearDaemonState, q as getProjectPath, p as projectPath, s as initializeSandbox, t as wrapCommand, d as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, g as delay, u as AsyncLock, v as ensurePrivateDirectory, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, x as hardenPrivateFile, f as packageJson, i as readSettings, y as readCredentials, z as credentialRelayProblem, B as deriveLocalCliUpdateSummary, j as encodeBase64, C as updateSettings, D as encodeBase64Url, E as decodeBase64, F as writeCredentialsLegacy, G as writeCredentialsDataKey, H as readCredentialsForConfiguredRelay, J as encrypt, r as readPersistedSessions, K as
|
|
4
|
+
import { n as readDaemonState, l as logger, c as configuration, o as clearDaemonState, q as getProjectPath, p as projectPath, s as initializeSandbox, t as wrapCommand, d as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, g as delay, u as AsyncLock, v as ensurePrivateDirectory, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, x as hardenPrivateFile, f as packageJson, i as readSettings, y as readCredentials, z as credentialRelayProblem, B as deriveLocalCliUpdateSummary, j as encodeBase64, C as updateSettings, D as encodeBase64Url, E as decodeBase64, F as writeCredentialsLegacy, G as writeCredentialsDataKey, H as readCredentialsForConfiguredRelay, J as encrypt, r as readPersistedSessions, K as isClaudeSessionId, L as getLocalHappyAgentCredentialPath, N as readLocalHappyAgentCredentials, O as decryptWithDataKey, P as decryptLegacy, Q as extractCompleteLines, S as decideBackfill, T as parseTerminalHookPayload, U as tmuxArgs, V as scrubTmuxClientEnv, W as decideMirrorBinding, X as persistSession, Y as MIRROR_BACKFILL_LINES_DEFAULT, Z as mirrorLineKey, _ as mirrorLocalId, $ as detectResumeSupport, a0 as detectCLIAvailability, a1 as acquireDaemonLock, a2 as summarizeSpawnSessionForLog, a3 as deletePersistedSessions, a4 as decrypt, a5 as writeDaemonState, A as ApiClient, a6 as fetchCliUpdateState, a7 as resolveCliUpdateCheckInterval, a8 as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, a9 as releaseDaemonLock, aa as isValidSessionId, ab as listVhTerminals, ac as isValidTerminalId, ad as readVhTerminal, ae as sendToVhTerminal, h as connectionState, af as startOfflineReconnection, ag as clearCredentials, ah as clearMachineId, k as ensurePrivateDirectorySync, w as writePrivateFileSync, ai as SandboxConfigSchema, b as createEnvelope, aj as CodexAppServerClient, ak as getLatestDaemonLog } from './types-B_8X_rZk.mjs';
|
|
5
5
|
import spawn$1, { spawn } from 'cross-spawn';
|
|
6
6
|
import path, { join, resolve, basename, isAbsolute, normalize, sep, dirname } from 'node:path';
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
@@ -42,6 +42,56 @@ import inquirer from 'inquirer';
|
|
|
42
42
|
import { fileURLToPath } from 'node:url';
|
|
43
43
|
import { createInterface as createInterface$1 } from 'node:readline/promises';
|
|
44
44
|
|
|
45
|
+
const PROCESS_IDENTITY_FIELDS = [
|
|
46
|
+
"hostPid",
|
|
47
|
+
"version",
|
|
48
|
+
"startedBy",
|
|
49
|
+
"startedFromDaemon",
|
|
50
|
+
"happyHomeDir",
|
|
51
|
+
"happyLibDir",
|
|
52
|
+
"happyToolsDir",
|
|
53
|
+
"capabilities",
|
|
54
|
+
"attachmentKinds",
|
|
55
|
+
"queueCancellation",
|
|
56
|
+
"sandbox",
|
|
57
|
+
"dangerouslySkipPermissions",
|
|
58
|
+
"os",
|
|
59
|
+
"host",
|
|
60
|
+
"homeDir",
|
|
61
|
+
"permissionMode"
|
|
62
|
+
];
|
|
63
|
+
function processIdentityFields(local) {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const key of PROCESS_IDENTITY_FIELDS) {
|
|
66
|
+
const value = local[key];
|
|
67
|
+
if (value !== void 0) out[key] = value;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function mergeReconnectMetadata(server, local, now) {
|
|
72
|
+
return {
|
|
73
|
+
...server,
|
|
74
|
+
...processIdentityFields(local),
|
|
75
|
+
lifecycleState: "running",
|
|
76
|
+
lifecycleStateSince: now,
|
|
77
|
+
archivedBy: void 0,
|
|
78
|
+
archiveReason: void 0
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function applyServerSnapshot(response, snapshot) {
|
|
82
|
+
return {
|
|
83
|
+
...response,
|
|
84
|
+
seq: snapshot.seq,
|
|
85
|
+
metadata: snapshot.metadata,
|
|
86
|
+
metadataVersion: snapshot.metadataVersion,
|
|
87
|
+
agentState: snapshot.agentState,
|
|
88
|
+
agentStateVersion: snapshot.agentStateVersion
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function withoutServerSnapshot(response) {
|
|
92
|
+
return { ...response, metadataVersion: 0, agentStateVersion: 0 };
|
|
93
|
+
}
|
|
94
|
+
|
|
45
95
|
function daemonControlHeaders(controlToken) {
|
|
46
96
|
return {
|
|
47
97
|
"Content-Type": "application/json",
|
|
@@ -1548,6 +1598,9 @@ function query(params) {
|
|
|
1548
1598
|
if (opts?.mcpServers && Object.keys(opts.mcpServers).length > 0) {
|
|
1549
1599
|
ensureLocalProxyBypass(env);
|
|
1550
1600
|
}
|
|
1601
|
+
if (sdkOptions.allowDangerouslySkipPermissions && typeof process.getuid === "function" && process.getuid() === 0 && env.IS_SANDBOX === void 0) {
|
|
1602
|
+
env.IS_SANDBOX = "1";
|
|
1603
|
+
}
|
|
1551
1604
|
sdkOptions.env = env;
|
|
1552
1605
|
if (opts?.canCallTool) {
|
|
1553
1606
|
const callback = opts.canCallTool;
|
|
@@ -1633,6 +1686,12 @@ function resolveRemoteClaudePermissionMode(currentMode, incomingMode, sandboxEna
|
|
|
1633
1686
|
}
|
|
1634
1687
|
return nextMode;
|
|
1635
1688
|
}
|
|
1689
|
+
function reconcilePublishedPermissionMode(input) {
|
|
1690
|
+
if (!input.effective) return null;
|
|
1691
|
+
const mismatch = input.effective !== input.intent;
|
|
1692
|
+
if (input.effective === input.published) return mismatch ? { publish: input.effective, mismatch } : null;
|
|
1693
|
+
return { publish: input.effective, mismatch };
|
|
1694
|
+
}
|
|
1636
1695
|
|
|
1637
1696
|
function parseCompact(message) {
|
|
1638
1697
|
const trimmed = message.trim();
|
|
@@ -1962,7 +2021,12 @@ async function claudeRemote(opts) {
|
|
|
1962
2021
|
mcpServers: systemInit.mcp_servers?.map((s) => ({ name: s.name, status: s.status })),
|
|
1963
2022
|
skills: systemInit.skills,
|
|
1964
2023
|
model: systemInit.model,
|
|
1965
|
-
modelIsDefault: initial.mode.model == null
|
|
2024
|
+
modelIsDefault: initial.mode.model == null,
|
|
2025
|
+
// The SDK's own verdict on the mode it will enforce —
|
|
2026
|
+
// settings (permissions.deny/ask/defaultMode/
|
|
2027
|
+
// disableBypassPermissionsMode) can override what we
|
|
2028
|
+
// asked for. runClaude publishes THIS, not our intent.
|
|
2029
|
+
permissionMode: systemInit.permissionMode
|
|
1966
2030
|
});
|
|
1967
2031
|
}
|
|
1968
2032
|
if (systemInit.session_id) {
|
|
@@ -1980,6 +2044,10 @@ async function claudeRemote(opts) {
|
|
|
1980
2044
|
if (message.type === "result") {
|
|
1981
2045
|
updateThinking(false);
|
|
1982
2046
|
logger.debug("[claudeRemote] Result received");
|
|
2047
|
+
const denials = message.permission_denials;
|
|
2048
|
+
if (denials && denials.length > 0) {
|
|
2049
|
+
logger.warn(`[claudeRemote] ${denials.length} tool call(s) auto-denied by Claude Code policy: ${denials.map((d) => d.tool_name ?? "?").join(", ")}`);
|
|
2050
|
+
}
|
|
1983
2051
|
if (isCompactCommand) {
|
|
1984
2052
|
const compactSucceeded = message.subtype === "success";
|
|
1985
2053
|
logger.debug(`[claudeRemote] Compaction ${compactSucceeded ? "completed" : "failed"}`);
|
|
@@ -2190,16 +2258,9 @@ class PermissionHandler {
|
|
|
2190
2258
|
}
|
|
2191
2259
|
} else {
|
|
2192
2260
|
if (response.approved && response.mode) {
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
this.permissionMode = response.mode;
|
|
2197
|
-
this.onModeChangedCallback?.(response.mode);
|
|
2198
|
-
} catch (err) {
|
|
2199
|
-
const reason = `Failed to switch permission mode: ${err instanceof Error ? err.message : String(err)}`;
|
|
2200
|
-
pending.resolve({ behavior: "deny", message: reason });
|
|
2201
|
-
return { ...response, approved: false, reason };
|
|
2202
|
-
}
|
|
2261
|
+
this.permissionMode = response.mode;
|
|
2262
|
+
this.deferredPermissionMode = response.mode;
|
|
2263
|
+
this.onModeChangedCallback?.(response.mode);
|
|
2203
2264
|
}
|
|
2204
2265
|
if (response.approved && !pending.suggestions?.length && response.allowTools?.length) {
|
|
2205
2266
|
response.allowTools.forEach((tool) => {
|
|
@@ -2540,6 +2601,17 @@ class PermissionHandler {
|
|
|
2540
2601
|
}
|
|
2541
2602
|
}
|
|
2542
2603
|
|
|
2604
|
+
function rewriteQueuedPermissionMode(queue, hasher, permissionMode) {
|
|
2605
|
+
let rewritten = 0;
|
|
2606
|
+
for (const item of queue) {
|
|
2607
|
+
if (item.mode.permissionMode === permissionMode) continue;
|
|
2608
|
+
item.mode = { ...item.mode, permissionMode };
|
|
2609
|
+
item.modeHash = hasher(item.mode);
|
|
2610
|
+
rewritten += 1;
|
|
2611
|
+
}
|
|
2612
|
+
return rewritten;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2543
2615
|
function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
|
|
2544
2616
|
logger.debugLargeJson("[CLAUDE INK] Message from remote mode:", message);
|
|
2545
2617
|
switch (message.type) {
|
|
@@ -2794,7 +2866,13 @@ class SDKToLogConverter {
|
|
|
2794
2866
|
type: "user",
|
|
2795
2867
|
message: userMsg.message,
|
|
2796
2868
|
...userMsg.parent_tool_use_id ? { parent_tool_use_id: userMsg.parent_tool_use_id } : {},
|
|
2797
|
-
...meta ? { isMeta: true } : {}
|
|
2869
|
+
...meta ? { isMeta: true } : {},
|
|
2870
|
+
// B-260-P2: structured tool output (Agent/Task final report +
|
|
2871
|
+
// run totals, or the `async_launched` stub marker) and the
|
|
2872
|
+
// message origin (task-notification). Both are consumed by
|
|
2873
|
+
// sessionProtocolMapper; the raw-line schema is passthrough.
|
|
2874
|
+
...userMsg.tool_use_result !== void 0 ? { tool_use_result: userMsg.tool_use_result } : {},
|
|
2875
|
+
...userMsg.origin ? { origin: userMsg.origin } : {}
|
|
2798
2876
|
};
|
|
2799
2877
|
if (Array.isArray(userMsg.message.content)) {
|
|
2800
2878
|
for (const content of userMsg.message.content) {
|
|
@@ -3305,7 +3383,7 @@ function createTurnSteeringController() {
|
|
|
3305
3383
|
};
|
|
3306
3384
|
}
|
|
3307
3385
|
|
|
3308
|
-
async function claudeRemoteLauncher(session, onPermissionModeChange) {
|
|
3386
|
+
async function claudeRemoteLauncher(session, onPermissionModeChange, onEffectivePermissionMode) {
|
|
3309
3387
|
logger.debug("[claudeRemoteLauncher] Starting remote launcher");
|
|
3310
3388
|
const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
3311
3389
|
logger.debug(`[claudeRemoteLauncher] TTY available: ${hasTTY}`);
|
|
@@ -3539,6 +3617,10 @@ async function claudeRemoteLauncher(session, onPermissionModeChange) {
|
|
|
3539
3617
|
mode = { ...mode, permissionMode: nextMode };
|
|
3540
3618
|
modeHash = session.queue.modeHasher(mode);
|
|
3541
3619
|
}
|
|
3620
|
+
rewriteQueuedPermissionMode(session.queue.queue, session.queue.modeHasher, nextMode);
|
|
3621
|
+
if (pending) {
|
|
3622
|
+
pending = { ...pending, mode: { ...pending.mode, permissionMode: nextMode } };
|
|
3623
|
+
}
|
|
3542
3624
|
onPermissionModeChange?.(nextMode);
|
|
3543
3625
|
return nextMode;
|
|
3544
3626
|
};
|
|
@@ -3611,6 +3693,7 @@ async function claudeRemoteLauncher(session, onPermissionModeChange) {
|
|
|
3611
3693
|
skillCount: metadata.skills?.length ?? 0
|
|
3612
3694
|
});
|
|
3613
3695
|
session.client.updateMetadata((currentMetadata) => applyClaudeSdkMetadata(currentMetadata, metadata));
|
|
3696
|
+
if (metadata.permissionMode) onEffectivePermissionMode?.(metadata.permissionMode);
|
|
3614
3697
|
},
|
|
3615
3698
|
onQueryReady: (q) => {
|
|
3616
3699
|
turnSteering.setInterrupt(q.interrupt);
|
|
@@ -3788,7 +3871,7 @@ async function loop(opts) {
|
|
|
3788
3871
|
break;
|
|
3789
3872
|
}
|
|
3790
3873
|
case "remote": {
|
|
3791
|
-
const reason = await claudeRemoteLauncher(session, opts.onPermissionModeChange);
|
|
3874
|
+
const reason = await claudeRemoteLauncher(session, opts.onPermissionModeChange, opts.onEffectivePermissionMode);
|
|
3792
3875
|
switch (reason) {
|
|
3793
3876
|
case "exit":
|
|
3794
3877
|
return 0;
|
|
@@ -5263,6 +5346,45 @@ function sanitizeSpawnPermissionMode(value) {
|
|
|
5263
5346
|
return ALLOWED_SPAWN_PERMISSION_MODES.includes(value) ? value : null;
|
|
5264
5347
|
}
|
|
5265
5348
|
|
|
5349
|
+
function resumePrecheck(metadata, fs) {
|
|
5350
|
+
const flavor = metadata.flavor ?? "claude";
|
|
5351
|
+
if (flavor !== "claude" && flavor !== "codex") {
|
|
5352
|
+
return { ok: false, reason: "unsupported-flavor", detail: `flavor "${flavor}" cannot be resumed` };
|
|
5353
|
+
}
|
|
5354
|
+
if (!metadata.path || !fs.cwdExists(metadata.path)) {
|
|
5355
|
+
return { ok: false, reason: "missing-cwd", detail: `working directory ${metadata.path ?? "(none)"} does not exist` };
|
|
5356
|
+
}
|
|
5357
|
+
if (flavor === "codex") {
|
|
5358
|
+
if (!metadata.codexThreadId) return { ok: false, reason: "no-backend-id", detail: "session has no Codex thread id" };
|
|
5359
|
+
return { ok: true };
|
|
5360
|
+
}
|
|
5361
|
+
if (!isClaudeSessionId(metadata.claudeSessionId)) {
|
|
5362
|
+
return { ok: false, reason: "no-backend-id", detail: "session has no Claude session id" };
|
|
5363
|
+
}
|
|
5364
|
+
if (!fs.conversationExists(metadata.path, metadata.claudeSessionId)) {
|
|
5365
|
+
return { ok: false, reason: "conversation-missing", detail: `conversation ${metadata.claudeSessionId} is not on disk` };
|
|
5366
|
+
}
|
|
5367
|
+
return { ok: true };
|
|
5368
|
+
}
|
|
5369
|
+
function sanitizeResumeModel(value) {
|
|
5370
|
+
if (typeof value !== "string") return null;
|
|
5371
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(value) ? value : null;
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5374
|
+
const DEFAULT_MAX_RESTARTS = 3;
|
|
5375
|
+
function decideRestart(priorAttempts, max = DEFAULT_MAX_RESTARTS) {
|
|
5376
|
+
if (priorAttempts >= max) {
|
|
5377
|
+
return {
|
|
5378
|
+
allowed: false,
|
|
5379
|
+
reason: `restart-limit: this session has already been restarted ${priorAttempts} time(s) this daemon session (max ${max}). Start a new session instead.`
|
|
5380
|
+
};
|
|
5381
|
+
}
|
|
5382
|
+
return { allowed: true, attempt: priorAttempts + 1 };
|
|
5383
|
+
}
|
|
5384
|
+
function recordRestartAttempt(priorAttempts) {
|
|
5385
|
+
return priorAttempts + 1;
|
|
5386
|
+
}
|
|
5387
|
+
|
|
5266
5388
|
const CONTROL_TOKEN_BYTES = 32;
|
|
5267
5389
|
function createDaemonControlToken() {
|
|
5268
5390
|
return randomBytes(CONTROL_TOKEN_BYTES).toString("base64url");
|
|
@@ -7080,6 +7202,7 @@ async function startDaemon() {
|
|
|
7080
7202
|
tracked.pid = livePid;
|
|
7081
7203
|
tracked.startedBy = "recovered after daemon restart";
|
|
7082
7204
|
pidToTrackedSession.set(livePid, tracked);
|
|
7205
|
+
persistSession(id, { ...s, savedAt: Date.now() });
|
|
7083
7206
|
} else {
|
|
7084
7207
|
sessionIdToFinishedSession.set(id, tracked);
|
|
7085
7208
|
}
|
|
@@ -7508,6 +7631,13 @@ async function startDaemon() {
|
|
|
7508
7631
|
const timeout = setTimeout(() => {
|
|
7509
7632
|
pidToAwaiter.delete(happyProcess.pid);
|
|
7510
7633
|
logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
7634
|
+
if (happyProcess.pid) {
|
|
7635
|
+
try {
|
|
7636
|
+
terminateProcess(happyProcess.pid, () => {
|
|
7637
|
+
});
|
|
7638
|
+
} catch {
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7511
7641
|
resolve({
|
|
7512
7642
|
type: "error",
|
|
7513
7643
|
errorMessage: `Session webhook timeout for PID ${happyProcess.pid}`
|
|
@@ -7530,6 +7660,19 @@ async function startDaemon() {
|
|
|
7530
7660
|
return sessionIdToFinishedSession.get(happySessionId);
|
|
7531
7661
|
};
|
|
7532
7662
|
const fetchServerSessionMetadata = async (sessionId, encryptionKey, encryptionVariant) => {
|
|
7663
|
+
try {
|
|
7664
|
+
const byId = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
|
|
7665
|
+
headers: { Authorization: `Bearer ${credentials.token}` },
|
|
7666
|
+
timeout: 1e4,
|
|
7667
|
+
validateStatus: (status) => status >= 200 && status < 300 || status === 404
|
|
7668
|
+
});
|
|
7669
|
+
if (byId.status !== 404) {
|
|
7670
|
+
const row = byId.data.session;
|
|
7671
|
+
if (row?.metadata) return decrypt(encryptionKey, encryptionVariant, decodeBase64(row.metadata));
|
|
7672
|
+
}
|
|
7673
|
+
} catch (error) {
|
|
7674
|
+
logger.debug(`[DAEMON RUN] by-id session metadata fetch failed, falling back to list: ${error instanceof Error ? error.message : error}`);
|
|
7675
|
+
}
|
|
7533
7676
|
try {
|
|
7534
7677
|
const response = await axios.get(`${configuration.serverUrl}/v1/sessions`, {
|
|
7535
7678
|
headers: { Authorization: `Bearer ${credentials.token}` },
|
|
@@ -7545,17 +7688,38 @@ async function startDaemon() {
|
|
|
7545
7688
|
return null;
|
|
7546
7689
|
}
|
|
7547
7690
|
};
|
|
7691
|
+
const resumeGates = /* @__PURE__ */ new Map();
|
|
7548
7692
|
const resumeSession = async (happySessionId, options) => {
|
|
7693
|
+
let gate = resumeGates.get(happySessionId);
|
|
7694
|
+
if (!gate) {
|
|
7695
|
+
gate = createSpawnGate();
|
|
7696
|
+
resumeGates.set(happySessionId, gate);
|
|
7697
|
+
}
|
|
7698
|
+
try {
|
|
7699
|
+
return await gate.join(() => resumeSessionImpl(happySessionId, options));
|
|
7700
|
+
} finally {
|
|
7701
|
+
if (!gate.inFlight()) resumeGates.delete(happySessionId);
|
|
7702
|
+
}
|
|
7703
|
+
};
|
|
7704
|
+
const resumeSessionImpl = async (happySessionId, options) => {
|
|
7549
7705
|
try {
|
|
7706
|
+
for (const [pid, live] of pidToTrackedSession.entries()) {
|
|
7707
|
+
if (live.happySessionId !== happySessionId) continue;
|
|
7708
|
+
if (isPidAlive(pid)) {
|
|
7709
|
+
logger.debug(`[DAEMON RUN] resume ${happySessionId}: process ${pid} already alive \u2014 idempotent success`);
|
|
7710
|
+
return { type: "success", sessionId: happySessionId };
|
|
7711
|
+
}
|
|
7712
|
+
onChildExited(pid);
|
|
7713
|
+
}
|
|
7550
7714
|
const tracked = findTrackedSessionById(happySessionId);
|
|
7551
7715
|
if (!tracked) {
|
|
7552
|
-
return { type: "error", errorMessage: `Session ${happySessionId} is not tracked by this daemon. It may have been started before the daemon or on another machine.` };
|
|
7716
|
+
return { type: "error", errorMessage: `resume-precheck:not-tracked: Session ${happySessionId} is not tracked by this daemon. It may have been started before the daemon, more than 14 days ago, or on another machine.` };
|
|
7553
7717
|
}
|
|
7554
7718
|
if (!tracked.happySessionMetadataFromLocalWebhook) {
|
|
7555
|
-
return { type: "error", errorMessage: `Session ${happySessionId} has no metadata. Cannot resume.` };
|
|
7719
|
+
return { type: "error", errorMessage: `resume-precheck:no-metadata: Session ${happySessionId} has no metadata. Cannot resume.` };
|
|
7556
7720
|
}
|
|
7557
7721
|
if (!tracked.encryption) {
|
|
7558
|
-
return { type: "error", errorMessage: `Session ${happySessionId} has no stored encryption data. It was likely started before this feature was available. Restart the daemon and start a new session to enable resume.` };
|
|
7722
|
+
return { type: "error", errorMessage: `resume-precheck:no-encryption: Session ${happySessionId} has no stored encryption data. It was likely started before this feature was available. Restart the daemon and start a new session to enable resume.` };
|
|
7559
7723
|
}
|
|
7560
7724
|
let metadata = tracked.happySessionMetadataFromLocalWebhook;
|
|
7561
7725
|
const needsFetch = !metadata.claudeSessionId && (!metadata.flavor || metadata.flavor === "claude") || !metadata.codexThreadId && metadata.flavor === "codex";
|
|
@@ -7567,18 +7731,24 @@ async function startDaemon() {
|
|
|
7567
7731
|
tracked.happySessionMetadataFromLocalWebhook = serverMetadata;
|
|
7568
7732
|
}
|
|
7569
7733
|
}
|
|
7734
|
+
const precheck = resumePrecheck(metadata, {
|
|
7735
|
+
cwdExists: (p) => existsSync$1(p),
|
|
7736
|
+
conversationExists: (cwd, claudeSessionId) => existsSync$1(join$1(getProjectPath(cwd), `${claudeSessionId}.jsonl`))
|
|
7737
|
+
});
|
|
7738
|
+
if (!precheck.ok) {
|
|
7739
|
+
return { type: "error", errorMessage: `resume-precheck:${precheck.reason}: ${precheck.detail}` };
|
|
7740
|
+
}
|
|
7570
7741
|
const launch = buildResumeLaunch(
|
|
7571
7742
|
{ id: happySessionId, active: true, metadata },
|
|
7572
7743
|
{ startedBy: "daemon", claudeStartingMode: "remote" }
|
|
7573
7744
|
);
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
}
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
}
|
|
7580
|
-
await
|
|
7581
|
-
return spawnTrackedHappyProcess({
|
|
7745
|
+
const model = sanitizeResumeModel(options?.model);
|
|
7746
|
+
if (model) launch.args.push("--model", model);
|
|
7747
|
+
else if (options?.model !== void 0) logger.debug(`[DAEMON RUN] resume: ignoring invalid model ${JSON.stringify(options.model)}`);
|
|
7748
|
+
const permissionMode = sanitizeSpawnPermissionMode(options?.permissionMode);
|
|
7749
|
+
if (permissionMode) launch.args.push("--permission-mode", permissionMode);
|
|
7750
|
+
else if (options?.permissionMode !== void 0) logger.debug(`[DAEMON RUN] resume: ignoring invalid permission mode ${JSON.stringify(options.permissionMode)}`);
|
|
7751
|
+
const result = await spawnTrackedHappyProcess({
|
|
7582
7752
|
args: launch.args,
|
|
7583
7753
|
cwd: launch.cwd,
|
|
7584
7754
|
env: {
|
|
@@ -7591,6 +7761,18 @@ async function startDaemon() {
|
|
|
7591
7761
|
HAPPY_RECONNECT_AGENT_STATE_VERSION: String(tracked.encryption.agentStateVersion)
|
|
7592
7762
|
}
|
|
7593
7763
|
});
|
|
7764
|
+
if (result.type === "success") {
|
|
7765
|
+
persistSession(happySessionId, {
|
|
7766
|
+
encryptionKey: encodeBase64(tracked.encryption.encryptionKey),
|
|
7767
|
+
encryptionVariant: tracked.encryption.encryptionVariant,
|
|
7768
|
+
seq: tracked.encryption.seq,
|
|
7769
|
+
metadataVersion: tracked.encryption.metadataVersion,
|
|
7770
|
+
agentStateVersion: tracked.encryption.agentStateVersion,
|
|
7771
|
+
metadata,
|
|
7772
|
+
savedAt: Date.now()
|
|
7773
|
+
});
|
|
7774
|
+
}
|
|
7775
|
+
return result;
|
|
7594
7776
|
} catch (error) {
|
|
7595
7777
|
const errorMessage = error instanceof Error ? error.message : error && typeof error === "object" ? JSON.stringify(error) : String(error);
|
|
7596
7778
|
logger.debug(`[DAEMON RUN] Failed to resume session: ${errorMessage}`, error instanceof Error ? error.stack : void 0);
|
|
@@ -7600,6 +7782,136 @@ async function startDaemon() {
|
|
|
7600
7782
|
};
|
|
7601
7783
|
}
|
|
7602
7784
|
};
|
|
7785
|
+
const restartCounts = /* @__PURE__ */ new Map();
|
|
7786
|
+
const stopSessionAndWait = (happySessionId, timeoutMs = 8e3) => {
|
|
7787
|
+
let targetPid = null;
|
|
7788
|
+
for (const [pid2, s] of pidToTrackedSession.entries()) {
|
|
7789
|
+
if (s.happySessionId === happySessionId && isPidAlive(pid2)) {
|
|
7790
|
+
targetPid = pid2;
|
|
7791
|
+
break;
|
|
7792
|
+
}
|
|
7793
|
+
}
|
|
7794
|
+
if (targetPid === null) return Promise.resolve();
|
|
7795
|
+
const pid = targetPid;
|
|
7796
|
+
return new Promise((resolve) => {
|
|
7797
|
+
let settled = false;
|
|
7798
|
+
const done = () => {
|
|
7799
|
+
if (!settled) {
|
|
7800
|
+
settled = true;
|
|
7801
|
+
resolve();
|
|
7802
|
+
}
|
|
7803
|
+
};
|
|
7804
|
+
const requested = terminateProcess(pid, () => {
|
|
7805
|
+
const tracked = pidToTrackedSession.get(pid);
|
|
7806
|
+
if (tracked) {
|
|
7807
|
+
if (tracked.happySessionId && tracked.encryption) {
|
|
7808
|
+
sessionIdToFinishedSession.set(tracked.happySessionId, tracked);
|
|
7809
|
+
}
|
|
7810
|
+
pidToTrackedSession.delete(pid);
|
|
7811
|
+
}
|
|
7812
|
+
done();
|
|
7813
|
+
});
|
|
7814
|
+
if (!requested) done();
|
|
7815
|
+
setTimeout(done, timeoutMs).unref?.();
|
|
7816
|
+
});
|
|
7817
|
+
};
|
|
7818
|
+
const restartSession = async (happySessionId, options) => {
|
|
7819
|
+
let gate = resumeGates.get(happySessionId);
|
|
7820
|
+
if (!gate) {
|
|
7821
|
+
gate = createSpawnGate();
|
|
7822
|
+
resumeGates.set(happySessionId, gate);
|
|
7823
|
+
}
|
|
7824
|
+
try {
|
|
7825
|
+
return await gate.replace(() => restartSessionImpl(happySessionId, options));
|
|
7826
|
+
} finally {
|
|
7827
|
+
if (!gate.inFlight()) resumeGates.delete(happySessionId);
|
|
7828
|
+
}
|
|
7829
|
+
};
|
|
7830
|
+
const restartSessionImpl = async (happySessionId, options) => {
|
|
7831
|
+
try {
|
|
7832
|
+
const prior = restartCounts.get(happySessionId) ?? 0;
|
|
7833
|
+
const decision = decideRestart(prior, DEFAULT_MAX_RESTARTS);
|
|
7834
|
+
if (!decision.allowed) {
|
|
7835
|
+
return { type: "error", errorMessage: `restart:${decision.reason}` };
|
|
7836
|
+
}
|
|
7837
|
+
await stopSessionAndWait(happySessionId);
|
|
7838
|
+
const tracked = findTrackedSessionById(happySessionId);
|
|
7839
|
+
if (!tracked) {
|
|
7840
|
+
return { type: "error", errorMessage: `restart-precheck:not-tracked: Session ${happySessionId} is not tracked by this daemon.` };
|
|
7841
|
+
}
|
|
7842
|
+
if (!tracked.happySessionMetadataFromLocalWebhook) {
|
|
7843
|
+
return { type: "error", errorMessage: `restart-precheck:no-metadata: Session ${happySessionId} has no metadata. Cannot restart.` };
|
|
7844
|
+
}
|
|
7845
|
+
if (!tracked.encryption) {
|
|
7846
|
+
return { type: "error", errorMessage: `restart-precheck:no-encryption: Session ${happySessionId} has no stored encryption data. Restart the daemon and start a new session.` };
|
|
7847
|
+
}
|
|
7848
|
+
let metadata = tracked.happySessionMetadataFromLocalWebhook;
|
|
7849
|
+
const needsFetch = !metadata.claudeSessionId && (!metadata.flavor || metadata.flavor === "claude") || !metadata.codexThreadId && metadata.flavor === "codex";
|
|
7850
|
+
if (needsFetch) {
|
|
7851
|
+
const serverMetadata = await fetchServerSessionMetadata(happySessionId, tracked.encryption.encryptionKey, tracked.encryption.encryptionVariant);
|
|
7852
|
+
if (serverMetadata) {
|
|
7853
|
+
metadata = serverMetadata;
|
|
7854
|
+
tracked.happySessionMetadataFromLocalWebhook = serverMetadata;
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
const precheck = resumePrecheck(metadata, {
|
|
7858
|
+
cwdExists: (p) => existsSync$1(p),
|
|
7859
|
+
conversationExists: (cwd2, claudeSessionId) => existsSync$1(join$1(getProjectPath(cwd2), `${claudeSessionId}.jsonl`))
|
|
7860
|
+
});
|
|
7861
|
+
let args;
|
|
7862
|
+
let cwd;
|
|
7863
|
+
if (precheck.ok) {
|
|
7864
|
+
const launch = buildResumeLaunch(
|
|
7865
|
+
{ id: happySessionId, active: true, metadata },
|
|
7866
|
+
{ startedBy: "daemon", claudeStartingMode: "remote" }
|
|
7867
|
+
);
|
|
7868
|
+
args = launch.args;
|
|
7869
|
+
cwd = launch.cwd;
|
|
7870
|
+
} else if ((metadata.flavor ?? "claude") === "claude" && typeof metadata.path === "string" && metadata.path.length > 0) {
|
|
7871
|
+
cwd = metadata.path;
|
|
7872
|
+
if (!existsSync$1(cwd)) {
|
|
7873
|
+
return { type: "error", errorMessage: `restart-precheck:cwd-missing: ${cwd}` };
|
|
7874
|
+
}
|
|
7875
|
+
args = ["claude", "--happy-starting-mode", "remote", "--started-by", "daemon"];
|
|
7876
|
+
} else {
|
|
7877
|
+
return { type: "error", errorMessage: `restart-precheck:${precheck.ok ? "unknown" : precheck.reason}: ${precheck.ok ? "" : precheck.detail}` };
|
|
7878
|
+
}
|
|
7879
|
+
const model = sanitizeResumeModel(options?.model);
|
|
7880
|
+
if (model) args.push("--model", model);
|
|
7881
|
+
const permissionMode = sanitizeSpawnPermissionMode(options?.permissionMode);
|
|
7882
|
+
if (permissionMode) args.push("--permission-mode", permissionMode);
|
|
7883
|
+
restartCounts.set(happySessionId, recordRestartAttempt(prior));
|
|
7884
|
+
const result = await spawnTrackedHappyProcess({
|
|
7885
|
+
args,
|
|
7886
|
+
cwd,
|
|
7887
|
+
env: {
|
|
7888
|
+
...process.env,
|
|
7889
|
+
HAPPY_RECONNECT_SESSION_ID: happySessionId,
|
|
7890
|
+
HAPPY_RECONNECT_ENCRYPTION_KEY: encodeBase64(tracked.encryption.encryptionKey),
|
|
7891
|
+
HAPPY_RECONNECT_ENCRYPTION_VARIANT: tracked.encryption.encryptionVariant,
|
|
7892
|
+
HAPPY_RECONNECT_SEQ: String(tracked.encryption.seq),
|
|
7893
|
+
HAPPY_RECONNECT_METADATA_VERSION: String(tracked.encryption.metadataVersion),
|
|
7894
|
+
HAPPY_RECONNECT_AGENT_STATE_VERSION: String(tracked.encryption.agentStateVersion)
|
|
7895
|
+
}
|
|
7896
|
+
});
|
|
7897
|
+
if (result.type === "success") {
|
|
7898
|
+
persistSession(happySessionId, {
|
|
7899
|
+
encryptionKey: encodeBase64(tracked.encryption.encryptionKey),
|
|
7900
|
+
encryptionVariant: tracked.encryption.encryptionVariant,
|
|
7901
|
+
seq: tracked.encryption.seq,
|
|
7902
|
+
metadataVersion: tracked.encryption.metadataVersion,
|
|
7903
|
+
agentStateVersion: tracked.encryption.agentStateVersion,
|
|
7904
|
+
metadata,
|
|
7905
|
+
savedAt: Date.now()
|
|
7906
|
+
});
|
|
7907
|
+
}
|
|
7908
|
+
return result;
|
|
7909
|
+
} catch (error) {
|
|
7910
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7911
|
+
logger.debug(`[DAEMON RUN] Failed to restart session: ${errorMessage}`, error instanceof Error ? error.stack : void 0);
|
|
7912
|
+
return { type: "error", errorMessage: `Failed to restart session: ${errorMessage}` };
|
|
7913
|
+
}
|
|
7914
|
+
};
|
|
7603
7915
|
const stopSession = (sessionId) => {
|
|
7604
7916
|
logger.debug(`[DAEMON RUN] Attempting to stop session ${sessionId}`);
|
|
7605
7917
|
for (const [pid, session] of pidToTrackedSession.entries()) {
|
|
@@ -7770,6 +8082,7 @@ async function startDaemon() {
|
|
|
7770
8082
|
apiMachine.setRPCHandlers({
|
|
7771
8083
|
spawnSession,
|
|
7772
8084
|
resumeSession,
|
|
8085
|
+
restartSession,
|
|
7773
8086
|
stopSession,
|
|
7774
8087
|
listTrackedSessionIds: () => [...pidToTrackedSession.values()].map((session) => session.happySessionId).filter((sessionId) => typeof sessionId === "string"),
|
|
7775
8088
|
requestShutdown: () => requestShutdown("happy-app")
|
|
@@ -9361,6 +9674,18 @@ async function runClaude(credentials, options = {}) {
|
|
|
9361
9674
|
if (reconnectSessionId && !await api.reactivateSession(response.id)) {
|
|
9362
9675
|
throw new Error(`Failed to reactivate archived session ${response.id}`);
|
|
9363
9676
|
}
|
|
9677
|
+
let reconnectSeeded = false;
|
|
9678
|
+
if (reconnectSessionId) {
|
|
9679
|
+
const snapshot = await api.getSession(response.id, response.encryptionKey, response.encryptionVariant);
|
|
9680
|
+
if (snapshot.ok) {
|
|
9681
|
+
response = applyServerSnapshot(response, snapshot.session);
|
|
9682
|
+
reconnectSeeded = true;
|
|
9683
|
+
logger.debug(`[START] Reconnect seeded from server: seq=${snapshot.session.seq} metadataVersion=${snapshot.session.metadataVersion}`);
|
|
9684
|
+
} else {
|
|
9685
|
+
response = withoutServerSnapshot(response);
|
|
9686
|
+
logger.debug(`[START] Reconnect without server snapshot (${snapshot.reason}); skipping existing messages`);
|
|
9687
|
+
}
|
|
9688
|
+
}
|
|
9364
9689
|
try {
|
|
9365
9690
|
logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
|
9366
9691
|
const result = await notifyDaemonSessionStarted(response.id, metadata, {
|
|
@@ -9378,15 +9703,17 @@ async function runClaude(credentials, options = {}) {
|
|
|
9378
9703
|
} catch (error) {
|
|
9379
9704
|
logger.debug("[START] Failed to report to daemon (may not be running):", error);
|
|
9380
9705
|
}
|
|
9381
|
-
const session = api.sessionSyncClient(response);
|
|
9706
|
+
const session = api.sessionSyncClient(response, reconnectSeeded ? { initialSeq: response.seq } : void 0);
|
|
9382
9707
|
if (reconnectSessionId) {
|
|
9383
9708
|
session.suppressNextArchiveSignal();
|
|
9384
|
-
session.skipExistingMessages();
|
|
9709
|
+
if (!reconnectSeeded) session.skipExistingMessages();
|
|
9385
9710
|
session.updateMetadata((meta) => ({
|
|
9386
|
-
...meta,
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9711
|
+
...mergeReconnectMetadata(meta, metadata, Date.now()),
|
|
9712
|
+
queueCancellation: true,
|
|
9713
|
+
// B-262 batch 2 (B5): a reconnected process re-publishes the mode
|
|
9714
|
+
// IT enforces; the server copy may still carry the previous
|
|
9715
|
+
// process's value.
|
|
9716
|
+
permissionMode: mapToClaudeMode(initialPermissionMode ?? "default")
|
|
9390
9717
|
}));
|
|
9391
9718
|
}
|
|
9392
9719
|
const forkClaudeSessionId = process.env.HAPPY_FORK_CLAUDE_SESSION_ID;
|
|
@@ -9526,6 +9853,22 @@ async function runClaude(credentials, options = {}) {
|
|
|
9526
9853
|
publishedPermissionMode = published;
|
|
9527
9854
|
session.updateMetadata((meta) => ({ ...meta, permissionMode: published }));
|
|
9528
9855
|
};
|
|
9856
|
+
const publishEffectivePermissionMode = (effective) => {
|
|
9857
|
+
const decision = reconcilePublishedPermissionMode({
|
|
9858
|
+
intent: mapToClaudeMode(currentPermissionMode ?? "default"),
|
|
9859
|
+
published: publishedPermissionMode,
|
|
9860
|
+
effective
|
|
9861
|
+
});
|
|
9862
|
+
if (!decision) return;
|
|
9863
|
+
if (decision.mismatch) {
|
|
9864
|
+
logger.warn(`[loop] Claude Code enforces permission mode '${effective}' but this process asked for '${mapToClaudeMode(currentPermissionMode ?? "default")}' \u2014 check ~/.claude settings (permissions.deny/ask/defaultMode/disableBypassPermissionsMode). Publishing the effective mode.`);
|
|
9865
|
+
}
|
|
9866
|
+
if (decision.publish !== publishedPermissionMode) {
|
|
9867
|
+
publishedPermissionMode = decision.publish;
|
|
9868
|
+
session.updateMetadata((meta) => ({ ...meta, permissionMode: decision.publish }));
|
|
9869
|
+
}
|
|
9870
|
+
};
|
|
9871
|
+
let lastExplicitModeSwitchAt = 0;
|
|
9529
9872
|
let currentModel = options.model ?? DEFAULT_CLAUDE_MODEL;
|
|
9530
9873
|
let currentFallbackModel = void 0;
|
|
9531
9874
|
let currentCustomSystemPrompt = void 0;
|
|
@@ -9570,7 +9913,11 @@ async function runClaude(credentials, options = {}) {
|
|
|
9570
9913
|
}
|
|
9571
9914
|
const attachmentsForThisMessage = await session.drainAttachmentsForUserMessage();
|
|
9572
9915
|
let messagePermissionMode = currentPermissionMode;
|
|
9573
|
-
|
|
9916
|
+
const messageCreatedAt = message.createdAt;
|
|
9917
|
+
const staleModeSnapshot = message.meta?.permissionMode !== void 0 && typeof messageCreatedAt === "number" && messageCreatedAt < lastExplicitModeSwitchAt;
|
|
9918
|
+
if (staleModeSnapshot) {
|
|
9919
|
+
logger.debug(`[loop] Ignoring stale meta.permissionMode=${message.meta?.permissionMode} (message ${messageCreatedAt} predates explicit switch ${lastExplicitModeSwitchAt}); keeping ${currentPermissionMode}`);
|
|
9920
|
+
} else if (message.meta?.permissionMode) {
|
|
9574
9921
|
const previousPermissionMode = currentPermissionMode;
|
|
9575
9922
|
messagePermissionMode = resolveRemoteClaudePermissionMode(
|
|
9576
9923
|
currentPermissionMode,
|
|
@@ -9834,9 +10181,11 @@ async function runClaude(credentials, options = {}) {
|
|
|
9834
10181
|
},
|
|
9835
10182
|
onAbort: resetCurrentModeDefaults,
|
|
9836
10183
|
onPermissionModeChange: (mode) => {
|
|
10184
|
+
lastExplicitModeSwitchAt = Date.now();
|
|
9837
10185
|
publishPermissionMode(mode);
|
|
9838
10186
|
logger.debug(`[loop] Permission mode updated from session RPC / approval to: ${mode}`);
|
|
9839
10187
|
},
|
|
10188
|
+
onEffectivePermissionMode: (mode) => publishEffectivePermissionMode(mode),
|
|
9840
10189
|
mcpServers: {
|
|
9841
10190
|
"happy": {
|
|
9842
10191
|
type: "http",
|
|
@@ -12070,7 +12419,7 @@ function setupOfflineReconnection(opts) {
|
|
|
12070
12419
|
});
|
|
12071
12420
|
return { session, reconnectionHandle, isOffline: true };
|
|
12072
12421
|
} else {
|
|
12073
|
-
session = api.sessionSyncClient(response);
|
|
12422
|
+
session = api.sessionSyncClient(response, opts.sessionClientOptions);
|
|
12074
12423
|
return { session, reconnectionHandle: null, isOffline: false };
|
|
12075
12424
|
}
|
|
12076
12425
|
}
|
|
@@ -12839,6 +13188,18 @@ async function runCodex(opts) {
|
|
|
12839
13188
|
if (reconnectSessionId && response && !await api.reactivateSession(response.id)) {
|
|
12840
13189
|
throw new Error(`Failed to reactivate archived session ${response.id}`);
|
|
12841
13190
|
}
|
|
13191
|
+
let reconnectSeeded = false;
|
|
13192
|
+
if (reconnectSessionId && response) {
|
|
13193
|
+
const snapshot = await api.getSession(response.id, response.encryptionKey, response.encryptionVariant);
|
|
13194
|
+
if (snapshot.ok) {
|
|
13195
|
+
response = applyServerSnapshot(response, snapshot.session);
|
|
13196
|
+
reconnectSeeded = true;
|
|
13197
|
+
logger.debug(`[START] Reconnect seeded from server: seq=${snapshot.session.seq} metadataVersion=${snapshot.session.metadataVersion}`);
|
|
13198
|
+
} else {
|
|
13199
|
+
response = withoutServerSnapshot(response);
|
|
13200
|
+
logger.debug(`[START] Reconnect without server snapshot (${snapshot.reason}); skipping existing messages`);
|
|
13201
|
+
}
|
|
13202
|
+
}
|
|
12842
13203
|
let session;
|
|
12843
13204
|
let permissionHandler;
|
|
12844
13205
|
let client;
|
|
@@ -12850,6 +13211,7 @@ async function runCodex(opts) {
|
|
|
12850
13211
|
metadata,
|
|
12851
13212
|
state,
|
|
12852
13213
|
response,
|
|
13214
|
+
sessionClientOptions: reconnectSeeded && response ? { initialSeq: response.seq } : void 0,
|
|
12853
13215
|
onSessionSwap: (newSession) => {
|
|
12854
13216
|
session = newSession;
|
|
12855
13217
|
if (permissionHandler) {
|
|
@@ -12860,12 +13222,8 @@ async function runCodex(opts) {
|
|
|
12860
13222
|
session = initialSession;
|
|
12861
13223
|
if (reconnectSessionId) {
|
|
12862
13224
|
session.suppressNextArchiveSignal();
|
|
12863
|
-
session.skipExistingMessages();
|
|
12864
|
-
session.updateMetadata((meta) => (
|
|
12865
|
-
...meta,
|
|
12866
|
-
lifecycleState: "running",
|
|
12867
|
-
archivedBy: void 0
|
|
12868
|
-
}));
|
|
13225
|
+
if (!reconnectSeeded) session.skipExistingMessages();
|
|
13226
|
+
session.updateMetadata((meta) => mergeReconnectMetadata(meta, metadata, Date.now()));
|
|
12869
13227
|
}
|
|
12870
13228
|
if (response) {
|
|
12871
13229
|
try {
|
|
@@ -13593,7 +13951,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13593
13951
|
process.exit(0);
|
|
13594
13952
|
} else if (subcommand === "install-terminal-hooks") {
|
|
13595
13953
|
try {
|
|
13596
|
-
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-
|
|
13954
|
+
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-DDRrUG_n.mjs');
|
|
13597
13955
|
const command = parseTerminalHooksArgs(args.slice(1));
|
|
13598
13956
|
if (command.action === "help") {
|
|
13599
13957
|
console.log(TERMINAL_HOOKS_HELP);
|
|
@@ -13610,7 +13968,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13610
13968
|
}
|
|
13611
13969
|
} else if (subcommand === "spawn") {
|
|
13612
13970
|
try {
|
|
13613
|
-
const { handleSpawnCommand } = await import('./spawn-
|
|
13971
|
+
const { handleSpawnCommand } = await import('./spawn-DS-Uz_Hw.mjs');
|
|
13614
13972
|
await handleSpawnCommand(args.slice(1));
|
|
13615
13973
|
} catch (error) {
|
|
13616
13974
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -13622,7 +13980,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13622
13980
|
return;
|
|
13623
13981
|
} else if (subcommand === "send") {
|
|
13624
13982
|
try {
|
|
13625
|
-
const { handleSendCommand } = await import('./send-
|
|
13983
|
+
const { handleSendCommand } = await import('./send-BQSaVz9r.mjs');
|
|
13626
13984
|
await handleSendCommand(args.slice(1));
|
|
13627
13985
|
} catch (error) {
|
|
13628
13986
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -13728,9 +14086,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13728
14086
|
if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
|
|
13729
14087
|
const projectId = args[3];
|
|
13730
14088
|
try {
|
|
13731
|
-
const { saveGoogleCloudProjectToConfig } = await import('./config-
|
|
13732
|
-
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-
|
|
13733
|
-
const { ApiClient: ApiClient2 } = await import('./types-
|
|
14089
|
+
const { saveGoogleCloudProjectToConfig } = await import('./config-CJQw0z1o.mjs');
|
|
14090
|
+
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-B_8X_rZk.mjs').then(function (n) { return n.al; });
|
|
14091
|
+
const { ApiClient: ApiClient2 } = await import('./types-B_8X_rZk.mjs').then(function (n) { return n.am; });
|
|
13734
14092
|
let userEmail = void 0;
|
|
13735
14093
|
try {
|
|
13736
14094
|
const credentials = await readCredentialsForConfiguredRelay2();
|
|
@@ -13761,7 +14119,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13761
14119
|
}
|
|
13762
14120
|
if (geminiSubcommand === "project" && args[2] === "get") {
|
|
13763
14121
|
try {
|
|
13764
|
-
const { readGeminiLocalConfig } = await import('./config-
|
|
14122
|
+
const { readGeminiLocalConfig } = await import('./config-CJQw0z1o.mjs');
|
|
13765
14123
|
const config = readGeminiLocalConfig();
|
|
13766
14124
|
if (config.googleCloudProject) {
|
|
13767
14125
|
console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
|
|
@@ -13801,7 +14159,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13801
14159
|
process.exit(0);
|
|
13802
14160
|
}
|
|
13803
14161
|
try {
|
|
13804
|
-
const { runGemini } = await import('./runGemini-
|
|
14162
|
+
const { runGemini } = await import('./runGemini-DHYs_Beq.mjs');
|
|
13805
14163
|
let startedBy = void 0;
|
|
13806
14164
|
for (let i = 1; i < args.length; i++) {
|
|
13807
14165
|
if (args[i] === "--started-by") {
|
|
@@ -13823,7 +14181,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13823
14181
|
return;
|
|
13824
14182
|
} else if (subcommand === "acp") {
|
|
13825
14183
|
try {
|
|
13826
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
14184
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-BP9pfzdZ.mjs');
|
|
13827
14185
|
let startedBy = void 0;
|
|
13828
14186
|
let verbose = false;
|
|
13829
14187
|
const acpArgs = [];
|
|
@@ -13863,7 +14221,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13863
14221
|
return;
|
|
13864
14222
|
} else if (subcommand === "openclaw") {
|
|
13865
14223
|
try {
|
|
13866
|
-
const { runOpenClaw } = await import('./runOpenClaw-
|
|
14224
|
+
const { runOpenClaw } = await import('./runOpenClaw-DTaXxvNd.mjs');
|
|
13867
14225
|
let startedBy = void 0;
|
|
13868
14226
|
let verbose = false;
|
|
13869
14227
|
let gatewayUrl;
|
|
@@ -13914,7 +14272,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13914
14272
|
return;
|
|
13915
14273
|
} else if (subcommand === "mcp" && args.length === 1) {
|
|
13916
14274
|
try {
|
|
13917
|
-
const { handleMcpCommand } = await import('./mcp-
|
|
14275
|
+
const { handleMcpCommand } = await import('./mcp-CYJAl5pk.mjs');
|
|
13918
14276
|
await handleMcpCommand();
|
|
13919
14277
|
} catch (error) {
|
|
13920
14278
|
process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
|