svamp-cli 0.2.258 → 0.2.259

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/{agentCommands-CQ89BZon.mjs → agentCommands-w72BRYhk.mjs} +6 -9
  2. package/dist/{auth-CaFsKYbx.mjs → auth-CLFNmaXL.mjs} +2 -5
  3. package/dist/cli.mjs +156 -69
  4. package/dist/{commands-DrfYv705.mjs → commands-BPxSEwAc.mjs} +3 -6
  5. package/dist/{commands-DutfM3U7.mjs → commands-BpcWHrQ7.mjs} +8 -9
  6. package/dist/{commands-D8l74hsL.mjs → commands-BtXxn3tI.mjs} +2 -5
  7. package/dist/{commands-B6eGMF5H.mjs → commands-CXiGBP6w.mjs} +2 -5
  8. package/dist/{commands--X8f4eSr.mjs → commands-D3jsQ9Sj.mjs} +8 -16
  9. package/dist/{commands-BgX0W0uK.mjs → commands-DVEsNZUB.mjs} +2 -5
  10. package/dist/{commands-Dd8xEIf0.mjs → commands-bQxTpr-M.mjs} +2 -5
  11. package/dist/{fleet-UvQnIWT1.mjs → fleet-BC9JHEWF.mjs} +3 -6
  12. package/dist/{frpc-IXxyfgPn.mjs → frpc-BCOnJSAM.mjs} +11 -10
  13. package/dist/{headlessCli-BK7WSS3D.mjs → headlessCli-fagwmUja.mjs} +3 -6
  14. package/dist/index.mjs +2 -5
  15. package/dist/{package-BdZX5mDv.mjs → package-Dn7kNPhz.mjs} +2 -2
  16. package/dist/{pinnedClaudeCode-DuLXaoGP.mjs → pinnedClaudeCode-B9O-hKxm.mjs} +1 -1
  17. package/dist/{rpc-CyACkgY1.mjs → rpc-D-1o_gWF.mjs} +2 -5
  18. package/dist/{rpc-D7DB3XCH.mjs → rpc-Dko-Fkk6.mjs} +2 -5
  19. package/dist/{run-C1ufZjIW.mjs → run-BWBtm0gY.mjs} +1 -5
  20. package/dist/{run-C1Unk4Zx.mjs → run-CT8Leg3-.mjs} +729 -425
  21. package/dist/{scheduler-7wve0Fy3.mjs → scheduler-CN8jChyQ.mjs} +2 -5
  22. package/dist/{serveCommands-iQJJLsMJ.mjs → serveCommands-sWtLnJRW.mjs} +5 -5
  23. package/dist/{serveManager-BszV0oIl.mjs → serveManager-BuXk1_SH.mjs} +122 -7
  24. package/dist/{sideband-DMPZ-ZoR.mjs → sideband-ByYvGtj3.mjs} +2 -5
  25. package/dist/{staticFileServer-FCEOLIYH.mjs → staticFileServer-CbYnj2bH.mjs} +21 -5
  26. package/package.json +2 -2
@@ -13,10 +13,7 @@ import { extname, resolve, join, sep, basename, dirname } from 'node:path';
13
13
  import { EventEmitter } from 'node:events';
14
14
  import os, { homedir, platform } from 'node:os';
15
15
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
16
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
17
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
18
- import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
19
- import { z } from 'zod';
16
+ import { createInterface } from 'node:readline';
20
17
  import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1 } from 'node:fs/promises';
21
18
  import { promisify as promisify$1 } from 'node:util';
22
19
  import { parse, stringify } from 'yaml';
@@ -1670,7 +1667,7 @@ function resolveSender(channel, input = {}) {
1670
1667
  const wsLc = hyphaWorkspace?.toLowerCase();
1671
1668
  if (id.hypha_allow.includes("*") || allowLc.includes(userLc) || wsLc && allowLc.includes(wsLc))
1672
1669
  return { sender: { name: hyphaUser, kind: "agent", verified: true } };
1673
- return { error: "caller not in hypha_allow" };
1670
+ if (id.mode !== "per-key") return { error: "caller not in hypha_allow" };
1674
1671
  }
