@episoda/cli 0.2.153 → 0.2.155
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 +1553 -19
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +9 -0
- 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.155",
|
|
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) {
|
|
@@ -9479,8 +10670,10 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9479
10670
|
}
|
|
9480
10671
|
const mcpConfigJson = JSON.stringify({ mcpServers: mcpConfig });
|
|
9481
10672
|
args.push("--mcp-config", mcpConfigJson);
|
|
9482
|
-
|
|
10673
|
+
const redactedConfig = mcpConfigJson.replace(/("[A-Z_]*(?:TOKEN|SECRET|KEY)"\s*:\s*")[^"]*(")/g, "$1[REDACTED]$2");
|
|
10674
|
+
console.log(`[AgentManager] EP1253: MCP config with env: ${redactedConfig.substring(0, 300)}...`);
|
|
9483
10675
|
}
|
|
10676
|
+
argsBeforeMessage = args.length;
|
|
9484
10677
|
args.push("--", message);
|
|
9485
10678
|
}
|
|
9486
10679
|
console.log(`[AgentManager] Spawning ${provider} CLI for session ${sessionId}`);
|
|
@@ -9725,7 +10918,95 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9725
10918
|
envVars.ANTHROPIC_API_KEY = session.credentials.apiKey;
|
|
9726
10919
|
}
|
|
9727
10920
|
}
|
|
9728
|
-
|
|
10921
|
+
if (provider === "claude") {
|
|
10922
|
+
envVars.CLAUDE_CODE_DISABLE_PLUGIN_CACHE = "1";
|
|
10923
|
+
}
|
|
10924
|
+
const daemonSpawnRssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
10925
|
+
const spawnTimestamp = Date.now();
|
|
10926
|
+
const modelLower = (session.credentials.preferredModel || "").toLowerCase();
|
|
10927
|
+
const modelBlockedForPersistent = provider === "claude" && CLAUDE_PERSISTENT_BLOCKED_MODELS.some((blocked) => modelLower.includes(blocked));
|
|
10928
|
+
if (modelBlockedForPersistent) {
|
|
10929
|
+
console.log(`[AgentManager] EP1360: Model "${session.credentials.preferredModel}" incompatible with persistent mode (#17406) \u2014 using spawn-per-message`);
|
|
10930
|
+
}
|
|
10931
|
+
if (this.isPersistentEnabled() && !modelBlockedForPersistent) {
|
|
10932
|
+
try {
|
|
10933
|
+
const baseArgs = args.slice(0, argsBeforeMessage);
|
|
10934
|
+
let persistentArgs;
|
|
10935
|
+
if (provider === "claude") {
|
|
10936
|
+
persistentArgs = [...baseArgs];
|
|
10937
|
+
persistentArgs.push(
|
|
10938
|
+
"--input-format",
|
|
10939
|
+
"stream-json",
|
|
10940
|
+
"--replay-user-messages"
|
|
10941
|
+
);
|
|
10942
|
+
} else {
|
|
10943
|
+
persistentArgs = ["app-server"];
|
|
10944
|
+
}
|
|
10945
|
+
let runtimeBinaryPath;
|
|
10946
|
+
let runtimeArgs;
|
|
10947
|
+
if (spawnCmd === "npx") {
|
|
10948
|
+
runtimeBinaryPath = "npx";
|
|
10949
|
+
runtimeArgs = ["--yes", binaryPath.replace("npx:", ""), ...persistentArgs];
|
|
10950
|
+
} else {
|
|
10951
|
+
runtimeBinaryPath = spawnCmd;
|
|
10952
|
+
runtimeArgs = persistentArgs;
|
|
10953
|
+
}
|
|
10954
|
+
const runtime = this.createRuntime(sessionId, provider, {
|
|
10955
|
+
sessionId,
|
|
10956
|
+
binaryPath: runtimeBinaryPath,
|
|
10957
|
+
args: runtimeArgs,
|
|
10958
|
+
env: envVars,
|
|
10959
|
+
cwd: session.projectPath,
|
|
10960
|
+
...provider === "codex" ? {
|
|
10961
|
+
model: session.credentials.preferredModel,
|
|
10962
|
+
resumeThreadId: session.agentSessionId || session.claudeSessionId
|
|
10963
|
+
} : {}
|
|
10964
|
+
});
|
|
10965
|
+
if (runtime) {
|
|
10966
|
+
if (runtime.turnState !== "idle") {
|
|
10967
|
+
const err = `TURN_IN_PROGRESS: Persistent runtime is busy (turnState=${runtime.turnState})`;
|
|
10968
|
+
console.warn(`[AgentManager] EP1360: ${err}, session=${sessionId}`);
|
|
10969
|
+
onError(err);
|
|
10970
|
+
return { success: false, error: err };
|
|
10971
|
+
}
|
|
10972
|
+
if (runtime.pid) {
|
|
10973
|
+
session.pid = runtime.pid;
|
|
10974
|
+
this.writePidFile(sessionId, runtime.pid);
|
|
10975
|
+
}
|
|
10976
|
+
const pid = runtime.pid;
|
|
10977
|
+
void this.getChildRssMb(pid).then((rss) => {
|
|
10978
|
+
if (rss !== null) {
|
|
10979
|
+
console.log(`[AgentManager] EP1360: Persistent turn start RSS: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${pid}`);
|
|
10980
|
+
}
|
|
10981
|
+
});
|
|
10982
|
+
const onCompleteWithRss = (agentSid, resultMetadata) => {
|
|
10983
|
+
void this.getChildRssMb(pid).then((rss) => {
|
|
10984
|
+
if (rss !== null) {
|
|
10985
|
+
console.log(`[AgentManager] EP1360: Persistent turn complete RSS: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${pid}`);
|
|
10986
|
+
}
|
|
10987
|
+
});
|
|
10988
|
+
wrapRuntimeOnComplete(agentSid, resultMetadata);
|
|
10989
|
+
};
|
|
10990
|
+
await runtime.sendMessage(runtimeFirstMessage, {
|
|
10991
|
+
onChunk,
|
|
10992
|
+
onToolUse,
|
|
10993
|
+
onToolResult,
|
|
10994
|
+
onCompaction: options.onCompaction,
|
|
10995
|
+
onComplete: onCompleteWithRss,
|
|
10996
|
+
onError: wrapRuntimeOnError
|
|
10997
|
+
});
|
|
10998
|
+
console.log(`[AgentManager] EP1360: Started persistent ${provider} runtime for session ${sessionId}, pid=${runtime.pid}`);
|
|
10999
|
+
return { success: true };
|
|
11000
|
+
}
|
|
11001
|
+
} catch (error) {
|
|
11002
|
+
console.warn(
|
|
11003
|
+
`[AgentManager] EP1360: Persistent runtime creation failed, falling back to spawn-per-message: ${error instanceof Error ? error.message : error}`
|
|
11004
|
+
);
|
|
11005
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
11006
|
+
this.disablePersistentForSession(sessionId, `SYNC_RUNTIME_ERROR: ${msg}`);
|
|
11007
|
+
}
|
|
11008
|
+
}
|
|
11009
|
+
const childProcess = (0, import_child_process14.spawn)(spawnCmd, spawnArgs, {
|
|
9729
11010
|
cwd: session.projectPath,
|
|
9730
11011
|
env: envVars,
|
|
9731
11012
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9736,6 +11017,14 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9736
11017
|
session.pid = childProcess.pid;
|
|
9737
11018
|
this.writePidFile(sessionId, childProcess.pid);
|
|
9738
11019
|
}
|
|
11020
|
+
let spawnChildRssMb = null;
|
|
11021
|
+
let resultChildRssMb = null;
|
|
11022
|
+
void this.getChildRssMb(childProcess.pid).then((rss) => {
|
|
11023
|
+
spawnChildRssMb = rss;
|
|
11024
|
+
if (rss !== null) {
|
|
11025
|
+
console.log(`[AgentManager] EP1360: Child RSS at spawn: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${childProcess.pid}`);
|
|
11026
|
+
}
|
|
11027
|
+
});
|
|
9739
11028
|
childProcess.stderr?.on("data", (data) => {
|
|
9740
11029
|
console.error(`[AgentManager] stderr: ${data.toString()}`);
|
|
9741
11030
|
});
|
|
@@ -9751,6 +11040,8 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9751
11040
|
let stdoutEventCount = 0;
|
|
9752
11041
|
let chunksSent = 0;
|
|
9753
11042
|
const streamStartTime = Date.now();
|
|
11043
|
+
let ttftLogged = false;
|
|
11044
|
+
let parsedLineCount = 0;
|
|
9754
11045
|
let hasContent = false;
|
|
9755
11046
|
childProcess.stdout?.on("data", (data) => {
|
|
9756
11047
|
const rawData = data.toString();
|
|
@@ -9765,6 +11056,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9765
11056
|
if (!line.trim()) continue;
|
|
9766
11057
|
try {
|
|
9767
11058
|
const parsed = JSON.parse(line);
|
|
11059
|
+
parsedLineCount++;
|
|
11060
|
+
if (!ttftLogged) {
|
|
11061
|
+
ttftLogged = true;
|
|
11062
|
+
const ttftMs = Date.now() - spawnTimestamp;
|
|
11063
|
+
console.log(`[AgentManager] EP1360: TTFT (spawn to first JSON): ${ttftMs}ms, provider=${provider}, session=${sessionId}, isFirstMessage=${isFirstMessage}`);
|
|
11064
|
+
}
|
|
9768
11065
|
if (stdoutEventCount <= 10 || chunksSent === 0) {
|
|
9769
11066
|
console.log(`[AgentManager] EP1191: Parsed event type: ${parsed.type}`);
|
|
9770
11067
|
}
|
|
@@ -9819,6 +11116,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9819
11116
|
}
|
|
9820
11117
|
break;
|
|
9821
11118
|
case "turn.completed":
|
|
11119
|
+
void this.getChildRssMb(childProcess.pid).then((rss) => {
|
|
11120
|
+
resultChildRssMb = rss;
|
|
11121
|
+
if (rss !== null) {
|
|
11122
|
+
console.log(`[AgentManager] EP1360: Child RSS at turn.completed: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${childProcess.pid}`);
|
|
11123
|
+
}
|
|
11124
|
+
});
|
|
9822
11125
|
if (parsed.usage) {
|
|
9823
11126
|
const u = parsed.usage;
|
|
9824
11127
|
const inputTokens = u.input_tokens ?? u.prompt_tokens ?? 0;
|
|
@@ -9894,6 +11197,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9894
11197
|
}
|
|
9895
11198
|
break;
|
|
9896
11199
|
case "result":
|
|
11200
|
+
void this.getChildRssMb(childProcess.pid).then((rss) => {
|
|
11201
|
+
resultChildRssMb = rss;
|
|
11202
|
+
if (rss !== null) {
|
|
11203
|
+
console.log(`[AgentManager] EP1360: Child RSS at result: ${rss}MB, provider=${provider}, session=${sessionId}, pid=${childProcess.pid}`);
|
|
11204
|
+
}
|
|
11205
|
+
});
|
|
9897
11206
|
if (parsed.session_id) {
|
|
9898
11207
|
extractedSessionId = parsed.session_id;
|
|
9899
11208
|
session.agentSessionId = extractedSessionId;
|
|
@@ -9965,6 +11274,12 @@ If changes are needed, explain what needs to be done.`;
|
|
|
9965
11274
|
});
|
|
9966
11275
|
childProcess.on("exit", (code, signal) => {
|
|
9967
11276
|
const duration = Date.now() - streamStartTime;
|
|
11277
|
+
const daemonExitRssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
11278
|
+
const daemonDeltaMb = daemonExitRssMb - daemonSpawnRssMb;
|
|
11279
|
+
console.log(`[AgentManager] EP1360: Daemon RSS at exit: ${daemonExitRssMb}MB (delta: ${daemonDeltaMb >= 0 ? "+" : ""}${daemonDeltaMb}MB from spawn ${daemonSpawnRssMb}MB), session=${sessionId}`);
|
|
11280
|
+
const childDeltaMb = spawnChildRssMb !== null && resultChildRssMb !== null ? Math.round((resultChildRssMb - spawnChildRssMb) * 10) / 10 : null;
|
|
11281
|
+
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}`);
|
|
11282
|
+
console.log(`[AgentManager] EP1360: Stream stats: parsedLines=${parsedLineCount}, rawDataEvents=${stdoutEventCount}, chunksSent=${chunksSent}, session=${sessionId}`);
|
|
9968
11283
|
console.log(`[AgentManager] EP1191: ${provider} CLI exited for session ${sessionId}: code=${code}, signal=${signal}, duration=${duration}ms, stdoutEvents=${stdoutEventCount}, chunksSent=${chunksSent}`);
|
|
9969
11284
|
this.processes.delete(sessionId);
|
|
9970
11285
|
this.removePidFile(sessionId);
|
|
@@ -10018,7 +11333,18 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10018
11333
|
console.log(`[AgentManager] Aborting session ${sessionId} with SIGINT`);
|
|
10019
11334
|
agentProcess.kill("SIGINT");
|
|
10020
11335
|
}
|
|
11336
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11337
|
+
if (runtime) {
|
|
11338
|
+
console.log(`[AgentManager] EP1360: Shutting down persistent runtime for aborted session ${sessionId}`);
|
|
11339
|
+
void runtime.shutdown();
|
|
11340
|
+
this.runtimes.delete(sessionId);
|
|
11341
|
+
}
|
|
10021
11342
|
const session = this.sessions.get(sessionId);
|
|
11343
|
+
if (session) {
|
|
11344
|
+
session.pid = void 0;
|
|
11345
|
+
}
|
|
11346
|
+
this.removePidFile(sessionId);
|
|
11347
|
+
this.persistentDisabledSessions.delete(sessionId);
|
|
10022
11348
|
if (session) {
|
|
10023
11349
|
session.status = "stopping";
|
|
10024
11350
|
}
|
|
@@ -10052,6 +11378,16 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10052
11378
|
});
|
|
10053
11379
|
});
|
|
10054
11380
|
}
|
|
11381
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11382
|
+
if (runtime) {
|
|
11383
|
+
try {
|
|
11384
|
+
await runtime.shutdown();
|
|
11385
|
+
} catch (error) {
|
|
11386
|
+
console.warn(`[AgentManager] EP1360: Error shutting down runtime for session ${sessionId}:`, error);
|
|
11387
|
+
}
|
|
11388
|
+
this.runtimes.delete(sessionId);
|
|
11389
|
+
}
|
|
11390
|
+
this.persistentDisabledSessions.delete(sessionId);
|
|
10055
11391
|
this.sessions.delete(sessionId);
|
|
10056
11392
|
this.processes.delete(sessionId);
|
|
10057
11393
|
this.removePidFile(sessionId);
|
|
@@ -10080,8 +11416,183 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10080
11416
|
* Stop all active sessions
|
|
10081
11417
|
*/
|
|
10082
11418
|
async stopAllSessions() {
|
|
11419
|
+
this.stopIdleScanner();
|
|
10083
11420
|
const sessionIds = Array.from(this.sessions.keys());
|
|
10084
11421
|
await Promise.all(sessionIds.map((id) => this.stopSession(id)));
|
|
11422
|
+
this.persistentDisabledSessions.clear();
|
|
11423
|
+
}
|
|
11424
|
+
// ---------------------------------------------------------------------------
|
|
11425
|
+
// EP1360: Persistent Runtime Lifecycle
|
|
11426
|
+
// ---------------------------------------------------------------------------
|
|
11427
|
+
/**
|
|
11428
|
+
* EP1360: Check if persistent runtimes are enabled.
|
|
11429
|
+
*/
|
|
11430
|
+
isPersistentEnabled() {
|
|
11431
|
+
return !AGENT_DISABLE_PERSISTENT;
|
|
11432
|
+
}
|
|
11433
|
+
/**
|
|
11434
|
+
* EP1360: Get the persistent runtime for a session, if one exists and is alive.
|
|
11435
|
+
*/
|
|
11436
|
+
getRuntime(sessionId) {
|
|
11437
|
+
if (this.persistentDisabledSessions.has(sessionId)) {
|
|
11438
|
+
return void 0;
|
|
11439
|
+
}
|
|
11440
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11441
|
+
if (runtime && runtime.isAlive()) {
|
|
11442
|
+
return runtime;
|
|
11443
|
+
}
|
|
11444
|
+
if (runtime && !runtime.isAlive()) {
|
|
11445
|
+
this.runtimes.delete(sessionId);
|
|
11446
|
+
const session = this.sessions.get(sessionId);
|
|
11447
|
+
if (session) {
|
|
11448
|
+
session.pid = void 0;
|
|
11449
|
+
}
|
|
11450
|
+
this.removePidFile(sessionId);
|
|
11451
|
+
}
|
|
11452
|
+
return void 0;
|
|
11453
|
+
}
|
|
11454
|
+
/**
|
|
11455
|
+
* EP1360: Create a persistent runtime for a session.
|
|
11456
|
+
* Returns the runtime if a slot is available (possibly after evicting an idle runtime).
|
|
11457
|
+
* Returns null if all slots are occupied by active streams (degrade to spawn-per-message).
|
|
11458
|
+
*/
|
|
11459
|
+
createRuntime(sessionId, provider, options) {
|
|
11460
|
+
if (AGENT_DISABLE_PERSISTENT) {
|
|
11461
|
+
return null;
|
|
11462
|
+
}
|
|
11463
|
+
if (this.persistentDisabledSessions.has(sessionId)) {
|
|
11464
|
+
return null;
|
|
11465
|
+
}
|
|
11466
|
+
const existing = this.getRuntime(sessionId);
|
|
11467
|
+
if (existing) {
|
|
11468
|
+
return existing;
|
|
11469
|
+
}
|
|
11470
|
+
if (this.runtimes.size >= AGENT_MAX_PERSISTENT) {
|
|
11471
|
+
if (!this.evictIdleRuntime()) {
|
|
11472
|
+
console.log(`[AgentManager] EP1360: All ${AGENT_MAX_PERSISTENT} persistent slots active \u2014 degrading to spawn-per-message for session ${sessionId}`);
|
|
11473
|
+
return null;
|
|
11474
|
+
}
|
|
11475
|
+
}
|
|
11476
|
+
let runtime;
|
|
11477
|
+
if (provider === "claude") {
|
|
11478
|
+
const claudeRuntime = new ClaudePersistentRuntime(options);
|
|
11479
|
+
claudeRuntime.spawn();
|
|
11480
|
+
runtime = claudeRuntime;
|
|
11481
|
+
} else {
|
|
11482
|
+
const codexRuntime = new CodexPersistentRuntime(options);
|
|
11483
|
+
codexRuntime.spawn();
|
|
11484
|
+
runtime = codexRuntime;
|
|
11485
|
+
}
|
|
11486
|
+
this.runtimes.set(sessionId, runtime);
|
|
11487
|
+
console.log(`[AgentManager] EP1360: Created persistent ${provider} runtime for session ${sessionId} (${this.runtimes.size}/${AGENT_MAX_PERSISTENT} slots used)`);
|
|
11488
|
+
return runtime;
|
|
11489
|
+
}
|
|
11490
|
+
/**
|
|
11491
|
+
* EP1360: Shut down a persistent runtime for a session.
|
|
11492
|
+
*/
|
|
11493
|
+
async shutdownRuntime(sessionId) {
|
|
11494
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11495
|
+
if (!runtime) return;
|
|
11496
|
+
try {
|
|
11497
|
+
await runtime.shutdown();
|
|
11498
|
+
} catch (error) {
|
|
11499
|
+
console.warn(`[AgentManager] EP1360: Error shutting down runtime for session ${sessionId}:`, error);
|
|
11500
|
+
}
|
|
11501
|
+
this.runtimes.delete(sessionId);
|
|
11502
|
+
const session = this.sessions.get(sessionId);
|
|
11503
|
+
if (session) {
|
|
11504
|
+
session.pid = void 0;
|
|
11505
|
+
}
|
|
11506
|
+
this.removePidFile(sessionId);
|
|
11507
|
+
}
|
|
11508
|
+
/**
|
|
11509
|
+
* EP1360: Permanently disable persistent mode for a session (remainder of session).
|
|
11510
|
+
* Minimal "auto-downgrade" mechanism: once the runtime shows runtime-level failure
|
|
11511
|
+
* signals (timeouts/crash/spawn), future messages use spawn-per-message.
|
|
11512
|
+
*/
|
|
11513
|
+
disablePersistentForSession(sessionId, reason) {
|
|
11514
|
+
if (this.persistentDisabledSessions.has(sessionId)) return;
|
|
11515
|
+
this.persistentDisabledSessions.set(sessionId, { reason, at: Date.now() });
|
|
11516
|
+
console.warn(`[AgentManager] EP1360: Disabling persistent mode for session ${sessionId} (downgrade to spawn-per-message): ${reason}`);
|
|
11517
|
+
const runtime = this.runtimes.get(sessionId);
|
|
11518
|
+
if (runtime) {
|
|
11519
|
+
void runtime.shutdown().catch((err) => {
|
|
11520
|
+
console.warn(`[AgentManager] EP1360: Error shutting down downgraded runtime for session ${sessionId}:`, err);
|
|
11521
|
+
});
|
|
11522
|
+
this.runtimes.delete(sessionId);
|
|
11523
|
+
}
|
|
11524
|
+
const session = this.sessions.get(sessionId);
|
|
11525
|
+
if (session) {
|
|
11526
|
+
session.pid = void 0;
|
|
11527
|
+
}
|
|
11528
|
+
this.removePidFile(sessionId);
|
|
11529
|
+
}
|
|
11530
|
+
/**
|
|
11531
|
+
* EP1360: Evict the least-recently-used idle runtime to free a slot.
|
|
11532
|
+
* Returns true if a slot was freed, false if all runtimes are actively streaming.
|
|
11533
|
+
*/
|
|
11534
|
+
evictIdleRuntime() {
|
|
11535
|
+
let oldestIdleSessionId = null;
|
|
11536
|
+
let oldestActivityTime = Infinity;
|
|
11537
|
+
for (const [sid, runtime2] of this.runtimes) {
|
|
11538
|
+
if (runtime2.turnState === "idle") {
|
|
11539
|
+
const session2 = this.sessions.get(sid);
|
|
11540
|
+
const activityTime = session2?.lastActivityAt?.getTime() ?? 0;
|
|
11541
|
+
if (activityTime < oldestActivityTime) {
|
|
11542
|
+
oldestActivityTime = activityTime;
|
|
11543
|
+
oldestIdleSessionId = sid;
|
|
11544
|
+
}
|
|
11545
|
+
}
|
|
11546
|
+
}
|
|
11547
|
+
if (!oldestIdleSessionId) {
|
|
11548
|
+
return false;
|
|
11549
|
+
}
|
|
11550
|
+
console.log(`[AgentManager] EP1360: Evicting idle runtime for session ${oldestIdleSessionId} (LRU)`);
|
|
11551
|
+
const runtime = this.runtimes.get(oldestIdleSessionId);
|
|
11552
|
+
if (runtime) {
|
|
11553
|
+
void runtime.shutdown().catch((err) => {
|
|
11554
|
+
console.warn(`[AgentManager] EP1360: Error during eviction shutdown:`, err);
|
|
11555
|
+
});
|
|
11556
|
+
}
|
|
11557
|
+
this.runtimes.delete(oldestIdleSessionId);
|
|
11558
|
+
const session = this.sessions.get(oldestIdleSessionId);
|
|
11559
|
+
if (session) {
|
|
11560
|
+
session.pid = void 0;
|
|
11561
|
+
}
|
|
11562
|
+
this.removePidFile(oldestIdleSessionId);
|
|
11563
|
+
return true;
|
|
11564
|
+
}
|
|
11565
|
+
/**
|
|
11566
|
+
* EP1360: Start the idle runtime scanner.
|
|
11567
|
+
* Runs every 60s and shuts down runtimes that have been idle longer than AGENT_IDLE_TIMEOUT_MS.
|
|
11568
|
+
*/
|
|
11569
|
+
startIdleScanner() {
|
|
11570
|
+
if (this.idleScanTimer) return;
|
|
11571
|
+
this.idleScanTimer = setInterval(() => {
|
|
11572
|
+
const now = Date.now();
|
|
11573
|
+
for (const [sid, runtime] of this.runtimes) {
|
|
11574
|
+
if (runtime.turnState !== "idle") continue;
|
|
11575
|
+
const session = this.sessions.get(sid);
|
|
11576
|
+
const lastActivity = session?.lastActivityAt?.getTime() ?? 0;
|
|
11577
|
+
const idleMs = now - lastActivity;
|
|
11578
|
+
if (idleMs > AGENT_IDLE_TIMEOUT_MS) {
|
|
11579
|
+
console.log(`[AgentManager] EP1360: Runtime for session ${sid} idle for ${Math.round(idleMs / 1e3)}s \u2014 shutting down`);
|
|
11580
|
+
void this.shutdownRuntime(sid);
|
|
11581
|
+
}
|
|
11582
|
+
}
|
|
11583
|
+
}, IDLE_SCAN_INTERVAL_MS);
|
|
11584
|
+
if (this.idleScanTimer.unref) {
|
|
11585
|
+
this.idleScanTimer.unref();
|
|
11586
|
+
}
|
|
11587
|
+
}
|
|
11588
|
+
/**
|
|
11589
|
+
* EP1360: Stop the idle runtime scanner.
|
|
11590
|
+
*/
|
|
11591
|
+
stopIdleScanner() {
|
|
11592
|
+
if (this.idleScanTimer) {
|
|
11593
|
+
clearInterval(this.idleScanTimer);
|
|
11594
|
+
this.idleScanTimer = null;
|
|
11595
|
+
}
|
|
10085
11596
|
}
|
|
10086
11597
|
/**
|
|
10087
11598
|
* Get session info
|
|
@@ -10132,7 +11643,8 @@ If changes are needed, explain what needs to be done.`;
|
|
|
10132
11643
|
moduleUid: session.moduleUid,
|
|
10133
11644
|
status: reconStatus,
|
|
10134
11645
|
agentSessionId: session.agentSessionId || session.claudeSessionId,
|
|
10135
|
-
lastActivityAt: session.lastActivityAt
|
|
11646
|
+
lastActivityAt: session.lastActivityAt,
|
|
11647
|
+
persistentRuntime: this.runtimes.has(sessionId)
|
|
10136
11648
|
});
|
|
10137
11649
|
}
|
|
10138
11650
|
return result;
|
|
@@ -10403,7 +11915,7 @@ var AgentCommandQueue = class {
|
|
|
10403
11915
|
};
|
|
10404
11916
|
|
|
10405
11917
|
// src/utils/dev-server.ts
|
|
10406
|
-
var
|
|
11918
|
+
var import_child_process15 = require("child_process");
|
|
10407
11919
|
var import_core12 = __toESM(require_dist());
|
|
10408
11920
|
var fs20 = __toESM(require("fs"));
|
|
10409
11921
|
var path21 = __toESM(require("path"));
|
|
@@ -10452,7 +11964,7 @@ function writeToLog(logPath, line, isError = false) {
|
|
|
10452
11964
|
}
|
|
10453
11965
|
async function killProcessOnPort(port) {
|
|
10454
11966
|
try {
|
|
10455
|
-
const result = (0,
|
|
11967
|
+
const result = (0, import_child_process15.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
|
|
10456
11968
|
if (!result) {
|
|
10457
11969
|
console.log(`[DevServer] EP929: No process found on port ${port}`);
|
|
10458
11970
|
return true;
|
|
@@ -10461,7 +11973,7 @@ async function killProcessOnPort(port) {
|
|
|
10461
11973
|
console.log(`[DevServer] EP929: Found ${pids.length} process(es) on port ${port}: ${pids.join(", ")}`);
|
|
10462
11974
|
for (const pid of pids) {
|
|
10463
11975
|
try {
|
|
10464
|
-
(0,
|
|
11976
|
+
(0, import_child_process15.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
10465
11977
|
console.log(`[DevServer] EP929: Sent SIGTERM to PID ${pid}`);
|
|
10466
11978
|
} catch {
|
|
10467
11979
|
}
|
|
@@ -10469,8 +11981,8 @@ async function killProcessOnPort(port) {
|
|
|
10469
11981
|
await new Promise((resolve4) => setTimeout(resolve4, 1e3));
|
|
10470
11982
|
for (const pid of pids) {
|
|
10471
11983
|
try {
|
|
10472
|
-
(0,
|
|
10473
|
-
(0,
|
|
11984
|
+
(0, import_child_process15.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
11985
|
+
(0, import_child_process15.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
10474
11986
|
console.log(`[DevServer] EP929: Force killed PID ${pid}`);
|
|
10475
11987
|
} catch {
|
|
10476
11988
|
}
|
|
@@ -10521,7 +12033,7 @@ function spawnDevServerProcess(projectPath, port, moduleUid, logPath, customComm
|
|
|
10521
12033
|
if (injectedCount > 0) {
|
|
10522
12034
|
console.log(`[DevServer] EP998: Injecting ${injectedCount} env vars from database`);
|
|
10523
12035
|
}
|
|
10524
|
-
const devProcess = (0,
|
|
12036
|
+
const devProcess = (0, import_child_process15.spawn)(cmd, args, {
|
|
10525
12037
|
cwd: projectPath,
|
|
10526
12038
|
env: mergedEnv,
|
|
10527
12039
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -10884,7 +12396,7 @@ function getInstallCommand2(cwd) {
|
|
|
10884
12396
|
}
|
|
10885
12397
|
|
|
10886
12398
|
// src/daemon/daemon-process.ts
|
|
10887
|
-
var
|
|
12399
|
+
var import_child_process16 = require("child_process");
|
|
10888
12400
|
var fs23 = __toESM(require("fs"));
|
|
10889
12401
|
var http2 = __toESM(require("http"));
|
|
10890
12402
|
var os9 = __toESM(require("os"));
|
|
@@ -11000,6 +12512,7 @@ async function fetchEnvVars2(projectId) {
|
|
|
11000
12512
|
}
|
|
11001
12513
|
}
|
|
11002
12514
|
var Daemon = class _Daemon {
|
|
12515
|
+
// sessionId -> last seq
|
|
11003
12516
|
constructor() {
|
|
11004
12517
|
this.machineId = "";
|
|
11005
12518
|
this.machineUuid = null;
|
|
@@ -11059,6 +12572,8 @@ var Daemon = class _Daemon {
|
|
|
11059
12572
|
// EP1324: Retry limiting for failed update attempts
|
|
11060
12573
|
this.lastFailedUpdateVersion = null;
|
|
11061
12574
|
this.updateFailedAttempts = 0;
|
|
12575
|
+
// EP1360: Per-session monotonic event seq for daemon→platform stream gap detection.
|
|
12576
|
+
this.agentEventSeq = /* @__PURE__ */ new Map();
|
|
11062
12577
|
this.ipcServer = new IPCServer();
|
|
11063
12578
|
}
|
|
11064
12579
|
static {
|
|
@@ -11098,6 +12613,11 @@ var Daemon = class _Daemon {
|
|
|
11098
12613
|
static {
|
|
11099
12614
|
this.MAX_UPDATE_ATTEMPTS = 3;
|
|
11100
12615
|
}
|
|
12616
|
+
nextAgentSeq(sessionId) {
|
|
12617
|
+
const next = (this.agentEventSeq.get(sessionId) ?? 0) + 1;
|
|
12618
|
+
this.agentEventSeq.set(sessionId, next);
|
|
12619
|
+
return next;
|
|
12620
|
+
}
|
|
11101
12621
|
/**
|
|
11102
12622
|
* Start the daemon
|
|
11103
12623
|
*/
|
|
@@ -11131,6 +12651,13 @@ var Daemon = class _Daemon {
|
|
|
11131
12651
|
orphanWorktreeCleanup: modeConfig.orphanWorktreeCleanup,
|
|
11132
12652
|
maxConcurrentModules: modeConfig.maxConcurrentModules
|
|
11133
12653
|
});
|
|
12654
|
+
if (!process.env.EPISODA_CONTAINER_ID) {
|
|
12655
|
+
if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) {
|
|
12656
|
+
console.log("[Daemon] EP1365: GITHUB_PERSONAL_ACCESS_TOKEN is set in environment");
|
|
12657
|
+
} else {
|
|
12658
|
+
console.warn("[Daemon] EP1365: GITHUB_PERSONAL_ACCESS_TOKEN not set - GitHub MCP tools in local dev may fail");
|
|
12659
|
+
}
|
|
12660
|
+
}
|
|
11134
12661
|
this.checkAndNotifyUpdates();
|
|
11135
12662
|
if (process.env.EPISODA_CLI_PIN_VERSION) {
|
|
11136
12663
|
console.log(`[Daemon] EP1319: CLI version pinned to ${process.env.EPISODA_CLI_PIN_VERSION}, periodic updates disabled`);
|
|
@@ -11211,7 +12738,7 @@ var Daemon = class _Daemon {
|
|
|
11211
12738
|
const configDir = getConfigDir9();
|
|
11212
12739
|
const logPath = path24.join(configDir, "daemon.log");
|
|
11213
12740
|
const logFd = fs23.openSync(logPath, "a");
|
|
11214
|
-
const child = (0,
|
|
12741
|
+
const child = (0, import_child_process16.spawn)("node", [__filename], {
|
|
11215
12742
|
detached: true,
|
|
11216
12743
|
stdio: ["ignore", logFd, logFd],
|
|
11217
12744
|
env: { ...process.env, EPISODA_DAEMON_MODE: "1" }
|
|
@@ -11981,7 +13508,7 @@ var Daemon = class _Daemon {
|
|
|
11981
13508
|
await client.send({
|
|
11982
13509
|
type: "agent_result",
|
|
11983
13510
|
commandId,
|
|
11984
|
-
result: { success: true, status: "chunk", sessionId, chunk }
|
|
13511
|
+
result: { success: true, status: "chunk", sessionId, seq: this.nextAgentSeq(sessionId), chunk }
|
|
11985
13512
|
});
|
|
11986
13513
|
} catch (sendError) {
|
|
11987
13514
|
console.error(`[Daemon] EP912: Failed to send chunk (WebSocket may be disconnected):`, sendError);
|
|
@@ -11999,6 +13526,7 @@ var Daemon = class _Daemon {
|
|
|
11999
13526
|
success: true,
|
|
12000
13527
|
status: "tool_use",
|
|
12001
13528
|
sessionId,
|
|
13529
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12002
13530
|
toolUse: event
|
|
12003
13531
|
}
|
|
12004
13532
|
});
|
|
@@ -12016,6 +13544,7 @@ var Daemon = class _Daemon {
|
|
|
12016
13544
|
success: true,
|
|
12017
13545
|
status: "tool_result",
|
|
12018
13546
|
sessionId,
|
|
13547
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12019
13548
|
toolResult: event
|
|
12020
13549
|
}
|
|
12021
13550
|
});
|
|
@@ -12034,6 +13563,7 @@ var Daemon = class _Daemon {
|
|
|
12034
13563
|
success: true,
|
|
12035
13564
|
status: "compaction",
|
|
12036
13565
|
sessionId,
|
|
13566
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12037
13567
|
compaction: event
|
|
12038
13568
|
}
|
|
12039
13569
|
});
|
|
@@ -12049,7 +13579,7 @@ var Daemon = class _Daemon {
|
|
|
12049
13579
|
await client.send({
|
|
12050
13580
|
type: "agent_result",
|
|
12051
13581
|
commandId,
|
|
12052
|
-
result: { success: true, status: "complete", sessionId, claudeSessionId, resultMetadata }
|
|
13582
|
+
result: { success: true, status: "complete", sessionId, seq: this.nextAgentSeq(sessionId), claudeSessionId, resultMetadata }
|
|
12053
13583
|
});
|
|
12054
13584
|
} catch (sendError) {
|
|
12055
13585
|
console.error(`[Daemon] EP912: Failed to send complete (WebSocket may be disconnected):`, sendError);
|
|
@@ -12063,7 +13593,7 @@ var Daemon = class _Daemon {
|
|
|
12063
13593
|
await client.send({
|
|
12064
13594
|
type: "agent_result",
|
|
12065
13595
|
commandId,
|
|
12066
|
-
result: { success: false, status: "error", sessionId, error }
|
|
13596
|
+
result: { success: false, status: "error", sessionId, seq: this.nextAgentSeq(sessionId), error }
|
|
12067
13597
|
});
|
|
12068
13598
|
} catch (sendError) {
|
|
12069
13599
|
console.error(`[Daemon] EP912: Failed to send error (WebSocket may be disconnected):`, sendError);
|
|
@@ -12136,6 +13666,7 @@ var Daemon = class _Daemon {
|
|
|
12136
13666
|
success: startResult.success,
|
|
12137
13667
|
status: startResult.success ? "started" : "error",
|
|
12138
13668
|
sessionId: cmd.sessionId,
|
|
13669
|
+
seq: this.nextAgentSeq(cmd.sessionId),
|
|
12139
13670
|
error: startResult.error
|
|
12140
13671
|
};
|
|
12141
13672
|
} else if (cmd.action === "message") {
|
|
@@ -12159,14 +13690,16 @@ var Daemon = class _Daemon {
|
|
|
12159
13690
|
success: sendResult.success,
|
|
12160
13691
|
status: sendResult.success ? "started" : "error",
|
|
12161
13692
|
sessionId: cmd.sessionId,
|
|
13693
|
+
seq: this.nextAgentSeq(cmd.sessionId),
|
|
12162
13694
|
error: sendResult.error
|
|
12163
13695
|
};
|
|
12164
13696
|
} else if (cmd.action === "abort") {
|
|
12165
13697
|
await agentManager.abortSession(cmd.sessionId);
|
|
12166
|
-
result = { success: true, status: "aborted", sessionId: cmd.sessionId };
|
|
13698
|
+
result = { success: true, status: "aborted", sessionId: cmd.sessionId, seq: this.nextAgentSeq(cmd.sessionId) };
|
|
12167
13699
|
} else if (cmd.action === "stop") {
|
|
12168
13700
|
await agentManager.stopSession(cmd.sessionId);
|
|
12169
|
-
result = { success: true, status: "complete", sessionId: cmd.sessionId };
|
|
13701
|
+
result = { success: true, status: "complete", sessionId: cmd.sessionId, seq: this.nextAgentSeq(cmd.sessionId) };
|
|
13702
|
+
this.agentEventSeq.delete(cmd.sessionId);
|
|
12170
13703
|
} else {
|
|
12171
13704
|
result = {
|
|
12172
13705
|
success: false,
|
|
@@ -12192,6 +13725,7 @@ var Daemon = class _Daemon {
|
|
|
12192
13725
|
success: false,
|
|
12193
13726
|
status: "error",
|
|
12194
13727
|
sessionId,
|
|
13728
|
+
seq: this.nextAgentSeq(sessionId),
|
|
12195
13729
|
error: errorMsg
|
|
12196
13730
|
}
|
|
12197
13731
|
});
|