very-happy-cli 0.2.91 → 0.2.93

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.
Files changed (31) hide show
  1. package/dist/{AcpBackend-B5RRlzLU.mjs → AcpBackend-C5507ePk.mjs} +1 -1
  2. package/dist/{AcpBackend-GqveWg8u.cjs → AcpBackend-COv_3umE.cjs} +1 -1
  3. package/dist/{AcpSessionManager-Ij1pI5Ex.mjs → AcpSessionManager-Bd1trXWx.mjs} +1 -1
  4. package/dist/{AcpSessionManager-YcrSZwvq.cjs → AcpSessionManager-vm4rIcH0.cjs} +1 -1
  5. package/dist/{config-ra8iKxdU.mjs → config-BJjvra5x.mjs} +2 -2
  6. package/dist/{config-C4NCw7Dz.cjs → config-SZEQWS8k.cjs} +2 -2
  7. package/dist/{index-C1HIm7NY.cjs → index-17NbIAkU.cjs} +4 -4
  8. package/dist/{index-UCYD5Wk3.mjs → index-C00jxV83.mjs} +337 -35
  9. package/dist/{index-CB5H7YI6.mjs → index-D-U-7DAy.mjs} +5 -5
  10. package/dist/{index-FkXJNwiy.cjs → index-DaVhaBF2.cjs} +353 -51
  11. package/dist/index.cjs +2 -2
  12. package/dist/index.mjs +2 -2
  13. package/dist/{installTerminalHooks-CoK5wNcA.cjs → installTerminalHooks-CRjlRj9K.cjs} +2 -2
  14. package/dist/{installTerminalHooks-TkkGzzbI.mjs → installTerminalHooks-DsECLFy3.mjs} +2 -2
  15. package/dist/lib.cjs +1 -1
  16. package/dist/lib.d.cts +69 -3
  17. package/dist/lib.d.mts +69 -3
  18. package/dist/lib.mjs +1 -1
  19. package/dist/{mcp-C3MiqObi.mjs → mcp-Bs6R9P7q.mjs} +2 -2
  20. package/dist/{mcp-DcRvswVI.cjs → mcp-C-dDCrXm.cjs} +2 -2
  21. package/dist/{runGemini-BbC0uwUj.mjs → runGemini-CrSCxKqI.mjs} +4 -4
  22. package/dist/{runGemini-Dnvd2gWq.cjs → runGemini-DapsCPLi.cjs} +4 -4
  23. package/dist/{runOpenClaw-CnuTBNQW.cjs → runOpenClaw-C9dhyV4x.cjs} +3 -3
  24. package/dist/{runOpenClaw--nbqTW3K.mjs → runOpenClaw-Cnagj-hv.mjs} +3 -3
  25. package/dist/{send-9nJxn09Y.mjs → send-DlHES1EO.mjs} +2 -2
  26. package/dist/{send-DkKOPatx.cjs → send-PmtzSBVM.cjs} +2 -2
  27. package/dist/{spawn-CRW4ioms.mjs → spawn-DgZUVvT2.mjs} +2 -2
  28. package/dist/{spawn-DE7WhS8Y.cjs → spawn-OE9kENQi.cjs} +2 -2
  29. package/dist/{types-ClDADuxP.cjs → types-BHVxYajT.cjs} +332 -88
  30. package/dist/{types-BKyjG23_.mjs → types-Cx5wn5Hf.mjs} +331 -88
  31. package/package.json +7 -7
@@ -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-BKyjG23_.mjs';
4
+ import { l as logger, d as contentLogMetadata, e as errorLogMetadata, f as packageJson, g as delay } from './types-Cx5wn5Hf.mjs';
5
5
 
