very-happy-cli 0.2.138 → 0.2.140

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 (45) hide show
  1. package/dist/{AcpBackend-BUoDXZnv.cjs → AcpBackend-C0Lq4uoA.cjs} +2 -2
  2. package/dist/{AcpBackend-bqVGMkPh.mjs → AcpBackend-DsSYq8T-.mjs} +2 -2
  3. package/dist/{AcpSessionManager-BBAQ8EQf.mjs → AcpSessionManager-BAErE9s0.mjs} +1 -1
  4. package/dist/{AcpSessionManager-DmmA52Ep.cjs → AcpSessionManager-DIqByTH7.cjs} +1 -1
  5. package/dist/{agentGuidance-4HV-BOge.cjs → agentGuidance-Dl3apItH.cjs} +1 -1
  6. package/dist/{agentGuidance-WHZsBKpm.mjs → agentGuidance-xk3v4_7E.mjs} +1 -1
  7. package/dist/codex/happyMcpStdioBridge.cjs +2 -2
  8. package/dist/codex/happyMcpStdioBridge.mjs +2 -2
  9. package/dist/{config-BRXqgJJj.mjs → config-BVoHul9V.mjs} +4 -4
  10. package/dist/{config-DpuZN2Rs.cjs → config-D0-jnCOC.cjs} +4 -4
  11. package/dist/{index-DQe7ljLR.mjs → index-BNydKYvS.mjs} +7 -7
  12. package/dist/{index-D8Q3OhdR.mjs → index-BPnAbcz_.mjs} +4 -4
  13. package/dist/{index-CHP265gs.cjs → index-BuQn9kUK.cjs} +4 -4
  14. package/dist/{index-Dbl5U_N0.mjs → index-D0IP7gXG.mjs} +116 -232
  15. package/dist/{index-3N-qu-RG.cjs → index-DJvzGXVN.cjs} +7 -7
  16. package/dist/{index-CRRK5P1r.cjs → index-__h-pCTK.cjs} +147 -268
  17. package/dist/index.cjs +4 -4
  18. package/dist/index.mjs +4 -4
  19. package/dist/{installTerminalHooks-DfUGgeyb.mjs → installTerminalHooks-D2ice1a-.mjs} +4 -4
  20. package/dist/{installTerminalHooks-CWGkoWDD.cjs → installTerminalHooks-D3jfvbyY.cjs} +4 -4
  21. package/dist/lib.cjs +2 -2
  22. package/dist/lib.d.cts +21 -0
  23. package/dist/lib.d.mts +21 -0
  24. package/dist/lib.mjs +2 -2
  25. package/dist/{limits-CfzVummd.cjs → limits-Dbip6eVO.cjs} +26 -7
  26. package/dist/{limits-DEsg_gHi.mjs → limits-nhD0Wran.mjs} +26 -8
  27. package/dist/{mcp-CN2UVfut.cjs → mcp-DeapmTWN.cjs} +21 -21
  28. package/dist/{mcp-UnjB3I8_.mjs → mcp-V26C05fN.mjs} +19 -19
  29. package/dist/{runGemini-BUVjTJKn.cjs → runGemini-D34eCFpz.cjs} +7 -7
  30. package/dist/{runGemini-BZ0xXRj-.mjs → runGemini-sa5kAgPe.mjs} +6 -6
  31. package/dist/{runOpenClaw-Bj2EgRHB.cjs → runOpenClaw-B8F8D--R.cjs} +6 -6
  32. package/dist/{runOpenClaw-BcUmnBpW.mjs → runOpenClaw-Bdsq8KBa.mjs} +5 -5
  33. package/dist/{send-BkypdBpK.cjs → send-DP_S3wn0.cjs} +4 -4
  34. package/dist/{send-snK_NGAA.mjs → send-KICC_ACz.mjs} +4 -4
  35. package/dist/{sessions-DxlC4C4g.mjs → sessions-C6EvnC4B.mjs} +4 -4
  36. package/dist/{sessions-9A-uoJL_.cjs → sessions-CGBBgVZn.cjs} +4 -4
  37. package/dist/{spawn-BORLpj60.cjs → spawn-BhUK0Vt2.cjs} +8 -8
  38. package/dist/{spawn-DFfHCafP.mjs → spawn-C1OqXJNb.mjs} +6 -6
  39. package/dist/{teams-9x2FO9RW.cjs → teams-C29nEHFD.cjs} +4 -4
  40. package/dist/{teams-Cfr-2Yd-.mjs → teams-Dm5HGpNz.mjs} +4 -4
  41. package/dist/{todo-CU5cjQZU.mjs → todo-C-vovA0N.mjs} +3 -3
  42. package/dist/{todo-DydKcknX.cjs → todo-CeJX0z_2.cjs} +3 -3
  43. package/dist/{types-CRxf_3n8.cjs → types-DCVS050m.cjs} +571 -13
  44. package/dist/{types-D5EsIAOe.mjs → types-thyvQB2a.mjs} +558 -13
  45. package/package.json +7 -7
@@ -1,5 +1,5 @@
1
1
  import axios from 'axios';