1675
1672
  if (id.mode === "fixed") {
1676
1673
  if (!id.fixed?.name) return { error: "fixed identity not configured" };
@@ -1727,6 +1724,11 @@ class ChannelOutbox {
1727
1724
  seqByChannel = /* @__PURE__ */ new Map();
1728
1725
  emitter = new EventEmitter();
1729
1726
  appendsSinceCompact = 0;
1727
+ // #0362: change-detection for reload() — a long-poll receiver reuses ONE instance and only
1728
+ // re-reads the on-disk log when it actually changed (mtime+size), instead of constructing a
1729
+ // fresh ChannelOutbox (full sync readFileSync) every 500ms poll tick.
1730
+ _lastMtimeMs = 0;
1731
+ _lastSize = -1;
1730
1732
  constructor(projectDir) {
1731
1733
  const dir = join(projectDir, ".svamp", "channels");
1732
1734
  this.file = join(dir, "_outbox.jsonl");
@@ -1737,6 +1739,29 @@ class ChannelOutbox {
1737
1739
  }
1738
1740
  this._load();
1739
1741
  }
1742
+ /** Current on-disk (mtimeMs, size), or (0, -1) if the file does not exist. */
1743
+ _stat() {
1744
+ try {
1745
+ const st = statSync(this.file);
1746
+ return { mtimeMs: st.mtimeMs, size: st.size };
1747
+ } catch {
1748
+ return { mtimeMs: 0, size: -1 };
1749
+ }
1750
+ }
1751
+ /**
1752
+ * Re-read the on-disk log ONLY if it changed since the last load (mtime+size), so a
1753
+ * cross-process append (another session/daemon replying) is still picked up. Returns true
1754
+ * if a reload happened. Cheap no-op when nothing changed — the fix for the receive() hot
1755
+ * loop that re-instantiated + fully re-read the outbox every poll tick (#0362).
1756
+ */
1757
+ reload() {
1758
+ const { mtimeMs, size } = this._stat();
1759
+ if (mtimeMs === this._lastMtimeMs && size === this._lastSize) return false;
1760
+ this.byChannel.clear();
1761
+ this.seqByChannel.clear();
1762
+ this._load();
1763
+ return true;
1764
+ }
1740
1765
  _load() {
1741
1766
  if (!existsSync(this.file)) return;
1742
1767
  const cutoff = Date.now() - TTL_MS;
@@ -1764,6 +1789,9 @@ class ChannelOutbox {
1764
1789
  let kept = 0;
1765
1790
  for (const arr of this.byChannel.values()) kept += arr.length;
1766
1791
  if (rawLines > kept) this._compact();
1792
+ const s = this._stat();
1793
+ this._lastMtimeMs = s.mtimeMs;
1794
+ this._lastSize = s.size;
1767
1795
  }
1768
1796
  /** Rewrite _outbox.jsonl to the current in-memory (TTL/MAX-bounded) window. Best-effort. */
1769
1797
  _compact() {
@@ -2985,7 +3013,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2985
3013
  const tunnels = handlers.tunnels;
2986
3014
  if (!tunnels) throw new Error("Tunnel management not available");
2987
3015
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2988
- const { FrpcTunnel } = await import('./frpc-IXxyfgPn.mjs');
3016
+ const { FrpcTunnel } = await import('./frpc-BCOnJSAM.mjs');
2989
3017
  const tunnel = new FrpcTunnel({
2990
3018
  name: params.name,
2991
3019
  ports: params.ports,
@@ -3452,7 +3480,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3452
3480
  }
3453
3481
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
3454
3482
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
3455
- const { toolsForRole } = await import('./sideband-DMPZ-ZoR.mjs');
3483
+ const { toolsForRole } = await import('./sideband-ByYvGtj3.mjs');
3456
3484
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
3457
3485
  return fmt(r2);
3458
3486
  }
@@ -3551,7 +3579,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3551
3579
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3552
3580
  const callId = "call_" + Math.random().toString(16).slice(2, 12);
3553
3581
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
3554
- const { queryCore } = await import('./commands-D8l74hsL.mjs');
3582
+ const { queryCore } = await import('./commands-BtXxn3tI.mjs');
3555
3583
  const timeout = c.reply?.timeout_sec || 120;
3556
3584
  let result;
3557
3585
  try {
@@ -3647,14 +3675,15 @@ ${d?.error || "not found"}`;
3647
3675
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3648
3676
  const cursor = Math.max(0, Number(kwargs.cursor) || 0);
3649
3677
  const waitMs = Math.min(Math.max(0, Number(kwargs.wait ?? 25) * 1e3), 6e4);
3678
+ const outbox = new ChannelOutbox(dir);
3650
3679
  const deadline = Date.now() + waitMs;
3651
3680
  for (; ; ) {
3652
- const outbox = new ChannelOutbox(dir);
3653
3681
  const replies = outbox.since(c.id, cursor, r.sender.name, kwargs.correlationId);
3654
3682
  if (replies.length || Date.now() >= deadline) {
3655
3683
  return { ok: true, replies, cursor: outbox.cursor(c.id) };
3656
3684
  }
3657
3685
  await new Promise((resolve) => setTimeout(resolve, 500));
3686
+ outbox.reload();
3658
3687
  }
3659
3688
  },
3660
3689
  // Safety-restricted file upload (#0093). Deny-by-default: only channels whose
@@ -3798,7 +3827,10 @@ function readAwaiting(sessionId) {
3798
3827
  function writeAwaiting(sessionId, map) {
3799
3828
  try {
3800
3829
  mkdirSync(join(SVAMP_HOME$2, "awaiting"), { recursive: true });
3801
- writeFileSync(awaitPath(sessionId), JSON.stringify(map));
3830
+ const p = awaitPath(sessionId);
3831
+ const tmp = `${p}.${process.pid}.${randomUUID()}.tmp`;
3832
+ writeFileSync(tmp, JSON.stringify(map));
3833
+ renameSync(tmp, p);
3802
3834
  } catch {
3803
3835
  }
3804
3836
  }
@@ -3834,7 +3866,17 @@ function computeOutboundHop(sessionId) {
3834
3866
  return { hopCount: 1, fromInboxTurn: false };
3835
3867
  }
3836
3868
  const buckets = /* @__PURE__ */ new Map();
3869
+ const FULL_REFILL_MS = URGENT_BURST * URGENT_REFILL_MS;
3870
+ let lastBucketSweep = 0;
3871
+ function pruneIdleBuckets(now) {
3872
+ if (now - lastBucketSweep < URGENT_REFILL_MS) return;
3873
+ lastBucketSweep = now;
3874
+ for (const [k, b] of buckets) {
3875
+ if (now - b.last >= FULL_REFILL_MS) buckets.delete(k);
3876
+ }
3877
+ }
3837
3878
  function takeUrgentToken(sender, now) {
3879
+ pruneIdleBuckets(now);
3838
3880
  let b = buckets.get(sender);
3839
3881
  if (!b) {
3840
3882
  b = { tokens: URGENT_BURST, last: now };
@@ -8023,8 +8065,8 @@ var acpBackend = /*#__PURE__*/Object.freeze({
8023
8065
  const KNOWN_ACP_AGENTS = {
8024
8066
  gemini: { command: "gemini", args: ["--experimental-acp"] }
8025
8067
  };
8026
- const KNOWN_MCP_AGENTS = {
8027
- codex: { command: "codex", args: ["mcp-server"] }
8068
+ const KNOWN_CODEX_AGENTS = {
8069
+ codex: { command: "codex", args: ["app-server", "--listen", "stdio://"] }
8028
8070
  };
8029
8071
  function resolveAcpAgentConfig(cliArgs) {
8030
8072
  if (cliArgs.length === 0) {
@@ -8051,12 +8093,12 @@ function resolveAcpAgentConfig(cliArgs) {
8051
8093
  args: [...knownAcp.args, ...passthroughArgs]
8052
8094
  };
8053
8095
  }
8054
- const knownMcp = KNOWN_MCP_AGENTS[agentName];
8055
- if (knownMcp) {
8096
+ const knownCodex = KNOWN_CODEX_AGENTS[agentName];
8097
+ if (knownCodex) {
8056
8098
  return {
8057
8099
  agentName,
8058
- command: knownMcp.command,
8059
- args: [...knownMcp.args, ...cliArgs.slice(1)]
8100
+ command: knownCodex.command,
8101
+ args: [...knownCodex.args, ...cliArgs.slice(1)]
8060
8102
  };
8061
8103
  }
8062
8104
  return {
@@ -8069,7 +8111,7 @@ function resolveAcpAgentConfig(cliArgs) {
8069
8111
  var acpAgentConfig = /*#__PURE__*/Object.freeze({
8070
8112
  __proto__: null,
8071
8113
  KNOWN_ACP_AGENTS: KNOWN_ACP_AGENTS,
8072
- KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS,
8114
+ KNOWN_CODEX_AGENTS: KNOWN_CODEX_AGENTS,
8073
8115
  resolveAcpAgentConfig: resolveAcpAgentConfig
8074
8116
  });
8075
8117
 
@@ -8078,12 +8120,12 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
8078
8120
  let turnText = "";
8079
8121
  let flushTimer = null;
8080
8122
  let bridgeStopped = false;
8123
+ const pushAssistant = (content) => {
8124
+ sessionService.pushMessage({ type: "assistant", message: { role: "assistant", content } }, "agent");
8125
+ };
8081
8126
  function flushText() {
8082
8127
  if (pendingText) {
8083
- sessionService.pushMessage({
8084
- type: "assistant",
8085
- content: [{ type: "text", text: pendingText }]
8086
- }, "agent");
8128
+ pushAssistant([{ type: "text", text: pendingText }]);
8087
8129
  pendingText = "";
8088
8130
  }
8089
8131
  if (flushTimer) {
@@ -8104,10 +8146,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
8104
8146
  } else if (msg.fullText) {
8105
8147
  turnText += msg.fullText;
8106
8148
  flushText();
8107
- sessionService.pushMessage({
8108
- type: "assistant",
8109
- content: [{ type: "text", text: msg.fullText }]
8110
- }, "agent");
8149
+ pushAssistant([{ type: "text", text: msg.fullText }]);
8111
8150
  }
8112
8151
  break;
8113
8152
  }
@@ -8144,25 +8183,25 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
8144
8183
  }
8145
8184
  case "tool-call": {
8146
8185
  flushText();
8147
- sessionService.pushMessage({
8148
- type: "assistant",
8149
- content: [{
8150
- type: "tool_use",
8151
- id: msg.callId,
8152
- name: msg.toolName,
8153
- input: msg.args
8154
- }]
8155
- }, "agent");
8186
+ pushAssistant([{
8187
+ type: "tool_use",
8188
+ id: msg.callId,
8189
+ name: msg.toolName,
8190
+ input: msg.args
8191
+ }]);
8156
8192
  break;
8157
8193
  }
8158
8194
  case "tool-result": {
8159
8195
  sessionService.pushMessage({
8160
- type: "assistant",
8161
- content: [{
8162
- type: "tool_result",
8163
- tool_use_id: msg.callId,
8164
- content: typeof msg.result === "string" ? msg.result : JSON.stringify(msg.result)
8165
- }]
8196
+ type: "user",
8197
+ message: {
8198
+ role: "user",
8199
+ content: [{
8200
+ type: "tool_result",
8201
+ tool_use_id: msg.callId,
8202
+ content: typeof msg.result === "string" ? msg.result : JSON.stringify(msg.result)
8203
+ }]
8204
+ }
8166
8205
  }, "agent");
8167
8206
  break;
8168
8207
  }
@@ -8205,10 +8244,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
8205
8244
  if (msg.name === "thinking") {
8206
8245
  const payload = msg.payload;
8207
8246
  const text = payload?.text || JSON.stringify(payload);
8208
- sessionService.pushMessage({
8209
- type: "assistant",
8210
- content: [{ type: "thinking", thinking: text }]
8211
- }, "agent");
8247
+ pushAssistant([{ type: "thinking", thinking: text }]);
8212
8248
  } else {
8213
8249
  sessionService.pushMessage({
8214
8250
  type: "session_event",
@@ -8266,440 +8302,649 @@ class HyphaPermissionHandler {
8266
8302
  }
8267
8303
  }
8268
8304
 
8269
- const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
8270
- function getCodexMcpCommand() {
8305
+ function mapCodexEventToText(event) {
8306
+ if (!event || typeof event !== "object") return "";
8307
+ if (typeof event.message === "string") return event.message;
8308
+ if (typeof event.text === "string") return event.text;
8309
+ const content = event.content;
8310
+ if (Array.isArray(content)) {
8311
+ return content.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join("");
8312
+ }
8313
+ const delta = event.delta;
8314
+ if (delta && typeof delta === "object" && typeof delta.text === "string") return delta.text;
8315
+ if (typeof delta === "string") return delta;
8316
+ return "";
8317
+ }
8318
+ function normalizeNotification(method, params) {
8319
+ if (method === "item/agentMessage/delta") {
8320
+ return typeof params?.delta === "string" ? { type: "agent_message_delta", delta: params.delta } : null;
8321
+ }
8322
+ if (method === "item/reasoning/delta") {
8323
+ return typeof params?.delta === "string" ? { type: "agent_reasoning_delta", delta: params.delta } : null;
8324
+ }
8325
+ if (method === "item/started" || method === "item/completed" || method === "item/updated") {
8326
+ const item = params?.item;
8327
+ if (!item || typeof item !== "object") return null;
8328
+ const done = method === "item/completed";
8329
+ switch (item.type) {
8330
+ case "agentMessage":
8331
+ return done && typeof item.text === "string" && item.text ? { type: "agent_message", message: item.text } : null;
8332
+ case "reasoning": {
8333
+ if (!done) return null;
8334
+ const text = Array.isArray(item.content) ? item.content.join("") : Array.isArray(item.summary) ? item.summary.join("") : typeof item.text === "string" ? item.text : "";
8335
+ return text ? { type: "agent_reasoning", text } : null;
8336
+ }
8337
+ case "commandExecution":
8338
+ return done ? { type: "exec_command_end", call_id: item.id, exit_code: item.exitCode, stdout: item.aggregatedOutput ?? "", stderr: "" } : { type: "exec_command_begin", call_id: item.id, command: item.command, cwd: item.cwd };
8339
+ case "fileChange":
8340
+ return done ? { type: "patch_apply_end", call_id: item.id, applied: item.status === "completed" || item.status === "applied" } : { type: "patch_apply_begin", call_id: item.id, changes: item.changes };
8341
+ case "mcpToolCall":
8342
+ return done ? { type: "mcp_tool_call_end", call_id: item.id, tool: item.tool, result: item.result, error: item.error } : { type: "mcp_tool_call_begin", call_id: item.id, tool: item.tool, server: item.server, arguments: item.arguments };
8343
+ case "userMessage":
8344
+ default:
8345
+ return null;
8346
+ }
8347
+ }
8348
+ if (method === "thread/tokenUsage/updated") {
8349
+ return { type: "token_usage", usage: params?.usage ?? params };
8350
+ }
8351
+ return null;
8352
+ }
8353
+
8354
+ function parseCodexVersion(v) {
8355
+ const m = v.match(/codex-cli\s+(\d+)\.(\d+)\.(\d+)/);
8356
+ if (!m) return null;
8357
+ const [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])];
8358
+ if (![major, minor, patch].every(Number.isFinite)) return null;
8359
+ return { major, minor, patch };
8360
+ }
8361
+ function codexAppServerAvailable() {
8271
8362
  try {
8272
- const version = execSync("codex --version", { encoding: "utf8" }).trim();
8273
- const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/);
8274
- if (!match) return "mcp-server";
8275
- const versionStr = match[1];
8276
- const [major, minor, patch] = versionStr.split(/[-.]/).map(Number);
8277
- if (major > 0 || minor > 43) return "mcp-server";
8278
- if (minor === 43 && patch === 0) {
8279
- if (versionStr.includes("-alpha.")) {
8280
- const alphaNum = parseInt(versionStr.split("-alpha.")[1]);
8281
- return alphaNum >= 5 ? "mcp-server" : "mcp";
8282
- }
8283
- return "mcp-server";
8284
- }
8285
- return "mcp";
8363
+ const v = parseCodexVersion(execSync("codex --version", { encoding: "utf8" }).trim());
8364
+ if (!v) return false;
8365
+ return v.major > 0 || v.minor >= 100;
8286
8366
  } catch {
8287
8367
  return null;
8288
8368
  }
8289
8369
  }
8290
- class CodexMcpBackend {
8291
- listeners = [];
8292
- client;
8293
- transport = null;
8294
- disposed = false;
8295
- turnCancelled = false;
8296
- // set by cancel() to suppress 'idle' in sendPrompt() finally
8297
- turnId = 0;
8298
- // monotonically increasing; each sendPrompt() captures its own ID
8299
- codexSessionId = null;
8300
- conversationId = null;
8301
- svampSessionId = null;
8302
- log;
8303
- options;
8370
+ class CodexAppServerClient {
8371
+ constructor(opts) {
8372
+ this.opts = opts;
8373
+ this.log = opts.log ?? (() => {
8374
+ });
8375
+ }
8376
+ process = null;
8377
+ readline = null;
8378
+ nextId = 1;
8379
+ pending = /* @__PURE__ */ new Map();
8304
8380
  connected = false;
8305
- // Pending elicitation approvals
8306
- pendingApprovals = /* @__PURE__ */ new Map();
8307
- // Temp files from isolation wrapping (cleaned up on disconnect)
8308
- _isolationCleanupFiles = [];
8309
- constructor(options) {
8310
- this.options = options;
8311
- this.log = options.log || (() => {
8381
+ _threadId = null;
8382
+ _turnId = null;
8383
+ // A turn is "in flight" from turn/start until task_complete / turn_aborted (or turn/completed).
8384
+ pendingTurn = null;
8385
+ completedTurnIds = /* @__PURE__ */ new Set();
8386
+ log;
8387
+ get threadId() {
8388
+ return this._threadId;
8389
+ }
8390
+ get isConnected() {
8391
+ return this.connected;
8392
+ }
8393
+ // ── lifecycle ────────────────────────────────────────────────────────────
8394
+ async start() {
8395
+ if (this.connected) return;
8396
+ let command = "codex";
8397
+ let args = ["app-server", "--listen", "stdio://", ...this.opts.extraArgs ?? []];
8398
+ if (this.opts.isolationConfig) {
8399
+ const wrapped = wrapWithIsolation(command, args, this.opts.isolationConfig);
8400
+ command = wrapped.command;
8401
+ args = wrapped.args;
8402
+ }
8403
+ const child = spawn(command, args, {
8404
+ cwd: this.opts.cwd,
8405
+ // RUST_LOG mutes rollout noise on stderr; keep the rest of env for auth (~/.codex or key).
8406
+ env: { RUST_LOG: "error", ...process.env, ...this.opts.env ?? {} },
8407
+ stdio: ["pipe", "pipe", "pipe"]
8312
8408
  });
8313
- this.client = new Client(
8314
- { name: "svamp-codex-client", version: "1.0.0" },
8315
- { capabilities: { elicitation: {} } }
8316
- );
8317
- this.client.setNotificationHandler(z.object({
8318
- method: z.literal("codex/event"),
8319
- params: z.object({ msg: z.any() })
8320
- }).passthrough(), (data) => {
8321
- const msg = data.params.msg;
8322
- this.updateIdentifiersFromEvent(msg);
8323
- this.handleCodexEvent(msg);
8409
+ this.process = child;
8410
+ child.stderr?.on("data", (d) => this.log("[codex-app-server:stderr]", d.toString().trim()));
8411
+ child.on("exit", (code) => {
8412
+ const wasConnected = this.connected;
8413
+ this.connected = false;
8414
+ this.failAllPending(new Error(`codex app-server exited (code ${code})`));
8415
+ this.resolvePendingTurn(true);
8416
+ if (wasConnected) this.opts.onExit?.(code);
8417
+ });
8418
+ child.on("error", (err) => {
8419
+ this.log("[codex-app-server] spawn error", err);
8420
+ });
8421
+ this.readline = createInterface({ input: child.stdout });
8422
+ this.readline.on("line", (line) => this.onLine(line));
8423
+ const initParams = {
8424
+ clientInfo: { name: "svamp-codex", title: "Svamp Codex Client", version: "2.0.0" },
8425
+ capabilities: { experimentalApi: true }
8426
+ };
8427
+ await this.request("initialize", initParams);
8428
+ this.notify("initialized");
8429
+ this.connected = true;
8430
+ this.log("[codex-app-server] connected + initialized");
8431
+ }
8432
+ async startThread(o) {
8433
+ const params = {
8434
+ model: o.model ?? null,
8435
+ modelProvider: null,
8436
+ profile: null,
8437
+ cwd: o.cwd ?? this.opts.cwd,
8438
+ approvalPolicy: o.approvalPolicy ?? null,
8439
+ sandbox: o.sandbox ?? null,
8440
+ config: null,
8441
+ baseInstructions: null,
8442
+ developerInstructions: null,
8443
+ compactPrompt: null,
8444
+ includeApplyPatchTool: null,
8445
+ experimentalRawEvents: false,
8446
+ persistExtendedHistory: true
8447
+ };
8448
+ const r = await this.request("thread/start", params);
8449
+ this._threadId = r.thread.id;
8450
+ this._turnId = null;
8451
+ this.log("[codex-app-server] thread started", this._threadId);
8452
+ return { threadId: r.thread.id, model: r.model };
8453
+ }
8454
+ async resumeThread(o) {
8455
+ const params = {
8456
+ threadId: o.threadId,
8457
+ model: o.model ?? null,
8458
+ modelProvider: null,
8459
+ cwd: o.cwd ?? this.opts.cwd,
8460
+ approvalPolicy: o.approvalPolicy ?? null,
8461
+ sandbox: o.sandbox ?? null,
8462
+ config: null,
8463
+ baseInstructions: null,
8464
+ developerInstructions: null,
8465
+ persistExtendedHistory: true
8466
+ };
8467
+ const r = await this.request("thread/resume", params);
8468
+ this._threadId = r.thread.id;
8469
+ this._turnId = null;
8470
+ this.log("[codex-app-server] thread resumed", this._threadId);
8471
+ return { threadId: r.thread.id, model: r.model };
8472
+ }
8473
+ /** Build the turn/start params (exported shape kept minimal; only set fields are sent). */
8474
+ buildTurnParams(prompt, o) {
8475
+ const params = { threadId: this._threadId, input: [{ type: "text", text: prompt }] };
8476
+ if (o?.approvalPolicy) params.approvalPolicy = o.approvalPolicy;
8477
+ if (o?.model) params.model = o.model;
8478
+ if (o?.effort) params.effort = o.effort;
8479
+ if (o?.sandbox) {
8480
+ params.sandboxPolicy = { type: o.sandbox === "workspace-write" ? "workspaceWrite" : o.sandbox === "danger-full-access" ? "dangerFullAccess" : "readOnly" };
8481
+ }
8482
+ return params;
8483
+ }
8484
+ /** Start a turn; resolves immediately (completion is tracked via events / waitForTurn). */
8485
+ async startTurn(prompt, o) {
8486
+ if (!this._threadId) throw new Error("No active codex thread \u2014 call startThread first");
8487
+ const result = await this.request("turn/start", this.buildTurnParams(prompt, o));
8488
+ const id = result?.turn?.id;
8489
+ if (typeof id === "string" && id) this._turnId = id;
8490
+ }
8491
+ /** Send a turn and await completion (resolves {aborted} on task_complete/turn_aborted/timeout). */
8492
+ async sendTurnAndWait(prompt, o) {
8493
+ const timeoutMs = o?.timeoutMs ?? 14 * 24 * 60 * 60 * 1e3;
8494
+ let timer = null;
8495
+ const completion = new Promise((resolve) => {
8496
+ this.pendingTurn = { resolve };
8497
+ timer = setTimeout(() => {
8498
+ this.log("[codex-app-server] turn timeout");
8499
+ this.resolvePendingTurn(true);
8500
+ }, timeoutMs);
8501
+ timer?.unref?.();
8324
8502
  });
8503
+ try {
8504
+ await this.startTurn(prompt, o);
8505
+ } catch (e) {
8506
+ if (timer) clearTimeout(timer);
8507
+ this.pendingTurn = null;
8508
+ throw e;
8509
+ }
8510
+ const aborted = await completion;
8511
+ if (timer) clearTimeout(timer);
8512
+ return { aborted };
8325
8513
  }
8326
- // ── AgentBackend interface ──────────────────────────────────────────
8327
- onMessage(handler) {
8328
- this.listeners.push(handler);
8514
+ /** Interrupt the in-flight turn (real server-side abort — the old MCP path couldn't do this). */
8515
+ async interrupt() {
8516
+ if (!this._threadId || !this._turnId) return;
8517
+ const params = { threadId: this._threadId, turnId: this._turnId };
8518
+ try {
8519
+ await this.request("turn/interrupt", params, 1e4);
8520
+ } catch (e) {
8521
+ this.log("[codex-app-server] interrupt err (may be benign)", e);
8522
+ }
8329
8523
  }
8330
- offMessage(handler) {
8331
- const idx = this.listeners.indexOf(handler);
8332
- if (idx !== -1) this.listeners.splice(idx, 1);
8524
+ async dispose() {
8525
+ this.readline?.close();
8526
+ this.readline = null;
8527
+ const proc = this.process;
8528
+ const pid = proc?.pid;
8529
+ try {
8530
+ proc?.stdin?.end();
8531
+ proc?.kill("SIGTERM");
8532
+ } catch {
8533
+ }
8534
+ if (pid) {
8535
+ const t = setTimeout(() => {
8536
+ try {
8537
+ process.kill(pid, 0);
8538
+ process.kill(pid, "SIGKILL");
8539
+ } catch {
8540
+ }
8541
+ }, 2e3);
8542
+ t.unref?.();
8543
+ }
8544
+ this.process = null;
8545
+ this.connected = false;
8546
+ this._threadId = null;
8547
+ this._turnId = null;
8548
+ this.failAllPending(new Error("codex app-server disposed"));
8549
+ this.resolvePendingTurn(true);
8550
+ }
8551
+ // ── JSON-RPC plumbing ──────────────────────────────────────────────────────
8552
+ request(method, params, timeoutMs) {
8553
+ const id = this.nextId++;
8554
+ return new Promise((resolve, reject) => {
8555
+ this.pending.set(id, { resolve, reject, method });
8556
+ if (timeoutMs) {
8557
+ const t = setTimeout(() => {
8558
+ if (this.pending.has(id)) {
8559
+ this.pending.delete(id);
8560
+ reject(new Error(`codex ${method} timed out after ${timeoutMs}ms`));
8561
+ }
8562
+ }, timeoutMs);
8563
+ t.unref?.();
8564
+ }
8565
+ this.write({ jsonrpc: "2.0", id, method, params });
8566
+ });
8567
+ }
8568
+ notify(method, params) {
8569
+ this.write({ jsonrpc: "2.0", method, params });
8570
+ }
8571
+ respond(id, result) {
8572
+ this.write({ jsonrpc: "2.0", id, result });
8333
8573
  }
8574
+ write(obj) {
8575
+ try {
8576
+ this.process?.stdin?.write(JSON.stringify(obj) + "\n");
8577
+ } catch (e) {
8578
+ this.log("[codex-app-server] write error", e);
8579
+ }
8580
+ }
8581
+ failAllPending(err) {
8582
+ for (const [id, p] of this.pending) {
8583
+ p.reject(err);
8584
+ this.pending.delete(id);
8585
+ }
8586
+ }
8587
+ onLine(line) {
8588
+ const trimmed = line.trim();
8589
+ if (!trimmed) return;
8590
+ let msg;
8591
+ try {
8592
+ msg = JSON.parse(trimmed);
8593
+ } catch {
8594
+ this.log("[codex-app-server] non-JSON line", trimmed.slice(0, 200));
8595
+ return;
8596
+ }
8597
+ if (msg.id !== void 0 && msg.method === void 0) {
8598
+ const p = this.pending.get(msg.id);
8599
+ if (!p) return;
8600
+ this.pending.delete(msg.id);
8601
+ const r = msg;
8602
+ if (r.error) p.reject(new Error(`codex ${p.method}: ${r.error.message}`));
8603
+ else p.resolve(r.result);
8604
+ return;
8605
+ }
8606
+ if (msg.id !== void 0 && typeof msg.method === "string") {
8607
+ void this.handleServerRequest(msg.id, msg.method, msg.params);
8608
+ return;
8609
+ }
8610
+ if (typeof msg.method === "string") {
8611
+ this.handleNotification(msg.method, msg.params);
8612
+ return;
8613
+ }
8614
+ }
8615
+ // ── server→client approval requests ────────────────────────────────────────
8616
+ async handleServerRequest(id, method, params) {
8617
+ const handler = this.opts.onApproval;
8618
+ if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") {
8619
+ const callId = String(params?.itemId ?? params?.callId ?? id);
8620
+ const decision = handler ? await handler({ type: "exec", callId, command: Array.isArray(params?.command) ? params.command : params?.command != null ? [params.command] : [], cwd: params?.cwd, reason: params?.reason }).catch(() => "denied") : "denied";
8621
+ this.respond(id, { decision: mapDecisionToWire(decision) });
8622
+ return;
8623
+ }
8624
+ if (method === "item/fileChange/requestApproval" || method === "applyPatchApproval") {
8625
+ const callId = String(params?.itemId ?? params?.callId ?? id);
8626
+ const decision = handler ? await handler({ type: "patch", callId, fileChanges: params?.fileChanges, reason: params?.reason }).catch(() => "denied") : "denied";
8627
+ this.respond(id, { decision: mapDecisionToWire(decision) });
8628
+ return;
8629
+ }
8630
+ if (method === "mcpServer/elicitation/request") {
8631
+ const callId = `${params?.serverName ?? "mcp"}:${id}`;
8632
+ const decision = handler ? await handler({ type: "mcp", callId, serverName: params?.serverName, toolName: params?.serverName, message: params?.message, input: params?._meta?.tool_params ?? {} }).catch(() => "denied") : "denied";
8633
+ const wire = mapDecisionToWire(decision);
8634
+ this.respond(id, { action: wire === "acceptForSession" ? "accept" : wire });
8635
+ return;
8636
+ }
8637
+ this.respond(id, {});
8638
+ }
8639
+ // ── notifications (events + lifecycle) ──────────────────────────────────────
8640
+ handleNotification(method, params) {
8641
+ if (method === "codex/event" || method.startsWith("codex/event/")) {
8642
+ const emsg = params?.msg;
8643
+ if (!emsg) return;
8644
+ if (emsg.type === "task_started" && emsg.turn_id) this._turnId = String(emsg.turn_id);
8645
+ this.opts.onEvent?.(emsg);
8646
+ if (emsg.type === "task_complete" || emsg.type === "turn_aborted") {
8647
+ const tid = emsg.turn_id ?? emsg.turnId ?? null;
8648
+ if (tid) this.completedTurnIds.add(String(tid));
8649
+ this.resolvePendingTurn(emsg.type === "turn_aborted");
8650
+ this._turnId = null;
8651
+ }
8652
+ return;
8653
+ }
8654
+ if (method === "turn/started") {
8655
+ const tid = params?.turn?.id ?? params?.turnId;
8656
+ if (tid) this._turnId = String(tid);
8657
+ return;
8658
+ }
8659
+ if (method === "turn/completed") {
8660
+ const tid = params?.turn?.id ?? params?.turnId ?? null;
8661
+ if (tid && this.completedTurnIds.has(String(tid))) return;
8662
+ const status = params?.turn?.status ?? params?.status;
8663
+ const aborted = typeof status === "string" && /abort|cancel|error/i.test(status);
8664
+ this.resolvePendingTurn(aborted);
8665
+ this._turnId = null;
8666
+ return;
8667
+ }
8668
+ const normalized = normalizeNotification(method, params);
8669
+ if (normalized) this.opts.onEvent?.(normalized);
8670
+ }
8671
+ resolvePendingTurn(aborted) {
8672
+ const p = this.pendingTurn;
8673
+ this.pendingTurn = null;
8674
+ p?.resolve(aborted);
8675
+ }
8676
+ }
8677
+ function mapDecisionToWire(d) {
8678
+ switch (d) {
8679
+ case "approved":
8680
+ return "accept";
8681
+ case "approved_for_session":
8682
+ return "acceptForSession";
8683
+ case "denied":
8684
+ return "decline";
8685
+ case "abort":
8686
+ return "cancel";
8687
+ }
8688
+ }
8689
+
8690
+ const CODEX_PROVIDER_KEY_ENV = "SVAMP_CODEX_API_KEY";
8691
+ const PROVIDER_NAME = "svamp";
8692
+ function resolveCodexProvider(env = process.env) {
8693
+ return {
8694
+ apiBase: env.SVAMP_CODEX_API_BASE || env.LLM_API_BASE || void 0,
8695
+ apiKey: env.SVAMP_CODEX_API_KEY || env.LLM_API_KEY || void 0,
8696
+ model: env.SVAMP_CODEX_MODEL || env.LLM_MODEL || void 0
8697
+ };
8698
+ }
8699
+ function hasCustomProvider(cfg) {
8700
+ return !!(cfg.apiBase && cfg.apiKey);
8701
+ }
8702
+ function buildCodexProviderArgs(cfg) {
8703
+ if (!hasCustomProvider(cfg)) {
8704
+ return { configArgs: cfg.model ? ["-c", `model=${JSON.stringify(cfg.model)}`] : [], env: {}, model: cfg.model };
8705
+ }
8706
+ const configArgs = [
8707
+ "-c",
8708
+ `model_provider=${JSON.stringify(PROVIDER_NAME)}`,
8709
+ "-c",
8710
+ `model_providers.${PROVIDER_NAME}.name=${JSON.stringify(PROVIDER_NAME)}`,
8711
+ "-c",
8712
+ `model_providers.${PROVIDER_NAME}.base_url=${JSON.stringify(cfg.apiBase)}`,
8713
+ "-c",
8714
+ `model_providers.${PROVIDER_NAME}.env_key=${JSON.stringify(CODEX_PROVIDER_KEY_ENV)}`,
8715
+ "-c",
8716
+ `model_providers.${PROVIDER_NAME}.wire_api=${JSON.stringify("responses")}`
8717
+ ];
8718
+ if (cfg.model) configArgs.push("-c", `model=${JSON.stringify(cfg.model)}`);
8719
+ return { configArgs, env: { [CODEX_PROVIDER_KEY_ENV]: cfg.apiKey }, model: cfg.model };
8720
+ }
8721
+
8722
+ var codexProvider = /*#__PURE__*/Object.freeze({
8723
+ __proto__: null,
8724
+ CODEX_PROVIDER_KEY_ENV: CODEX_PROVIDER_KEY_ENV,
8725
+ buildCodexProviderArgs: buildCodexProviderArgs,
8726
+ hasCustomProvider: hasCustomProvider,
8727
+ resolveCodexProvider: resolveCodexProvider
8728
+ });
8729
+
8730
+ class CodexAppServerBackend {
8731
+ constructor(opts) {
8732
+ this.opts = opts;
8733
+ this.log = opts.log ?? (() => {
8734
+ });
8735
+ const provider = opts.provider ?? resolveCodexProvider();
8736
+ const { configArgs, env: providerEnv, model: providerModel } = buildCodexProviderArgs(provider);
8737
+ this.model = opts.model ?? providerModel;
8738
+ this.client = new CodexAppServerClient({
8739
+ cwd: opts.cwd,
8740
+ env: { ...opts.env ?? {}, ...providerEnv },
8741
+ extraArgs: configArgs,
8742
+ log: this.log,
8743
+ isolationConfig: opts.isolationConfig,
8744
+ onEvent: (m) => this.handleCodexEvent(m),
8745
+ onApproval: (r) => this.handleApproval(r),
8746
+ onExit: (code) => this.emit({ type: "status", status: "error", detail: `codex app-server exited (code ${code})` })
8747
+ });
8748
+ }
8749
+ listeners = [];
8750
+ client;
8751
+ sessionId = randomUUID();
8752
+ log;
8753
+ model;
8754
+ started = false;
8755
+ // Approval promises keyed by callId, resolved by respondToPermission().
8756
+ pendingApprovals = /* @__PURE__ */ new Map();
8757
+ // Whether we streamed any delta for the in-flight assistant message (to avoid a duplicate
8758
+ // fullText push on the final agent_message when the model DID stream).
8759
+ streamedThisMessage = false;
8760
+ // Tracks the in-flight turn so waitForResponseComplete() can await it.
8761
+ turnDone = null;
8762
+ /** True/false if codex supports app-server; null if codex isn't installed. */
8763
+ static available() {
8764
+ return codexAppServerAvailable();
8765
+ }
8766
+ // ── AgentBackend ───────────────────────────────────────────────────────────
8334
8767
  async startSession(initialPrompt) {
8335
- const sessionId = randomUUID();
8336
- this.svampSessionId = sessionId;
8337
8768
  this.emit({ type: "status", status: "starting" });
8338
- await this.connect();
8339
- this.emit({ type: "status", status: "idle" });
8340
- if (initialPrompt) {
8341
- this.sendPrompt(sessionId, initialPrompt).catch((err) => {
8342
- this.log(`[Codex] Error sending initial prompt: ${err.message}`);
8343
- this.emit({ type: "status", status: "error", detail: err.message });
8344
- });
8769
+ const avail = codexAppServerAvailable();
8770
+ if (avail === null) throw new Error("Codex CLI not found. Install it (`npm i -g @openai/codex`) and authenticate (`svamp daemon codex-auth`).");
8771
+ if (avail === false) throw new Error("Installed Codex is too old for app-server \u2014 upgrade to >= 0.100 (`npm i -g @openai/codex@latest`).");
8772
+ await this.client.start();
8773
+ if (this.opts.resumeThreadId) {
8774
+ await this.client.resumeThread({ threadId: this.opts.resumeThreadId, model: this.model, approvalPolicy: this.opts.approvalPolicy, sandbox: this.opts.sandbox });
8775
+ } else {
8776
+ const r = await this.client.startThread({ model: this.model, approvalPolicy: this.opts.approvalPolicy, sandbox: this.opts.sandbox });
8777
+ this.model = this.model ?? r.model;
8345
8778
  }
8346
- return { sessionId };
8779
+ this.started = true;
8780
+ this.emit({ type: "status", status: "idle" });
8781
+ if (initialPrompt) await this.sendPrompt(this.sessionId, initialPrompt);
8782
+ return { sessionId: this.sessionId };
8347
8783
  }
8348
- async sendPrompt(sessionId, prompt) {
8349
- if (!this.connected) throw new Error("Codex not connected");
8350
- this.turnCancelled = false;
8351
- const myTurnId = ++this.turnId;
8784
+ async sendPrompt(_sessionId, prompt) {
8785
+ if (!this.started) throw new Error("Codex session not started");
8352
8786
  this.emit({ type: "status", status: "running" });
8353
- let hadError = false;
8787
+ this.streamedThisMessage = false;
8788
+ let resolveTurn;
8789
+ this.turnDone = new Promise((res) => {
8790
+ resolveTurn = res;
8791
+ });
8354
8792
  try {
8355
- let response;
8356
- if (this.codexSessionId) {
8357
- response = await this.continueSession(prompt);
8358
- } else {
8359
- const config = {
8360
- prompt,
8361
- cwd: this.options.cwd,
8362
- ...this.options.model ? { model: this.options.model } : {}
8363
- };
8364
- response = await this.startCodexSession(config);
8365
- }
8366
- if (response?.content && Array.isArray(response.content)) {
8367
- for (const block of response.content) {
8368
- if (block.type === "text" && block.text) {
8369
- this.emit({ type: "model-output", fullText: block.text });
8370
- }
8371
- }
8372
- }
8793
+ await this.client.sendTurnAndWait(prompt, { model: this.model, approvalPolicy: this.opts.approvalPolicy, sandbox: this.opts.sandbox });
8373
8794
  } catch (err) {
8374
- hadError = true;
8375
- this.log(`[Codex] Error in sendPrompt: ${err.message}`);
8376
- this.emit({ type: "status", status: "error", detail: err.message });
8377
- throw err;
8795
+ this.emit({ type: "status", status: "error", detail: err instanceof Error ? err.message : String(err) });
8378
8796
  } finally {
8379
- if (!this.turnCancelled && !hadError && this.turnId === myTurnId) {
8380
- this.emit({ type: "status", status: "idle" });
8381
- }
8797
+ this.emit({ type: "status", status: "idle" });
8798
+ resolveTurn();
8799
+ this.turnDone = null;
8382
8800
  }
8383
8801
  }
8384
8802
  async cancel(_sessionId) {
8385
- this.log("[Codex] Cancel requested");
8386
- this.turnCancelled = true;
8803
+ await this.client.interrupt();
8387
8804
  this.emit({ type: "status", status: "cancelled" });
8388
8805
  }
8806
+ onMessage(handler) {
8807
+ this.listeners.push(handler);
8808
+ }
8809
+ offMessage(handler) {
8810
+ this.listeners = this.listeners.filter((h) => h !== handler);
8811
+ }
8389
8812
  async respondToPermission(requestId, approved) {
8390
- const pending = this.pendingApprovals.get(requestId);
8391
- if (pending) {
8813
+ const resolve = this.pendingApprovals.get(requestId);
8814
+ if (resolve) {
8392
8815
  this.pendingApprovals.delete(requestId);
8393
- pending.resolve({
8394
- action: approved ? "accept" : "decline",
8395
- content: { approval: approved ? "approve" : "deny" }
8396
- });
8397
- this.emit({ type: "permission-response", id: requestId, approved });
8816
+ resolve(approved ? "approved" : "denied");
8398
8817
  }
8818
+ this.emit({ type: "permission-response", id: requestId, approved });
8819
+ }
8820
+ async waitForResponseComplete(timeoutMs = 14 * 24 * 60 * 60 * 1e3) {
8821
+ if (!this.turnDone) return;
8822
+ await Promise.race([this.turnDone, new Promise((res) => {
8823
+ const t = setTimeout(res, timeoutMs);
8824
+ t.unref?.();
8825
+ })]);
8399
8826
  }
8400
8827
  async dispose() {
8401
- if (this.disposed) return;
8402
- this.disposed = true;
8403
- this.log("[Codex] Disposing backend");
8404
- for (const [, pending] of this.pendingApprovals) {
8405
- pending.resolve({ action: "decline" });
8406
- }
8828
+ for (const [, resolve] of this.pendingApprovals) resolve("denied");
8407
8829
  this.pendingApprovals.clear();
8408
- await this.disconnect();
8409
- this.listeners = [];
8830
+ await this.client.dispose();
8410
8831
  }
8411
- /** Get the transport's child process PID for tracking */
8412
- get pid() {
8413
- return this.transport?.pid ?? null;
8832
+ /** The live Codex thread id (for the daemon to persist and resume after a restart). */
8833
+ getThreadId() {
8834
+ return this.client.threadId;
8414
8835
  }
8415
- /**
8416
- * Return a process-like object for TrackedSession.childProcess.
8417
- * We expose a minimal { kill } object that closes the transport.
8418
- */
8419
- getProcess() {
8420
- if (!this.transport) return null;
8421
- const pid = this.pid;
8422
- return {
8423
- kill: (signal) => {
8424
- if (pid) {
8425
- try {
8426
- process.kill(pid, signal || "SIGTERM");
8427
- } catch {
8428
- }
8429
- }
8430
- }
8431
- };
8836
+ /** Update the model used for subsequent turns (per-turn override — no restart needed). */
8837
+ setModel(model) {
8838
+ this.model = model;
8432
8839
  }
8433
- // ── MCP connection ─────────────────────────────────────────────────
8434
- async connect() {
8435
- if (this.connected) return;
8436
- const mcpCommand = getCodexMcpCommand();
8437
- if (mcpCommand === null) {
8438
- throw new Error(
8439
- "Codex CLI not found or not executable.\n\nTo install codex:\n npm install -g @openai/codex\n"
8440
- );
8441
- }
8442
- this.log(`[Codex] Connecting via: codex ${mcpCommand}`);
8443
- const env = {};
8444
- for (const key of Object.keys(process.env)) {
8445
- const value = process.env[key];
8446
- if (typeof value === "string") env[key] = value;
8447
- }
8448
- if (this.options.env) {
8449
- Object.assign(env, this.options.env);
8450
- }
8451
- const rolloutFilter = "codex_core::rollout::list=off";
8452
- const existingRustLog = env.RUST_LOG?.trim();
8453
- if (!existingRustLog) {
8454
- env.RUST_LOG = rolloutFilter;
8455
- } else if (!existingRustLog.includes("codex_core::rollout::list=")) {
8456
- env.RUST_LOG = `${existingRustLog},${rolloutFilter}`;
8457
- }
8458
- let transportCommand = "codex";
8459
- let transportArgs = [mcpCommand];
8460
- if (this.options.isolationConfig) {
8461
- const wrapped = wrapWithIsolation(transportCommand, transportArgs, this.options.isolationConfig);
8462
- transportCommand = wrapped.command;
8463
- transportArgs = wrapped.args;
8464
- if (wrapped.env) Object.assign(env, wrapped.env);
8465
- if (wrapped.cleanupFiles) {
8466
- this._isolationCleanupFiles = wrapped.cleanupFiles;
8467
- }
8468
- this.log(`[Codex] Isolation: ${this.options.isolationConfig.method}`);
8469
- }
8470
- this.transport = new StdioClientTransport({
8471
- command: transportCommand,
8472
- args: transportArgs,
8473
- env
8474
- });
8475
- this.registerPermissionHandlers();
8476
- try {
8477
- await this.client.connect(this.transport);
8478
- this.connected = true;
8479
- this.log("[Codex] MCP connection established");
8480
- } catch (err) {
8481
- this.transport = null;
8482
- throw err;
8483
- }
8484
- }
8485
- async disconnect() {
8486
- if (!this.connected) return;
8487
- const pid = this.pid;
8488
- this.log(`[Codex] Disconnecting (pid=${pid ?? "none"})`);
8489
- try {
8490
- await this.client.close();
8491
- } catch {
8492
- try {
8493
- await this.transport?.close?.();
8494
- } catch {
8495
- }
8496
- }
8497
- if (pid) {
8840
+ // ── internal ─────────────────────────────────────────────────────────────
8841
+ emit(msg) {
8842
+ for (const h of this.listeners) {
8498
8843
  try {
8499
- process.kill(pid, 0);
8500
- try {
8501
- process.kill(pid, "SIGKILL");
8502
- } catch {
8503
- }
8504
- } catch {
8844
+ h(msg);
8845
+ } catch (e) {
8846
+ this.log("[codex-backend] listener error", e);
8505
8847
  }
8506
8848
  }
8507
- this.transport = null;
8508
- this.connected = false;
8509
- if (this._isolationCleanupFiles.length > 0) {
8510
- const { rm } = await import('node:fs/promises');
8511
- for (const f of this._isolationCleanupFiles) rm(f, { force: true }).catch(() => {
8512
- });
8513
- this._isolationCleanupFiles = [];
8514
- }
8515
8849
  }
8516
- // ── Permission handling ────────────────────────────────────────────
8517
- registerPermissionHandlers() {
8518
- this.client.setRequestHandler(
8519
- ElicitRequestSchema,
8520
- async (request) => {
8521
- const params = request.params;
8522
- const callId = params.codex_call_id || randomUUID();
8523
- this.log(`[Codex] Elicitation request: ${params.message}`);
8524
- this.emit({
8525
- type: "permission-request",
8526
- id: callId,
8527
- reason: params.codex_command ? `Execute: ${Array.isArray(params.codex_command) ? params.codex_command.join(" ") : params.codex_command}` : params.message || "Codex requires approval",
8528
- payload: {
8529
- toolName: "CodexBash",
8530
- input: {
8531
- command: params.codex_command,
8532
- cwd: params.codex_cwd
8533
- }
8534
- }
8535
- });
8536
- return new Promise((resolve) => {
8537
- this.pendingApprovals.set(callId, { resolve });
8538
- setTimeout(() => {
8539
- if (this.pendingApprovals.has(callId)) {
8540
- this.pendingApprovals.delete(callId);
8541
- resolve({ action: "decline" });
8542
- }
8543
- }, 5 * 60 * 1e3);
8544
- });
8545
- }
8546
- );
8850
+ handleApproval(req) {
8851
+ const id = req.callId || randomUUID();
8852
+ const reason = req.type === "exec" ? `Execute: ${(req.command ?? []).join(" ")}` : req.type === "patch" ? "Apply file changes" : req.toolName ? `Tool: ${req.toolName}` : "Codex requires approval";
8853
+ const payload = req.type === "exec" ? { toolName: "CodexBash", input: { command: req.command, cwd: req.cwd } } : req.type === "patch" ? { toolName: "CodexPatch", input: { fileChanges: req.fileChanges } } : { toolName: req.toolName ?? "CodexTool", input: req.input };
8854
+ this.emit({ type: "permission-request", id, reason, payload });
8855
+ return new Promise((resolve) => {
8856
+ this.pendingApprovals.set(id, resolve);
8857
+ const t = setTimeout(() => {
8858
+ if (this.pendingApprovals.delete(id)) resolve("denied");
8859
+ }, 5 * 60 * 1e3);
8860
+ t.unref?.();
8861
+ });
8547
8862
  }
8548
- // ── Codex MCP tools ────────────────────────────────────────────────
8549
- async startCodexSession(config) {
8550
- const response = await this.client.callTool({
8551
- name: "codex",
8552
- arguments: config
8553
- }, void 0, { timeout: DEFAULT_TIMEOUT });
8554
- this.extractIdentifiers(response);
8555
- return response;
8556
- }
8557
- async continueSession(prompt) {
8558
- if (!this.conversationId) {
8559
- this.conversationId = this.codexSessionId;
8560
- }
8561
- const response = await this.client.callTool({
8562
- name: "codex-reply",
8563
- arguments: {
8564
- sessionId: this.codexSessionId,
8565
- conversationId: this.conversationId,
8566
- prompt
8567
- }
8568
- }, void 0, { timeout: DEFAULT_TIMEOUT });
8569
- this.extractIdentifiers(response);
8570
- return response;
8571
- }
8572
- // ── Event handling ─────────────────────────────────────────────────
8573
8863
  handleCodexEvent(event) {
8574
- if (!event || typeof event !== "object") return;
8575
- const eventType = event.type;
8576
- this.log(`[Codex] Event: ${eventType}`);
8577
- switch (eventType) {
8864
+ const t = event.type;
8865
+ switch (t) {
8578
8866
  case "task_started":
8579
8867
  this.emit({ type: "status", status: "running" });
8580
8868
  break;
8581
8869
  case "task_complete":
8582
- break;
8583
8870
  case "turn_aborted":
8584
8871
  break;
8585
- case "agent_message": {
8586
- const content = event.content;
8587
- if (Array.isArray(content)) {
8588
- for (const block of content) {
8589
- if (block.type === "output_text" && block.text) {
8590
- this.emit({ type: "model-output", fullText: block.text });
8591
- }
8592
- }
8872
+ case "agent_message_delta": {
8873
+ const delta = event.delta ?? (typeof event.text === "string" ? event.text : "");
8874
+ if (delta) {
8875
+ this.streamedThisMessage = true;
8876
+ this.emit({ type: "model-output", textDelta: delta });
8593
8877
  }
8594
8878
  break;
8595
8879
  }
8596
- case "agent_reasoning_delta": {
8597
- const text = event.delta?.text || event.text;
8598
- if (text) {
8599
- this.emit({ type: "event", name: "thinking", payload: { text } });
8880
+ case "agent_message": {
8881
+ if (this.streamedThisMessage) {
8882
+ this.streamedThisMessage = false;
8883
+ break;
8600
8884
  }
8885
+ const text = mapCodexEventToText(event);
8886
+ if (text) this.emit({ type: "model-output", fullText: text });
8601
8887
  break;
8602
8888
  }
8889
+ case "agent_reasoning_delta":
8603
8890
  case "agent_reasoning": {
8604
- const text = event.text || (Array.isArray(event.content) ? event.content.map((c) => c.text).join("") : "");
8605
- if (text) {
8606
- this.emit({ type: "event", name: "thinking", payload: { text } });
8607
- }
8891
+ const text = event.delta?.text || event.text || (Array.isArray(event.content) ? event.content.map((c) => c?.text).join("") : "");
8892
+ if (text) this.emit({ type: "event", name: "thinking", payload: { text } });
8608
8893
  break;
8609
8894
  }
8610
8895
  case "exec_command_begin": {
8611
- const callId = event.call_id || event.callId || randomUUID();
8612
- const command = Array.isArray(event.command) ? event.command.join(" ") : String(event.command || "");
8613
- this.emit({
8614
- type: "tool-call",
8615
- toolName: "CodexBash",
8616
- callId,
8617
- args: { command, cwd: event.cwd }
8618
- });
8896
+ const callId = String(event.call_id ?? event.callId ?? randomUUID());
8897
+ const command = Array.isArray(event.command) ? event.command.join(" ") : String(event.command ?? "");
8898
+ this.emit({ type: "tool-call", toolName: "CodexBash", callId, args: { command, cwd: event.cwd } });
8619
8899
  break;
8620
8900
  }
8621
8901
  case "exec_command_end": {
8622
- const callId = event.call_id || event.callId || "";
8623
- this.emit({
8624
- type: "tool-result",
8625
- toolName: "CodexBash",
8626
- callId,
8627
- result: {
8628
- exitCode: event.exit_code ?? event.exitCode,
8629
- stdout: event.stdout || "",
8630
- stderr: event.stderr || ""
8631
- }
8632
- });
8902
+ const callId = String(event.call_id ?? event.callId ?? "");
8903
+ this.emit({ type: "tool-result", toolName: "CodexBash", callId, result: { exitCode: event.exit_code ?? event.exitCode, stdout: event.stdout ?? "", stderr: event.stderr ?? "" } });
8633
8904
  break;
8634
8905
  }
8635
8906
  case "patch_apply_begin": {
8636
- const callId = event.call_id || event.callId || randomUUID();
8637
- this.emit({
8638
- type: "tool-call",
8639
- toolName: "CodexPatch",
8640
- callId,
8641
- args: { filePath: event.file_path || event.filePath, patch: event.patch }
8642
- });
8907
+ const callId = String(event.call_id ?? event.callId ?? randomUUID());
8908
+ this.emit({ type: "tool-call", toolName: "CodexPatch", callId, args: { filePath: event.file_path ?? event.filePath, patch: event.patch } });
8643
8909
  break;
8644
8910
  }
8645
8911
  case "patch_apply_end": {
8646
- const callId = event.call_id || event.callId || "";
8647
- this.emit({
8648
- type: "tool-result",
8649
- toolName: "CodexPatch",
8650
- callId,
8651
- result: {
8652
- filePath: event.file_path || event.filePath,
8653
- applied: event.applied,
8654
- error: event.error
8655
- }
8656
- });
8912
+ const callId = String(event.call_id ?? event.callId ?? "");
8913
+ this.emit({ type: "tool-result", toolName: "CodexPatch", callId, result: { filePath: event.file_path ?? event.filePath, applied: event.applied, error: event.error } });
8657
8914
  break;
8658
8915
  }
8659
- default:
8660
- this.log(`[Codex] Unhandled event: ${eventType}`);
8916
+ case "mcp_tool_call_begin": {
8917
+ const callId = String(event.call_id ?? event.callId ?? randomUUID());
8918
+ this.emit({ type: "tool-call", toolName: String(event.tool ?? "CodexMcpTool"), callId, args: { server: event.server, arguments: event.arguments } });
8661
8919
  break;
8662
- }
8663
- }
8664
- // ── Identifier extraction ──────────────────────────────────────────
8665
- updateIdentifiersFromEvent(event) {
8666
- if (!event || typeof event !== "object") return;
8667
- const candidates = [event];
8668
- if (event.data && typeof event.data === "object") candidates.push(event.data);
8669
- for (const c of candidates) {
8670
- const sid = c.session_id ?? c.sessionId;
8671
- if (sid) this.codexSessionId = sid;
8672
- const cid = c.conversation_id ?? c.conversationId;
8673
- if (cid) this.conversationId = cid;
8674
- }
8675
- }
8676
- extractIdentifiers(response) {
8677
- const meta = response?.meta || {};
8678
- this.codexSessionId = meta.sessionId || response?.sessionId || this.codexSessionId;
8679
- this.conversationId = meta.conversationId || response?.conversationId || this.conversationId;
8680
- const content = response?.content;
8681
- if (Array.isArray(content)) {
8682
- for (const item of content) {
8683
- if (!this.codexSessionId && item?.sessionId) this.codexSessionId = item.sessionId;
8684
- if (!this.conversationId && item?.conversationId) this.conversationId = item.conversationId;
8685
8920
  }
8686
- }
8687
- }
8688
- // ── Helpers ────────────────────────────────────────────────────────
8689
- emit(msg) {
8690
- if (this.disposed) return;
8691
- for (const h of this.listeners) {
8692
- try {
8693
- h(msg);
8694
- } catch {
8921
+ case "mcp_tool_call_end": {
8922
+ const callId = String(event.call_id ?? event.callId ?? "");
8923
+ this.emit({ type: "tool-result", toolName: String(event.tool ?? "CodexMcpTool"), callId, result: { result: event.result, error: event.error } });
8924
+ break;
8925
+ }
8926
+ case "token_count":
8927
+ case "token_usage": {
8928
+ this.emit({ type: "event", name: "usage", payload: event.usage ?? event.info ?? event });
8929
+ break;
8695
8930
  }
8931
+ case "error": {
8932
+ const msg = event.message ?? event.error ?? "Codex error";
8933
+ this.emit({ type: "status", status: "error", detail: String(msg) });
8934
+ break;
8935
+ }
8936
+ default:
8937
+ if (t === "_rpc/thread/tokenUsage/updated") {
8938
+ this.emit({ type: "event", name: "usage", payload: event.params });
8939
+ }
8940
+ break;
8696
8941
  }
8697
8942
  }
8698
8943
  }
8699
8944
 
8700
- var codexMcpBackend = /*#__PURE__*/Object.freeze({
8945
+ var codexAppServerBackend = /*#__PURE__*/Object.freeze({
8701
8946
  __proto__: null,
8702
- CodexMcpBackend: CodexMcpBackend
8947
+ CodexAppServerBackend: CodexAppServerBackend
8703
8948
  });
8704
8949
 
8705
8950
  const GEMINI_TIMEOUTS = {
@@ -9676,9 +9921,11 @@ class TokenCache {
9676
9921
  const oldest = this.cache.keys().next().value;
9677
9922
  if (oldest) this.cache.delete(oldest);
9678
9923
  }
9924
+ const ttlExpiry = Date.now() + this.ttlMs;
9925
+ const jwtExpMs = parseJwtExpMs(token);
9679
9926
  this.cache.set(token, {
9680
9927
  email,
9681
- expiresAt: Date.now() + this.ttlMs
9928
+ expiresAt: jwtExpMs !== null ? Math.min(ttlExpiry, jwtExpMs) : ttlExpiry
9682
9929
  });
9683
9930
  }
9684
9931
  clear() {
@@ -9710,6 +9957,16 @@ function parseCookies(header) {
9710
9957
  }
9711
9958
  return cookies;
9712
9959
  }
9960
+ function parseJwtExpMs(token) {
9961
+ try {
9962
+ const parts = token.split(".");
9963
+ if (parts.length !== 3) return null;
9964
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
9965
+ return typeof payload.exp === "number" ? payload.exp * 1e3 : null;
9966
+ } catch {
9967
+ return null;
9968
+ }
9969
+ }
9713
9970
  function parseJwtEmail(token) {
9714
9971
  try {
9715
9972
  const parts = token.split(".");
@@ -9728,6 +9985,19 @@ function parseJwtEmail(token) {
9728
9985
  return null;
9729
9986
  }
9730
9987
  }
9988
+ function isStructurallyLiveJwt(token) {
9989
+ try {
9990
+ const parts = token.split(".");
9991
+ if (parts.length !== 3) return false;
9992
+ const payload = JSON.parse(
9993
+ Buffer.from(parts[1], "base64url").toString("utf-8")
9994
+ );
9995
+ if (payload.exp && payload.exp * 1e3 < Date.now()) return false;
9996
+ return true;
9997
+ } catch {
9998
+ return false;
9999
+ }
10000
+ }
9731
10001
  async function verifyTokenViaHypha(token, hyphaServerUrl) {
9732
10002
  try {
9733
10003
  const baseUrl = hyphaServerUrl.replace(/\/$/, "");
@@ -9766,11 +10036,7 @@ class ServeAuth {
9766
10036
  if (!token) return null;
9767
10037
  const cached = this.cache.get(token);
9768
10038
  if (cached) return cached.email;
9769
- const localEmail = parseJwtEmail(token);
9770
- if (localEmail) {
9771
- this.cache.set(token, localEmail);
9772
- return localEmail;
9773
- }
10039
+ if (!isStructurallyLiveJwt(token)) return null;
9774
10040
  const serverEmail = await verifyTokenViaHypha(token, this.hyphaServerUrl);
9775
10041
  if (serverEmail) {
9776
10042
  this.cache.set(token, serverEmail);
@@ -11321,7 +11587,27 @@ function escalateWorkflowFailure(projectRoot, wf, run) {
11321
11587
  return void 0;
11322
11588
  }
11323
11589
  }
11324
- async function runWorkflow(projectRoot, wf, opts) {
11590
+ const inFlightRuns = /* @__PURE__ */ new Map();
11591
+ function workflowLockKey(projectRoot, name) {
11592
+ return `${projectRoot}\0${name}`;
11593
+ }
11594
+ function runWorkflow(projectRoot, wf, opts) {
11595
+ if (opts.allowConcurrent) return runWorkflowInner(projectRoot, wf, opts);
11596
+ const key = workflowLockKey(projectRoot, wf.name);
11597
+ const existing = inFlightRuns.get(key);
11598
+ if (existing) {
11599
+ opts.log?.(`[workflow] run coalesced "${wf.name}" in ${projectRoot} \u2014 a run of this workflow is already in flight`);
11600
+ opts.onCoalesced?.();
11601
+ return existing;
11602
+ }
11603
+ const p = runWorkflowInner(projectRoot, wf, opts);
11604
+ inFlightRuns.set(key, p);
11605
+ void p.finally(() => {
11606
+ if (inFlightRuns.get(key) === p) inFlightRuns.delete(key);
11607
+ });
11608
+ return p;
11609
+ }
11610
+ async function runWorkflowInner(projectRoot, wf, opts) {
11325
11611
  const now = opts.now || (() => {
11326
11612
  const d = /* @__PURE__ */ new Date();
11327
11613
  return { iso: d.toISOString(), ms: d.getTime() };
@@ -11570,6 +11856,7 @@ function computeSoftTurnCap(hardMax, cadence) {
11570
11856
  const DEFAULT_LOOP_MAX_EXTENSIONS = 3;
11571
11857
  const DEFAULT_LOOP_EXTENSION_TURNS = 25;
11572
11858
  function resolveMaxExtensions(envValue) {
11859
+ if (envValue == null || envValue.trim() === "") return DEFAULT_LOOP_MAX_EXTENSIONS;
11573
11860
  const n = Number(envValue);
11574
11861
  if (Number.isFinite(n) && n >= 0) return Math.floor(n);
11575
11862
  return DEFAULT_LOOP_MAX_EXTENSIONS;
@@ -11623,6 +11910,7 @@ function resolveStuckLimit(envValue) {
11623
11910
  return DEFAULT_LOOP_STUCK_CHECKPOINTS;
11624
11911
  }
11625
11912
  function resolveMaxAutoResumes(envValue) {
11913
+ if (envValue == null || envValue.trim() === "") return DEFAULT_LOOP_MAX_AUTORESUMES;
11626
11914
  const n = Number(envValue);
11627
11915
  if (Number.isFinite(n) && n >= 0) return Math.floor(n);
11628
11916
  return DEFAULT_LOOP_MAX_AUTORESUMES;
@@ -13558,7 +13846,7 @@ async function startDaemon(options) {
13558
13846
  try {
13559
13847
  const dir = loadSessionIndex()[sessionId]?.directory;
13560
13848
  if (!dir) return;
13561
- const { reconcileServiceLinks } = await import('./agentCommands-CQ89BZon.mjs');
13849
+ const { reconcileServiceLinks } = await import('./agentCommands-w72BRYhk.mjs');
13562
13850
  const configPath = getSvampConfigPath(dir, sessionId);
13563
13851
  const config = readSvampConfig(configPath);
13564
13852
  const entries = Array.from(urls.entries());
@@ -13576,7 +13864,7 @@ async function startDaemon(options) {
13576
13864
  }
13577
13865
  }
13578
13866
  async function createExposedTunnel(spec) {
13579
- const { FrpcTunnel } = await import('./frpc-IXxyfgPn.mjs');
13867
+ const { FrpcTunnel } = await import('./frpc-BCOnJSAM.mjs');
13580
13868
  const tunnel = new FrpcTunnel({
13581
13869
  name: spec.name,
13582
13870
  ports: spec.ports,
@@ -13597,14 +13885,14 @@ async function startDaemon(options) {
13597
13885
  }
13598
13886
  const tunnelRecreateState = /* @__PURE__ */ new Map();
13599
13887
  const tunnelRecreateInFlight = /* @__PURE__ */ new Set();
13600
- const { ServeManager } = await import('./serveManager-BszV0oIl.mjs');
13888
+ const { ServeManager } = await import('./serveManager-BuXk1_SH.mjs');
13601
13889
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
13602
13890
  ensureAutoInstalledSkills(logger).catch(() => {
13603
13891
  });
13604
13892
  ensureAutoInstalledCommands(logger);
13605
13893
  (async () => {
13606
13894
  try {
13607
- const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-DuLXaoGP.mjs');
13895
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-B9O-hKxm.mjs');
13608
13896
  beginClaudeVersionReconcile((msg) => logger.log(msg));
13609
13897
  } catch (e) {
13610
13898
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
@@ -13843,7 +14131,7 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
13843
14131
  }
13844
14132
  const sessionId = options2.sessionId || composeSessionId(generateFriendlyName(collectKnownFriendlyNames()), collectKnownSessionIds());
13845
14133
  const agentName = options2.agent || agentConfig.agent_type || "claude";
13846
- if (agentName !== "claude" && (KNOWN_ACP_AGENTS[agentName] || KNOWN_MCP_AGENTS[agentName])) {
14134
+ if (agentName !== "claude" && (KNOWN_ACP_AGENTS[agentName] || KNOWN_CODEX_AGENTS[agentName])) {
13847
14135
  return await spawnAgentSession(sessionId, directory, agentName, options2, resumeSessionId);
13848
14136
  }
13849
14137
  let stagedCredentials = null;
@@ -15713,11 +16001,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15713
16001
  });
15714
16002
  },
15715
16003
  onIssue: async (params) => {
15716
- const { issueRpc } = await import('./rpc-D7DB3XCH.mjs');
16004
+ const { issueRpc } = await import('./rpc-Dko-Fkk6.mjs');
15717
16005
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
15718
16006
  },
15719
16007
  onWorkflow: async (params) => {
15720
- const { workflowRpc } = await import('./rpc-CyACkgY1.mjs');
16008
+ const { workflowRpc } = await import('./rpc-D-1o_gWF.mjs');
15721
16009
  return workflowRpc(params?.cwd || directory, params || {});
15722
16010
  },
15723
16011
  onRipgrep: async (args, cwd) => {
@@ -16266,11 +16554,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
16266
16554
  });
16267
16555
  },
16268
16556
  onIssue: async (params) => {
16269
- const { issueRpc } = await import('./rpc-D7DB3XCH.mjs');
16557
+ const { issueRpc } = await import('./rpc-Dko-Fkk6.mjs');
16270
16558
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
16271
16559
  },
16272
16560
  onWorkflow: async (params) => {
16273
- const { workflowRpc } = await import('./rpc-CyACkgY1.mjs');
16561
+ const { workflowRpc } = await import('./rpc-D-1o_gWF.mjs');
16274
16562
  return workflowRpc(params?.cwd || directory, params || {});
16275
16563
  },
16276
16564
  onRipgrep: async (args, cwd) => {
@@ -16417,9 +16705,17 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
16417
16705
  }
16418
16706
  }
16419
16707
  let agentBackend;
16420
- if (KNOWN_MCP_AGENTS[agentName]) {
16421
- agentBackend = new CodexMcpBackend({
16708
+ if (KNOWN_CODEX_AGENTS[agentName]) {
16709
+ const avail = codexAppServerAvailable();
16710
+ if (avail === null) throw new Error("Codex CLI not found \u2014 install it (`npm i -g @openai/codex`) and authenticate (`svamp daemon codex-auth`).");
16711
+ if (avail === false) throw new Error("Installed Codex is too old for app-server \u2014 upgrade to >= 0.100 (`npm i -g @openai/codex@latest`).");
16712
+ const provider = resolveCodexProvider();
16713
+ const codexModel = options2.model || agentConfig?.default_model || provider.model || void 0;
16714
+ logger.log(`[Agent Session ${sessionId}] Codex backend: app-server (model=${codexModel ?? "default"}${provider.apiBase ? `, provider=${provider.apiBase}` : ""})`);
16715
+ agentBackend = new CodexAppServerBackend({
16422
16716
  cwd: directory,
16717
+ model: codexModel,
16718
+ provider,
16423
16719
  env: options2.environmentVariables,
16424
16720
  log: logger.log,
16425
16721
  isolationConfig: agentIsoConfig
@@ -16483,6 +16779,10 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
16483
16779
  return { ok: pending.length === 0, output: pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues." };
16484
16780
  };
16485
16781
  try {
16782
+ if (!ls.acp_budget_warned && ls.budget && (ls.budget.max_tokens || ls.budget.max_tokens_per_hour)) {
16783
+ sessionService.pushMessage({ type: "message", message: "\u26A0\uFE0F A token/rate cost cap (--max-tokens / --max-tokens-per-hour) is INERT for this agent \u2014 Codex/Gemini (ACP) expose no token usage, so this loop is bounded only by --max (iterations) and --max-runtime-sec. Set one of those to cap cost.", level: "warning" }, "event");
16784
+ ls.acp_budget_warned = true;
16785
+ }
16486
16786
  const acpMaxExt = resolveMaxExtensions(process.env.SVAMP_LOOP_MAX_EXTENSIONS);
16487
16787
  const acpEffMax = effectiveSoftCap(ls.max_iterations, ls.extensions, acpMaxExt);
16488
16788
  const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false, kind: void 0 } : checkLoopBudget(ledger, { max_iterations: acpEffMax, ...ls.budget || {} }, now, startedAt);
@@ -16500,6 +16800,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
16500
16800
  return;
16501
16801
  }
16502
16802
  const oracle = await runOracle();
16803
+ if (loopCancelledDuringVerify(readLoopState(directory, sessionId))) return;
16503
16804
  if (!oracle.ok) {
16504
16805
  writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", ledger });
16505
16806
  dispatchAcpPrompt(`Continue toward the goal: ${ls.goal_task || ls.until || "the task"}. Not done yet \u2014 the loop oracle still reports pending work:
@@ -16521,6 +16822,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
16521
16822
  log: (m) => logger.log(`[Session ${sessionId}] ${m}`)
16522
16823
  }
16523
16824
  );
16825
+ if (loopCancelledDuringVerify(readLoopState(directory, sessionId))) return;
16524
16826
  if (result.action === "rekick") {
16525
16827
  writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", holds: holds + 1, started_at: startedAt, ledger });
16526
16828
  const guidance = result.guidance.replace(/\s+/g, " ").slice(0, 1200);
@@ -16700,6 +17002,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
16700
17002
  }
16701
17003
  if (!loopDir) loopDir = loadSessionIndex()[sessionId]?.directory;
16702
17004
  const wasInMemory = teardownTrackedSession(sessionId);
17005
+ idleTriggerTracker.forget(sessionId);
16703
17006
  const markedArchived = markSessionAsArchived(sessionId);
16704
17007
  if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
16705
17008
  deactivateLoop(loopDir, sessionId);
@@ -16757,6 +17060,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
16757
17060
  }
16758
17061
  if (!loopDir) loopDir = loadSessionIndex()[sessionId]?.directory;
16759
17062
  teardownTrackedSession(sessionId);
17063
+ idleTriggerTracker.forget(sessionId);
16760
17064
  deletePersistedSession(sessionId);
16761
17065
  if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
16762
17066
  deactivateLoop(loopDir, sessionId);
@@ -17038,7 +17342,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
17038
17342
  }
17039
17343
  if (persistedSessions.length > 0) {
17040
17344
  try {
17041
- const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-DuLXaoGP.mjs');
17345
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-B9O-hKxm.mjs');
17042
17346
  await awaitClaudeVersionReady();
17043
17347
  } catch {
17044
17348
  }
@@ -17235,7 +17539,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
17235
17539
  const PING_TIMEOUT_MS = 15e3;
17236
17540
  const POST_RECONNECT_GRACE_MS = 2e4;
17237
17541
  const RECONNECT_JITTER_MS = 2500;
17238
- const { WorkflowScheduler } = await import('./scheduler-7wve0Fy3.mjs');
17542
+ const { WorkflowScheduler } = await import('./scheduler-CN8jChyQ.mjs');
17239
17543
  const workflowScheduler = new WorkflowScheduler({
17240
17544
  projectRoots: () => {
17241
17545
  const dirs = /* @__PURE__ */ new Set();
@@ -17846,4 +18150,4 @@ var run = /*#__PURE__*/Object.freeze({
17846
18150
  writeStopMarker: writeStopMarker
17847
18151
  });
17848
18152
 
17849
- export { downloadSkillFile as $, listRuns as A, getWorkflow as B, runWorkflow as C, setWorkflowEnabled as D, removeWorkflow as E, saveWorkflow as F, rawWorkflow as G, listWorkflows as H, isWorkflowEnabled as I, workflowCrons as J, cronMatches as K, summarize as L, workflowSteps as M, loadMachineContext as N, buildMachineInstructions as O, machineToolsForRole as P, buildMachineTools as Q, READ_ONLY_TOOLS as R, ServeAuth as S, parseFrontmatter as T, getSkillsServer as U, getSkillsWorkspaceName as V, getSkillsCollectionName as W, fetchWithTimeout as X, searchSkills as Y, SKILLS_DIR as Z, getSkillInfo as _, createSessionStore as a, listSkillFiles as a0, resolveModel as a1, formatHandle as a2, normalizeAllowedUser as a3, loadSecurityContextConfig as a4, resolveSecurityContext as a5, buildSecurityContextFromFlags as a6, mergeSecurityContexts as a7, buildSessionShareUrl as a8, computeOutboundHop as a9, registerAwaitingReply as aa, buildMachineShareUrl as ab, parseHandle as ac, handleMatchesMetadata as ad, describeMisconfiguration as ae, buildMachineDeps as af, applyClaudeProxyEnv as ag, composeSessionId as ah, generateFriendlyName as ai, generateHookSettings as aj, claudeAuth as ak, projectInfo as al, DefaultTransport$1 as am, acpBackend as an, acpAgentConfig as ao, codexMcpBackend as ap, GeminiTransport$1 as aq, instanceConfig as ar, api as as, run as at, stopDaemon as b, connectToHypha as c, daemonStatus as d, clearStopMarker as e, stopMarkerExists as f, getHyphaServerUrl$1 as g, getFrpsSubdomainHost as h, getFrpsServerPort as i, getFrpsServerAddr as j, shortId as k, getHyphaServerUrl as l, hasCookieToken as m, resolveProjectRoot as n, getIssue as o, resumeIssue as p, pauseIssue as q, registerMachineService as r, startDaemon as s, addComment as t, updateIssue as u, addIssue as v, listIssues as w, searchIssues as x, isVisibleTo as y, getRun as z };
18153
+ export { downloadSkillFile as $, listRuns as A, getWorkflow as B, runWorkflow as C, setWorkflowEnabled as D, removeWorkflow as E, saveWorkflow as F, rawWorkflow as G, listWorkflows as H, isWorkflowEnabled as I, workflowCrons as J, cronMatches as K, summarize as L, workflowSteps as M, loadMachineContext as N, buildMachineInstructions as O, machineToolsForRole as P, buildMachineTools as Q, READ_ONLY_TOOLS as R, ServeAuth as S, parseFrontmatter as T, getSkillsServer as U, getSkillsWorkspaceName as V, getSkillsCollectionName as W, fetchWithTimeout as X, searchSkills as Y, SKILLS_DIR as Z, getSkillInfo as _, createSessionStore as a, listSkillFiles as a0, resolveModel as a1, formatHandle as a2, normalizeAllowedUser as a3, loadSecurityContextConfig as a4, resolveSecurityContext as a5, buildSecurityContextFromFlags as a6, mergeSecurityContexts as a7, buildSessionShareUrl as a8, computeOutboundHop as a9, registerAwaitingReply as aa, buildMachineShareUrl as ab, parseHandle as ac, handleMatchesMetadata as ad, describeMisconfiguration as ae, buildMachineDeps as af, applyClaudeProxyEnv as ag, composeSessionId as ah, generateFriendlyName as ai, generateHookSettings as aj, claudeAuth as ak, projectInfo as al, DefaultTransport$1 as am, acpBackend as an, acpAgentConfig as ao, codexProvider as ap, codexAppServerBackend as aq, GeminiTransport$1 as ar, instanceConfig as as, api as at, run as au, stopDaemon as b, connectToHypha as c, daemonStatus as d, clearStopMarker as e, stopMarkerExists as f, getHyphaServerUrl$1 as g, getFrpsSubdomainHost as h, getFrpsServerPort as i, getFrpsServerAddr as j, shortId as k, getHyphaServerUrl as l, hasCookieToken as m, resolveProjectRoot as n, getIssue as o, resumeIssue as p, pauseIssue as q, registerMachineService as r, startDaemon as s, addComment as t, updateIssue as u, addIssue as v, listIssues as w, searchIssues as x, isVisibleTo as y, getRun as z };