very-happy-cli 0.2.134 → 0.2.135

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 (45) hide show
  1. package/dist/{AcpBackend-CeNp7tgD.cjs → AcpBackend-C0Bwx6bc.cjs} +2 -2
  2. package/dist/{AcpBackend-B8KEjDwF.mjs → AcpBackend-CkDpu1n_.mjs} +2 -2
  3. package/dist/{AcpSessionManager-KlDZbEmZ.mjs → AcpSessionManager-DB_mfFaH.mjs} +1 -1
  4. package/dist/{AcpSessionManager-Bu1d05bx.cjs → AcpSessionManager-DqvUp-UU.cjs} +1 -1
  5. package/dist/{agentGuidance-CPG5Cr5T.mjs → agentGuidance-7MByWxKj.mjs} +1 -1
  6. package/dist/{agentGuidance-Cg8Lha39.cjs → agentGuidance-DlZM_YPV.cjs} +1 -1
  7. package/dist/codex/happyMcpStdioBridge.cjs +2 -2
  8. package/dist/codex/happyMcpStdioBridge.mjs +2 -2
  9. package/dist/{config-C3e90AIH.cjs → config-BO0ZHyoB.cjs} +4 -4
  10. package/dist/{config-C4gWfnJX.mjs → config-DzvbcL_x.mjs} +4 -4
  11. package/dist/{index-jqG8CV0c.cjs → index-BEpZjXs4.cjs} +6 -6
  12. package/dist/{index-B6UvB88K.mjs → index-C33JB4xB.mjs} +7 -7
  13. package/dist/{index-CEDPokKf.mjs → index-CpEdOGpe.mjs} +4 -4
  14. package/dist/{index-CX-zp3FH.cjs → index-DHI6Y1xW.cjs} +4 -4
  15. package/dist/{index-B1plzL9M.cjs → index-DNbKbe23.cjs} +1522 -1200
  16. package/dist/{index-BqUOj1zK.mjs → index-gefLe1Bk.mjs} +1520 -1198
  17. package/dist/index.cjs +4 -4
  18. package/dist/index.mjs +4 -4
  19. package/dist/{installTerminalHooks-BT4Dq28Y.mjs → installTerminalHooks-BWoHGCew.mjs} +4 -4
  20. package/dist/{installTerminalHooks-DPmEaHg2.cjs → installTerminalHooks-gmWKACNb.cjs} +4 -4
  21. package/dist/lib.cjs +2 -2
  22. package/dist/lib.d.cts +14 -0
  23. package/dist/lib.d.mts +14 -0
  24. package/dist/lib.mjs +2 -2
  25. package/dist/{limits-BGeIBBE3.mjs → limits-CPTgiQ9A.mjs} +8 -8
  26. package/dist/{limits-D6kiAfXJ.cjs → limits-K17AgMHD.cjs} +8 -8
  27. package/dist/{mcp-DGZmUpuZ.cjs → mcp-CK1NCZm0.cjs} +4 -4
  28. package/dist/{mcp-BGu_dUaJ.mjs → mcp-DhuxpwTB.mjs} +4 -4
  29. package/dist/{runGemini-DGTNwiLo.mjs → runGemini-BVJE7LZl.mjs} +6 -6
  30. package/dist/{runGemini-BPPnulIk.cjs → runGemini-n0wD2Z9R.cjs} +6 -6
  31. package/dist/{runOpenClaw-CxgTFOSM.mjs → runOpenClaw-CJaIYpB7.mjs} +5 -5
  32. package/dist/{runOpenClaw-BB3Ivt1S.cjs → runOpenClaw-fYx1XweW.cjs} +5 -5
  33. package/dist/{send-Bo6iKuXr.cjs → send-WlJ4r3U2.cjs} +4 -4
  34. package/dist/{send-DHShoBeq.mjs → send-fWHOFJV9.mjs} +4 -4
  35. package/dist/{sessions-CJuwvEr0.mjs → sessions-5fJxQ2xj.mjs} +4 -4
  36. package/dist/{sessions-DyC5Attg.cjs → sessions-Cx_3cvN6.cjs} +4 -4
  37. package/dist/{spawn-QtKlr8Ig.mjs → spawn-CF1ZV-YH.mjs} +4 -4
  38. package/dist/{spawn-gha6nGx-.cjs → spawn-DVJtNwWN.cjs} +4 -4
  39. package/dist/{teams-BbzWNV9e.cjs → teams-BbYi0fWG.cjs} +4 -4
  40. package/dist/{teams-BF-GxUZc.mjs → teams-gzvKrMfn.mjs} +4 -4
  41. package/dist/{todo-CkJ5UjWs.mjs → todo-BAK2DVLV.mjs} +3 -3
  42. package/dist/{todo-BaNaNZ8c.cjs → todo-DHY20tpD.cjs} +3 -3
  43. package/dist/{types-Cq-tvv9t.mjs → types-BEXGahmj.mjs} +45 -9
  44. package/dist/{types-Dabm17Ln.cjs → types-DjAHWb4J.cjs} +46 -9
  45. package/package.json +8 -8
@@ -3,13 +3,13 @@
3
3
  var chalk = require('chalk');
4
4
  var os = require('node:os');
5
5
  var node_crypto = require('node:crypto');
6
- var api = require('./types-Dabm17Ln.cjs');
7
- var persistence = require('./limits-D6kiAfXJ.cjs');
6
+ var api = require('./types-DjAHWb4J.cjs');
7
+ var persistence = require('./limits-K17AgMHD.cjs');
8
8
  var spawn = require('cross-spawn');
9
9
  var path = require('node:path');
10
10
  var node_readline = require('node:readline');
11
11
  var node_fs = require('node:fs');
12
- var agentGuidance = require('./agentGuidance-Cg8Lha39.cjs');
12
+ var agentGuidance = require('./agentGuidance-DlZM_YPV.cjs');
13
13
  var promises = require('node:fs/promises');
14
14
  var fs = require('fs/promises');
15
15
  var ink = require('ink');
@@ -1353,6 +1353,152 @@ async function claudeLocalLauncher(session) {
1353
1353
  return exitReason || { type: "exit", code: 0 };
1354
1354
  }
1355
1355
 
1356
+ const text = (value) => typeof value === "string" && value.length > 0 && value.length <= 1024 && !value.includes("\0") ? value : null;
1357
+ const publicMcpStatus = (servers) => servers.map(({ name, status, serverInfo, scope, tools }) => ({ name, status, serverInfo, scope, tools: tools?.map(({ name: name2 }) => ({ name: name2 })) }));
1358
+ const fingerprint = (result) => JSON.stringify([result.canRewind, [...result.filesChanged ?? []].sort(), result.insertions, result.deletions]);
1359
+ function isRuntimeCheckpointReplay(message) {
1360
+ const m = message;
1361
+ return m.type === "user" && m.isReplay === true && !m.parent_tool_use_id && !(Array.isArray(m.message?.content) && m.message.content.some((block) => block.type === "tool_result"));
1362
+ }
1363
+ function createRuntimeControls(isThinking) {
1364
+ let query = null, generation = node_crypto.randomInt(0, 2 ** 40), activityRevision = 0;
1365
+ const operations = /* @__PURE__ */ new Map();
1366
+ const confirmations = /* @__PURE__ */ new Map();
1367
+ const tasks = /* @__PURE__ */ new Map();
1368
+ const checkpoints = /* @__PURE__ */ new Map();
1369
+ let pending = false;
1370
+ const setQuery = (next) => {
1371
+ for (const operation of operations.values()) if (operation.status === "running") {
1372
+ operation.status = "failed";
1373
+ operation.error = "Claude query ended before this operation completed";
1374
+ }
1375
+ query = next;
1376
+ generation++;
1377
+ pending = false;
1378
+ confirmations.clear();
1379
+ tasks.clear();
1380
+ checkpoints.clear();
1381
+ };
1382
+ function observe(message) {
1383
+ const m = message;
1384
+ if (m.type === "user" || m.type === "assistant") {
1385
+ activityRevision++;
1386
+ confirmations.clear();
1387
+ }
1388
+ const content = typeof m.message?.content === "string" ? m.message.content : Array.isArray(m.message?.content) ? m.message.content.filter((block) => block.type === "text").map((block) => block.text).join(" ") : "";
1389
+ if (isRuntimeCheckpointReplay(message) && text(m.uuid) && content) {
1390
+ checkpoints.set(m.uuid, { id: m.uuid, title: content.slice(0, 120) });
1391
+ if (checkpoints.size > 100) checkpoints.delete(checkpoints.keys().next().value);
1392
+ }
1393
+ if (m.type === "system" && text(m.task_id) && ["task_started", "task_updated", "task_progress", "task_notification"].includes(m.subtype)) {
1394
+ const previous = tasks.get(m.task_id);
1395
+ if (m.subtype !== "task_notification" && ["completed", "failed", "stopped", "cancelled"].includes(String(previous?.status))) return;
1396
+ tasks.set(m.task_id, {
1397
+ ...previous,
1398
+ id: m.task_id,
1399
+ ...text(m.tool_use_id) ? { toolUseId: m.tool_use_id } : {},
1400
+ ...text(m.description) ? { description: m.description } : {},
1401
+ ...text(m.task_type) ? { type: m.task_type } : {},
1402
+ status: m.subtype === "task_notification" ? m.status ?? "completed" : "running"
1403
+ });
1404
+ if (tasks.size > 100) tasks.delete(tasks.keys().next().value);
1405
+ }
1406
+ }
1407
+ function request(input) {
1408
+ const r = input && typeof input === "object" ? input : {};
1409
+ if (r.action === "status") return { queryGeneration: generation, active: !!query, isRunning: isThinking(), canRewind: !!query && !isThinking() && !pending && query.canControl(), busy: pending || !!query && !query.canControl(), tasks: [...tasks.values()], checkpoints: [...checkpoints.values()], operations: [...operations.values()].filter((op2) => op2.queryGeneration === generation) };
1410
+ if (r.action === "operation") {
1411
+ const op2 = operations.get(String(r.operationId));
1412
+ return op2 ? { ...op2 } : { error: "Operation not found" };
1413
+ }
1414
+ const q = query;
1415
+ if (!q) return { error: "No active Claude SDK query" };
1416
+ if (pending || !q.canControl()) return { error: "Claude is waiting for an interaction or another control operation; retry after it finishes" };
1417
+ const action = String(r.action), server = text(r.serverName), task = text(r.taskId), target = text(r.userMessageId);
1418
+ const runGeneration = generation, runRevision = activityRevision;
1419
+ let execute;
1420
+ switch (action) {
1421
+ case "reload-skills":
1422
+ execute = () => q.reloadSkills();
1423
+ break;
1424
+ case "reload-plugins":
1425
+ execute = async () => {
1426
+ const result = await q.reloadPlugins();
1427
+ return { ...result, mcpServers: publicMcpStatus(result.mcpServers ?? []) };
1428
+ };
1429
+ break;
1430
+ case "mcp-status":
1431
+ execute = async () => publicMcpStatus(await q.mcpServerStatus());
1432
+ break;
1433
+ case "mcp-reconnect":
1434
+ if (!server) return { error: "Invalid server name" };
1435
+ execute = () => q.reconnectMcpServer(server);
1436
+ break;
1437
+ case "mcp-toggle":
1438
+ if (!server || typeof r.enabled !== "boolean") return { error: "Invalid server or enabled flag" };
1439
+ execute = () => q.toggleMcpServer(server, r.enabled);
1440
+ break;
1441
+ case "stop-task":
1442
+ if (!task || !tasks.has(task)) return { error: "Unknown task in this query" };
1443
+ execute = () => q.stopTask(task);
1444
+ break;
1445
+ case "background-tasks":
1446
+ if (r.toolUseId !== void 0 && !text(r.toolUseId)) return { error: "Invalid tool use ID" };
1447
+ execute = async () => ({ backgrounded: await q.backgroundTasks(r.toolUseId) });
1448
+ break;
1449
+ case "rewind-preview":
1450
+ if (isThinking() || !target || !checkpoints.has(target)) return { error: "Rewind requires an idle query and a known user checkpoint" };
1451
+ execute = async () => {
1452
+ const result = await q.rewindFiles(target, { dryRun: true });
1453
+ if (!result.canRewind) return result;
1454
+ if (query !== q || generation !== runGeneration || activityRevision !== runRevision || isThinking() || !q.canControl()) throw Error("Query changed; preview again");
1455
+ const token = node_crypto.randomUUID();
1456
+ confirmations.set(token, { generation, revision: activityRevision, userMessageId: target, fingerprint: fingerprint(result), expires: Date.now() + 3e5 });
1457
+ return { ...result, confirmationToken: token };
1458
+ };
1459
+ break;
1460
+ case "rewind-apply": {
1461
+ const confirmation = confirmations.get(String(r.confirmationToken));
1462
+ if (isThinking() || !confirmation || confirmation.generation !== generation || confirmation.expires < Date.now()) return { error: "Preview expired or query is running; preview again" };
1463
+ confirmations.delete(String(r.confirmationToken));
1464
+ execute = async () => {
1465
+ const fresh = await q.rewindFiles(confirmation.userMessageId, { dryRun: true });
1466
+ if (query !== q || generation !== runGeneration || activityRevision !== confirmation.revision || isThinking() || !q.canControl() || fingerprint(fresh) !== confirmation.fingerprint) throw Error("Files or query changed; preview again");
1467
+ return q.rewindFiles(confirmation.userMessageId, { dryRun: false });
1468
+ };
1469
+ break;
1470
+ }
1471
+ default:
1472
+ return { error: "Unknown Claude control action" };
1473
+ }
1474
+ pending = true;
1475
+ const op = { id: node_crypto.randomUUID(), queryGeneration: runGeneration, action, status: "running" };
1476
+ operations.set(op.id, op);
1477
+ if (operations.size > 50) for (const [id, old] of operations) {
1478
+ if (old.status !== "running") {
1479
+ operations.delete(id);
1480
+ break;
1481
+ }
1482
+ }
1483
+ void Promise.resolve().then(() => {
1484
+ if (query !== q || generation !== runGeneration || !q.canControl()) throw Error("Claude query changed or is waiting for an interaction");
1485
+ return execute();
1486
+ }).then((result) => {
1487
+ if (generation !== runGeneration || op.status !== "running") return;
1488
+ op.status = "completed";
1489
+ op.result = result ?? null;
1490
+ }, (error) => {
1491
+ if (generation !== runGeneration || op.status !== "running") return;
1492
+ op.status = "failed";
1493
+ op.error = error instanceof Error ? error.message : "Claude control failed";
1494
+ }).finally(() => {
1495
+ if (generation === runGeneration) pending = false;
1496
+ });
1497
+ return { operationId: op.id };
1498
+ }
1499
+ return { setQuery, observe, request };
1500
+ }
1501
+
1356
1502
  class MessageBuffer {
1357
1503
  messages = [];
1358
1504
  listeners = [];
@@ -1622,6 +1768,9 @@ function query(params) {
1622
1768
  tools: opts?.tools,
1623
1769
  persistSession: opts?.persistSession,
1624
1770
  includePartialMessages: opts?.includePartialMessages,
1771
+ forwardSubagentText: true,
1772
+ enableFileCheckpointing: true,
1773
+ extraArgs: { "replay-user-messages": null },
1625
1774
  onElicitation: opts?.onElicitation,
1626
1775
  onUserDialog: opts?.onUserDialog,
1627
1776
  supportedDialogKinds: opts?.onUserDialog ? opts.supportedDialogKinds : void 0
@@ -2064,6 +2213,20 @@ async function claudeRemote(opts) {
2064
2213
  content: initial.message
2065
2214
  }
2066
2215
  });
