very-happy-cli 0.2.137 → 0.2.139
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AcpBackend-POcimRNQ.mjs → AcpBackend-BwTEBAyP.mjs} +2 -2
- package/dist/{AcpBackend-Bcmm51Ya.cjs → AcpBackend-ioCPQLt4.cjs} +2 -2
- package/dist/{AcpSessionManager-D54Juu2-.mjs → AcpSessionManager-Bh3KETfz.mjs} +1 -1
- package/dist/{AcpSessionManager-CjrwSZbh.cjs → AcpSessionManager-o9T6W69-.cjs} +1 -1
- package/dist/{agentGuidance-DulugsMk.mjs → agentGuidance-DRnXdFAY.mjs} +1 -1
- package/dist/{agentGuidance-D1tKg2UW.cjs → agentGuidance-Dxt2uz3R.cjs} +1 -1
- package/dist/codex/happyMcpStdioBridge.cjs +2 -2
- package/dist/codex/happyMcpStdioBridge.mjs +2 -2
- package/dist/{config-Dr3A4IGp.cjs → config-nyMHCx-i.cjs} +4 -4
- package/dist/{config-C1gQprK_.mjs → config-o9NFsj8x.mjs} +4 -4
- package/dist/{index-D5MRxCHs.mjs → index-BZr5l8TH.mjs} +185 -230
- package/dist/{index-B11rFE7f.mjs → index-BkmfJyCf.mjs} +7 -7
- package/dist/{index-B0KIJdSt.cjs → index-C3NtU4Ch.cjs} +216 -266
- package/dist/{index-C3agc8KT.mjs → index-CTemcNko.mjs} +4 -4
- package/dist/{index-hGg5G5uO.cjs → index-gkFhwPXq.cjs} +4 -4
- package/dist/{index-C3JqT3Ql.cjs → index-klhxcpC8.cjs} +7 -7
- package/dist/index.cjs +4 -4
- package/dist/index.mjs +4 -4
- package/dist/{installTerminalHooks-DfZTXVWO.mjs → installTerminalHooks-8CHTgUC-.mjs} +4 -4
- package/dist/{installTerminalHooks-Culr259-.cjs → installTerminalHooks-DZUBgnFh.cjs} +4 -4
- package/dist/lib.cjs +2 -2
- package/dist/lib.d.cts +21 -0
- package/dist/lib.d.mts +21 -0
- package/dist/lib.mjs +2 -2
- package/dist/{limits-CJrrAhB4.cjs → limits-C_RGzva6.cjs} +26 -7
- package/dist/{limits-CAD4UF8w.mjs → limits-DefXc1TL.mjs} +26 -8
- package/dist/{mcp-CdnZbYKV.mjs → mcp-8DcunR0J.mjs} +19 -19
- package/dist/{mcp-5Jf6V4eb.cjs → mcp-czNIfD59.cjs} +21 -21
- package/dist/{runGemini-CwWEorS7.mjs → runGemini-CDUObQTD.mjs} +6 -6
- package/dist/{runGemini-GjBPzXEt.cjs → runGemini-Dy3y-lD9.cjs} +7 -7
- package/dist/{runOpenClaw-BZHHy9T7.mjs → runOpenClaw-B6KMCsVB.mjs} +5 -5
- package/dist/{runOpenClaw-DGe5c571.cjs → runOpenClaw-CJusckVM.cjs} +6 -6
- package/dist/{send-teYpfTtw.cjs → send-B4Qo-87h.cjs} +4 -4
- package/dist/{send-BN2PncAH.mjs → send-BT32Jiel.mjs} +4 -4
- package/dist/{sessions-qP1YUf5E.cjs → sessions-CN2y0xBl.cjs} +4 -4
- package/dist/{sessions-TLUfYdLX.mjs → sessions-Dh0Lg-5z.mjs} +4 -4
- package/dist/{spawn-D3P8wJ_r.mjs → spawn-DFIyobCt.mjs} +6 -6
- package/dist/{spawn-GekaZ7h2.cjs → spawn-jWj3xFhX.cjs} +8 -8
- package/dist/{teams-BSqAW2S5.cjs → teams-BhFspeAY.cjs} +4 -4
- package/dist/{teams-WSjEXh4d.mjs → teams-CZ9ICWeB.mjs} +4 -4
- package/dist/{todo-DsnXWl77.cjs → todo-B0kvqUTQ.cjs} +3 -3
- package/dist/{todo-BblDgT77.mjs → todo-jL3ttna3.mjs} +3 -3
- package/dist/{types-B8g_V-LQ.mjs → types-CrKeJuyF.mjs} +616 -13
- package/dist/{types-BAhQF6Xp.cjs → types-CxGm_YJ-.cjs} +629 -13
- package/package.json +7 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var axios = require('axios');
|
|
4
|
-
var persistence = require('./limits-
|
|
4
|
+
var persistence = require('./limits-C_RGzva6.cjs');
|
|
5
5
|
var node_events = require('node:events');
|
|
6
6
|
var socket_ioClient = require('socket.io-client');
|
|
7
7
|
var z = require('zod');
|
|
@@ -1004,6 +1004,15 @@ z.z.object({
|
|
|
1004
1004
|
rpcAvailable: z.z.boolean(),
|
|
1005
1005
|
detectedAt: z.z.number()
|
|
1006
1006
|
}).optional(),
|
|
1007
|
+
/**
|
|
1008
|
+
* B-464 capability flag: this daemon answers `codex-list-history` /
|
|
1009
|
+
* `codex-import-session` (import a Codex CLI / desktop thread that was never
|
|
1010
|
+
* started through very-happy). Same trust rule (`detectedAt >= startedAt`).
|
|
1011
|
+
*/
|
|
1012
|
+
codexHistory: z.z.object({
|
|
1013
|
+
rpcAvailable: z.z.boolean(),
|
|
1014
|
+
detectedAt: z.z.number()
|
|
1015
|
+
}).optional(),
|
|
1007
1016
|
/** Relay-owned CLI compatibility/update policy last checked by this daemon. */
|
|
1008
1017
|
cliUpdate: CliUpdateStateSchema.optional(),
|
|
1009
1018
|
/**
|
|
@@ -1369,7 +1378,7 @@ class RpcHandlerManager {
|
|
|
1369
1378
|
}
|
|
1370
1379
|
}
|
|
1371
1380
|
|
|
1372
|
-
const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-
|
|
1381
|
+
const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CxGm_YJ-.cjs', document.baseURI).href))));
|
|
1373
1382
|
function projectPath() {
|
|
1374
1383
|
const path$1 = path.resolve(__dirname$1, "..");
|
|
1375
1384
|
return path$1;
|
|
@@ -2911,6 +2920,268 @@ const MAX_CHAT_ATTACHMENT_SOURCE_BYTES = 50 * 1024 * 1024;
|
|
|
2911
2920
|
const CHAT_ATTACHMENT_ENCRYPTION_OVERHEAD_BYTES = 40;
|
|
2912
2921
|
const MAX_CHAT_ATTACHMENT_ENCRYPTED_BYTES = MAX_CHAT_ATTACHMENT_SOURCE_BYTES + CHAT_ATTACHMENT_ENCRYPTION_OVERHEAD_BYTES;
|
|
2913
2922
|
|
|
2923
|
+
const TURN_LEASE_MS = 6e4;
|
|
2924
|
+
const TURN_BUSY_TTL_MS = 15e4;
|
|
2925
|
+
class TurnActivityTracker {
|
|
2926
|
+
constructor(ttlMs = TURN_BUSY_TTL_MS) {
|
|
2927
|
+
this.ttlMs = ttlMs;
|
|
2928
|
+
}
|
|
2929
|
+
busy = /* @__PURE__ */ new Map();
|
|
2930
|
+
apply(sessionId, event, now = Date.now()) {
|
|
2931
|
+
if (event === "turn_started") this.busy.set(sessionId, now);
|
|
2932
|
+
else this.busy.delete(sessionId);
|
|
2933
|
+
}
|
|
2934
|
+
/** The wrapper is gone: whatever it reported no longer holds. */
|
|
2935
|
+
forget(sessionId) {
|
|
2936
|
+
this.busy.delete(sessionId);
|
|
2937
|
+
}
|
|
2938
|
+
/** Sessions whose last renewal is still inside the TTL. */
|
|
2939
|
+
activeSessions(now = Date.now()) {
|
|
2940
|
+
const out = [];
|
|
2941
|
+
for (const [id, at] of this.busy) {
|
|
2942
|
+
if (now - at <= this.ttlMs) out.push(id);
|
|
2943
|
+
else this.busy.delete(id);
|
|
2944
|
+
}
|
|
2945
|
+
return out;
|
|
2946
|
+
}
|
|
2947
|
+
hasActiveTurn(now = Date.now()) {
|
|
2948
|
+
return this.activeSessions(now).length > 0;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
class TurnReporter {
|
|
2952
|
+
constructor(send, leaseMs = TURN_LEASE_MS) {
|
|
2953
|
+
this.send = send;
|
|
2954
|
+
this.leaseMs = leaseMs;
|
|
2955
|
+
}
|
|
2956
|
+
thinking = false;
|
|
2957
|
+
lastStartedAt = 0;
|
|
2958
|
+
/** Called on every keepAlive tick; emits only on an edge or a due renewal. */
|
|
2959
|
+
observe(thinking, now = Date.now()) {
|
|
2960
|
+
if (thinking) {
|
|
2961
|
+
const due = !this.thinking || now - this.lastStartedAt >= this.leaseMs;
|
|
2962
|
+
this.thinking = true;
|
|
2963
|
+
if (!due) return null;
|
|
2964
|
+
this.lastStartedAt = now;
|
|
2965
|
+
this.send("turn_started");
|
|
2966
|
+
return "turn_started";
|
|
2967
|
+
}
|
|
2968
|
+
if (!this.thinking) return null;
|
|
2969
|
+
this.thinking = false;
|
|
2970
|
+
this.lastStartedAt = 0;
|
|
2971
|
+
this.send("turn_ended");
|
|
2972
|
+
return "turn_ended";
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
function daemonControlHeaders(controlToken) {
|
|
2977
|
+
return {
|
|
2978
|
+
"Content-Type": "application/json",
|
|
2979
|
+
...controlToken ? { Authorization: `Bearer ${controlToken}` } : {}
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
async function daemonPost(path, body) {
|
|
2983
|
+
const state = await persistence.readDaemonState();
|
|
2984
|
+
if (!state?.httpPort) {
|
|
2985
|
+
const errorMessage = "No daemon running, no state file found";
|
|
2986
|
+
persistence.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
2987
|
+
return {
|
|
2988
|
+
error: errorMessage
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
try {
|
|
2992
|
+
process.kill(state.pid, 0);
|
|
2993
|
+
} catch (error) {
|
|
2994
|
+
const errorMessage = "Daemon is not running, file is stale";
|
|
2995
|
+
persistence.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
2996
|
+
return {
|
|
2997
|
+
error: errorMessage
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
try {
|
|
3001
|
+
const timeout = process.env.HAPPY_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPPY_DAEMON_HTTP_TIMEOUT) : 1e4;
|
|
3002
|
+
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
3003
|
+
method: "POST",
|
|
3004
|
+
// A state written by an old daemon has no token and must remain callable;
|
|
3005
|
+
// a new daemon always writes one and rejects requests without it.
|
|
3006
|
+
headers: daemonControlHeaders(state.controlToken),
|
|
3007
|
+
body: JSON.stringify(body || {}),
|
|
3008
|
+
// Mostly increased for stress test
|
|
3009
|
+
signal: AbortSignal.timeout(timeout)
|
|
3010
|
+
});
|
|
3011
|
+
if (!response.ok) {
|
|
3012
|
+
const failure = await response.json().catch(() => null);
|
|
3013
|
+
const reason = typeof failure?.error === "string" && failure.error ? failure.error : `HTTP ${response.status}`;
|
|
3014
|
+
const errorMessage = `Request failed: ${path}, ${reason}`;
|
|
3015
|
+
persistence.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3016
|
+
return {
|
|
3017
|
+
error: errorMessage
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
return await response.json();
|
|
3021
|
+
} catch (error) {
|
|
3022
|
+
const errorMessage = `Request failed: ${path}, ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
3023
|
+
persistence.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3024
|
+
return {
|
|
3025
|
+
error: errorMessage
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
const SESSION_STARTED_RETRY_TIMEOUT_MS = 3e3;
|
|
3030
|
+
const SESSION_STARTED_RETRY_INTERVAL_MS = 100;
|
|
3031
|
+
async function notifyDaemonSessionStarted(sessionId, metadata, encryption) {
|
|
3032
|
+
const payload = { sessionId, metadata, encryption };
|
|
3033
|
+
const deadline = Date.now() + SESSION_STARTED_RETRY_TIMEOUT_MS;
|
|
3034
|
+
let result;
|
|
3035
|
+
while (true) {
|
|
3036
|
+
result = await daemonPost("/session-started", payload);
|
|
3037
|
+
if (!result?.error) {
|
|
3038
|
+
return result;
|
|
3039
|
+
}
|
|
3040
|
+
if (Date.now() >= deadline) {
|
|
3041
|
+
return result;
|
|
3042
|
+
}
|
|
3043
|
+
await new Promise((resolve) => setTimeout(resolve, SESSION_STARTED_RETRY_INTERVAL_MS));
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
async function listDaemonSessions() {
|
|
3047
|
+
const result = await daemonPost("/list");
|
|
3048
|
+
return result.children || [];
|
|
3049
|
+
}
|
|
3050
|
+
async function stopDaemonSession(sessionId) {
|
|
3051
|
+
const result = await daemonPost("/stop-session", { sessionId });
|
|
3052
|
+
return result.success || false;
|
|
3053
|
+
}
|
|
3054
|
+
async function spawnDaemonSession(directory, sessionId, opts) {
|
|
3055
|
+
const result = await daemonPost("/spawn-session", {
|
|
3056
|
+
directory,
|
|
3057
|
+
sessionId,
|
|
3058
|
+
spawnedBy: opts?.spawnedBy,
|
|
3059
|
+
permissionMode: opts?.permissionMode,
|
|
3060
|
+
agent: opts?.agent,
|
|
3061
|
+
environmentVariables: opts?.environmentVariables
|
|
3062
|
+
});
|
|
3063
|
+
return result;
|
|
3064
|
+
}
|
|
3065
|
+
async function notifyDaemonSessionEvent(sessionId, event, spawnedBy) {
|
|
3066
|
+
return daemonPost("/session-event", { sessionId, event, spawnedBy });
|
|
3067
|
+
}
|
|
3068
|
+
async function notifyDaemonClaudeAuthFailed(sessionId) {
|
|
3069
|
+
return daemonPost("/session-event", { sessionId, event: "auth_failed" });
|
|
3070
|
+
}
|
|
3071
|
+
async function notifyDaemonTurnEvent(sessionId, event) {
|
|
3072
|
+
return daemonPost("/session-event", { sessionId, event });
|
|
3073
|
+
}
|
|
3074
|
+
async function stopDaemonHttp() {
|
|
3075
|
+
await daemonPost("/stop");
|
|
3076
|
+
}
|
|
3077
|
+
async function pushClipboardViaDaemon(text) {
|
|
3078
|
+
const result = await daemonPost("/clipboard", { text });
|
|
3079
|
+
if (result?.error && result.delivered === void 0) {
|
|
3080
|
+
return { delivered: false, error: result.error };
|
|
3081
|
+
}
|
|
3082
|
+
return result;
|
|
3083
|
+
}
|
|
3084
|
+
async function setTerminalTitleViaDaemon(terminalId, title, ifAbsent = false) {
|
|
3085
|
+
const result = await daemonPost("/terminal-title", { terminalId, title, ifAbsent });
|
|
3086
|
+
if (result?.error) {
|
|
3087
|
+
return { ok: false, error: result.error };
|
|
3088
|
+
}
|
|
3089
|
+
return { ok: result?.status === "ok", error: result?.status === "ok" ? void 0 : "unexpected daemon response" };
|
|
3090
|
+
}
|
|
3091
|
+
async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
3092
|
+
const state = await persistence.readDaemonState();
|
|
3093
|
+
if (!state) {
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
3096
|
+
try {
|
|
3097
|
+
process.kill(state.pid, 0);
|
|
3098
|
+
} catch {
|
|
3099
|
+
persistence.logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3100
|
+
await cleanupDaemonState();
|
|
3101
|
+
return false;
|
|
3102
|
+
}
|
|
3103
|
+
if (state.httpPort) {
|
|
3104
|
+
try {
|
|
3105
|
+
const response = await fetch(`http://127.0.0.1:${state.httpPort}/list`, {
|
|
3106
|
+
method: "POST",
|
|
3107
|
+
headers: daemonControlHeaders(state.controlToken),
|
|
3108
|
+
body: "{}",
|
|
3109
|
+
signal: AbortSignal.timeout(2e3)
|
|
3110
|
+
});
|
|
3111
|
+
if (response.ok) {
|
|
3112
|
+
return true;
|
|
3113
|
+
}
|
|
3114
|
+
} catch {
|
|
3115
|
+
persistence.logger.debug(`[DAEMON RUN] PID ${state.pid} is alive but HTTP health check failed on port ${state.httpPort}, cleaning up stale state`);
|
|
3116
|
+
await cleanupDaemonState();
|
|
3117
|
+
return false;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
return true;
|
|
3121
|
+
}
|
|
3122
|
+
async function isDaemonRunningCurrentlyInstalledHappyVersion() {
|
|
3123
|
+
persistence.logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
3124
|
+
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3125
|
+
if (!runningDaemon) {
|
|
3126
|
+
persistence.logger.debug("[DAEMON CONTROL] No daemon running, returning false");
|
|
3127
|
+
return false;
|
|
3128
|
+
}
|
|
3129
|
+
const state = await persistence.readDaemonState();
|
|
3130
|
+
if (!state) {
|
|
3131
|
+
persistence.logger.debug("[DAEMON CONTROL] No daemon state found, returning false");
|
|
3132
|
+
return false;
|
|
3133
|
+
}
|
|
3134
|
+
const currentCliVersion = persistence.configuration.currentCliVersion;
|
|
3135
|
+
persistence.logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
|
3136
|
+
return currentCliVersion === state.startedWithCliVersion;
|
|
3137
|
+
}
|
|
3138
|
+
async function cleanupDaemonState() {
|
|
3139
|
+
try {
|
|
3140
|
+
await persistence.clearDaemonState();
|
|
3141
|
+
persistence.logger.debug("[DAEMON RUN] Daemon state file removed");
|
|
3142
|
+
} catch (error) {
|
|
3143
|
+
persistence.logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
async function stopDaemon() {
|
|
3147
|
+
try {
|
|
3148
|
+
const state = await persistence.readDaemonState();
|
|
3149
|
+
if (!state) {
|
|
3150
|
+
persistence.logger.debug("No daemon state found");
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
persistence.logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
3154
|
+
try {
|
|
3155
|
+
await stopDaemonHttp();
|
|
3156
|
+
await waitForProcessDeath(state.pid, 2e3);
|
|
3157
|
+
persistence.logger.debug("Daemon stopped gracefully via HTTP");
|
|
3158
|
+
return;
|
|
3159
|
+
} catch (error) {
|
|
3160
|
+
persistence.logger.debug("HTTP stop failed, will force kill", error);
|
|
3161
|
+
}
|
|
3162
|
+
try {
|
|
3163
|
+
process.kill(state.pid, "SIGKILL");
|
|
3164
|
+
persistence.logger.debug("Force killed daemon");
|
|
3165
|
+
} catch (error) {
|
|
3166
|
+
persistence.logger.debug("Daemon already dead");
|
|
3167
|
+
}
|
|
3168
|
+
} catch (error) {
|
|
3169
|
+
persistence.logger.debug("Error stopping daemon", error);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
async function waitForProcessDeath(pid, timeout) {
|
|
3173
|
+
const start = Date.now();
|
|
3174
|
+
while (Date.now() - start < timeout) {
|
|
3175
|
+
try {
|
|
3176
|
+
process.kill(pid, 0);
|
|
3177
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3178
|
+
} catch {
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
throw new Error("Process did not die within timeout");
|
|
3183
|
+
}
|
|
3184
|
+
|
|
2914
3185
|
class ApiSessionClient extends node_events.EventEmitter {
|
|
2915
3186
|
token;
|
|
2916
3187
|
sessionId;
|
|
@@ -3692,10 +3963,14 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3692
3963
|
/**
|
|
3693
3964
|
* Send a ping message to keep the connection alive
|
|
3694
3965
|
*/
|
|
3966
|
+
/** B-466: turn edges (and a periodic lease) reported to the local daemon so
|
|
3967
|
+
* auto-update waits for "no turn in flight" instead of "no sessions". */
|
|
3968
|
+
turnReporter = new TurnReporter((event) => void notifyDaemonTurnEvent(this.sessionId, event).catch((error) => persistence.logger.debug("[API] Failed to report turn event to daemon:", error)));
|
|
3695
3969
|
keepAlive(thinking, mode) {
|
|
3696
3970
|
if (process.env.DEBUG) {
|
|
3697
3971
|
persistence.logger.debug(`[API] Sending keep alive message: ${thinking}`);
|
|
3698
3972
|
}
|
|
3973
|
+
this.turnReporter.observe(thinking);
|
|
3699
3974
|
this.socket.volatile.emit("session-alive", {
|
|
3700
3975
|
sid: this.sessionId,
|
|
3701
3976
|
time: Date.now(),
|
|
@@ -12384,7 +12659,7 @@ function isVeryHappyOneShotPrompt(firstPrompt) {
|
|
|
12384
12659
|
return VERY_HAPPY_ONE_SHOT_PROMPT_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
12385
12660
|
}
|
|
12386
12661
|
|
|
12387
|
-
const UUID_RE$
|
|
12662
|
+
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;
|
|
12388
12663
|
async function listClaudeProjectDirs(projectsRoot) {
|
|
12389
12664
|
let names;
|
|
12390
12665
|
try {
|
|
@@ -12403,7 +12678,7 @@ async function listClaudeProjectDirs(projectsRoot) {
|
|
|
12403
12678
|
}
|
|
12404
12679
|
return dirs;
|
|
12405
12680
|
}
|
|
12406
|
-
async function collectCandidates(projectDirs, exclude) {
|
|
12681
|
+
async function collectCandidates$1(projectDirs, exclude) {
|
|
12407
12682
|
const out = [];
|
|
12408
12683
|
for (const dir of projectDirs) {
|
|
12409
12684
|
let names;
|
|
@@ -12415,7 +12690,7 @@ async function collectCandidates(projectDirs, exclude) {
|
|
|
12415
12690
|
for (const name of names) {
|
|
12416
12691
|
if (!name.endsWith(".jsonl")) continue;
|
|
12417
12692
|
const id = name.slice(0, -".jsonl".length);
|
|
12418
|
-
if (!UUID_RE$
|
|
12693
|
+
if (!UUID_RE$2.test(id) || exclude.has(id.toLowerCase())) continue;
|
|
12419
12694
|
const path = path$1.join(dir, name);
|
|
12420
12695
|
try {
|
|
12421
12696
|
const s = await promises.stat(path);
|
|
@@ -12434,7 +12709,7 @@ async function collectCandidates(projectDirs, exclude) {
|
|
|
12434
12709
|
return true;
|
|
12435
12710
|
});
|
|
12436
12711
|
}
|
|
12437
|
-
async function readHead(path, bytes) {
|
|
12712
|
+
async function readHead$1(path, bytes) {
|
|
12438
12713
|
const fh = await promises.open(path, "r");
|
|
12439
12714
|
try {
|
|
12440
12715
|
const buffer = Buffer.alloc(bytes);
|
|
@@ -12444,11 +12719,11 @@ async function readHead(path, bytes) {
|
|
|
12444
12719
|
await fh.close();
|
|
12445
12720
|
}
|
|
12446
12721
|
}
|
|
12447
|
-
function oneLine(text, max) {
|
|
12722
|
+
function oneLine$1(text, max) {
|
|
12448
12723
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
12449
12724
|
return collapsed.length > max ? `${collapsed.slice(0, max - 1)}\u2026` : collapsed;
|
|
12450
12725
|
}
|
|
12451
|
-
function parseTimestamp(raw) {
|
|
12726
|
+
function parseTimestamp$1(raw) {
|
|
12452
12727
|
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
12453
12728
|
if (typeof raw === "string") {
|
|
12454
12729
|
const t = Date.parse(raw);
|
|
@@ -12475,7 +12750,7 @@ function parseClaudeHistoryHead(head, promptChars) {
|
|
|
12475
12750
|
}
|
|
12476
12751
|
if (!parsed || typeof parsed !== "object") continue;
|
|
12477
12752
|
if (parsed.type === "summary" && typeof parsed.summary === "string" && !summary) {
|
|
12478
|
-
const s = oneLine(parsed.summary, promptChars);
|
|
12753
|
+
const s = oneLine$1(parsed.summary, promptChars);
|
|
12479
12754
|
if (s) summary = s;
|
|
12480
12755
|
continue;
|
|
12481
12756
|
}
|
|
@@ -12483,7 +12758,7 @@ function parseClaudeHistoryHead(head, promptChars) {
|
|
|
12483
12758
|
if (typeof parsed.entrypoint === "string" && parsed.entrypoint && !entrypoint) entrypoint = parsed.entrypoint;
|
|
12484
12759
|
if (typeof parsed.gitBranch === "string" && parsed.gitBranch && !gitBranch) gitBranch = parsed.gitBranch;
|
|
12485
12760
|
if (typeof parsed.version === "string" && parsed.version && !version) version = parsed.version;
|
|
12486
|
-
if (startedAt === null) startedAt = parseTimestamp(parsed.timestamp);
|
|
12761
|
+
if (startedAt === null) startedAt = parseTimestamp$1(parsed.timestamp);
|
|
12487
12762
|
if (!firstPrompt && parsed.type === "user" && !parsed.isSidechain) {
|
|
12488
12763
|
const content = parsed.message?.content;
|
|
12489
12764
|
let text = null;
|
|
@@ -12495,7 +12770,7 @@ function parseClaudeHistoryHead(head, promptChars) {
|
|
|
12495
12770
|
}
|
|
12496
12771
|
if (text) {
|
|
12497
12772
|
if (isVeryHappyOneShotPrompt(text)) return null;
|
|
12498
|
-
const cleaned = oneLine(stripHarnessTags(text), promptChars);
|
|
12773
|
+
const cleaned = oneLine$1(stripHarnessTags(text), promptChars);
|
|
12499
12774
|
if (cleaned) firstPrompt = cleaned;
|
|
12500
12775
|
}
|
|
12501
12776
|
}
|
|
@@ -12521,7 +12796,7 @@ async function listClaudeSessionHistory(options) {
|
|
|
12521
12796
|
const headBytes = Math.max(4096, options.headBytes ?? 64 * 1024);
|
|
12522
12797
|
const promptChars = Math.max(20, options.promptChars ?? 200);
|
|
12523
12798
|
const exclude = new Set(Array.from(options.exclude ?? [], (id) => id.toLowerCase()));
|
|
12524
|
-
const candidates = await collectCandidates(options.projectDirs, exclude);
|
|
12799
|
+
const candidates = await collectCandidates$1(options.projectDirs, exclude);
|
|
12525
12800
|
const entries = [];
|
|
12526
12801
|
let scanned = 0;
|
|
12527
12802
|
let truncated = false;
|
|
@@ -12533,7 +12808,7 @@ async function listClaudeSessionHistory(options) {
|
|
|
12533
12808
|
scanned += 1;
|
|
12534
12809
|
let head;
|
|
12535
12810
|
try {
|
|
12536
|
-
head = await readHead(candidate.path, headBytes);
|
|
12811
|
+
head = await readHead$1(candidate.path, headBytes);
|
|
12537
12812
|
} catch {
|
|
12538
12813
|
continue;
|
|
12539
12814
|
}
|
|
@@ -12980,6 +13255,9 @@ class CodexAppServerClient {
|
|
|
12980
13255
|
// Tracks in-flight interruptTurn() RPCs so sendTurnAndWait can wait for them
|
|
12981
13256
|
// before starting a new turn (prevents stale turn/interrupt from aborting the next turn).
|
|
12982
13257
|
pendingInterrupt = null;
|
|
13258
|
+
// In-flight idle release (B-461); ensureConnected() waits for it so a message
|
|
13259
|
+
// arriving mid-release cannot spawn a second app-server against a dying one.
|
|
13260
|
+
idleRelease = null;
|
|
12983
13261
|
notificationProtocol = "unknown";
|
|
12984
13262
|
completedTurnIds = /* @__PURE__ */ new Set();
|
|
12985
13263
|
rawFileChangesByItemId = /* @__PURE__ */ new Map();
|
|
@@ -13313,6 +13591,53 @@ class CodexAppServerClient {
|
|
|
13313
13591
|
async disconnect() {
|
|
13314
13592
|
await this.disconnectInternal();
|
|
13315
13593
|
}
|
|
13594
|
+
/** True while an app-server child process is owned by this client. */
|
|
13595
|
+
get hasProcess() {
|
|
13596
|
+
return this.process !== null;
|
|
13597
|
+
}
|
|
13598
|
+
/**
|
|
13599
|
+
* Drop the app-server process while the session is idle, keeping the thread
|
|
13600
|
+
* id and defaults so the next turn can `thread/resume` it (B-461). Refuses —
|
|
13601
|
+
* returning false — whenever anything is still in flight: an active turn, a
|
|
13602
|
+
* pending interrupt, or any outstanding RPC. See codexIdleRelease.ts.
|
|
13603
|
+
*/
|
|
13604
|
+
async releaseIdleProcess() {
|
|
13605
|
+
if (this.idleRelease) return this.idleRelease;
|
|
13606
|
+
if (!this.connected && !this.process) return false;
|
|
13607
|
+
if (this.pendingTurnCompletion || this.pendingInterrupt || this.pending.size > 0) {
|
|
13608
|
+
persistence.logger.debug("[CodexAppServer] Idle release skipped: work still in flight");
|
|
13609
|
+
return false;
|
|
13610
|
+
}
|
|
13611
|
+
const run = (async () => {
|
|
13612
|
+
const threadId = this._threadId;
|
|
13613
|
+
persistence.logger.debug(`[CodexAppServer] Releasing idle app-server; thread=${threadId ?? "none"} kept for resume`);
|
|
13614
|
+
await this.disconnectInternal({ preserveThreadState: !!threadId });
|
|
13615
|
+
return true;
|
|
13616
|
+
})();
|
|
13617
|
+
this.idleRelease = run;
|
|
13618
|
+
try {
|
|
13619
|
+
return await run;
|
|
13620
|
+
} finally {
|
|
13621
|
+
if (this.idleRelease === run) this.idleRelease = null;
|
|
13622
|
+
}
|
|
13623
|
+
}
|
|
13624
|
+
/**
|
|
13625
|
+
* Make sure an app-server is running and, if a thread is remembered, resumed
|
|
13626
|
+
* in it. Cheap no-op while connected. Covers both the idle release above and
|
|
13627
|
+
* an app-server that exited on its own between turns.
|
|
13628
|
+
*/
|
|
13629
|
+
async ensureConnected() {
|
|
13630
|
+
if (this.idleRelease) await this.idleRelease;
|
|
13631
|
+
if (this.connected) return;
|
|
13632
|
+
const threadId = this._threadId;
|
|
13633
|
+
if (this.process) {
|
|
13634
|
+
await this.disconnectInternal({ preserveThreadState: !!threadId });
|
|
13635
|
+
}
|
|
13636
|
+
await this.connect();
|
|
13637
|
+
if (threadId) {
|
|
13638
|
+
await this.resumeThread({ threadId });
|
|
13639
|
+
}
|
|
13640
|
+
}
|
|
13316
13641
|
buildThreadConfig(mcpServers) {
|
|
13317
13642
|
return mcpServers ? { mcp_servers: mcpServers } : null;
|
|
13318
13643
|
}
|
|
@@ -13327,6 +13652,7 @@ class CodexAppServerClient {
|
|
|
13327
13652
|
}
|
|
13328
13653
|
// ─── Thread management ──────────────────────────────────────
|
|
13329
13654
|
async startThread(opts) {
|
|
13655
|
+
await this.ensureConnected();
|
|
13330
13656
|
const params = {
|
|
13331
13657
|
model: opts.model ?? null,
|
|
13332
13658
|
modelProvider: null,
|
|
@@ -13354,6 +13680,7 @@ class CodexAppServerClient {
|
|
|
13354
13680
|
if (!threadId) {
|
|
13355
13681
|
throw new Error("No thread available to resume.");
|
|
13356
13682
|
}
|
|
13683
|
+
await this.ensureConnected();
|
|
13357
13684
|
const defaults = this.threadDefaults ?? {};
|
|
13358
13685
|
const params = {
|
|
13359
13686
|
threadId,
|
|
@@ -13381,6 +13708,7 @@ class CodexAppServerClient {
|
|
|
13381
13708
|
return { threadId: result.thread.id, model: result.model };
|
|
13382
13709
|
}
|
|
13383
13710
|
async forkThread(opts) {
|
|
13711
|
+
await this.ensureConnected();
|
|
13384
13712
|
const defaults = this.threadDefaults ?? {};
|
|
13385
13713
|
const params = {
|
|
13386
13714
|
threadId: opts.threadId,
|
|
@@ -13409,6 +13737,7 @@ class CodexAppServerClient {
|
|
|
13409
13737
|
return { threadId: result.thread.id, model: result.model, thread: result.thread };
|
|
13410
13738
|
}
|
|
13411
13739
|
async readThread(opts) {
|
|
13740
|
+
await this.ensureConnected();
|
|
13412
13741
|
const params = {
|
|
13413
13742
|
threadId: opts.threadId,
|
|
13414
13743
|
includeTurns: opts.includeTurns ?? true
|
|
@@ -13416,6 +13745,7 @@ class CodexAppServerClient {
|
|
|
13416
13745
|
return await this.request("thread/read", params);
|
|
13417
13746
|
}
|
|
13418
13747
|
async rollbackThread(opts) {
|
|
13748
|
+
await this.ensureConnected();
|
|
13419
13749
|
const params = {
|
|
13420
13750
|
threadId: opts.threadId,
|
|
13421
13751
|
numTurns: opts.numTurns
|
|
@@ -13423,6 +13753,7 @@ class CodexAppServerClient {
|
|
|
13423
13753
|
return await this.request("thread/rollback", params);
|
|
13424
13754
|
}
|
|
13425
13755
|
async injectItems(opts) {
|
|
13756
|
+
await this.ensureConnected();
|
|
13426
13757
|
const params = {
|
|
13427
13758
|
threadId: opts.threadId,
|
|
13428
13759
|
items: opts.items
|
|
@@ -13527,6 +13858,7 @@ class CodexAppServerClient {
|
|
|
13527
13858
|
if (!this._threadId) {
|
|
13528
13859
|
throw new Error("No active thread. Call startThread first.");
|
|
13529
13860
|
}
|
|
13861
|
+
await this.ensureConnected();
|
|
13530
13862
|
const input = [
|
|
13531
13863
|
{ type: "text", text: prompt },
|
|
13532
13864
|
...(opts?.images ?? []).map((path) => ({ type: "localImage", path }))
|
|
@@ -13572,6 +13904,7 @@ class CodexAppServerClient {
|
|
|
13572
13904
|
await this.pendingInterrupt;
|
|
13573
13905
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
13574
13906
|
}
|
|
13907
|
+
await this.ensureConnected();
|
|
13575
13908
|
const timeoutMs = opts?.turnTimeoutMs ?? CodexAppServerClient.TURN_TIMEOUT_MS;
|
|
13576
13909
|
let timer = null;
|
|
13577
13910
|
const completion = new Promise((resolve) => {
|
|
@@ -13914,6 +14247,235 @@ class CodexAppServerClient {
|
|
|
13914
14247
|
}
|
|
13915
14248
|
}
|
|
13916
14249
|
|
|
14250
|
+
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;
|
|
14251
|
+
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;
|
|
14252
|
+
const VERY_HAPPY_CODEX_ORIGINATOR = "happy-codex";
|
|
14253
|
+
function getCodexHomeDir() {
|
|
14254
|
+
return process.env.CODEX_HOME || path$1.join(os$1.homedir(), ".codex");
|
|
14255
|
+
}
|
|
14256
|
+
function getCodexSessionsRoot(codexHome = getCodexHomeDir()) {
|
|
14257
|
+
return path$1.join(codexHome, "sessions");
|
|
14258
|
+
}
|
|
14259
|
+
function getCodexSessionIndexPath(codexHome = getCodexHomeDir()) {
|
|
14260
|
+
return path$1.join(codexHome, "session_index.jsonl");
|
|
14261
|
+
}
|
|
14262
|
+
async function collectCandidates(root, exclude) {
|
|
14263
|
+
const out = [];
|
|
14264
|
+
async function walk(dir, depth) {
|
|
14265
|
+
let names;
|
|
14266
|
+
try {
|
|
14267
|
+
names = await promises.readdir(dir);
|
|
14268
|
+
} catch {
|
|
14269
|
+
return;
|
|
14270
|
+
}
|
|
14271
|
+
for (const name of names) {
|
|
14272
|
+
const full = path$1.join(dir, name);
|
|
14273
|
+
const match = ROLLOUT_RE.exec(name);
|
|
14274
|
+
if (match) {
|
|
14275
|
+
const id = match[1].toLowerCase();
|
|
14276
|
+
if (exclude.has(id)) continue;
|
|
14277
|
+
try {
|
|
14278
|
+
const s = await promises.stat(full);
|
|
14279
|
+
if (!s.isFile() || s.size === 0) continue;
|
|
14280
|
+
out.push({ path: full, id, updatedAt: s.mtimeMs, sizeBytes: s.size });
|
|
14281
|
+
} catch {
|
|
14282
|
+
}
|
|
14283
|
+
continue;
|
|
14284
|
+
}
|
|
14285
|
+
if (name.endsWith(".jsonl") || depth >= 4) continue;
|
|
14286
|
+
try {
|
|
14287
|
+
if ((await promises.stat(full)).isDirectory()) await walk(full, depth + 1);
|
|
14288
|
+
} catch {
|
|
14289
|
+
}
|
|
14290
|
+
}
|
|
14291
|
+
}
|
|
14292
|
+
await walk(root, 0);
|
|
14293
|
+
out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
14294
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14295
|
+
return out.filter((c) => {
|
|
14296
|
+
if (seen.has(c.id)) return false;
|
|
14297
|
+
seen.add(c.id);
|
|
14298
|
+
return true;
|
|
14299
|
+
});
|
|
14300
|
+
}
|
|
14301
|
+
async function readHead(path, bytes) {
|
|
14302
|
+
const fh = await promises.open(path, "r");
|
|
14303
|
+
try {
|
|
14304
|
+
const buffer = Buffer.alloc(bytes);
|
|
14305
|
+
const { bytesRead } = await fh.read(buffer, 0, bytes, 0);
|
|
14306
|
+
return buffer.subarray(0, bytesRead).toString("utf-8");
|
|
14307
|
+
} finally {
|
|
14308
|
+
await fh.close();
|
|
14309
|
+
}
|
|
14310
|
+
}
|
|
14311
|
+
function oneLine(text, max) {
|
|
14312
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
14313
|
+
return collapsed.length > max ? `${collapsed.slice(0, max - 1)}\u2026` : collapsed;
|
|
14314
|
+
}
|
|
14315
|
+
function parseTimestamp(raw) {
|
|
14316
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
14317
|
+
if (typeof raw === "string") {
|
|
14318
|
+
const t = Date.parse(raw);
|
|
14319
|
+
if (Number.isFinite(t)) return t;
|
|
14320
|
+
}
|
|
14321
|
+
return null;
|
|
14322
|
+
}
|
|
14323
|
+
function isHarnessUserText(text) {
|
|
14324
|
+
const t = text.trimStart();
|
|
14325
|
+
return t.startsWith("# AGENTS.md instructions") || t.startsWith("<environment_context>") || t.startsWith("<user_instructions>") || t.startsWith("<INSTRUCTIONS>");
|
|
14326
|
+
}
|
|
14327
|
+
function stripHarnessBlocks(text) {
|
|
14328
|
+
return text.replace(/<(environment_context|user_instructions|INSTRUCTIONS|turn_aborted|system-reminder)>[\s\S]*?<\/\1>/g, " ").replace(/^# AGENTS\.md instructions[^\n]*\n/m, " ");
|
|
14329
|
+
}
|
|
14330
|
+
function textFromUserContent(content) {
|
|
14331
|
+
if (typeof content === "string") return content;
|
|
14332
|
+
if (!Array.isArray(content)) return null;
|
|
14333
|
+
const parts = [];
|
|
14334
|
+
for (const part of content) {
|
|
14335
|
+
if (!part || typeof part !== "object") continue;
|
|
14336
|
+
const type = part.type;
|
|
14337
|
+
const text = part.text;
|
|
14338
|
+
if ((type === "input_text" || type === "text") && typeof text === "string") parts.push(text);
|
|
14339
|
+
}
|
|
14340
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
14341
|
+
}
|
|
14342
|
+
function parseCodexHistoryHead(head, promptChars) {
|
|
14343
|
+
let id;
|
|
14344
|
+
let cwd;
|
|
14345
|
+
let firstPrompt;
|
|
14346
|
+
let fallbackPrompt;
|
|
14347
|
+
let startedAt = null;
|
|
14348
|
+
let entrypoint;
|
|
14349
|
+
let originator;
|
|
14350
|
+
let gitBranch;
|
|
14351
|
+
let version;
|
|
14352
|
+
let sawMeta = false;
|
|
14353
|
+
for (const line of head.split("\n")) {
|
|
14354
|
+
if (line.length === 0) continue;
|
|
14355
|
+
let parsed;
|
|
14356
|
+
try {
|
|
14357
|
+
parsed = JSON.parse(line);
|
|
14358
|
+
} catch {
|
|
14359
|
+
continue;
|
|
14360
|
+
}
|
|
14361
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
14362
|
+
const payload = parsed.payload;
|
|
14363
|
+
if (parsed.type === "session_meta" && payload && typeof payload === "object") {
|
|
14364
|
+
sawMeta = true;
|
|
14365
|
+
if (typeof payload.id === "string" && UUID_RE$1.test(payload.id)) id = payload.id.toLowerCase();
|
|
14366
|
+
if (typeof payload.cwd === "string" && payload.cwd) cwd = payload.cwd;
|
|
14367
|
+
if (typeof payload.source === "string" && payload.source) entrypoint = payload.source;
|
|
14368
|
+
if (typeof payload.originator === "string" && payload.originator) originator = payload.originator;
|
|
14369
|
+
if (typeof payload.cli_version === "string" && payload.cli_version) version = payload.cli_version;
|
|
14370
|
+
const branch = payload.git && typeof payload.git === "object" ? payload.git.branch : void 0;
|
|
14371
|
+
if (typeof branch === "string" && branch) gitBranch = branch;
|
|
14372
|
+
startedAt = parseTimestamp(payload.timestamp) ?? parseTimestamp(parsed.timestamp);
|
|
14373
|
+
continue;
|
|
14374
|
+
}
|
|
14375
|
+
if (!sawMeta) return null;
|
|
14376
|
+
if (firstPrompt) break;
|
|
14377
|
+
if (!payload || typeof payload !== "object") continue;
|
|
14378
|
+
if (parsed.type === "event_msg") {
|
|
14379
|
+
if (payload.type === "user_message" && typeof payload.message === "string") {
|
|
14380
|
+
const cleaned = oneLine(stripHarnessBlocks(payload.message), promptChars);
|
|
14381
|
+
if (cleaned) firstPrompt = cleaned;
|
|
14382
|
+
} else if (payload.type === "item_completed" && payload.item && payload.item.type === "UserMessage") {
|
|
14383
|
+
const text = textFromUserContent(payload.item.content);
|
|
14384
|
+
const cleaned = text ? oneLine(stripHarnessBlocks(text), promptChars) : "";
|
|
14385
|
+
if (cleaned) firstPrompt = cleaned;
|
|
14386
|
+
}
|
|
14387
|
+
continue;
|
|
14388
|
+
}
|
|
14389
|
+
if (parsed.type === "response_item" && payload.type === "message" && payload.role === "user" && !fallbackPrompt) {
|
|
14390
|
+
const text = textFromUserContent(payload.content);
|
|
14391
|
+
if (text && !isHarnessUserText(text)) {
|
|
14392
|
+
const cleaned = oneLine(stripHarnessBlocks(text), promptChars);
|
|
14393
|
+
if (cleaned) fallbackPrompt = cleaned;
|
|
14394
|
+
}
|
|
14395
|
+
}
|
|
14396
|
+
}
|
|
14397
|
+
if (!sawMeta || !cwd) return null;
|
|
14398
|
+
const prompt = firstPrompt ?? fallbackPrompt;
|
|
14399
|
+
if (!prompt) return null;
|
|
14400
|
+
return {
|
|
14401
|
+
...id ? { id } : {},
|
|
14402
|
+
cwd,
|
|
14403
|
+
firstPrompt: prompt,
|
|
14404
|
+
startedAt: startedAt ?? 0,
|
|
14405
|
+
...entrypoint ? { entrypoint } : {},
|
|
14406
|
+
...originator ? { originator } : {},
|
|
14407
|
+
...gitBranch ? { gitBranch } : {},
|
|
14408
|
+
...version ? { version } : {}
|
|
14409
|
+
};
|
|
14410
|
+
}
|
|
14411
|
+
function parseCodexSessionIndex(text) {
|
|
14412
|
+
const names = /* @__PURE__ */ new Map();
|
|
14413
|
+
for (const line of text.split("\n")) {
|
|
14414
|
+
if (!line.trim()) continue;
|
|
14415
|
+
let parsed;
|
|
14416
|
+
try {
|
|
14417
|
+
parsed = JSON.parse(line);
|
|
14418
|
+
} catch {
|
|
14419
|
+
continue;
|
|
14420
|
+
}
|
|
14421
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
14422
|
+
if (typeof parsed.id !== "string" || !UUID_RE$1.test(parsed.id)) continue;
|
|
14423
|
+
if (typeof parsed.thread_name !== "string" || !parsed.thread_name.trim()) continue;
|
|
14424
|
+
names.set(parsed.id.toLowerCase(), parsed.thread_name.trim());
|
|
14425
|
+
}
|
|
14426
|
+
return names;
|
|
14427
|
+
}
|
|
14428
|
+
async function readSessionIndex(path) {
|
|
14429
|
+
if (!path) return /* @__PURE__ */ new Map();
|
|
14430
|
+
try {
|
|
14431
|
+
return parseCodexSessionIndex(await promises.readFile(path, "utf-8"));
|
|
14432
|
+
} catch {
|
|
14433
|
+
return /* @__PURE__ */ new Map();
|
|
14434
|
+
}
|
|
14435
|
+
}
|
|
14436
|
+
async function listCodexSessionHistory(options) {
|
|
14437
|
+
const limit = Math.max(1, Math.min(options.limit ?? 60, 500));
|
|
14438
|
+
const headBytes = Math.max(4096, options.headBytes ?? 256 * 1024);
|
|
14439
|
+
const promptChars = Math.max(20, options.promptChars ?? 200);
|
|
14440
|
+
const exclude = new Set(Array.from(options.exclude ?? [], (id) => id.toLowerCase()));
|
|
14441
|
+
const [candidates, names] = await Promise.all([
|
|
14442
|
+
collectCandidates(options.sessionsRoot, exclude),
|
|
14443
|
+
readSessionIndex(options.sessionIndexPath)
|
|
14444
|
+
]);
|
|
14445
|
+
const entries = [];
|
|
14446
|
+
let scanned = 0;
|
|
14447
|
+
let truncated = false;
|
|
14448
|
+
for (const candidate of candidates) {
|
|
14449
|
+
if (entries.length >= limit) {
|
|
14450
|
+
truncated = true;
|
|
14451
|
+
break;
|
|
14452
|
+
}
|
|
14453
|
+
scanned += 1;
|
|
14454
|
+
let head;
|
|
14455
|
+
try {
|
|
14456
|
+
head = await readHead(candidate.path, headBytes);
|
|
14457
|
+
} catch {
|
|
14458
|
+
continue;
|
|
14459
|
+
}
|
|
14460
|
+
const parsed = parseCodexHistoryHead(head, promptChars);
|
|
14461
|
+
if (!parsed) continue;
|
|
14462
|
+
if (parsed.originator === VERY_HAPPY_CODEX_ORIGINATOR) continue;
|
|
14463
|
+
if (options.directory && parsed.cwd !== options.directory) continue;
|
|
14464
|
+
const codexThreadId = parsed.id ?? candidate.id;
|
|
14465
|
+
if (exclude.has(codexThreadId)) continue;
|
|
14466
|
+
const { id: _id, ...rest } = parsed;
|
|
14467
|
+
const summary = names.get(codexThreadId);
|
|
14468
|
+
entries.push({
|
|
14469
|
+
codexThreadId,
|
|
14470
|
+
updatedAt: Math.round(candidate.updatedAt),
|
|
14471
|
+
sizeBytes: candidate.sizeBytes,
|
|
14472
|
+
...rest,
|
|
14473
|
+
...summary ? { summary: oneLine(summary, promptChars) } : {}
|
|
14474
|
+
});
|
|
14475
|
+
}
|
|
14476
|
+
return { entries, truncated, scanned };
|
|
14477
|
+
}
|
|
14478
|
+
|
|
13917
14479
|
function selectLowestLatencyRelay(candidates, probes) {
|
|
13918
14480
|
const rttById = new Map(probes.map((probe) => [probe.relayId, probe.rttMs]));
|
|
13919
14481
|
let selected = null;
|
|
@@ -14631,6 +15193,45 @@ class ApiMachineClient {
|
|
|
14631
15193
|
});
|
|
14632
15194
|
return { type: "success", ...result };
|
|
14633
15195
|
});
|
|
15196
|
+
this.rpcHandlerManager.registerHandler("codex-list-history", async (params) => {
|
|
15197
|
+
const { directory, limit, exclude } = params || {};
|
|
15198
|
+
if (directory !== void 0 && (typeof directory !== "string" || directory.length === 0)) {
|
|
15199
|
+
throw new Error("directory must be a non-empty string when provided");
|
|
15200
|
+
}
|
|
15201
|
+
const excludeIds = [
|
|
15202
|
+
...Array.isArray(exclude) ? exclude.filter((id) => typeof id === "string" && UUID_RE.test(id)) : [],
|
|
15203
|
+
...persistence.readTrackedCodexThreadIds()
|
|
15204
|
+
];
|
|
15205
|
+
const result = await listCodexSessionHistory({
|
|
15206
|
+
sessionsRoot: getCodexSessionsRoot(),
|
|
15207
|
+
sessionIndexPath: getCodexSessionIndexPath(),
|
|
15208
|
+
...typeof directory === "string" ? { directory } : {},
|
|
15209
|
+
limit: typeof limit === "number" && Number.isFinite(limit) ? Math.max(1, Math.min(Math.floor(limit), 200)) : 60,
|
|
15210
|
+
exclude: excludeIds
|
|
15211
|
+
});
|
|
15212
|
+
return { type: "success", ...result };
|
|
15213
|
+
});
|
|
15214
|
+
this.rpcHandlerManager.registerHandler("codex-import-session", async (params) => {
|
|
15215
|
+
const { directory, codexThreadId, approvedNewDirectoryCreation, permissionMode, title } = params || {};
|
|
15216
|
+
if (typeof directory !== "string" || directory.length === 0) {
|
|
15217
|
+
throw new Error("directory is required");
|
|
15218
|
+
}
|
|
15219
|
+
if (typeof codexThreadId !== "string" || !UUID_RE.test(codexThreadId)) {
|
|
15220
|
+
throw new Error("codexThreadId must be a valid UUID");
|
|
15221
|
+
}
|
|
15222
|
+
const result = await spawnSession({
|
|
15223
|
+
directory,
|
|
15224
|
+
machineId: this.machine.id,
|
|
15225
|
+
approvedNewDirectoryCreation: approvedNewDirectoryCreation === true,
|
|
15226
|
+
agent: "codex",
|
|
15227
|
+
importCodexThreadId: codexThreadId.toLowerCase(),
|
|
15228
|
+
permissionMode: typeof permissionMode === "string" ? permissionMode : void 0,
|
|
15229
|
+
importTitle: typeof title === "string" ? title : void 0
|
|
15230
|
+
});
|
|
15231
|
+
if (result.type !== "success") return result;
|
|
15232
|
+
persistence.logger.debug(`[API MACHINE] Importing Codex thread ${codexThreadId} as session ${result.sessionId}`);
|
|
15233
|
+
return { type: "success", sessionId: result.sessionId };
|
|
15234
|
+
});
|
|
14634
15235
|
this.rpcHandlerManager.registerHandler("codex-fork-thread", async (params) => {
|
|
14635
15236
|
const directory = requireNonEmptyString(params?.directory, "directory");
|
|
14636
15237
|
const codexThreadId = requireNonEmptyString(params?.codexThreadId, "codexThreadId");
|
|
@@ -14979,6 +15580,8 @@ class ApiMachineClient {
|
|
|
14979
15580
|
tmuxSessions: { rpcAvailable: true, detectedAt: now, killAttached: true },
|
|
14980
15581
|
// B-290 capability flag: `claude-list-history` (same restamp discipline).
|
|
14981
15582
|
claudeHistory: { rpcAvailable: true, detectedAt: now },
|
|
15583
|
+
// B-464 capability flag: `codex-list-history` / `codex-import-session`.
|
|
15584
|
+
codexHistory: { rpcAvailable: true, detectedAt: now },
|
|
14982
15585
|
// B-084: closed records survive daemon restarts (persisted
|
|
14983
15586
|
// in closed-terminals.json), so the connect snapshot ships
|
|
14984
15587
|
// them too — not just the incremental pushes.
|
|
@@ -16351,7 +16954,10 @@ exports.MIRROR_BACKFILL_LINES_DEFAULT = MIRROR_BACKFILL_LINES_DEFAULT;
|
|
|
16351
16954
|
exports.RawJSONLinesSchema = RawJSONLinesSchema;
|
|
16352
16955
|
exports.STREAM_DELTA_MAX_CHARS = STREAM_DELTA_MAX_CHARS;
|
|
16353
16956
|
exports.TITLE_PROMPT_PREFIX = TITLE_PROMPT_PREFIX;
|
|
16957
|
+
exports.TurnActivityTracker = TurnActivityTracker;
|
|
16354
16958
|
exports.api = api;
|
|
16959
|
+
exports.checkIfDaemonRunningAndCleanupStaleState = checkIfDaemonRunningAndCleanupStaleState;
|
|
16960
|
+
exports.cleanupDaemonState = cleanupDaemonState;
|
|
16355
16961
|
exports.commandOnPath = commandOnPath;
|
|
16356
16962
|
exports.compareExactVersions = compareExactVersions;
|
|
16357
16963
|
exports.connectionState = connectionState;
|
|
@@ -16371,21 +16977,31 @@ exports.getLocalHappyAgentCredentialPath = getLocalHappyAgentCredentialPath;
|
|
|
16371
16977
|
exports.getProjectPath = getProjectPath;
|
|
16372
16978
|
exports.initializeSandbox = initializeSandbox;
|
|
16373
16979
|
exports.isClaudeSessionId = isClaudeSessionId;
|
|
16980
|
+
exports.isDaemonRunningCurrentlyInstalledHappyVersion = isDaemonRunningCurrentlyInstalledHappyVersion;
|
|
16374
16981
|
exports.isValidSessionId = isValidSessionId;
|
|
16375
16982
|
exports.isValidTerminalId = isValidTerminalId;
|
|
16983
|
+
exports.listDaemonSessions = listDaemonSessions;
|
|
16376
16984
|
exports.listVhTerminals = listVhTerminals;
|
|
16377
16985
|
exports.mirrorLineKey = mirrorLineKey;
|
|
16378
16986
|
exports.mirrorLocalId = mirrorLocalId;
|
|
16987
|
+
exports.notifyDaemonClaudeAuthFailed = notifyDaemonClaudeAuthFailed;
|
|
16988
|
+
exports.notifyDaemonSessionEvent = notifyDaemonSessionEvent;
|
|
16989
|
+
exports.notifyDaemonSessionStarted = notifyDaemonSessionStarted;
|
|
16379
16990
|
exports.parseExactVersion = parseExactVersion;
|
|
16380
16991
|
exports.parseTerminalHookPayload = parseTerminalHookPayload;
|
|
16381
16992
|
exports.projectPath = projectPath;
|
|
16993
|
+
exports.pushClipboardViaDaemon = pushClipboardViaDaemon;
|
|
16382
16994
|
exports.readLocalHappyAgentCredentials = readLocalHappyAgentCredentials;
|
|
16383
16995
|
exports.readVhTerminal = readVhTerminal;
|
|
16384
16996
|
exports.resolveCliUpdateCheckInterval = resolveCliUpdateCheckInterval;
|
|
16385
16997
|
exports.scrubTmuxClientEnv = scrubTmuxClientEnv;
|
|
16386
16998
|
exports.sendToVhTerminal = sendToVhTerminal;
|
|
16999
|
+
exports.setTerminalTitleViaDaemon = setTerminalTitleViaDaemon;
|
|
16387
17000
|
exports.shellescape = shellescape;
|
|
17001
|
+
exports.spawnDaemonSession = spawnDaemonSession;
|
|
16388
17002
|
exports.startOfflineReconnection = startOfflineReconnection;
|
|
17003
|
+
exports.stopDaemon = stopDaemon;
|
|
17004
|
+
exports.stopDaemonSession = stopDaemonSession;
|
|
16389
17005
|
exports.streamKeyOf = streamKeyOf;
|
|
16390
17006
|
exports.summarizeSpawnSessionForLog = summarizeSpawnSessionForLog;
|
|
16391
17007
|
exports.tmuxArgs = tmuxArgs;
|