2
- import { l as logger, i as encodeBase64, j as encrypt, k as decrypt, m as decodeBase64, c as configuration, o as decryptBlob, q as prepareClipboardText, g as readSettings, r as readPersistedSessions, s as credentialRelayProblem, p as packageJson, t as readTrackedClaudeSessionIds, u as libsodiumEncryptForPublicKey, v as libsodiumPublicKeyFromSecretKey, x as getRandomBytes, y as redactLogValue } from './limits-DEsg_gHi.mjs';
2
+ import { l as logger, i as encodeBase64, j as encrypt, k as decrypt, m as decodeBase64, c as configuration, o as readDaemonState, q as clearDaemonState, s as decryptBlob, t as prepareClipboardText, g as readSettings, r as readPersistedSessions, u as credentialRelayProblem, p as packageJson, v as readTrackedClaudeSessionIds, x as readTrackedCodexThreadIds, y as libsodiumEncryptForPublicKey, z as libsodiumPublicKeyFromSecretKey, A as getRandomBytes, B as redactLogValue } from './limits-nhD0Wran.mjs';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { io } from 'socket.io-client';
5
5
  import * as z from 'zod';
@@ -982,6 +982,15 @@ z$1.object({
982
982
  rpcAvailable: z$1.boolean(),
983
983
  detectedAt: z$1.number()
984
984
  }).optional(),
985
+ /**
986
+ * B-464 capability flag: this daemon answers `codex-list-history` /
987
+ * `codex-import-session` (import a Codex CLI / desktop thread that was never
988
+ * started through very-happy). Same trust rule (`detectedAt >= startedAt`).
989
+ */
990
+ codexHistory: z$1.object({
991
+ rpcAvailable: z$1.boolean(),
992
+ detectedAt: z$1.number()
993
+ }).optional(),
985
994
  /** Relay-owned CLI compatibility/update policy last checked by this daemon. */
986
995
  cliUpdate: CliUpdateStateSchema.optional(),
