very-happy-cli 0.2.93 → 0.2.95

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-COv_3umE.cjs → AcpBackend-BXsbwTbl.cjs} +1 -1
  2. package/dist/{AcpBackend-C5507ePk.mjs → AcpBackend-Cuiff4Nu.mjs} +1 -1
  3. package/dist/{AcpSessionManager-Bd1trXWx.mjs → AcpSessionManager-BU69q_vR.mjs} +1 -1
  4. package/dist/{AcpSessionManager-vm4rIcH0.cjs → AcpSessionManager-BXGmMoe8.cjs} +1 -1
  5. package/dist/{config-BJjvra5x.mjs → config-2GD21yg_.mjs} +2 -2
  6. package/dist/{config-SZEQWS8k.cjs → config-BVOE0pni.cjs} +2 -2
  7. package/dist/{index-DaVhaBF2.cjs → index-BzbaMrIa.cjs} +333 -49
  8. package/dist/{index-17NbIAkU.cjs → index-CbPc1iVx.cjs} +4 -4
  9. package/dist/{index-C00jxV83.mjs → index-DVEdpWq0.mjs} +331 -47
  10. package/dist/{index-D-U-7DAy.mjs → index-DZ0uw6ef.mjs} +5 -5
  11. package/dist/index.cjs +2 -2
  12. package/dist/index.mjs +2 -2
  13. package/dist/{installTerminalHooks-CRjlRj9K.cjs → installTerminalHooks-BrrsyTMd.cjs} +2 -2
  14. package/dist/{installTerminalHooks-DsECLFy3.mjs → installTerminalHooks-FiujjEvb.mjs} +2 -2
  15. package/dist/lib.cjs +1 -1
  16. package/dist/lib.d.cts +9 -0
  17. package/dist/lib.d.mts +9 -0
  18. package/dist/lib.mjs +1 -1
  19. package/dist/{mcp-Bs6R9P7q.mjs → mcp-0mZcHPtz.mjs} +2 -2
  20. package/dist/{mcp-C-dDCrXm.cjs → mcp-ziyMk3eS.cjs} +2 -2
  21. package/dist/{runGemini-DapsCPLi.cjs → runGemini-C91O1oxi.cjs} +4 -4
  22. package/dist/{runGemini-CrSCxKqI.mjs → runGemini-D_InkOhk.mjs} +4 -4
  23. package/dist/{runOpenClaw-C9dhyV4x.cjs → runOpenClaw-B1RRfA1V.cjs} +3 -3
  24. package/dist/{runOpenClaw-Cnagj-hv.mjs → runOpenClaw-BjebGmV6.mjs} +3 -3
  25. package/dist/{send-DlHES1EO.mjs → send-CxLL0z9J.mjs} +2 -2
  26. package/dist/{send-PmtzSBVM.cjs → send-DgK6MtgT.cjs} +2 -2
  27. package/dist/{spawn-OE9kENQi.cjs → spawn-Dian7x_g.cjs} +2 -2
  28. package/dist/{spawn-DgZUVvT2.mjs → spawn-uTPD3qOt.mjs} +2 -2
  29. package/dist/{types-Cx5wn5Hf.mjs → types-BLE8Hfmh.mjs} +34 -14
  30. package/dist/{types-BHVxYajT.cjs → types-HRuZ13LS.cjs} +34 -14
  31. package/package.json +7 -7
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
- var AcpBackend = require('./AcpBackend-COv_3umE.cjs');
4
- var persistence = require('./types-BHVxYajT.cjs');
5
- var AcpSessionManager = require('./AcpSessionManager-vm4rIcH0.cjs');
3
+ var AcpBackend = require('./AcpBackend-BXsbwTbl.cjs');
4
+ var persistence = require('./types-HRuZ13LS.cjs');
5
+ var AcpSessionManager = require('./AcpSessionManager-BXGmMoe8.cjs');
6
6
  var node_crypto = require('node:crypto');
7
7
  var path = require('node:path');
8
- var index = require('./index-DaVhaBF2.cjs');
8
+ var index = require('./index-BzbaMrIa.cjs');
9
9
  require('node:child_process');
10
10
  require('@agentclientprotocol/sdk');
11
11
  require('axios');