2216
+ let callbackDepth = 0;
2217
+ const guardCallback = (callback) => (async (...args) => {
2218
+ callbackDepth++;
2219
+ try {
2220
+ return await callback(...args);
2221
+ } finally {
2222
+ setImmediate(() => {
2223
+ callbackDepth--;
2224
+ });
2225
+ }
2226
+ });
2227
+ if (sdkOptions.canCallTool) sdkOptions.canCallTool = guardCallback(sdkOptions.canCallTool);
2228
+ if (sdkOptions.onElicitation) sdkOptions.onElicitation = guardCallback(sdkOptions.onElicitation);
2229
+ if (sdkOptions.onUserDialog) sdkOptions.onUserDialog = guardCallback(sdkOptions.onUserDialog);
2067
2230
  const response = query({
2068
2231
  prompt: messages,
2069
2232
  options: sdkOptions
@@ -2084,6 +2247,15 @@ async function claudeRemote(opts) {
2084
2247
  }
2085
2248
  if (opts.onQueryReady) {
2086
2249
  opts.onQueryReady({
2250
+ canControl: () => callbackDepth === 0,
2251
+ reloadSkills: () => response.reloadSkills(),
2252
+ reloadPlugins: () => response.reloadPlugins(),
2253
+ mcpServerStatus: () => response.mcpServerStatus(),
2254
+ reconnectMcpServer: (name) => response.reconnectMcpServer(name),
2255
+ toggleMcpServer: (name, enabled) => response.toggleMcpServer(name, enabled),
2256
+ stopTask: (id) => response.stopTask(id),
2257
+ backgroundTasks: (id) => response.backgroundTasks(id),
2258
+ rewindFiles: (id, options) => response.rewindFiles(id, options),
2087
2259
  setPermissionMode: (mode2) => response.setPermissionMode(mode2),
2088
2260
  setModel: (model) => response.setModel(modelTarget(model)),
2089
2261
  interrupt: async () => {
@@ -2727,7 +2899,7 @@ class PermissionHandler {
2727
2899
  } else if (pending.kind === "elicitation") {
2728
2900
  pending.resolve(message.approved ? { action: "accept", content: elicitationContent(message.updatedInput) } : { action: message.decision === "abort" ? "cancel" : "decline" });
2729
2901
  } else {
2730
- pending.resolve(message.approved ? { behavior: "completed", result: message.updatedInput?.result ?? "retry_fallback" } : { behavior: "cancelled" });
2902
+ pending.resolve(message.approved && pending.request.dialogKind === "refusal_fallback_prompt" && (message.updatedInput?.result === void 0 || message.updatedInput.result === "retry_fallback") ? { behavior: "completed", result: "retry_fallback" } : { behavior: "cancelled" });
2731
2903
  }
2732
2904
  this.recordCompletedRequest(id, effectiveResponse);
2733
2905
  this.scheduleDeferredPermissionModeUpdate();
@@ -3189,6 +3361,7 @@ class SDKToLogConverter {
3189
3361
  }
3190
3362
  }
3191
3363
 
3364
+ const transcriptSystemSubtypes = /* @__PURE__ */ new Set(["task_started", "task_updated", "task_progress", "task_notification"]);
3192
3365
  class OutgoingMessageQueue {
3193
3366
  constructor(sendFunction) {
3194
3367
  this.sendFunction = sendFunction;
@@ -3270,7 +3443,7 @@ class OutgoingMessageQueue {
3270
3443
  break;
3271
3444
  }
3272
3445
  if (!item.sent) {
3273
- if (item.logMessage.type !== "system") {
3446
+ if (item.logMessage.type !== "system" || transcriptSystemSubtypes.has(item.logMessage.subtype)) {
3274
3447
  this.sendFunction(item.logMessage);
3275
3448
  }
3276
3449
  item.sent = true;
@@ -3898,6 +4071,8 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
3898
4071
  let abortController = null;
3899
4072
  let abortFuture = null;
3900
4073
  const turnSteering = createTurnSteeringController();
4074
+ const runtimeControls = createRuntimeControls(() => session.thinking);
4075
+ session.client.rpcHandlerManager.registerHandler("claude-runtime-control", async (input) => runtimeControls.request(input));
3901
4076
  async function abort() {
3902
4077
  if (abortController && !abortController.signal.aborted) {
3903
4078
  abortController.abort();
@@ -3958,6 +4133,8 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
3958
4133
  let ongoingToolCalls = /* @__PURE__ */ new Map();
3959
4134
  let notifiedQuestionToolCalls = /* @__PURE__ */ new Set();
3960
4135
  function onMessage(message) {
4136
+ runtimeControls.observe(message);
4137
+ if (isRuntimeCheckpointReplay(message)) return;
3961
4138
  if (message.type === "user" && isClaudeInterruptSentinelContent(message.message.content)) {
3962
4139
  sdkToLogConverter.convert(message);
3963
4140
  return;
@@ -4180,6 +4357,7 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
4180
4357
  if (metadata.permissionMode) onEffectivePermissionMode?.(metadata.permissionMode);
4181
4358
  },
4182
4359
  onQueryReady: (q) => {
4360
+ runtimeControls.setQuery(q);
4183
4361
  turnSteering.setInterrupt(q.interrupt);
4184
4362
  session.setSteerHandler(async (input) => {
4185
4363
  if (!modeGate.matches(input.mode)) return false;
@@ -4269,6 +4447,7 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
4269
4447
  continue;
4270
4448
  }
4271
4449
  } finally {
4450
+ runtimeControls.setQuery(null);
4272
4451
  turnSteering.reset();
4273
4452
  session.setSteerHandler(null);
4274
4453
  livePermissionModeHandler = null;
@@ -5319,6 +5498,80 @@ ${typeLabels[type] || type}:`));
5319
5498
  console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
5320
5499
  }
5321
5500
 
5501
+ const AGENT_CHECK_INTERVAL = 6 * 60 * 60 * 1e3;
5502
+ function stableVersion(value) {
5503
+ const match = value.trim().match(/(?:^|\s|v)(\d+\.\d+\.\d+)([-+][\w.-]+)?(?=\s|$)/);
5504
+ return match && !match[2] ? match[1] : null;
5505
+ }
5506
+ function newerVersion(current, latest) {
5507
+ const a = current.split(".").map(Number), b = latest.split(".").map(Number);
5508
+ for (let i = 0; i < 3; i++) {
5509
+ if (b[i] !== a[i]) return b[i] > a[i];
5510
+ }
5511
+ return false;
5512
+ }
5513
+ const runVersionCommand = (command, args, shell) => new Promise((resolve, reject) => {
5514
+ node_child_process.execFile(
5515
+ command,
5516
+ args,
5517
+ { timeout: 5e3, maxBuffer: 32 * 1024, windowsHide: true, shell },
5518
+ (error, stdout) => error ? reject(error) : resolve(stdout)
5519
+ );
5520
+ });
5521
+ async function probeAgentVersion(command, platform = process.platform, run = runVersionCommand) {
5522
+ if (platform === "win32") {
5523
+ try {
5524
+ await run("where.exe", [command], false);
5525
+ } catch (error) {
5526
+ return { version: null, missing: error.code === 1 };
5527
+ }
5528
+ }
5529
+ try {
5530
+ return { version: stableVersion(await run(command, ["--version"], platform === "win32")), missing: false };
5531
+ } catch (error) {
5532
+ return { version: null, missing: error.code === "ENOENT" };
5533
+ }
5534
+ }
5535
+ async function registryVersion(name) {
5536
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, { signal: AbortSignal.timeout(5e3) });
5537
+ if (!response.ok) throw new Error("Registry unavailable");
5538
+ const data = await response.json();
5539
+ return typeof data.version === "string" ? stableVersion(data.version) : null;
5540
+ }
5541
+ async function detectAgentVersions(deps = { probe: probeAgentVersion, registryVersion }, now = Date.now()) {
5542
+ const agents = await Promise.all(api.AGENT_VERSION_SOURCES.map(async (source) => {
5543
+ const local = source.command ? await deps.probe(source.command).catch(() => ({ version: null, missing: false })) : { version: stableVersion(persistence.packageJson.dependencies["@anthropic-ai/claude-agent-sdk"]), missing: false };
5544
+ if (local.missing) return { id: source.id, installed: null, latest: null, status: "not-installed" };
5545
+ const latest = await deps.registryVersion(source.package).catch(() => null);
5546
+ const installed = local.version;
5547
+ const status = !installed || !latest ? "unknown" : newerVersion(installed, latest) ? "update-available" : "current";
5548
+ return { id: source.id, installed, latest, status };
5549
+ }));
5550
+ return { checkedAt: now, daemonPid: process.pid, agents };
5551
+ }
5552
+ function startAgentVersionChecks(publish, onError, detect = detectAgentVersions) {
5553
+ let stopped = false, pending = false;
5554
+ async function refresh() {
5555
+ if (stopped || pending) return;
5556
+ pending = true;
5557
+ try {
5558
+ const snapshot = await detect();
5559
+ if (!stopped) await publish(snapshot);
5560
+ } catch (error) {
5561
+ onError(error);
5562
+ } finally {
5563
+ pending = false;
5564
+ }
5565
+ }
5566
+ void refresh();
5567
+ const timer = setInterval(() => void refresh(), AGENT_CHECK_INTERVAL);
5568
+ timer.unref();
5569
+ return () => {
5570
+ stopped = true;
5571
+ clearInterval(timer);
5572
+ };
5573
+ }
5574
+
5322
5575
  function sanitizeSpawnModel(value) {
5323
5576
  if (typeof value !== "string") return null;
5324
5577
  return /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}(\[1m\])?$/i.test(value) ? value : null;
@@ -7750,7 +8003,7 @@ function parseJsonObject(text) {
7750
8003
  return null;
7751
8004
  }
7752
8005
  }
7753
- function resolveSdkClaudeBinary(platform = process.platform, arch = process.arch, resolver = (s) => node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-B1plzL9M.cjs', document.baseURI).href))).resolve(s)) {
8006
+ function resolveSdkClaudeBinary(platform = process.platform, arch = process.arch, resolver = (s) => node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DNbKbe23.cjs', document.baseURI).href))).resolve(s)) {
7754
8007
  const platformArch = `${platform}-${arch}`;
7755
8008
  const candidates = [
7756
8009
  `@anthropic-ai/claude-agent-sdk-${platformArch}/claude`,
@@ -10991,6 +11244,10 @@ async function startDaemon() {
10991
11244
  persistence.logger.warn(`[DAEMON RUN] Ignoring invalid HAPPY_CLI_UPDATE_CHECK_INTERVAL; using ${api.DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS}ms`);
10992
11245
  }
10993
11246
  const cliUpdateInterval = setInterval(() => void refreshCliUpdate(), cliUpdateIntervalMs);
11247
+ const stopAgentVersionChecks = startAgentVersionChecks(
11248
+ (snapshot) => apiMachine.updateDaemonState((state) => ({ ...state, status: state?.status ?? "running", agentVersions: snapshot })),
11249
+ (error) => persistence.logger.debug("[DAEMON RUN] Agent version check failed", error)
11250
+ );
10994
11251
  const heartbeatIntervalMs = parseInt(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL || "60000");
10995
11252
  const restartOnStaleVersionAndHeartbeat = setInterval(serialTask(async () => {
10996
11253
  try {
@@ -11031,6 +11288,7 @@ async function startDaemon() {
11031
11288
  teamWorker?.stop();
11032
11289
  clearInterval(restartOnStaleVersionAndHeartbeat);
11033
11290
  clearInterval(cliUpdateInterval);
11291
+ stopAgentVersionChecks();
11034
11292
  claudeAuthService.stop();
11035
11293
  apiMachine.shutdown();
11036
11294
  await stopControlServer();
@@ -11092,6 +11350,7 @@ async function startDaemon() {
11092
11350
  persistence.logger.debug("[DAEMON RUN] Health check interval cleared");
11093
11351
  }
11094
11352
  clearInterval(cliUpdateInterval);
11353
+ stopAgentVersionChecks();
11095
11354
  await apiMachine.updateDaemonState((state) => ({
11096
11355
  ...state,
11097
11356
  status: "shutting-down",
@@ -12181,7 +12440,7 @@ function registerSideQuestionHandler(rpc, deps) {
12181
12440
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
12182
12441
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
12183
12442
  const run = deps.run ?? (async (input) => {
12184
- const { query } = await Promise.resolve().then(function () { return require('./index-CX-zp3FH.cjs'); });
12443
+ const { query } = await Promise.resolve().then(function () { return require('./index-DHI6Y1xW.cjs'); });
12185
12444
  return runSideQuestion(query, input);
12186
12445
  });
12187
12446
  const slots = /* @__PURE__ */ new Map();
@@ -12283,7 +12542,7 @@ const MAX_TITLE_CHARS = 60;
12283
12542
  const GENERATION_TIMEOUT_MS = 3e4;
12284
12543
  function resolveClaudeBinary() {
12285
12544
  try {
12286
- const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-B1plzL9M.cjs', document.baseURI).href)));
12545
+ const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DNbKbe23.cjs', document.baseURI).href)));
12287
12546
  const utilsPath = path.resolve(path.join(api.projectPath(), "scripts", "claude_version_utils.cjs"));
12288
12547
  const { getClaudeCliPath } = require$1(utilsPath);
12289
12548
  const path$1 = getClaudeCliPath();
@@ -12744,7 +13003,7 @@ async function runClaude(credentials, options = {}) {
12744
13003
  // a brand-new session cannot pick a PDF until after sending once.
12745
13004
  attachmentKinds: [...CLAUDE_ATTACHMENT_KINDS],
12746
13005
  queueCancellation: true,
12747
- capabilities: ["claude-steer-v1", "claude-live-permission-v1", "claude-live-permission-v2", "claude-btw-v1"],
13006
+ capabilities: ["claude-steer-v1", "claude-live-permission-v1", "claude-live-permission-v2", "claude-btw-v1", "claude-runtime-controls-v1"],
12748
13007
  // Effective mode this process enforces. Kept current by
12749
13008
  // publishPermissionMode below; the web renders it instead of guessing.
12750
13009
  permissionMode: mapToClaudeMode(initialPermissionMode ?? "default"),
@@ -14501,9 +14760,9 @@ function resolveLocalSignupBootstrap(configuredMode, configuredInviteCodes, gene
14501
14760
  };
14502
14761
  }
14503
14762
 
14504
- const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-B1plzL9M.cjs', document.baseURI).href)));
14763
+ const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DNbKbe23.cjs', document.baseURI).href)));
14505
14764
  const __dirname$1 = path.dirname(__filename$1);
14506
- const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-B1plzL9M.cjs', document.baseURI).href)));
14765
+ const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DNbKbe23.cjs', document.baseURI).href)));
14507
14766
  const PRISMA_QUERY_ENGINE_FILES = {
14508
14767
  "arm64-darwin": "libquery_engine-darwin-arm64.dylib.node",
14509
14768
  "x64-darwin": "libquery_engine-darwin.dylib.node",
@@ -14925,1269 +15184,1325 @@ async function ensureDaemonRunning() {
14925
15184
  persistence.logger.debug(`Happy background service did not become ready within ${DAEMON_READY_TIMEOUT_MS}ms; continuing anyway`);
14926
15185
  }
14927
15186
 
14928
- function registerAgentAttachmentDownloads(session) {
14929
- session.onFileEvent((fileEvent) => {
14930
- const attachment = fileEvent.content.data.ev;
14931
- const download = (async () => {
14932
- try {
14933
- const data = await session.downloadAndDecryptAttachment(attachment.ref);
14934
- if (!data) throw new Error("Attachment decryption failed");
14935
- return { data, mimeType: attachment.mimeType ?? "image/jpeg", name: attachment.name };
14936
- } catch {
14937
- session.sendSessionEvent({ type: "message", message: `Could not download attachment: ${attachment.name}. Please attach it again.` });
14938
- return null;
14939
- }
14940
- })();
14941
- session.trackAttachmentDownload(download);
14942
- });
15187
+ function getStartedSubagents(state) {
15188
+ return state.startedSubagents ?? /* @__PURE__ */ new Set();
14943
15189
  }
14944
-
14945
- class BasePermissionHandler {
14946
- pendingRequests = /* @__PURE__ */ new Map();
14947
- session;
14948
- isResetting = false;
14949
- constructor(session) {
14950
- this.session = session;
14951
- this.setupRpcHandler();
15190
+ function getActiveSubagents(state) {
15191
+ return state.activeSubagents ?? /* @__PURE__ */ new Set();
15192
+ }
15193
+ function getProviderSubagentToSessionSubagent(state) {
15194
+ return state.providerSubagentToSessionSubagent ?? /* @__PURE__ */ new Map();
15195
+ }
15196
+ function maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes) {
15197
+ if (!subagent || startedSubagents.has(subagent)) {
15198
+ return;
14952
15199
  }
14953
- /**
14954
- * Update the session reference (used after offline reconnection swaps sessions).
14955
- * This is critical for avoiding stale session references after onSessionSwap.
14956
- */
14957
- updateSession(newSession) {
14958
- persistence.logger.debug(`${this.getLogPrefix()} Session reference updated`);
14959
- this.session = newSession;
14960
- this.setupRpcHandler();
15200
+ envelopes.push(api.createEnvelope("agent", { t: "start" }, { ...opts, subagent }));
15201
+ startedSubagents.add(subagent);
15202
+ activeSubagents.add(subagent);
15203
+ }
15204
+ function emitSubagentStops(opts, startedSubagents, activeSubagents) {
15205
+ const envelopes = [];
15206
+ for (const subagent of activeSubagents) {
15207
+ envelopes.push(api.createEnvelope("agent", { t: "stop" }, { ...opts, subagent }));
14961
15208
  }
14962
- /**
14963
- * Setup RPC handler for permission responses.
14964
- */
14965
- setupRpcHandler() {
14966
- this.session.rpcHandlerManager.registerHandler(
14967
- "permission",
14968
- async (response) => {
14969
- const pending = this.pendingRequests.get(response.id);
14970
- if (!pending) {
14971
- persistence.logger.debug(`${this.getLogPrefix()} Permission request not found or already resolved`);
14972
- return;
14973
- }
14974
- this.pendingRequests.delete(response.id);
14975
- const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
14976
- pending.resolve(result);
14977
- this.session.updateAgentState((currentState) => {
14978
- const request = currentState.requests?.[response.id];
14979
- if (!request) return currentState;
14980
- const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
14981
- let res = {
14982
- ...currentState,
14983
- requests: remainingRequests,
14984
- completedRequests: {
14985
- ...currentState.completedRequests,
14986
- [response.id]: {
14987
- ...request,
14988
- completedAt: Date.now(),
14989
- status: response.approved ? "approved" : "denied",
14990
- decision: result.decision
14991
- }
14992
- }
14993
- };
14994
- return res;
14995
- });
14996
- persistence.logger.debug(`${this.getLogPrefix()} Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`);
14997
- }
14998
- );
15209
+ activeSubagents.clear();
15210
+ startedSubagents.clear();
15211
+ return envelopes;
15212
+ }
15213
+ function buildEnvelopeOptions(currentTurnId, subagent) {
15214
+ return {
15215
+ ...currentTurnId ? { turn: currentTurnId } : {},
15216
+ ...subagent ? { subagent } : {}
15217
+ };
15218
+ }
15219
+ function pickProviderSubagent(message) {
15220
+ const candidates = [message.subagent, message.parent_call_id, message.parentCallId];
15221
+ for (const candidate of candidates) {
15222
+ if (typeof candidate === "string" && candidate.length > 0) {
15223
+ return candidate;
15224
+ }
14999
15225
  }
15000
- /**
15001
- * Add a pending request to the agent state.
15002
- */
15003
- addPendingRequestToState(toolCallId, toolName, input) {
15004
- this.session.updateAgentState((currentState) => ({
15005
- ...currentState,
15006
- requests: {
15007
- ...currentState.requests,
15008
- [toolCallId]: {
15009
- tool: toolName,
15010
- arguments: input,
15011
- createdAt: Date.now()
15012
- }
15013
- }
15014
- }));
15226
+ return void 0;
15227
+ }
15228
+ function resolveSessionSubagent(message, providerSubagentToSessionSubagent) {
15229
+ const providerSubagent = pickProviderSubagent(message);
15230
+ if (!providerSubagent) {
15231
+ return void 0;
15015
15232
  }
15016
- /**
15017
- * Abort all pending permission requests.
15018
- * Unlike reset(), this resolves (not rejects) pending promises with { decision: 'abort' },
15019
- * causing the approval response to send 'cancel' to the provider. This is used when the
15020
- * user presses the abort/stop button — it unblocks any pending tool approval so the provider
15021
- * can process the turn cancellation.
15022
- */
15023
- abortAll() {
15024
- const pendingSnapshot = Array.from(this.pendingRequests.entries());
15025
- if (pendingSnapshot.length === 0) return;
15026
- this.pendingRequests.clear();
15027
- for (const [id, pending] of pendingSnapshot) {
15028
- try {
15029
- pending.resolve({ decision: "abort" });
15030
- } catch (err) {
15031
- persistence.logger.debug(`${this.getLogPrefix()} Error resolving aborted request ${id}:`, err);
15032
- }
15033
- }
15034
- this.session.updateAgentState((currentState) => {
15035
- const pendingRequests = currentState.requests || {};
15036
- const completedRequests = { ...currentState.completedRequests };
15037
- for (const [id, request] of Object.entries(pendingRequests)) {
15038
- completedRequests[id] = {
15039
- ...request,
15040
- completedAt: Date.now(),
15041
- status: "canceled",
15042
- reason: "Aborted by user"
15043
- };
15044
- }
15045
- return {
15046
- ...currentState,
15047
- requests: {},
15048
- completedRequests
15049
- };
15050
- });
15051
- persistence.logger.debug(`${this.getLogPrefix()} Aborted ${pendingSnapshot.length} pending permission(s)`);
15052
- }
15053
- /**
15054
- * Reset state for new sessions.
15055
- * This method is idempotent - safe to call multiple times.
15056
- */
15057
- reset(reason = "Session reset") {
15058
- if (this.isResetting) {
15059
- persistence.logger.debug(`${this.getLogPrefix()} Reset already in progress, skipping`);
15060
- return;
15061
- }
15062
- this.isResetting = true;
15063
- try {
15064
- const pendingSnapshot = Array.from(this.pendingRequests.entries());
15065
- this.pendingRequests.clear();
15066
- for (const [id, pending] of pendingSnapshot) {
15067
- try {
15068
- pending.reject(new Error("Session reset"));
15069
- } catch (err) {
15070
- persistence.logger.debug(`${this.getLogPrefix()} Error rejecting pending request ${id}:`, err);
15071
- }
15072
- }
15073
- this.session.updateAgentState((currentState) => {
15074
- const pendingRequests = currentState.requests || {};
15075
- const completedRequests = { ...currentState.completedRequests };
15076
- for (const [id, request] of Object.entries(pendingRequests)) {
15077
- completedRequests[id] = {
15078
- ...request,
15079
- completedAt: Date.now(),
15080
- status: "canceled",
15081
- reason
15082
- };
15083
- }
15084
- return {
15085
- ...currentState,
15086
- requests: {},
15087
- completedRequests
15088
- };
15089
- });
15090
- persistence.logger.debug(`${this.getLogPrefix()} Permission handler reset`);
15091
- } finally {
15092
- this.isResetting = false;
15093
- }
15233
+ const existing = providerSubagentToSessionSubagent.get(providerSubagent);
15234
+ if (existing) {
15235
+ return existing;
15094
15236
  }
15237
+ const created = cuid2.createId();
15238
+ providerSubagentToSessionSubagent.set(providerSubagent, created);
15239
+ return created;
15095
15240
  }
15096
-
15097
- class CodexPermissionHandler extends BasePermissionHandler {
15098
- // Exact tool names that should always be auto-approved. Include the bare
15099
- // form (used by Codex elicitation messages like `tool "change_title"`)
15100
- // and the MCP-qualified form for defense in depth.
15101
- static ALWAYS_AUTO_APPROVE_NAMES = /* @__PURE__ */ new Set([
15102
- "change_title",
15103
- "mcp__happy__change_title"
15104
- ]);
15105
- // Tool-call IDs that should auto-approve when they exactly match one of
15106
- // these values or start with `<name>-` (e.g. `change_title-1765385846663`).
15107
- // Substring matching was a bypass vector — any tool whose ID happened to
15108
- // contain `change_title` as a substring would be silently approved.
15109
- static ALWAYS_AUTO_APPROVE_ID_PREFIXES = [
15110
- "change_title"
15111
- ];
15112
- constructor(session) {
15113
- super(session);
15241
+ function pickCallId(message) {
15242
+ const callId = message.call_id ?? message.callId;
15243
+ if (typeof callId === "string" && callId.length > 0) {
15244
+ return callId;
15114
15245
  }
15115
- getLogPrefix() {
15116
- return "[Codex]";
15246
+ return node_crypto.randomUUID();
15247
+ }
15248
+ function summarizeCommand(command) {
15249
+ if (typeof command === "string" && command.trim().length > 0) {
15250
+ return command;
15117
15251
  }
15118
- shouldAutoApprove(toolName, toolCallId) {
15119
- if (CodexPermissionHandler.ALWAYS_AUTO_APPROVE_NAMES.has(toolName)) {
15120
- return true;
15121
- }
15122
- for (const prefix of CodexPermissionHandler.ALWAYS_AUTO_APPROVE_ID_PREFIXES) {
15123
- if (toolCallId === prefix || toolCallId.startsWith(`${prefix}-`)) {
15124
- return true;
15125
- }
15126
- }
15127
- return false;
15252
+ if (Array.isArray(command)) {
15253
+ const cmd = command.map((v) => String(v)).join(" ").trim();
15254
+ return cmd.length > 0 ? cmd : null;
15128
15255
  }
15129
- /**
15130
- * Handle a tool permission request
15131
- * @param toolCallId - The unique ID of the tool call
15132
- * @param toolName - The name of the tool being called
15133
- * @param input - The input parameters for the tool
15134
- * @returns Promise resolving to permission result
15135
- */
15136
- async handleToolCall(toolCallId, toolName, input) {
15137
- if (this.shouldAutoApprove(toolName, toolCallId)) {
15138
- persistence.logger.debug(`${this.getLogPrefix()} Auto-approving tool ${toolName} (${toolCallId})`);
15139
- this.session.updateAgentState((currentState) => ({
15140
- ...currentState,
15141
- completedRequests: {
15142
- ...currentState.completedRequests,
15143
- [toolCallId]: {
15144
- tool: toolName,
15145
- arguments: input,
15146
- createdAt: Date.now(),
15147
- completedAt: Date.now(),
15148
- status: "approved",
15149
- decision: "approved"
15150
- }
15151
- }
15152
- }));
15153
- return { decision: "approved" };
15154
- }
15155
- return new Promise((resolve, reject) => {
15156
- this.pendingRequests.set(toolCallId, {
15157
- resolve,
15158
- reject,
15159
- toolName,
15160
- input
15161
- });
15162
- this.addPendingRequestToState(toolCallId, toolName, input);
15163
- persistence.logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})`);
15164
- });
15256
+ return null;
15257
+ }
15258
+ function commandToTitle(command) {
15259
+ if (!command) {
15260
+ return "Run command";
15165
15261
  }
15262
+ const short = command.length > 80 ? `${command.slice(0, 77)}...` : command;
15263
+ return `Run \`${short}\``;
15166
15264
  }
15167
-
15168
- class BaseReasoningProcessor {
15169
- accumulator = "";
15170
- inTitleCapture = false;
15171
- titleBuffer = "";
15172
- contentBuffer = "";
15173
- hasTitle = false;
15174
- currentCallId = null;
15175
- toolCallStarted = false;
15176
- currentTitle = null;
15177
- onMessage = null;
15178
- constructor(onMessage) {
15179
- this.onMessage = onMessage || null;
15180
- this.reset();
15265
+ function turnTimestampMs(turn) {
15266
+ const seconds = turn.startedAt ?? turn.completedAt;
15267
+ return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
15268
+ }
15269
+ function completedTimestampMs(turn) {
15270
+ const seconds = turn.completedAt ?? turn.startedAt;
15271
+ return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
15272
+ }
15273
+ function textFromInputItems(items) {
15274
+ if (!Array.isArray(items)) {
15275
+ return null;
15181
15276
  }
15182
- /**
15183
- * Set the message callback for sending messages directly.
15184
- */
15185
- setMessageCallback(callback) {
15186
- this.onMessage = callback;
15277
+ const text = items.filter((item) => Boolean(item) && typeof item === "object" && item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n").trim();
15278
+ return text.length > 0 ? text : null;
15279
+ }
15280
+ function reasoningText(item) {
15281
+ const summary = item.summary;
15282
+ const content = item.content;
15283
+ const parts = [
15284
+ ...Array.isArray(summary) ? summary : [],
15285
+ ...Array.isArray(content) ? content : []
15286
+ ].filter((value) => typeof value === "string" && value.trim().length > 0);
15287
+ const text = parts.join("\n").trim();
15288
+ return text.length > 0 ? text : null;
15289
+ }
15290
+ function turnStatus(turn) {
15291
+ const status = typeof turn.status === "string" ? turn.status : null;
15292
+ if (status === "failed") {
15293
+ return "failed";
15187
15294
  }
15188
- /**
15189
- * Process a reasoning section break - indicates a new reasoning section is starting.
15190
- */
15191
- handleSectionBreak() {
15192
- this.finishCurrentToolCall("canceled");
15193
- this.resetState();
15194
- persistence.logger.debug(`${this.getLogPrefix()} Section break - reset state`);
15295
+ if (status === "cancelled" || status === "canceled" || status === "aborted" || status === "interrupted") {
15296
+ return "cancelled";
15195
15297
  }
15196
- /**
15197
- * Process a reasoning delta/chunk and accumulate content.
15198
- */
15199
- processInput(input) {
15200
- this.accumulator += input;
15201
- if (!this.inTitleCapture && !this.hasTitle && !this.contentBuffer) {
15202
- if (this.accumulator.startsWith("**")) {
15203
- this.inTitleCapture = true;
15204
- this.titleBuffer = this.accumulator.substring(2);
15205
- persistence.logger.debug(`${this.getLogPrefix()} Started title capture`);
15206
- } else if (this.accumulator.length > 0) {
15207
- this.contentBuffer = this.accumulator;
15208
- }
15209
- } else if (this.inTitleCapture) {
15210
- this.titleBuffer = this.accumulator.substring(2);
15211
- const titleEndIndex = this.titleBuffer.indexOf("**");
15212
- if (titleEndIndex !== -1) {
15213
- const title = this.titleBuffer.substring(0, titleEndIndex);
15214
- const afterTitle = this.titleBuffer.substring(titleEndIndex + 2);
15215
- this.hasTitle = true;
15216
- this.inTitleCapture = false;
15217
- this.currentTitle = title;
15218
- this.contentBuffer = afterTitle;
15219
- this.currentCallId = node_crypto.randomUUID();
15220
- persistence.logger.debug(`${this.getLogPrefix()} Title captured: "${title}"`);
15221
- this.sendToolCallStart(title);
15222
- }
15223
- } else if (this.hasTitle) {
15224
- const titleStartIndex = this.accumulator.indexOf("**");
15225
- if (titleStartIndex !== -1) {
15226
- this.contentBuffer = this.accumulator.substring(
15227
- titleStartIndex + 2 + this.currentTitle.length + 2
15228
- );
15229
- }
15230
- } else {
15231
- this.contentBuffer = this.accumulator;
15232
- }
15298
+ return "completed";
15299
+ }
15300
+ function emitHistoricalToolCall(envelopes, turn, item, name, title, args, output) {
15301
+ const time = turnTimestampMs(turn);
15302
+ const opts = { turn: turn.id, time, codexItemId: item.id };
15303
+ envelopes.push(api.createEnvelope("agent", {
15304
+ t: "tool-call-start",
15305
+ call: item.id,
15306
+ name,
15307
+ title,
15308
+ description: title,
15309
+ args
15310
+ }, {
15311
+ ...opts,
15312
+ id: `${item.id}:start`
15313
+ }));
15314
+ if (output && output.trim().length > 0) {
15315
+ envelopes.push(api.createEnvelope("agent", {
15316
+ t: "text",
15317
+ text: output,
15318
+ thinking: true
15319
+ }, {
15320
+ ...opts,
15321
+ id: `${item.id}:output`
15322
+ }));
15233
15323
  }
15234
- /**
15235
- * Send the tool call start message.
15236
- */
15237
- sendToolCallStart(title) {
15238
- if (!this.currentCallId || this.toolCallStarted) {
15239
- return;
15240
- }
15241
- const toolCall = {
15242
- type: "tool-call",
15243
- name: this.getToolName(),
15244
- callId: this.currentCallId,
15245
- input: {
15246
- title
15247
- },
15248
- id: node_crypto.randomUUID()
15249
- };
15250
- persistence.logger.debug(`${this.getLogPrefix()} Sending tool call start for: "${title}"`);
15251
- this.onMessage?.(toolCall);
15252
- this.toolCallStarted = true;
15324
+ if ("status" in item && (item.status === "inProgress" || item.status === "running")) return;
15325
+ envelopes.push(api.createEnvelope("agent", {
15326
+ t: "tool-call-end",
15327
+ call: item.id
15328
+ }, {
15329
+ ...opts,
15330
+ id: `${item.id}:end`,
15331
+ time: completedTimestampMs(turn)
15332
+ }));
15333
+ }
15334
+ function mapCodexCollaborationItem(item, opts = {}, source = "history") {
15335
+ if (item.type === "subAgentActivity" && typeof item.agentThreadId === "string") {
15336
+ item = { ...item, type: "collabAgentToolCall", tool: item.kind, status: "completed", receiverThreadIds: [item.agentThreadId] };
15337
+ }
15338
+ if (item.type !== "collabAgentToolCall" || typeof item.id !== "string" || typeof item.tool !== "string") return [];
15339
+ const args = {
15340
+ operation: item.tool,
15341
+ ...typeof item.prompt === "string" ? { prompt: item.prompt } : {},
15342
+ ...typeof item.model === "string" ? { model: item.model } : {},
15343
+ receiverThreadIds: Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : [],
15344
+ agentsStates: item.agentsStates ?? {}
15345
+ };
15346
+ const startId = source === "live" ? `${item.id}:${item.status ?? "unknown"}:start` : `${item.id}:start`;
15347
+ const envelopes = [api.createEnvelope("agent", {
15348
+ t: "tool-call-start",
15349
+ call: item.id,
15350
+ name: "CodexCollaboration",
15351
+ title: `Codex \xB7 ${item.tool}`,
15352
+ description: `Codex \xB7 ${item.tool}`,
15353
+ args
15354
+ }, { ...opts, id: startId })];
15355
+ if (item.status === "completed" || item.status === "failed" || item.status === "interrupted") {
15356
+ envelopes.push(api.createEnvelope("agent", {
15357
+ t: "tool-call-end",
15358
+ call: item.id,
15359
+ result: { text: JSON.stringify(args, null, 2), isError: item.status !== "completed" }
15360
+ }, { ...opts, id: `${item.id}:end` }));
15253
15361
  }
15254
- /**
15255
- * Complete the reasoning section.
15256
- * Returns true if reasoning was completed, false if there was nothing to complete.
15257
- */
15258
- completeReasoning(fullText) {
15259
- const text = fullText ?? this.accumulator;
15260
- if (!text.trim() && !this.toolCallStarted) {
15261
- persistence.logger.debug(`${this.getLogPrefix()} Complete called but no content accumulated, skipping`);
15262
- return false;
15263
- }
15264
- let title;
15265
- let content = text;
15266
- if (text.startsWith("**")) {
15267
- const titleEndIndex = text.indexOf("**", 2);
15268
- if (titleEndIndex !== -1) {
15269
- title = text.substring(2, titleEndIndex);
15270
- content = text.substring(titleEndIndex + 2).trim();
15362
+ return envelopes;
15363
+ }
15364
+ function mapCodexThreadToSessionEnvelopes(thread) {
15365
+ const envelopes = [];
15366
+ for (const turn of thread.turns ?? []) {
15367
+ const startedAt = turnTimestampMs(turn);
15368
+ const completedAt = completedTimestampMs(turn);
15369
+ envelopes.push(api.createEnvelope("agent", { t: "turn-start" }, {
15370
+ id: `${turn.id}:start`,
15371
+ turn: turn.id,
15372
+ time: startedAt
15373
+ }));
15374
+ for (const item of turn.items ?? []) {
15375
+ switch (item.type) {
15376
+ case "subAgentActivity":
15377
+ case "collabAgentToolCall": {
15378
+ envelopes.push(...mapCodexCollaborationItem(item, { turn: turn.id, time: startedAt, codexItemId: item.id }));
15379
+ break;
15380
+ }
15381
+ case "userMessage": {
15382
+ const text = textFromInputItems(item.content);
15383
+ if (text) {
15384
+ envelopes.push(api.createEnvelope("user", { t: "text", text }, {
15385
+ id: item.id,
15386
+ time: startedAt,
15387
+ codexItemId: item.id
15388
+ }));
15389
+ }
15390
+ break;
15391
+ }
15392
+ case "agentMessage": {
15393
+ const text = typeof item.text === "string" ? item.text.trim() : "";
15394
+ if (text.length > 0) {
15395
+ envelopes.push(api.createEnvelope("agent", { t: "text", text }, {
15396
+ id: item.id,
15397
+ turn: turn.id,
15398
+ time: completedAt,
15399
+ codexItemId: item.id
15400
+ }));
15401
+ }
15402
+ break;
15403
+ }
15404
+ case "reasoning": {
15405
+ const text = reasoningText(item);
15406
+ if (text) {
15407
+ envelopes.push(api.createEnvelope("agent", { t: "text", text, thinking: true }, {
15408
+ id: item.id,
15409
+ turn: turn.id,
15410
+ time: startedAt,
15411
+ codexItemId: item.id
15412
+ }));
15413
+ }
15414
+ break;
15415
+ }
15416
+ case "commandExecution": {
15417
+ const command = typeof item.command === "string" ? item.command : "";
15418
+ emitHistoricalToolCall(
15419
+ envelopes,
15420
+ turn,
15421
+ item,
15422
+ "CodexBash",
15423
+ commandToTitle(command),
15424
+ { command, cwd: item.cwd },
15425
+ typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : null
15426
+ );
15427
+ break;
15428
+ }
15429
+ case "fileChange": {
15430
+ const title = "Apply patch";
15431
+ emitHistoricalToolCall(
15432
+ envelopes,
15433
+ turn,
15434
+ item,
15435
+ "CodexPatch",
15436
+ title,
15437
+ { changes: item.changes, status: item.status },
15438
+ null
15439
+ );
15440
+ break;
15441
+ }
15442
+ case "mcpToolCall": {
15443
+ const title = `${item.server}.${item.tool}`;
15444
+ const output = item.error !== void 0 && item.error !== null ? String(item.error) : item.result !== void 0 && item.result !== null ? String(item.result) : null;
15445
+ emitHistoricalToolCall(
15446
+ envelopes,
15447
+ turn,
15448
+ item,
15449
+ "McpTool",
15450
+ title,
15451
+ {
15452
+ server: item.server,
15453
+ tool: item.tool,
15454
+ arguments: item.arguments
15455
+ },
15456
+ output
15457
+ );
15458
+ break;
15459
+ }
15271
15460
  }
15272
15461
  }
15273
- persistence.logger.debug(`${this.getLogPrefix()} Complete reasoning - Title: "${title}", Has content: ${content.length > 0}`);
15274
- if (title && !this.toolCallStarted) {
15275
- this.currentCallId = this.currentCallId || node_crypto.randomUUID();
15276
- this.sendToolCallStart(title);
15277
- }
15278
- if (this.toolCallStarted && this.currentCallId) {
15279
- const toolResult = {
15280
- type: "tool-call-result",
15281
- callId: this.currentCallId,
15282
- output: {
15283
- content,
15284
- status: "completed"
15285
- },
15286
- id: node_crypto.randomUUID()
15287
- };
15288
- persistence.logger.debug(`${this.getLogPrefix()} Sending tool call result`);
15289
- this.onMessage?.(toolResult);
15290
- } else if (content.trim()) {
15291
- const reasoningMessage = {
15292
- type: "reasoning",
15293
- message: content,
15294
- id: node_crypto.randomUUID()
15295
- };
15296
- persistence.logger.debug(`${this.getLogPrefix()} Sending reasoning message`);
15297
- this.onMessage?.(reasoningMessage);
15298
- }
15299
- this.resetState();
15300
- return true;
15301
- }
15302
- /**
15303
- * Abort the current reasoning section.
15304
- */
15305
- abort() {
15306
- persistence.logger.debug(`${this.getLogPrefix()} Abort called`);
15307
- this.finishCurrentToolCall("canceled");
15308
- this.resetState();
15309
- }
15310
- /**
15311
- * Reset the processor state.
15312
- */
15313
- reset() {
15314
- this.finishCurrentToolCall("canceled");
15315
- this.resetState();
15316
- }
15317
- /**
15318
- * Finish current tool call if one is in progress.
15319
- */
15320
- finishCurrentToolCall(status) {
15321
- if (this.toolCallStarted && this.currentCallId) {
15322
- const toolResult = {
15323
- type: "tool-call-result",
15324
- callId: this.currentCallId,
15325
- output: {
15326
- content: this.contentBuffer || "",
15327
- status
15328
- },
15329
- id: node_crypto.randomUUID()
15330
- };
15331
- persistence.logger.debug(`${this.getLogPrefix()} Sending tool call result with status: ${status}`);
15332
- this.onMessage?.(toolResult);
15333
- }
15334
- }
15335
- /**
15336
- * Reset internal state.
15337
- */
15338
- resetState() {
15339
- this.accumulator = "";
15340
- this.inTitleCapture = false;
15341
- this.titleBuffer = "";
15342
- this.contentBuffer = "";
15343
- this.hasTitle = false;
15344
- this.currentCallId = null;
15345
- this.toolCallStarted = false;
15346
- this.currentTitle = null;
15462
+ envelopes.push(api.createEnvelope("agent", { t: "turn-end", status: turnStatus(turn) }, {
15463
+ id: `${turn.id}:end`,
15464
+ turn: turn.id,
15465
+ time: completedAt
15466
+ }));
15347
15467
  }
15348
- /**
15349
- * Get the current call ID for tool result matching.
15350
- */
15351
- getCurrentCallId() {
15352
- return this.currentCallId;
15468
+ return envelopes;
15469
+ }
15470
+ function patchDescription(changes) {
15471
+ if (!changes || typeof changes !== "object") {
15472
+ return "Applying patch";
15353
15473
  }
15354
- /**
15355
- * Check if a tool call has been started.
15356
- */
15357
- hasStartedToolCall() {
15358
- return this.toolCallStarted;
15474
+ const fileCount = Object.keys(changes).length;
15475
+ if (fileCount === 1) {
15476
+ return "Applying patch to 1 file";
15359
15477
  }
15478
+ return `Applying patch to ${fileCount} files`;
15360
15479
  }
15361
-
15362
- class ReasoningProcessor extends BaseReasoningProcessor {
15363
- getToolName() {
15364
- return "CodexReasoning";
15480
+ function pickTurnEndStatus(message, type) {
15481
+ const rawStatus = message.status;
15482
+ if (rawStatus === "completed" || rawStatus === "failed" || rawStatus === "cancelled") {
15483
+ return rawStatus;
15365
15484
  }
15366
- getLogPrefix() {
15367
- return "[ReasoningProcessor]";
15485
+ if (rawStatus === "canceled") {
15486
+ return "cancelled";
15368
15487
  }
15369
- /**
15370
- * Process a reasoning delta and accumulate content.
15371
- */
15372
- processDelta(delta) {
15373
- this.processInput(delta);
15488
+ if (type === "turn_aborted") {
15489
+ const reason = message.reason;
15490
+ const error = message.error;
15491
+ if (typeof reason === "string" && /(fail|error)/i.test(reason) || typeof error === "string" && error.length > 0 || error !== void 0 && error !== null && typeof error === "object") {
15492
+ return "failed";
15493
+ }
15494
+ return "cancelled";
15374
15495
  }
15375
- /**
15376
- * Complete the reasoning section with final text.
15377
- */
15378
- complete(fullText) {
15379
- this.completeReasoning(fullText);
15496
+ if (message.error !== void 0 && message.error !== null) {
15497
+ return "failed";
15380
15498
  }
15499
+ return "completed";
15381
15500
  }
15382
-
15383
- class DiffProcessor {
15384
- previousDiff = null;
15385
- onMessage = null;
15386
- constructor(onMessage) {
15387
- this.onMessage = onMessage || null;
15501
+ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15502
+ const type = message.type;
15503
+ const startedSubagents = getStartedSubagents(state);
15504
+ const activeSubagents = getActiveSubagents(state);
15505
+ const providerSubagentToSessionSubagent = getProviderSubagentToSessionSubagent(state);
15506
+ if (type === "collab_agent_item") {
15507
+ return {
15508
+ currentTurnId: state.currentTurnId,
15509
+ startedSubagents,
15510
+ activeSubagents,
15511
+ providerSubagentToSessionSubagent,
15512
+ envelopes: mapCodexCollaborationItem(message.item, buildEnvelopeOptions(state.currentTurnId), "live")
15513
+ };
15388
15514
  }
15389
- /**
15390
- * Process a turn_diff message and check if the unified_diff has changed
15391
- */
15392
- processDiff(unifiedDiff) {
15393
- if (this.previousDiff !== unifiedDiff) {
15394
- persistence.logger.debug("[DiffProcessor] Unified diff changed, sending CodexDiff tool call");
15395
- const callId = node_crypto.randomUUID();
15396
- const toolCall = {
15397
- type: "tool-call",
15398
- name: "CodexDiff",
15399
- callId,
15400
- input: {
15401
- unified_diff: unifiedDiff
15402
- },
15403
- id: node_crypto.randomUUID()
15515
+ if (type === "task_started") {
15516
+ const turnId = cuid2.createId();
15517
+ const turnStart = api.createEnvelope("agent", { t: "turn-start" }, { turn: turnId });
15518
+ startedSubagents.clear();
15519
+ activeSubagents.clear();
15520
+ providerSubagentToSessionSubagent.clear();
15521
+ return {
15522
+ currentTurnId: turnId,
15523
+ startedSubagents,
15524
+ activeSubagents,
15525
+ providerSubagentToSessionSubagent,
15526
+ envelopes: [turnStart]
15527
+ };
15528
+ }
15529
+ if (type === "task_complete" || type === "turn_aborted") {
15530
+ if (!state.currentTurnId) {
15531
+ return {
15532
+ currentTurnId: null,
15533
+ startedSubagents,
15534
+ activeSubagents,
15535
+ providerSubagentToSessionSubagent,
15536
+ envelopes: []
15404
15537
  };
15405
- this.onMessage?.(toolCall);
15406
- const toolResult = {
15407
- type: "tool-call-result",
15408
- callId,
15409
- output: {
15410
- status: "completed"
15411
- },
15412
- id: node_crypto.randomUUID()
15538
+ }
15539
+ const lifecycleOpts = { turn: state.currentTurnId };
15540
+ providerSubagentToSessionSubagent.clear();
15541
+ return {
15542
+ currentTurnId: null,
15543
+ startedSubagents,
15544
+ activeSubagents,
15545
+ providerSubagentToSessionSubagent,
15546
+ envelopes: [
15547
+ ...emitSubagentStops(lifecycleOpts, startedSubagents, activeSubagents),
15548
+ api.createEnvelope("agent", {
15549
+ t: "turn-end",
15550
+ status: pickTurnEndStatus(message, type)
15551
+ }, lifecycleOpts)
15552
+ ]
15553
+ };
15554
+ }
15555
+ if (type === "token_count") {
15556
+ return {
15557
+ currentTurnId: state.currentTurnId,
15558
+ startedSubagents,
15559
+ activeSubagents,
15560
+ providerSubagentToSessionSubagent,
15561
+ envelopes: []
15562
+ };
15563
+ }
15564
+ const subagent = resolveSessionSubagent(message, providerSubagentToSessionSubagent);
15565
+ const opts = buildEnvelopeOptions(state.currentTurnId, subagent);
15566
+ if (type === "agent_message") {
15567
+ if (typeof message.message !== "string") {
15568
+ return {
15569
+ currentTurnId: state.currentTurnId,
15570
+ startedSubagents,
15571
+ activeSubagents,
15572
+ providerSubagentToSessionSubagent,
15573
+ envelopes: []
15413
15574
  };
15414
- this.onMessage?.(toolResult);
15415
15575
  }
15416
- this.previousDiff = unifiedDiff;
15417
- persistence.logger.debug("[DiffProcessor] Updated stored diff");
15576
+ const envelopes = [];
15577
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15578
+ envelopes.push(api.createEnvelope("agent", { t: "text", text: message.message }, opts));
15579
+ return {
15580
+ currentTurnId: state.currentTurnId,
15581
+ startedSubagents,
15582
+ activeSubagents,
15583
+ providerSubagentToSessionSubagent,
15584
+ envelopes
15585
+ };
15418
15586
  }
15419
- /**
15420
- * Reset the processor state (called on task_complete or turn_aborted)
15421
- */
15422
- reset() {
15423
- persistence.logger.debug("[DiffProcessor] Resetting diff state");
15424
- this.previousDiff = null;
15587
+ if (type === "agent_reasoning" || type === "agent_reasoning_delta") {
15588
+ const text = typeof message.text === "string" ? message.text : typeof message.delta === "string" ? message.delta : null;
15589
+ if (!text) {
15590
+ return {
15591
+ currentTurnId: state.currentTurnId,
15592
+ startedSubagents,
15593
+ activeSubagents,
15594
+ providerSubagentToSessionSubagent,
15595
+ envelopes: []
15596
+ };
15597
+ }
15598
+ const envelopes = [];
15599
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15600
+ envelopes.push(api.createEnvelope("agent", { t: "text", text, thinking: true }, opts));
15601
+ return {
15602
+ currentTurnId: state.currentTurnId,
15603
+ startedSubagents,
15604
+ activeSubagents,
15605
+ providerSubagentToSessionSubagent,
15606
+ envelopes
15607
+ };
15425
15608
  }
15426
- /**
15427
- * Set the message callback for sending messages directly
15428
- */
15429
- setMessageCallback(callback) {
15430
- this.onMessage = callback;
15609
+ if (type === "exec_command_begin") {
15610
+ const call = pickCallId(message);
15611
+ const { call_id: _callIdSnake, callId: _callIdCamel, type: _type, ...args } = message;
15612
+ const command = summarizeCommand(args.command);
15613
+ const description = typeof args.description === "string" ? args.description : command ?? "Execute command";
15614
+ const envelopes = [];
15615
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15616
+ envelopes.push(
15617
+ api.createEnvelope("agent", {
15618
+ t: "tool-call-start",
15619
+ call,
15620
+ name: "CodexBash",
15621
+ title: commandToTitle(command),
15622
+ description,
15623
+ args
15624
+ }, opts)
15625
+ );
15626
+ return {
15627
+ currentTurnId: state.currentTurnId,
15628
+ startedSubagents,
15629
+ activeSubagents,
15630
+ providerSubagentToSessionSubagent,
15631
+ envelopes
15632
+ };
15431
15633
  }
15432
- /**
15433
- * Get the current diff value
15434
- */
15435
- getCurrentDiff() {
15436
- return this.previousDiff;
15634
+ if (type === "exec_command_end") {
15635
+ const call = pickCallId(message);
15636
+ const envelopes = [];
15637
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15638
+ envelopes.push(api.createEnvelope("agent", { t: "tool-call-end", call }, opts));
15639
+ return {
15640
+ currentTurnId: state.currentTurnId,
15641
+ startedSubagents,
15642
+ activeSubagents,
15643
+ providerSubagentToSessionSubagent,
15644
+ envelopes
15645
+ };
15646
+ }
15647
+ if (type === "patch_apply_begin") {
15648
+ const call = pickCallId(message);
15649
+ const autoApproved = message.auto_approved;
15650
+ const changes = message.changes;
15651
+ const envelopes = [];
15652
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15653
+ envelopes.push(
15654
+ api.createEnvelope("agent", {
15655
+ t: "tool-call-start",
15656
+ call,
15657
+ name: "CodexPatch",
15658
+ title: "Apply patch",
15659
+ description: patchDescription(changes),
15660
+ args: {
15661
+ auto_approved: autoApproved,
15662
+ changes
15663
+ }
15664
+ }, opts)
15665
+ );
15666
+ return {
15667
+ currentTurnId: state.currentTurnId,
15668
+ startedSubagents,
15669
+ activeSubagents,
15670
+ providerSubagentToSessionSubagent,
15671
+ envelopes
15672
+ };
15673
+ }
15674
+ if (type === "patch_apply_end") {
15675
+ const call = pickCallId(message);
15676
+ const envelopes = [];
15677
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15678
+ envelopes.push(api.createEnvelope("agent", { t: "tool-call-end", call }, opts));
15679
+ return {
15680
+ currentTurnId: state.currentTurnId,
15681
+ startedSubagents,
15682
+ activeSubagents,
15683
+ providerSubagentToSessionSubagent,
15684
+ envelopes
15685
+ };
15686
+ }
15687
+ return {
15688
+ currentTurnId: state.currentTurnId,
15689
+ startedSubagents,
15690
+ activeSubagents,
15691
+ providerSubagentToSessionSubagent,
15692
+ envelopes: []
15693
+ };
15694
+ }
15695
+ function mapCodexProcessorMessageToSessionEnvelopes(message, state) {
15696
+ const toolLikeMessage = message;
15697
+ const opts = buildEnvelopeOptions(state.currentTurnId);
15698
+ if (message.type === "reasoning") {
15699
+ return [api.createEnvelope("agent", {
15700
+ t: "text",
15701
+ text: message.message,
15702
+ thinking: true
15703
+ }, opts)];
15704
+ }
15705
+ if (message.type === "tool-call") {
15706
+ const title = typeof toolLikeMessage.input?.title === "string" ? toolLikeMessage.input.title : `${toolLikeMessage.name || "Tool"} call`;
15707
+ return [api.createEnvelope("agent", {
15708
+ t: "tool-call-start",
15709
+ call: toolLikeMessage.callId,
15710
+ name: toolLikeMessage.name || "unknown",
15711
+ title,
15712
+ description: title,
15713
+ args: toolLikeMessage.input && typeof toolLikeMessage.input === "object" ? toolLikeMessage.input : {}
15714
+ }, opts)];
15715
+ }
15716
+ if (message.type === "tool-call-result") {
15717
+ const envelopes = [];
15718
+ const content = toolLikeMessage.output?.content;
15719
+ if (typeof content === "string" && content.trim().length > 0) {
15720
+ envelopes.push(api.createEnvelope("agent", {
15721
+ t: "text",
15722
+ text: content,
15723
+ thinking: true
15724
+ }, opts));
15725
+ }
15726
+ envelopes.push(api.createEnvelope("agent", {
15727
+ t: "tool-call-end",
15728
+ call: toolLikeMessage.callId
15729
+ }, opts));
15730
+ return envelopes;
15437
15731
  }
15732
+ return [];
15438
15733
  }
15439
15734
 
15440
- const CodexDisplay = ({ messageBuffer, logPath, onExit }) => {
15441
- const [messages, setMessages] = React.useState([]);
15442
- const [confirmationMode, setConfirmationMode] = React.useState(false);
15443
- const [actionInProgress, setActionInProgress] = React.useState(false);
15444
- const confirmationTimeoutRef = React.useRef(null);
15445
- const { stdout } = ink.useStdout();
15446
- const terminalWidth = stdout.columns || 80;
15447
- const terminalHeight = stdout.rows || 24;
15448
- React.useEffect(() => {
15449
- setMessages(messageBuffer.getMessages());
15450
- const unsubscribe = messageBuffer.onUpdate((newMessages) => {
15451
- setMessages(newMessages);
15452
- });
15453
- return () => {
15454
- unsubscribe();
15455
- if (confirmationTimeoutRef.current) {
15456
- clearTimeout(confirmationTimeoutRef.current);
15735
+ async function readChildThread(client, childId) {
15736
+ if (!client.threadId || typeof childId !== "string" || !childId) throw new Error("Invalid child thread");
15737
+ const parentId = client.threadId;
15738
+ const { thread: parent } = await client.readThread({ threadId: parentId, includeTurns: true });
15739
+ const belongs = parent.id === parentId && parent.turns?.some((turn) => turn.items.some((item) => item.type === "collabAgentToolCall" && Array.isArray(item.receiverThreadIds) && item.receiverThreadIds.includes(childId) || item.type === "subAgentActivity" && item.agentThreadId === childId));
15740
+ if (!belongs) throw new Error("Thread is not a child of this session");
15741
+ const { thread } = await client.readThread({ threadId: childId, includeTurns: true });
15742
+ if (thread.id !== childId) throw new Error("Child thread mismatch");
15743
+ return { envelopes: mapCodexThreadToSessionEnvelopes(thread) };
15744
+ }
15745
+
15746
+ function registerAgentAttachmentDownloads(session) {
15747
+ session.onFileEvent((fileEvent) => {
15748
+ const attachment = fileEvent.content.data.ev;
15749
+ const download = (async () => {
15750
+ try {
15751
+ const data = await session.downloadAndDecryptAttachment(attachment.ref);
15752
+ if (!data) throw new Error("Attachment decryption failed");
15753
+ return { data, mimeType: attachment.mimeType ?? "image/jpeg", name: attachment.name };
15754
+ } catch {
15755
+ session.sendSessionEvent({ type: "message", message: `Could not download attachment: ${attachment.name}. Please attach it again.` });
15756
+ return null;
15457
15757
  }
15458
- };
15459
- }, [messageBuffer]);
15460
- const resetConfirmation = React.useCallback(() => {
15461
- setConfirmationMode(false);
15462
- if (confirmationTimeoutRef.current) {
15463
- clearTimeout(confirmationTimeoutRef.current);
15464
- confirmationTimeoutRef.current = null;
15465
- }
15466
- }, []);
15467
- const setConfirmationWithTimeout = React.useCallback(() => {
15468
- setConfirmationMode(true);
15469
- if (confirmationTimeoutRef.current) {
15470
- clearTimeout(confirmationTimeoutRef.current);
15471
- }
15472
- confirmationTimeoutRef.current = setTimeout(() => {
15473
- resetConfirmation();
15474
- }, 15e3);
15475
- }, [resetConfirmation]);
15476
- ink.useInput(React.useCallback(async (input, key) => {
15477
- if (actionInProgress) return;
15478
- if (key.ctrl && input === "c") {
15479
- if (confirmationMode) {
15480
- resetConfirmation();
15481
- setActionInProgress(true);
15482
- await new Promise((resolve) => setTimeout(resolve, 100));
15483
- onExit?.();
15484
- } else {
15485
- setConfirmationWithTimeout();
15758
+ })();
15759
+ session.trackAttachmentDownload(download);
15760
+ });
15761
+ }
15762
+
15763
+ class BasePermissionHandler {
15764
+ pendingRequests = /* @__PURE__ */ new Map();
15765
+ session;
15766
+ isResetting = false;
15767
+ constructor(session) {
15768
+ this.session = session;
15769
+ this.setupRpcHandler();
15770
+ }
15771
+ /**
15772
+ * Update the session reference (used after offline reconnection swaps sessions).
15773
+ * This is critical for avoiding stale session references after onSessionSwap.
15774
+ */
15775
+ updateSession(newSession) {
15776
+ persistence.logger.debug(`${this.getLogPrefix()} Session reference updated`);
15777
+ this.session = newSession;
15778
+ this.setupRpcHandler();
15779
+ }
15780
+ /**
15781
+ * Setup RPC handler for permission responses.
15782
+ */
15783
+ setupRpcHandler() {
15784
+ this.session.rpcHandlerManager.registerHandler(
15785
+ "permission",
15786
+ async (response) => {
15787
+ const pending = this.pendingRequests.get(response.id);
15788
+ if (!pending) {
15789
+ persistence.logger.debug(`${this.getLogPrefix()} Permission request not found or already resolved`);
15790
+ return;
15791
+ }
15792
+ this.pendingRequests.delete(response.id);
15793
+ const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
15794
+ pending.resolve(result);
15795
+ this.session.updateAgentState((currentState) => {
15796
+ const request = currentState.requests?.[response.id];
15797
+ if (!request) return currentState;
15798
+ const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
15799
+ let res = {
15800
+ ...currentState,
15801
+ requests: remainingRequests,
15802
+ completedRequests: {
15803
+ ...currentState.completedRequests,
15804
+ [response.id]: {
15805
+ ...request,
15806
+ completedAt: Date.now(),
15807
+ status: response.approved ? "approved" : "denied",
15808
+ decision: result.decision
15809
+ }
15810
+ }
15811
+ };
15812
+ return res;
15813
+ });
15814
+ persistence.logger.debug(`${this.getLogPrefix()} Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`);
15486
15815
  }
15487
- return;
15488
- }
15489
- if (confirmationMode) {
15490
- resetConfirmation();
15491
- }
15492
- }, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
15493
- const getMessageColor = (type) => {
15494
- switch (type) {
15495
- case "user":
15496
- return "magenta";
15497
- case "assistant":
15498
- return "cyan";
15499
- case "system":
15500
- return "blue";
15501
- case "tool":
15502
- return "yellow";
15503
- case "result":
15504
- return "green";
15505
- case "status":
15506
- return "gray";
15507
- default:
15508
- return "white";
15509
- }
15510
- };
15511
- const formatMessage = (msg) => {
15512
- const lines = msg.content.split("\n");
15513
- const maxLineLength = terminalWidth - 10;
15514
- return lines.map((line) => {
15515
- if (line.length <= maxLineLength) return line;
15516
- const chunks = [];
15517
- for (let i = 0; i < line.length; i += maxLineLength) {
15518
- chunks.push(line.slice(i, i + maxLineLength));
15816
+ );
15817
+ }
15818
+ /**
15819
+ * Add a pending request to the agent state.
15820
+ */
15821
+ addPendingRequestToState(toolCallId, toolName, input) {
15822
+ this.session.updateAgentState((currentState) => ({
15823
+ ...currentState,
15824
+ requests: {
15825
+ ...currentState.requests,
15826
+ [toolCallId]: {
15827
+ tool: toolName,
15828
+ arguments: input,
15829
+ createdAt: Date.now()
15830
+ }
15519
15831
  }
15520
- return chunks.join("\n");
15521
- }).join("\n");
15522
- };
15523
- return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
15524
- ink.Box,
15525
- {
15526
- flexDirection: "column",
15527
- width: terminalWidth,
15528
- height: terminalHeight - 4,
15529
- borderStyle: "round",
15530
- borderColor: "gray",
15531
- paddingX: 1,
15532
- overflow: "hidden"
15533
- },
15534
- /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "\u{1F916} Codex Agent Messages"), /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
15535
- /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
15536
- // Show only the last messages that fit in the available space
15537
- messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(ink.Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
15538
- ))
15539
- ), /* @__PURE__ */ React.createElement(
15540
- ink.Box,
15541
- {
15542
- width: terminalWidth,
15543
- borderStyle: "round",
15544
- borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "green",
15545
- paddingX: 2,
15546
- justifyContent: "center",
15547
- alignItems: "center",
15548
- flexDirection: "column"
15549
- },
15550
- /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", alignItems: "center" }, actionInProgress ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Exiting agent...") : confirmationMode ? /* @__PURE__ */ React.createElement(ink.Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit the agent") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Text, { color: "green", bold: true }, "\u{1F916} Codex Agent Running \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
15551
- ));
15552
- };
15553
-
15554
- function createOfflineSessionStub(sessionTag) {
15555
- return {
15556
- sessionId: `offline-${sessionTag}`,
15557
- sendCodexMessage: () => {
15558
- },
15559
- sendAgentMessage: () => {
15560
- },
15561
- sendAgentUsageSnapshot: () => false,
15562
- sendClaudeSessionMessage: () => {
15563
- },
15564
- keepAlive: () => {
15565
- },
15566
- sendSessionEvent: () => {
15567
- },
15568
- sendSessionDeath: () => {
15569
- },
15570
- updateLifecycleState: () => {
15571
- },
15572
- requestControlTransfer: async () => {
15573
- },
15574
- flush: async () => {
15575
- },
15576
- close: async () => {
15577
- },
15578
- updateMetadata: () => {
15579
- },
15580
- updateAgentState: () => {
15581
- },
15582
- onUserMessage: () => {
15583
- },
15584
- onFileEvent: () => {
15585
- },
15586
- trackAttachmentDownload: () => {
15587
- },
15588
- drainAttachmentsForUserMessage: async () => [],
15589
- rpcHandlerManager: {
15590
- registerHandler: () => {
15832
+ }));
15833
+ }
15834
+ /**
15835
+ * Abort all pending permission requests.
15836
+ * Unlike reset(), this resolves (not rejects) pending promises with { decision: 'abort' },
15837
+ * causing the approval response to send 'cancel' to the provider. This is used when the
15838
+ * user presses the abort/stop button — it unblocks any pending tool approval so the provider
15839
+ * can process the turn cancellation.
15840
+ */
15841
+ abortAll() {
15842
+ const pendingSnapshot = Array.from(this.pendingRequests.entries());
15843
+ if (pendingSnapshot.length === 0) return;
15844
+ this.pendingRequests.clear();
15845
+ for (const [id, pending] of pendingSnapshot) {
15846
+ try {
15847
+ pending.resolve({ decision: "abort" });
15848
+ } catch (err) {
15849
+ persistence.logger.debug(`${this.getLogPrefix()} Error resolving aborted request ${id}:`, err);
15591
15850
  }
15592
15851
  }
15593
- };
15594
- }
15595
-
15596
- function setupOfflineReconnection(opts) {
15597
- const { api: api$1, sessionTag, metadata, state, response, onSessionSwap } = opts;
15598
- let session;
15599
- let reconnectionHandle = null;
15600
- if (!response) {
15601
- session = createOfflineSessionStub(sessionTag);
15602
- reconnectionHandle = api.startOfflineReconnection({
15603
- serverUrl: persistence.configuration.serverUrl,
15604
- onReconnected: async () => {
15605
- const resp = await api$1.getOrCreateSession({ tag: sessionTag, metadata, state });
15606
- if (!resp) throw new Error("Server unavailable");
15607
- const realSession = api$1.sessionSyncClient(resp);
15608
- onSessionSwap(realSession);
15609
- return realSession;
15610
- },
15611
- onNotify: (msg) => {
15612
- console.log(msg);
15852
+ this.session.updateAgentState((currentState) => {
15853
+ const pendingRequests = currentState.requests || {};
15854
+ const completedRequests = { ...currentState.completedRequests };
15855
+ for (const [id, request] of Object.entries(pendingRequests)) {
15856
+ completedRequests[id] = {
15857
+ ...request,
15858
+ completedAt: Date.now(),
15859
+ status: "canceled",
15860
+ reason: "Aborted by user"
15861
+ };
15613
15862
  }
15863
+ return {
15864
+ ...currentState,
15865
+ requests: {},
15866
+ completedRequests
15867
+ };
15614
15868
  });
15615
- return { session, reconnectionHandle, isOffline: true };
15616
- } else {
15617
- session = api$1.sessionSyncClient(response, opts.sessionClientOptions);
15618
- return { session, reconnectionHandle: null, isOffline: false };
15619
- }
15620
- }
15621
-
15622
- function resolveCodexExecutionPolicy(permissionMode, sandboxManagedByHappy) {
15623
- if (sandboxManagedByHappy) {
15624
- return {
15625
- approvalPolicy: "never",
15626
- sandbox: "danger-full-access"
15627
- };
15869
+ persistence.logger.debug(`${this.getLogPrefix()} Aborted ${pendingSnapshot.length} pending permission(s)`);
15628
15870
  }
15629
- const approvalPolicy = (() => {
15630
- switch (permissionMode) {
15631
- // Codex native modes
15632
- case "default":
15633
- return "untrusted";
15634
- // Ask for non-trusted commands
15635
- case "read-only":
15636
- return "never";
15637
- // Never ask, read-only enforced by sandbox
15638
- case "safe-yolo":
15639
- return "on-request";
15640
- // Workspace auto-run; request approval for escalation (0.153 removed on-failure)
15641
- case "yolo":
15642
- return "never";
15643
- // Full YOLO: never interrupt for approvals
15644
- // Defensive fallback for Claude-specific modes (backward compatibility)
15645
- case "bypassPermissions":
15646
- return "never";
15647
- // Full access: map to yolo behavior
15648
- case "acceptEdits":
15649
- return "on-request";
15650
- // Let model decide (closest to auto-approve edits)
15651
- case "plan":
15652
- return "untrusted";
15653
- // Conservative: ask for non-trusted
15654
- default:
15655
- return "untrusted";
15871
+ /**
15872
+ * Reset state for new sessions.
15873
+ * This method is idempotent - safe to call multiple times.
15874
+ */
15875
+ reset(reason = "Session reset") {
15876
+ if (this.isResetting) {
15877
+ persistence.logger.debug(`${this.getLogPrefix()} Reset already in progress, skipping`);
15878
+ return;
15656
15879
  }
15657
- })();
15658
- const sandbox = (() => {
15659
- switch (permissionMode) {
15660
- // Codex native modes
15661
- case "default":
15662
- return "workspace-write";
15663
- // Can write in workspace
15664
- case "read-only":
15665
- return "read-only";
15666
- // Read-only filesystem
15667
- case "safe-yolo":
15668
- return "workspace-write";
15669
- // Can write in workspace
15670
- case "yolo":
15671
- return "danger-full-access";
15672
- // Full system access
15673
- // Defensive fallback for Claude-specific modes
15674
- case "bypassPermissions":
15675
- return "danger-full-access";
15676
- // Full access: map to yolo
15677
- case "acceptEdits":
15678
- return "workspace-write";
15679
- // Can edit files in workspace
15680
- case "plan":
15681
- return "workspace-write";
15682
- // Can write for planning
15683
- default:
15684
- return "workspace-write";
15880
+ this.isResetting = true;
15881
+ try {
15882
+ const pendingSnapshot = Array.from(this.pendingRequests.entries());
15883
+ this.pendingRequests.clear();
15884
+ for (const [id, pending] of pendingSnapshot) {
15885
+ try {
15886
+ pending.reject(new Error("Session reset"));
15887
+ } catch (err) {
15888
+ persistence.logger.debug(`${this.getLogPrefix()} Error rejecting pending request ${id}:`, err);
15889
+ }
15890
+ }
15891
+ this.session.updateAgentState((currentState) => {
15892
+ const pendingRequests = currentState.requests || {};
15893
+ const completedRequests = { ...currentState.completedRequests };
15894
+ for (const [id, request] of Object.entries(pendingRequests)) {
15895
+ completedRequests[id] = {
15896
+ ...request,
15897
+ completedAt: Date.now(),
15898
+ status: "canceled",
15899
+ reason
15900
+ };
15901
+ }
15902
+ return {
15903
+ ...currentState,
15904
+ requests: {},
15905
+ completedRequests
15906
+ };
15907
+ });
15908
+ persistence.logger.debug(`${this.getLogPrefix()} Permission handler reset`);
15909
+ } finally {
15910
+ this.isResetting = false;
15685
15911
  }
15686
- })();
15687
- return { approvalPolicy, sandbox };
15912
+ }
15688
15913
  }
15689
15914
 
15690
- function getStartedSubagents(state) {
15691
- return state.startedSubagents ?? /* @__PURE__ */ new Set();
15692
- }
15693
- function getActiveSubagents(state) {
15694
- return state.activeSubagents ?? /* @__PURE__ */ new Set();
15695
- }
15696
- function getProviderSubagentToSessionSubagent(state) {
15697
- return state.providerSubagentToSessionSubagent ?? /* @__PURE__ */ new Map();
15698
- }
15699
- function maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes) {
15700
- if (!subagent || startedSubagents.has(subagent)) {
15701
- return;
15915
+ class CodexPermissionHandler extends BasePermissionHandler {
15916
+ // Exact tool names that should always be auto-approved. Include the bare
15917
+ // form (used by Codex elicitation messages like `tool "change_title"`)
15918
+ // and the MCP-qualified form for defense in depth.
15919
+ static ALWAYS_AUTO_APPROVE_NAMES = /* @__PURE__ */ new Set([
15920
+ "change_title",
15921
+ "mcp__happy__change_title"
15922
+ ]);
15923
+ // Tool-call IDs that should auto-approve when they exactly match one of
15924
+ // these values or start with `<name>-` (e.g. `change_title-1765385846663`).
15925
+ // Substring matching was a bypass vector — any tool whose ID happened to
15926
+ // contain `change_title` as a substring would be silently approved.
15927
+ static ALWAYS_AUTO_APPROVE_ID_PREFIXES = [
15928
+ "change_title"
15929
+ ];
15930
+ constructor(session) {
15931
+ super(session);
15702
15932
  }
15703
- envelopes.push(api.createEnvelope("agent", { t: "start" }, { ...opts, subagent }));
15704
- startedSubagents.add(subagent);
15705
- activeSubagents.add(subagent);
15706
- }
15707
- function emitSubagentStops(opts, startedSubagents, activeSubagents) {
15708
- const envelopes = [];
15709
- for (const subagent of activeSubagents) {
15710
- envelopes.push(api.createEnvelope("agent", { t: "stop" }, { ...opts, subagent }));
15933
+ getLogPrefix() {
15934
+ return "[Codex]";
15711
15935
  }
15712
- activeSubagents.clear();
15713
- startedSubagents.clear();
15714
- return envelopes;
15715
- }
15716
- function buildEnvelopeOptions(currentTurnId, subagent) {
15717
- return {
15718
- ...currentTurnId ? { turn: currentTurnId } : {},
15719
- ...subagent ? { subagent } : {}
15720
- };
15721
- }
15722
- function pickProviderSubagent(message) {
15723
- const candidates = [message.subagent, message.parent_call_id, message.parentCallId];
15724
- for (const candidate of candidates) {
15725
- if (typeof candidate === "string" && candidate.length > 0) {
15726
- return candidate;
15936
+ shouldAutoApprove(toolName, toolCallId) {
15937
+ if (CodexPermissionHandler.ALWAYS_AUTO_APPROVE_NAMES.has(toolName)) {
15938
+ return true;
15727
15939
  }
15940
+ for (const prefix of CodexPermissionHandler.ALWAYS_AUTO_APPROVE_ID_PREFIXES) {
15941
+ if (toolCallId === prefix || toolCallId.startsWith(`${prefix}-`)) {
15942
+ return true;
15943
+ }
15944
+ }
15945
+ return false;
15728
15946
  }
15729
- return void 0;
15730
- }
15731
- function resolveSessionSubagent(message, providerSubagentToSessionSubagent) {
15732
- const providerSubagent = pickProviderSubagent(message);
15733
- if (!providerSubagent) {
15734
- return void 0;
15735
- }
15736
- const existing = providerSubagentToSessionSubagent.get(providerSubagent);
15737
- if (existing) {
15738
- return existing;
15947
+ /**
15948
+ * Handle a tool permission request
15949
+ * @param toolCallId - The unique ID of the tool call
15950
+ * @param toolName - The name of the tool being called
15951
+ * @param input - The input parameters for the tool
15952
+ * @returns Promise resolving to permission result
15953
+ */
15954
+ async handleToolCall(toolCallId, toolName, input) {
15955
+ if (this.shouldAutoApprove(toolName, toolCallId)) {
15956
+ persistence.logger.debug(`${this.getLogPrefix()} Auto-approving tool ${toolName} (${toolCallId})`);
15957
+ this.session.updateAgentState((currentState) => ({
15958
+ ...currentState,
15959
+ completedRequests: {
15960
+ ...currentState.completedRequests,
15961
+ [toolCallId]: {
15962
+ tool: toolName,
15963
+ arguments: input,
15964
+ createdAt: Date.now(),
15965
+ completedAt: Date.now(),
15966
+ status: "approved",
15967
+ decision: "approved"
15968
+ }
15969
+ }
15970
+ }));
15971
+ return { decision: "approved" };
15972
+ }
15973
+ return new Promise((resolve, reject) => {
15974
+ this.pendingRequests.set(toolCallId, {
15975
+ resolve,
15976
+ reject,
15977
+ toolName,
15978
+ input
15979
+ });
15980
+ this.addPendingRequestToState(toolCallId, toolName, input);
15981
+ persistence.logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})`);
15982
+ });
15739
15983
  }
15740
- const created = cuid2.createId();
15741
- providerSubagentToSessionSubagent.set(providerSubagent, created);
15742
- return created;
15743
15984
  }
15744
- function pickCallId(message) {
15745
- const callId = message.call_id ?? message.callId;
15746
- if (typeof callId === "string" && callId.length > 0) {
15747
- return callId;
15985
+
15986
+ class BaseReasoningProcessor {
15987
+ accumulator = "";
15988
+ inTitleCapture = false;
15989
+ titleBuffer = "";
15990
+ contentBuffer = "";
15991
+ hasTitle = false;
15992
+ currentCallId = null;
15993
+ toolCallStarted = false;
15994
+ currentTitle = null;
15995
+ onMessage = null;
15996
+ constructor(onMessage) {
15997
+ this.onMessage = onMessage || null;
15998
+ this.reset();
15748
15999
  }
15749
- return node_crypto.randomUUID();
15750
- }
15751
- function summarizeCommand(command) {
15752
- if (typeof command === "string" && command.trim().length > 0) {
15753
- return command;
16000
+ /**
16001
+ * Set the message callback for sending messages directly.
16002
+ */
16003
+ setMessageCallback(callback) {
16004
+ this.onMessage = callback;
15754
16005
  }
15755
- if (Array.isArray(command)) {
15756
- const cmd = command.map((v) => String(v)).join(" ").trim();
15757
- return cmd.length > 0 ? cmd : null;
16006
+ /**
16007
+ * Process a reasoning section break - indicates a new reasoning section is starting.
16008
+ */
16009
+ handleSectionBreak() {
16010
+ this.finishCurrentToolCall("canceled");
16011
+ this.resetState();
16012
+ persistence.logger.debug(`${this.getLogPrefix()} Section break - reset state`);
15758
16013
  }
15759
- return null;
15760
- }
15761
- function commandToTitle(command) {
15762
- if (!command) {
15763
- return "Run command";
16014
+ /**
16015
+ * Process a reasoning delta/chunk and accumulate content.
16016
+ */
16017
+ processInput(input) {
16018
+ this.accumulator += input;
16019
+ if (!this.inTitleCapture && !this.hasTitle && !this.contentBuffer) {
16020
+ if (this.accumulator.startsWith("**")) {
16021
+ this.inTitleCapture = true;
16022
+ this.titleBuffer = this.accumulator.substring(2);
16023
+ persistence.logger.debug(`${this.getLogPrefix()} Started title capture`);
16024
+ } else if (this.accumulator.length > 0) {
16025
+ this.contentBuffer = this.accumulator;
16026
+ }
16027
+ } else if (this.inTitleCapture) {
16028
+ this.titleBuffer = this.accumulator.substring(2);
16029
+ const titleEndIndex = this.titleBuffer.indexOf("**");
16030
+ if (titleEndIndex !== -1) {
16031
+ const title = this.titleBuffer.substring(0, titleEndIndex);
16032
+ const afterTitle = this.titleBuffer.substring(titleEndIndex + 2);
16033
+ this.hasTitle = true;
16034
+ this.inTitleCapture = false;
16035
+ this.currentTitle = title;
16036
+ this.contentBuffer = afterTitle;
16037
+ this.currentCallId = node_crypto.randomUUID();
16038
+ persistence.logger.debug(`${this.getLogPrefix()} Title captured: "${title}"`);
16039
+ this.sendToolCallStart(title);
16040
+ }
16041
+ } else if (this.hasTitle) {
16042
+ const titleStartIndex = this.accumulator.indexOf("**");
16043
+ if (titleStartIndex !== -1) {
16044
+ this.contentBuffer = this.accumulator.substring(
16045
+ titleStartIndex + 2 + this.currentTitle.length + 2
16046
+ );
16047
+ }
16048
+ } else {
16049
+ this.contentBuffer = this.accumulator;
16050
+ }
15764
16051
  }
15765
- const short = command.length > 80 ? `${command.slice(0, 77)}...` : command;
15766
- return `Run \`${short}\``;
15767
- }
15768
- function turnTimestampMs(turn) {
15769
- const seconds = turn.startedAt ?? turn.completedAt;
15770
- return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
15771
- }
15772
- function completedTimestampMs(turn) {
15773
- const seconds = turn.completedAt ?? turn.startedAt;
15774
- return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
15775
- }
15776
- function textFromInputItems(items) {
15777
- if (!Array.isArray(items)) {
15778
- return null;
16052
+ /**
16053
+ * Send the tool call start message.
16054
+ */
16055
+ sendToolCallStart(title) {
16056
+ if (!this.currentCallId || this.toolCallStarted) {
16057
+ return;
16058
+ }
16059
+ const toolCall = {
16060
+ type: "tool-call",
16061
+ name: this.getToolName(),
16062
+ callId: this.currentCallId,
16063
+ input: {
16064
+ title
16065
+ },
16066
+ id: node_crypto.randomUUID()
16067
+ };
16068
+ persistence.logger.debug(`${this.getLogPrefix()} Sending tool call start for: "${title}"`);
16069
+ this.onMessage?.(toolCall);
16070
+ this.toolCallStarted = true;
15779
16071
  }
15780
- const text = items.filter((item) => Boolean(item) && typeof item === "object" && item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n").trim();
15781
- return text.length > 0 ? text : null;
15782
- }
15783
- function reasoningText(item) {
15784
- const summary = item.summary;
15785
- const content = item.content;
15786
- const parts = [
15787
- ...Array.isArray(summary) ? summary : [],
15788
- ...Array.isArray(content) ? content : []
15789
- ].filter((value) => typeof value === "string" && value.trim().length > 0);
15790
- const text = parts.join("\n").trim();
15791
- return text.length > 0 ? text : null;
15792
- }
15793
- function turnStatus(turn) {
15794
- const status = typeof turn.status === "string" ? turn.status : null;
15795
- if (status === "failed") {
15796
- return "failed";
16072
+ /**
16073
+ * Complete the reasoning section.
16074
+ * Returns true if reasoning was completed, false if there was nothing to complete.
16075
+ */
16076
+ completeReasoning(fullText) {
16077
+ const text = fullText ?? this.accumulator;
16078
+ if (!text.trim() && !this.toolCallStarted) {
16079
+ persistence.logger.debug(`${this.getLogPrefix()} Complete called but no content accumulated, skipping`);
16080
+ return false;
16081
+ }
16082
+ let title;
16083
+ let content = text;
16084
+ if (text.startsWith("**")) {
16085
+ const titleEndIndex = text.indexOf("**", 2);
16086
+ if (titleEndIndex !== -1) {
16087
+ title = text.substring(2, titleEndIndex);
16088
+ content = text.substring(titleEndIndex + 2).trim();
16089
+ }
16090
+ }
16091
+ persistence.logger.debug(`${this.getLogPrefix()} Complete reasoning - Title: "${title}", Has content: ${content.length > 0}`);
16092
+ if (title && !this.toolCallStarted) {
16093
+ this.currentCallId = this.currentCallId || node_crypto.randomUUID();
16094
+ this.sendToolCallStart(title);
16095
+ }
16096
+ if (this.toolCallStarted && this.currentCallId) {
16097
+ const toolResult = {
16098
+ type: "tool-call-result",
16099
+ callId: this.currentCallId,
16100
+ output: {
16101
+ content,
16102
+ status: "completed"
16103
+ },
16104
+ id: node_crypto.randomUUID()
16105
+ };
16106
+ persistence.logger.debug(`${this.getLogPrefix()} Sending tool call result`);
16107
+ this.onMessage?.(toolResult);
16108
+ } else if (content.trim()) {
16109
+ const reasoningMessage = {
16110
+ type: "reasoning",
16111
+ message: content,
16112
+ id: node_crypto.randomUUID()
16113
+ };
16114
+ persistence.logger.debug(`${this.getLogPrefix()} Sending reasoning message`);
16115
+ this.onMessage?.(reasoningMessage);
16116
+ }
16117
+ this.resetState();
16118
+ return true;
15797
16119
  }
15798
- if (status === "cancelled" || status === "canceled" || status === "aborted" || status === "interrupted") {
15799
- return "cancelled";
16120
+ /**
16121
+ * Abort the current reasoning section.
16122
+ */
16123
+ abort() {
16124
+ persistence.logger.debug(`${this.getLogPrefix()} Abort called`);
16125
+ this.finishCurrentToolCall("canceled");
16126
+ this.resetState();
15800
16127
  }
15801
- return "completed";
15802
- }
15803
- function emitHistoricalToolCall(envelopes, turn, item, name, title, args, output) {
15804
- const time = turnTimestampMs(turn);
15805
- const opts = { turn: turn.id, time, codexItemId: item.id };
15806
- envelopes.push(api.createEnvelope("agent", {
15807
- t: "tool-call-start",
15808
- call: item.id,
15809
- name,
15810
- title,
15811
- description: title,
15812
- args
15813
- }, {
15814
- ...opts,
15815
- id: `${item.id}:start`
15816
- }));
15817
- if (output && output.trim().length > 0) {
15818
- envelopes.push(api.createEnvelope("agent", {
15819
- t: "text",
15820
- text: output,
15821
- thinking: true
15822
- }, {
15823
- ...opts,
15824
- id: `${item.id}:output`
15825
- }));
16128
+ /**
16129
+ * Reset the processor state.
16130
+ */
16131
+ reset() {
16132
+ this.finishCurrentToolCall("canceled");
16133
+ this.resetState();
15826
16134
  }
15827
- envelopes.push(api.createEnvelope("agent", {
15828
- t: "tool-call-end",
15829
- call: item.id
15830
- }, {
15831
- ...opts,
15832
- id: `${item.id}:end`,
15833
- time: completedTimestampMs(turn)
15834
- }));
15835
- }
15836
- function mapCodexThreadToSessionEnvelopes(thread) {
15837
- const envelopes = [];
15838
- for (const turn of thread.turns ?? []) {
15839
- const startedAt = turnTimestampMs(turn);
15840
- const completedAt = completedTimestampMs(turn);
15841
- envelopes.push(api.createEnvelope("agent", { t: "turn-start" }, {
15842
- id: `${turn.id}:start`,
15843
- turn: turn.id,
15844
- time: startedAt
15845
- }));
15846
- for (const item of turn.items ?? []) {
15847
- switch (item.type) {
15848
- case "userMessage": {
15849
- const text = textFromInputItems(item.content);
15850
- if (text) {
15851
- envelopes.push(api.createEnvelope("user", { t: "text", text }, {
15852
- id: item.id,
15853
- time: startedAt,
15854
- codexItemId: item.id
15855
- }));
15856
- }
15857
- break;
15858
- }
15859
- case "agentMessage": {
15860
- const text = typeof item.text === "string" ? item.text.trim() : "";
15861
- if (text.length > 0) {
15862
- envelopes.push(api.createEnvelope("agent", { t: "text", text }, {
15863
- id: item.id,
15864
- turn: turn.id,
15865
- time: completedAt,
15866
- codexItemId: item.id
15867
- }));
15868
- }
15869
- break;
15870
- }
15871
- case "reasoning": {
15872
- const text = reasoningText(item);
15873
- if (text) {
15874
- envelopes.push(api.createEnvelope("agent", { t: "text", text, thinking: true }, {
15875
- id: item.id,
15876
- turn: turn.id,
15877
- time: startedAt,
15878
- codexItemId: item.id
15879
- }));
15880
- }
15881
- break;
15882
- }
15883
- case "commandExecution": {
15884
- const command = typeof item.command === "string" ? item.command : "";
15885
- emitHistoricalToolCall(
15886
- envelopes,
15887
- turn,
15888
- item,
15889
- "CodexBash",
15890
- commandToTitle(command),
15891
- { command, cwd: item.cwd },
15892
- typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : null
15893
- );
15894
- break;
15895
- }
15896
- case "fileChange": {
15897
- const title = "Apply patch";
15898
- emitHistoricalToolCall(
15899
- envelopes,
15900
- turn,
15901
- item,
15902
- "CodexPatch",
15903
- title,
15904
- { changes: item.changes, status: item.status },
15905
- null
15906
- );
15907
- break;
15908
- }
15909
- case "mcpToolCall": {
15910
- const title = `${item.server}.${item.tool}`;
15911
- const output = item.error !== void 0 && item.error !== null ? String(item.error) : item.result !== void 0 && item.result !== null ? String(item.result) : null;
15912
- emitHistoricalToolCall(
15913
- envelopes,
15914
- turn,
15915
- item,
15916
- "McpTool",
15917
- title,
15918
- {
15919
- server: item.server,
15920
- tool: item.tool,
15921
- arguments: item.arguments
15922
- },
15923
- output
15924
- );
15925
- break;
15926
- }
15927
- }
16135
+ /**
16136
+ * Finish current tool call if one is in progress.
16137
+ */
16138
+ finishCurrentToolCall(status) {
16139
+ if (this.toolCallStarted && this.currentCallId) {
16140
+ const toolResult = {
16141
+ type: "tool-call-result",
16142
+ callId: this.currentCallId,
16143
+ output: {
16144
+ content: this.contentBuffer || "",
16145
+ status
16146
+ },
16147
+ id: node_crypto.randomUUID()
16148
+ };
16149
+ persistence.logger.debug(`${this.getLogPrefix()} Sending tool call result with status: ${status}`);
16150
+ this.onMessage?.(toolResult);
15928
16151
  }
15929
- envelopes.push(api.createEnvelope("agent", { t: "turn-end", status: turnStatus(turn) }, {
15930
- id: `${turn.id}:end`,
15931
- turn: turn.id,
15932
- time: completedAt
15933
- }));
15934
16152
  }
15935
- return envelopes;
15936
- }
15937
- function patchDescription(changes) {
15938
- if (!changes || typeof changes !== "object") {
15939
- return "Applying patch";
16153
+ /**
16154
+ * Reset internal state.
16155
+ */
16156
+ resetState() {
16157
+ this.accumulator = "";
16158
+ this.inTitleCapture = false;
16159
+ this.titleBuffer = "";
16160
+ this.contentBuffer = "";
16161
+ this.hasTitle = false;
16162
+ this.currentCallId = null;
16163
+ this.toolCallStarted = false;
16164
+ this.currentTitle = null;
15940
16165
  }
15941
- const fileCount = Object.keys(changes).length;
15942
- if (fileCount === 1) {
15943
- return "Applying patch to 1 file";
16166
+ /**
16167
+ * Get the current call ID for tool result matching.
16168
+ */
16169
+ getCurrentCallId() {
16170
+ return this.currentCallId;
16171
+ }
16172
+ /**
16173
+ * Check if a tool call has been started.
16174
+ */
16175
+ hasStartedToolCall() {
16176
+ return this.toolCallStarted;
15944
16177
  }
15945
- return `Applying patch to ${fileCount} files`;
15946
16178
  }
15947
- function pickTurnEndStatus(message, type) {
15948
- const rawStatus = message.status;
15949
- if (rawStatus === "completed" || rawStatus === "failed" || rawStatus === "cancelled") {
15950
- return rawStatus;
16179
+
16180
+ class ReasoningProcessor extends BaseReasoningProcessor {
16181
+ getToolName() {
16182
+ return "CodexReasoning";
15951
16183
  }
15952
- if (rawStatus === "canceled") {
15953
- return "cancelled";
16184
+ getLogPrefix() {
16185
+ return "[ReasoningProcessor]";
15954
16186
  }
15955
- if (type === "turn_aborted") {
15956
- const reason = message.reason;
15957
- const error = message.error;
15958
- if (typeof reason === "string" && /(fail|error)/i.test(reason) || typeof error === "string" && error.length > 0 || error !== void 0 && error !== null && typeof error === "object") {
15959
- return "failed";
15960
- }
15961
- return "cancelled";
16187
+ /**
16188
+ * Process a reasoning delta and accumulate content.
16189
+ */
16190
+ processDelta(delta) {
16191
+ this.processInput(delta);
15962
16192
  }
15963
- if (message.error !== void 0 && message.error !== null) {
15964
- return "failed";
16193
+ /**
16194
+ * Complete the reasoning section with final text.
16195
+ */
16196
+ complete(fullText) {
16197
+ this.completeReasoning(fullText);
15965
16198
  }
15966
- return "completed";
15967
16199
  }
15968
- function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15969
- const type = message.type;
15970
- const startedSubagents = getStartedSubagents(state);
15971
- const activeSubagents = getActiveSubagents(state);
15972
- const providerSubagentToSessionSubagent = getProviderSubagentToSessionSubagent(state);
15973
- if (type === "task_started") {
15974
- const turnId = cuid2.createId();
15975
- const turnStart = api.createEnvelope("agent", { t: "turn-start" }, { turn: turnId });
15976
- startedSubagents.clear();
15977
- activeSubagents.clear();
15978
- providerSubagentToSessionSubagent.clear();
15979
- return {
15980
- currentTurnId: turnId,
15981
- startedSubagents,
15982
- activeSubagents,
15983
- providerSubagentToSessionSubagent,
15984
- envelopes: [turnStart]
15985
- };
15986
- }
15987
- if (type === "task_complete" || type === "turn_aborted") {
15988
- if (!state.currentTurnId) {
15989
- return {
15990
- currentTurnId: null,
15991
- startedSubagents,
15992
- activeSubagents,
15993
- providerSubagentToSessionSubagent,
15994
- envelopes: []
15995
- };
15996
- }
15997
- const lifecycleOpts = { turn: state.currentTurnId };
15998
- providerSubagentToSessionSubagent.clear();
15999
- return {
16000
- currentTurnId: null,
16001
- startedSubagents,
16002
- activeSubagents,
16003
- providerSubagentToSessionSubagent,
16004
- envelopes: [
16005
- ...emitSubagentStops(lifecycleOpts, startedSubagents, activeSubagents),
16006
- api.createEnvelope("agent", {
16007
- t: "turn-end",
16008
- status: pickTurnEndStatus(message, type)
16009
- }, lifecycleOpts)
16010
- ]
16011
- };
16012
- }
16013
- if (type === "token_count") {
16014
- return {
16015
- currentTurnId: state.currentTurnId,
16016
- startedSubagents,
16017
- activeSubagents,
16018
- providerSubagentToSessionSubagent,
16019
- envelopes: []
16020
- };
16200
+
16201
+ class DiffProcessor {
16202
+ previousDiff = null;
16203
+ onMessage = null;
16204
+ constructor(onMessage) {
16205
+ this.onMessage = onMessage || null;
16021
16206
  }
16022
- const subagent = resolveSessionSubagent(message, providerSubagentToSessionSubagent);
16023
- const opts = buildEnvelopeOptions(state.currentTurnId, subagent);
16024
- if (type === "agent_message") {
16025
- if (typeof message.message !== "string") {
16026
- return {
16027
- currentTurnId: state.currentTurnId,
16028
- startedSubagents,
16029
- activeSubagents,
16030
- providerSubagentToSessionSubagent,
16031
- envelopes: []
16207
+ /**
16208
+ * Process a turn_diff message and check if the unified_diff has changed
16209
+ */
16210
+ processDiff(unifiedDiff) {
16211
+ if (this.previousDiff !== unifiedDiff) {
16212
+ persistence.logger.debug("[DiffProcessor] Unified diff changed, sending CodexDiff tool call");
16213
+ const callId = node_crypto.randomUUID();
16214
+ const toolCall = {
16215
+ type: "tool-call",
16216
+ name: "CodexDiff",
16217
+ callId,
16218
+ input: {
16219
+ unified_diff: unifiedDiff
16220
+ },
16221
+ id: node_crypto.randomUUID()
16032
16222
  };
16033
- }
16034
- const envelopes = [];
16035
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16036
- envelopes.push(api.createEnvelope("agent", { t: "text", text: message.message }, opts));
16037
- return {
16038
- currentTurnId: state.currentTurnId,
16039
- startedSubagents,
16040
- activeSubagents,
16041
- providerSubagentToSessionSubagent,
16042
- envelopes
16043
- };
16044
- }
16045
- if (type === "agent_reasoning" || type === "agent_reasoning_delta") {
16046
- const text = typeof message.text === "string" ? message.text : typeof message.delta === "string" ? message.delta : null;
16047
- if (!text) {
16048
- return {
16049
- currentTurnId: state.currentTurnId,
16050
- startedSubagents,
16051
- activeSubagents,
16052
- providerSubagentToSessionSubagent,
16053
- envelopes: []
16223
+ this.onMessage?.(toolCall);
16224
+ const toolResult = {
16225
+ type: "tool-call-result",
16226
+ callId,
16227
+ output: {
16228
+ status: "completed"
16229
+ },
16230
+ id: node_crypto.randomUUID()
16054
16231
  };
16232
+ this.onMessage?.(toolResult);
16055
16233
  }
16056
- const envelopes = [];
16057
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16058
- envelopes.push(api.createEnvelope("agent", { t: "text", text, thinking: true }, opts));
16059
- return {
16060
- currentTurnId: state.currentTurnId,
16061
- startedSubagents,
16062
- activeSubagents,
16063
- providerSubagentToSessionSubagent,
16064
- envelopes
16065
- };
16234
+ this.previousDiff = unifiedDiff;
16235
+ persistence.logger.debug("[DiffProcessor] Updated stored diff");
16066
16236
  }
16067
- if (type === "exec_command_begin") {
16068
- const call = pickCallId(message);
16069
- const { call_id: _callIdSnake, callId: _callIdCamel, type: _type, ...args } = message;
16070
- const command = summarizeCommand(args.command);
16071
- const description = typeof args.description === "string" ? args.description : command ?? "Execute command";
16072
- const envelopes = [];
16073
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16074
- envelopes.push(
16075
- api.createEnvelope("agent", {
16076
- t: "tool-call-start",
16077
- call,
16078
- name: "CodexBash",
16079
- title: commandToTitle(command),
16080
- description,
16081
- args
16082
- }, opts)
16083
- );
16084
- return {
16085
- currentTurnId: state.currentTurnId,
16086
- startedSubagents,
16087
- activeSubagents,
16088
- providerSubagentToSessionSubagent,
16089
- envelopes
16090
- };
16237
+ /**
16238
+ * Reset the processor state (called on task_complete or turn_aborted)
16239
+ */
16240
+ reset() {
16241
+ persistence.logger.debug("[DiffProcessor] Resetting diff state");
16242
+ this.previousDiff = null;
16091
16243
  }
16092
- if (type === "exec_command_end") {
16093
- const call = pickCallId(message);
16094
- const envelopes = [];
16095
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16096
- envelopes.push(api.createEnvelope("agent", { t: "tool-call-end", call }, opts));
16097
- return {
16098
- currentTurnId: state.currentTurnId,
16099
- startedSubagents,
16100
- activeSubagents,
16101
- providerSubagentToSessionSubagent,
16102
- envelopes
16103
- };
16244
+ /**
16245
+ * Set the message callback for sending messages directly
16246
+ */
16247
+ setMessageCallback(callback) {
16248
+ this.onMessage = callback;
16104
16249
  }
16105
- if (type === "patch_apply_begin") {
16106
- const call = pickCallId(message);
16107
- const autoApproved = message.auto_approved;
16108
- const changes = message.changes;
16109
- const envelopes = [];
16110
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16111
- envelopes.push(
16112
- api.createEnvelope("agent", {
16113
- t: "tool-call-start",
16114
- call,
16115
- name: "CodexPatch",
16116
- title: "Apply patch",
16117
- description: patchDescription(changes),
16118
- args: {
16119
- auto_approved: autoApproved,
16120
- changes
16121
- }
16122
- }, opts)
16123
- );
16124
- return {
16125
- currentTurnId: state.currentTurnId,
16126
- startedSubagents,
16127
- activeSubagents,
16128
- providerSubagentToSessionSubagent,
16129
- envelopes
16130
- };
16250
+ /**
16251
+ * Get the current diff value
16252
+ */
16253
+ getCurrentDiff() {
16254
+ return this.previousDiff;
16131
16255
  }
16132
- if (type === "patch_apply_end") {
16133
- const call = pickCallId(message);
16134
- const envelopes = [];
16135
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16136
- envelopes.push(api.createEnvelope("agent", { t: "tool-call-end", call }, opts));
16137
- return {
16138
- currentTurnId: state.currentTurnId,
16139
- startedSubagents,
16140
- activeSubagents,
16141
- providerSubagentToSessionSubagent,
16142
- envelopes
16256
+ }
16257
+
16258
+ const CodexDisplay = ({ messageBuffer, logPath, onExit }) => {
16259
+ const [messages, setMessages] = React.useState([]);
16260
+ const [confirmationMode, setConfirmationMode] = React.useState(false);
16261
+ const [actionInProgress, setActionInProgress] = React.useState(false);
16262
+ const confirmationTimeoutRef = React.useRef(null);
16263
+ const { stdout } = ink.useStdout();
16264
+ const terminalWidth = stdout.columns || 80;
16265
+ const terminalHeight = stdout.rows || 24;
16266
+ React.useEffect(() => {
16267
+ setMessages(messageBuffer.getMessages());
16268
+ const unsubscribe = messageBuffer.onUpdate((newMessages) => {
16269
+ setMessages(newMessages);
16270
+ });
16271
+ return () => {
16272
+ unsubscribe();
16273
+ if (confirmationTimeoutRef.current) {
16274
+ clearTimeout(confirmationTimeoutRef.current);
16275
+ }
16143
16276
  };
16144
- }
16277
+ }, [messageBuffer]);
16278
+ const resetConfirmation = React.useCallback(() => {
16279
+ setConfirmationMode(false);
16280
+ if (confirmationTimeoutRef.current) {
16281
+ clearTimeout(confirmationTimeoutRef.current);
16282
+ confirmationTimeoutRef.current = null;
16283
+ }
16284
+ }, []);
16285
+ const setConfirmationWithTimeout = React.useCallback(() => {
16286
+ setConfirmationMode(true);
16287
+ if (confirmationTimeoutRef.current) {
16288
+ clearTimeout(confirmationTimeoutRef.current);
16289
+ }
16290
+ confirmationTimeoutRef.current = setTimeout(() => {
16291
+ resetConfirmation();
16292
+ }, 15e3);
16293
+ }, [resetConfirmation]);
16294
+ ink.useInput(React.useCallback(async (input, key) => {
16295
+ if (actionInProgress) return;
16296
+ if (key.ctrl && input === "c") {
16297
+ if (confirmationMode) {
16298
+ resetConfirmation();
16299
+ setActionInProgress(true);
16300
+ await new Promise((resolve) => setTimeout(resolve, 100));
16301
+ onExit?.();
16302
+ } else {
16303
+ setConfirmationWithTimeout();
16304
+ }
16305
+ return;
16306
+ }
16307
+ if (confirmationMode) {
16308
+ resetConfirmation();
16309
+ }
16310
+ }, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
16311
+ const getMessageColor = (type) => {
16312
+ switch (type) {
16313
+ case "user":
16314
+ return "magenta";
16315
+ case "assistant":
16316
+ return "cyan";
16317
+ case "system":
16318
+ return "blue";
16319
+ case "tool":
16320
+ return "yellow";
16321
+ case "result":
16322
+ return "green";
16323
+ case "status":
16324
+ return "gray";
16325
+ default:
16326
+ return "white";
16327
+ }
16328
+ };
16329
+ const formatMessage = (msg) => {
16330
+ const lines = msg.content.split("\n");
16331
+ const maxLineLength = terminalWidth - 10;
16332
+ return lines.map((line) => {
16333
+ if (line.length <= maxLineLength) return line;
16334
+ const chunks = [];
16335
+ for (let i = 0; i < line.length; i += maxLineLength) {
16336
+ chunks.push(line.slice(i, i + maxLineLength));
16337
+ }
16338
+ return chunks.join("\n");
16339
+ }).join("\n");
16340
+ };
16341
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
16342
+ ink.Box,
16343
+ {
16344
+ flexDirection: "column",
16345
+ width: terminalWidth,
16346
+ height: terminalHeight - 4,
16347
+ borderStyle: "round",
16348
+ borderColor: "gray",
16349
+ paddingX: 1,
16350
+ overflow: "hidden"
16351
+ },
16352
+ /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "\u{1F916} Codex Agent Messages"), /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
16353
+ /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
16354
+ // Show only the last messages that fit in the available space
16355
+ messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(ink.Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
16356
+ ))
16357
+ ), /* @__PURE__ */ React.createElement(
16358
+ ink.Box,
16359
+ {
16360
+ width: terminalWidth,
16361
+ borderStyle: "round",
16362
+ borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "green",
16363
+ paddingX: 2,
16364
+ justifyContent: "center",
16365
+ alignItems: "center",
16366
+ flexDirection: "column"
16367
+ },
16368
+ /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", alignItems: "center" }, actionInProgress ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Exiting agent...") : confirmationMode ? /* @__PURE__ */ React.createElement(ink.Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit the agent") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Text, { color: "green", bold: true }, "\u{1F916} Codex Agent Running \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
16369
+ ));
16370
+ };
16371
+
16372
+ function createOfflineSessionStub(sessionTag) {
16145
16373
  return {
16146
- currentTurnId: state.currentTurnId,
16147
- startedSubagents,
16148
- activeSubagents,
16149
- providerSubagentToSessionSubagent,
16150
- envelopes: []
16374
+ sessionId: `offline-${sessionTag}`,
16375
+ sendCodexMessage: () => {
16376
+ },
16377
+ sendAgentMessage: () => {
16378
+ },
16379
+ sendAgentUsageSnapshot: () => false,
16380
+ sendClaudeSessionMessage: () => {
16381
+ },
16382
+ keepAlive: () => {
16383
+ },
16384
+ sendSessionEvent: () => {
16385
+ },
16386
+ sendSessionDeath: () => {
16387
+ },
16388
+ updateLifecycleState: () => {
16389
+ },
16390
+ requestControlTransfer: async () => {
16391
+ },
16392
+ flush: async () => {
16393
+ },
16394
+ close: async () => {
16395
+ },
16396
+ updateMetadata: () => {
16397
+ },
16398
+ updateAgentState: () => {
16399
+ },
16400
+ onUserMessage: () => {
16401
+ },
16402
+ onFileEvent: () => {
16403
+ },
16404
+ trackAttachmentDownload: () => {
16405
+ },
16406
+ drainAttachmentsForUserMessage: async () => [],
16407
+ rpcHandlerManager: {
16408
+ registerHandler: () => {
16409
+ }
16410
+ }
16151
16411
  };
16152
16412
  }
16153
- function mapCodexProcessorMessageToSessionEnvelopes(message, state) {
16154
- const toolLikeMessage = message;
16155
- const opts = buildEnvelopeOptions(state.currentTurnId);
16156
- if (message.type === "reasoning") {
16157
- return [api.createEnvelope("agent", {
16158
- t: "text",
16159
- text: message.message,
16160
- thinking: true
16161
- }, opts)];
16413
+
16414
+ function setupOfflineReconnection(opts) {
16415
+ const { api: api$1, sessionTag, metadata, state, response, onSessionSwap } = opts;
16416
+ let session;
16417
+ let reconnectionHandle = null;
16418
+ if (!response) {
16419
+ session = createOfflineSessionStub(sessionTag);
16420
+ reconnectionHandle = api.startOfflineReconnection({
16421
+ serverUrl: persistence.configuration.serverUrl,
16422
+ onReconnected: async () => {
16423
+ const resp = await api$1.getOrCreateSession({ tag: sessionTag, metadata, state });
16424
+ if (!resp) throw new Error("Server unavailable");
16425
+ const realSession = api$1.sessionSyncClient(resp);
16426
+ onSessionSwap(realSession);
16427
+ return realSession;
16428
+ },
16429
+ onNotify: (msg) => {
16430
+ console.log(msg);
16431
+ }
16432
+ });
16433
+ return { session, reconnectionHandle, isOffline: true };
16434
+ } else {
16435
+ session = api$1.sessionSyncClient(response, opts.sessionClientOptions);
16436
+ return { session, reconnectionHandle: null, isOffline: false };
16162
16437
  }
16163
- if (message.type === "tool-call") {
16164
- const title = typeof toolLikeMessage.input?.title === "string" ? toolLikeMessage.input.title : `${toolLikeMessage.name || "Tool"} call`;
16165
- return [api.createEnvelope("agent", {
16166
- t: "tool-call-start",
16167
- call: toolLikeMessage.callId,
16168
- name: toolLikeMessage.name || "unknown",
16169
- title,
16170
- description: title,
16171
- args: toolLikeMessage.input && typeof toolLikeMessage.input === "object" ? toolLikeMessage.input : {}
16172
- }, opts)];
16438
+ }
16439
+
16440
+ function resolveCodexExecutionPolicy(permissionMode, sandboxManagedByHappy) {
16441
+ if (sandboxManagedByHappy) {
16442
+ return {
16443
+ approvalPolicy: "never",
16444
+ sandbox: "danger-full-access"
16445
+ };
16173
16446
  }
16174
- if (message.type === "tool-call-result") {
16175
- const envelopes = [];
16176
- const content = toolLikeMessage.output?.content;
16177
- if (typeof content === "string" && content.trim().length > 0) {
16178
- envelopes.push(api.createEnvelope("agent", {
16179
- t: "text",
16180
- text: content,
16181
- thinking: true
16182
- }, opts));
16447
+ const approvalPolicy = (() => {
16448
+ switch (permissionMode) {
16449
+ // Codex native modes
16450
+ case "default":
16451
+ return "untrusted";
16452
+ // Ask for non-trusted commands
16453
+ case "read-only":
16454
+ return "never";
16455
+ // Never ask, read-only enforced by sandbox
16456
+ case "safe-yolo":
16457
+ return "on-request";
16458
+ // Workspace auto-run; request approval for escalation (0.153 removed on-failure)
16459
+ case "yolo":
16460
+ return "never";
16461
+ // Full YOLO: never interrupt for approvals
16462
+ // Defensive fallback for Claude-specific modes (backward compatibility)
16463
+ case "bypassPermissions":
16464
+ return "never";
16465
+ // Full access: map to yolo behavior
16466
+ case "acceptEdits":
16467
+ return "on-request";
16468
+ // Let model decide (closest to auto-approve edits)
16469
+ case "plan":
16470
+ return "untrusted";
16471
+ // Conservative: ask for non-trusted
16472
+ default:
16473
+ return "untrusted";
16183
16474
  }
16184
- envelopes.push(api.createEnvelope("agent", {
16185
- t: "tool-call-end",
16186
- call: toolLikeMessage.callId
16187
- }, opts));
16188
- return envelopes;
16189
- }
16190
- return [];
16475
+ })();
16476
+ const sandbox = (() => {
16477
+ switch (permissionMode) {
16478
+ // Codex native modes
16479
+ case "default":
16480
+ return "workspace-write";
16481
+ // Can write in workspace
16482
+ case "read-only":
16483
+ return "read-only";
16484
+ // Read-only filesystem
16485
+ case "safe-yolo":
16486
+ return "workspace-write";
16487
+ // Can write in workspace
16488
+ case "yolo":
16489
+ return "danger-full-access";
16490
+ // Full system access
16491
+ // Defensive fallback for Claude-specific modes
16492
+ case "bypassPermissions":
16493
+ return "danger-full-access";
16494
+ // Full access: map to yolo
16495
+ case "acceptEdits":
16496
+ return "workspace-write";
16497
+ // Can edit files in workspace
16498
+ case "plan":
16499
+ return "workspace-write";
16500
+ // Can write for planning
16501
+ default:
16502
+ return "workspace-write";
16503
+ }
16504
+ })();
16505
+ return { approvalPolicy, sandbox };
16191
16506
  }
16192
16507
 
16193
16508
  async function resumeExistingThread(opts) {
@@ -16659,6 +16974,13 @@ async function runCodex(opts) {
16659
16974
  process.exit(1);
16660
16975
  }
16661
16976
  };
16977
+ session.rpcHandlerManager.registerHandler("codex-child-read", async (args) => {
16978
+ try {
16979
+ return await readChildThread(client, args?.threadId);
16980
+ } catch (error) {
16981
+ return { error: error instanceof Error ? error.message : "Child read failed" };
16982
+ }
16983
+ });
16662
16984
  session.rpcHandlerManager.registerHandler("abort", handleAbort);
16663
16985
  const disposeTermination = registerKillSessionHandler(session.rpcHandlerManager, handleKillSession, session, { processSignals: true });
16664
16986
  const messageBuffer = new MessageBuffer();
@@ -17199,7 +17521,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17199
17521
  process.exit(0);
17200
17522
  } else if (subcommand === "install-terminal-hooks") {
17201
17523
  try {
17202
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-DPmEaHg2.cjs'); });
17524
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-gmWKACNb.cjs'); });
17203
17525
  const command = parseTerminalHooksArgs(args.slice(1));
17204
17526
  if (command.action === "help") {
17205
17527
  console.log(TERMINAL_HOOKS_HELP);
@@ -17216,7 +17538,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17216
17538
  }
17217
17539
  } else if (subcommand === "spawn") {
17218
17540
  try {
17219
- const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-gha6nGx-.cjs'); });
17541
+ const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-DVJtNwWN.cjs'); });
17220
17542
  await handleSpawnCommand(args.slice(1));
17221
17543
  } catch (error) {
17222
17544
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -17228,7 +17550,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17228
17550
  return;
17229
17551
  } else if (subcommand === "sessions") {
17230
17552
  try {
17231
- const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-DyC5Attg.cjs'); });
17553
+ const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-Cx_3cvN6.cjs'); });
17232
17554
  await handleSessionsCommand(args.slice(1));
17233
17555
  } catch (error) {
17234
17556
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -17240,7 +17562,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17240
17562
  return;
17241
17563
  } else if (subcommand === "send") {
17242
17564
  try {
17243
- const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-Bo6iKuXr.cjs'); });
17565
+ const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-WlJ4r3U2.cjs'); });
17244
17566
  await handleSendCommand(args.slice(1));
17245
17567
  } catch (error) {
17246
17568
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -17346,9 +17668,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17346
17668
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
17347
17669
  const projectId = args[3];
17348
17670
  try {
17349
- const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-C3e90AIH.cjs'); });
17350
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./limits-D6kiAfXJ.cjs'); }).then(function (n) { return n.persistence; });
17351
- const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-Dabm17Ln.cjs'); }).then(function (n) { return n.api; });
17671
+ const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-BO0ZHyoB.cjs'); });
17672
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./limits-K17AgMHD.cjs'); }).then(function (n) { return n.persistence; });
17673
+ const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-DjAHWb4J.cjs'); }).then(function (n) { return n.api; });
17352
17674
  let userEmail = void 0;
17353
17675
  try {
17354
17676
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -17379,7 +17701,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17379
17701
  }
17380
17702
  if (geminiSubcommand === "project" && args[2] === "get") {
17381
17703
  try {
17382
- const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-C3e90AIH.cjs'); });
17704
+ const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-BO0ZHyoB.cjs'); });
17383
17705
  const config = readGeminiLocalConfig();
17384
17706
  if (config.googleCloudProject) {
17385
17707
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -17419,7 +17741,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17419
17741
  process.exit(0);
17420
17742
  }
17421
17743
  try {
17422
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-BPPnulIk.cjs'); });
17744
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-n0wD2Z9R.cjs'); });
17423
17745
  let startedBy = void 0;
17424
17746
  for (let i = 1; i < args.length; i++) {
17425
17747
  if (args[i] === "--started-by") {
@@ -17449,7 +17771,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17449
17771
  }
17450
17772
  };
17451
17773
  try {
17452
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-jqG8CV0c.cjs'); });
17774
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-BEpZjXs4.cjs'); });
17453
17775
  const parsed = parsePiRunnerArgs(args.slice(1));
17454
17776
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
17455
17777
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -17477,7 +17799,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17477
17799
  return;
17478
17800
  } else if (subcommand === "acp") {
17479
17801
  try {
17480
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-jqG8CV0c.cjs'); });
17802
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-BEpZjXs4.cjs'); });
17481
17803
  let startedBy = void 0;
17482
17804
  let verbose = false;
17483
17805
  const acpArgs = [];
@@ -17517,7 +17839,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17517
17839
  return;
17518
17840
  } else if (subcommand === "openclaw") {
17519
17841
  try {
17520
- const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-BB3Ivt1S.cjs'); });
17842
+ const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-fYx1XweW.cjs'); });
17521
17843
  let startedBy = void 0;
17522
17844
  let verbose = false;
17523
17845
  let gatewayUrl;
@@ -17568,7 +17890,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17568
17890
  return;
17569
17891
  } else if (subcommand === "todo") {
17570
17892
  try {
17571
- const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-BaNaNZ8c.cjs'); });
17893
+ const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-DHY20tpD.cjs'); });
17572
17894
  await handleTodoCommand(args.slice(1));
17573
17895
  } catch (error) {
17574
17896
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -17577,7 +17899,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17577
17899
  return;
17578
17900
  } else if (subcommand === "teams") {
17579
17901
  try {
17580
- const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-BbzWNV9e.cjs'); });
17902
+ const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-BbYi0fWG.cjs'); });
17581
17903
  await handleTeamsCommand(args.slice(1));
17582
17904
  } catch (error) {
17583
17905
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -17586,7 +17908,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17586
17908
  return;
17587
17909
  } else if (subcommand === "mcp" && args.length === 1) {
17588
17910
  try {
17589
- const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-DGZmUpuZ.cjs'); });
17911
+ const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-CK1NCZm0.cjs'); });
17590
17912
  await handleMcpCommand();
17591
17913
  } catch (error) {
17592
17914
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}