@episoda/cli 0.2.152 → 0.2.154
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/daemon/daemon-process.js +1549 -20
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -2913,7 +2913,7 @@ var require_package = __commonJS({
|
|
|
2913
2913
|
"package.json"(exports2, module2) {
|
|
2914
2914
|
module2.exports = {
|
|
2915
2915
|
name: "@episoda/cli",
|
|
2916
|
-
version: "0.2.
|
|
2916
|
+
version: "0.2.154",
|
|
2917
2917
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
2918
2918
|
main: "dist/index.js",
|
|
2919
2919
|
types: "dist/index.d.ts",
|
|
@@ -7754,8 +7754,9 @@ async function handleWorktreeCreate(request2) {
|
|
|
7754
7754
|
createBranch,
|
|
7755
7755
|
repoUrl,
|
|
7756
7756
|
envVars,
|
|
7757
|
-
setupScript
|
|
7757
|
+
setupScript,
|
|
7758
7758
|
// EP1229: detachedHead removed - planning worktrees no longer exist
|
|
7759
|
+
moduleType
|
|
7759
7760
|
} = request2;
|
|
7760
7761
|
console.log(`[Worktree] K1273: Creating worktree for ${moduleUid}`);
|
|
7761
7762
|
console.log(`[Worktree] Project: ${projectSlug}`);
|
|
@@ -7878,7 +7879,9 @@ ${buildCmd}` : buildCmd;
|
|
|
7878
7879
|
}
|
|
7879
7880
|
let previewUrl;
|
|
7880
7881
|
let port;
|
|
7881
|
-
if (
|
|
7882
|
+
if (moduleType === "ops") {
|
|
7883
|
+
console.log(`[Worktree] EP1363: Skipping preview for ops module ${moduleUid}`);
|
|
7884
|
+
} else if (finalStatus === "ready" && !isCloud) {
|
|
7882
7885
|
port = allocatePort(moduleUid);
|
|
7883
7886
|
console.log(`[Worktree] EP1143: Allocated port ${port} for ${moduleUid}`);
|
|
7884
7887
|
const previewManager = getPreviewManager();
|
|
@@ -8532,7 +8535,7 @@ function generateCodexMcpConfigToml(servers, projectPath) {
|
|
|
8532
8535
|
}
|
|
8533
8536
|
|
|
8534
8537
|
// src/agent/agent-manager.ts
|
|
8535
|
-
var
|
|
8538
|
+
var import_child_process14 = require("child_process");
|
|
8536
8539
|
var path20 = __toESM(require("path"));
|
|
8537
8540
|
var fs19 = __toESM(require("fs"));
|
|
8538
8541
|
var os7 = __toESM(require("os"));
|
|
@@ -8839,6 +8842,1057 @@ function generateClaudeConfig(options = {}) {
|
|
|
8839
8842
|
|
|
8840
8843
|
// src/agent/agent-manager.ts
|
|
8841
8844
|
var import_core11 = __toESM(require_dist());
|
|
8845
|
+
|
|
8846
|
+
// src/agent/claude-persistent-runtime.ts
|
|
8847
|
+
var import_child_process12 = require("child_process");
|
|
8848
|
+
var ECHO_TIMEOUT_MS = parseInt(process.env.AGENT_ECHO_TIMEOUT_MS || "10000", 10);
|
|
8849
|
+
var INACTIVITY_TIMEOUT_MS = parseInt(process.env.AGENT_STREAM_INACTIVITY_TIMEOUT_MS || "180000", 10);
|
|
8850
|
+
var SHUTDOWN_SIGTERM_WAIT_MS = 2e3;
|
|
8851
|
+
var SHUTDOWN_SIGKILL_WAIT_MS = 2e3;
|
|
8852
|
+
var STDIN_DRAIN_TIMEOUT_MS = 1e4;
|
|
8853
|
+
var RUNTIME_DEBUG = process.env.AGENT_RUNTIME_DEBUG === "1";
|
|
8854
|
+
var ClaudePersistentRuntime = class {
|
|
8855
|
+
constructor(options) {
|
|
8856
|
+
this.options = options;
|
|
8857
|
+
this.provider = "claude";
|
|
8858
|
+
this.process = null;
|
|
8859
|
+
this._turnState = "idle";
|
|
8860
|
+
this.alive = false;
|
|
8861
|
+
this.stuck = false;
|
|
8862
|
+
// Set when SIGKILL doesn't work (macOS kernel wait)
|
|
8863
|
+
// Current turn's callbacks — set on each sendMessage(), cleared on turn end
|
|
8864
|
+
this.callbacks = null;
|
|
8865
|
+
// Keep a small stderr tail so we can print it on failure without spamming logs.
|
|
8866
|
+
this.stderrTail = "";
|
|
8867
|
+
this.maxStderrTailBytes = 8192;
|
|
8868
|
+
// Stdout JSONL parsing state
|
|
8869
|
+
this.stdoutBuffer = "";
|
|
8870
|
+
this.parsedLineCount = 0;
|
|
8871
|
+
this.chunksSent = 0;
|
|
8872
|
+
this.hasContent = false;
|
|
8873
|
+
// For paragraph breaks between messages
|
|
8874
|
+
// EP1360: Per-turn TTFT tracking
|
|
8875
|
+
this.turnTtftLogged = false;
|
|
8876
|
+
// EP1360: tool_use_id dedup (#20583 — Claude Code can emit duplicate tool_use IDs)
|
|
8877
|
+
this.seenToolUseIds = /* @__PURE__ */ new Set();
|
|
8878
|
+
// Timers
|
|
8879
|
+
this.echoTimer = null;
|
|
8880
|
+
this.inactivityTimer = null;
|
|
8881
|
+
this.turnStartTime = 0;
|
|
8882
|
+
this.sawAnyStdoutThisTurn = false;
|
|
8883
|
+
// EP1360: Instrumentation
|
|
8884
|
+
this.spawnTimestamp = 0;
|
|
8885
|
+
this.sessionId = options.sessionId;
|
|
8886
|
+
}
|
|
8887
|
+
// -------------------------------------------------------------------------
|
|
8888
|
+
// Public interface
|
|
8889
|
+
// -------------------------------------------------------------------------
|
|
8890
|
+
get turnState() {
|
|
8891
|
+
return this._turnState;
|
|
8892
|
+
}
|
|
8893
|
+
get agentSessionId() {
|
|
8894
|
+
return this._agentSessionId;
|
|
8895
|
+
}
|
|
8896
|
+
get pid() {
|
|
8897
|
+
return this._pid;
|
|
8898
|
+
}
|
|
8899
|
+
/**
|
|
8900
|
+
* Spawn the persistent CLI process.
|
|
8901
|
+
* Must be called once before sendMessage(). Separated from constructor
|
|
8902
|
+
* because spawn is synchronous but we need to set up handlers.
|
|
8903
|
+
*/
|
|
8904
|
+
spawn() {
|
|
8905
|
+
if (this.process) {
|
|
8906
|
+
throw new Error(`[ClaudePersistentRuntime] Process already spawned for session ${this.sessionId}`);
|
|
8907
|
+
}
|
|
8908
|
+
const { binaryPath, args, env, cwd } = this.options;
|
|
8909
|
+
this.spawnTimestamp = Date.now();
|
|
8910
|
+
const spawnRssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
8911
|
+
console.log(`[ClaudePersistentRuntime] Spawning persistent Claude process for session ${this.sessionId}, RSS=${spawnRssMb}MB`);
|
|
8912
|
+
this.process = (0, import_child_process12.spawn)(binaryPath, args, {
|
|
8913
|
+
cwd,
|
|
8914
|
+
env,
|
|
8915
|
+
// CLAUDE_CODE_DISABLE_PLUGIN_CACHE=1 is set by agent-manager in envVars
|
|
8916
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
8917
|
+
});
|
|
8918
|
+
this._pid = this.process.pid;
|
|
8919
|
+
this.alive = true;
|
|
8920
|
+
this.process.stderr?.on("data", (data) => {
|
|
8921
|
+
const chunk = data.toString();
|
|
8922
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-this.maxStderrTailBytes);
|
|
8923
|
+
if (RUNTIME_DEBUG) {
|
|
8924
|
+
console.error(`[ClaudePersistentRuntime] stderr (session=${this.sessionId}): ${chunk.trimEnd()}`);
|
|
8925
|
+
}
|
|
8926
|
+
});
|
|
8927
|
+
this.process.stdout?.on("data", (data) => {
|
|
8928
|
+
this.resetInactivityTimer();
|
|
8929
|
+
this.stdoutBuffer += data.toString();
|
|
8930
|
+
const lines = this.stdoutBuffer.split("\n");
|
|
8931
|
+
this.stdoutBuffer = lines.pop() || "";
|
|
8932
|
+
for (const line of lines) {
|
|
8933
|
+
if (!line.trim()) continue;
|
|
8934
|
+
try {
|
|
8935
|
+
const parsed = JSON.parse(line);
|
|
8936
|
+
this.parsedLineCount++;
|
|
8937
|
+
if (this.parsedLineCount === 1) {
|
|
8938
|
+
const ttftMs = Date.now() - this.spawnTimestamp;
|
|
8939
|
+
console.log(`[ClaudePersistentRuntime] EP1360: TTFT (spawn to first JSON): ${ttftMs}ms, session=${this.sessionId}`);
|
|
8940
|
+
}
|
|
8941
|
+
this.handleParsedEvent(parsed);
|
|
8942
|
+
} catch {
|
|
8943
|
+
if (line.trim() && this.callbacks) {
|
|
8944
|
+
this.callbacks.onChunk(line + "\n");
|
|
8945
|
+
}
|
|
8946
|
+
}
|
|
8947
|
+
}
|
|
8948
|
+
});
|
|
8949
|
+
this.process.on("exit", (code, signal) => {
|
|
8950
|
+
const exitRssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
8951
|
+
console.log(`[ClaudePersistentRuntime] Process exited: code=${code}, signal=${signal}, parsedLines=${this.parsedLineCount}, session=${this.sessionId}, RSS=${exitRssMb}MB`);
|
|
8952
|
+
this.alive = false;
|
|
8953
|
+
this.process = null;
|
|
8954
|
+
if (this._turnState !== "idle" && this.callbacks) {
|
|
8955
|
+
const msg = signal ? `Claude process killed by ${signal}` : `Claude process exited with code ${code}`;
|
|
8956
|
+
if (this.stderrTail.trim()) {
|
|
8957
|
+
console.error(`[ClaudePersistentRuntime] stderr tail (session=${this.sessionId}): ${this.stderrTail.trimEnd()}`);
|
|
8958
|
+
}
|
|
8959
|
+
this.callbacks.onError(`PROCESS_EXIT: ${msg}`);
|
|
8960
|
+
this.endTurn();
|
|
8961
|
+
}
|
|
8962
|
+
});
|
|
8963
|
+
this.process.on("error", (error) => {
|
|
8964
|
+
console.error(`[ClaudePersistentRuntime] Spawn error: ${error.message}, session=${this.sessionId}`);
|
|
8965
|
+
this.alive = false;
|
|
8966
|
+
if (this.callbacks) {
|
|
8967
|
+
this.callbacks.onError(`SPAWN_ERROR: ${error.message}`);
|
|
8968
|
+
this.endTurn();
|
|
8969
|
+
}
|
|
8970
|
+
});
|
|
8971
|
+
}
|
|
8972
|
+
async sendMessage(message, callbacks) {
|
|
8973
|
+
if (this._turnState !== "idle") {
|
|
8974
|
+
throw new Error(
|
|
8975
|
+
`[ClaudePersistentRuntime] Cannot send message: turn state is '${this._turnState}', expected 'idle'. Turn serialization requires the previous turn to complete before sending a new message. session=${this.sessionId}`
|
|
8976
|
+
);
|
|
8977
|
+
}
|
|
8978
|
+
if (!this.alive || !this.process?.stdin) {
|
|
8979
|
+
throw new Error(
|
|
8980
|
+
`[ClaudePersistentRuntime] Cannot send message: process is not alive. session=${this.sessionId}`
|
|
8981
|
+
);
|
|
8982
|
+
}
|
|
8983
|
+
this.callbacks = callbacks;
|
|
8984
|
+
this.resultModel = void 0;
|
|
8985
|
+
this.resultCostUsd = void 0;
|
|
8986
|
+
this.resultUsage = void 0;
|
|
8987
|
+
this.resultNumTurns = void 0;
|
|
8988
|
+
this.turnStartTime = Date.now();
|
|
8989
|
+
this.turnTtftLogged = false;
|
|
8990
|
+
this.seenToolUseIds.clear();
|
|
8991
|
+
this.sawAnyStdoutThisTurn = false;
|
|
8992
|
+
this._turnState = "waiting_for_echo";
|
|
8993
|
+
const payload = JSON.stringify({ type: "user", message: { role: "user", content: message } }) + "\n";
|
|
8994
|
+
try {
|
|
8995
|
+
await this.writeToStdin(payload);
|
|
8996
|
+
} catch (err) {
|
|
8997
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8998
|
+
this.callbacks.onError(`SYNC_RUNTIME_ERROR: stdin write failed: ${msg}`);
|
|
8999
|
+
this.endTurn();
|
|
9000
|
+
throw err;
|
|
9001
|
+
}
|
|
9002
|
+
this.echoTimer = setTimeout(() => {
|
|
9003
|
+
if (this._turnState === "waiting_for_echo") {
|
|
9004
|
+
console.warn(`[ClaudePersistentRuntime] Echo timeout after ${ECHO_TIMEOUT_MS}ms \u2014 auto-degrading. session=${this.sessionId}`);
|
|
9005
|
+
if (this.callbacks) {
|
|
9006
|
+
this.callbacks.onError(`ECHO_TIMEOUT: No stdout event within ${ECHO_TIMEOUT_MS}ms after send`);
|
|
9007
|
+
this.endTurn();
|
|
9008
|
+
}
|
|
9009
|
+
}
|
|
9010
|
+
}, ECHO_TIMEOUT_MS);
|
|
9011
|
+
this.resetInactivityTimer();
|
|
9012
|
+
}
|
|
9013
|
+
isAlive() {
|
|
9014
|
+
return this.alive && !this.stuck;
|
|
9015
|
+
}
|
|
9016
|
+
async shutdown() {
|
|
9017
|
+
this.clearTimers();
|
|
9018
|
+
if (!this.process || !this.alive) {
|
|
9019
|
+
return;
|
|
9020
|
+
}
|
|
9021
|
+
console.log(`[ClaudePersistentRuntime] Shutting down session ${this.sessionId} (3-stage)`);
|
|
9022
|
+
try {
|
|
9023
|
+
this.process.stdin?.end();
|
|
9024
|
+
} catch {
|
|
9025
|
+
}
|
|
9026
|
+
const exited = await this.waitForExit(SHUTDOWN_SIGTERM_WAIT_MS);
|
|
9027
|
+
if (exited) return;
|
|
9028
|
+
console.log(`[ClaudePersistentRuntime] SIGTERM for session ${this.sessionId}`);
|
|
9029
|
+
try {
|
|
9030
|
+
this.process.kill("SIGTERM");
|
|
9031
|
+
} catch {
|
|
9032
|
+
return;
|
|
9033
|
+
}
|
|
9034
|
+
const exitedAfterTerm = await this.waitForExit(SHUTDOWN_SIGKILL_WAIT_MS);
|
|
9035
|
+
if (exitedAfterTerm) return;
|
|
9036
|
+
console.log(`[ClaudePersistentRuntime] SIGKILL for session ${this.sessionId}`);
|
|
9037
|
+
try {
|
|
9038
|
+
this.process.kill("SIGKILL");
|
|
9039
|
+
} catch {
|
|
9040
|
+
return;
|
|
9041
|
+
}
|
|
9042
|
+
const exitedAfterKill = await this.waitForExit(2e3);
|
|
9043
|
+
if (!exitedAfterKill) {
|
|
9044
|
+
console.error(`[ClaudePersistentRuntime] Process did not exit after SIGKILL \u2014 marking as stuck. session=${this.sessionId}`);
|
|
9045
|
+
this.stuck = true;
|
|
9046
|
+
this.alive = false;
|
|
9047
|
+
}
|
|
9048
|
+
}
|
|
9049
|
+
// -------------------------------------------------------------------------
|
|
9050
|
+
// Event handling
|
|
9051
|
+
// -------------------------------------------------------------------------
|
|
9052
|
+
handleParsedEvent(parsed) {
|
|
9053
|
+
const type = parsed.type;
|
|
9054
|
+
if (this._turnState === "waiting_for_echo") {
|
|
9055
|
+
this.sawAnyStdoutThisTurn = true;
|
|
9056
|
+
this.clearEchoTimer();
|
|
9057
|
+
this._turnState = "streaming";
|
|
9058
|
+
if (RUNTIME_DEBUG) {
|
|
9059
|
+
console.log(`[ClaudePersistentRuntime] First stdout for turn \u2014 streaming started. session=${this.sessionId}`);
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
9062
|
+
if (type === "user") {
|
|
9063
|
+
return;
|
|
9064
|
+
}
|
|
9065
|
+
if (!this.callbacks) return;
|
|
9066
|
+
switch (type) {
|
|
9067
|
+
case "stream_event": {
|
|
9068
|
+
const ev = parsed.event;
|
|
9069
|
+
const evType = ev?.type;
|
|
9070
|
+
if (!evType) break;
|
|
9071
|
+
if (evType === "content_block_delta") {
|
|
9072
|
+
const delta = ev?.delta;
|
|
9073
|
+
const text = delta?.text ?? "";
|
|
9074
|
+
if (text) {
|
|
9075
|
+
this.chunksSent++;
|
|
9076
|
+
if (!this.turnTtftLogged) {
|
|
9077
|
+
this.turnTtftLogged = true;
|
|
9078
|
+
const turnTtft = Date.now() - this.turnStartTime;
|
|
9079
|
+
console.log(`[ClaudePersistentRuntime] EP1360: Turn TTFT: ${turnTtft}ms, session=${this.sessionId}`);
|
|
9080
|
+
}
|
|
9081
|
+
this.callbacks.onChunk(text);
|
|
9082
|
+
}
|
|
9083
|
+
}
|
|
9084
|
+
if (evType === "content_block_start" && this.callbacks.onToolUse) {
|
|
9085
|
+
const block = ev?.content_block;
|
|
9086
|
+
if (block?.type === "tool_use") {
|
|
9087
|
+
const toolId = block.id;
|
|
9088
|
+
if (toolId && this.seenToolUseIds.has(toolId)) break;
|
|
9089
|
+
if (toolId) this.seenToolUseIds.add(toolId);
|
|
9090
|
+
const toolEvent = {
|
|
9091
|
+
id: toolId || `tool-${Date.now()}`,
|
|
9092
|
+
name: block.name,
|
|
9093
|
+
input: block.input || {}
|
|
9094
|
+
};
|
|
9095
|
+
this.callbacks.onToolUse(toolEvent);
|
|
9096
|
+
}
|
|
9097
|
+
}
|
|
9098
|
+
break;
|
|
9099
|
+
}
|
|
9100
|
+
case "assistant": {
|
|
9101
|
+
const content = parsed.message?.content;
|
|
9102
|
+
if (content) {
|
|
9103
|
+
const shouldEmitText = this.chunksSent === 0;
|
|
9104
|
+
if (shouldEmitText && this.hasContent) {
|
|
9105
|
+
this.callbacks.onChunk("\n\n");
|
|
9106
|
+
}
|
|
9107
|
+
for (const block of content) {
|
|
9108
|
+
if (block.type === "text" && block.text && shouldEmitText) {
|
|
9109
|
+
this.hasContent = true;
|
|
9110
|
+
this.chunksSent++;
|
|
9111
|
+
if (!this.turnTtftLogged) {
|
|
9112
|
+
this.turnTtftLogged = true;
|
|
9113
|
+
const turnTtft = Date.now() - this.turnStartTime;
|
|
9114
|
+
console.log(`[ClaudePersistentRuntime] EP1360: Turn TTFT: ${turnTtft}ms, session=${this.sessionId}`);
|
|
9115
|
+
}
|
|
9116
|
+
this.callbacks.onChunk(block.text);
|
|
9117
|
+
} else if (block.type === "tool_use" && this.callbacks.onToolUse) {
|
|
9118
|
+
const toolId = block.id;
|
|
9119
|
+
if (toolId && this.seenToolUseIds.has(toolId)) {
|
|
9120
|
+
continue;
|
|
9121
|
+
}
|
|
9122
|
+
if (toolId) {
|
|
9123
|
+
this.seenToolUseIds.add(toolId);
|
|
9124
|
+
}
|
|
9125
|
+
const toolEvent = {
|
|
9126
|
+
id: toolId,
|
|
9127
|
+
name: block.name,
|
|
9128
|
+
input: block.input || {}
|
|
9129
|
+
};
|
|
9130
|
+
this.callbacks.onToolUse(toolEvent);
|
|
9131
|
+
} else if (block.type === "tool_result" && this.callbacks.onToolResult) {
|
|
9132
|
+
const toolResult = {
|
|
9133
|
+
id: block.tool_use_id || block.id || `tool-result-${Date.now()}`,
|
|
9134
|
+
name: block.name,
|
|
9135
|
+
output: block.output ?? block.result ?? block.content ?? block
|
|
9136
|
+
};
|
|
9137
|
+
this.callbacks.onToolResult(toolResult);
|
|
9138
|
+
}
|
|
9139
|
+
}
|
|
9140
|
+
}
|
|
9141
|
+
break;
|
|
9142
|
+
}
|
|
9143
|
+
case "content_block_delta": {
|
|
9144
|
+
const delta = parsed.delta;
|
|
9145
|
+
if (delta?.text) {
|
|
9146
|
+
this.chunksSent++;
|
|
9147
|
+
if (!this.turnTtftLogged) {
|
|
9148
|
+
this.turnTtftLogged = true;
|
|
9149
|
+
const turnTtft = Date.now() - this.turnStartTime;
|
|
9150
|
+
console.log(`[ClaudePersistentRuntime] EP1360: Turn TTFT: ${turnTtft}ms, session=${this.sessionId}`);
|
|
9151
|
+
}
|
|
9152
|
+
this.callbacks.onChunk(delta.text);
|
|
9153
|
+
}
|
|
9154
|
+
break;
|
|
9155
|
+
}
|
|
9156
|
+
case "tool_result": {
|
|
9157
|
+
if (this.callbacks.onToolResult) {
|
|
9158
|
+
const toolResult = {
|
|
9159
|
+
id: parsed.tool_use_id || parsed.toolUseId || parsed.id || `tool-result-${Date.now()}`,
|
|
9160
|
+
name: parsed.name || parsed.tool_name,
|
|
9161
|
+
output: parsed.output ?? parsed.result ?? parsed.content ?? parsed.message ?? parsed
|
|
9162
|
+
};
|
|
9163
|
+
this.callbacks.onToolResult(toolResult);
|
|
9164
|
+
}
|
|
9165
|
+
break;
|
|
9166
|
+
}
|
|
9167
|
+
case "result": {
|
|
9168
|
+
if (parsed.session_id) {
|
|
9169
|
+
this._agentSessionId = parsed.session_id;
|
|
9170
|
+
}
|
|
9171
|
+
const resultObj = parsed.result;
|
|
9172
|
+
if (resultObj?.session_id) {
|
|
9173
|
+
this._agentSessionId = resultObj.session_id;
|
|
9174
|
+
}
|
|
9175
|
+
this.resultModel = parsed.model;
|
|
9176
|
+
if (typeof parsed.total_cost_usd === "number") {
|
|
9177
|
+
this.resultCostUsd = parsed.total_cost_usd;
|
|
9178
|
+
} else if (typeof parsed.cost_usd === "number") {
|
|
9179
|
+
this.resultCostUsd = parsed.cost_usd;
|
|
9180
|
+
}
|
|
9181
|
+
this.resultNumTurns = typeof parsed.num_turns === "number" ? parsed.num_turns : void 0;
|
|
9182
|
+
if (parsed.usage) {
|
|
9183
|
+
const u = parsed.usage;
|
|
9184
|
+
const inputTokens = u.input_tokens ?? u.prompt_tokens ?? 0;
|
|
9185
|
+
const outputTokens = u.output_tokens ?? u.completion_tokens ?? 0;
|
|
9186
|
+
this.resultUsage = {
|
|
9187
|
+
inputTokens,
|
|
9188
|
+
outputTokens,
|
|
9189
|
+
cacheReadTokens: u.cache_read_input_tokens ?? u.cache_read_tokens,
|
|
9190
|
+
cacheWriteTokens: u.cache_creation_input_tokens ?? u.cache_write_tokens,
|
|
9191
|
+
totalTokens: inputTokens + outputTokens
|
|
9192
|
+
};
|
|
9193
|
+
}
|
|
9194
|
+
this.completeTurn();
|
|
9195
|
+
break;
|
|
9196
|
+
}
|
|
9197
|
+
case "system": {
|
|
9198
|
+
if (parsed.session_id) {
|
|
9199
|
+
this._agentSessionId = parsed.session_id;
|
|
9200
|
+
}
|
|
9201
|
+
if (parsed.subtype === "compact_boundary" && this.callbacks.onCompaction) {
|
|
9202
|
+
const metadata = parsed.compactMetadata;
|
|
9203
|
+
const compactionEvent = {
|
|
9204
|
+
seq: this.parsedLineCount,
|
|
9205
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9206
|
+
provider: "claude",
|
|
9207
|
+
type: "session.compaction",
|
|
9208
|
+
raw: parsed,
|
|
9209
|
+
trigger: metadata?.trigger ?? "auto",
|
|
9210
|
+
preCompactionTokens: metadata?.preCompactTokenCount
|
|
9211
|
+
};
|
|
9212
|
+
console.log(`[ClaudePersistentRuntime] Compaction detected \u2014 trigger: ${compactionEvent.trigger}, session=${this.sessionId}`);
|
|
9213
|
+
this.callbacks.onCompaction(compactionEvent);
|
|
9214
|
+
}
|
|
9215
|
+
break;
|
|
9216
|
+
}
|
|
9217
|
+
case "error": {
|
|
9218
|
+
const errorMsg = parsed.error?.message || parsed.message || "Unknown error from Claude Code";
|
|
9219
|
+
this.callbacks.onError(errorMsg);
|
|
9220
|
+
this.endTurn();
|
|
9221
|
+
break;
|
|
9222
|
+
}
|
|
9223
|
+
default:
|
|
9224
|
+
if (type) {
|
|
9225
|
+
console.log(`[ClaudePersistentRuntime] Unhandled event: ${type}, session=${this.sessionId}`);
|
|
9226
|
+
}
|
|
9227
|
+
}
|
|
9228
|
+
}
|
|
9229
|
+
// -------------------------------------------------------------------------
|
|
9230
|
+
// Turn lifecycle
|
|
9231
|
+
// -------------------------------------------------------------------------
|
|
9232
|
+
completeTurn() {
|
|
9233
|
+
if (!this.callbacks) return;
|
|
9234
|
+
const durationMs = Date.now() - this.turnStartTime;
|
|
9235
|
+
const resultMeta = this.resultModel || this.resultCostUsd !== void 0 || this.resultUsage ? {
|
|
9236
|
+
model: this.resultModel,
|
|
9237
|
+
costUsd: this.resultCostUsd,
|
|
9238
|
+
durationMs,
|
|
9239
|
+
numTurns: this.resultNumTurns,
|
|
9240
|
+
usage: this.resultUsage
|
|
9241
|
+
} : void 0;
|
|
9242
|
+
console.log(`[ClaudePersistentRuntime] Turn completed: duration=${durationMs}ms, chunks=${this.chunksSent}, session=${this.sessionId}`);
|
|
9243
|
+
const cb = this.callbacks;
|
|
9244
|
+
this.endTurn();
|
|
9245
|
+
cb.onComplete(this._agentSessionId, resultMeta);
|
|
9246
|
+
}
|
|
9247
|
+
endTurn() {
|
|
9248
|
+
this.clearTimers();
|
|
9249
|
+
this.callbacks = null;
|
|
9250
|
+
this._turnState = "idle";
|
|
9251
|
+
this.chunksSent = 0;
|
|
9252
|
+
}
|
|
9253
|
+
// -------------------------------------------------------------------------
|
|
9254
|
+
// stdin
|
|
9255
|
+
// -------------------------------------------------------------------------
|
|
9256
|
+
writeToStdin(data) {
|
|
9257
|
+
if (!this.process?.stdin || this.process.stdin.destroyed) {
|
|
9258
|
+
throw new Error(`[ClaudePersistentRuntime] stdin not available. session=${this.sessionId}`);
|
|
9259
|
+
}
|
|
9260
|
+
return new Promise((resolve4, reject) => {
|
|
9261
|
+
const stdin = this.process.stdin;
|
|
9262
|
+
const ok = stdin.write(data);
|
|
9263
|
+
if (ok) {
|
|
9264
|
+
resolve4();
|
|
9265
|
+
return;
|
|
9266
|
+
}
|
|
9267
|
+
const onDrain = () => {
|
|
9268
|
+
cleanup();
|
|
9269
|
+
resolve4();
|
|
9270
|
+
};
|
|
9271
|
+
const onError = (err) => {
|
|
9272
|
+
cleanup();
|
|
9273
|
+
reject(err);
|
|
9274
|
+
};
|
|
9275
|
+
const onTimeout = () => {
|
|
9276
|
+
cleanup();
|
|
9277
|
+
reject(new Error(`STDIN_DRAIN_TIMEOUT after ${STDIN_DRAIN_TIMEOUT_MS}ms`));
|
|
9278
|
+
};
|
|
9279
|
+
const cleanup = () => {
|
|
9280
|
+
clearTimeout(timer);
|
|
9281
|
+
stdin.off("drain", onDrain);
|
|
9282
|
+
stdin.off("error", onError);
|
|
9283
|
+
};
|
|
9284
|
+
const timer = setTimeout(onTimeout, STDIN_DRAIN_TIMEOUT_MS);
|
|
9285
|
+
stdin.once("drain", onDrain);
|
|
9286
|
+
stdin.once("error", onError);
|
|
9287
|
+
});
|
|
9288
|
+
}
|
|
9289
|
+
// -------------------------------------------------------------------------
|
|
9290
|
+
// Timers
|
|
9291
|
+
// -------------------------------------------------------------------------
|
|
9292
|
+
clearEchoTimer() {
|
|
9293
|
+
if (this.echoTimer) {
|
|
9294
|
+
clearTimeout(this.echoTimer);
|
|
9295
|
+
this.echoTimer = null;
|
|
9296
|
+
}
|
|
9297
|
+
}
|
|
9298
|
+
resetInactivityTimer() {
|
|
9299
|
+
if (this.inactivityTimer) {
|
|
9300
|
+
clearTimeout(this.inactivityTimer);
|
|
9301
|
+
}
|
|
9302
|
+
if (this._turnState === "idle") return;
|
|
9303
|
+
this.inactivityTimer = setTimeout(() => {
|
|
9304
|
+
if (this._turnState !== "idle") {
|
|
9305
|
+
console.warn(`[ClaudePersistentRuntime] Inactivity timeout (${INACTIVITY_TIMEOUT_MS}ms) \u2014 no stdout during streaming. session=${this.sessionId}`);
|
|
9306
|
+
if (this.callbacks) {
|
|
9307
|
+
this.callbacks.onError(`INACTIVITY_TIMEOUT: No stdout for ${INACTIVITY_TIMEOUT_MS}ms during streaming`);
|
|
9308
|
+
this.endTurn();
|
|
9309
|
+
}
|
|
9310
|
+
}
|
|
9311
|
+
}, INACTIVITY_TIMEOUT_MS);
|
|
9312
|
+
}
|
|
9313
|
+
clearTimers() {
|
|
9314
|
+
this.clearEchoTimer();
|
|
9315
|
+
if (this.inactivityTimer) {
|
|
9316
|
+
clearTimeout(this.inactivityTimer);
|
|
9317
|
+
this.inactivityTimer = null;
|
|
9318
|
+
}
|
|
9319
|
+
}
|
|
9320
|
+
// -------------------------------------------------------------------------
|
|
9321
|
+
// Helpers
|
|
9322
|
+
// -------------------------------------------------------------------------
|
|
9323
|
+
waitForExit(timeoutMs) {
|
|
9324
|
+
return new Promise((resolve4) => {
|
|
9325
|
+
if (!this.process || !this.alive) {
|
|
9326
|
+
resolve4(true);
|
|
9327
|
+
return;
|
|
9328
|
+
}
|
|
9329
|
+
const onExit = () => {
|
|
9330
|
+
clearTimeout(timer);
|
|
9331
|
+
resolve4(true);
|
|
9332
|
+
};
|
|
9333
|
+
const timer = setTimeout(() => {
|
|
9334
|
+
this.process?.removeListener("exit", onExit);
|
|
9335
|
+
resolve4(false);
|
|
9336
|
+
}, timeoutMs);
|
|
9337
|
+
this.process.once("exit", onExit);
|
|
9338
|
+
});
|
|
9339
|
+
}
|
|
9340
|
+
};
|
|
9341
|
+
|
|
9342
|
+
// src/agent/codex-persistent-runtime.ts
|
|
9343
|
+
var import_child_process13 = require("child_process");
|
|
9344
|
+
var INIT_TIMEOUT_MS = 3e4;
|
|
9345
|
+
var INACTIVITY_TIMEOUT_MS2 = parseInt(process.env.AGENT_STREAM_INACTIVITY_TIMEOUT_MS || "180000", 10);
|
|
9346
|
+
var SHUTDOWN_SIGTERM_WAIT_MS2 = 2e3;
|
|
9347
|
+
var SHUTDOWN_SIGKILL_WAIT_MS2 = 2e3;
|
|
9348
|
+
var STDIN_DRAIN_TIMEOUT_MS2 = 1e4;
|
|
9349
|
+
var RUNTIME_DEBUG2 = process.env.AGENT_RUNTIME_DEBUG === "1";
|
|
9350
|
+
var CodexPersistentRuntime = class {
|
|
9351
|
+
constructor(options) {
|
|
9352
|
+
this.options = options;
|
|
9353
|
+
this.provider = "codex";
|
|
9354
|
+
this.process = null;
|
|
9355
|
+
this._turnState = "idle";
|
|
9356
|
+
this.alive = false;
|
|
9357
|
+
this.stuck = false;
|
|
9358
|
+
this.callbacks = null;
|
|
9359
|
+
// Keep a small stderr tail so we can print it on failure without spamming logs.
|
|
9360
|
+
this.stderrTail = "";
|
|
9361
|
+
this.maxStderrTailBytes = 8192;
|
|
9362
|
+
this.stdoutBuffer = "";
|
|
9363
|
+
this.parsedLineCount = 0;
|
|
9364
|
+
// Per-turn accounting
|
|
9365
|
+
this.turnStartTime = 0;
|
|
9366
|
+
this.chunksSent = 0;
|
|
9367
|
+
this.turnTtftLogged = false;
|
|
9368
|
+
// Timers
|
|
9369
|
+
this.inactivityTimer = null;
|
|
9370
|
+
// Spawn instrumentation
|
|
9371
|
+
this.spawnTimestamp = 0;
|
|
9372
|
+
// JSON-RPC state
|
|
9373
|
+
this.nextId = 1;
|
|
9374
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
9375
|
+
this.initializing = null;
|
|
9376
|
+
this.initialized = false;
|
|
9377
|
+
this.threadIdWaiters = [];
|
|
9378
|
+
this.debugTraceCount = 0;
|
|
9379
|
+
this.sessionId = options.sessionId;
|
|
9380
|
+
}
|
|
9381
|
+
get turnState() {
|
|
9382
|
+
return this._turnState;
|
|
9383
|
+
}
|
|
9384
|
+
get agentSessionId() {
|
|
9385
|
+
return this._agentSessionId;
|
|
9386
|
+
}
|
|
9387
|
+
get pid() {
|
|
9388
|
+
return this._pid;
|
|
9389
|
+
}
|
|
9390
|
+
spawn() {
|
|
9391
|
+
if (this.process) {
|
|
9392
|
+
throw new Error(`[CodexPersistentRuntime] Process already spawned for session ${this.sessionId}`);
|
|
9393
|
+
}
|
|
9394
|
+
const { binaryPath, args, env, cwd } = this.options;
|
|
9395
|
+
this.spawnTimestamp = Date.now();
|
|
9396
|
+
const spawnRssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
9397
|
+
console.log(`[CodexPersistentRuntime] Spawning persistent Codex app-server for session ${this.sessionId}, RSS=${spawnRssMb}MB`);
|
|
9398
|
+
this.process = (0, import_child_process13.spawn)(binaryPath, args, {
|
|
9399
|
+
cwd,
|
|
9400
|
+
env,
|
|
9401
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
9402
|
+
});
|
|
9403
|
+
this._pid = this.process.pid;
|
|
9404
|
+
this.alive = true;
|
|
9405
|
+
this.process.stderr?.on("data", (data) => {
|
|
9406
|
+
const chunk = data.toString();
|
|
9407
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-this.maxStderrTailBytes);
|
|
9408
|
+
if (RUNTIME_DEBUG2) {
|
|
9409
|
+
console.error(`[CodexPersistentRuntime] stderr (session=${this.sessionId}): ${chunk.trimEnd()}`);
|
|
9410
|
+
}
|
|
9411
|
+
});
|
|
9412
|
+
this.process.stdout?.on("data", (data) => {
|
|
9413
|
+
this.resetInactivityTimer();
|
|
9414
|
+
this.stdoutBuffer += data.toString();
|
|
9415
|
+
const lines = this.stdoutBuffer.split("\n");
|
|
9416
|
+
this.stdoutBuffer = lines.pop() || "";
|
|
9417
|
+
for (const line of lines) {
|
|
9418
|
+
if (!line.trim()) continue;
|
|
9419
|
+
try {
|
|
9420
|
+
const parsed = JSON.parse(line);
|
|
9421
|
+
this.parsedLineCount++;
|
|
9422
|
+
if (this.parsedLineCount === 1) {
|
|
9423
|
+
const ttftMs = Date.now() - this.spawnTimestamp;
|
|
9424
|
+
console.log(`[CodexPersistentRuntime] EP1360: TTFT (spawn to first JSON): ${ttftMs}ms, session=${this.sessionId}`);
|
|
9425
|
+
}
|
|
9426
|
+
this.handleJsonRpcMessage(parsed);
|
|
9427
|
+
} catch {
|
|
9428
|
+
if (line.trim() && this.callbacks) {
|
|
9429
|
+
this.callbacks.onChunk(line + "\n");
|
|
9430
|
+
}
|
|
9431
|
+
}
|
|
9432
|
+
}
|
|
9433
|
+
});
|
|
9434
|
+
this.process.on("exit", (code, signal) => {
|
|
9435
|
+
const exitRssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
9436
|
+
console.log(`[CodexPersistentRuntime] Process exited: code=${code}, signal=${signal}, parsedLines=${this.parsedLineCount}, session=${this.sessionId}, RSS=${exitRssMb}MB`);
|
|
9437
|
+
this.alive = false;
|
|
9438
|
+
this.process = null;
|
|
9439
|
+
for (const [id, p] of this.pending) {
|
|
9440
|
+
clearTimeout(p.timeout);
|
|
9441
|
+
p.reject(new Error(`Codex app-server exited (id=${id})`));
|
|
9442
|
+
}
|
|
9443
|
+
this.pending.clear();
|
|
9444
|
+
if (this._turnState !== "idle" && this.callbacks) {
|
|
9445
|
+
const msg = signal ? `Codex process killed by ${signal}` : `Codex process exited with code ${code}`;
|
|
9446
|
+
if (this.stderrTail.trim()) {
|
|
9447
|
+
console.error(`[CodexPersistentRuntime] stderr tail (session=${this.sessionId}): ${this.stderrTail.trimEnd()}`);
|
|
9448
|
+
}
|
|
9449
|
+
this.callbacks.onError(`PROCESS_EXIT: ${msg}`);
|
|
9450
|
+
this.endTurn();
|
|
9451
|
+
}
|
|
9452
|
+
});
|
|
9453
|
+
this.process.on("error", (error) => {
|
|
9454
|
+
console.error(`[CodexPersistentRuntime] Spawn error: ${error.message}, session=${this.sessionId}`);
|
|
9455
|
+
this.alive = false;
|
|
9456
|
+
for (const [id, p] of this.pending) {
|
|
9457
|
+
clearTimeout(p.timeout);
|
|
9458
|
+
p.reject(error);
|
|
9459
|
+
}
|
|
9460
|
+
this.pending.clear();
|
|
9461
|
+
if (this.callbacks) {
|
|
9462
|
+
this.callbacks.onError(`SPAWN_ERROR: ${error.message}`);
|
|
9463
|
+
this.endTurn();
|
|
9464
|
+
}
|
|
9465
|
+
});
|
|
9466
|
+
}
|
|
9467
|
+
async sendMessage(message, callbacks) {
|
|
9468
|
+
if (this._turnState !== "idle") {
|
|
9469
|
+
throw new Error(
|
|
9470
|
+
`[CodexPersistentRuntime] Cannot send message: turn state is '${this._turnState}', expected 'idle'. session=${this.sessionId}`
|
|
9471
|
+
);
|
|
9472
|
+
}
|
|
9473
|
+
if (!this.alive || !this.process?.stdin) {
|
|
9474
|
+
throw new Error(`[CodexPersistentRuntime] Cannot send message: process is not alive. session=${this.sessionId}`);
|
|
9475
|
+
}
|
|
9476
|
+
this.callbacks = callbacks;
|
|
9477
|
+
this.turnStartTime = Date.now();
|
|
9478
|
+
this.chunksSent = 0;
|
|
9479
|
+
this.turnTtftLogged = false;
|
|
9480
|
+
this.resultModel = void 0;
|
|
9481
|
+
this.resultUsage = void 0;
|
|
9482
|
+
this.resultNumTurns = void 0;
|
|
9483
|
+
this._turnState = "streaming";
|
|
9484
|
+
void (async () => {
|
|
9485
|
+
try {
|
|
9486
|
+
await this.ensureInitialized();
|
|
9487
|
+
const threadId = this._agentSessionId;
|
|
9488
|
+
if (!threadId) {
|
|
9489
|
+
throw new Error("Missing threadId after initialization");
|
|
9490
|
+
}
|
|
9491
|
+
await this.sendRequest("turn/start", {
|
|
9492
|
+
threadId,
|
|
9493
|
+
input: [{ type: "text", text: message }]
|
|
9494
|
+
}, INIT_TIMEOUT_MS);
|
|
9495
|
+
} catch (err) {
|
|
9496
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9497
|
+
if (this.callbacks) {
|
|
9498
|
+
this.callbacks.onError(`SYNC_RUNTIME_ERROR: ${msg}`);
|
|
9499
|
+
this.endTurn();
|
|
9500
|
+
}
|
|
9501
|
+
}
|
|
9502
|
+
})();
|
|
9503
|
+
this.resetInactivityTimer();
|
|
9504
|
+
}
|
|
9505
|
+
isAlive() {
|
|
9506
|
+
return this.alive && !this.stuck;
|
|
9507
|
+
}
|
|
9508
|
+
async shutdown() {
|
|
9509
|
+
this.clearTimers();
|
|
9510
|
+
if (!this.process || !this.alive) return;
|
|
9511
|
+
console.log(`[CodexPersistentRuntime] Shutting down session ${this.sessionId} (3-stage)`);
|
|
9512
|
+
try {
|
|
9513
|
+
this.process.stdin?.end();
|
|
9514
|
+
} catch {
|
|
9515
|
+
}
|
|
9516
|
+
const exited = await this.waitForExit(SHUTDOWN_SIGTERM_WAIT_MS2);
|
|
9517
|
+
if (exited) return;
|
|
9518
|
+
console.log(`[CodexPersistentRuntime] SIGTERM for session ${this.sessionId}`);
|
|
9519
|
+
try {
|
|
9520
|
+
this.process.kill("SIGTERM");
|
|
9521
|
+
} catch {
|
|
9522
|
+
return;
|
|
9523
|
+
}
|
|
9524
|
+
const exitedAfterTerm = await this.waitForExit(SHUTDOWN_SIGKILL_WAIT_MS2);
|
|
9525
|
+
if (exitedAfterTerm) return;
|
|
9526
|
+
console.log(`[CodexPersistentRuntime] SIGKILL for session ${this.sessionId}`);
|
|
9527
|
+
try {
|
|
9528
|
+
this.process.kill("SIGKILL");
|
|
9529
|
+
} catch {
|
|
9530
|
+
return;
|
|
9531
|
+
}
|
|
9532
|
+
const exitedAfterKill = await this.waitForExit(2e3);
|
|
9533
|
+
if (!exitedAfterKill) {
|
|
9534
|
+
console.error(`[CodexPersistentRuntime] Process did not exit after SIGKILL \u2014 marking as stuck. session=${this.sessionId}`);
|
|
9535
|
+
this.stuck = true;
|
|
9536
|
+
this.alive = false;
|
|
9537
|
+
}
|
|
9538
|
+
}
|
|
9539
|
+
// -------------------------------------------------------------------------
|
|
9540
|
+
// JSON-RPC plumbing
|
|
9541
|
+
// -------------------------------------------------------------------------
|
|
9542
|
+
handleJsonRpcMessage(msg) {
|
|
9543
|
+
if (RUNTIME_DEBUG2 && this.debugTraceCount < 20) {
|
|
9544
|
+
this.debugTraceCount++;
|
|
9545
|
+
const kind = typeof msg.id === "number" ? "response" : typeof msg.method === "string" ? `notif:${msg.method}` : "unknown";
|
|
9546
|
+
console.log(`[CodexPersistentRuntime] debug json-rpc (${kind}) session=${this.sessionId}: ${JSON.stringify(msg).slice(0, 2e3)}`);
|
|
9547
|
+
}
|
|
9548
|
+
const isResponse = typeof msg.id === "number" && (msg.result !== void 0 || msg.error !== void 0);
|
|
9549
|
+
if (isResponse) {
|
|
9550
|
+
const id = msg.id;
|
|
9551
|
+
const pending = this.pending.get(id);
|
|
9552
|
+
if (!pending) return;
|
|
9553
|
+
clearTimeout(pending.timeout);
|
|
9554
|
+
this.pending.delete(id);
|
|
9555
|
+
if (msg.error) {
|
|
9556
|
+
const e = msg.error;
|
|
9557
|
+
pending.reject(new Error(e?.message || "JSON-RPC error"));
|
|
9558
|
+
} else {
|
|
9559
|
+
pending.resolve(msg.result);
|
|
9560
|
+
}
|
|
9561
|
+
return;
|
|
9562
|
+
}
|
|
9563
|
+
const method = msg.method;
|
|
9564
|
+
if (!method) return;
|
|
9565
|
+
if (method === "thread/started" || method === "thread.started") {
|
|
9566
|
+
const params2 = msg.params ?? {};
|
|
9567
|
+
const threadId = this.extractThreadId(params2);
|
|
9568
|
+
if (threadId) {
|
|
9569
|
+
this.setThreadId(threadId, "notification");
|
|
9570
|
+
console.log(`[CodexPersistentRuntime] Thread started: ${threadId}, session=${this.sessionId}`);
|
|
9571
|
+
}
|
|
9572
|
+
return;
|
|
9573
|
+
}
|
|
9574
|
+
if (!this.callbacks) return;
|
|
9575
|
+
const params = msg.params ?? {};
|
|
9576
|
+
switch (method) {
|
|
9577
|
+
case "item/agentMessage/delta":
|
|
9578
|
+
case "item.agentMessage.delta": {
|
|
9579
|
+
const delta = params.delta || params.text || "";
|
|
9580
|
+
if (delta) {
|
|
9581
|
+
this.chunksSent++;
|
|
9582
|
+
if (!this.turnTtftLogged) {
|
|
9583
|
+
this.turnTtftLogged = true;
|
|
9584
|
+
const turnTtft = Date.now() - this.turnStartTime;
|
|
9585
|
+
console.log(`[CodexPersistentRuntime] EP1360: Turn TTFT: ${turnTtft}ms, session=${this.sessionId}`);
|
|
9586
|
+
}
|
|
9587
|
+
this.callbacks.onChunk(delta);
|
|
9588
|
+
}
|
|
9589
|
+
break;
|
|
9590
|
+
}
|
|
9591
|
+
case "item/started":
|
|
9592
|
+
case "item.started": {
|
|
9593
|
+
const item = params.item;
|
|
9594
|
+
if (!item) break;
|
|
9595
|
+
if (item.type === "command_execution" && this.callbacks.onToolUse) {
|
|
9596
|
+
const toolEvent = {
|
|
9597
|
+
id: item.id || `codex-cmd-${Date.now()}`,
|
|
9598
|
+
name: "Bash",
|
|
9599
|
+
input: { command: item.command || item.input || "" }
|
|
9600
|
+
};
|
|
9601
|
+
this.callbacks.onToolUse(toolEvent);
|
|
9602
|
+
}
|
|
9603
|
+
break;
|
|
9604
|
+
}
|
|
9605
|
+
case "item/completed":
|
|
9606
|
+
case "item.completed": {
|
|
9607
|
+
const item = params.item;
|
|
9608
|
+
if (!item) break;
|
|
9609
|
+
if (item.type === "agent_message" && item.text) {
|
|
9610
|
+
this.chunksSent++;
|
|
9611
|
+
if (!this.turnTtftLogged) {
|
|
9612
|
+
this.turnTtftLogged = true;
|
|
9613
|
+
const turnTtft = Date.now() - this.turnStartTime;
|
|
9614
|
+
console.log(`[CodexPersistentRuntime] EP1360: Turn TTFT: ${turnTtft}ms, session=${this.sessionId}`);
|
|
9615
|
+
}
|
|
9616
|
+
this.callbacks.onChunk(item.text);
|
|
9617
|
+
}
|
|
9618
|
+
if (item.type === "command_execution" && this.callbacks.onToolResult) {
|
|
9619
|
+
const toolResult = {
|
|
9620
|
+
id: item.id || `codex-cmd-${Date.now()}`,
|
|
9621
|
+
name: "Bash",
|
|
9622
|
+
output: item.output ?? item.result ?? item
|
|
9623
|
+
};
|
|
9624
|
+
this.callbacks.onToolResult(toolResult);
|
|
9625
|
+
}
|
|
9626
|
+
if (item.type === "contextCompaction" && this.callbacks.onCompaction) {
|
|
9627
|
+
const compactionEvent = {
|
|
9628
|
+
seq: this.parsedLineCount,
|
|
9629
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9630
|
+
provider: "codex",
|
|
9631
|
+
type: "session.compaction",
|
|
9632
|
+
raw: msg,
|
|
9633
|
+
trigger: "auto",
|
|
9634
|
+
summary: item.summary
|
|
9635
|
+
};
|
|
9636
|
+
this.callbacks.onCompaction(compactionEvent);
|
|
9637
|
+
}
|
|
9638
|
+
break;
|
|
9639
|
+
}
|
|
9640
|
+
case "turn/completed":
|
|
9641
|
+
case "turn.completed": {
|
|
9642
|
+
const usage = params.usage;
|
|
9643
|
+
if (usage) {
|
|
9644
|
+
const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
|
|
9645
|
+
const outputTokens = usage.output_tokens ?? usage.completion_tokens ?? 0;
|
|
9646
|
+
this.resultUsage = { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
|
|
9647
|
+
}
|
|
9648
|
+
this.resultModel = params.model ?? this.resultModel;
|
|
9649
|
+
this.resultNumTurns = typeof params.num_turns === "number" ? params.num_turns : this.resultNumTurns;
|
|
9650
|
+
this.completeTurn();
|
|
9651
|
+
break;
|
|
9652
|
+
}
|
|
9653
|
+
case "turn/failed":
|
|
9654
|
+
case "turn.failed":
|
|
9655
|
+
case "error": {
|
|
9656
|
+
const errMsg = params.message || params.error?.message || "Codex operation failed";
|
|
9657
|
+
this.callbacks.onError(errMsg);
|
|
9658
|
+
this.endTurn();
|
|
9659
|
+
break;
|
|
9660
|
+
}
|
|
9661
|
+
default:
|
|
9662
|
+
break;
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9665
|
+
async ensureInitialized() {
|
|
9666
|
+
if (this.initialized) return;
|
|
9667
|
+
if (this.initializing) return this.initializing;
|
|
9668
|
+
this.initializing = (async () => {
|
|
9669
|
+
await this.sendRequest("initialize", {
|
|
9670
|
+
clientInfo: { name: "episoda", version: "1.0" }
|
|
9671
|
+
}, INIT_TIMEOUT_MS);
|
|
9672
|
+
await this.sendNotification("initialized", {});
|
|
9673
|
+
const resumeThreadId = this.options.resumeThreadId;
|
|
9674
|
+
const model = this.options.model;
|
|
9675
|
+
if (resumeThreadId) {
|
|
9676
|
+
const resumeMethods = ["thread/resume", "thread.resume"];
|
|
9677
|
+
let resumed = false;
|
|
9678
|
+
for (const method of resumeMethods) {
|
|
9679
|
+
try {
|
|
9680
|
+
const res = await this.sendRequest(method, { threadId: resumeThreadId }, INIT_TIMEOUT_MS);
|
|
9681
|
+
const threadId = this.extractThreadId(res) || resumeThreadId;
|
|
9682
|
+
if (threadId) {
|
|
9683
|
+
this.setThreadId(threadId, `response:${method}`);
|
|
9684
|
+
console.log(`[CodexPersistentRuntime] Thread resumed (${method}): ${threadId}, session=${this.sessionId}`);
|
|
9685
|
+
resumed = true;
|
|
9686
|
+
break;
|
|
9687
|
+
}
|
|
9688
|
+
} catch (err) {
|
|
9689
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9690
|
+
console.warn(`[CodexPersistentRuntime] Thread resume failed (${method}): ${msg}, session=${this.sessionId}`);
|
|
9691
|
+
}
|
|
9692
|
+
}
|
|
9693
|
+
if (!resumed) {
|
|
9694
|
+
console.warn(`[CodexPersistentRuntime] Falling back to thread/start (resume unsupported or failed). session=${this.sessionId}`);
|
|
9695
|
+
}
|
|
9696
|
+
}
|
|
9697
|
+
if (!this._agentSessionId) {
|
|
9698
|
+
const startMethods = ["thread/start", "thread.start"];
|
|
9699
|
+
let started = false;
|
|
9700
|
+
for (const method of startMethods) {
|
|
9701
|
+
try {
|
|
9702
|
+
const res = await this.sendRequest(method, model ? { model } : {}, INIT_TIMEOUT_MS);
|
|
9703
|
+
const threadId = this.extractThreadId(res);
|
|
9704
|
+
if (threadId) {
|
|
9705
|
+
this.setThreadId(threadId, `response:${method}`);
|
|
9706
|
+
console.log(`[CodexPersistentRuntime] Thread started (response): ${threadId}, session=${this.sessionId}`);
|
|
9707
|
+
}
|
|
9708
|
+
started = true;
|
|
9709
|
+
break;
|
|
9710
|
+
} catch (err) {
|
|
9711
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9712
|
+
console.warn(`[CodexPersistentRuntime] Thread start failed (${method}): ${msg}, session=${this.sessionId}`);
|
|
9713
|
+
}
|
|
9714
|
+
}
|
|
9715
|
+
if (!started) {
|
|
9716
|
+
throw new Error("Failed to start thread (no supported method)");
|
|
9717
|
+
}
|
|
9718
|
+
}
|
|
9719
|
+
await this.waitForThreadId(1e4);
|
|
9720
|
+
this.initialized = true;
|
|
9721
|
+
})().finally(() => {
|
|
9722
|
+
this.initializing = null;
|
|
9723
|
+
});
|
|
9724
|
+
return this.initializing;
|
|
9725
|
+
}
|
|
9726
|
+
setThreadId(threadId, source) {
|
|
9727
|
+
if (!threadId) return;
|
|
9728
|
+
if (this._agentSessionId && this._agentSessionId !== threadId) {
|
|
9729
|
+
console.warn(
|
|
9730
|
+
`[CodexPersistentRuntime] ThreadId changed (${source}): ${this._agentSessionId} -> ${threadId}, session=${this.sessionId}`
|
|
9731
|
+
);
|
|
9732
|
+
}
|
|
9733
|
+
this._agentSessionId = threadId;
|
|
9734
|
+
if (this.threadIdWaiters.length) {
|
|
9735
|
+
const waiters = this.threadIdWaiters;
|
|
9736
|
+
this.threadIdWaiters = [];
|
|
9737
|
+
for (const w of waiters) {
|
|
9738
|
+
try {
|
|
9739
|
+
w(threadId);
|
|
9740
|
+
} catch {
|
|
9741
|
+
}
|
|
9742
|
+
}
|
|
9743
|
+
}
|
|
9744
|
+
}
|
|
9745
|
+
waitForThreadId(timeoutMs) {
|
|
9746
|
+
if (this._agentSessionId) return Promise.resolve(this._agentSessionId);
|
|
9747
|
+
return new Promise((resolve4, reject) => {
|
|
9748
|
+
const onId = (id) => {
|
|
9749
|
+
clearTimeout(timer);
|
|
9750
|
+
resolve4(id);
|
|
9751
|
+
};
|
|
9752
|
+
const timer = setTimeout(() => {
|
|
9753
|
+
this.threadIdWaiters = this.threadIdWaiters.filter((w) => w !== onId);
|
|
9754
|
+
reject(new Error(`Timeout waiting for threadId after ${timeoutMs}ms`));
|
|
9755
|
+
}, timeoutMs);
|
|
9756
|
+
this.threadIdWaiters.push(onId);
|
|
9757
|
+
});
|
|
9758
|
+
}
|
|
9759
|
+
extractThreadId(obj) {
|
|
9760
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
9761
|
+
const rec = obj;
|
|
9762
|
+
const direct = rec.threadId ?? rec.thread_id ?? rec.id;
|
|
9763
|
+
if (typeof direct === "string" && direct.trim()) return direct;
|
|
9764
|
+
const thread = rec.thread;
|
|
9765
|
+
if (thread && typeof thread === "object") {
|
|
9766
|
+
const t = thread;
|
|
9767
|
+
const nested = t.id ?? t.threadId ?? t.thread_id;
|
|
9768
|
+
if (typeof nested === "string" && nested.trim()) return nested;
|
|
9769
|
+
}
|
|
9770
|
+
const result = rec.result;
|
|
9771
|
+
if (result && typeof result === "object") return this.extractThreadId(result);
|
|
9772
|
+
return void 0;
|
|
9773
|
+
}
|
|
9774
|
+
async sendNotification(method, params) {
|
|
9775
|
+
const msg = { jsonrpc: "2.0", method, params };
|
|
9776
|
+
await this.writeToStdin(JSON.stringify(msg) + "\n");
|
|
9777
|
+
}
|
|
9778
|
+
sendRequest(method, params, timeoutMs = INIT_TIMEOUT_MS) {
|
|
9779
|
+
const id = this.nextId++;
|
|
9780
|
+
const msg = { jsonrpc: "2.0", id, method, params };
|
|
9781
|
+
return new Promise(async (resolve4, reject) => {
|
|
9782
|
+
const timeout = setTimeout(() => {
|
|
9783
|
+
this.pending.delete(id);
|
|
9784
|
+
reject(new Error(`JSON-RPC timeout: ${method}`));
|
|
9785
|
+
}, timeoutMs);
|
|
9786
|
+
this.pending.set(id, { resolve: resolve4, reject, timeout });
|
|
9787
|
+
try {
|
|
9788
|
+
await this.writeToStdin(JSON.stringify(msg) + "\n");
|
|
9789
|
+
} catch (err) {
|
|
9790
|
+
clearTimeout(timeout);
|
|
9791
|
+
this.pending.delete(id);
|
|
9792
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
9793
|
+
}
|
|
9794
|
+
});
|
|
9795
|
+
}
|
|
9796
|
+
writeToStdin(data) {
|
|
9797
|
+
if (!this.process?.stdin || this.process.stdin.destroyed) {
|
|
9798
|
+
throw new Error(`[CodexPersistentRuntime] stdin not available. session=${this.sessionId}`);
|
|
9799
|
+
}
|
|
9800
|
+
return new Promise((resolve4, reject) => {
|
|
9801
|
+
const stdin = this.process.stdin;
|
|
9802
|
+
const ok = stdin.write(data);
|
|
9803
|
+
if (ok) {
|
|
9804
|
+
resolve4();
|
|
9805
|
+
return;
|
|
9806
|
+
}
|
|
9807
|
+
const onDrain = () => {
|
|
9808
|
+
cleanup();
|
|
9809
|
+
resolve4();
|
|
9810
|
+
};
|
|
9811
|
+
const onError = (err) => {
|
|
9812
|
+
cleanup();
|
|
9813
|
+
reject(err);
|
|
9814
|
+
};
|
|
9815
|
+
const onTimeout = () => {
|
|
9816
|
+
cleanup();
|
|
9817
|
+
reject(new Error(`STDIN_DRAIN_TIMEOUT after ${STDIN_DRAIN_TIMEOUT_MS2}ms`));
|
|
9818
|
+
};
|
|
9819
|
+
const cleanup = () => {
|
|
9820
|
+
clearTimeout(timer);
|
|
9821
|
+
stdin.off("drain", onDrain);
|
|
9822
|
+
stdin.off("error", onError);
|
|
9823
|
+
};
|
|
9824
|
+
const timer = setTimeout(onTimeout, STDIN_DRAIN_TIMEOUT_MS2);
|
|
9825
|
+
stdin.once("drain", onDrain);
|
|
9826
|
+
stdin.once("error", onError);
|
|
9827
|
+
});
|
|
9828
|
+
}
|
|
9829
|
+
// -------------------------------------------------------------------------
|
|
9830
|
+
// Turn lifecycle
|
|
9831
|
+
// -------------------------------------------------------------------------
|
|
9832
|
+
completeTurn() {
|
|
9833
|
+
if (!this.callbacks) return;
|
|
9834
|
+
const durationMs = Date.now() - this.turnStartTime;
|
|
9835
|
+
const resultMeta = this.resultModel || this.resultUsage ? {
|
|
9836
|
+
model: this.resultModel,
|
|
9837
|
+
durationMs,
|
|
9838
|
+
numTurns: this.resultNumTurns,
|
|
9839
|
+
usage: this.resultUsage
|
|
9840
|
+
} : void 0;
|
|
9841
|
+
console.log(`[CodexPersistentRuntime] Turn completed: duration=${durationMs}ms, chunks=${this.chunksSent}, session=${this.sessionId}`);
|
|
9842
|
+
const cb = this.callbacks;
|
|
9843
|
+
this.endTurn();
|
|
9844
|
+
cb.onComplete(this._agentSessionId, resultMeta);
|
|
9845
|
+
}
|
|
9846
|
+
endTurn() {
|
|
9847
|
+
this.clearTimers();
|
|
9848
|
+
this.callbacks = null;
|
|
9849
|
+
this._turnState = "idle";
|
|
9850
|
+
this.chunksSent = 0;
|
|
9851
|
+
}
|
|
9852
|
+
// -------------------------------------------------------------------------
|
|
9853
|
+
// Timers
|
|
9854
|
+
// -------------------------------------------------------------------------
|
|
9855
|
+
resetInactivityTimer() {
|
|
9856
|
+
if (this.inactivityTimer) {
|
|
9857
|
+
clearTimeout(this.inactivityTimer);
|
|
9858
|
+
}
|
|
9859
|
+
if (this._turnState === "idle") return;
|
|
9860
|
+
this.inactivityTimer = setTimeout(() => {
|
|
9861
|
+
if (this._turnState !== "idle") {
|
|
9862
|
+
console.warn(`[CodexPersistentRuntime] Inactivity timeout (${INACTIVITY_TIMEOUT_MS2}ms). session=${this.sessionId}`);
|
|
9863
|
+
if (this.callbacks) {
|
|
9864
|
+
this.callbacks.onError(`INACTIVITY_TIMEOUT: No stdout for ${INACTIVITY_TIMEOUT_MS2}ms during streaming`);
|
|
9865
|
+
this.endTurn();
|
|
9866
|
+
}
|
|
9867
|
+
}
|
|
9868
|
+
}, INACTIVITY_TIMEOUT_MS2);
|
|
9869
|
+
}
|
|
9870
|
+
clearTimers() {
|
|
9871
|
+
if (this.inactivityTimer) {
|
|
9872
|
+
clearTimeout(this.inactivityTimer);
|
|
9873
|
+
this.inactivityTimer = null;
|
|
9874
|
+
}
|
|
9875
|
+
}
|
|
9876
|
+
waitForExit(timeoutMs) {
|
|
9877
|
+
return new Promise((resolve4) => {
|
|
9878
|
+
if (!this.process || !this.alive) {
|
|
9879
|
+
resolve4(true);
|
|
9880
|
+
return;
|
|
9881
|
+
}
|
|
9882
|
+
const onExit = () => {
|
|
9883
|
+
clearTimeout(timer);
|
|
9884
|
+
resolve4(true);
|
|
9885
|
+
};
|
|
9886
|
+
const timer = setTimeout(() => {
|
|
9887
|
+
this.process?.removeListener("exit", onExit);
|
|
9888
|
+
resolve4(false);
|
|
9889
|
+
}, timeoutMs);
|
|
9890
|
+
this.process.once("exit", onExit);
|
|
9891
|
+
});
|
|
9892
|
+
}
|
|
9893
|
+
};
|
|
9894
|
+
|
|
9895
|
+
// src/agent/agent-manager.ts
|
|
8842
9896
|
var instance3 = null;
|
|
8843
9897
|
function getAgentManager() {
|
|
8844
9898
|
if (!instance3) {
|
|
@@ -8846,6 +9900,11 @@ function getAgentManager() {
|
|
|
8846
9900
|
}
|
|
8847
9901
|
return instance3;
|
|
8848
9902
|
}
|
|
9903
|
+
var AGENT_DISABLE_PERSISTENT = process.env.AGENT_DISABLE_PERSISTENT === "1";
|
|
9904
|
+
var AGENT_MAX_PERSISTENT = parseInt(process.env.AGENT_MAX_PERSISTENT || "5", 10);
|
|
9905
|
+
var AGENT_IDLE_TIMEOUT_MS = parseInt(process.env.AGENT_IDLE_TIMEOUT_MS || "300000", 10);
|
|
9906
|
+
var IDLE_SCAN_INTERVAL_MS = 6e4;
|
|
9907
|
+
var CLAUDE_PERSISTENT_BLOCKED_MODELS = ["haiku"];
|
|
8849
9908
|
var AgentManager = class {
|
|
8850
9909
|
constructor() {
|
|
8851
9910
|
this.sessions = /* @__PURE__ */ new Map();
|
|
@@ -8853,8 +9912,41 @@ var AgentManager = class {
|
|
|
8853
9912
|
this.initialized = false;
|
|
8854
9913
|
// EP1133: Lock for config file writes to prevent race conditions
|
|
8855
9914
|
this.configWriteLock = Promise.resolve();
|
|
9915
|
+
// EP1360: Persistent runtime management
|
|
9916
|
+
this.runtimes = /* @__PURE__ */ new Map();
|
|
9917
|
+
// EP1360: Per-session downgrade. If a persistent runtime wedges/crashes for a session,
|
|
9918
|
+
// we permanently fall back to spawn-per-message for the remainder of that session.
|
|
9919
|
+
this.persistentDisabledSessions = /* @__PURE__ */ new Map();
|
|
9920
|
+
this.idleScanTimer = null;
|
|
8856
9921
|
this.pidDir = path20.join(os7.homedir(), ".episoda", "agent-pids");
|
|
8857
9922
|
}
|
|
9923
|
+
/**
|
|
9924
|
+
* EP1360: Best-effort child process RSS sampling.
|
|
9925
|
+
* Returns RSS in MB, or null if not available.
|
|
9926
|
+
*
|
|
9927
|
+
* NOTE: We intentionally avoid adding native deps here; `ps` is available
|
|
9928
|
+
* on macOS/Linux. This instrumentation must never break message flow.
|
|
9929
|
+
*/
|
|
9930
|
+
async getChildRssMb(pid) {
|
|
9931
|
+
if (!pid || pid <= 0) return null;
|
|
9932
|
+
try {
|
|
9933
|
+
if (typeof import_child_process14.execFile !== "function") return null;
|
|
9934
|
+
const stdout = await new Promise((resolve4, reject) => {
|
|
9935
|
+
(0, import_child_process14.execFile)("ps", ["-o", "rss=", "-p", String(pid)], { timeout: 1e3 }, (err, out) => {
|
|
9936
|
+
if (err) {
|
|
9937
|
+
reject(err);
|
|
9938
|
+
return;
|
|
9939
|
+
}
|
|
9940
|
+
resolve4(String(out));
|
|
9941
|
+
});
|
|
9942
|
+
});
|
|
9943
|
+
const kb = Number(String(stdout).trim());
|
|
9944
|
+
if (!Number.isFinite(kb) || kb <= 0) return null;
|
|
9945
|
+
return Math.round(kb / 1024 * 10) / 10;
|
|
9946
|
+
} catch {
|
|
9947
|
+
return null;
|
|
9948
|
+
}
|
|
9949
|
+
}
|
|
8858
9950
|
/**
|
|
8859
9951
|
* EP1133: Acquire lock for config file writes
|
|
8860
9952
|
* Ensures sequential writes to prevent file corruption
|
|
@@ -9149,6 +10241,12 @@ var AgentManager = class {
|
|
|
9149
10241
|
} catch (error) {
|
|
9150
10242
|
console.warn("[AgentManager] Codex CLI not available:", error instanceof Error ? error.message : error);
|
|
9151
10243
|
}
|
|
10244
|
+
if (!AGENT_DISABLE_PERSISTENT) {
|
|
10245
|
+
this.startIdleScanner();
|
|
10246
|
+
console.log(`[AgentManager] EP1360: Persistent runtimes enabled (max=${AGENT_MAX_PERSISTENT}, idleTimeout=${AGENT_IDLE_TIMEOUT_MS}ms)`);
|
|
10247
|
+
} else {
|
|
10248
|
+
console.log("[AgentManager] EP1360: Persistent runtimes DISABLED (AGENT_DISABLE_PERSISTENT=1)");
|
|
10249
|
+
}
|
|
9152
10250
|
this.initialized = true;
|
|
9153
10251
|
console.log("[AgentManager] Initialized");
|
|
9154
10252
|
}
|
|
@@ -9273,7 +10371,8 @@ var AgentManager = class {
|
|
|
9273
10371
|
/**
|
|
9274
10372
|
* Send a message to an agent session
|
|
9275
10373
|
*
|
|
9276
|
-
*
|
|
10374
|
+
* EP1360: Uses persistent runtimes when enabled/available; falls back to
|
|
10375
|
+
* spawn-per-message for incident response, pool pressure, or per-session downgrade.
|
|
9277
10376
|
* EP1133: Supports both Claude Code and Codex CLI with provider-specific handling.
|
|
9278
10377
|
* EP1251: Supports credentials refresh for GitHub token on resume.
|
|
9279
10378
|
*/
|
|
@@ -9283,6 +10382,22 @@ var AgentManager = class {
|
|
|
9283
10382
|
if (!session) {
|
|
9284
10383
|
return { success: false, error: "Session not found" };
|
|
9285
10384
|
}
|
|
10385
|
+
const shouldDisablePersistentFromError = (error) => {
|
|
10386
|
+
return error.startsWith("ECHO_TIMEOUT:") || error.startsWith("INACTIVITY_TIMEOUT:") || error.startsWith("PROCESS_EXIT:") || error.startsWith("SPAWN_ERROR:") || error.startsWith("SYNC_RUNTIME_ERROR:");
|
|
10387
|
+
};
|
|
10388
|
+
const wrapRuntimeOnComplete = (agentSid, resultMetadata) => {
|
|
10389
|
+
if (agentSid) {
|
|
10390
|
+
session.agentSessionId = agentSid;
|
|
10391
|
+
session.claudeSessionId = agentSid;
|
|
10392
|
+
}
|
|
10393
|
+
onComplete(agentSid, resultMetadata);
|
|
10394
|
+
};
|
|
10395
|
+
const wrapRuntimeOnError = (error) => {
|
|
10396
|
+
if (shouldDisablePersistentFromError(error)) {
|
|
10397
|
+
this.disablePersistentForSession(sessionId, error);
|
|
10398
|
+
}
|
|
10399
|
+
onError(error);
|
|
10400
|
+
};
|
|
9286
10401
|
const mcpAuth = await this.resolveMcpAuth();
|
|
9287
10402
|
if (mcpAuth.source === "config") {
|
|
9288
10403
|
console.log("[AgentManager] EP1287: Using MCP token from ~/.episoda/config.json");
|
|
@@ -9295,6 +10410,78 @@ var AgentManager = class {
|
|
|
9295
10410
|
session.canWrite = canWrite;
|
|
9296
10411
|
session.readOnlyReason = readOnlyReason;
|
|
9297
10412
|
}
|
|
10413
|
+
if (!isFirstMessage) {
|
|
10414
|
+
const runtime = this.getRuntime(sessionId);
|
|
10415
|
+
if (runtime) {
|
|
10416
|
+
try {
|
|
10417
|
+
if (runtime.turnState !== "idle") {
|
|
10418
|
+
const err = `TURN_IN_PROGRESS: Persistent runtime is busy (turnState=${runtime.turnState})`;
|
|
10419
|
+
console.warn(`[AgentManager] EP1360: ${err}, session=${sessionId}`);
|
|
10420
|
+
onError(err);
|
|
10421
|
+
return { success: false, error: err };
|
|
10422
|
+
}
|
|
10423
|
+
let runtimeMessage = message;
|
|
10424
|
+
if (session.canWrite === false) {
|
|
10425
|
+
if (session.provider === "codex") {
|
|
10426
|
+
const readOnlyReminder = `
|
|
10427
|
+
[SYSTEM REMINDER - READ-ONLY MODE]
|
|
10428
|
+
You are in READ-ONLY mode. Do NOT:
|
|
10429
|
+
- Create, modify, or delete any files
|
|
10430
|
+
- Run commands that write to the filesystem
|
|
10431
|
+
- Make git commits or changes
|
|
10432
|
+
Reason: ${session.readOnlyReason || "No write access to this module."}
|
|
10433
|
+
If changes are needed, explain what needs to be done but do not execute.
|
|
10434
|
+
[END SYSTEM REMINDER]
|
|
10435
|
+
|
|
10436
|
+
`;
|
|
10437
|
+
runtimeMessage = readOnlyReminder + runtimeMessage;
|
|
10438
|
+
} else {
|
|
10439
|
+
const readOnlyReminder = `
|
|
10440
|
+
REMINDER: You are in READ-ONLY mode. You cannot create, modify, or delete files.
|
|
10441
|
+
Reason: ${session.readOnlyReason || "No write access to this module."}
|
|
10442
|
+
If changes are needed, explain what needs to be done.`;
|
|
10443
|
+
runtimeMessage = `${readOnlyReminder}
|
|
10444
|
+
|
|
10445
|
+
${runtimeMessage}`;
|
|
10446
|
+
}
|
|
10447
|
+
}
|
|
10448
|
+
const pid = runtime.pid;
|
|
10449
|
+
void this.getChildRssMb(pid).then((rss) => {
|
|
10450
|
+
if (rss !== null) {
|
|
10451
|
+
console.log(`[AgentManager] EP1360: Persistent turn start RSS: ${rss}MB, provider=${session.provider}, session=${sessionId}, pid=${pid}`);
|
|
10452
|
+
}
|
|
10453
|
+
});
|
|
10454
|
+
const onCompleteWithRss = (agentSid, resultMetadata) => {
|
|
10455
|
+
void this.getChildRssMb(pid).then((rss) => {
|
|
10456
|
+
if (rss !== null) {
|
|
10457
|
+
console.log(`[AgentManager] EP1360: Persistent turn complete RSS: ${rss}MB, provider=${session.provider}, session=${sessionId}, pid=${pid}`);
|
|
10458
|
+
}
|
|
10459
|
+
});
|
|
10460
|
+
wrapRuntimeOnComplete(agentSid, resultMetadata);
|
|
10461
|
+
};
|
|
10462
|
+
await runtime.sendMessage(runtimeMessage, {
|
|
10463
|
+
onChunk,
|
|
10464
|
+
onToolUse,
|
|
10465
|
+
onToolResult,
|
|
10466
|
+
onCompaction: options.onCompaction,
|
|
10467
|
+
onComplete: onCompleteWithRss,
|
|
10468
|
+
onError: wrapRuntimeOnError
|
|
10469
|
+
});
|
|
10470
|
+
console.log(`[AgentManager] EP1360: Sent follow-up message via persistent ${session.provider} runtime, session=${sessionId}`);
|
|
10471
|
+
return { success: true };
|
|
10472
|
+
} catch (error) {
|
|
10473
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
10474
|
+
if (msg.includes("turn state is '") || msg.startsWith("TURN_IN_PROGRESS:")) {
|
|
10475
|
+
const err = `TURN_IN_PROGRESS: ${msg}`;
|
|
10476
|
+
console.warn(`[AgentManager] EP1360: ${err}, session=${sessionId}`);
|
|
10477
|
+
onError(err);
|
|
10478
|
+
return { success: false, error: err };
|
|
10479
|
+
}
|
|
10480
|
+
console.warn(`[AgentManager] EP1360: Persistent runtime failed for follow-up, falling back to spawn-per-message: ${msg}`);
|
|
10481
|
+
this.disablePersistentForSession(sessionId, `SYNC_RUNTIME_ERROR: ${msg}`);
|
|
10482
|
+
}
|
|
10483
|
+
}
|
|
10484
|
+
}
|
|
9298
10485
|
if (credentials?.githubToken) {
|
|
9299
10486
|
session.credentials.githubToken = credentials.githubToken;
|
|
9300
10487
|
session.credentials.githubTokenExpiresAt = credentials.githubTokenExpiresAt;
|
|
@@ -9342,6 +10529,8 @@ Violations will result in errors and may affect your ability to assist.
|
|
|
9342
10529
|
}
|
|
9343
10530
|
let binaryPath;
|
|
9344
10531
|
let args;
|
|
10532
|
+
let argsBeforeMessage = 0;
|
|
10533
|
+
let runtimeFirstMessage = message;
|
|
9345
10534
|
if (provider === "codex") {
|
|
9346
10535
|
binaryPath = await ensureCodexBinary();
|
|
9347
10536
|
args = [
|
|
@@ -9387,6 +10576,8 @@ If changes are needed, explain what needs to be done but do not execute.
|
|
|
9387
10576
|
`;
|
|
9388
10577
|
fullMessage = readOnlyReminder + fullMessage;
|
|
9389
10578
|
}
|
|
10579
|
+
argsBeforeMessage = args.length;
|
|
10580
|
+
runtimeFirstMessage = fullMessage;
|
|
9390
10581
|
args.push(fullMessage);
|
|
9391
10582
|
} else {
|
|
9392
10583
|
binaryPath = await ensureClaudeBinary();
|
|
@@ -9422,6 +10613,9 @@ Reason: ${session.readOnlyReason || "No write access to this module."}
|
|
|
9422
10613
|
If changes are needed, explain what needs to be done.`;
|
|
9423
10614
|
args.push("--append-system-prompt", readOnlyReminder);
|
|
9424
10615
|
console.log("[AgentManager] EP1205: Appended read-only reminder to subsequent message");
|
|
10616
|
+
runtimeFirstMessage = `${readOnlyReminder}
|
|
10617
|
+
|
|
10618
|
+
${message}`;
|
|
9425
10619
|
}
|
|
9426
10620
|
}
|
|
9427
10621
|
if (resumeSessionId) {
|
|
@@ -9478,6 +10672,7 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9478
10672
|
args.push("--mcp-config", mcpConfigJson);
|
|
9479
10673
|
console.log(`[AgentManager] EP1253: MCP config with env: ${mcpConfigJson.substring(0, 200)}...`);
|
|
9480
10674
|
}
|
|
10675
|
+
argsBeforeMessage = args.length;
|
|
9481
10676
|
args.push("--", message);
|
|
9482
10677
|
}
|
|
9483
10678
|
console.log(`[AgentManager] Spawning ${provider} CLI for session ${sessionId}`);
|
|
@@ -9722,7 +10917,95 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9722
10917
|
envVars.ANTHROPIC_API_KEY = session.credentials.apiKey;
|
|
9723
10918
|
}
|
|
9724
10919
|
}
|
|
9725
|
-
|
|
10920
|
+
if (provider === "claude") {
|
|
10921
|
+
envVars.CLAUDE_CODE_DISABLE_PLUGIN_CACHE = "1";
|
|
10922
|
+
}
|
|
10923
|
+
const daemonSpawnRssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
10924
|
+
const spawnTimestamp = Date.now();
|
|
10925
|
+
const modelLower = (session.credentials.preferredModel || "").toLowerCase();
|
|
10926
|
+
const modelBlockedForPersistent = provider === "claude" && CLAUDE_PERSISTENT_BLOCKED_MODELS.some((blocked) => modelLower.includes(blocked));
|
|
10927
|
+
if (modelBlockedForPersistent) {
|
|
10928
|
+
console.log(`[AgentManager] EP1360: Model "${session.credentials.preferredModel}" incompatible with persistent mode (#17406) \u2014 using spawn-per-message`);
|
|
10929
|
+
}
|
|
10930
|
+
if (this.isPersistentEnabled() && !modelBlockedForPersistent) {
|
|
10931
|
+
try {
|
|
10932
|
+
const baseArgs = args.slice(0, argsBeforeMessage);
|
|
10933
|
+
let persistentArgs;
|
|
10934
|
+
if (provider === "claude") {
|
|
10935
|
+
persistentArgs = [...baseArgs];
|
|
10936
|
+
persistentArgs.push(
|
|
10937
|
+
"--input-format",
|
|
10938
|
+
"stream-json",
|
|
10939
|
+
"--replay-user-messages"
|
|
10940
|
+
);
|
|
10941
|
+
} else {
|
|
10942
|
+
persistentArgs = ["app-server"];
|
|
10943
|
+
}
|
|
10944
|
+
let runtimeBinaryPath;
|
|
10945
|
+
let runtimeArgs;
|
|
10946
|
+
if (spawnCmd === "npx") {
|
|
10947
|
+
runtimeBinaryPath = "npx";
|
|
10948
|
+
runtimeArgs = ["--yes", binaryPath.replace("npx:", ""), ...persistentArgs];
|
|
10949
|
+
} else {
|
|
10950
|
+
runtimeBinaryPath = spawnCmd;
|
|
10951
|
+
runtimeArgs = persistentArgs;
|
|
10952
|
+
}
|
|
10953
|
+
const runtime = this.createRuntime(sessionId, provider, {
|
|
10954
|
+
sessionId,
|
|
10955
|
+
binaryPath: runtimeBinaryPath,
|
|
10956
|
+
args: runtimeArgs,
|
|
10957
|
+
env: envVars,
|
|
10958
|
+
cwd: session.projectPath,
|
|
10959
|
+
...provider === "codex" ? {
|
|
10960
|
+
model: session.credentials.preferredModel,
|
|
10961
|
+
resumeThreadId: session.agentSessionId || session.claudeSessionId
|
|
10962
|
+
} : {}
|
|
10963
|
+
});
|
|
10964
|
+
if (runtime) {
|
|
10965
|
+
if (runtime.turnState !== "idle") {
|
|
10966
|
+
const err = `TURN_IN_PROGRESS: Persistent runtime is busy (turnState=${runtime.turnState})`;
|
|
10967
|
+
console.warn(`[AgentManager] EP1360: ${err}, session=${sessionId}`);
|
|
10968
|
+
onError(err);
|
|
10969
|
+
return { success: false, error: err };
|
|
10970
|
+
}
|
|
10971
|
+
if (runtime.pid) {
|
|
10972
|
+
session.pid = runtime.pid;
|
|
10973
|
+
this.writePidFile(sessionId, runtime.pid);
|
|
10974
|
+
}
|
|
10975
|
+
const pid = runtime.pid;
|
|
10976
|
+
void this.getChildRssMb(pid).then((rss) => {
|
|
10977
|
+
if (rss !== null) {
|
|
10978
|
+
console.log(`[AgentManager] EP1360: Persistent turn start RSS: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${pid}`);
|
|
10979
|
+
}
|
|
10980
|
+
});
|
|
10981
|
+
const onCompleteWithRss = (agentSid, resultMetadata) => {
|
|
10982
|
+
void this.getChildRssMb(pid).then((rss) => {
|
|
10983
|
+
if (rss !== null) {
|
|
10984
|
+
console.log(`[AgentManager] EP1360: Persistent turn complete RSS: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${pid}`);
|
|
10985
|
+
}
|
|
10986
|
+
});
|
|
10987
|
+
wrapRuntimeOnComplete(agentSid, resultMetadata);
|
|
10988
|
+
};
|
|
10989
|
+
await runtime.sendMessage(runtimeFirstMessage, {
|
|
10990
|
+
onChunk,
|
|
10991
|
+
onToolUse,
|
|
10992
|
+
onToolResult,
|
|
10993
|
+
onCompaction: options.onCompaction,
|
|
10994
|
+
onComplete: onCompleteWithRss,
|
|
10995
|
+
onError: wrapRuntimeOnError
|
|
10996
|
+
});
|
|
10997
|
+
console.log(`[AgentManager] EP1360: Started persistent ${provider} runtime for session ${sessionId}, pid=${runtime.pid}`);
|
|
10998
|
+
return { success: true };
|
|
10999
|
+
}
|
|
11000
|
+
} catch (error) {
|
|
11001
|
+
console.warn(
|
|
11002
|
+
`[AgentManager] EP1360: Persistent runtime creation failed, falling back to spawn-per-message: ${error instanceof Error ? error.message : error}`
|
|
11003
|
+
);
|
|
11004
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
11005
|
+
this.disablePersistentForSession(sessionId, `SYNC_RUNTIME_ERROR: ${msg}`);
|
|
11006
|
+
}
|
|
11007
|
+
}
|
|
11008
|
+
const childProcess = (0, import_child_process14.spawn)(spawnCmd, spawnArgs, {
|
|
9726
11009
|
cwd: session.projectPath,
|
|
9727
11010
|
env: envVars,
|
|
9728
11011
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9733,6 +11016,14 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9733
11016
|
session.pid = childProcess.pid;
|
|
9734
11017
|
this.writePidFile(sessionId, childProcess.pid);
|
|
9735
11018
|
}
|
|
11019
|
+
let spawnChildRssMb = null;
|
|
11020
|
+
let resultChildRssMb = null;
|
|
11021
|
+
void this.getChildRssMb(childProcess.pid).then((rss) => {
|
|
11022
|
+
spawnChildRssMb = rss;
|
|
11023
|
+
if (rss !== null) {
|
|
11024
|
+
console.log(`[AgentManager] EP1360: Child RSS at spawn: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${childProcess.pid}`);
|
|
11025
|
+
}
|
|
11026
|
+
});
|
|
9736
11027
|
childProcess.stderr?.on("data", (data) => {
|
|
9737
11028
|
console.error(`[AgentManager] stderr: ${data.toString()}`);
|
|
9738
11029
|
});
|
|
@@ -9748,6 +11039,8 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9748
11039
|
let stdoutEventCount = 0;
|
|
9749
11040
|
let chunksSent = 0;
|
|
9750
11041
|
const streamStartTime = Date.now();
|
|
11042
|
+
let ttftLogged = false;
|
|
11043
|
+
let parsedLineCount = 0;
|
|
9751
11044
|
let hasContent = false;
|
|
9752
11045
|
childProcess.stdout?.on("data", (data) => {
|
|
9753
11046
|
const rawData = data.toString();
|
|
@@ -9762,6 +11055,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9762
11055
|
if (!line.trim()) continue;
|
|
9763
11056
|
try {
|
|
9764
11057
|
const parsed = JSON.parse(line);
|
|
11058
|
+
parsedLineCount++;
|
|
11059
|
+
if (!ttftLogged) {
|
|
11060
|
+
ttftLogged = true;
|
|
11061
|
+
const ttftMs = Date.now() - spawnTimestamp;
|
|
11062
|
+
console.log(`[AgentManager] EP1360: TTFT (spawn to first JSON): ${ttftMs}ms, provider=${provider}, session=${sessionId}, isFirstMessage=${isFirstMessage}`);
|
|
11063
|
+
}
|
|
9765
11064
|
if (stdoutEventCount <= 10 || chunksSent === 0) {
|
|
9766
11065
|
console.log(`[AgentManager] EP1191: Parsed event type: ${parsed.type}`);
|
|
9767
11066
|
}
|
|
@@ -9816,6 +11115,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9816
11115
|
}
|
|
9817
11116
|
break;
|
|
9818
11117
|
case "turn.completed":
|
|
11118
|
+
void this.getChildRssMb(childProcess.pid).then((rss) => {
|
|
11119
|
+
resultChildRssMb = rss;
|
|
11120
|
+
if (rss !== null) {
|
|
11121
|
+
console.log(`[AgentManager] EP1360: Child RSS at turn.completed: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${childProcess.pid}`);
|
|
11122
|
+
}
|
|
11123
|
+
});
|
|
9819
11124
|
if (parsed.usage) {
|
|
9820
11125
|
const u = parsed.usage;
|
|
9821
11126
|
const inputTokens = u.input_tokens ?? u.prompt_tokens ?? 0;
|
|
@@ -9891,6 +11196,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9891
11196
|
}
|
|
9892
11197
|
break;
|
|
9893
11198
|
case "result":
|
|
11199
|
+
void this.getChildRssMb(childProcess.pid).then((rss) => {
|
|
11200
|
+
resultChildRssMb = rss;
|
|
11201
|
+
if (rss !== null) {
|
|
11202
|
+
console.log(`[AgentManager] EP1360: Child RSS at result: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${childProcess.pid}`);
|
|
11203
|
+
}
|
|
11204
|
+
});
|
|
9894
11205
|
if (parsed.session_id) {
|
|
9895
11206
|
extractedSessionId = parsed.session_id;
|
|
9896
11207
|
session.agentSessionId = extractedSessionId;
|
|
@@ -9962,6 +11273,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9962
11273
|
});
|
|
9963
11274
|
childProcess.on("exit", (code, signal) => {
|
|
9964
11275
|
const duration = Date.now() - streamStartTime;
|
|
11276
|
+
const daemonExitRssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
11277
|
+
const daemonDeltaMb = daemonExitRssMb - daemonSpawnRssMb;
|
|
11278
|
+
console.log(`[AgentManager] EP1360: Daemon RSS at exit: ${daemonExitRssMb}MB (delta: ${daemonDeltaMb >= 0 ? "+" : ""}${daemonDeltaMb}MB from spawn ${daemonSpawnRssMb}MB), session=${sessionId}`);
|
|
11279
|
+
const childDeltaMb = spawnChildRssMb !== null && resultChildRssMb !== null ? Math.round((resultChildRssMb - spawnChildRssMb) * 10) / 10 : null;
|
|
11280
|
+
console.log(`[AgentManager] EP1360: Child RSS (spawn-per-message): spawn=${spawnChildRssMb ?? "null"}MB, result=${resultChildRssMb ?? "null"}MB${childDeltaMb !== null ? ` (delta: ${childDeltaMb >= 0 ? "+" : ""}${childDeltaMb}MB)` : ""}, session=${sessionId}, pid=${childProcess.pid}`);
|
|
11281
|
+
console.log(`[AgentManager] EP1360: Stream stats: parsedLines=${parsedLineCount}, rawDataEvents=${stdoutEventCount}, chunksSent=${chunksSent}, session=${sessionId}`);
|
|
9965
11282
|
console.log(`[AgentManager] EP1191: ${provider} CLI exited for session ${sessionId}: code=${code}, signal=${signal}, duration=${duration}ms, stdoutEvents=${stdoutEventCount}, chunksSent=${chunksSent}`);
|
|
9966
11283
|
this.processes.delete(sessionId);
|
|
9967
11284
|
this.removePidFile(sessionId);
|
|
@@ -10015,7 +11332,18 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10015
11332
|
console.log(`[AgentManager] Aborting session ${sessionId} with SIGINT`);
|
|
10016
11333
|
agentProcess.kill("SIGINT");
|
|
10017
11334
|
}
|
|
11335
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11336
|
+
if (runtime) {
|
|
11337
|
+
console.log(`[AgentManager] EP1360: Shutting down persistent runtime for aborted session ${sessionId}`);
|
|
11338
|
+
void runtime.shutdown();
|
|
11339
|
+
this.runtimes.delete(sessionId);
|
|
11340
|
+
}
|
|
10018
11341
|
const session = this.sessions.get(sessionId);
|
|
11342
|
+
if (session) {
|
|
11343
|
+
session.pid = void 0;
|
|
11344
|
+
}
|
|
11345
|
+
this.removePidFile(sessionId);
|
|
11346
|
+
this.persistentDisabledSessions.delete(sessionId);
|
|
10019
11347
|
if (session) {
|
|
10020
11348
|
session.status = "stopping";
|
|
10021
11349
|
}
|
|
@@ -10049,6 +11377,16 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10049
11377
|
});
|
|
10050
11378
|
});
|
|
10051
11379
|
}
|
|
11380
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11381
|
+
if (runtime) {
|
|
11382
|
+
try {
|
|
11383
|
+
await runtime.shutdown();
|
|
11384
|
+
} catch (error) {
|
|
11385
|
+
console.warn(`[AgentManager] EP1360: Error shutting down runtime for session ${sessionId}:`, error);
|
|
11386
|
+
}
|
|
11387
|
+
this.runtimes.delete(sessionId);
|
|
11388
|
+
}
|
|
11389
|
+
this.persistentDisabledSessions.delete(sessionId);
|
|
10052
11390
|
this.sessions.delete(sessionId);
|
|
10053
11391
|
this.processes.delete(sessionId);
|
|
10054
11392
|
this.removePidFile(sessionId);
|
|
@@ -10077,8 +11415,183 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10077
11415
|
* Stop all active sessions
|
|
10078
11416
|
*/
|
|
10079
11417
|
async stopAllSessions() {
|
|
11418
|
+
this.stopIdleScanner();
|
|
10080
11419
|
const sessionIds = Array.from(this.sessions.keys());
|
|
10081
11420
|
await Promise.all(sessionIds.map((id) => this.stopSession(id)));
|
|
11421
|
+
this.persistentDisabledSessions.clear();
|
|
11422
|
+
}
|
|
11423
|
+
// ---------------------------------------------------------------------------
|
|
11424
|
+
// EP1360: Persistent Runtime Lifecycle
|
|
11425
|
+
// ---------------------------------------------------------------------------
|
|
11426
|
+
/**
|
|
11427
|
+
* EP1360: Check if persistent runtimes are enabled.
|
|
11428
|
+
*/
|
|
11429
|
+
isPersistentEnabled() {
|
|
11430
|
+
return !AGENT_DISABLE_PERSISTENT;
|
|
11431
|
+
}
|
|
11432
|
+
/**
|
|
11433
|
+
* EP1360: Get the persistent runtime for a session, if one exists and is alive.
|
|
11434
|
+
*/
|
|
11435
|
+
getRuntime(sessionId) {
|
|
11436
|
+
if (this.persistentDisabledSessions.has(sessionId)) {
|
|
11437
|
+
return void 0;
|
|
11438
|
+
}
|
|
11439
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11440
|
+
if (runtime && runtime.isAlive()) {
|
|
11441
|
+
return runtime;
|
|
11442
|
+
}
|
|
11443
|
+
if (runtime && !runtime.isAlive()) {
|
|
11444
|
+
this.runtimes.delete(sessionId);
|
|
11445
|
+
const session = this.sessions.get(sessionId);
|
|
11446
|
+
if (session) {
|
|
11447
|
+
session.pid = void 0;
|
|
11448
|
+
}
|
|
11449
|
+
this.removePidFile(sessionId);
|
|
11450
|
+
}
|
|
11451
|
+
return void 0;
|
|
11452
|
+
}
|
|
11453
|
+
/**
|
|
11454
|
+
* EP1360: Create a persistent runtime for a session.
|
|
11455
|
+
* Returns the runtime if a slot is available (possibly after evicting an idle runtime).
|
|
11456
|
+
* Returns null if all slots are occupied by active streams (degrade to spawn-per-message).
|
|
11457
|
+
*/
|
|
11458
|
+
createRuntime(sessionId, provider, options) {
|
|
11459
|
+
if (AGENT_DISABLE_PERSISTENT) {
|
|
11460
|
+
return null;
|
|
11461
|
+
}
|
|
11462
|
+
if (this.persistentDisabledSessions.has(sessionId)) {
|
|
11463
|
+
return null;
|
|
11464
|
+
}
|
|
11465
|
+
const existing = this.getRuntime(sessionId);
|
|
11466
|
+
if (existing) {
|
|
11467
|
+
return existing;
|
|
11468
|
+
}
|
|
11469
|
+
if (this.runtimes.size >= AGENT_MAX_PERSISTENT) {
|
|
11470
|
+
if (!this.evictIdleRuntime()) {
|
|
11471
|
+
console.log(`[AgentManager] EP1360: All ${AGENT_MAX_PERSISTENT} persistent slots active \u2014 degrading to spawn-per-message for session ${sessionId}`);
|
|
11472
|
+
return null;
|
|
11473
|
+
}
|
|
11474
|
+
}
|
|
11475
|
+
let runtime;
|
|
11476
|
+
if (provider === "claude") {
|
|
11477
|
+
const claudeRuntime = new ClaudePersistentRuntime(options);
|
|
11478
|
+
claudeRuntime.spawn();
|
|
11479
|
+
runtime = claudeRuntime;
|
|
11480
|
+
} else {
|
|
11481
|
+
const codexRuntime = new CodexPersistentRuntime(options);
|
|
11482
|
+
codexRuntime.spawn();
|
|
11483
|
+
runtime = codexRuntime;
|
|
11484
|
+
}
|
|
11485
|
+
this.runtimes.set(sessionId, runtime);
|
|
11486
|
+
console.log(`[AgentManager] EP1360: Created persistent ${provider} runtime for session ${sessionId} (${this.runtimes.size}/${AGENT_MAX_PERSISTENT} slots used)`);
|
|
11487
|
+
return runtime;
|
|
11488
|
+
}
|
|
11489
|
+
/**
|
|
11490
|
+
* EP1360: Shut down a persistent runtime for a session.
|
|
11491
|
+
*/
|
|
11492
|
+
async shutdownRuntime(sessionId) {
|
|
11493
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11494
|
+
if (!runtime) return;
|
|
11495
|
+
try {
|
|
11496
|
+
await runtime.shutdown();
|
|
11497
|
+
} catch (error) {
|
|
11498
|
+
console.warn(`[AgentManager] EP1360: Error shutting down runtime for session ${sessionId}:`, error);
|
|
11499
|
+
}
|
|
11500
|
+
this.runtimes.delete(sessionId);
|
|
11501
|
+
const session = this.sessions.get(sessionId);
|
|
11502
|
+
if (session) {
|
|
11503
|
+
session.pid = void 0;
|
|
11504
|
+
}
|
|
11505
|
+
this.removePidFile(sessionId);
|
|
11506
|
+
}
|
|
11507
|
+
/**
|
|
11508
|
+
* EP1360: Permanently disable persistent mode for a session (remainder of session).
|
|
11509
|
+
* Minimal "auto-downgrade" mechanism: once the runtime shows runtime-level failure
|
|
11510
|
+
* signals (timeouts/crash/spawn), future messages use spawn-per-message.
|
|
11511
|
+
*/
|
|
11512
|
+
disablePersistentForSession(sessionId, reason) {
|
|
11513
|
+
if (this.persistentDisabledSessions.has(sessionId)) return;
|
|
11514
|
+
this.persistentDisabledSessions.set(sessionId, { reason, at: Date.now() });
|
|
11515
|
+
console.warn(`[AgentManager] EP1360: Disabling persistent mode for session ${sessionId} (downgrade to spawn-per-message): ${reason}`);
|
|
11516
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11517
|
+
if (runtime) {
|
|
11518
|
+
void runtime.shutdown().catch((err) => {
|
|
11519
|
+
console.warn(`[AgentManager] EP1360: Error shutting down downgraded runtime for session ${sessionId}:`, err);
|
|
11520
|
+
});
|
|
11521
|
+
this.runtimes.delete(sessionId);
|
|
11522
|
+
}
|
|
11523
|
+
const session = this.sessions.get(sessionId);
|
|
11524
|
+
if (session) {
|
|
11525
|
+
session.pid = void 0;
|
|
11526
|
+
}
|
|
11527
|
+
this.removePidFile(sessionId);
|
|
11528
|
+
}
|
|
11529
|
+
/**
|
|
11530
|
+
* EP1360: Evict the least-recently-used idle runtime to free a slot.
|
|
11531
|
+
* Returns true if a slot was freed, false if all runtimes are actively streaming.
|
|
11532
|
+
*/
|
|
11533
|
+
evictIdleRuntime() {
|
|
11534
|
+
let oldestIdleSessionId = null;
|
|
11535
|
+
let oldestActivityTime = Infinity;
|
|
11536
|
+
for (const [sid, runtime2] of this.runtimes) {
|
|
11537
|
+
if (runtime2.turnState === "idle") {
|
|
11538
|
+
const session2 = this.sessions.get(sid);
|
|
11539
|
+
const activityTime = session2?.lastActivityAt?.getTime() ?? 0;
|
|
11540
|
+
if (activityTime < oldestActivityTime) {
|
|
11541
|
+
oldestActivityTime = activityTime;
|
|
11542
|
+
oldestIdleSessionId = sid;
|
|
11543
|
+
}
|
|
11544
|
+
}
|
|
11545
|
+
}
|
|
11546
|
+
if (!oldestIdleSessionId) {
|
|
11547
|
+
return false;
|
|
11548
|
+
}
|
|
11549
|
+
console.log(`[AgentManager] EP1360: Evicting idle runtime for session ${oldestIdleSessionId} (LRU)`);
|
|
11550
|
+
const runtime = this.runtimes.get(oldestIdleSessionId);
|
|
11551
|
+
if (runtime) {
|
|
11552
|
+
void runtime.shutdown().catch((err) => {
|
|
11553
|
+
console.warn(`[AgentManager] EP1360: Error during eviction shutdown:`, err);
|
|
11554
|
+
});
|
|
11555
|
+
}
|
|
11556
|
+
this.runtimes.delete(oldestIdleSessionId);
|
|
11557
|
+
const session = this.sessions.get(oldestIdleSessionId);
|
|
11558
|
+
if (session) {
|
|
11559
|
+
session.pid = void 0;
|
|
11560
|
+
}
|
|
11561
|
+
this.removePidFile(oldestIdleSessionId);
|
|
11562
|
+
return true;
|
|
11563
|
+
}
|
|
11564
|
+
/**
|
|
11565
|
+
* EP1360: Start the idle runtime scanner.
|
|
11566
|
+
* Runs every 60s and shuts down runtimes that have been idle longer than AGENT_IDLE_TIMEOUT_MS.
|
|
11567
|
+
*/
|
|
11568
|
+
startIdleScanner() {
|
|
11569
|
+
if (this.idleScanTimer) return;
|
|
11570
|
+
this.idleScanTimer = setInterval(() => {
|
|
11571
|
+
const now = Date.now();
|
|
11572
|
+
for (const [sid, runtime] of this.runtimes) {
|
|
11573
|
+
if (runtime.turnState !== "idle") continue;
|
|
11574
|
+
const session = this.sessions.get(sid);
|
|
11575
|
+
const lastActivity = session?.lastActivityAt?.getTime() ?? 0;
|
|
11576
|
+
const idleMs = now - lastActivity;
|
|
11577
|
+
if (idleMs > AGENT_IDLE_TIMEOUT_MS) {
|
|
11578
|
+
console.log(`[AgentManager] EP1360: Runtime for session ${sid} idle for ${Math.round(idleMs / 1e3)}s \u2014 shutting down`);
|
|
11579
|
+
void this.shutdownRuntime(sid);
|
|
11580
|
+
}
|
|
11581
|
+
}
|
|
11582
|
+
}, IDLE_SCAN_INTERVAL_MS);
|
|
11583
|
+
if (this.idleScanTimer.unref) {
|
|
11584
|
+
this.idleScanTimer.unref();
|
|
11585
|
+
}
|
|
11586
|
+
}
|
|
11587
|
+
/**
|
|
11588
|
+
* EP1360: Stop the idle runtime scanner.
|
|
11589
|
+
*/
|
|
11590
|
+
stopIdleScanner() {
|
|
11591
|
+
if (this.idleScanTimer) {
|
|
11592
|
+
clearInterval(this.idleScanTimer);
|
|
11593
|
+
this.idleScanTimer = null;
|
|
11594
|
+
}
|
|
10082
11595
|
}
|
|
10083
11596
|
/**
|
|
10084
11597
|
* Get session info
|
|
@@ -10129,7 +11642,8 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10129
11642
|
moduleUid: session.moduleUid,
|
|
10130
11643
|
status: reconStatus,
|
|
10131
11644
|
agentSessionId: session.agentSessionId || session.claudeSessionId,
|
|
10132
|
-
lastActivityAt: session.lastActivityAt
|
|
11645
|
+
lastActivityAt: session.lastActivityAt,
|
|
11646
|
+
persistentRuntime: this.runtimes.has(sessionId)
|
|
10133
11647
|
});
|
|
10134
11648
|
}
|
|
10135
11649
|
return result;
|
|
@@ -10400,7 +11914,7 @@ var AgentCommandQueue = class {
|
|
|
10400
11914
|
};
|
|
10401
11915
|
|
|
10402
11916
|
// src/utils/dev-server.ts
|
|
10403
|
-
var
|
|
11917
|
+
var import_child_process15 = require("child_process");
|
|
10404
11918
|
var import_core12 = __toESM(require_dist());
|
|
10405
11919
|
var fs20 = __toESM(require("fs"));
|
|
10406
11920
|
var path21 = __toESM(require("path"));
|
|
@@ -10449,7 +11963,7 @@ function writeToLog(logPath, line, isError = false) {
|
|
|
10449
11963
|
}
|
|
10450
11964
|
async function killProcessOnPort(port) {
|
|
10451
11965
|
try {
|
|
10452
|
-
const result = (0,
|
|
11966
|
+
const result = (0, import_child_process15.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
|
|
10453
11967
|
if (!result) {
|
|
10454
11968
|
console.log(`[DevServer] EP929: No process found on port ${port}`);
|
|
10455
11969
|
return true;
|
|
@@ -10458,7 +11972,7 @@ async function killProcessOnPort(port) {
|
|
|
10458
11972
|
console.log(`[DevServer] EP929: Found ${pids.length} process(es) on port ${port}: ${pids.join(", ")}`);
|
|
10459
11973
|
for (const pid of pids) {
|
|
10460
11974
|
try {
|
|
10461
|
-
(0,
|
|
11975
|
+
(0, import_child_process15.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
10462
11976
|
console.log(`[DevServer] EP929: Sent SIGTERM to PID ${pid}`);
|
|
10463
11977
|
} catch {
|
|
10464
11978
|
}
|
|
@@ -10466,8 +11980,8 @@ async function killProcessOnPort(port) {
|
|
|
10466
11980
|
await new Promise((resolve4) => setTimeout(resolve4, 1e3));
|
|
10467
11981
|
for (const pid of pids) {
|
|
10468
11982
|
try {
|
|
10469
|
-
(0,
|
|
10470
|
-
(0,
|
|
11983
|
+
(0, import_child_process15.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
11984
|
+
(0, import_child_process15.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
10471
11985
|
console.log(`[DevServer] EP929: Force killed PID ${pid}`);
|
|
10472
11986
|
} catch {
|
|
10473
11987
|
}
|
|
@@ -10518,7 +12032,7 @@ function spawnDevServerProcess(projectPath, port, moduleUid, logPath, customComm
|
|
|
10518
12032
|
if (injectedCount > 0) {
|
|
10519
12033
|
console.log(`[DevServer] EP998: Injecting ${injectedCount} env vars from database`);
|
|
10520
12034
|
}
|
|
10521
|
-
const devProcess = (0,
|
|
12035
|
+
const devProcess = (0, import_child_process15.spawn)(cmd, args, {
|
|
10522
12036
|
cwd: projectPath,
|
|
10523
12037
|
env: mergedEnv,
|
|
10524
12038
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -10881,7 +12395,7 @@ function getInstallCommand2(cwd) {
|
|
|
10881
12395
|
}
|
|
10882
12396
|
|
|
10883
12397
|
// src/daemon/daemon-process.ts
|
|
10884
|
-
var
|
|
12398
|
+
var import_child_process16 = require("child_process");
|
|
10885
12399
|
var fs23 = __toESM(require("fs"));
|
|
10886
12400
|
var http2 = __toESM(require("http"));
|
|
10887
12401
|
var os9 = __toESM(require("os"));
|
|
@@ -10997,6 +12511,7 @@ async function fetchEnvVars2(projectId) {
|
|
|
10997
12511
|
}
|
|
10998
12512
|
}
|
|
10999
12513
|
var Daemon = class _Daemon {
|
|
12514
|
+
// sessionId -> last seq
|
|
11000
12515
|
constructor() {
|
|
11001
12516
|
this.machineId = "";
|
|
11002
12517
|
this.machineUuid = null;
|
|
@@ -11056,6 +12571,8 @@ var Daemon = class _Daemon {
|
|
|
11056
12571
|
// EP1324: Retry limiting for failed update attempts
|
|
11057
12572
|
this.lastFailedUpdateVersion = null;
|
|
11058
12573
|
this.updateFailedAttempts = 0;
|
|
12574
|
+
// EP1360: Per-session monotonic event seq for daemon→platform stream gap detection.
|
|
12575
|
+
this.agentEventSeq = /* @__PURE__ */ new Map();
|
|
11059
12576
|
this.ipcServer = new IPCServer();
|
|
11060
12577
|
}
|
|
11061
12578
|
static {
|
|
@@ -11095,6 +12612,11 @@ var Daemon = class _Daemon {
|
|
|
11095
12612
|
static {
|
|
11096
12613
|
this.MAX_UPDATE_ATTEMPTS = 3;
|
|
11097
12614
|
}
|
|
12615
|
+
nextAgentSeq(sessionId) {
|
|
12616
|
+
const next = (this.agentEventSeq.get(sessionId) ?? 0) + 1;
|
|
12617
|
+
this.agentEventSeq.set(sessionId, next);
|
|
12618
|
+
return next;
|
|
12619
|
+
}
|
|
11098
12620
|
/**
|
|
11099
12621
|
* Start the daemon
|
|
11100
12622
|
*/
|
|
@@ -11208,7 +12730,7 @@ var Daemon = class _Daemon {
|
|
|
11208
12730
|
const configDir = getConfigDir9();
|
|
11209
12731
|
const logPath = path24.join(configDir, "daemon.log");
|
|
11210
12732
|
const logFd = fs23.openSync(logPath, "a");
|
|
11211
|
-
const child = (0,
|
|
12733
|
+
const child = (0, import_child_process16.spawn)("node", [__filename], {
|
|
11212
12734
|
detached: true,
|
|
11213
12735
|
stdio: ["ignore", logFd, logFd],
|
|
11214
12736
|
env: { ...process.env, EPISODA_DAEMON_MODE: "1" }
|
|
@@ -11978,7 +13500,7 @@ var Daemon = class _Daemon {
|
|
|
11978
13500
|
await client.send({
|
|
11979
13501
|
type: "agent_result",
|
|
11980
13502
|
commandId,
|
|
11981
|
-
result: { success: true, status: "chunk", sessionId, chunk }
|
|
13503
|
+
result: { success: true, status: "chunk", sessionId, seq: this.nextAgentSeq(sessionId), chunk }
|
|
11982
13504
|
});
|
|
11983
13505
|
} catch (sendError) {
|
|
11984
13506
|
console.error(`[Daemon] EP912: Failed to send chunk (WebSocket may be disconnected):`, sendError);
|
|
@@ -11996,6 +13518,7 @@ var Daemon = class _Daemon {
|
|
|
11996
13518
|
success: true,
|
|
11997
13519
|
status: "tool_use",
|
|
11998
13520
|
sessionId,
|
|
13521
|
+
seq: this.nextAgentSeq(sessionId),
|
|
11999
13522
|
toolUse: event
|
|
12000
13523
|
}
|
|
12001
13524
|
});
|
|
@@ -12013,6 +13536,7 @@ var Daemon = class _Daemon {
|
|
|
12013
13536
|
success: true,
|
|
12014
13537
|
status: "tool_result",
|
|
12015
13538
|
sessionId,
|
|
13539
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12016
13540
|
toolResult: event
|
|
12017
13541
|
}
|
|
12018
13542
|
});
|
|
@@ -12031,6 +13555,7 @@ var Daemon = class _Daemon {
|
|
|
12031
13555
|
success: true,
|
|
12032
13556
|
status: "compaction",
|
|
12033
13557
|
sessionId,
|
|
13558
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12034
13559
|
compaction: event
|
|
12035
13560
|
}
|
|
12036
13561
|
});
|
|
@@ -12046,7 +13571,7 @@ var Daemon = class _Daemon {
|
|
|
12046
13571
|
await client.send({
|
|
12047
13572
|
type: "agent_result",
|
|
12048
13573
|
commandId,
|
|
12049
|
-
result: { success: true, status: "complete", sessionId, claudeSessionId, resultMetadata }
|
|
13574
|
+
result: { success: true, status: "complete", sessionId, seq: this.nextAgentSeq(sessionId), claudeSessionId, resultMetadata }
|
|
12050
13575
|
});
|
|
12051
13576
|
} catch (sendError) {
|
|
12052
13577
|
console.error(`[Daemon] EP912: Failed to send complete (WebSocket may be disconnected):`, sendError);
|
|
@@ -12060,7 +13585,7 @@ var Daemon = class _Daemon {
|
|
|
12060
13585
|
await client.send({
|
|
12061
13586
|
type: "agent_result",
|
|
12062
13587
|
commandId,
|
|
12063
|
-
result: { success: false, status: "error", sessionId, error }
|
|
13588
|
+
result: { success: false, status: "error", sessionId, seq: this.nextAgentSeq(sessionId), error }
|
|
12064
13589
|
});
|
|
12065
13590
|
} catch (sendError) {
|
|
12066
13591
|
console.error(`[Daemon] EP912: Failed to send error (WebSocket may be disconnected):`, sendError);
|
|
@@ -12133,6 +13658,7 @@ var Daemon = class _Daemon {
|
|
|
12133
13658
|
success: startResult.success,
|
|
12134
13659
|
status: startResult.success ? "started" : "error",
|
|
12135
13660
|
sessionId: cmd.sessionId,
|
|
13661
|
+
seq: this.nextAgentSeq(cmd.sessionId),
|
|
12136
13662
|
error: startResult.error
|
|
12137
13663
|
};
|
|
12138
13664
|
} else if (cmd.action === "message") {
|
|
@@ -12156,14 +13682,16 @@ var Daemon = class _Daemon {
|
|
|
12156
13682
|
success: sendResult.success,
|
|
12157
13683
|
status: sendResult.success ? "started" : "error",
|
|
12158
13684
|
sessionId: cmd.sessionId,
|
|
13685
|
+
seq: this.nextAgentSeq(cmd.sessionId),
|
|
12159
13686
|
error: sendResult.error
|
|
12160
13687
|
};
|
|
12161
13688
|
} else if (cmd.action === "abort") {
|
|
12162
13689
|
await agentManager.abortSession(cmd.sessionId);
|
|
12163
|
-
result = { success: true, status: "aborted", sessionId: cmd.sessionId };
|
|
13690
|
+
result = { success: true, status: "aborted", sessionId: cmd.sessionId, seq: this.nextAgentSeq(cmd.sessionId) };
|
|
12164
13691
|
} else if (cmd.action === "stop") {
|
|
12165
13692
|
await agentManager.stopSession(cmd.sessionId);
|
|
12166
|
-
result = { success: true, status: "complete", sessionId: cmd.sessionId };
|
|
13693
|
+
result = { success: true, status: "complete", sessionId: cmd.sessionId, seq: this.nextAgentSeq(cmd.sessionId) };
|
|
13694
|
+
this.agentEventSeq.delete(cmd.sessionId);
|
|
12167
13695
|
} else {
|
|
12168
13696
|
result = {
|
|
12169
13697
|
success: false,
|
|
@@ -12189,6 +13717,7 @@ var Daemon = class _Daemon {
|
|
|
12189
13717
|
success: false,
|
|
12190
13718
|
status: "error",
|
|
12191
13719
|
sessionId,
|
|
13720
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12192
13721
|
error: errorMsg
|
|
12193
13722
|
}
|
|
12194
13723
|
});
|