@episoda/cli 0.2.153 → 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 +1544 -18
- 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",
|
|
@@ -8535,7 +8535,7 @@ function generateCodexMcpConfigToml(servers, projectPath) {
|
|
|
8535
8535
|
}
|
|
8536
8536
|
|
|
8537
8537
|
// src/agent/agent-manager.ts
|
|
8538
|
-
var
|
|
8538
|
+
var import_child_process14 = require("child_process");
|
|
8539
8539
|
var path20 = __toESM(require("path"));
|
|
8540
8540
|
var fs19 = __toESM(require("fs"));
|
|
8541
8541
|
var os7 = __toESM(require("os"));
|
|
@@ -8842,6 +8842,1057 @@ function generateClaudeConfig(options = {}) {
|
|
|
8842
8842
|
|
|
8843
8843
|
// src/agent/agent-manager.ts
|
|
8844
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
|
|
8845
9896
|
var instance3 = null;
|
|
8846
9897
|
function getAgentManager() {
|
|
8847
9898
|
if (!instance3) {
|
|
@@ -8849,6 +9900,11 @@ function getAgentManager() {
|
|
|
8849
9900
|
}
|
|
8850
9901
|
return instance3;
|
|
8851
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"];
|
|
8852
9908
|
var AgentManager = class {
|
|
8853
9909
|
constructor() {
|
|
8854
9910
|
this.sessions = /* @__PURE__ */ new Map();
|
|
@@ -8856,8 +9912,41 @@ var AgentManager = class {
|
|
|
8856
9912
|
this.initialized = false;
|
|
8857
9913
|
// EP1133: Lock for config file writes to prevent race conditions
|
|
8858
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;
|
|
8859
9921
|
this.pidDir = path20.join(os7.homedir(), ".episoda", "agent-pids");
|
|
8860
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
|
+
}
|
|
8861
9950
|
/**
|
|
8862
9951
|
* EP1133: Acquire lock for config file writes
|
|
8863
9952
|
* Ensures sequential writes to prevent file corruption
|
|
@@ -9152,6 +10241,12 @@ var AgentManager = class {
|
|
|
9152
10241
|
} catch (error) {
|
|
9153
10242
|
console.warn("[AgentManager] Codex CLI not available:", error instanceof Error ? error.message : error);
|
|
9154
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
|
+
}
|
|
9155
10250
|
this.initialized = true;
|
|
9156
10251
|
console.log("[AgentManager] Initialized");
|
|
9157
10252
|
}
|
|
@@ -9276,7 +10371,8 @@ var AgentManager = class {
|
|
|
9276
10371
|
/**
|
|
9277
10372
|
* Send a message to an agent session
|
|
9278
10373
|
*
|
|
9279
|
-
*
|
|
10374
|
+
* EP1360: Uses persistent runtimes when enabled/available; falls back to
|
|
10375
|
+
* spawn-per-message for incident response, pool pressure, or per-session downgrade.
|
|
9280
10376
|
* EP1133: Supports both Claude Code and Codex CLI with provider-specific handling.
|
|
9281
10377
|
* EP1251: Supports credentials refresh for GitHub token on resume.
|
|
9282
10378
|
*/
|
|
@@ -9286,6 +10382,22 @@ var AgentManager = class {
|
|
|
9286
10382
|
if (!session) {
|
|
9287
10383
|
return { success: false, error: "Session not found" };
|
|
9288
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
|
+
};
|
|
9289
10401
|
const mcpAuth = await this.resolveMcpAuth();
|
|
9290
10402
|
if (mcpAuth.source === "config") {
|
|
9291
10403
|
console.log("[AgentManager] EP1287: Using MCP token from ~/.episoda/config.json");
|
|
@@ -9298,6 +10410,78 @@ var AgentManager = class {
|
|
|
9298
10410
|
session.canWrite = canWrite;
|
|
9299
10411
|
session.readOnlyReason = readOnlyReason;
|
|
9300
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
|
+
}
|
|
9301
10485
|
if (credentials?.githubToken) {
|
|
9302
10486
|
session.credentials.githubToken = credentials.githubToken;
|
|
9303
10487
|
session.credentials.githubTokenExpiresAt = credentials.githubTokenExpiresAt;
|
|
@@ -9345,6 +10529,8 @@ Violations will result in errors and may affect your ability to assist.
|
|
|
9345
10529
|
}
|
|
9346
10530
|
let binaryPath;
|
|
9347
10531
|
let args;
|
|
10532
|
+
let argsBeforeMessage = 0;
|
|
10533
|
+
let runtimeFirstMessage = message;
|
|
9348
10534
|
if (provider === "codex") {
|
|
9349
10535
|
binaryPath = await ensureCodexBinary();
|
|
9350
10536
|
args = [
|
|
@@ -9390,6 +10576,8 @@ If changes are needed, explain what needs to be done but do not execute.
|
|
|
9390
10576
|
`;
|
|
9391
10577
|
fullMessage = readOnlyReminder + fullMessage;
|
|
9392
10578
|
}
|
|
10579
|
+
argsBeforeMessage = args.length;
|
|
10580
|
+
runtimeFirstMessage = fullMessage;
|
|
9393
10581
|
args.push(fullMessage);
|
|
9394
10582
|
} else {
|
|
9395
10583
|
binaryPath = await ensureClaudeBinary();
|
|
@@ -9425,6 +10613,9 @@ Reason: ${session.readOnlyReason || "No write access to this module."}
|
|
|
9425
10613
|
If changes are needed, explain what needs to be done.`;
|
|
9426
10614
|
args.push("--append-system-prompt", readOnlyReminder);
|
|
9427
10615
|
console.log("[AgentManager] EP1205: Appended read-only reminder to subsequent message");
|
|
10616
|
+
runtimeFirstMessage = `${readOnlyReminder}
|
|
10617
|
+
|
|
10618
|
+
${message}`;
|
|
9428
10619
|
}
|
|
9429
10620
|
}
|
|
9430
10621
|
if (resumeSessionId) {
|
|
@@ -9481,6 +10672,7 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9481
10672
|
args.push("--mcp-config", mcpConfigJson);
|
|
9482
10673
|
console.log(`[AgentManager] EP1253: MCP config with env: ${mcpConfigJson.substring(0, 200)}...`);
|
|
9483
10674
|
}
|
|
10675
|
+
argsBeforeMessage = args.length;
|
|
9484
10676
|
args.push("--", message);
|
|
9485
10677
|
}
|
|
9486
10678
|
console.log(`[AgentManager] Spawning ${provider} CLI for session ${sessionId}`);
|
|
@@ -9725,7 +10917,95 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9725
10917
|
envVars.ANTHROPIC_API_KEY = session.credentials.apiKey;
|
|
9726
10918
|
}
|
|
9727
10919
|
}
|
|
9728
|
-
|
|
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, {
|
|
9729
11009
|
cwd: session.projectPath,
|
|
9730
11010
|
env: envVars,
|
|
9731
11011
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9736,6 +11016,14 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9736
11016
|
session.pid = childProcess.pid;
|
|
9737
11017
|
this.writePidFile(sessionId, childProcess.pid);
|
|
9738
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
|
+
});
|
|
9739
11027
|
childProcess.stderr?.on("data", (data) => {
|
|
9740
11028
|
console.error(`[AgentManager] stderr: ${data.toString()}`);
|
|
9741
11029
|
});
|
|
@@ -9751,6 +11039,8 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9751
11039
|
let stdoutEventCount = 0;
|
|
9752
11040
|
let chunksSent = 0;
|
|
9753
11041
|
const streamStartTime = Date.now();
|
|
11042
|
+
let ttftLogged = false;
|
|
11043
|
+
let parsedLineCount = 0;
|
|
9754
11044
|
let hasContent = false;
|
|
9755
11045
|
childProcess.stdout?.on("data", (data) => {
|
|
9756
11046
|
const rawData = data.toString();
|
|
@@ -9765,6 +11055,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9765
11055
|
if (!line.trim()) continue;
|
|
9766
11056
|
try {
|
|
9767
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
|
+
}
|
|
9768
11064
|
if (stdoutEventCount <= 10 || chunksSent === 0) {
|
|
9769
11065
|
console.log(`[AgentManager] EP1191: Parsed event type: ${parsed.type}`);
|
|
9770
11066
|
}
|
|
@@ -9819,6 +11115,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9819
11115
|
}
|
|
9820
11116
|
break;
|
|
9821
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
|
+
});
|
|
9822
11124
|
if (parsed.usage) {
|
|
9823
11125
|
const u = parsed.usage;
|
|
9824
11126
|
const inputTokens = u.input_tokens ?? u.prompt_tokens ?? 0;
|
|
@@ -9894,6 +11196,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9894
11196
|
}
|
|
9895
11197
|
break;
|
|
9896
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
|
+
});
|
|
9897
11205
|
if (parsed.session_id) {
|
|
9898
11206
|
extractedSessionId = parsed.session_id;
|
|
9899
11207
|
session.agentSessionId = extractedSessionId;
|
|
@@ -9965,6 +11273,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9965
11273
|
});
|
|
9966
11274
|
childProcess.on("exit", (code, signal) => {
|
|
9967
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}`);
|
|
9968
11282
|
console.log(`[AgentManager] EP1191: ${provider} CLI exited for session ${sessionId}: code=${code}, signal=${signal}, duration=${duration}ms, stdoutEvents=${stdoutEventCount}, chunksSent=${chunksSent}`);
|
|
9969
11283
|
this.processes.delete(sessionId);
|
|
9970
11284
|
this.removePidFile(sessionId);
|
|
@@ -10018,7 +11332,18 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10018
11332
|
console.log(`[AgentManager] Aborting session ${sessionId} with SIGINT`);
|
|
10019
11333
|
agentProcess.kill("SIGINT");
|
|
10020
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
|
+
}
|
|
10021
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);
|
|
10022
11347
|
if (session) {
|
|
10023
11348
|
session.status = "stopping";
|
|
10024
11349
|
}
|
|
@@ -10052,6 +11377,16 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10052
11377
|
});
|
|
10053
11378
|
});
|
|
10054
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);
|
|
10055
11390
|
this.sessions.delete(sessionId);
|
|
10056
11391
|
this.processes.delete(sessionId);
|
|
10057
11392
|
this.removePidFile(sessionId);
|
|
@@ -10080,8 +11415,183 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10080
11415
|
* Stop all active sessions
|
|
10081
11416
|
*/
|
|
10082
11417
|
async stopAllSessions() {
|
|
11418
|
+
this.stopIdleScanner();
|
|
10083
11419
|
const sessionIds = Array.from(this.sessions.keys());
|
|
10084
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
|
+
}
|
|
10085
11595
|
}
|
|
10086
11596
|
/**
|
|
10087
11597
|
* Get session info
|
|
@@ -10132,7 +11642,8 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10132
11642
|
moduleUid: session.moduleUid,
|
|
10133
11643
|
status: reconStatus,
|
|
10134
11644
|
agentSessionId: session.agentSessionId || session.claudeSessionId,
|
|
10135
|
-
lastActivityAt: session.lastActivityAt
|
|
11645
|
+
lastActivityAt: session.lastActivityAt,
|
|
11646
|
+
persistentRuntime: this.runtimes.has(sessionId)
|
|
10136
11647
|
});
|
|
10137
11648
|
}
|
|
10138
11649
|
return result;
|
|
@@ -10403,7 +11914,7 @@ var AgentCommandQueue = class {
|
|
|
10403
11914
|
};
|
|
10404
11915
|
|
|
10405
11916
|
// src/utils/dev-server.ts
|
|
10406
|
-
var
|
|
11917
|
+
var import_child_process15 = require("child_process");
|
|
10407
11918
|
var import_core12 = __toESM(require_dist());
|
|
10408
11919
|
var fs20 = __toESM(require("fs"));
|
|
10409
11920
|
var path21 = __toESM(require("path"));
|
|
@@ -10452,7 +11963,7 @@ function writeToLog(logPath, line, isError = false) {
|
|
|
10452
11963
|
}
|
|
10453
11964
|
async function killProcessOnPort(port) {
|
|
10454
11965
|
try {
|
|
10455
|
-
const result = (0,
|
|
11966
|
+
const result = (0, import_child_process15.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
|
|
10456
11967
|
if (!result) {
|
|
10457
11968
|
console.log(`[DevServer] EP929: No process found on port ${port}`);
|
|
10458
11969
|
return true;
|
|
@@ -10461,7 +11972,7 @@ async function killProcessOnPort(port) {
|
|
|
10461
11972
|
console.log(`[DevServer] EP929: Found ${pids.length} process(es) on port ${port}: ${pids.join(", ")}`);
|
|
10462
11973
|
for (const pid of pids) {
|
|
10463
11974
|
try {
|
|
10464
|
-
(0,
|
|
11975
|
+
(0, import_child_process15.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
10465
11976
|
console.log(`[DevServer] EP929: Sent SIGTERM to PID ${pid}`);
|
|
10466
11977
|
} catch {
|
|
10467
11978
|
}
|
|
@@ -10469,8 +11980,8 @@ async function killProcessOnPort(port) {
|
|
|
10469
11980
|
await new Promise((resolve4) => setTimeout(resolve4, 1e3));
|
|
10470
11981
|
for (const pid of pids) {
|
|
10471
11982
|
try {
|
|
10472
|
-
(0,
|
|
10473
|
-
(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" });
|
|
10474
11985
|
console.log(`[DevServer] EP929: Force killed PID ${pid}`);
|
|
10475
11986
|
} catch {
|
|
10476
11987
|
}
|
|
@@ -10521,7 +12032,7 @@ function spawnDevServerProcess(projectPath, port, moduleUid, logPath, customComm
|
|
|
10521
12032
|
if (injectedCount > 0) {
|
|
10522
12033
|
console.log(`[DevServer] EP998: Injecting ${injectedCount} env vars from database`);
|
|
10523
12034
|
}
|
|
10524
|
-
const devProcess = (0,
|
|
12035
|
+
const devProcess = (0, import_child_process15.spawn)(cmd, args, {
|
|
10525
12036
|
cwd: projectPath,
|
|
10526
12037
|
env: mergedEnv,
|
|
10527
12038
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -10884,7 +12395,7 @@ function getInstallCommand2(cwd) {
|
|
|
10884
12395
|
}
|
|
10885
12396
|
|
|
10886
12397
|
// src/daemon/daemon-process.ts
|
|
10887
|
-
var
|
|
12398
|
+
var import_child_process16 = require("child_process");
|
|
10888
12399
|
var fs23 = __toESM(require("fs"));
|
|
10889
12400
|
var http2 = __toESM(require("http"));
|
|
10890
12401
|
var os9 = __toESM(require("os"));
|
|
@@ -11000,6 +12511,7 @@ async function fetchEnvVars2(projectId) {
|
|
|
11000
12511
|
}
|
|
11001
12512
|
}
|
|
11002
12513
|
var Daemon = class _Daemon {
|
|
12514
|
+
// sessionId -> last seq
|
|
11003
12515
|
constructor() {
|
|
11004
12516
|
this.machineId = "";
|
|
11005
12517
|
this.machineUuid = null;
|
|
@@ -11059,6 +12571,8 @@ var Daemon = class _Daemon {
|
|
|
11059
12571
|
// EP1324: Retry limiting for failed update attempts
|
|
11060
12572
|
this.lastFailedUpdateVersion = null;
|
|
11061
12573
|
this.updateFailedAttempts = 0;
|
|
12574
|
+
// EP1360: Per-session monotonic event seq for daemon→platform stream gap detection.
|
|
12575
|
+
this.agentEventSeq = /* @__PURE__ */ new Map();
|
|
11062
12576
|
this.ipcServer = new IPCServer();
|
|
11063
12577
|
}
|
|
11064
12578
|
static {
|
|
@@ -11098,6 +12612,11 @@ var Daemon = class _Daemon {
|
|
|
11098
12612
|
static {
|
|
11099
12613
|
this.MAX_UPDATE_ATTEMPTS = 3;
|
|
11100
12614
|
}
|
|
12615
|
+
nextAgentSeq(sessionId) {
|
|
12616
|
+
const next = (this.agentEventSeq.get(sessionId) ?? 0) + 1;
|
|
12617
|
+
this.agentEventSeq.set(sessionId, next);
|
|
12618
|
+
return next;
|
|
12619
|
+
}
|
|
11101
12620
|
/**
|
|
11102
12621
|
* Start the daemon
|
|
11103
12622
|
*/
|
|
@@ -11211,7 +12730,7 @@ var Daemon = class _Daemon {
|
|
|
11211
12730
|
const configDir = getConfigDir9();
|
|
11212
12731
|
const logPath = path24.join(configDir, "daemon.log");
|
|
11213
12732
|
const logFd = fs23.openSync(logPath, "a");
|
|
11214
|
-
const child = (0,
|
|
12733
|
+
const child = (0, import_child_process16.spawn)("node", [__filename], {
|
|
11215
12734
|
detached: true,
|
|
11216
12735
|
stdio: ["ignore", logFd, logFd],
|
|
11217
12736
|
env: { ...process.env, EPISODA_DAEMON_MODE: "1" }
|
|
@@ -11981,7 +13500,7 @@ var Daemon = class _Daemon {
|
|
|
11981
13500
|
await client.send({
|
|
11982
13501
|
type: "agent_result",
|
|
11983
13502
|
commandId,
|
|
11984
|
-
result: { success: true, status: "chunk", sessionId, chunk }
|
|
13503
|
+
result: { success: true, status: "chunk", sessionId, seq: this.nextAgentSeq(sessionId), chunk }
|
|
11985
13504
|
});
|
|
11986
13505
|
} catch (sendError) {
|
|
11987
13506
|
console.error(`[Daemon] EP912: Failed to send chunk (WebSocket may be disconnected):`, sendError);
|
|
@@ -11999,6 +13518,7 @@ var Daemon = class _Daemon {
|
|
|
11999
13518
|
success: true,
|
|
12000
13519
|
status: "tool_use",
|
|
12001
13520
|
sessionId,
|
|
13521
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12002
13522
|
toolUse: event
|
|
12003
13523
|
}
|
|
12004
13524
|
});
|
|
@@ -12016,6 +13536,7 @@ var Daemon = class _Daemon {
|
|
|
12016
13536
|
success: true,
|
|
12017
13537
|
status: "tool_result",
|
|
12018
13538
|
sessionId,
|
|
13539
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12019
13540
|
toolResult: event
|
|
12020
13541
|
}
|
|
12021
13542
|
});
|
|
@@ -12034,6 +13555,7 @@ var Daemon = class _Daemon {
|
|
|
12034
13555
|
success: true,
|
|
12035
13556
|
status: "compaction",
|
|
12036
13557
|
sessionId,
|
|
13558
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12037
13559
|
compaction: event
|
|
12038
13560
|
}
|
|
12039
13561
|
});
|
|
@@ -12049,7 +13571,7 @@ var Daemon = class _Daemon {
|
|
|
12049
13571
|
await client.send({
|
|
12050
13572
|
type: "agent_result",
|
|
12051
13573
|
commandId,
|
|
12052
|
-
result: { success: true, status: "complete", sessionId, claudeSessionId, resultMetadata }
|
|
13574
|
+
result: { success: true, status: "complete", sessionId, seq: this.nextAgentSeq(sessionId), claudeSessionId, resultMetadata }
|
|
12053
13575
|
});
|
|
12054
13576
|
} catch (sendError) {
|
|
12055
13577
|
console.error(`[Daemon] EP912: Failed to send complete (WebSocket may be disconnected):`, sendError);
|
|
@@ -12063,7 +13585,7 @@ var Daemon = class _Daemon {
|
|
|
12063
13585
|
await client.send({
|
|
12064
13586
|
type: "agent_result",
|
|
12065
13587
|
commandId,
|
|
12066
|
-
result: { success: false, status: "error", sessionId, error }
|
|
13588
|
+
result: { success: false, status: "error", sessionId, seq: this.nextAgentSeq(sessionId), error }
|
|
12067
13589
|
});
|
|
12068
13590
|
} catch (sendError) {
|
|
12069
13591
|
console.error(`[Daemon] EP912: Failed to send error (WebSocket may be disconnected):`, sendError);
|
|
@@ -12136,6 +13658,7 @@ var Daemon = class _Daemon {
|
|
|
12136
13658
|
success: startResult.success,
|
|
12137
13659
|
status: startResult.success ? "started" : "error",
|
|
12138
13660
|
sessionId: cmd.sessionId,
|
|
13661
|
+
seq: this.nextAgentSeq(cmd.sessionId),
|
|
12139
13662
|
error: startResult.error
|
|
12140
13663
|
};
|
|
12141
13664
|
} else if (cmd.action === "message") {
|
|
@@ -12159,14 +13682,16 @@ var Daemon = class _Daemon {
|
|
|
12159
13682
|
success: sendResult.success,
|
|
12160
13683
|
status: sendResult.success ? "started" : "error",
|
|
12161
13684
|
sessionId: cmd.sessionId,
|
|
13685
|
+
seq: this.nextAgentSeq(cmd.sessionId),
|
|
12162
13686
|
error: sendResult.error
|
|
12163
13687
|
};
|
|
12164
13688
|
} else if (cmd.action === "abort") {
|
|
12165
13689
|
await agentManager.abortSession(cmd.sessionId);
|
|
12166
|
-
result = { success: true, status: "aborted", sessionId: cmd.sessionId };
|
|
13690
|
+
result = { success: true, status: "aborted", sessionId: cmd.sessionId, seq: this.nextAgentSeq(cmd.sessionId) };
|
|
12167
13691
|
} else if (cmd.action === "stop") {
|
|
12168
13692
|
await agentManager.stopSession(cmd.sessionId);
|
|
12169
|
-
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);
|
|
12170
13695
|
} else {
|
|
12171
13696
|
result = {
|
|
12172
13697
|
success: false,
|
|
@@ -12192,6 +13717,7 @@ var Daemon = class _Daemon {
|
|
|
12192
13717
|
success: false,
|
|
12193
13718
|
status: "error",
|
|
12194
13719
|
sessionId,
|
|
13720
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12195
13721
|
error: errorMsg
|
|
12196
13722
|
}
|
|
12197
13723
|
});
|