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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var node_child_process = require('node:child_process');
|
|
4
4
|
var sdk = require('@agentclientprotocol/sdk');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
|
-
var persistence = require('./types-
|
|
6
|
+
var persistence = require('./types-CjlB_fKT.cjs');
|
|
7
7
|
|
|
8
8
|
const DEFAULT_TIMEOUTS = {
|
|
9
9
|
/** Default initialization timeout: 60 seconds */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { l as logger, d as contentLogMetadata, e as errorLogMetadata, f as packageJson, g as delay } from './types-
|
|
4
|
+
import { l as logger, d as contentLogMetadata, e as errorLogMetadata, f as packageJson, g as delay } from './types-B_8X_rZk.mjs';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_TIMEOUTS = {
|
|
7
7
|
/** Default initialization timeout: 60 seconds */
|
|
@@ -4,8 +4,8 @@ var fs = require('fs');
|
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var os = require('os');
|
|
6
6
|
var child_process = require('child_process');
|
|
7
|
-
var persistence = require('./types-
|
|
8
|
-
var index = require('./index-
|
|
7
|
+
var persistence = require('./types-CjlB_fKT.cjs');
|
|
8
|
+
var index = require('./index-B63Vjt4W.cjs');
|
|
9
9
|
require('axios');
|
|
10
10
|
require('chalk');
|
|
11
11
|
require('node:util');
|
|
@@ -2,8 +2,8 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
-
import { l as logger } from './types-
|
|
6
|
-
import { D as DEFAULT_GEMINI_MODEL, G as GEMINI_MODEL_ENV } from './index-
|
|
5
|
+
import { l as logger } from './types-B_8X_rZk.mjs';
|
|
6
|
+
import { D as DEFAULT_GEMINI_MODEL, G as GEMINI_MODEL_ENV } from './index-8LDXVYWi.mjs';
|
|
7
7
|
import 'axios';
|
|
8
8
|
import 'chalk';
|
|
9
9
|
import 'node:util';
|
|
@@ -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;
|
|
@@ -5293,6 +5346,45 @@ function sanitizeSpawnPermissionMode(value) {
|
|
|
5293
5346
|
return ALLOWED_SPAWN_PERMISSION_MODES.includes(value) ? value : null;
|
|
5294
5347
|
}
|
|
5295
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
|
+
|
|
5296
5388
|
const CONTROL_TOKEN_BYTES = 32;
|
|
5297
5389
|
function createDaemonControlToken() {
|
|
5298
5390
|
return randomBytes(CONTROL_TOKEN_BYTES).toString("base64url");
|
|
@@ -7110,6 +7202,7 @@ async function startDaemon() {
|
|
|
7110
7202
|
tracked.pid = livePid;
|
|
7111
7203
|
tracked.startedBy = "recovered after daemon restart";
|
|
7112
7204
|
pidToTrackedSession.set(livePid, tracked);
|
|
7205
|
+
persistSession(id, { ...s, savedAt: Date.now() });
|
|
7113
7206
|
} else {
|
|
7114
7207
|
sessionIdToFinishedSession.set(id, tracked);
|
|
7115
7208
|
}
|
|
@@ -7538,6 +7631,13 @@ async function startDaemon() {
|
|
|
7538
7631
|
const timeout = setTimeout(() => {
|
|
7539
7632
|
pidToAwaiter.delete(happyProcess.pid);
|
|
7540
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
|
+
}
|
|
7541
7641
|
resolve({
|
|
7542
7642
|
type: "error",
|
|
7543
7643
|
errorMessage: `Session webhook timeout for PID ${happyProcess.pid}`
|
|
@@ -7560,6 +7660,19 @@ async function startDaemon() {
|
|
|
7560
7660
|
return sessionIdToFinishedSession.get(happySessionId);
|
|
7561
7661
|
};
|
|
7562
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
|
+
}
|
|
7563
7676
|
try {
|
|
7564
7677
|
const response = await axios.get(`${configuration.serverUrl}/v1/sessions`, {
|
|
7565
7678
|
headers: { Authorization: `Bearer ${credentials.token}` },
|
|
@@ -7575,17 +7688,38 @@ async function startDaemon() {
|
|
|
7575
7688
|
return null;
|
|
7576
7689
|
}
|
|
7577
7690
|
};
|
|
7691
|
+
const resumeGates = /* @__PURE__ */ new Map();
|
|
7578
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) => {
|
|
7579
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
|
+
}
|
|
7580
7714
|
const tracked = findTrackedSessionById(happySessionId);
|
|
7581
7715
|
if (!tracked) {
|
|
7582
|
-
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.` };
|
|
7583
7717
|
}
|
|
7584
7718
|
if (!tracked.happySessionMetadataFromLocalWebhook) {
|
|
7585
|
-
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.` };
|
|
7586
7720
|
}
|
|
7587
7721
|
if (!tracked.encryption) {
|
|
7588
|
-
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.` };
|
|
7589
7723
|
}
|
|
7590
7724
|
let metadata = tracked.happySessionMetadataFromLocalWebhook;
|
|
7591
7725
|
const needsFetch = !metadata.claudeSessionId && (!metadata.flavor || metadata.flavor === "claude") || !metadata.codexThreadId && metadata.flavor === "codex";
|
|
@@ -7597,18 +7731,24 @@ async function startDaemon() {
|
|
|
7597
7731
|
tracked.happySessionMetadataFromLocalWebhook = serverMetadata;
|
|
7598
7732
|
}
|
|
7599
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
|
+
}
|
|
7600
7741
|
const launch = buildResumeLaunch(
|
|
7601
7742
|
{ id: happySessionId, active: true, metadata },
|
|
7602
7743
|
{ startedBy: "daemon", claudeStartingMode: "remote" }
|
|
7603
7744
|
);
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
}
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
}
|
|
7610
|
-
await
|
|
7611
|
-
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({
|
|
7612
7752
|
args: launch.args,
|
|
7613
7753
|
cwd: launch.cwd,
|
|
7614
7754
|
env: {
|
|
@@ -7621,6 +7761,18 @@ async function startDaemon() {
|
|
|
7621
7761
|
HAPPY_RECONNECT_AGENT_STATE_VERSION: String(tracked.encryption.agentStateVersion)
|
|
7622
7762
|
}
|
|
7623
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;
|
|
7624
7776
|
} catch (error) {
|
|
7625
7777
|
const errorMessage = error instanceof Error ? error.message : error && typeof error === "object" ? JSON.stringify(error) : String(error);
|
|
7626
7778
|
logger.debug(`[DAEMON RUN] Failed to resume session: ${errorMessage}`, error instanceof Error ? error.stack : void 0);
|
|
@@ -7630,6 +7782,136 @@ async function startDaemon() {
|
|
|
7630
7782
|
};
|
|
7631
7783
|
}
|
|
7632
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
|
+
};
|
|
7633
7915
|
const stopSession = (sessionId) => {
|
|
7634
7916
|
logger.debug(`[DAEMON RUN] Attempting to stop session ${sessionId}`);
|
|
7635
7917
|
for (const [pid, session] of pidToTrackedSession.entries()) {
|
|
@@ -7800,6 +8082,7 @@ async function startDaemon() {
|
|
|
7800
8082
|
apiMachine.setRPCHandlers({
|
|
7801
8083
|
spawnSession,
|
|
7802
8084
|
resumeSession,
|
|
8085
|
+
restartSession,
|
|
7803
8086
|
stopSession,
|
|
7804
8087
|
listTrackedSessionIds: () => [...pidToTrackedSession.values()].map((session) => session.happySessionId).filter((sessionId) => typeof sessionId === "string"),
|
|
7805
8088
|
requestShutdown: () => requestShutdown("happy-app")
|
|
@@ -9391,6 +9674,18 @@ async function runClaude(credentials, options = {}) {
|
|
|
9391
9674
|
if (reconnectSessionId && !await api.reactivateSession(response.id)) {
|
|
9392
9675
|
throw new Error(`Failed to reactivate archived session ${response.id}`);
|
|
9393
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
|
+
}
|
|
9394
9689
|
try {
|
|
9395
9690
|
logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
|
9396
9691
|
const result = await notifyDaemonSessionStarted(response.id, metadata, {
|
|
@@ -9408,14 +9703,12 @@ async function runClaude(credentials, options = {}) {
|
|
|
9408
9703
|
} catch (error) {
|
|
9409
9704
|
logger.debug("[START] Failed to report to daemon (may not be running):", error);
|
|
9410
9705
|
}
|
|
9411
|
-
const session = api.sessionSyncClient(response);
|
|
9706
|
+
const session = api.sessionSyncClient(response, reconnectSeeded ? { initialSeq: response.seq } : void 0);
|
|
9412
9707
|
if (reconnectSessionId) {
|
|
9413
9708
|
session.suppressNextArchiveSignal();
|
|
9414
|
-
session.skipExistingMessages();
|
|
9709
|
+
if (!reconnectSeeded) session.skipExistingMessages();
|
|
9415
9710
|
session.updateMetadata((meta) => ({
|
|
9416
|
-
...meta,
|
|
9417
|
-
lifecycleState: "running",
|
|
9418
|
-
archivedBy: void 0,
|
|
9711
|
+
...mergeReconnectMetadata(meta, metadata, Date.now()),
|
|
9419
9712
|
queueCancellation: true,
|
|
9420
9713
|
// B-262 batch 2 (B5): a reconnected process re-publishes the mode
|
|
9421
9714
|
// IT enforces; the server copy may still carry the previous
|
|
@@ -12126,7 +12419,7 @@ function setupOfflineReconnection(opts) {
|
|
|
12126
12419
|
});
|
|
12127
12420
|
return { session, reconnectionHandle, isOffline: true };
|
|
12128
12421
|
} else {
|
|
12129
|
-
session = api.sessionSyncClient(response);
|
|
12422
|
+
session = api.sessionSyncClient(response, opts.sessionClientOptions);
|
|
12130
12423
|
return { session, reconnectionHandle: null, isOffline: false };
|
|
12131
12424
|
}
|
|
12132
12425
|
}
|
|
@@ -12895,6 +13188,18 @@ async function runCodex(opts) {
|
|
|
12895
13188
|
if (reconnectSessionId && response && !await api.reactivateSession(response.id)) {
|
|
12896
13189
|
throw new Error(`Failed to reactivate archived session ${response.id}`);
|
|
12897
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
|
+
}
|
|
12898
13203
|
let session;
|
|
12899
13204
|
let permissionHandler;
|
|
12900
13205
|
let client;
|
|
@@ -12906,6 +13211,7 @@ async function runCodex(opts) {
|
|
|
12906
13211
|
metadata,
|
|
12907
13212
|
state,
|
|
12908
13213
|
response,
|
|
13214
|
+
sessionClientOptions: reconnectSeeded && response ? { initialSeq: response.seq } : void 0,
|
|
12909
13215
|
onSessionSwap: (newSession) => {
|
|
12910
13216
|
session = newSession;
|
|
12911
13217
|
if (permissionHandler) {
|
|
@@ -12916,12 +13222,8 @@ async function runCodex(opts) {
|
|
|
12916
13222
|
session = initialSession;
|
|
12917
13223
|
if (reconnectSessionId) {
|
|
12918
13224
|
session.suppressNextArchiveSignal();
|
|
12919
|
-
session.skipExistingMessages();
|
|
12920
|
-
session.updateMetadata((meta) => (
|
|
12921
|
-
...meta,
|
|
12922
|
-
lifecycleState: "running",
|
|
12923
|
-
archivedBy: void 0
|
|
12924
|
-
}));
|
|
13225
|
+
if (!reconnectSeeded) session.skipExistingMessages();
|
|
13226
|
+
session.updateMetadata((meta) => mergeReconnectMetadata(meta, metadata, Date.now()));
|
|
12925
13227
|
}
|
|
12926
13228
|
if (response) {
|
|
12927
13229
|
try {
|
|
@@ -13649,7 +13951,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13649
13951
|
process.exit(0);
|
|
13650
13952
|
} else if (subcommand === "install-terminal-hooks") {
|
|
13651
13953
|
try {
|
|
13652
|
-
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-
|
|
13954
|
+
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-DDRrUG_n.mjs');
|
|
13653
13955
|
const command = parseTerminalHooksArgs(args.slice(1));
|
|
13654
13956
|
if (command.action === "help") {
|
|
13655
13957
|
console.log(TERMINAL_HOOKS_HELP);
|
|
@@ -13666,7 +13968,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13666
13968
|
}
|
|
13667
13969
|
} else if (subcommand === "spawn") {
|
|
13668
13970
|
try {
|
|
13669
|
-
const { handleSpawnCommand } = await import('./spawn-
|
|
13971
|
+
const { handleSpawnCommand } = await import('./spawn-DS-Uz_Hw.mjs');
|
|
13670
13972
|
await handleSpawnCommand(args.slice(1));
|
|
13671
13973
|
} catch (error) {
|
|
13672
13974
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -13678,7 +13980,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13678
13980
|
return;
|
|
13679
13981
|
} else if (subcommand === "send") {
|
|
13680
13982
|
try {
|
|
13681
|
-
const { handleSendCommand } = await import('./send-
|
|
13983
|
+
const { handleSendCommand } = await import('./send-BQSaVz9r.mjs');
|
|
13682
13984
|
await handleSendCommand(args.slice(1));
|
|
13683
13985
|
} catch (error) {
|
|
13684
13986
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -13784,9 +14086,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13784
14086
|
if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
|
|
13785
14087
|
const projectId = args[3];
|
|
13786
14088
|
try {
|
|
13787
|
-
const { saveGoogleCloudProjectToConfig } = await import('./config-
|
|
13788
|
-
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-
|
|
13789
|
-
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; });
|
|
13790
14092
|
let userEmail = void 0;
|
|
13791
14093
|
try {
|
|
13792
14094
|
const credentials = await readCredentialsForConfiguredRelay2();
|
|
@@ -13817,7 +14119,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13817
14119
|
}
|
|
13818
14120
|
if (geminiSubcommand === "project" && args[2] === "get") {
|
|
13819
14121
|
try {
|
|
13820
|
-
const { readGeminiLocalConfig } = await import('./config-
|
|
14122
|
+
const { readGeminiLocalConfig } = await import('./config-CJQw0z1o.mjs');
|
|
13821
14123
|
const config = readGeminiLocalConfig();
|
|
13822
14124
|
if (config.googleCloudProject) {
|
|
13823
14125
|
console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
|
|
@@ -13857,7 +14159,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13857
14159
|
process.exit(0);
|
|
13858
14160
|
}
|
|
13859
14161
|
try {
|
|
13860
|
-
const { runGemini } = await import('./runGemini-
|
|
14162
|
+
const { runGemini } = await import('./runGemini-DHYs_Beq.mjs');
|
|
13861
14163
|
let startedBy = void 0;
|
|
13862
14164
|
for (let i = 1; i < args.length; i++) {
|
|
13863
14165
|
if (args[i] === "--started-by") {
|
|
@@ -13879,7 +14181,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13879
14181
|
return;
|
|
13880
14182
|
} else if (subcommand === "acp") {
|
|
13881
14183
|
try {
|
|
13882
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
14184
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-BP9pfzdZ.mjs');
|
|
13883
14185
|
let startedBy = void 0;
|
|
13884
14186
|
let verbose = false;
|
|
13885
14187
|
const acpArgs = [];
|
|
@@ -13919,7 +14221,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13919
14221
|
return;
|
|
13920
14222
|
} else if (subcommand === "openclaw") {
|
|
13921
14223
|
try {
|
|
13922
|
-
const { runOpenClaw } = await import('./runOpenClaw
|
|
14224
|
+
const { runOpenClaw } = await import('./runOpenClaw-DTaXxvNd.mjs');
|
|
13923
14225
|
let startedBy = void 0;
|
|
13924
14226
|
let verbose = false;
|
|
13925
14227
|
let gatewayUrl;
|
|
@@ -13970,7 +14272,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
13970
14272
|
return;
|
|
13971
14273
|
} else if (subcommand === "mcp" && args.length === 1) {
|
|
13972
14274
|
try {
|
|
13973
|
-
const { handleMcpCommand } = await import('./mcp-
|
|
14275
|
+
const { handleMcpCommand } = await import('./mcp-CYJAl5pk.mjs');
|
|
13974
14276
|
await handleMcpCommand();
|
|
13975
14277
|
} catch (error) {
|
|
13976
14278
|
process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
|