@@ -1,11 +1,11 @@
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 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';
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 MIRROR_BACKFILL_LINES_DEFAULT, Y as persistSession, Z as mirrorLineKey, _ as mirrorLocalId, w as writePrivateFileSync, $ 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, ai as SandboxConfigSchema, b as createEnvelope, aj as CodexAppServerClient, ak as getLatestDaemonLog } from './types-BLE8Hfmh.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';
8
- import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, writeFileSync, rmSync, chmodSync as chmodSync$1 } from 'node:fs';
8
+ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, renameSync, writeFileSync, rmSync, chmodSync as chmodSync$1 } from 'node:fs';
9
9
  import { t as trimIdent, A as AGENT_GUIDANCE, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, c as REPORT_PROGRESS_TOOL_TITLE, d as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-C2_pkgRI.mjs';
10
10
  import { readFile, writeFile, rename, unlink, mkdir, stat, open as open$1, appendFile } from 'node:fs/promises';
11
11
  import fs, { watch, access } from 'fs/promises';
@@ -6766,10 +6766,20 @@ function createMirrorScanner(opts) {
6766
6766
  }
6767
6767
 
6768
6768
  const TERMINAL_MIRROR_FLAVOR = "terminal-mirror";
6769
+ const RECONCILE_HYSTERESIS_MS = 2e4;
6769
6770
  function createMirrorManager(deps) {
6770
6771
  const backfillLines = deps.backfillLines ?? MIRROR_BACKFILL_LINES_DEFAULT;
6771
6772
  const bindings = /* @__PURE__ */ new Map();
6773
+ const lastEndedAt = /* @__PURE__ */ new Map();
6772
6774
  let chain = Promise.resolve();
6775
+ const reactivate = async (binding) => {
6776
+ try {
6777
+ const ok = await deps.api.reactivateSession(binding.happySessionId);
6778
+ binding.needsReactivate = !ok;
6779
+ } catch {
6780
+ binding.needsReactivate = true;
6781
+ }
6782
+ };
6773
6783
  const isTerminalAlive = deps.isTerminalAlive ?? ((terminalId) => {
6774
6784
  try {
6775
6785
  return spawnSync("tmux", tmuxArgs(["has-session", "-t", `=vh-${terminalId}:`]), { stdio: "ignore", timeout: 3e3, env: scrubTmuxClientEnv({ ...process.env }) }).status === 0;
@@ -6821,6 +6831,8 @@ function createMirrorManager(deps) {
6821
6831
  const endBinding = async (binding, reason) => {
6822
6832
  if (binding.status === "ended") return;
6823
6833
  binding.status = "ended";
6834
+ lastEndedAt.set(binding.terminalId, Date.now());
6835
+ binding.needsReactivate = false;
6824
6836
  logger.debug(`[MIRROR] ending binding for terminal ${binding.terminalId} (${reason})`);
6825
6837
  await binding.scanner?.cleanup();
6826
6838
  binding.scanner = null;
@@ -6848,6 +6860,76 @@ function createMirrorManager(deps) {
6848
6860
  }
6849
6861
  await binding.client.close();
6850
6862
  };
6863
+ const adoptPersisted = async (terminalId, persisted) => {
6864
+ if (bindings.has(terminalId)) return;
6865
+ let best = null;
6866
+ for (const [sessionId2, entry2] of Object.entries(persisted)) {
6867
+ const meta2 = entry2.metadata;
6868
+ if (meta2?.flavor !== TERMINAL_MIRROR_FLAVOR) continue;
6869
+ if (meta2.machineId !== deps.machineId) continue;
6870
+ if (meta2.terminalId !== terminalId) continue;
6871
+ if (!meta2.claudeSessionId) continue;
6872
+ if (!best || entry2.savedAt > best.entry.savedAt) best = { sessionId: sessionId2, entry: entry2 };
6873
+ }
6874
+ if (!best) return;
6875
+ const { sessionId, entry } = best;
6876
+ const meta = entry.metadata;
6877
+ const session = {
6878
+ id: sessionId,
6879
+ seq: entry.seq,
6880
+ metadata: meta,
6881
+ metadataVersion: entry.metadataVersion,
6882
+ agentState: null,
6883
+ agentStateVersion: entry.agentStateVersion,
6884
+ encryptionKey: decodeBase64(entry.encryptionKey),
6885
+ encryptionVariant: entry.encryptionVariant
6886
+ };
6887
+ const client = deps.api.sessionSyncClient(session);
6888
+ client.skipExistingMessages();
6889
+ const binding = {
6890
+ terminalId,
6891
+ happySessionId: sessionId,
6892
+ claudeSessionId: meta.claudeSessionId,
6893
+ status: "active",
6894
+ client,
6895
+ scanner: null,
6896
+ persist: {
6897
+ encryptionKey: entry.encryptionKey,
6898
+ encryptionVariant: entry.encryptionVariant,
6899
+ seq: entry.seq,
6900
+ metadataVersion: entry.metadataVersion,
6901
+ agentStateVersion: entry.agentStateVersion
6902
+ },
6903
+ metadata: meta
6904
+ };
6905
+ bindings.set(terminalId, binding);
6906
+ await reactivate(binding);
6907
+ updateBindingMetadata(binding, {
6908
+ lifecycleState: "running",
6909
+ lifecycleStateSince: Date.now(),
6910
+ archivedBy: void 0,
6911
+ archiveReason: void 0
6912
+ });
6913
+ const scanner = attachScanner(binding, { withTruncationNotice: false });
6914
+ scanner.addFile(join(getProjectPath(meta.path), `${meta.claudeSessionId}.jsonl`), "backfill-tail");
6915
+ logger.debug(`[MIRROR] reconcile adopted terminal ${terminalId} \u2192 ${sessionId}`);
6916
+ deps.onBindingsChanged?.();
6917
+ };
6918
+ const reactivateInPlace = async (binding) => {
6919
+ if (binding.status !== "ended") return;
6920
+ binding.status = "active";
6921
+ await reactivate(binding);
6922
+ updateBindingMetadata(binding, {
6923
+ lifecycleState: "running",
6924
+ lifecycleStateSince: Date.now(),
6925
+ archivedBy: void 0,
6926
+ archiveReason: void 0
6927
+ });
6928
+ const scanner = binding.scanner ?? attachScanner(binding, { withTruncationNotice: false });
6929
+ scanner.addFile(join(getProjectPath(binding.metadata.path), `${binding.claudeSessionId}.jsonl`), "backfill-tail");
6930
+ logger.debug(`[MIRROR] reconcile reactivated (in place) terminal ${binding.terminalId}`);
6931
+ deps.onBindingsChanged?.();
6932
+ };
6851
6933
  const createBinding = async (event, replaces) => {
6852
6934
  if (replaces) {
6853
6935
  await teardownBinding(replaces, "superseded by a fresh conversation");
@@ -6908,6 +6990,7 @@ function createMirrorManager(deps) {
6908
6990
  binding.claudeSessionId = event.claudeSessionId;
6909
6991
  if (binding.status === "ended") {
6910
6992
  binding.status = "active";
6993
+ await reactivate(binding);
6911
6994
  updateBindingMetadata(binding, {
6912
6995
  lifecycleState: "running",
6913
6996
  lifecycleStateSince: Date.now(),
@@ -6977,6 +7060,43 @@ function createMirrorManager(deps) {
6977
7060
  }
6978
7061
  }
6979
7062
  },
7063
+ reconcile(list) {
7064
+ let persisted = null;
7065
+ const now = Date.now();
7066
+ for (const item of list) {
7067
+ const binding = bindings.get(item.id) ?? null;
7068
+ if (item.agentState === "shell") {
7069
+ if (binding && binding.status === "active") {
7070
+ chain = chain.then(() => {
7071
+ const b = bindings.get(item.id);
7072
+ return b && b.status === "active" ? endBinding(b, "claude exited (pane observation)") : void 0;
7073
+ }).catch((e) => logger.debug(`[MIRROR] reconcile end failed for ${item.id}:`, e));
7074
+ }
7075
+ continue;
7076
+ }
7077
+ if (binding && binding.status === "active" && binding.needsReactivate) {
7078
+ chain = chain.then(() => {
7079
+ const b = bindings.get(item.id);
7080
+ return b && b.status === "active" && b.needsReactivate ? reactivate(b) : void 0;
7081
+ }).catch((e) => logger.debug(`[MIRROR] reconcile reactivate-retry failed for ${item.id}:`, e));
7082
+ }
7083
+ if (!item.claudeConfident) continue;
7084
+ const endedAt = lastEndedAt.get(item.id);
7085
+ if (endedAt !== void 0 && now - endedAt < RECONCILE_HYSTERESIS_MS) continue;
7086
+ if (binding && binding.status === "ended") {
7087
+ chain = chain.then(() => {
7088
+ const b = bindings.get(item.id);
7089
+ return b && b.status === "ended" ? reactivateInPlace(b) : void 0;
7090
+ }).catch((e) => logger.debug(`[MIRROR] reconcile in-place reactivate failed for ${item.id}:`, e));
7091
+ } else if (!binding) {
7092
+ persisted ??= readPersistedSessions();
7093
+ const p = persisted;
7094
+ chain = chain.then(() => {
7095
+ return bindings.has(item.id) ? void 0 : adoptPersisted(item.id, p);
7096
+ }).catch((e) => logger.debug(`[MIRROR] reconcile adopt failed for ${item.id}:`, e));
7097
+ }
7098
+ }
7099
+ },
6980
7100
  resolveMirrorSessionId(terminalId) {
6981
7101
  return bindings.get(terminalId)?.happySessionId;
6982
7102
  },
@@ -7091,6 +7211,144 @@ function recoverableSessionPid(metadata, liveHappyPids) {
7091
7211
  const pid = metadata?.hostPid;
7092
7212
  return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && liveHappyPids.has(pid) ? pid : null;
7093
7213
  }
7214
+ function escapeRegExp(value) {
7215
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7216
+ }
7217
+ function sessionAgentConversationId(metadata) {
7218
+ if (!metadata) return null;
7219
+ if (metadata.flavor === "codex") return metadata.codexThreadId ?? null;
7220
+ return metadata.claudeSessionId ?? metadata.codexThreadId ?? null;
7221
+ }
7222
+ function isDaemonWrapperForConversation(command, conversationId) {
7223
+ if (!command || !conversationId) return false;
7224
+ const startedByDaemon = /(?:^|\s)--started-by\s+daemon(?:\s|$)/.test(command);
7225
+ if (!startedByDaemon) return false;
7226
+ const resume = new RegExp(`(?:^|\\s)--resume(?:\\s+|=)${escapeRegExp(conversationId)}(?:\\s|$)`);
7227
+ return resume.test(command);
7228
+ }
7229
+ function findSessionWrapperPids(metadata, liveHappyProcesses, options = {}) {
7230
+ const out = [];
7231
+ const livePids = new Set(liveHappyProcesses.map((p) => p.pid));
7232
+ const lockPid = options.lockPid ?? null;
7233
+ if (lockPid !== null && lockPid !== options.excludePid && livePids.has(lockPid)) out.push(lockPid);
7234
+ const persisted = recoverableSessionPid(metadata, livePids);
7235
+ if (persisted !== null && persisted !== options.excludePid && !out.includes(persisted)) out.push(persisted);
7236
+ const conversationId = sessionAgentConversationId(metadata);
7237
+ if (conversationId) {
7238
+ for (const proc of liveHappyProcesses) {
7239
+ if (proc.pid === options.excludePid || out.includes(proc.pid)) continue;
7240
+ if (isDaemonWrapperForConversation(proc.command, conversationId)) out.push(proc.pid);
7241
+ }
7242
+ }
7243
+ return out;
7244
+ }
7245
+ function mergeRestoreMetadata(stale, reported) {
7246
+ if (!reported) return stale;
7247
+ return { ...stale, ...processIdentityFields(reported) };
7248
+ }
7249
+
7250
+ const SESSION_LOCK_DIR = "session-locks";
7251
+ const defaultRuntime = () => ({
7252
+ dir: join(configuration.happyHomeDir, SESSION_LOCK_DIR),
7253
+ selfPid: process.pid,
7254
+ now: () => Date.now(),
7255
+ signal: (pid, signal) => process.kill(pid, signal),
7256
+ isAlive: (pid) => {
7257
+ try {
7258
+ process.kill(pid, 0);
7259
+ return true;
7260
+ } catch {
7261
+ return false;
7262
+ }
7263
+ },
7264
+ schedule: (callback, delayMs) => {
7265
+ setTimeout(callback, delayMs);
7266
+ }
7267
+ });
7268
+ function sessionLockPath(happySessionId, dir = join(configuration.happyHomeDir, SESSION_LOCK_DIR)) {
7269
+ return join(dir, `${happySessionId}.json`);
7270
+ }
7271
+ function readSessionLock(happySessionId, dir) {
7272
+ try {
7273
+ const path = sessionLockPath(happySessionId, dir);
7274
+ if (!existsSync(path)) return null;
7275
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
7276
+ if (typeof parsed?.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
7277
+ return {
7278
+ pid: parsed.pid,
7279
+ startedAt: typeof parsed.startedAt === "number" ? parsed.startedAt : 0,
7280
+ version: typeof parsed.version === "string" ? parsed.version : "",
7281
+ ...typeof parsed.flavor === "string" ? { flavor: parsed.flavor } : {}
7282
+ };
7283
+ } catch {
7284
+ return null;
7285
+ }
7286
+ }
7287
+ function liveSessionLockHolder(happySessionId, runtime = defaultRuntime()) {
7288
+ const record = readSessionLock(happySessionId, runtime.dir);
7289
+ if (!record || record.pid === runtime.selfPid) return null;
7290
+ return runtime.isAlive(record.pid) ? record : null;
7291
+ }
7292
+ function writeSessionLock(happySessionId, record, dir) {
7293
+ mkdirSync(dir, { recursive: true, mode: 448 });
7294
+ const path = sessionLockPath(happySessionId, dir);
7295
+ const tmp = `${path}.${record.pid}.tmp`;
7296
+ writePrivateFileSync(tmp, JSON.stringify(record));
7297
+ renameSync(tmp, path);
7298
+ }
7299
+ function acquireSessionLock(happySessionId, options, runtime = defaultRuntime()) {
7300
+ const record = () => ({
7301
+ pid: runtime.selfPid,
7302
+ startedAt: runtime.now(),
7303
+ version: options.version,
7304
+ ...options.flavor ? { flavor: options.flavor } : {}
7305
+ });
7306
+ const holder = liveSessionLockHolder(happySessionId, runtime);
7307
+ if (!holder) {
7308
+ writeSessionLock(happySessionId, record(), runtime.dir);
7309
+ return Promise.resolve({ ok: true, replaced: null });
7310
+ }
7311
+ if (!options.takeover) {
7312
+ return Promise.resolve({ ok: false, holder });
7313
+ }
7314
+ return new Promise((resolve) => {
7315
+ const requested = terminateProcess(holder.pid, (stopped) => {
7316
+ if (!stopped) {
7317
+ resolve({ ok: false, holder });
7318
+ return;
7319
+ }
7320
+ writeSessionLock(happySessionId, record(), runtime.dir);
7321
+ resolve({ ok: true, replaced: holder });
7322
+ }, runtime, options.graceMs ?? 2e3);
7323
+ if (!requested) resolve({ ok: false, holder });
7324
+ });
7325
+ }
7326
+ function releaseSessionLock(happySessionId, runtime = defaultRuntime()) {
7327
+ try {
7328
+ const record = readSessionLock(happySessionId, runtime.dir);
7329
+ if (record && record.pid === runtime.selfPid) {
7330
+ unlinkSync(sessionLockPath(happySessionId, runtime.dir));
7331
+ }
7332
+ } catch {
7333
+ }
7334
+ }
7335
+ async function claimSessionOrExit(happySessionId, options) {
7336
+ const result = await acquireSessionLock(happySessionId, {
7337
+ takeover: options.takeover,
7338
+ version: configuration.currentCliVersion,
7339
+ flavor: options.flavor
7340
+ });
7341
+ if (!result.ok) {
7342
+ const message = `Session ${happySessionId} is already run by very-happy pid ${result.holder.pid} (v${result.holder.version || "?"}); this process yields.`;
7343
+ logger.debug(`[SESSION LOCK] ${message}`);
7344
+ console.error(message);
7345
+ process.exit(0);
7346
+ }
7347
+ if (result.replaced) {
7348
+ logger.debug(`[SESSION LOCK] Took over session ${happySessionId} from pid ${result.replaced.pid} (v${result.replaced.version || "?"})`);
7349
+ }
7350
+ process.on("exit", () => releaseSessionLock(happySessionId));
7351
+ }
7094
7352
 
7095
7353
  function shellescape(s) {
7096
7354
  return "'" + s.replace(/'/g, "'\\''") + "'";
@@ -7182,7 +7440,7 @@ async function startDaemon() {
7182
7440
  const pidToTrackedSession = /* @__PURE__ */ new Map();
7183
7441
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
7184
7442
  const persisted = readPersistedSessions();
7185
- const liveHappyPids = new Set((await findAllHappyProcesses()).map((entry) => entry.pid));
7443
+ const liveHappyProcesses = await findAllHappyProcesses();
7186
7444
  for (const [id, s] of Object.entries(persisted)) {
7187
7445
  const tracked = {
7188
7446
  startedBy: "persisted",
@@ -7197,12 +7455,20 @@ async function startDaemon() {
7197
7455
  },
7198
7456
  pid: 0
7199
7457
  };
7200
- const livePid = recoverableSessionPid(s.metadata, liveHappyPids);
7201
- if (livePid !== null) {
7458
+ const livePids = findSessionWrapperPids(s.metadata, liveHappyProcesses, { excludePid: process.pid, lockPid: readSessionLock(id)?.pid });
7459
+ if (livePids.length > 0) {
7460
+ const [livePid, ...duplicates] = livePids;
7202
7461
  tracked.pid = livePid;
7203
7462
  tracked.startedBy = "recovered after daemon restart";
7204
7463
  pidToTrackedSession.set(livePid, tracked);
7205
- persistSession(id, { ...s, savedAt: Date.now() });
7464
+ if (livePid !== s.metadata?.hostPid) {
7465
+ logger.debug(`[DAEMON RUN] Session ${id}: persisted hostPid ${s.metadata?.hostPid} is stale; adopted live wrapper ${livePid} by its --resume command line`);
7466
+ }
7467
+ for (const dup of duplicates) {
7468
+ logger.warn(`[DAEMON RUN] Session ${id} has a duplicate live wrapper ${dup} (kept ${livePid}); restart the session to collapse them`);
7469
+ pidToTrackedSession.set(dup, { ...tracked, pid: dup, startedBy: "recovered after daemon restart (duplicate wrapper)" });
7470
+ }
7471
+ persistSession(id, { ...s, metadata: { ...s.metadata, hostPid: livePid }, savedAt: Date.now() });
7206
7472
  } else {
7207
7473
  sessionIdToFinishedSession.set(id, tracked);
7208
7474
  }
@@ -7232,6 +7498,11 @@ async function startDaemon() {
7232
7498
  savedAt: Date.now()
7233
7499
  });
7234
7500
  }
7501
+ for (const [otherPid, other] of pidToTrackedSession.entries()) {
7502
+ if (other.happySessionId === sessionId && otherPid !== pid && isPidAlive(otherPid)) {
7503
+ logger.warn(`[DAEMON RUN] Session ${sessionId} now has two live wrappers (${otherPid} and ${pid}); restart the session to collapse them`);
7504
+ }
7505
+ }
7235
7506
  const existingSession = pidToTrackedSession.get(pid);
7236
7507
  if (existingSession && existingSession.startedBy === "daemon") {
7237
7508
  existingSession.happySessionId = sessionId;
@@ -7659,6 +7930,21 @@ async function startDaemon() {
7659
7930
  }
7660
7931
  return sessionIdToFinishedSession.get(happySessionId);
7661
7932
  };
7933
+ const persistRestoreRecord = (happySessionId, fallback, metadata) => {
7934
+ const candidates = [...pidToTrackedSession.values()].filter((s) => s.happySessionId === happySessionId && isPidAlive(s.pid));
7935
+ const live = candidates.find((s) => s.childProcess) ?? candidates[0];
7936
+ const encryption = live?.encryption ?? fallback.encryption;
7937
+ if (!encryption) return;
7938
+ persistSession(happySessionId, {
7939
+ encryptionKey: encodeBase64(encryption.encryptionKey),
7940
+ encryptionVariant: encryption.encryptionVariant,
7941
+ seq: encryption.seq,
7942
+ metadataVersion: encryption.metadataVersion,
7943
+ agentStateVersion: encryption.agentStateVersion,
7944
+ metadata: mergeRestoreMetadata(metadata, live?.happySessionMetadataFromLocalWebhook),
7945
+ savedAt: Date.now()
7946
+ });
7947
+ };
7662
7948
  const fetchServerSessionMetadata = async (sessionId, encryptionKey, encryptionVariant) => {
7663
7949
  try {
7664
7950
  const byId = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
@@ -7715,6 +8001,14 @@ async function startDaemon() {
7715
8001
  if (!tracked) {
7716
8002
  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.` };
7717
8003
  }
8004
+ const orphans = findSessionWrapperPids(tracked.happySessionMetadataFromLocalWebhook, await findAllHappyProcesses(), { excludePid: process.pid, lockPid: readSessionLock(happySessionId)?.pid }).filter((pid) => isPidAlive(pid) && !pidToTrackedSession.has(pid));
8005
+ if (orphans.length > 0) {
8006
+ for (const pid of orphans) {
8007
+ pidToTrackedSession.set(pid, { ...tracked, pid, startedBy: "adopted untracked wrapper" });
8008
+ }
8009
+ logger.debug(`[DAEMON RUN] resume ${happySessionId}: adopted untracked live wrapper(s) ${orphans.join(", ")} \u2014 idempotent success`);
8010
+ return { type: "success", sessionId: happySessionId };
8011
+ }
7718
8012
  if (!tracked.happySessionMetadataFromLocalWebhook) {
7719
8013
  return { type: "error", errorMessage: `resume-precheck:no-metadata: Session ${happySessionId} has no metadata. Cannot resume.` };
7720
8014
  }
@@ -7762,15 +8056,7 @@ async function startDaemon() {
7762
8056
  }
7763
8057
  });
7764
8058
  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
- });
8059
+ persistRestoreRecord(happySessionId, tracked, metadata);
7774
8060
  }
7775
8061
  return result;
7776
8062
  } catch (error) {
@@ -7783,17 +8069,18 @@ async function startDaemon() {
7783
8069
  }
7784
8070
  };
7785
8071
  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) => {
8072
+ const stopSessionAndWait = async (happySessionId, timeoutMs = 8e3) => {
8073
+ const targets = /* @__PURE__ */ new Set();
8074
+ for (const [pid, s] of pidToTrackedSession.entries()) {
8075
+ if (s.happySessionId === happySessionId && isPidAlive(pid)) targets.add(pid);
8076
+ }
8077
+ const known = findTrackedSessionById(happySessionId)?.happySessionMetadataFromLocalWebhook;
8078
+ for (const pid of findSessionWrapperPids(known, await findAllHappyProcesses(), { excludePid: process.pid, lockPid: readSessionLock(happySessionId)?.pid })) {
8079
+ if (isPidAlive(pid)) targets.add(pid);
8080
+ }
8081
+ if (targets.size === 0) return;
8082
+ logger.debug(`[DAEMON RUN] restart ${happySessionId}: stopping live wrapper(s) ${[...targets].join(", ")}`);
8083
+ await Promise.all([...targets].map((pid) => new Promise((resolve) => {
7797
8084
  let settled = false;
7798
8085
  const done = () => {
7799
8086
  if (!settled) {
@@ -7813,7 +8100,7 @@ async function startDaemon() {
7813
8100
  });
7814
8101
  if (!requested) done();
7815
8102
  setTimeout(done, timeoutMs).unref?.();
7816
- });
8103
+ })));
7817
8104
  };
7818
8105
  const restartSession = async (happySessionId, options) => {
7819
8106
  let gate = resumeGates.get(happySessionId);
@@ -7895,15 +8182,7 @@ async function startDaemon() {
7895
8182
  }
7896
8183
  });
7897
8184
  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
- });
8185
+ persistRestoreRecord(happySessionId, tracked, metadata);
7907
8186
  }
7908
8187
  return result;
7909
8188
  } catch (error) {
@@ -8074,6 +8353,7 @@ async function startDaemon() {
8074
8353
  resolveMirrorSessionId: (terminalId) => mirrorManager.resolveMirrorSessionId(terminalId),
8075
8354
  onTerminalClosed: (terminalId) => mirrorManager.onTerminalClosed(terminalId),
8076
8355
  onTerminalList: (terminals) => mirrorManager.observeTerminalList(terminals),
8356
+ onTerminalListTick: (terminals) => mirrorManager.reconcile(terminals),
8077
8357
  isMirrorInputAllowed: (terminalId) => mirrorManager.isMirrorInputAllowed(terminalId)
8078
8358
  });
8079
8359
  mirrorManager.restore().catch((error) => {
@@ -9671,6 +9951,7 @@ async function runClaude(credentials, options = {}) {
9671
9951
  process.exit(0);
9672
9952
  }
9673
9953
  logger.debug(`Session created: ${response.id}`);
9954
+ await claimSessionOrExit(response.id, { takeover: !!reconnectSessionId, flavor: "claude" });
9674
9955
  if (reconnectSessionId && !await api.reactivateSession(response.id)) {
9675
9956
  throw new Error(`Failed to reactivate archived session ${response.id}`);
9676
9957
  }
@@ -13185,6 +13466,9 @@ async function runCodex(opts) {
13185
13466
  } else {
13186
13467
  response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
13187
13468
  }
13469
+ if (response) {
13470
+ await claimSessionOrExit(response.id, { takeover: !!reconnectSessionId, flavor: "codex" });
13471
+ }
13188
13472
  if (reconnectSessionId && response && !await api.reactivateSession(response.id)) {
13189
13473
  throw new Error(`Failed to reactivate archived session ${response.id}`);
13190
13474
  }
@@ -13951,7 +14235,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
13951
14235
  process.exit(0);
13952
14236
  } else if (subcommand === "install-terminal-hooks") {
13953
14237
  try {
13954
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-DsECLFy3.mjs');
14238
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-FiujjEvb.mjs');
13955
14239
  const command = parseTerminalHooksArgs(args.slice(1));
13956
14240
  if (command.action === "help") {
13957
14241
  console.log(TERMINAL_HOOKS_HELP);
@@ -13968,7 +14252,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
13968
14252
  }
13969
14253
  } else if (subcommand === "spawn") {
13970
14254
  try {
13971
- const { handleSpawnCommand } = await import('./spawn-DgZUVvT2.mjs');
14255
+ const { handleSpawnCommand } = await import('./spawn-uTPD3qOt.mjs');
13972
14256
  await handleSpawnCommand(args.slice(1));
13973
14257
  } catch (error) {
13974
14258
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -13980,7 +14264,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
13980
14264
  return;
13981
14265
  } else if (subcommand === "send") {
13982
14266
  try {
13983
- const { handleSendCommand } = await import('./send-DlHES1EO.mjs');
14267
+ const { handleSendCommand } = await import('./send-CxLL0z9J.mjs');
13984
14268
  await handleSendCommand(args.slice(1));
13985
14269
  } catch (error) {
13986
14270
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -14086,9 +14370,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
14086
14370
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
14087
14371
  const projectId = args[3];
14088
14372
  try {
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; });
14373
+ const { saveGoogleCloudProjectToConfig } = await import('./config-2GD21yg_.mjs');
14374
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./types-BLE8Hfmh.mjs').then(function (n) { return n.al; });
14375
+ const { ApiClient: ApiClient2 } = await import('./types-BLE8Hfmh.mjs').then(function (n) { return n.am; });
14092
14376
  let userEmail = void 0;
14093
14377
  try {
14094
14378
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -14119,7 +14403,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
14119
14403
  }
14120
14404
  if (geminiSubcommand === "project" && args[2] === "get") {
14121
14405
  try {
14122
- const { readGeminiLocalConfig } = await import('./config-BJjvra5x.mjs');
14406
+ const { readGeminiLocalConfig } = await import('./config-2GD21yg_.mjs');
14123
14407
  const config = readGeminiLocalConfig();
14124
14408
  if (config.googleCloudProject) {
14125
14409
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -14159,7 +14443,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
14159
14443
  process.exit(0);
14160
14444
  }
14161
14445
  try {
14162
- const { runGemini } = await import('./runGemini-CrSCxKqI.mjs');
14446
+ const { runGemini } = await import('./runGemini-D_InkOhk.mjs');
14163
14447
  let startedBy = void 0;
14164
14448
  for (let i = 1; i < args.length; i++) {
14165
14449
  if (args[i] === "--started-by") {
@@ -14181,7 +14465,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
14181
14465
  return;
14182
14466
  } else if (subcommand === "acp") {
14183
14467
  try {
14184
- const { runAcp, resolveAcpAgentConfig } = await import('./index-D-U-7DAy.mjs');
14468
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-DZ0uw6ef.mjs');
14185
14469
  let startedBy = void 0;
14186
14470
  let verbose = false;
14187
14471
  const acpArgs = [];
@@ -14221,7 +14505,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
14221
14505
  return;
14222
14506
  } else if (subcommand === "openclaw") {
14223
14507
  try {
14224
- const { runOpenClaw } = await import('./runOpenClaw-Cnagj-hv.mjs');
14508
+ const { runOpenClaw } = await import('./runOpenClaw-BjebGmV6.mjs');
14225
14509
  let startedBy = void 0;
14226
14510
  let verbose = false;
14227
14511
  let gatewayUrl;
@@ -14272,7 +14556,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
14272
14556
  return;
14273
14557
  } else if (subcommand === "mcp" && args.length === 1) {
14274
14558
  try {
14275
- const { handleMcpCommand } = await import('./mcp-Bs6R9P7q.mjs');
14559
+ const { handleMcpCommand } = await import('./mcp-0mZcHPtz.mjs');
14276
14560
  await handleMcpCommand();
14277
14561
  } catch (error) {
14278
14562
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -1,10 +1,10 @@
1
- import { D as DefaultTransport, A as AcpBackend } from './AcpBackend-C5507ePk.mjs';
2
- export { a as DEFAULT_IDLE_TIMEOUT_MS, b as DEFAULT_TOOL_CALL_TIMEOUT_MS, e as extractErrorDetail, f as formatDuration, c as formatDurationMinutes, h as handleAgentMessageChunk, d as handleAgentThoughtChunk, g as handleLegacyMessageChunk, i as handlePlanUpdate, j as handleThinkingUpdate, k as handleToolCall, l as handleToolCallUpdate, p as parseArgsFromContent } from './AcpBackend-C5507ePk.mjs';
3
- import { h as connectionState, A as ApiClient, i as readSettings, j as encodeBase64, l as logger, p as projectPath } from './types-Cx5wn5Hf.mjs';
4
- import { A as AcpSessionManager } from './AcpSessionManager-Bd1trXWx.mjs';
1
+ import { D as DefaultTransport, A as AcpBackend } from './AcpBackend-Cuiff4Nu.mjs';
2
+ export { a as DEFAULT_IDLE_TIMEOUT_MS, b as DEFAULT_TOOL_CALL_TIMEOUT_MS, e as extractErrorDetail, f as formatDuration, c as formatDurationMinutes, h as handleAgentMessageChunk, d as handleAgentThoughtChunk, g as handleLegacyMessageChunk, i as handlePlanUpdate, j as handleThinkingUpdate, k as handleToolCall, l as handleToolCallUpdate, p as parseArgsFromContent } from './AcpBackend-Cuiff4Nu.mjs';
3
+ import { h as connectionState, A as ApiClient, i as readSettings, j as encodeBase64, l as logger, p as projectPath } from './types-BLE8Hfmh.mjs';
4
+ import { A as AcpSessionManager } from './AcpSessionManager-BU69q_vR.mjs';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { join } from 'node:path';
7
- import { i as initialMachineMetadata, d as createSessionMetadata, e as setupOfflineReconnection, n as notifyDaemonSessionStarted, M as MessageQueue2, h as hashObject, f as startHappyServer, r as registerKillSessionHandler, B as BasePermissionHandler } from './index-C00jxV83.mjs';
7
+ import { i as initialMachineMetadata, d as createSessionMetadata, e as setupOfflineReconnection, n as notifyDaemonSessionStarted, M as MessageQueue2, h as hashObject, f as startHappyServer, r as registerKillSessionHandler, B as BasePermissionHandler } from './index-DVEdpWq0.mjs';
8
8
  import 'node:child_process';
9
9
  import '@agentclientprotocol/sdk';
10
10
  import 'axios';
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  require('chalk');
4
- require('./index-DaVhaBF2.cjs');
5
- require('./types-BHVxYajT.cjs');
4
+ require('./index-BzbaMrIa.cjs');
5
+ require('./types-HRuZ13LS.cjs');
6
6
  require('zod');
7
7
  require('node:child_process');
8
8
  require('node:os');
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import 'chalk';
2
- import './index-C00jxV83.mjs';
3
- import './types-Cx5wn5Hf.mjs';
2
+ import './index-DVEdpWq0.mjs';
3
+ import './types-BLE8Hfmh.mjs';
4
4
  import 'zod';
5
5
  import 'node:child_process';
6
6
  import 'node:os';
@@ -5,8 +5,8 @@ var path = require('node:path');
5
5
  var os = require('node:os');
6
6
  var node_child_process = require('node:child_process');
7
7
  var chalk = require('chalk');
8
- var persistence = require('./types-BHVxYajT.cjs');
9
- var index = require('./index-DaVhaBF2.cjs');
8
+ var persistence = require('./types-HRuZ13LS.cjs');
9
+ var index = require('./index-BzbaMrIa.cjs');
10
10
  require('axios');
11
11
  require('node:util');
12
12
  require('node:fs/promises');
@@ -3,8 +3,8 @@ import { join, resolve } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import chalk from 'chalk';
6
- import { p as projectPath } from './types-Cx5wn5Hf.mjs';
7
- import { t as tmuxSupportsSessionEnv } from './index-C00jxV83.mjs';
6
+ import { p as projectPath } from './types-BLE8Hfmh.mjs';
7
+ import { t as tmuxSupportsSessionEnv } from './index-DVEdpWq0.mjs';
8
8
  import 'axios';
9
9
  import 'node:util';
10
10
  import 'node:fs/promises';