6
6
  const DEFAULT_TIMEOUTS = {
7
7
  /** Default initialization timeout: 60 seconds */
@@ -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-ClDADuxP.cjs');
6
+ var persistence = require('./types-BHVxYajT.cjs');
7
7
 
8
8
  const DEFAULT_TIMEOUTS = {
9
9
  /** Default initialization timeout: 60 seconds */
@@ -1,5 +1,5 @@
1
1
  import { createId } from '@paralleldrive/cuid2';
2
- import { b as createEnvelope } from './types-BKyjG23_.mjs';
2
+ import { b as createEnvelope } from './types-Cx5wn5Hf.mjs';
3
3
 
4
4
  function turnOptions(turnId, time) {
5
5
  return turnId ? { turn: turnId, time } : { time };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var cuid2 = require('@paralleldrive/cuid2');
4
- var persistence = require('./types-ClDADuxP.cjs');
4
+ var persistence = require('./types-BHVxYajT.cjs');
5
5
 
6
6
  function turnOptions(turnId, time) {
7
7
  return turnId ? { turn: turnId, time } : { time };
@@ -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-BKyjG23_.mjs';
6
- import { D as DEFAULT_GEMINI_MODEL, G as GEMINI_MODEL_ENV } from './index-UCYD5Wk3.mjs';
5
+ import { l as logger } from './types-Cx5wn5Hf.mjs';
6
+ import { D as DEFAULT_GEMINI_MODEL, G as GEMINI_MODEL_ENV } from './index-C00jxV83.mjs';
7
7
  import 'axios';
8
8
  import 'chalk';
9
9
  import 'node:util';
@@ -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-ClDADuxP.cjs');
8
- var index = require('./index-FkXJNwiy.cjs');
7
+ var persistence = require('./types-BHVxYajT.cjs');
8
+ var index = require('./index-DaVhaBF2.cjs');
9
9
  require('axios');
10
10
  require('chalk');
11
11
  require('node:util');
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
- var AcpBackend = require('./AcpBackend-GqveWg8u.cjs');
4
- var persistence = require('./types-ClDADuxP.cjs');
5
- var AcpSessionManager = require('./AcpSessionManager-YcrSZwvq.cjs');
3
+ var AcpBackend = require('./AcpBackend-COv_3umE.cjs');
4
+ var persistence = require('./types-BHVxYajT.cjs');
5
+ var AcpSessionManager = require('./AcpSessionManager-vm4rIcH0.cjs');
6
6
  var node_crypto = require('node:crypto');
7
7
  var path = require('node:path');
8
- var index = require('./index-FkXJNwiy.cjs');
8
+ var index = require('./index-DaVhaBF2.cjs');
9
9
  require('node:child_process');
10
10
  require('@agentclientprotocol/sdk');
11
11
  require('axios');
@@ -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 getLocalHappyAgentCredentialPath, L as readLocalHappyAgentCredentials, N as decryptWithDataKey, O as decryptLegacy, P as extractCompleteLines, Q as decideBackfill, S as parseTerminalHookPayload, T as tmuxArgs, U as scrubTmuxClientEnv, V as decideMirrorBinding, W as persistSession, X as MIRROR_BACKFILL_LINES_DEFAULT, Y as mirrorLineKey, Z as mirrorLocalId, _ as detectResumeSupport, $ as detectCLIAvailability, a0 as acquireDaemonLock, a1 as summarizeSpawnSessionForLog, a2 as deletePersistedSessions, a3 as decrypt, a4 as writeDaemonState, A as ApiClient, a5 as fetchCliUpdateState, a6 as resolveCliUpdateCheckInterval, a7 as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, a8 as releaseDaemonLock, a9 as isValidSessionId, aa as listVhTerminals, ab as isValidTerminalId, ac as readVhTerminal, ad as sendToVhTerminal, h as connectionState, ae as startOfflineReconnection, af as clearCredentials, ag as clearMachineId, k as ensurePrivateDirectorySync, w as writePrivateFileSync, ah as SandboxConfigSchema, b as createEnvelope, ai as CodexAppServerClient, aj as getLatestDaemonLog } from './types-BKyjG23_.mjs';
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-Cx5wn5Hf.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
- if (options?.model) {
7605
- launch.args.push("--model", options.model);
7606
- }
7607
- if (options?.permissionMode) {
7608
- launch.args.push("--permission-mode", options.permissionMode);
7609
- }
7610
- await fs.access(launch.cwd);
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-TkkGzzbI.mjs');
13954
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-DsECLFy3.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-CRW4ioms.mjs');
13971
+ const { handleSpawnCommand } = await import('./spawn-DgZUVvT2.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-9nJxn09Y.mjs');
13983
+ const { handleSendCommand } = await import('./send-DlHES1EO.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-ra8iKxdU.mjs');
13788
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-BKyjG23_.mjs').then(function (n) { return n.ak; });
13789
- const { ApiClient: ApiClient2 } = await import('./types-BKyjG23_.mjs').then(function (n) { return n.al; });
14089
+ const { saveGoogleCloudProjectToConfig } = await import('./config-BJjvra5x.mjs');
14090
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-Cx5wn5Hf.mjs').then(function (n) { return n.al; });
14091
+ const { ApiClient: ApiClient2 } = await import('./types-Cx5wn5Hf.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-ra8iKxdU.mjs');
14122
+ const { readGeminiLocalConfig } = await import('./config-BJjvra5x.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-BbC0uwUj.mjs');
14162
+ const { runGemini } = await import('./runGemini-CrSCxKqI.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-CB5H7YI6.mjs');
14184
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-D-U-7DAy.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--nbqTW3K.mjs');
14224
+ const { runOpenClaw } = await import('./runOpenClaw-Cnagj-hv.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-C3MiqObi.mjs');
14275
+ const { handleMcpCommand } = await import('./mcp-Bs6R9P7q.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)}