987
996
  /**
@@ -2889,6 +2898,268 @@ const MAX_CHAT_ATTACHMENT_SOURCE_BYTES = 50 * 1024 * 1024;
2889
2898
  const CHAT_ATTACHMENT_ENCRYPTION_OVERHEAD_BYTES = 40;
2890
2899
  const MAX_CHAT_ATTACHMENT_ENCRYPTED_BYTES = MAX_CHAT_ATTACHMENT_SOURCE_BYTES + CHAT_ATTACHMENT_ENCRYPTION_OVERHEAD_BYTES;
2891
2900
 
2901
+ const TURN_LEASE_MS = 6e4;
2902
+ const TURN_BUSY_TTL_MS = 15e4;
2903
+ class TurnActivityTracker {
2904
+ constructor(ttlMs = TURN_BUSY_TTL_MS) {
2905
+ this.ttlMs = ttlMs;
2906
+ }
2907
+ busy = /* @__PURE__ */ new Map();
2908
+ apply(sessionId, event, now = Date.now()) {
2909
+ if (event === "turn_started") this.busy.set(sessionId, now);
2910
+ else this.busy.delete(sessionId);
2911
+ }
2912
+ /** The wrapper is gone: whatever it reported no longer holds. */
2913
+ forget(sessionId) {
2914
+ this.busy.delete(sessionId);
2915
+ }
2916
+ /** Sessions whose last renewal is still inside the TTL. */
2917
+ activeSessions(now = Date.now()) {
2918
+ const out = [];
2919
+ for (const [id, at] of this.busy) {
2920
+ if (now - at <= this.ttlMs) out.push(id);
2921
+ else this.busy.delete(id);
2922
+ }
2923
+ return out;
2924
+ }
2925
+ hasActiveTurn(now = Date.now()) {
2926
+ return this.activeSessions(now).length > 0;
2927
+ }
2928
+ }
2929
+ class TurnReporter {
2930
+ constructor(send, leaseMs = TURN_LEASE_MS) {
2931
+ this.send = send;
2932
+ this.leaseMs = leaseMs;
2933
+ }
2934
+ thinking = false;
2935
+ lastStartedAt = 0;
2936
+ /** Called on every keepAlive tick; emits only on an edge or a due renewal. */
2937
+ observe(thinking, now = Date.now()) {
2938
+ if (thinking) {
2939
+ const due = !this.thinking || now - this.lastStartedAt >= this.leaseMs;
2940
+ this.thinking = true;
2941
+ if (!due) return null;
2942
+ this.lastStartedAt = now;
2943
+ this.send("turn_started");
2944
+ return "turn_started";
2945
+ }
2946
+ if (!this.thinking) return null;
2947
+ this.thinking = false;
2948
+ this.lastStartedAt = 0;
2949
+ this.send("turn_ended");
2950
+ return "turn_ended";
2951
+ }
2952
+ }
2953
+
2954
+ function daemonControlHeaders(controlToken) {
2955
+ return {
2956
+ "Content-Type": "application/json",
2957
+ ...controlToken ? { Authorization: `Bearer ${controlToken}` } : {}
2958
+ };
2959
+ }
2960
+ async function daemonPost(path, body) {
2961
+ const state = await readDaemonState();
2962
+ if (!state?.httpPort) {
2963
+ const errorMessage = "No daemon running, no state file found";
2964
+ logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
2965
+ return {
2966
+ error: errorMessage
2967
+ };
2968
+ }
2969
+ try {
2970
+ process.kill(state.pid, 0);
2971
+ } catch (error) {
2972
+ const errorMessage = "Daemon is not running, file is stale";
2973
+ logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
2974
+ return {
2975
+ error: errorMessage
2976
+ };
2977
+ }
2978
+ try {
2979
+ const timeout = process.env.HAPPY_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPPY_DAEMON_HTTP_TIMEOUT) : 1e4;
2980
+ const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
2981
+ method: "POST",
2982
+ // A state written by an old daemon has no token and must remain callable;
2983
+ // a new daemon always writes one and rejects requests without it.
2984
+ headers: daemonControlHeaders(state.controlToken),
2985
+ body: JSON.stringify(body || {}),
2986
+ // Mostly increased for stress test
2987
+ signal: AbortSignal.timeout(timeout)
2988
+ });
2989
+ if (!response.ok) {
2990
+ const failure = await response.json().catch(() => null);
2991
+ const reason = typeof failure?.error === "string" && failure.error ? failure.error : `HTTP ${response.status}`;
2992
+ const errorMessage = `Request failed: ${path}, ${reason}`;
2993
+ logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
2994
+ return {
2995
+ error: errorMessage
2996
+ };
2997
+ }
2998
+ return await response.json();
2999
+ } catch (error) {
3000
+ const errorMessage = `Request failed: ${path}, ${error instanceof Error ? error.message : "Unknown error"}`;
3001
+ logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
3002
+ return {
3003
+ error: errorMessage
3004
+ };
3005
+ }
3006
+ }
3007
+ const SESSION_STARTED_RETRY_TIMEOUT_MS = 3e3;
3008
+ const SESSION_STARTED_RETRY_INTERVAL_MS = 100;
3009
+ async function notifyDaemonSessionStarted(sessionId, metadata, encryption) {
3010
+ const payload = { sessionId, metadata, encryption };
3011
+ const deadline = Date.now() + SESSION_STARTED_RETRY_TIMEOUT_MS;
3012
+ let result;
3013
+ while (true) {
3014
+ result = await daemonPost("/session-started", payload);
3015
+ if (!result?.error) {
3016
+ return result;
3017
+ }
3018
+ if (Date.now() >= deadline) {
3019
+ return result;
3020
+ }
3021
+ await new Promise((resolve) => setTimeout(resolve, SESSION_STARTED_RETRY_INTERVAL_MS));
3022
+ }
3023
+ }
3024
+ async function listDaemonSessions() {
3025
+ const result = await daemonPost("/list");
3026
+ return result.children || [];
3027
+ }
3028
+ async function stopDaemonSession(sessionId) {
3029
+ const result = await daemonPost("/stop-session", { sessionId });
3030
+ return result.success || false;
3031
+ }
3032
+ async function spawnDaemonSession(directory, sessionId, opts) {
3033
+ const result = await daemonPost("/spawn-session", {
3034
+ directory,
3035
+ sessionId,
3036
+ spawnedBy: opts?.spawnedBy,
3037
+ permissionMode: opts?.permissionMode,
3038
+ agent: opts?.agent,
3039
+ environmentVariables: opts?.environmentVariables
3040
+ });
3041
+ return result;
3042
+ }
3043
+ async function notifyDaemonSessionEvent(sessionId, event, spawnedBy) {
3044
+ return daemonPost("/session-event", { sessionId, event, spawnedBy });
3045
+ }
3046
+ async function notifyDaemonClaudeAuthFailed(sessionId) {
3047
+ return daemonPost("/session-event", { sessionId, event: "auth_failed" });
3048
+ }
3049
+ async function notifyDaemonTurnEvent(sessionId, event) {
3050
+ return daemonPost("/session-event", { sessionId, event });
3051
+ }
3052
+ async function stopDaemonHttp() {
3053
+ await daemonPost("/stop");
3054
+ }
3055
+ async function pushClipboardViaDaemon(text) {
3056
+ const result = await daemonPost("/clipboard", { text });
3057
+ if (result?.error && result.delivered === void 0) {
3058
+ return { delivered: false, error: result.error };
3059
+ }
3060
+ return result;
3061
+ }
3062
+ async function setTerminalTitleViaDaemon(terminalId, title, ifAbsent = false) {
3063
+ const result = await daemonPost("/terminal-title", { terminalId, title, ifAbsent });
3064
+ if (result?.error) {
3065
+ return { ok: false, error: result.error };
3066
+ }
3067
+ return { ok: result?.status === "ok", error: result?.status === "ok" ? void 0 : "unexpected daemon response" };
3068
+ }
3069
+ async function checkIfDaemonRunningAndCleanupStaleState() {
3070
+ const state = await readDaemonState();
3071
+ if (!state) {
3072
+ return false;
3073
+ }
3074
+ try {
3075
+ process.kill(state.pid, 0);
3076
+ } catch {
3077
+ logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
3078
+ await cleanupDaemonState();
3079
+ return false;
3080
+ }
3081
+ if (state.httpPort) {
3082
+ try {
3083
+ const response = await fetch(`http://127.0.0.1:${state.httpPort}/list`, {
3084
+ method: "POST",
3085
+ headers: daemonControlHeaders(state.controlToken),
3086
+ body: "{}",
3087
+ signal: AbortSignal.timeout(2e3)
3088
+ });
3089
+ if (response.ok) {
3090
+ return true;
3091
+ }
3092
+ } catch {
3093
+ logger.debug(`[DAEMON RUN] PID ${state.pid} is alive but HTTP health check failed on port ${state.httpPort}, cleaning up stale state`);
3094
+ await cleanupDaemonState();
3095
+ return false;
3096
+ }
3097
+ }
3098
+ return true;
3099
+ }
3100
+ async function isDaemonRunningCurrentlyInstalledHappyVersion() {
3101
+ logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
3102
+ const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
3103
+ if (!runningDaemon) {
3104
+ logger.debug("[DAEMON CONTROL] No daemon running, returning false");
3105
+ return false;
3106
+ }
3107
+ const state = await readDaemonState();
3108
+ if (!state) {
3109
+ logger.debug("[DAEMON CONTROL] No daemon state found, returning false");
3110
+ return false;
3111
+ }
3112
+ const currentCliVersion = configuration.currentCliVersion;
3113
+ logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
3114
+ return currentCliVersion === state.startedWithCliVersion;
3115
+ }
3116
+ async function cleanupDaemonState() {
3117
+ try {
3118
+ await clearDaemonState();
3119
+ logger.debug("[DAEMON RUN] Daemon state file removed");
3120
+ } catch (error) {
3121
+ logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
3122
+ }
3123
+ }
3124
+ async function stopDaemon() {
3125
+ try {
3126
+ const state = await readDaemonState();
3127
+ if (!state) {
3128
+ logger.debug("No daemon state found");
3129
+ return;
3130
+ }
3131
+ logger.debug(`Stopping daemon with PID ${state.pid}`);
3132
+ try {
3133
+ await stopDaemonHttp();
3134
+ await waitForProcessDeath(state.pid, 2e3);
3135
+ logger.debug("Daemon stopped gracefully via HTTP");
3136
+ return;
3137
+ } catch (error) {
3138
+ logger.debug("HTTP stop failed, will force kill", error);
3139
+ }
3140
+ try {
3141
+ process.kill(state.pid, "SIGKILL");
3142
+ logger.debug("Force killed daemon");
3143
+ } catch (error) {
3144
+ logger.debug("Daemon already dead");
3145
+ }
3146
+ } catch (error) {
3147
+ logger.debug("Error stopping daemon", error);
3148
+ }
3149
+ }
3150
+ async function waitForProcessDeath(pid, timeout) {
3151
+ const start = Date.now();
3152
+ while (Date.now() - start < timeout) {
3153
+ try {
3154
+ process.kill(pid, 0);
3155
+ await new Promise((resolve) => setTimeout(resolve, 100));
3156
+ } catch {
3157
+ return;
3158
+ }
3159
+ }
3160
+ throw new Error("Process did not die within timeout");
3161
+ }
3162
+
2892
3163
  class ApiSessionClient extends EventEmitter {
2893
3164
  token;
2894
3165
  sessionId;
@@ -3670,10 +3941,14 @@ class ApiSessionClient extends EventEmitter {
3670
3941
  /**
3671
3942
  * Send a ping message to keep the connection alive
3672
3943
  */
3944
+ /** B-466: turn edges (and a periodic lease) reported to the local daemon so
3945
+ * auto-update waits for "no turn in flight" instead of "no sessions". */
3946
+ turnReporter = new TurnReporter((event) => void notifyDaemonTurnEvent(this.sessionId, event).catch((error) => logger.debug("[API] Failed to report turn event to daemon:", error)));
3673
3947
  keepAlive(thinking, mode) {
3674
3948
  if (process.env.DEBUG) {
3675
3949
  logger.debug(`[API] Sending keep alive message: ${thinking}`);
3676
3950
  }
3951
+ this.turnReporter.observe(thinking);
3677
3952
  this.socket.volatile.emit("session-alive", {
3678
3953
  sid: this.sessionId,
3679
3954
  time: Date.now(),
@@ -12362,7 +12637,7 @@ function isVeryHappyOneShotPrompt(firstPrompt) {
12362
12637
  return VERY_HAPPY_ONE_SHOT_PROMPT_PREFIXES.some((prefix) => text.startsWith(prefix));
12363
12638
  }
12364
12639
 
12365
- const UUID_RE$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12640
+ const UUID_RE$2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12366
12641
  async function listClaudeProjectDirs(projectsRoot) {
12367
12642
  let names;
12368
12643
  try {
@@ -12381,7 +12656,7 @@ async function listClaudeProjectDirs(projectsRoot) {
12381
12656
  }
12382
12657
  return dirs;
12383
12658
  }
12384
- async function collectCandidates(projectDirs, exclude) {
12659
+ async function collectCandidates$1(projectDirs, exclude) {
12385
12660
  const out = [];
12386
12661
  for (const dir of projectDirs) {
12387
12662
  let names;
@@ -12393,7 +12668,7 @@ async function collectCandidates(projectDirs, exclude) {
12393
12668
  for (const name of names) {
12394
12669
  if (!name.endsWith(".jsonl")) continue;
12395
12670
  const id = name.slice(0, -".jsonl".length);
12396
- if (!UUID_RE$1.test(id) || exclude.has(id.toLowerCase())) continue;
12671
+ if (!UUID_RE$2.test(id) || exclude.has(id.toLowerCase())) continue;
12397
12672
  const path = join$1(dir, name);
12398
12673
  try {
12399
12674
  const s = await stat$1(path);
@@ -12412,7 +12687,7 @@ async function collectCandidates(projectDirs, exclude) {
12412
12687
  return true;
12413
12688
  });
12414
12689
  }
12415
- async function readHead(path, bytes) {
12690
+ async function readHead$1(path, bytes) {
12416
12691
  const fh = await open(path, "r");
12417
12692
  try {
12418
12693
  const buffer = Buffer.alloc(bytes);
@@ -12422,11 +12697,11 @@ async function readHead(path, bytes) {
12422
12697
  await fh.close();
12423
12698
  }
12424
12699
  }
12425
- function oneLine(text, max) {
12700
+ function oneLine$1(text, max) {
12426
12701
  const collapsed = text.replace(/\s+/g, " ").trim();
12427
12702
  return collapsed.length > max ? `${collapsed.slice(0, max - 1)}\u2026` : collapsed;
12428
12703
  }
12429
- function parseTimestamp(raw) {
12704
+ function parseTimestamp$1(raw) {
12430
12705
  if (typeof raw === "number" && Number.isFinite(raw)) return raw;
12431
12706
  if (typeof raw === "string") {
12432
12707
  const t = Date.parse(raw);
@@ -12453,7 +12728,7 @@ function parseClaudeHistoryHead(head, promptChars) {
12453
12728
  }
12454
12729
  if (!parsed || typeof parsed !== "object") continue;
12455
12730
  if (parsed.type === "summary" && typeof parsed.summary === "string" && !summary) {
12456
- const s = oneLine(parsed.summary, promptChars);
12731
+ const s = oneLine$1(parsed.summary, promptChars);
12457
12732
  if (s) summary = s;
12458
12733
  continue;
12459
12734
  }
@@ -12461,7 +12736,7 @@ function parseClaudeHistoryHead(head, promptChars) {
12461
12736
  if (typeof parsed.entrypoint === "string" && parsed.entrypoint && !entrypoint) entrypoint = parsed.entrypoint;
12462
12737
  if (typeof parsed.gitBranch === "string" && parsed.gitBranch && !gitBranch) gitBranch = parsed.gitBranch;
12463
12738
  if (typeof parsed.version === "string" && parsed.version && !version) version = parsed.version;
12464
- if (startedAt === null) startedAt = parseTimestamp(parsed.timestamp);
12739
+ if (startedAt === null) startedAt = parseTimestamp$1(parsed.timestamp);
12465
12740
  if (!firstPrompt && parsed.type === "user" && !parsed.isSidechain) {
12466
12741
  const content = parsed.message?.content;
12467
12742
  let text = null;
@@ -12473,7 +12748,7 @@ function parseClaudeHistoryHead(head, promptChars) {
12473
12748
  }
12474
12749
  if (text) {
12475
12750
  if (isVeryHappyOneShotPrompt(text)) return null;
12476
- const cleaned = oneLine(stripHarnessTags(text), promptChars);
12751
+ const cleaned = oneLine$1(stripHarnessTags(text), promptChars);
12477
12752
  if (cleaned) firstPrompt = cleaned;
12478
12753
  }
12479
12754
  }
@@ -12499,7 +12774,7 @@ async function listClaudeSessionHistory(options) {
12499
12774
  const headBytes = Math.max(4096, options.headBytes ?? 64 * 1024);
12500
12775
  const promptChars = Math.max(20, options.promptChars ?? 200);
12501
12776
  const exclude = new Set(Array.from(options.exclude ?? [], (id) => id.toLowerCase()));
12502
- const candidates = await collectCandidates(options.projectDirs, exclude);
12777
+ const candidates = await collectCandidates$1(options.projectDirs, exclude);
12503
12778
  const entries = [];
12504
12779
  let scanned = 0;
12505
12780
  let truncated = false;
@@ -12511,7 +12786,7 @@ async function listClaudeSessionHistory(options) {
12511
12786
  scanned += 1;
12512
12787
  let head;
12513
12788
  try {
12514
- head = await readHead(candidate.path, headBytes);
12789
+ head = await readHead$1(candidate.path, headBytes);
12515
12790
  } catch {
12516
12791
  continue;
12517
12792
  }
@@ -13950,6 +14225,235 @@ class CodexAppServerClient {
13950
14225
  }
13951
14226
  }
13952
14227
 
14228
+ const UUID_RE$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14229
+ const ROLLOUT_RE = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
14230
+ const VERY_HAPPY_CODEX_ORIGINATOR = "happy-codex";
14231
+ function getCodexHomeDir() {
14232
+ return process.env.CODEX_HOME || join$1(homedir(), ".codex");
14233
+ }
14234
+ function getCodexSessionsRoot(codexHome = getCodexHomeDir()) {
14235
+ return join$1(codexHome, "sessions");
14236
+ }
14237
+ function getCodexSessionIndexPath(codexHome = getCodexHomeDir()) {
14238
+ return join$1(codexHome, "session_index.jsonl");
14239
+ }
14240
+ async function collectCandidates(root, exclude) {
14241
+ const out = [];
14242
+ async function walk(dir, depth) {
14243
+ let names;
14244
+ try {
14245
+ names = await readdir$1(dir);
14246
+ } catch {
14247
+ return;
14248
+ }
14249
+ for (const name of names) {
14250
+ const full = join$1(dir, name);
14251
+ const match = ROLLOUT_RE.exec(name);
14252
+ if (match) {
14253
+ const id = match[1].toLowerCase();
14254
+ if (exclude.has(id)) continue;
14255
+ try {
14256
+ const s = await stat$1(full);
14257
+ if (!s.isFile() || s.size === 0) continue;
14258
+ out.push({ path: full, id, updatedAt: s.mtimeMs, sizeBytes: s.size });
14259
+ } catch {
14260
+ }
14261
+ continue;
14262
+ }
14263
+ if (name.endsWith(".jsonl") || depth >= 4) continue;
14264
+ try {
14265
+ if ((await stat$1(full)).isDirectory()) await walk(full, depth + 1);
14266
+ } catch {
14267
+ }
14268
+ }
14269
+ }
14270
+ await walk(root, 0);
14271
+ out.sort((a, b) => b.updatedAt - a.updatedAt);
14272
+ const seen = /* @__PURE__ */ new Set();
14273
+ return out.filter((c) => {
14274
+ if (seen.has(c.id)) return false;
14275
+ seen.add(c.id);
14276
+ return true;
14277
+ });
14278
+ }
14279
+ async function readHead(path, bytes) {
14280
+ const fh = await open(path, "r");
14281
+ try {
14282
+ const buffer = Buffer.alloc(bytes);
14283
+ const { bytesRead } = await fh.read(buffer, 0, bytes, 0);
14284
+ return buffer.subarray(0, bytesRead).toString("utf-8");
14285
+ } finally {
14286
+ await fh.close();
14287
+ }
14288
+ }
14289
+ function oneLine(text, max) {
14290
+ const collapsed = text.replace(/\s+/g, " ").trim();
14291
+ return collapsed.length > max ? `${collapsed.slice(0, max - 1)}\u2026` : collapsed;
14292
+ }
14293
+ function parseTimestamp(raw) {
14294
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
14295
+ if (typeof raw === "string") {
14296
+ const t = Date.parse(raw);
14297
+ if (Number.isFinite(t)) return t;
14298
+ }
14299
+ return null;
14300
+ }
14301
+ function isHarnessUserText(text) {
14302
+ const t = text.trimStart();
14303
+ return t.startsWith("# AGENTS.md instructions") || t.startsWith("<environment_context>") || t.startsWith("<user_instructions>") || t.startsWith("<INSTRUCTIONS>");
14304
+ }
14305
+ function stripHarnessBlocks(text) {
14306
+ return text.replace(/<(environment_context|user_instructions|INSTRUCTIONS|turn_aborted|system-reminder)>[\s\S]*?<\/\1>/g, " ").replace(/^# AGENTS\.md instructions[^\n]*\n/m, " ");
14307
+ }
14308
+ function textFromUserContent(content) {
14309
+ if (typeof content === "string") return content;
14310
+ if (!Array.isArray(content)) return null;
14311
+ const parts = [];
14312
+ for (const part of content) {
14313
+ if (!part || typeof part !== "object") continue;
14314
+ const type = part.type;
14315
+ const text = part.text;
14316
+ if ((type === "input_text" || type === "text") && typeof text === "string") parts.push(text);
14317
+ }
14318
+ return parts.length > 0 ? parts.join("\n") : null;
14319
+ }
14320
+ function parseCodexHistoryHead(head, promptChars) {
14321
+ let id;
14322
+ let cwd;
14323
+ let firstPrompt;
14324
+ let fallbackPrompt;
14325
+ let startedAt = null;
14326
+ let entrypoint;
14327
+ let originator;
14328
+ let gitBranch;
14329
+ let version;
14330
+ let sawMeta = false;
14331
+ for (const line of head.split("\n")) {
14332
+ if (line.length === 0) continue;
14333
+ let parsed;
14334
+ try {
14335
+ parsed = JSON.parse(line);
14336
+ } catch {
14337
+ continue;
14338
+ }
14339
+ if (!parsed || typeof parsed !== "object") continue;
14340
+ const payload = parsed.payload;
14341
+ if (parsed.type === "session_meta" && payload && typeof payload === "object") {
14342
+ sawMeta = true;
14343
+ if (typeof payload.id === "string" && UUID_RE$1.test(payload.id)) id = payload.id.toLowerCase();
14344
+ if (typeof payload.cwd === "string" && payload.cwd) cwd = payload.cwd;
14345
+ if (typeof payload.source === "string" && payload.source) entrypoint = payload.source;
14346
+ if (typeof payload.originator === "string" && payload.originator) originator = payload.originator;
14347
+ if (typeof payload.cli_version === "string" && payload.cli_version) version = payload.cli_version;
14348
+ const branch = payload.git && typeof payload.git === "object" ? payload.git.branch : void 0;
14349
+ if (typeof branch === "string" && branch) gitBranch = branch;
14350
+ startedAt = parseTimestamp(payload.timestamp) ?? parseTimestamp(parsed.timestamp);
14351
+ continue;
14352
+ }
14353
+ if (!sawMeta) return null;
14354
+ if (firstPrompt) break;
14355
+ if (!payload || typeof payload !== "object") continue;
14356
+ if (parsed.type === "event_msg") {
14357
+ if (payload.type === "user_message" && typeof payload.message === "string") {
14358
+ const cleaned = oneLine(stripHarnessBlocks(payload.message), promptChars);
14359
+ if (cleaned) firstPrompt = cleaned;
14360
+ } else if (payload.type === "item_completed" && payload.item && payload.item.type === "UserMessage") {
14361
+ const text = textFromUserContent(payload.item.content);
14362
+ const cleaned = text ? oneLine(stripHarnessBlocks(text), promptChars) : "";
14363
+ if (cleaned) firstPrompt = cleaned;
14364
+ }
14365
+ continue;
14366
+ }
14367
+ if (parsed.type === "response_item" && payload.type === "message" && payload.role === "user" && !fallbackPrompt) {
14368
+ const text = textFromUserContent(payload.content);
14369
+ if (text && !isHarnessUserText(text)) {
14370
+ const cleaned = oneLine(stripHarnessBlocks(text), promptChars);
14371
+ if (cleaned) fallbackPrompt = cleaned;
14372
+ }
14373
+ }
14374
+ }
14375
+ if (!sawMeta || !cwd) return null;
14376
+ const prompt = firstPrompt ?? fallbackPrompt;
14377
+ if (!prompt) return null;
14378
+ return {
14379
+ ...id ? { id } : {},
14380
+ cwd,
14381
+ firstPrompt: prompt,
14382
+ startedAt: startedAt ?? 0,
14383
+ ...entrypoint ? { entrypoint } : {},
14384
+ ...originator ? { originator } : {},
14385
+ ...gitBranch ? { gitBranch } : {},
14386
+ ...version ? { version } : {}
14387
+ };
14388
+ }
14389
+ function parseCodexSessionIndex(text) {
14390
+ const names = /* @__PURE__ */ new Map();
14391
+ for (const line of text.split("\n")) {
14392
+ if (!line.trim()) continue;
14393
+ let parsed;
14394
+ try {
14395
+ parsed = JSON.parse(line);
14396
+ } catch {
14397
+ continue;
14398
+ }
14399
+ if (!parsed || typeof parsed !== "object") continue;
14400
+ if (typeof parsed.id !== "string" || !UUID_RE$1.test(parsed.id)) continue;
14401
+ if (typeof parsed.thread_name !== "string" || !parsed.thread_name.trim()) continue;
14402
+ names.set(parsed.id.toLowerCase(), parsed.thread_name.trim());
14403
+ }
14404
+ return names;
14405
+ }
14406
+ async function readSessionIndex(path) {
14407
+ if (!path) return /* @__PURE__ */ new Map();
14408
+ try {
14409
+ return parseCodexSessionIndex(await readFile$1(path, "utf-8"));
14410
+ } catch {
14411
+ return /* @__PURE__ */ new Map();
14412
+ }
14413
+ }
14414
+ async function listCodexSessionHistory(options) {
14415
+ const limit = Math.max(1, Math.min(options.limit ?? 60, 500));
14416
+ const headBytes = Math.max(4096, options.headBytes ?? 256 * 1024);
14417
+ const promptChars = Math.max(20, options.promptChars ?? 200);
14418
+ const exclude = new Set(Array.from(options.exclude ?? [], (id) => id.toLowerCase()));
14419
+ const [candidates, names] = await Promise.all([
14420
+ collectCandidates(options.sessionsRoot, exclude),
14421
+ readSessionIndex(options.sessionIndexPath)
14422
+ ]);
14423
+ const entries = [];
14424
+ let scanned = 0;
14425
+ let truncated = false;
14426
+ for (const candidate of candidates) {
14427
+ if (entries.length >= limit) {
14428
+ truncated = true;
14429
+ break;
14430
+ }
14431
+ scanned += 1;
14432
+ let head;
14433
+ try {
14434
+ head = await readHead(candidate.path, headBytes);
14435
+ } catch {
14436
+ continue;
14437
+ }
14438
+ const parsed = parseCodexHistoryHead(head, promptChars);
14439
+ if (!parsed) continue;
14440
+ if (parsed.originator === VERY_HAPPY_CODEX_ORIGINATOR) continue;
14441
+ if (options.directory && parsed.cwd !== options.directory) continue;
14442
+ const codexThreadId = parsed.id ?? candidate.id;
14443
+ if (exclude.has(codexThreadId)) continue;
14444
+ const { id: _id, ...rest } = parsed;
14445
+ const summary = names.get(codexThreadId);
14446
+ entries.push({
14447
+ codexThreadId,
14448
+ updatedAt: Math.round(candidate.updatedAt),
14449
+ sizeBytes: candidate.sizeBytes,
14450
+ ...rest,
14451
+ ...summary ? { summary: oneLine(summary, promptChars) } : {}
14452
+ });
14453
+ }
14454
+ return { entries, truncated, scanned };
14455
+ }
14456
+
13953
14457
  function selectLowestLatencyRelay(candidates, probes) {
13954
14458
  const rttById = new Map(probes.map((probe) => [probe.relayId, probe.rttMs]));
13955
14459
  let selected = null;
@@ -14667,6 +15171,45 @@ class ApiMachineClient {
14667
15171
  });
14668
15172
  return { type: "success", ...result };
14669
15173
  });
15174
+ this.rpcHandlerManager.registerHandler("codex-list-history", async (params) => {
15175
+ const { directory, limit, exclude } = params || {};
15176
+ if (directory !== void 0 && (typeof directory !== "string" || directory.length === 0)) {
15177
+ throw new Error("directory must be a non-empty string when provided");
15178
+ }
15179
+ const excludeIds = [
15180
+ ...Array.isArray(exclude) ? exclude.filter((id) => typeof id === "string" && UUID_RE.test(id)) : [],
15181
+ ...readTrackedCodexThreadIds()
15182
+ ];
15183
+ const result = await listCodexSessionHistory({
15184
+ sessionsRoot: getCodexSessionsRoot(),
15185
+ sessionIndexPath: getCodexSessionIndexPath(),
15186
+ ...typeof directory === "string" ? { directory } : {},
15187
+ limit: typeof limit === "number" && Number.isFinite(limit) ? Math.max(1, Math.min(Math.floor(limit), 200)) : 60,
15188
+ exclude: excludeIds
15189
+ });
15190
+ return { type: "success", ...result };
15191
+ });
15192
+ this.rpcHandlerManager.registerHandler("codex-import-session", async (params) => {
15193
+ const { directory, codexThreadId, approvedNewDirectoryCreation, permissionMode, title } = params || {};
15194
+ if (typeof directory !== "string" || directory.length === 0) {
15195
+ throw new Error("directory is required");
15196
+ }
15197
+ if (typeof codexThreadId !== "string" || !UUID_RE.test(codexThreadId)) {
15198
+ throw new Error("codexThreadId must be a valid UUID");
15199
+ }
15200
+ const result = await spawnSession({
15201
+ directory,
15202
+ machineId: this.machine.id,
15203
+ approvedNewDirectoryCreation: approvedNewDirectoryCreation === true,
15204
+ agent: "codex",
15205
+ importCodexThreadId: codexThreadId.toLowerCase(),
15206
+ permissionMode: typeof permissionMode === "string" ? permissionMode : void 0,
15207
+ importTitle: typeof title === "string" ? title : void 0
15208
+ });
15209
+ if (result.type !== "success") return result;
15210
+ logger.debug(`[API MACHINE] Importing Codex thread ${codexThreadId} as session ${result.sessionId}`);
15211
+ return { type: "success", sessionId: result.sessionId };
15212
+ });
14670
15213
  this.rpcHandlerManager.registerHandler("codex-fork-thread", async (params) => {
14671
15214
  const directory = requireNonEmptyString(params?.directory, "directory");
14672
15215
  const codexThreadId = requireNonEmptyString(params?.codexThreadId, "codexThreadId");
@@ -15015,6 +15558,8 @@ class ApiMachineClient {
15015
15558
  tmuxSessions: { rpcAvailable: true, detectedAt: now, killAttached: true },
15016
15559
  // B-290 capability flag: `claude-list-history` (same restamp discipline).
15017
15560
  claudeHistory: { rpcAvailable: true, detectedAt: now },
15561
+ // B-464 capability flag: `codex-list-history` / `codex-import-session`.
15562
+ codexHistory: { rpcAvailable: true, detectedAt: now },
15018
15563
  // B-084: closed records survive daemon restarts (persisted
15019
15564
  // in closed-terminals.json), so the connect snapshot ships
15020
15565
  // them too — not just the incremental pushes.
@@ -16371,4 +16916,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
16371
16916
  }).passthrough()
16372
16917
  ]);
16373
16918
 
16374
- export { CodexAppServerClient as $, ApiClient as A, BUILTIN_TODO_SKILL as B, tmuxArgs as C, scrubTmuxClientEnv as D, decideMirrorBinding as E, MIRROR_BACKFILL_LINES_DEFAULT as F, mirrorLineKey as G, mirrorLocalId as H, InvalidateSync as I, detectCLIAvailability as J, detectResumeSupport as K, summarizeSpawnSessionForLog as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, shellescape as N, fetchCliUpdateState as O, resolveCliUpdateCheckInterval as P, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as Q, RawJSONLinesSchema as R, STREAM_DELTA_MAX_CHARS as S, listVhTerminals as T, isValidTerminalId as U, readVhTerminal as V, sendToVhTerminal as W, ContextUsageSchema as X, TITLE_PROMPT_PREFIX as Y, BOARD_ANALYZER_PROMPT_PREFIX as Z, startOfflineReconnection as _, ApiSessionClient as a, api as a0, createBuiltinTodoClient as b, createEnvelope as c, contentLogMetadata as d, errorLogMetadata as e, connectionState as f, BUILTIN_TODO_DISCOVERY as g, delay as h, isValidSessionId as i, getProjectPath as j, initializeSandbox as k, AsyncLock as l, commandOnPath as m, deriveLocalCliUpdateSummary as n, AGENT_VERSION_SOURCES as o, projectPath as p, isClaudeSessionId as q, getLocalHappyAgentCredentialPath as r, streamKeyOf as s, readLocalHappyAgentCredentials as t, parseExactVersion as u, compareExactVersions as v, wrapCommand as w, extractCompleteLines as x, decideBackfill as y, parseTerminalHookPayload as z };
16919
+ export { shellescape as $, ApiClient as A, BUILTIN_TODO_SKILL as B, getLocalHappyAgentCredentialPath as C, readLocalHappyAgentCredentials as D, parseExactVersion as E, compareExactVersions as F, extractCompleteLines as G, decideBackfill as H, InvalidateSync as I, parseTerminalHookPayload as J, tmuxArgs as K, scrubTmuxClientEnv as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, decideMirrorBinding as N, MIRROR_BACKFILL_LINES_DEFAULT as O, mirrorLineKey as P, mirrorLocalId as Q, RawJSONLinesSchema as R, STREAM_DELTA_MAX_CHARS as S, listDaemonSessions as T, stopDaemonSession as U, detectCLIAvailability as V, detectResumeSupport as W, isDaemonRunningCurrentlyInstalledHappyVersion as X, stopDaemon as Y, TurnActivityTracker as Z, summarizeSpawnSessionForLog as _, ApiSessionClient as a, fetchCliUpdateState as a0, resolveCliUpdateCheckInterval as a1, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as a2, cleanupDaemonState as a3, listVhTerminals as a4, isValidTerminalId as a5, readVhTerminal as a6, sendToVhTerminal as a7, ContextUsageSchema as a8, TITLE_PROMPT_PREFIX as a9, BOARD_ANALYZER_PROMPT_PREFIX as aa, startOfflineReconnection as ab, CodexAppServerClient as ac, api as ad, streamKeyOf as b, checkIfDaemonRunningAndCleanupStaleState as c, createEnvelope as d, createBuiltinTodoClient as e, pushClipboardViaDaemon as f, setTerminalTitleViaDaemon as g, errorLogMetadata as h, isValidSessionId as i, contentLogMetadata as j, connectionState as k, BUILTIN_TODO_DISCOVERY as l, delay as m, notifyDaemonSessionStarted as n, notifyDaemonSessionEvent as o, projectPath as p, getProjectPath as q, initializeSandbox as r, spawnDaemonSession as s, AsyncLock as t, notifyDaemonClaudeAuthFailed as u, commandOnPath as v, wrapCommand as w, deriveLocalCliUpdateSummary as x, AGENT_VERSION_SOURCES as y, isClaudeSessionId as z };