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
@@ -1,13 +1,13 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
2
2
  import os, { homedir, userInfo, tmpdir } from 'node:os';
3
- import { randomUUID, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
4
- import { j as getProjectPath, g as BUILTIN_TODO_DISCOVERY, p as projectPath, k as initializeSandbox, w as wrapCommand, d as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, h as delay, l as AsyncLock, S as STREAM_DELTA_MAX_CHARS, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, m as commandOnPath, n as deriveLocalCliUpdateSummary, o as isClaudeSessionId, q as getLocalHappyAgentCredentialPath, r as readLocalHappyAgentCredentials, t as parseExactVersion, u as compareExactVersions, v as extractCompleteLines, x as decideBackfill, y as parseTerminalHookPayload, z as tmuxArgs, C as scrubTmuxClientEnv, D as decideMirrorBinding, E as MIRROR_BACKFILL_LINES_DEFAULT, F as mirrorLineKey, G as mirrorLocalId, H as detectCLIAvailability, J as detectResumeSupport, K as summarizeSpawnSessionForLog, L as shellescape, A as ApiClient, N as fetchCliUpdateState, O as resolveCliUpdateCheckInterval, P as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, i as isValidSessionId, Q as listVhTerminals, T as isValidTerminalId, U as readVhTerminal, V as sendToVhTerminal, W as ContextUsageSchema, X as TITLE_PROMPT_PREFIX, Y as BOARD_ANALYZER_PROMPT_PREFIX, f as connectionState, Z as startOfflineReconnection, c as createEnvelope, _ as CodexAppServerClient } from './types-Cq-tvv9t.mjs';
5
- import { z as readDaemonState, l as logger, c as configuration, A as clearDaemonState, B as ensurePrivateDirectory, D as hardenPrivateFile, p as packageJson, g as readSettings, E as readCredentials, s as credentialRelayProblem, i as encodeBase64, F as readSettingsOutcome, G as updateSettings, H as encodeBase64Url, m as decodeBase64, I as writeCredentialsLegacy, J as writeCredentialsDataKey, d as readCredentialsForConfiguredRelay, j as encrypt, r as readPersistedSessions, w as writePrivateFileSync, K as decryptWithDataKey, L as decryptLegacy, M as persistSession, k as decrypt, f as ensurePrivateDirectorySync, N as acquireDaemonLock, O as deletePersistedSessions, P as writeDaemonState, Q as writeSettings, R as releaseDaemonLock, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, e as CLIPBOARD_MAX_BYTES, S as clearCredentials, T as clearMachineId, U as SandboxConfigSchema, V as getLatestDaemonLog } from './limits-BGeIBBE3.mjs';
3
+ import { randomUUID, randomInt, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
4
+ import { j as getProjectPath, g as BUILTIN_TODO_DISCOVERY, p as projectPath, k as initializeSandbox, w as wrapCommand, d as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, h as delay, l as AsyncLock, S as STREAM_DELTA_MAX_CHARS, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, m as commandOnPath, n as deriveLocalCliUpdateSummary, o as AGENT_VERSION_SOURCES, q as isClaudeSessionId, r as getLocalHappyAgentCredentialPath, t as readLocalHappyAgentCredentials, u as parseExactVersion, v as compareExactVersions, x as extractCompleteLines, y as decideBackfill, z as parseTerminalHookPayload, C as tmuxArgs, D as scrubTmuxClientEnv, E as decideMirrorBinding, F as MIRROR_BACKFILL_LINES_DEFAULT, G as mirrorLineKey, H as mirrorLocalId, J as detectCLIAvailability, K as detectResumeSupport, L as summarizeSpawnSessionForLog, N as shellescape, A as ApiClient, O as fetchCliUpdateState, P as resolveCliUpdateCheckInterval, Q as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, i as isValidSessionId, T as listVhTerminals, U as isValidTerminalId, V as readVhTerminal, W as sendToVhTerminal, X as ContextUsageSchema, Y as TITLE_PROMPT_PREFIX, Z as BOARD_ANALYZER_PROMPT_PREFIX, f as connectionState, _ as startOfflineReconnection, c as createEnvelope, $ as CodexAppServerClient } from './types-BEXGahmj.mjs';
5
+ import { z as readDaemonState, l as logger, c as configuration, A as clearDaemonState, B as ensurePrivateDirectory, D as hardenPrivateFile, p as packageJson, g as readSettings, E as readCredentials, s as credentialRelayProblem, i as encodeBase64, F as readSettingsOutcome, G as updateSettings, H as encodeBase64Url, m as decodeBase64, I as writeCredentialsLegacy, J as writeCredentialsDataKey, d as readCredentialsForConfiguredRelay, j as encrypt, r as readPersistedSessions, w as writePrivateFileSync, K as decryptWithDataKey, L as decryptLegacy, M as persistSession, k as decrypt, f as ensurePrivateDirectorySync, N as acquireDaemonLock, O as deletePersistedSessions, P as writeDaemonState, Q as writeSettings, R as releaseDaemonLock, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, e as CLIPBOARD_MAX_BYTES, S as clearCredentials, T as clearMachineId, U as SandboxConfigSchema, V as getLatestDaemonLog } from './limits-CPTgiQ9A.mjs';
6
6
  import spawn$1, { spawn } from 'cross-spawn';
7
7
  import path, { join, resolve, basename, isAbsolute, dirname, sep, delimiter, normalize } from 'node:path';
8
8
  import { createInterface } from 'node:readline';
9
9
  import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, renameSync, writeFileSync, accessSync, constants, openSync, fsyncSync, closeSync, rmSync as rmSync$1, chmodSync as chmodSync$1 } from 'node:fs';
10
- import { f as trimIdent, A as AGENT_GUIDANCE, t as teamScopePath, T as TeamConnectionPreflightError, e as createTeamsClient, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, g as TEAM_TOOL_NAMES, r as registerTeamsTools, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, h as REPORT_PROGRESS_TOOL_TITLE, i as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-CPG5Cr5T.mjs';
10
+ import { f as trimIdent, A as AGENT_GUIDANCE, t as teamScopePath, T as TeamConnectionPreflightError, e as createTeamsClient, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, g as TEAM_TOOL_NAMES, r as registerTeamsTools, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, h as REPORT_PROGRESS_TOOL_TITLE, i as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-7MByWxKj.mjs';
11
11
  import { readFile, writeFile, rename, unlink, mkdir, symlink, rm, readlink, lstat, stat, open as open$1, appendFile } from 'node:fs/promises';
12
12
  import fs, { watch, access } from 'fs/promises';
13
13
  import { useStdout, useInput, Box, Text, render } from 'ink';
@@ -20,7 +20,7 @@ import tweetnacl from 'tweetnacl';
20
20
  import os$1, { homedir as homedir$1 } from 'os';
21
21
  import { spawn as spawn$2, execSync as execSync$1, exec as exec$1 } from 'child_process';
22
22
  import { createId } from '@paralleldrive/cuid2';
23
- import { execSync, spawnSync, spawn as spawn$3, execFile, execFileSync } from 'node:child_process';
23
+ import { execSync, spawnSync, execFile, spawn as spawn$3, execFileSync } from 'node:child_process';
24
24
  import { existsSync as existsSync$1, statSync as statSync$1, mkdtempSync, rmSync, writeFileSync as writeFileSync$1, chmodSync, unlinkSync as unlinkSync$1 } from 'fs';
25
25
  import { join as join$1 } from 'path';
26
26
  import '@anthropic-ai/sandbox-runtime';
@@ -1331,6 +1331,152 @@ async function claudeLocalLauncher(session) {
1331
1331
  return exitReason || { type: "exit", code: 0 };
1332
1332
  }
1333
1333
 
1334
+ const text = (value) => typeof value === "string" && value.length > 0 && value.length <= 1024 && !value.includes("\0") ? value : null;
1335
+ const publicMcpStatus = (servers) => servers.map(({ name, status, serverInfo, scope, tools }) => ({ name, status, serverInfo, scope, tools: tools?.map(({ name: name2 }) => ({ name: name2 })) }));
1336
+ const fingerprint = (result) => JSON.stringify([result.canRewind, [...result.filesChanged ?? []].sort(), result.insertions, result.deletions]);
1337
+ function isRuntimeCheckpointReplay(message) {
1338
+ const m = message;
1339
+ 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"));
1340
+ }
1341
+ function createRuntimeControls(isThinking) {
1342
+ let query = null, generation = randomInt(0, 2 ** 40), activityRevision = 0;
1343
+ const operations = /* @__PURE__ */ new Map();
1344
+ const confirmations = /* @__PURE__ */ new Map();
1345
+ const tasks = /* @__PURE__ */ new Map();
1346
+ const checkpoints = /* @__PURE__ */ new Map();
1347
+ let pending = false;
1348
+ const setQuery = (next) => {
1349
+ for (const operation of operations.values()) if (operation.status === "running") {
1350
+ operation.status = "failed";
1351
+ operation.error = "Claude query ended before this operation completed";
1352
+ }
1353
+ query = next;
1354
+ generation++;
1355
+ pending = false;
1356
+ confirmations.clear();
1357
+ tasks.clear();
1358
+ checkpoints.clear();
1359
+ };
1360
+ function observe(message) {
1361
+ const m = message;
1362
+ if (m.type === "user" || m.type === "assistant") {
1363
+ activityRevision++;
1364
+ confirmations.clear();
1365
+ }
1366
+ 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(" ") : "";
1367
+ if (isRuntimeCheckpointReplay(message) && text(m.uuid) && content) {
1368
+ checkpoints.set(m.uuid, { id: m.uuid, title: content.slice(0, 120) });
1369
+ if (checkpoints.size > 100) checkpoints.delete(checkpoints.keys().next().value);
1370
+ }
1371
+ if (m.type === "system" && text(m.task_id) && ["task_started", "task_updated", "task_progress", "task_notification"].includes(m.subtype)) {
1372
+ const previous = tasks.get(m.task_id);
1373
+ if (m.subtype !== "task_notification" && ["completed", "failed", "stopped", "cancelled"].includes(String(previous?.status))) return;
1374
+ tasks.set(m.task_id, {
1375
+ ...previous,
1376
+ id: m.task_id,
1377
+ ...text(m.tool_use_id) ? { toolUseId: m.tool_use_id } : {},
1378
+ ...text(m.description) ? { description: m.description } : {},
1379
+ ...text(m.task_type) ? { type: m.task_type } : {},
1380
+ status: m.subtype === "task_notification" ? m.status ?? "completed" : "running"
1381
+ });
1382
+ if (tasks.size > 100) tasks.delete(tasks.keys().next().value);
1383
+ }
1384
+ }
1385
+ function request(input) {
1386
+ const r = input && typeof input === "object" ? input : {};
1387
+ 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) };
1388
+ if (r.action === "operation") {
1389
+ const op2 = operations.get(String(r.operationId));
1390
+ return op2 ? { ...op2 } : { error: "Operation not found" };
1391
+ }
1392
+ const q = query;
1393
+ if (!q) return { error: "No active Claude SDK query" };
1394
+ if (pending || !q.canControl()) return { error: "Claude is waiting for an interaction or another control operation; retry after it finishes" };
1395
+ const action = String(r.action), server = text(r.serverName), task = text(r.taskId), target = text(r.userMessageId);
1396
+ const runGeneration = generation, runRevision = activityRevision;
1397
+ let execute;
1398
+ switch (action) {
1399
+ case "reload-skills":
1400
+ execute = () => q.reloadSkills();
1401
+ break;
1402
+ case "reload-plugins":
1403
+ execute = async () => {
1404
+ const result = await q.reloadPlugins();
1405
+ return { ...result, mcpServers: publicMcpStatus(result.mcpServers ?? []) };
1406
+ };
1407
+ break;
1408
+ case "mcp-status":
1409
+ execute = async () => publicMcpStatus(await q.mcpServerStatus());
1410
+ break;
1411
+ case "mcp-reconnect":
1412
+ if (!server) return { error: "Invalid server name" };
1413
+ execute = () => q.reconnectMcpServer(server);
1414
+ break;
1415
+ case "mcp-toggle":
1416
+ if (!server || typeof r.enabled !== "boolean") return { error: "Invalid server or enabled flag" };
1417
+ execute = () => q.toggleMcpServer(server, r.enabled);
1418
+ break;
1419
+ case "stop-task":
1420
+ if (!task || !tasks.has(task)) return { error: "Unknown task in this query" };
1421
+ execute = () => q.stopTask(task);
1422
+ break;
1423
+ case "background-tasks":
1424
+ if (r.toolUseId !== void 0 && !text(r.toolUseId)) return { error: "Invalid tool use ID" };
1425
+ execute = async () => ({ backgrounded: await q.backgroundTasks(r.toolUseId) });
1426
+ break;
1427
+ case "rewind-preview":
1428
+ if (isThinking() || !target || !checkpoints.has(target)) return { error: "Rewind requires an idle query and a known user checkpoint" };
1429
+ execute = async () => {
1430
+ const result = await q.rewindFiles(target, { dryRun: true });
1431
+ if (!result.canRewind) return result;
1432
+ if (query !== q || generation !== runGeneration || activityRevision !== runRevision || isThinking() || !q.canControl()) throw Error("Query changed; preview again");
1433
+ const token = randomUUID();
1434
+ confirmations.set(token, { generation, revision: activityRevision, userMessageId: target, fingerprint: fingerprint(result), expires: Date.now() + 3e5 });
1435
+ return { ...result, confirmationToken: token };
1436
+ };
1437
+ break;
1438
+ case "rewind-apply": {
1439
+ const confirmation = confirmations.get(String(r.confirmationToken));
1440
+ if (isThinking() || !confirmation || confirmation.generation !== generation || confirmation.expires < Date.now()) return { error: "Preview expired or query is running; preview again" };
1441
+ confirmations.delete(String(r.confirmationToken));
1442
+ execute = async () => {
1443
+ const fresh = await q.rewindFiles(confirmation.userMessageId, { dryRun: true });
1444
+ if (query !== q || generation !== runGeneration || activityRevision !== confirmation.revision || isThinking() || !q.canControl() || fingerprint(fresh) !== confirmation.fingerprint) throw Error("Files or query changed; preview again");
1445
+ return q.rewindFiles(confirmation.userMessageId, { dryRun: false });
1446
+ };
1447
+ break;
1448
+ }
1449
+ default:
1450
+ return { error: "Unknown Claude control action" };
1451
+ }
1452
+ pending = true;
1453
+ const op = { id: randomUUID(), queryGeneration: runGeneration, action, status: "running" };
1454
+ operations.set(op.id, op);
1455
+ if (operations.size > 50) for (const [id, old] of operations) {
1456
+ if (old.status !== "running") {
1457
+ operations.delete(id);
1458
+ break;
1459
+ }
1460
+ }
1461
+ void Promise.resolve().then(() => {
1462
+ if (query !== q || generation !== runGeneration || !q.canControl()) throw Error("Claude query changed or is waiting for an interaction");
1463
+ return execute();
1464
+ }).then((result) => {
1465
+ if (generation !== runGeneration || op.status !== "running") return;
1466
+ op.status = "completed";
1467
+ op.result = result ?? null;
1468
+ }, (error) => {
1469
+ if (generation !== runGeneration || op.status !== "running") return;
1470
+ op.status = "failed";
1471
+ op.error = error instanceof Error ? error.message : "Claude control failed";
1472
+ }).finally(() => {
1473
+ if (generation === runGeneration) pending = false;
1474
+ });
1475
+ return { operationId: op.id };
1476
+ }
1477
+ return { setQuery, observe, request };
1478
+ }
1479
+
1334
1480
  class MessageBuffer {
1335
1481
  messages = [];
1336
1482
  listeners = [];
@@ -1600,6 +1746,9 @@ function query(params) {
1600
1746
  tools: opts?.tools,
1601
1747
  persistSession: opts?.persistSession,
1602
1748
  includePartialMessages: opts?.includePartialMessages,
1749
+ forwardSubagentText: true,
1750
+ enableFileCheckpointing: true,
1751
+ extraArgs: { "replay-user-messages": null },
1603
1752
  onElicitation: opts?.onElicitation,
1604
1753
  onUserDialog: opts?.onUserDialog,
1605
1754
  supportedDialogKinds: opts?.onUserDialog ? opts.supportedDialogKinds : void 0
@@ -2042,6 +2191,20 @@ async function claudeRemote(opts) {
2042
2191
  content: initial.message
2043
2192
  }
2044
2193
  });
2194
+ let callbackDepth = 0;
2195
+ const guardCallback = (callback) => (async (...args) => {
2196
+ callbackDepth++;
2197
+ try {
2198
+ return await callback(...args);
2199
+ } finally {
2200
+ setImmediate(() => {
2201
+ callbackDepth--;
2202
+ });
2203
+ }
2204
+ });
2205
+ if (sdkOptions.canCallTool) sdkOptions.canCallTool = guardCallback(sdkOptions.canCallTool);
2206
+ if (sdkOptions.onElicitation) sdkOptions.onElicitation = guardCallback(sdkOptions.onElicitation);
2207
+ if (sdkOptions.onUserDialog) sdkOptions.onUserDialog = guardCallback(sdkOptions.onUserDialog);
2045
2208
  const response = query({
2046
2209
  prompt: messages,
2047
2210
  options: sdkOptions
@@ -2062,6 +2225,15 @@ async function claudeRemote(opts) {
2062
2225
  }
2063
2226
  if (opts.onQueryReady) {
2064
2227
  opts.onQueryReady({
2228
+ canControl: () => callbackDepth === 0,
2229
+ reloadSkills: () => response.reloadSkills(),
2230
+ reloadPlugins: () => response.reloadPlugins(),
2231
+ mcpServerStatus: () => response.mcpServerStatus(),
2232
+ reconnectMcpServer: (name) => response.reconnectMcpServer(name),
2233
+ toggleMcpServer: (name, enabled) => response.toggleMcpServer(name, enabled),
2234
+ stopTask: (id) => response.stopTask(id),
2235
+ backgroundTasks: (id) => response.backgroundTasks(id),
2236
+ rewindFiles: (id, options) => response.rewindFiles(id, options),
2065
2237
  setPermissionMode: (mode2) => response.setPermissionMode(mode2),
2066
2238
  setModel: (model) => response.setModel(modelTarget(model)),
2067
2239
  interrupt: async () => {
@@ -2705,7 +2877,7 @@ class PermissionHandler {
2705
2877
  } else if (pending.kind === "elicitation") {
2706
2878
  pending.resolve(message.approved ? { action: "accept", content: elicitationContent(message.updatedInput) } : { action: message.decision === "abort" ? "cancel" : "decline" });
2707
2879
  } else {
2708
- pending.resolve(message.approved ? { behavior: "completed", result: message.updatedInput?.result ?? "retry_fallback" } : { behavior: "cancelled" });
2880
+ 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" });
2709
2881
  }
2710
2882
  this.recordCompletedRequest(id, effectiveResponse);
2711
2883
  this.scheduleDeferredPermissionModeUpdate();
@@ -3167,6 +3339,7 @@ class SDKToLogConverter {
3167
3339
  }
3168
3340
  }
3169
3341
 
3342
+ const transcriptSystemSubtypes = /* @__PURE__ */ new Set(["task_started", "task_updated", "task_progress", "task_notification"]);
3170
3343
  class OutgoingMessageQueue {
3171
3344
  constructor(sendFunction) {
3172
3345
  this.sendFunction = sendFunction;
@@ -3248,7 +3421,7 @@ class OutgoingMessageQueue {
3248
3421
  break;
3249
3422
  }
3250
3423
  if (!item.sent) {
3251
- if (item.logMessage.type !== "system") {
3424
+ if (item.logMessage.type !== "system" || transcriptSystemSubtypes.has(item.logMessage.subtype)) {
3252
3425
  this.sendFunction(item.logMessage);
3253
3426
  }
3254
3427
  item.sent = true;
@@ -3876,6 +4049,8 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
3876
4049
  let abortController = null;
3877
4050
  let abortFuture = null;
3878
4051
  const turnSteering = createTurnSteeringController();
4052
+ const runtimeControls = createRuntimeControls(() => session.thinking);
4053
+ session.client.rpcHandlerManager.registerHandler("claude-runtime-control", async (input) => runtimeControls.request(input));
3879
4054
  async function abort() {
3880
4055
  if (abortController && !abortController.signal.aborted) {
3881
4056
  abortController.abort();
@@ -3936,6 +4111,8 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
3936
4111
  let ongoingToolCalls = /* @__PURE__ */ new Map();
3937
4112
  let notifiedQuestionToolCalls = /* @__PURE__ */ new Set();
3938
4113
  function onMessage(message) {
4114
+ runtimeControls.observe(message);
4115
+ if (isRuntimeCheckpointReplay(message)) return;
3939
4116
  if (message.type === "user" && isClaudeInterruptSentinelContent(message.message.content)) {
3940
4117
  sdkToLogConverter.convert(message);
3941
4118
  return;
@@ -4158,6 +4335,7 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
4158
4335
  if (metadata.permissionMode) onEffectivePermissionMode?.(metadata.permissionMode);
4159
4336
  },
4160
4337
  onQueryReady: (q) => {
4338
+ runtimeControls.setQuery(q);
4161
4339
  turnSteering.setInterrupt(q.interrupt);
4162
4340
  session.setSteerHandler(async (input) => {
4163
4341
  if (!modeGate.matches(input.mode)) return false;
@@ -4247,6 +4425,7 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
4247
4425
  continue;
4248
4426
  }
4249
4427
  } finally {
4428
+ runtimeControls.setQuery(null);
4250
4429
  turnSteering.reset();
4251
4430
  session.setSteerHandler(null);
4252
4431
  livePermissionModeHandler = null;
@@ -5297,6 +5476,80 @@ ${typeLabels[type] || type}:`));
5297
5476
  console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
5298
5477
  }
5299
5478
 
5479
+ const AGENT_CHECK_INTERVAL = 6 * 60 * 60 * 1e3;
5480
+ function stableVersion(value) {
5481
+ const match = value.trim().match(/(?:^|\s|v)(\d+\.\d+\.\d+)([-+][\w.-]+)?(?=\s|$)/);
5482
+ return match && !match[2] ? match[1] : null;
5483
+ }
5484
+ function newerVersion(current, latest) {
5485
+ const a = current.split(".").map(Number), b = latest.split(".").map(Number);
5486
+ for (let i = 0; i < 3; i++) {
5487
+ if (b[i] !== a[i]) return b[i] > a[i];
5488
+ }
5489
+ return false;
5490
+ }
5491
+ const runVersionCommand = (command, args, shell) => new Promise((resolve, reject) => {
5492
+ execFile(
5493
+ command,
5494
+ args,
5495
+ { timeout: 5e3, maxBuffer: 32 * 1024, windowsHide: true, shell },
5496
+ (error, stdout) => error ? reject(error) : resolve(stdout)
5497
+ );
5498
+ });
5499
+ async function probeAgentVersion(command, platform = process.platform, run = runVersionCommand) {
5500
+ if (platform === "win32") {
5501
+ try {
5502
+ await run("where.exe", [command], false);
5503
+ } catch (error) {
5504
+ return { version: null, missing: error.code === 1 };
5505
+ }
5506
+ }
5507
+ try {
5508
+ return { version: stableVersion(await run(command, ["--version"], platform === "win32")), missing: false };
5509
+ } catch (error) {
5510
+ return { version: null, missing: error.code === "ENOENT" };
5511
+ }
5512
+ }
5513
+ async function registryVersion(name) {
5514
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, { signal: AbortSignal.timeout(5e3) });
5515
+ if (!response.ok) throw new Error("Registry unavailable");
5516
+ const data = await response.json();
5517
+ return typeof data.version === "string" ? stableVersion(data.version) : null;
5518
+ }
5519
+ async function detectAgentVersions(deps = { probe: probeAgentVersion, registryVersion }, now = Date.now()) {
5520
+ const agents = await Promise.all(AGENT_VERSION_SOURCES.map(async (source) => {
5521
+ const local = source.command ? await deps.probe(source.command).catch(() => ({ version: null, missing: false })) : { version: stableVersion(packageJson.dependencies["@anthropic-ai/claude-agent-sdk"]), missing: false };
5522
+ if (local.missing) return { id: source.id, installed: null, latest: null, status: "not-installed" };
5523
+ const latest = await deps.registryVersion(source.package).catch(() => null);
5524
+ const installed = local.version;
5525
+ const status = !installed || !latest ? "unknown" : newerVersion(installed, latest) ? "update-available" : "current";
5526
+ return { id: source.id, installed, latest, status };
5527
+ }));
5528
+ return { checkedAt: now, daemonPid: process.pid, agents };
5529
+ }
5530
+ function startAgentVersionChecks(publish, onError, detect = detectAgentVersions) {
5531
+ let stopped = false, pending = false;
5532
+ async function refresh() {
5533
+ if (stopped || pending) return;
5534
+ pending = true;
5535
+ try {
5536
+ const snapshot = await detect();
5537
+ if (!stopped) await publish(snapshot);
5538
+ } catch (error) {
5539
+ onError(error);
5540
+ } finally {
5541
+ pending = false;
5542
+ }
5543
+ }
5544
+ void refresh();
5545
+ const timer = setInterval(() => void refresh(), AGENT_CHECK_INTERVAL);
5546
+ timer.unref();
5547
+ return () => {
5548
+ stopped = true;
5549
+ clearInterval(timer);
5550
+ };
5551
+ }
5552
+
5300
5553
  function sanitizeSpawnModel(value) {
5301
5554
  if (typeof value !== "string") return null;
5302
5555
  return /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}(\[1m\])?$/i.test(value) ? value : null;
@@ -10969,6 +11222,10 @@ async function startDaemon() {
10969
11222
  logger.warn(`[DAEMON RUN] Ignoring invalid HAPPY_CLI_UPDATE_CHECK_INTERVAL; using ${DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS}ms`);
10970
11223
  }
10971
11224
  const cliUpdateInterval = setInterval(() => void refreshCliUpdate(), cliUpdateIntervalMs);
11225
+ const stopAgentVersionChecks = startAgentVersionChecks(
11226
+ (snapshot) => apiMachine.updateDaemonState((state) => ({ ...state, status: state?.status ?? "running", agentVersions: snapshot })),
11227
+ (error) => logger.debug("[DAEMON RUN] Agent version check failed", error)
11228
+ );
10972
11229
  const heartbeatIntervalMs = parseInt(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL || "60000");
10973
11230
  const restartOnStaleVersionAndHeartbeat = setInterval(serialTask(async () => {
10974
11231
  try {
@@ -11009,6 +11266,7 @@ async function startDaemon() {
11009
11266
  teamWorker?.stop();
11010
11267
  clearInterval(restartOnStaleVersionAndHeartbeat);
11011
11268
  clearInterval(cliUpdateInterval);
11269
+ stopAgentVersionChecks();
11012
11270
  claudeAuthService.stop();
11013
11271
  apiMachine.shutdown();
11014
11272
  await stopControlServer();
@@ -11070,6 +11328,7 @@ async function startDaemon() {
11070
11328
  logger.debug("[DAEMON RUN] Health check interval cleared");
11071
11329
  }
11072
11330
  clearInterval(cliUpdateInterval);
11331
+ stopAgentVersionChecks();
11073
11332
  await apiMachine.updateDaemonState((state) => ({
11074
11333
  ...state,
11075
11334
  status: "shutting-down",
@@ -12159,7 +12418,7 @@ function registerSideQuestionHandler(rpc, deps) {
12159
12418
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
12160
12419
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
12161
12420
  const run = deps.run ?? (async (input) => {
12162
- const { query } = await import('./index-CEDPokKf.mjs');
12421
+ const { query } = await import('./index-CpEdOGpe.mjs');
12163
12422
  return runSideQuestion(query, input);
12164
12423
  });
12165
12424
  const slots = /* @__PURE__ */ new Map();
@@ -12722,7 +12981,7 @@ async function runClaude(credentials, options = {}) {
12722
12981
  // a brand-new session cannot pick a PDF until after sending once.
12723
12982
  attachmentKinds: [...CLAUDE_ATTACHMENT_KINDS],
12724
12983
  queueCancellation: true,
12725
- capabilities: ["claude-steer-v1", "claude-live-permission-v1", "claude-live-permission-v2", "claude-btw-v1"],
12984
+ capabilities: ["claude-steer-v1", "claude-live-permission-v1", "claude-live-permission-v2", "claude-btw-v1", "claude-runtime-controls-v1"],
12726
12985
  // Effective mode this process enforces. Kept current by
12727
12986
  // publishPermissionMode below; the web renders it instead of guessing.
12728
12987
  permissionMode: mapToClaudeMode(initialPermissionMode ?? "default"),
@@ -14903,1269 +15162,1325 @@ async function ensureDaemonRunning() {
14903
15162
  logger.debug(`Happy background service did not become ready within ${DAEMON_READY_TIMEOUT_MS}ms; continuing anyway`);
14904
15163
  }
14905
15164
 
14906
- function registerAgentAttachmentDownloads(session) {
14907
- session.onFileEvent((fileEvent) => {
14908
- const attachment = fileEvent.content.data.ev;
14909
- const download = (async () => {
14910
- try {
14911
- const data = await session.downloadAndDecryptAttachment(attachment.ref);
14912
- if (!data) throw new Error("Attachment decryption failed");
14913
- return { data, mimeType: attachment.mimeType ?? "image/jpeg", name: attachment.name };
14914
- } catch {
14915
- session.sendSessionEvent({ type: "message", message: `Could not download attachment: ${attachment.name}. Please attach it again.` });
14916
- return null;
14917
- }
14918
- })();
14919
- session.trackAttachmentDownload(download);
14920
- });
15165
+ function getStartedSubagents(state) {
15166
+ return state.startedSubagents ?? /* @__PURE__ */ new Set();
14921
15167
  }
14922
-
14923
- class BasePermissionHandler {
14924
- pendingRequests = /* @__PURE__ */ new Map();
14925
- session;
14926
- isResetting = false;
14927
- constructor(session) {
14928
- this.session = session;
14929
- this.setupRpcHandler();
15168
+ function getActiveSubagents(state) {
15169
+ return state.activeSubagents ?? /* @__PURE__ */ new Set();
15170
+ }
15171
+ function getProviderSubagentToSessionSubagent(state) {
15172
+ return state.providerSubagentToSessionSubagent ?? /* @__PURE__ */ new Map();
15173
+ }
15174
+ function maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes) {
15175
+ if (!subagent || startedSubagents.has(subagent)) {
15176
+ return;
14930
15177
  }
14931
- /**
14932
- * Update the session reference (used after offline reconnection swaps sessions).
14933
- * This is critical for avoiding stale session references after onSessionSwap.
14934
- */
14935
- updateSession(newSession) {
14936
- logger.debug(`${this.getLogPrefix()} Session reference updated`);
14937
- this.session = newSession;
14938
- this.setupRpcHandler();
15178
+ envelopes.push(createEnvelope("agent", { t: "start" }, { ...opts, subagent }));
15179
+ startedSubagents.add(subagent);
15180
+ activeSubagents.add(subagent);
15181
+ }
15182
+ function emitSubagentStops(opts, startedSubagents, activeSubagents) {
15183
+ const envelopes = [];
15184
+ for (const subagent of activeSubagents) {
15185
+ envelopes.push(createEnvelope("agent", { t: "stop" }, { ...opts, subagent }));
14939
15186
  }
14940
- /**
14941
- * Setup RPC handler for permission responses.
14942
- */
14943
- setupRpcHandler() {
14944
- this.session.rpcHandlerManager.registerHandler(
14945
- "permission",
14946
- async (response) => {
14947
- const pending = this.pendingRequests.get(response.id);
14948
- if (!pending) {
14949
- logger.debug(`${this.getLogPrefix()} Permission request not found or already resolved`);
14950
- return;
14951
- }
14952
- this.pendingRequests.delete(response.id);
14953
- const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
14954
- pending.resolve(result);
14955
- this.session.updateAgentState((currentState) => {
14956
- const request = currentState.requests?.[response.id];
14957
- if (!request) return currentState;
14958
- const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
14959
- let res = {
14960
- ...currentState,
14961
- requests: remainingRequests,
14962
- completedRequests: {
14963
- ...currentState.completedRequests,
14964
- [response.id]: {
14965
- ...request,
14966
- completedAt: Date.now(),
14967
- status: response.approved ? "approved" : "denied",
14968
- decision: result.decision
14969
- }
14970
- }
14971
- };
14972
- return res;
14973
- });
14974
- logger.debug(`${this.getLogPrefix()} Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`);
14975
- }
14976
- );
15187
+ activeSubagents.clear();
15188
+ startedSubagents.clear();
15189
+ return envelopes;
15190
+ }
15191
+ function buildEnvelopeOptions(currentTurnId, subagent) {
15192
+ return {
15193
+ ...currentTurnId ? { turn: currentTurnId } : {},
15194
+ ...subagent ? { subagent } : {}
15195
+ };
15196
+ }
15197
+ function pickProviderSubagent(message) {
15198
+ const candidates = [message.subagent, message.parent_call_id, message.parentCallId];
15199
+ for (const candidate of candidates) {
15200
+ if (typeof candidate === "string" && candidate.length > 0) {
15201
+ return candidate;
15202
+ }
14977
15203
  }
14978
- /**
14979
- * Add a pending request to the agent state.
14980
- */
14981
- addPendingRequestToState(toolCallId, toolName, input) {
14982
- this.session.updateAgentState((currentState) => ({
14983
- ...currentState,
14984
- requests: {
14985
- ...currentState.requests,
14986
- [toolCallId]: {
14987
- tool: toolName,
14988
- arguments: input,
14989
- createdAt: Date.now()
14990
- }
14991
- }
14992
- }));
15204
+ return void 0;
15205
+ }
15206
+ function resolveSessionSubagent(message, providerSubagentToSessionSubagent) {
15207
+ const providerSubagent = pickProviderSubagent(message);
15208
+ if (!providerSubagent) {
15209
+ return void 0;
14993
15210
  }
14994
- /**
14995
- * Abort all pending permission requests.
14996
- * Unlike reset(), this resolves (not rejects) pending promises with { decision: 'abort' },
14997
- * causing the approval response to send 'cancel' to the provider. This is used when the
14998
- * user presses the abort/stop button — it unblocks any pending tool approval so the provider
14999
- * can process the turn cancellation.
15000
- */
15001
- abortAll() {
15002
- const pendingSnapshot = Array.from(this.pendingRequests.entries());
15003
- if (pendingSnapshot.length === 0) return;
15004
- this.pendingRequests.clear();
15005
- for (const [id, pending] of pendingSnapshot) {
15006
- try {
15007
- pending.resolve({ decision: "abort" });
15008
- } catch (err) {
15009
- logger.debug(`${this.getLogPrefix()} Error resolving aborted request ${id}:`, err);
15010
- }
15011
- }
15012
- this.session.updateAgentState((currentState) => {
15013
- const pendingRequests = currentState.requests || {};
15014
- const completedRequests = { ...currentState.completedRequests };
15015
- for (const [id, request] of Object.entries(pendingRequests)) {
15016
- completedRequests[id] = {
15017
- ...request,
15018
- completedAt: Date.now(),
15019
- status: "canceled",
15020
- reason: "Aborted by user"
15021
- };
15022
- }
15023
- return {
15024
- ...currentState,
15025
- requests: {},
15026
- completedRequests
15027
- };
15028
- });
15029
- logger.debug(`${this.getLogPrefix()} Aborted ${pendingSnapshot.length} pending permission(s)`);
15030
- }
15031
- /**
15032
- * Reset state for new sessions.
15033
- * This method is idempotent - safe to call multiple times.
15034
- */
15035
- reset(reason = "Session reset") {
15036
- if (this.isResetting) {
15037
- logger.debug(`${this.getLogPrefix()} Reset already in progress, skipping`);
15038
- return;
15039
- }
15040
- this.isResetting = true;
15041
- try {
15042
- const pendingSnapshot = Array.from(this.pendingRequests.entries());
15043
- this.pendingRequests.clear();
15044
- for (const [id, pending] of pendingSnapshot) {
15045
- try {
15046
- pending.reject(new Error("Session reset"));
15047
- } catch (err) {
15048
- logger.debug(`${this.getLogPrefix()} Error rejecting pending request ${id}:`, err);
15049
- }
15050
- }
15051
- this.session.updateAgentState((currentState) => {
15052
- const pendingRequests = currentState.requests || {};
15053
- const completedRequests = { ...currentState.completedRequests };
15054
- for (const [id, request] of Object.entries(pendingRequests)) {
15055
- completedRequests[id] = {
15056
- ...request,
15057
- completedAt: Date.now(),
15058
- status: "canceled",
15059
- reason
15060
- };
15061
- }
15062
- return {
15063
- ...currentState,
15064
- requests: {},
15065
- completedRequests
15066
- };
15067
- });
15068
- logger.debug(`${this.getLogPrefix()} Permission handler reset`);
15069
- } finally {
15070
- this.isResetting = false;
15071
- }
15211
+ const existing = providerSubagentToSessionSubagent.get(providerSubagent);
15212
+ if (existing) {
15213
+ return existing;
15072
15214
  }
15215
+ const created = createId();
15216
+ providerSubagentToSessionSubagent.set(providerSubagent, created);
15217
+ return created;
15073
15218
  }
15074
-
15075
- class CodexPermissionHandler extends BasePermissionHandler {
15076
- // Exact tool names that should always be auto-approved. Include the bare
15077
- // form (used by Codex elicitation messages like `tool "change_title"`)
15078
- // and the MCP-qualified form for defense in depth.
15079
- static ALWAYS_AUTO_APPROVE_NAMES = /* @__PURE__ */ new Set([
15080
- "change_title",
15081
- "mcp__happy__change_title"
15082
- ]);
15083
- // Tool-call IDs that should auto-approve when they exactly match one of
15084
- // these values or start with `<name>-` (e.g. `change_title-1765385846663`).
15085
- // Substring matching was a bypass vector — any tool whose ID happened to
15086
- // contain `change_title` as a substring would be silently approved.
15087
- static ALWAYS_AUTO_APPROVE_ID_PREFIXES = [
15088
- "change_title"
15089
- ];
15090
- constructor(session) {
15091
- super(session);
15219
+ function pickCallId(message) {
15220
+ const callId = message.call_id ?? message.callId;
15221
+ if (typeof callId === "string" && callId.length > 0) {
15222
+ return callId;
15092
15223
  }
15093
- getLogPrefix() {
15094
- return "[Codex]";
15224
+ return randomUUID();
15225
+ }
15226
+ function summarizeCommand(command) {
15227
+ if (typeof command === "string" && command.trim().length > 0) {
15228
+ return command;
15095
15229
  }
15096
- shouldAutoApprove(toolName, toolCallId) {
15097
- if (CodexPermissionHandler.ALWAYS_AUTO_APPROVE_NAMES.has(toolName)) {
15098
- return true;
15099
- }
15100
- for (const prefix of CodexPermissionHandler.ALWAYS_AUTO_APPROVE_ID_PREFIXES) {
15101
- if (toolCallId === prefix || toolCallId.startsWith(`${prefix}-`)) {
15102
- return true;
15103
- }
15104
- }
15105
- return false;
15230
+ if (Array.isArray(command)) {
15231
+ const cmd = command.map((v) => String(v)).join(" ").trim();
15232
+ return cmd.length > 0 ? cmd : null;
15106
15233
  }
15107
- /**
15108
- * Handle a tool permission request
15109
- * @param toolCallId - The unique ID of the tool call
15110
- * @param toolName - The name of the tool being called
15111
- * @param input - The input parameters for the tool
15112
- * @returns Promise resolving to permission result
15113
- */
15114
- async handleToolCall(toolCallId, toolName, input) {
15115
- if (this.shouldAutoApprove(toolName, toolCallId)) {
15116
- logger.debug(`${this.getLogPrefix()} Auto-approving tool ${toolName} (${toolCallId})`);
15117
- this.session.updateAgentState((currentState) => ({
15118
- ...currentState,
15119
- completedRequests: {
15120
- ...currentState.completedRequests,
15121
- [toolCallId]: {
15122
- tool: toolName,
15123
- arguments: input,
15124
- createdAt: Date.now(),
15125
- completedAt: Date.now(),
15126
- status: "approved",
15127
- decision: "approved"
15128
- }
15129
- }
15130
- }));
15131
- return { decision: "approved" };
15132
- }
15133
- return new Promise((resolve, reject) => {
15134
- this.pendingRequests.set(toolCallId, {
15135
- resolve,
15136
- reject,
15137
- toolName,
15138
- input
15139
- });
15140
- this.addPendingRequestToState(toolCallId, toolName, input);
15141
- logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})`);
15142
- });
15234
+ return null;
15235
+ }
15236
+ function commandToTitle(command) {
15237
+ if (!command) {
15238
+ return "Run command";
15143
15239
  }
15240
+ const short = command.length > 80 ? `${command.slice(0, 77)}...` : command;
15241
+ return `Run \`${short}\``;
15144
15242
  }
15145
-
15146
- class BaseReasoningProcessor {
15147
- accumulator = "";
15148
- inTitleCapture = false;
15149
- titleBuffer = "";
15150
- contentBuffer = "";
15151
- hasTitle = false;
15152
- currentCallId = null;
15153
- toolCallStarted = false;
15154
- currentTitle = null;
15155
- onMessage = null;
15156
- constructor(onMessage) {
15157
- this.onMessage = onMessage || null;
15158
- this.reset();
15243
+ function turnTimestampMs(turn) {
15244
+ const seconds = turn.startedAt ?? turn.completedAt;
15245
+ return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
15246
+ }
15247
+ function completedTimestampMs(turn) {
15248
+ const seconds = turn.completedAt ?? turn.startedAt;
15249
+ return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
15250
+ }
15251
+ function textFromInputItems(items) {
15252
+ if (!Array.isArray(items)) {
15253
+ return null;
15159
15254
  }
15160
- /**
15161
- * Set the message callback for sending messages directly.
15162
- */
15163
- setMessageCallback(callback) {
15164
- this.onMessage = callback;
15255
+ const text = items.filter((item) => Boolean(item) && typeof item === "object" && item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n").trim();
15256
+ return text.length > 0 ? text : null;
15257
+ }
15258
+ function reasoningText(item) {
15259
+ const summary = item.summary;
15260
+ const content = item.content;
15261
+ const parts = [
15262
+ ...Array.isArray(summary) ? summary : [],
15263
+ ...Array.isArray(content) ? content : []
15264
+ ].filter((value) => typeof value === "string" && value.trim().length > 0);
15265
+ const text = parts.join("\n").trim();
15266
+ return text.length > 0 ? text : null;
15267
+ }
15268
+ function turnStatus(turn) {
15269
+ const status = typeof turn.status === "string" ? turn.status : null;
15270
+ if (status === "failed") {
15271
+ return "failed";
15165
15272
  }
15166
- /**
15167
- * Process a reasoning section break - indicates a new reasoning section is starting.
15168
- */
15169
- handleSectionBreak() {
15170
- this.finishCurrentToolCall("canceled");
15171
- this.resetState();
15172
- logger.debug(`${this.getLogPrefix()} Section break - reset state`);
15273
+ if (status === "cancelled" || status === "canceled" || status === "aborted" || status === "interrupted") {
15274
+ return "cancelled";
15173
15275
  }
15174
- /**
15175
- * Process a reasoning delta/chunk and accumulate content.
15176
- */
15177
- processInput(input) {
15178
- this.accumulator += input;
15179
- if (!this.inTitleCapture && !this.hasTitle && !this.contentBuffer) {
15180
- if (this.accumulator.startsWith("**")) {
15181
- this.inTitleCapture = true;
15182
- this.titleBuffer = this.accumulator.substring(2);
15183
- logger.debug(`${this.getLogPrefix()} Started title capture`);
15184
- } else if (this.accumulator.length > 0) {
15185
- this.contentBuffer = this.accumulator;
15186
- }
15187
- } else if (this.inTitleCapture) {
15188
- this.titleBuffer = this.accumulator.substring(2);
15189
- const titleEndIndex = this.titleBuffer.indexOf("**");
15190
- if (titleEndIndex !== -1) {
15191
- const title = this.titleBuffer.substring(0, titleEndIndex);
15192
- const afterTitle = this.titleBuffer.substring(titleEndIndex + 2);
15193
- this.hasTitle = true;
15194
- this.inTitleCapture = false;
15195
- this.currentTitle = title;
15196
- this.contentBuffer = afterTitle;
15197
- this.currentCallId = randomUUID();
15198
- logger.debug(`${this.getLogPrefix()} Title captured: "${title}"`);
15199
- this.sendToolCallStart(title);
15200
- }
15201
- } else if (this.hasTitle) {
15202
- const titleStartIndex = this.accumulator.indexOf("**");
15203
- if (titleStartIndex !== -1) {
15204
- this.contentBuffer = this.accumulator.substring(
15205
- titleStartIndex + 2 + this.currentTitle.length + 2
15206
- );
15207
- }
15208
- } else {
15209
- this.contentBuffer = this.accumulator;
15210
- }
15276
+ return "completed";
15277
+ }
15278
+ function emitHistoricalToolCall(envelopes, turn, item, name, title, args, output) {
15279
+ const time = turnTimestampMs(turn);
15280
+ const opts = { turn: turn.id, time, codexItemId: item.id };
15281
+ envelopes.push(createEnvelope("agent", {
15282
+ t: "tool-call-start",
15283
+ call: item.id,
15284
+ name,
15285
+ title,
15286
+ description: title,
15287
+ args
15288
+ }, {
15289
+ ...opts,
15290
+ id: `${item.id}:start`
15291
+ }));
15292
+ if (output && output.trim().length > 0) {
15293
+ envelopes.push(createEnvelope("agent", {
15294
+ t: "text",
15295
+ text: output,
15296
+ thinking: true
15297
+ }, {
15298
+ ...opts,
15299
+ id: `${item.id}:output`
15300
+ }));
15211
15301
  }
15212
- /**
15213
- * Send the tool call start message.
15214
- */
15215
- sendToolCallStart(title) {
15216
- if (!this.currentCallId || this.toolCallStarted) {
15217
- return;
15218
- }
15219
- const toolCall = {
15220
- type: "tool-call",
15221
- name: this.getToolName(),
15222
- callId: this.currentCallId,
15223
- input: {
15224
- title
15225
- },
15226
- id: randomUUID()
15227
- };
15228
- logger.debug(`${this.getLogPrefix()} Sending tool call start for: "${title}"`);
15229
- this.onMessage?.(toolCall);
15230
- this.toolCallStarted = true;
15302
+ if ("status" in item && (item.status === "inProgress" || item.status === "running")) return;
15303
+ envelopes.push(createEnvelope("agent", {
15304
+ t: "tool-call-end",
15305
+ call: item.id
15306
+ }, {
15307
+ ...opts,
15308
+ id: `${item.id}:end`,
15309
+ time: completedTimestampMs(turn)
15310
+ }));
15311
+ }
15312
+ function mapCodexCollaborationItem(item, opts = {}, source = "history") {
15313
+ if (item.type === "subAgentActivity" && typeof item.agentThreadId === "string") {
15314
+ item = { ...item, type: "collabAgentToolCall", tool: item.kind, status: "completed", receiverThreadIds: [item.agentThreadId] };
15315
+ }
15316
+ if (item.type !== "collabAgentToolCall" || typeof item.id !== "string" || typeof item.tool !== "string") return [];
15317
+ const args = {
15318
+ operation: item.tool,
15319
+ ...typeof item.prompt === "string" ? { prompt: item.prompt } : {},
15320
+ ...typeof item.model === "string" ? { model: item.model } : {},
15321
+ receiverThreadIds: Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : [],
15322
+ agentsStates: item.agentsStates ?? {}
15323
+ };
15324
+ const startId = source === "live" ? `${item.id}:${item.status ?? "unknown"}:start` : `${item.id}:start`;
15325
+ const envelopes = [createEnvelope("agent", {
15326
+ t: "tool-call-start",
15327
+ call: item.id,
15328
+ name: "CodexCollaboration",
15329
+ title: `Codex \xB7 ${item.tool}`,
15330
+ description: `Codex \xB7 ${item.tool}`,
15331
+ args
15332
+ }, { ...opts, id: startId })];
15333
+ if (item.status === "completed" || item.status === "failed" || item.status === "interrupted") {
15334
+ envelopes.push(createEnvelope("agent", {
15335
+ t: "tool-call-end",
15336
+ call: item.id,
15337
+ result: { text: JSON.stringify(args, null, 2), isError: item.status !== "completed" }
15338
+ }, { ...opts, id: `${item.id}:end` }));
15231
15339
  }
15232
- /**
15233
- * Complete the reasoning section.
15234
- * Returns true if reasoning was completed, false if there was nothing to complete.
15235
- */
15236
- completeReasoning(fullText) {
15237
- const text = fullText ?? this.accumulator;
15238
- if (!text.trim() && !this.toolCallStarted) {
15239
- logger.debug(`${this.getLogPrefix()} Complete called but no content accumulated, skipping`);
15240
- return false;
15241
- }
15242
- let title;
15243
- let content = text;
15244
- if (text.startsWith("**")) {
15245
- const titleEndIndex = text.indexOf("**", 2);
15246
- if (titleEndIndex !== -1) {
15247
- title = text.substring(2, titleEndIndex);
15248
- content = text.substring(titleEndIndex + 2).trim();
15340
+ return envelopes;
15341
+ }
15342
+ function mapCodexThreadToSessionEnvelopes(thread) {
15343
+ const envelopes = [];
15344
+ for (const turn of thread.turns ?? []) {
15345
+ const startedAt = turnTimestampMs(turn);
15346
+ const completedAt = completedTimestampMs(turn);
15347
+ envelopes.push(createEnvelope("agent", { t: "turn-start" }, {
15348
+ id: `${turn.id}:start`,
15349
+ turn: turn.id,
15350
+ time: startedAt
15351
+ }));
15352
+ for (const item of turn.items ?? []) {
15353
+ switch (item.type) {
15354
+ case "subAgentActivity":
15355
+ case "collabAgentToolCall": {
15356
+ envelopes.push(...mapCodexCollaborationItem(item, { turn: turn.id, time: startedAt, codexItemId: item.id }));
15357
+ break;
15358
+ }
15359
+ case "userMessage": {
15360
+ const text = textFromInputItems(item.content);
15361
+ if (text) {
15362
+ envelopes.push(createEnvelope("user", { t: "text", text }, {
15363
+ id: item.id,
15364
+ time: startedAt,
15365
+ codexItemId: item.id
15366
+ }));
15367
+ }
15368
+ break;
15369
+ }
15370
+ case "agentMessage": {
15371
+ const text = typeof item.text === "string" ? item.text.trim() : "";
15372
+ if (text.length > 0) {
15373
+ envelopes.push(createEnvelope("agent", { t: "text", text }, {
15374
+ id: item.id,
15375
+ turn: turn.id,
15376
+ time: completedAt,
15377
+ codexItemId: item.id
15378
+ }));
15379
+ }
15380
+ break;
15381
+ }
15382
+ case "reasoning": {
15383
+ const text = reasoningText(item);
15384
+ if (text) {
15385
+ envelopes.push(createEnvelope("agent", { t: "text", text, thinking: true }, {
15386
+ id: item.id,
15387
+ turn: turn.id,
15388
+ time: startedAt,
15389
+ codexItemId: item.id
15390
+ }));
15391
+ }
15392
+ break;
15393
+ }
15394
+ case "commandExecution": {
15395
+ const command = typeof item.command === "string" ? item.command : "";
15396
+ emitHistoricalToolCall(
15397
+ envelopes,
15398
+ turn,
15399
+ item,
15400
+ "CodexBash",
15401
+ commandToTitle(command),
15402
+ { command, cwd: item.cwd },
15403
+ typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : null
15404
+ );
15405
+ break;
15406
+ }
15407
+ case "fileChange": {
15408
+ const title = "Apply patch";
15409
+ emitHistoricalToolCall(
15410
+ envelopes,
15411
+ turn,
15412
+ item,
15413
+ "CodexPatch",
15414
+ title,
15415
+ { changes: item.changes, status: item.status },
15416
+ null
15417
+ );
15418
+ break;
15419
+ }
15420
+ case "mcpToolCall": {
15421
+ const title = `${item.server}.${item.tool}`;
15422
+ const output = item.error !== void 0 && item.error !== null ? String(item.error) : item.result !== void 0 && item.result !== null ? String(item.result) : null;
15423
+ emitHistoricalToolCall(
15424
+ envelopes,
15425
+ turn,
15426
+ item,
15427
+ "McpTool",
15428
+ title,
15429
+ {
15430
+ server: item.server,
15431
+ tool: item.tool,
15432
+ arguments: item.arguments
15433
+ },
15434
+ output
15435
+ );
15436
+ break;
15437
+ }
15249
15438
  }
15250
15439
  }
15251
- logger.debug(`${this.getLogPrefix()} Complete reasoning - Title: "${title}", Has content: ${content.length > 0}`);
15252
- if (title && !this.toolCallStarted) {
15253
- this.currentCallId = this.currentCallId || randomUUID();
15254
- this.sendToolCallStart(title);
15255
- }
15256
- if (this.toolCallStarted && this.currentCallId) {
15257
- const toolResult = {
15258
- type: "tool-call-result",
15259
- callId: this.currentCallId,
15260
- output: {
15261
- content,
15262
- status: "completed"
15263
- },
15264
- id: randomUUID()
15265
- };
15266
- logger.debug(`${this.getLogPrefix()} Sending tool call result`);
15267
- this.onMessage?.(toolResult);
15268
- } else if (content.trim()) {
15269
- const reasoningMessage = {
15270
- type: "reasoning",
15271
- message: content,
15272
- id: randomUUID()
15273
- };
15274
- logger.debug(`${this.getLogPrefix()} Sending reasoning message`);
15275
- this.onMessage?.(reasoningMessage);
15276
- }
15277
- this.resetState();
15278
- return true;
15279
- }
15280
- /**
15281
- * Abort the current reasoning section.
15282
- */
15283
- abort() {
15284
- logger.debug(`${this.getLogPrefix()} Abort called`);
15285
- this.finishCurrentToolCall("canceled");
15286
- this.resetState();
15287
- }
15288
- /**
15289
- * Reset the processor state.
15290
- */
15291
- reset() {
15292
- this.finishCurrentToolCall("canceled");
15293
- this.resetState();
15294
- }
15295
- /**
15296
- * Finish current tool call if one is in progress.
15297
- */
15298
- finishCurrentToolCall(status) {
15299
- if (this.toolCallStarted && this.currentCallId) {
15300
- const toolResult = {
15301
- type: "tool-call-result",
15302
- callId: this.currentCallId,
15303
- output: {
15304
- content: this.contentBuffer || "",
15305
- status
15306
- },
15307
- id: randomUUID()
15308
- };
15309
- logger.debug(`${this.getLogPrefix()} Sending tool call result with status: ${status}`);
15310
- this.onMessage?.(toolResult);
15311
- }
15312
- }
15313
- /**
15314
- * Reset internal state.
15315
- */
15316
- resetState() {
15317
- this.accumulator = "";
15318
- this.inTitleCapture = false;
15319
- this.titleBuffer = "";
15320
- this.contentBuffer = "";
15321
- this.hasTitle = false;
15322
- this.currentCallId = null;
15323
- this.toolCallStarted = false;
15324
- this.currentTitle = null;
15440
+ envelopes.push(createEnvelope("agent", { t: "turn-end", status: turnStatus(turn) }, {
15441
+ id: `${turn.id}:end`,
15442
+ turn: turn.id,
15443
+ time: completedAt
15444
+ }));
15325
15445
  }
15326
- /**
15327
- * Get the current call ID for tool result matching.
15328
- */
15329
- getCurrentCallId() {
15330
- return this.currentCallId;
15446
+ return envelopes;
15447
+ }
15448
+ function patchDescription(changes) {
15449
+ if (!changes || typeof changes !== "object") {
15450
+ return "Applying patch";
15331
15451
  }
15332
- /**
15333
- * Check if a tool call has been started.
15334
- */
15335
- hasStartedToolCall() {
15336
- return this.toolCallStarted;
15452
+ const fileCount = Object.keys(changes).length;
15453
+ if (fileCount === 1) {
15454
+ return "Applying patch to 1 file";
15337
15455
  }
15456
+ return `Applying patch to ${fileCount} files`;
15338
15457
  }
15339
-
15340
- class ReasoningProcessor extends BaseReasoningProcessor {
15341
- getToolName() {
15342
- return "CodexReasoning";
15458
+ function pickTurnEndStatus(message, type) {
15459
+ const rawStatus = message.status;
15460
+ if (rawStatus === "completed" || rawStatus === "failed" || rawStatus === "cancelled") {
15461
+ return rawStatus;
15343
15462
  }
15344
- getLogPrefix() {
15345
- return "[ReasoningProcessor]";
15463
+ if (rawStatus === "canceled") {
15464
+ return "cancelled";
15346
15465
  }
15347
- /**
15348
- * Process a reasoning delta and accumulate content.
15349
- */
15350
- processDelta(delta) {
15351
- this.processInput(delta);
15466
+ if (type === "turn_aborted") {
15467
+ const reason = message.reason;
15468
+ const error = message.error;
15469
+ if (typeof reason === "string" && /(fail|error)/i.test(reason) || typeof error === "string" && error.length > 0 || error !== void 0 && error !== null && typeof error === "object") {
15470
+ return "failed";
15471
+ }
15472
+ return "cancelled";
15352
15473
  }
15353
- /**
15354
- * Complete the reasoning section with final text.
15355
- */
15356
- complete(fullText) {
15357
- this.completeReasoning(fullText);
15474
+ if (message.error !== void 0 && message.error !== null) {
15475
+ return "failed";
15358
15476
  }
15477
+ return "completed";
15359
15478
  }
15360
-
15361
- class DiffProcessor {
15362
- previousDiff = null;
15363
- onMessage = null;
15364
- constructor(onMessage) {
15365
- this.onMessage = onMessage || null;
15479
+ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15480
+ const type = message.type;
15481
+ const startedSubagents = getStartedSubagents(state);
15482
+ const activeSubagents = getActiveSubagents(state);
15483
+ const providerSubagentToSessionSubagent = getProviderSubagentToSessionSubagent(state);
15484
+ if (type === "collab_agent_item") {
15485
+ return {
15486
+ currentTurnId: state.currentTurnId,
15487
+ startedSubagents,
15488
+ activeSubagents,
15489
+ providerSubagentToSessionSubagent,
15490
+ envelopes: mapCodexCollaborationItem(message.item, buildEnvelopeOptions(state.currentTurnId), "live")
15491
+ };
15366
15492
  }
15367
- /**
15368
- * Process a turn_diff message and check if the unified_diff has changed
15369
- */
15370
- processDiff(unifiedDiff) {
15371
- if (this.previousDiff !== unifiedDiff) {
15372
- logger.debug("[DiffProcessor] Unified diff changed, sending CodexDiff tool call");
15373
- const callId = randomUUID();
15374
- const toolCall = {
15375
- type: "tool-call",
15376
- name: "CodexDiff",
15377
- callId,
15378
- input: {
15379
- unified_diff: unifiedDiff
15380
- },
15381
- id: randomUUID()
15493
+ if (type === "task_started") {
15494
+ const turnId = createId();
15495
+ const turnStart = createEnvelope("agent", { t: "turn-start" }, { turn: turnId });
15496
+ startedSubagents.clear();
15497
+ activeSubagents.clear();
15498
+ providerSubagentToSessionSubagent.clear();
15499
+ return {
15500
+ currentTurnId: turnId,
15501
+ startedSubagents,
15502
+ activeSubagents,
15503
+ providerSubagentToSessionSubagent,
15504
+ envelopes: [turnStart]
15505
+ };
15506
+ }
15507
+ if (type === "task_complete" || type === "turn_aborted") {
15508
+ if (!state.currentTurnId) {
15509
+ return {
15510
+ currentTurnId: null,
15511
+ startedSubagents,
15512
+ activeSubagents,
15513
+ providerSubagentToSessionSubagent,
15514
+ envelopes: []
15382
15515
  };
15383
- this.onMessage?.(toolCall);
15384
- const toolResult = {
15385
- type: "tool-call-result",
15386
- callId,
15387
- output: {
15388
- status: "completed"
15389
- },
15390
- id: randomUUID()
15516
+ }
15517
+ const lifecycleOpts = { turn: state.currentTurnId };
15518
+ providerSubagentToSessionSubagent.clear();
15519
+ return {
15520
+ currentTurnId: null,
15521
+ startedSubagents,
15522
+ activeSubagents,
15523
+ providerSubagentToSessionSubagent,
15524
+ envelopes: [
15525
+ ...emitSubagentStops(lifecycleOpts, startedSubagents, activeSubagents),
15526
+ createEnvelope("agent", {
15527
+ t: "turn-end",
15528
+ status: pickTurnEndStatus(message, type)
15529
+ }, lifecycleOpts)
15530
+ ]
15531
+ };
15532
+ }
15533
+ if (type === "token_count") {
15534
+ return {
15535
+ currentTurnId: state.currentTurnId,
15536
+ startedSubagents,
15537
+ activeSubagents,
15538
+ providerSubagentToSessionSubagent,
15539
+ envelopes: []
15540
+ };
15541
+ }
15542
+ const subagent = resolveSessionSubagent(message, providerSubagentToSessionSubagent);
15543
+ const opts = buildEnvelopeOptions(state.currentTurnId, subagent);
15544
+ if (type === "agent_message") {
15545
+ if (typeof message.message !== "string") {
15546
+ return {
15547
+ currentTurnId: state.currentTurnId,
15548
+ startedSubagents,
15549
+ activeSubagents,
15550
+ providerSubagentToSessionSubagent,
15551
+ envelopes: []
15391
15552
  };
15392
- this.onMessage?.(toolResult);
15393
15553
  }
15394
- this.previousDiff = unifiedDiff;
15395
- logger.debug("[DiffProcessor] Updated stored diff");
15554
+ const envelopes = [];
15555
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15556
+ envelopes.push(createEnvelope("agent", { t: "text", text: message.message }, opts));
15557
+ return {
15558
+ currentTurnId: state.currentTurnId,
15559
+ startedSubagents,
15560
+ activeSubagents,
15561
+ providerSubagentToSessionSubagent,
15562
+ envelopes
15563
+ };
15396
15564
  }
15397
- /**
15398
- * Reset the processor state (called on task_complete or turn_aborted)
15399
- */
15400
- reset() {
15401
- logger.debug("[DiffProcessor] Resetting diff state");
15402
- this.previousDiff = null;
15565
+ if (type === "agent_reasoning" || type === "agent_reasoning_delta") {
15566
+ const text = typeof message.text === "string" ? message.text : typeof message.delta === "string" ? message.delta : null;
15567
+ if (!text) {
15568
+ return {
15569
+ currentTurnId: state.currentTurnId,
15570
+ startedSubagents,
15571
+ activeSubagents,
15572
+ providerSubagentToSessionSubagent,
15573
+ envelopes: []
15574
+ };
15575
+ }
15576
+ const envelopes = [];
15577
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15578
+ envelopes.push(createEnvelope("agent", { t: "text", text, thinking: true }, opts));
15579
+ return {
15580
+ currentTurnId: state.currentTurnId,
15581
+ startedSubagents,
15582
+ activeSubagents,
15583
+ providerSubagentToSessionSubagent,
15584
+ envelopes
15585
+ };
15403
15586
  }
15404
- /**
15405
- * Set the message callback for sending messages directly
15406
- */
15407
- setMessageCallback(callback) {
15408
- this.onMessage = callback;
15587
+ if (type === "exec_command_begin") {
15588
+ const call = pickCallId(message);
15589
+ const { call_id: _callIdSnake, callId: _callIdCamel, type: _type, ...args } = message;
15590
+ const command = summarizeCommand(args.command);
15591
+ const description = typeof args.description === "string" ? args.description : command ?? "Execute command";
15592
+ const envelopes = [];
15593
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15594
+ envelopes.push(
15595
+ createEnvelope("agent", {
15596
+ t: "tool-call-start",
15597
+ call,
15598
+ name: "CodexBash",
15599
+ title: commandToTitle(command),
15600
+ description,
15601
+ args
15602
+ }, opts)
15603
+ );
15604
+ return {
15605
+ currentTurnId: state.currentTurnId,
15606
+ startedSubagents,
15607
+ activeSubagents,
15608
+ providerSubagentToSessionSubagent,
15609
+ envelopes
15610
+ };
15409
15611
  }
15410
- /**
15411
- * Get the current diff value
15412
- */
15413
- getCurrentDiff() {
15414
- return this.previousDiff;
15612
+ if (type === "exec_command_end") {
15613
+ const call = pickCallId(message);
15614
+ const envelopes = [];
15615
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15616
+ envelopes.push(createEnvelope("agent", { t: "tool-call-end", call }, opts));
15617
+ return {
15618
+ currentTurnId: state.currentTurnId,
15619
+ startedSubagents,
15620
+ activeSubagents,
15621
+ providerSubagentToSessionSubagent,
15622
+ envelopes
15623
+ };
15624
+ }
15625
+ if (type === "patch_apply_begin") {
15626
+ const call = pickCallId(message);
15627
+ const autoApproved = message.auto_approved;
15628
+ const changes = message.changes;
15629
+ const envelopes = [];
15630
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15631
+ envelopes.push(
15632
+ createEnvelope("agent", {
15633
+ t: "tool-call-start",
15634
+ call,
15635
+ name: "CodexPatch",
15636
+ title: "Apply patch",
15637
+ description: patchDescription(changes),
15638
+ args: {
15639
+ auto_approved: autoApproved,
15640
+ changes
15641
+ }
15642
+ }, opts)
15643
+ );
15644
+ return {
15645
+ currentTurnId: state.currentTurnId,
15646
+ startedSubagents,
15647
+ activeSubagents,
15648
+ providerSubagentToSessionSubagent,
15649
+ envelopes
15650
+ };
15651
+ }
15652
+ if (type === "patch_apply_end") {
15653
+ const call = pickCallId(message);
15654
+ const envelopes = [];
15655
+ maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15656
+ envelopes.push(createEnvelope("agent", { t: "tool-call-end", call }, opts));
15657
+ return {
15658
+ currentTurnId: state.currentTurnId,
15659
+ startedSubagents,
15660
+ activeSubagents,
15661
+ providerSubagentToSessionSubagent,
15662
+ envelopes
15663
+ };
15664
+ }
15665
+ return {
15666
+ currentTurnId: state.currentTurnId,
15667
+ startedSubagents,
15668
+ activeSubagents,
15669
+ providerSubagentToSessionSubagent,
15670
+ envelopes: []
15671
+ };
15672
+ }
15673
+ function mapCodexProcessorMessageToSessionEnvelopes(message, state) {
15674
+ const toolLikeMessage = message;
15675
+ const opts = buildEnvelopeOptions(state.currentTurnId);
15676
+ if (message.type === "reasoning") {
15677
+ return [createEnvelope("agent", {
15678
+ t: "text",
15679
+ text: message.message,
15680
+ thinking: true
15681
+ }, opts)];
15682
+ }
15683
+ if (message.type === "tool-call") {
15684
+ const title = typeof toolLikeMessage.input?.title === "string" ? toolLikeMessage.input.title : `${toolLikeMessage.name || "Tool"} call`;
15685
+ return [createEnvelope("agent", {
15686
+ t: "tool-call-start",
15687
+ call: toolLikeMessage.callId,
15688
+ name: toolLikeMessage.name || "unknown",
15689
+ title,
15690
+ description: title,
15691
+ args: toolLikeMessage.input && typeof toolLikeMessage.input === "object" ? toolLikeMessage.input : {}
15692
+ }, opts)];
15693
+ }
15694
+ if (message.type === "tool-call-result") {
15695
+ const envelopes = [];
15696
+ const content = toolLikeMessage.output?.content;
15697
+ if (typeof content === "string" && content.trim().length > 0) {
15698
+ envelopes.push(createEnvelope("agent", {
15699
+ t: "text",
15700
+ text: content,
15701
+ thinking: true
15702
+ }, opts));
15703
+ }
15704
+ envelopes.push(createEnvelope("agent", {
15705
+ t: "tool-call-end",
15706
+ call: toolLikeMessage.callId
15707
+ }, opts));
15708
+ return envelopes;
15415
15709
  }
15710
+ return [];
15416
15711
  }
15417
15712
 
15418
- const CodexDisplay = ({ messageBuffer, logPath, onExit }) => {
15419
- const [messages, setMessages] = useState([]);
15420
- const [confirmationMode, setConfirmationMode] = useState(false);
15421
- const [actionInProgress, setActionInProgress] = useState(false);
15422
- const confirmationTimeoutRef = useRef(null);
15423
- const { stdout } = useStdout();
15424
- const terminalWidth = stdout.columns || 80;
15425
- const terminalHeight = stdout.rows || 24;
15426
- useEffect(() => {
15427
- setMessages(messageBuffer.getMessages());
15428
- const unsubscribe = messageBuffer.onUpdate((newMessages) => {
15429
- setMessages(newMessages);
15430
- });
15431
- return () => {
15432
- unsubscribe();
15433
- if (confirmationTimeoutRef.current) {
15434
- clearTimeout(confirmationTimeoutRef.current);
15713
+ async function readChildThread(client, childId) {
15714
+ if (!client.threadId || typeof childId !== "string" || !childId) throw new Error("Invalid child thread");
15715
+ const parentId = client.threadId;
15716
+ const { thread: parent } = await client.readThread({ threadId: parentId, includeTurns: true });
15717
+ 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));
15718
+ if (!belongs) throw new Error("Thread is not a child of this session");
15719
+ const { thread } = await client.readThread({ threadId: childId, includeTurns: true });
15720
+ if (thread.id !== childId) throw new Error("Child thread mismatch");
15721
+ return { envelopes: mapCodexThreadToSessionEnvelopes(thread) };
15722
+ }
15723
+
15724
+ function registerAgentAttachmentDownloads(session) {
15725
+ session.onFileEvent((fileEvent) => {
15726
+ const attachment = fileEvent.content.data.ev;
15727
+ const download = (async () => {
15728
+ try {
15729
+ const data = await session.downloadAndDecryptAttachment(attachment.ref);
15730
+ if (!data) throw new Error("Attachment decryption failed");
15731
+ return { data, mimeType: attachment.mimeType ?? "image/jpeg", name: attachment.name };
15732
+ } catch {
15733
+ session.sendSessionEvent({ type: "message", message: `Could not download attachment: ${attachment.name}. Please attach it again.` });
15734
+ return null;
15435
15735
  }
15436
- };
15437
- }, [messageBuffer]);
15438
- const resetConfirmation = useCallback(() => {
15439
- setConfirmationMode(false);
15440
- if (confirmationTimeoutRef.current) {
15441
- clearTimeout(confirmationTimeoutRef.current);
15442
- confirmationTimeoutRef.current = null;
15443
- }
15444
- }, []);
15445
- const setConfirmationWithTimeout = useCallback(() => {
15446
- setConfirmationMode(true);
15447
- if (confirmationTimeoutRef.current) {
15448
- clearTimeout(confirmationTimeoutRef.current);
15449
- }
15450
- confirmationTimeoutRef.current = setTimeout(() => {
15451
- resetConfirmation();
15452
- }, 15e3);
15453
- }, [resetConfirmation]);
15454
- useInput(useCallback(async (input, key) => {
15455
- if (actionInProgress) return;
15456
- if (key.ctrl && input === "c") {
15457
- if (confirmationMode) {
15458
- resetConfirmation();
15459
- setActionInProgress(true);
15460
- await new Promise((resolve) => setTimeout(resolve, 100));
15461
- onExit?.();
15462
- } else {
15463
- setConfirmationWithTimeout();
15736
+ })();
15737
+ session.trackAttachmentDownload(download);
15738
+ });
15739
+ }
15740
+
15741
+ class BasePermissionHandler {
15742
+ pendingRequests = /* @__PURE__ */ new Map();
15743
+ session;
15744
+ isResetting = false;
15745
+ constructor(session) {
15746
+ this.session = session;
15747
+ this.setupRpcHandler();
15748
+ }
15749
+ /**
15750
+ * Update the session reference (used after offline reconnection swaps sessions).
15751
+ * This is critical for avoiding stale session references after onSessionSwap.
15752
+ */
15753
+ updateSession(newSession) {
15754
+ logger.debug(`${this.getLogPrefix()} Session reference updated`);
15755
+ this.session = newSession;
15756
+ this.setupRpcHandler();
15757
+ }
15758
+ /**
15759
+ * Setup RPC handler for permission responses.
15760
+ */
15761
+ setupRpcHandler() {
15762
+ this.session.rpcHandlerManager.registerHandler(
15763
+ "permission",
15764
+ async (response) => {
15765
+ const pending = this.pendingRequests.get(response.id);
15766
+ if (!pending) {
15767
+ logger.debug(`${this.getLogPrefix()} Permission request not found or already resolved`);
15768
+ return;
15769
+ }
15770
+ this.pendingRequests.delete(response.id);
15771
+ const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
15772
+ pending.resolve(result);
15773
+ this.session.updateAgentState((currentState) => {
15774
+ const request = currentState.requests?.[response.id];
15775
+ if (!request) return currentState;
15776
+ const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
15777
+ let res = {
15778
+ ...currentState,
15779
+ requests: remainingRequests,
15780
+ completedRequests: {
15781
+ ...currentState.completedRequests,
15782
+ [response.id]: {
15783
+ ...request,
15784
+ completedAt: Date.now(),
15785
+ status: response.approved ? "approved" : "denied",
15786
+ decision: result.decision
15787
+ }
15788
+ }
15789
+ };
15790
+ return res;
15791
+ });
15792
+ logger.debug(`${this.getLogPrefix()} Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`);
15464
15793
  }
15465
- return;
15466
- }
15467
- if (confirmationMode) {
15468
- resetConfirmation();
15469
- }
15470
- }, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
15471
- const getMessageColor = (type) => {
15472
- switch (type) {
15473
- case "user":
15474
- return "magenta";
15475
- case "assistant":
15476
- return "cyan";
15477
- case "system":
15478
- return "blue";
15479
- case "tool":
15480
- return "yellow";
15481
- case "result":
15482
- return "green";
15483
- case "status":
15484
- return "gray";
15485
- default:
15486
- return "white";
15487
- }
15488
- };
15489
- const formatMessage = (msg) => {
15490
- const lines = msg.content.split("\n");
15491
- const maxLineLength = terminalWidth - 10;
15492
- return lines.map((line) => {
15493
- if (line.length <= maxLineLength) return line;
15494
- const chunks = [];
15495
- for (let i = 0; i < line.length; i += maxLineLength) {
15496
- chunks.push(line.slice(i, i + maxLineLength));
15794
+ );
15795
+ }
15796
+ /**
15797
+ * Add a pending request to the agent state.
15798
+ */
15799
+ addPendingRequestToState(toolCallId, toolName, input) {
15800
+ this.session.updateAgentState((currentState) => ({
15801
+ ...currentState,
15802
+ requests: {
15803
+ ...currentState.requests,
15804
+ [toolCallId]: {
15805
+ tool: toolName,
15806
+ arguments: input,
15807
+ createdAt: Date.now()
15808
+ }
15497
15809
  }
15498
- return chunks.join("\n");
15499
- }).join("\n");
15500
- };
15501
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
15502
- Box,
15503
- {
15504
- flexDirection: "column",
15505
- width: terminalWidth,
15506
- height: terminalHeight - 4,
15507
- borderStyle: "round",
15508
- borderColor: "gray",
15509
- paddingX: 1,
15510
- overflow: "hidden"
15511
- },
15512
- /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "\u{1F916} Codex Agent Messages"), /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
15513
- /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
15514
- // Show only the last messages that fit in the available space
15515
- messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
15516
- ))
15517
- ), /* @__PURE__ */ React.createElement(
15518
- Box,
15519
- {
15520
- width: terminalWidth,
15521
- borderStyle: "round",
15522
- borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "green",
15523
- paddingX: 2,
15524
- justifyContent: "center",
15525
- alignItems: "center",
15526
- flexDirection: "column"
15527
- },
15528
- /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", alignItems: "center" }, actionInProgress ? /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "Exiting agent...") : confirmationMode ? /* @__PURE__ */ React.createElement(Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit the agent") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "green", bold: true }, "\u{1F916} Codex Agent Running \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
15529
- ));
15530
- };
15531
-
15532
- function createOfflineSessionStub(sessionTag) {
15533
- return {
15534
- sessionId: `offline-${sessionTag}`,
15535
- sendCodexMessage: () => {
15536
- },
15537
- sendAgentMessage: () => {
15538
- },
15539
- sendAgentUsageSnapshot: () => false,
15540
- sendClaudeSessionMessage: () => {
15541
- },
15542
- keepAlive: () => {
15543
- },
15544
- sendSessionEvent: () => {
15545
- },
15546
- sendSessionDeath: () => {
15547
- },
15548
- updateLifecycleState: () => {
15549
- },
15550
- requestControlTransfer: async () => {
15551
- },
15552
- flush: async () => {
15553
- },
15554
- close: async () => {
15555
- },
15556
- updateMetadata: () => {
15557
- },
15558
- updateAgentState: () => {
15559
- },
15560
- onUserMessage: () => {
15561
- },
15562
- onFileEvent: () => {
15563
- },
15564
- trackAttachmentDownload: () => {
15565
- },
15566
- drainAttachmentsForUserMessage: async () => [],
15567
- rpcHandlerManager: {
15568
- registerHandler: () => {
15810
+ }));
15811
+ }
15812
+ /**
15813
+ * Abort all pending permission requests.
15814
+ * Unlike reset(), this resolves (not rejects) pending promises with { decision: 'abort' },
15815
+ * causing the approval response to send 'cancel' to the provider. This is used when the
15816
+ * user presses the abort/stop button — it unblocks any pending tool approval so the provider
15817
+ * can process the turn cancellation.
15818
+ */
15819
+ abortAll() {
15820
+ const pendingSnapshot = Array.from(this.pendingRequests.entries());
15821
+ if (pendingSnapshot.length === 0) return;
15822
+ this.pendingRequests.clear();
15823
+ for (const [id, pending] of pendingSnapshot) {
15824
+ try {
15825
+ pending.resolve({ decision: "abort" });
15826
+ } catch (err) {
15827
+ logger.debug(`${this.getLogPrefix()} Error resolving aborted request ${id}:`, err);
15569
15828
  }
15570
15829
  }
15571
- };
15572
- }
15573
-
15574
- function setupOfflineReconnection(opts) {
15575
- const { api, sessionTag, metadata, state, response, onSessionSwap } = opts;
15576
- let session;
15577
- let reconnectionHandle = null;
15578
- if (!response) {
15579
- session = createOfflineSessionStub(sessionTag);
15580
- reconnectionHandle = startOfflineReconnection({
15581
- serverUrl: configuration.serverUrl,
15582
- onReconnected: async () => {
15583
- const resp = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
15584
- if (!resp) throw new Error("Server unavailable");
15585
- const realSession = api.sessionSyncClient(resp);
15586
- onSessionSwap(realSession);
15587
- return realSession;
15588
- },
15589
- onNotify: (msg) => {
15590
- console.log(msg);
15830
+ this.session.updateAgentState((currentState) => {
15831
+ const pendingRequests = currentState.requests || {};
15832
+ const completedRequests = { ...currentState.completedRequests };
15833
+ for (const [id, request] of Object.entries(pendingRequests)) {
15834
+ completedRequests[id] = {
15835
+ ...request,
15836
+ completedAt: Date.now(),
15837
+ status: "canceled",
15838
+ reason: "Aborted by user"
15839
+ };
15591
15840
  }
15841
+ return {
15842
+ ...currentState,
15843
+ requests: {},
15844
+ completedRequests
15845
+ };
15592
15846
  });
15593
- return { session, reconnectionHandle, isOffline: true };
15594
- } else {
15595
- session = api.sessionSyncClient(response, opts.sessionClientOptions);
15596
- return { session, reconnectionHandle: null, isOffline: false };
15597
- }
15598
- }
15599
-
15600
- function resolveCodexExecutionPolicy(permissionMode, sandboxManagedByHappy) {
15601
- if (sandboxManagedByHappy) {
15602
- return {
15603
- approvalPolicy: "never",
15604
- sandbox: "danger-full-access"
15605
- };
15847
+ logger.debug(`${this.getLogPrefix()} Aborted ${pendingSnapshot.length} pending permission(s)`);
15606
15848
  }
15607
- const approvalPolicy = (() => {
15608
- switch (permissionMode) {
15609
- // Codex native modes
15610
- case "default":
15611
- return "untrusted";
15612
- // Ask for non-trusted commands
15613
- case "read-only":
15614
- return "never";
15615
- // Never ask, read-only enforced by sandbox
15616
- case "safe-yolo":
15617
- return "on-request";
15618
- // Workspace auto-run; request approval for escalation (0.153 removed on-failure)
15619
- case "yolo":
15620
- return "never";
15621
- // Full YOLO: never interrupt for approvals
15622
- // Defensive fallback for Claude-specific modes (backward compatibility)
15623
- case "bypassPermissions":
15624
- return "never";
15625
- // Full access: map to yolo behavior
15626
- case "acceptEdits":
15627
- return "on-request";
15628
- // Let model decide (closest to auto-approve edits)
15629
- case "plan":
15630
- return "untrusted";
15631
- // Conservative: ask for non-trusted
15632
- default:
15633
- return "untrusted";
15849
+ /**
15850
+ * Reset state for new sessions.
15851
+ * This method is idempotent - safe to call multiple times.
15852
+ */
15853
+ reset(reason = "Session reset") {
15854
+ if (this.isResetting) {
15855
+ logger.debug(`${this.getLogPrefix()} Reset already in progress, skipping`);
15856
+ return;
15634
15857
  }
15635
- })();
15636
- const sandbox = (() => {
15637
- switch (permissionMode) {
15638
- // Codex native modes
15639
- case "default":
15640
- return "workspace-write";
15641
- // Can write in workspace
15642
- case "read-only":
15643
- return "read-only";
15644
- // Read-only filesystem
15645
- case "safe-yolo":
15646
- return "workspace-write";
15647
- // Can write in workspace
15648
- case "yolo":
15649
- return "danger-full-access";
15650
- // Full system access
15651
- // Defensive fallback for Claude-specific modes
15652
- case "bypassPermissions":
15653
- return "danger-full-access";
15654
- // Full access: map to yolo
15655
- case "acceptEdits":
15656
- return "workspace-write";
15657
- // Can edit files in workspace
15658
- case "plan":
15659
- return "workspace-write";
15660
- // Can write for planning
15661
- default:
15662
- return "workspace-write";
15858
+ this.isResetting = true;
15859
+ try {
15860
+ const pendingSnapshot = Array.from(this.pendingRequests.entries());
15861
+ this.pendingRequests.clear();
15862
+ for (const [id, pending] of pendingSnapshot) {
15863
+ try {
15864
+ pending.reject(new Error("Session reset"));
15865
+ } catch (err) {
15866
+ logger.debug(`${this.getLogPrefix()} Error rejecting pending request ${id}:`, err);
15867
+ }
15868
+ }
15869
+ this.session.updateAgentState((currentState) => {
15870
+ const pendingRequests = currentState.requests || {};
15871
+ const completedRequests = { ...currentState.completedRequests };
15872
+ for (const [id, request] of Object.entries(pendingRequests)) {
15873
+ completedRequests[id] = {
15874
+ ...request,
15875
+ completedAt: Date.now(),
15876
+ status: "canceled",
15877
+ reason
15878
+ };
15879
+ }
15880
+ return {
15881
+ ...currentState,
15882
+ requests: {},
15883
+ completedRequests
15884
+ };
15885
+ });
15886
+ logger.debug(`${this.getLogPrefix()} Permission handler reset`);
15887
+ } finally {
15888
+ this.isResetting = false;
15663
15889
  }
15664
- })();
15665
- return { approvalPolicy, sandbox };
15890
+ }
15666
15891
  }
15667
15892
 
15668
- function getStartedSubagents(state) {
15669
- return state.startedSubagents ?? /* @__PURE__ */ new Set();
15670
- }
15671
- function getActiveSubagents(state) {
15672
- return state.activeSubagents ?? /* @__PURE__ */ new Set();
15673
- }
15674
- function getProviderSubagentToSessionSubagent(state) {
15675
- return state.providerSubagentToSessionSubagent ?? /* @__PURE__ */ new Map();
15676
- }
15677
- function maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes) {
15678
- if (!subagent || startedSubagents.has(subagent)) {
15679
- return;
15893
+ class CodexPermissionHandler extends BasePermissionHandler {
15894
+ // Exact tool names that should always be auto-approved. Include the bare
15895
+ // form (used by Codex elicitation messages like `tool "change_title"`)
15896
+ // and the MCP-qualified form for defense in depth.
15897
+ static ALWAYS_AUTO_APPROVE_NAMES = /* @__PURE__ */ new Set([
15898
+ "change_title",
15899
+ "mcp__happy__change_title"
15900
+ ]);
15901
+ // Tool-call IDs that should auto-approve when they exactly match one of
15902
+ // these values or start with `<name>-` (e.g. `change_title-1765385846663`).
15903
+ // Substring matching was a bypass vector — any tool whose ID happened to
15904
+ // contain `change_title` as a substring would be silently approved.
15905
+ static ALWAYS_AUTO_APPROVE_ID_PREFIXES = [
15906
+ "change_title"
15907
+ ];
15908
+ constructor(session) {
15909
+ super(session);
15680
15910
  }
15681
- envelopes.push(createEnvelope("agent", { t: "start" }, { ...opts, subagent }));
15682
- startedSubagents.add(subagent);
15683
- activeSubagents.add(subagent);
15684
- }
15685
- function emitSubagentStops(opts, startedSubagents, activeSubagents) {
15686
- const envelopes = [];
15687
- for (const subagent of activeSubagents) {
15688
- envelopes.push(createEnvelope("agent", { t: "stop" }, { ...opts, subagent }));
15911
+ getLogPrefix() {
15912
+ return "[Codex]";
15689
15913
  }
15690
- activeSubagents.clear();
15691
- startedSubagents.clear();
15692
- return envelopes;
15693
- }
15694
- function buildEnvelopeOptions(currentTurnId, subagent) {
15695
- return {
15696
- ...currentTurnId ? { turn: currentTurnId } : {},
15697
- ...subagent ? { subagent } : {}
15698
- };
15699
- }
15700
- function pickProviderSubagent(message) {
15701
- const candidates = [message.subagent, message.parent_call_id, message.parentCallId];
15702
- for (const candidate of candidates) {
15703
- if (typeof candidate === "string" && candidate.length > 0) {
15704
- return candidate;
15914
+ shouldAutoApprove(toolName, toolCallId) {
15915
+ if (CodexPermissionHandler.ALWAYS_AUTO_APPROVE_NAMES.has(toolName)) {
15916
+ return true;
15705
15917
  }
15918
+ for (const prefix of CodexPermissionHandler.ALWAYS_AUTO_APPROVE_ID_PREFIXES) {
15919
+ if (toolCallId === prefix || toolCallId.startsWith(`${prefix}-`)) {
15920
+ return true;
15921
+ }
15922
+ }
15923
+ return false;
15706
15924
  }
15707
- return void 0;
15708
- }
15709
- function resolveSessionSubagent(message, providerSubagentToSessionSubagent) {
15710
- const providerSubagent = pickProviderSubagent(message);
15711
- if (!providerSubagent) {
15712
- return void 0;
15713
- }
15714
- const existing = providerSubagentToSessionSubagent.get(providerSubagent);
15715
- if (existing) {
15716
- return existing;
15925
+ /**
15926
+ * Handle a tool permission request
15927
+ * @param toolCallId - The unique ID of the tool call
15928
+ * @param toolName - The name of the tool being called
15929
+ * @param input - The input parameters for the tool
15930
+ * @returns Promise resolving to permission result
15931
+ */
15932
+ async handleToolCall(toolCallId, toolName, input) {
15933
+ if (this.shouldAutoApprove(toolName, toolCallId)) {
15934
+ logger.debug(`${this.getLogPrefix()} Auto-approving tool ${toolName} (${toolCallId})`);
15935
+ this.session.updateAgentState((currentState) => ({
15936
+ ...currentState,
15937
+ completedRequests: {
15938
+ ...currentState.completedRequests,
15939
+ [toolCallId]: {
15940
+ tool: toolName,
15941
+ arguments: input,
15942
+ createdAt: Date.now(),
15943
+ completedAt: Date.now(),
15944
+ status: "approved",
15945
+ decision: "approved"
15946
+ }
15947
+ }
15948
+ }));
15949
+ return { decision: "approved" };
15950
+ }
15951
+ return new Promise((resolve, reject) => {
15952
+ this.pendingRequests.set(toolCallId, {
15953
+ resolve,
15954
+ reject,
15955
+ toolName,
15956
+ input
15957
+ });
15958
+ this.addPendingRequestToState(toolCallId, toolName, input);
15959
+ logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})`);
15960
+ });
15717
15961
  }
15718
- const created = createId();
15719
- providerSubagentToSessionSubagent.set(providerSubagent, created);
15720
- return created;
15721
15962
  }
15722
- function pickCallId(message) {
15723
- const callId = message.call_id ?? message.callId;
15724
- if (typeof callId === "string" && callId.length > 0) {
15725
- return callId;
15963
+
15964
+ class BaseReasoningProcessor {
15965
+ accumulator = "";
15966
+ inTitleCapture = false;
15967
+ titleBuffer = "";
15968
+ contentBuffer = "";
15969
+ hasTitle = false;
15970
+ currentCallId = null;
15971
+ toolCallStarted = false;
15972
+ currentTitle = null;
15973
+ onMessage = null;
15974
+ constructor(onMessage) {
15975
+ this.onMessage = onMessage || null;
15976
+ this.reset();
15726
15977
  }
15727
- return randomUUID();
15728
- }
15729
- function summarizeCommand(command) {
15730
- if (typeof command === "string" && command.trim().length > 0) {
15731
- return command;
15978
+ /**
15979
+ * Set the message callback for sending messages directly.
15980
+ */
15981
+ setMessageCallback(callback) {
15982
+ this.onMessage = callback;
15732
15983
  }
15733
- if (Array.isArray(command)) {
15734
- const cmd = command.map((v) => String(v)).join(" ").trim();
15735
- return cmd.length > 0 ? cmd : null;
15984
+ /**
15985
+ * Process a reasoning section break - indicates a new reasoning section is starting.
15986
+ */
15987
+ handleSectionBreak() {
15988
+ this.finishCurrentToolCall("canceled");
15989
+ this.resetState();
15990
+ logger.debug(`${this.getLogPrefix()} Section break - reset state`);
15736
15991
  }
15737
- return null;
15738
- }
15739
- function commandToTitle(command) {
15740
- if (!command) {
15741
- return "Run command";
15992
+ /**
15993
+ * Process a reasoning delta/chunk and accumulate content.
15994
+ */
15995
+ processInput(input) {
15996
+ this.accumulator += input;
15997
+ if (!this.inTitleCapture && !this.hasTitle && !this.contentBuffer) {
15998
+ if (this.accumulator.startsWith("**")) {
15999
+ this.inTitleCapture = true;
16000
+ this.titleBuffer = this.accumulator.substring(2);
16001
+ logger.debug(`${this.getLogPrefix()} Started title capture`);
16002
+ } else if (this.accumulator.length > 0) {
16003
+ this.contentBuffer = this.accumulator;
16004
+ }
16005
+ } else if (this.inTitleCapture) {
16006
+ this.titleBuffer = this.accumulator.substring(2);
16007
+ const titleEndIndex = this.titleBuffer.indexOf("**");
16008
+ if (titleEndIndex !== -1) {
16009
+ const title = this.titleBuffer.substring(0, titleEndIndex);
16010
+ const afterTitle = this.titleBuffer.substring(titleEndIndex + 2);
16011
+ this.hasTitle = true;
16012
+ this.inTitleCapture = false;
16013
+ this.currentTitle = title;
16014
+ this.contentBuffer = afterTitle;
16015
+ this.currentCallId = randomUUID();
16016
+ logger.debug(`${this.getLogPrefix()} Title captured: "${title}"`);
16017
+ this.sendToolCallStart(title);
16018
+ }
16019
+ } else if (this.hasTitle) {
16020
+ const titleStartIndex = this.accumulator.indexOf("**");
16021
+ if (titleStartIndex !== -1) {
16022
+ this.contentBuffer = this.accumulator.substring(
16023
+ titleStartIndex + 2 + this.currentTitle.length + 2
16024
+ );
16025
+ }
16026
+ } else {
16027
+ this.contentBuffer = this.accumulator;
16028
+ }
15742
16029
  }
15743
- const short = command.length > 80 ? `${command.slice(0, 77)}...` : command;
15744
- return `Run \`${short}\``;
15745
- }
15746
- function turnTimestampMs(turn) {
15747
- const seconds = turn.startedAt ?? turn.completedAt;
15748
- return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
15749
- }
15750
- function completedTimestampMs(turn) {
15751
- const seconds = turn.completedAt ?? turn.startedAt;
15752
- return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
15753
- }
15754
- function textFromInputItems(items) {
15755
- if (!Array.isArray(items)) {
15756
- return null;
16030
+ /**
16031
+ * Send the tool call start message.
16032
+ */
16033
+ sendToolCallStart(title) {
16034
+ if (!this.currentCallId || this.toolCallStarted) {
16035
+ return;
16036
+ }
16037
+ const toolCall = {
16038
+ type: "tool-call",
16039
+ name: this.getToolName(),
16040
+ callId: this.currentCallId,
16041
+ input: {
16042
+ title
16043
+ },
16044
+ id: randomUUID()
16045
+ };
16046
+ logger.debug(`${this.getLogPrefix()} Sending tool call start for: "${title}"`);
16047
+ this.onMessage?.(toolCall);
16048
+ this.toolCallStarted = true;
15757
16049
  }
15758
- const text = items.filter((item) => Boolean(item) && typeof item === "object" && item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n").trim();
15759
- return text.length > 0 ? text : null;
15760
- }
15761
- function reasoningText(item) {
15762
- const summary = item.summary;
15763
- const content = item.content;
15764
- const parts = [
15765
- ...Array.isArray(summary) ? summary : [],
15766
- ...Array.isArray(content) ? content : []
15767
- ].filter((value) => typeof value === "string" && value.trim().length > 0);
15768
- const text = parts.join("\n").trim();
15769
- return text.length > 0 ? text : null;
15770
- }
15771
- function turnStatus(turn) {
15772
- const status = typeof turn.status === "string" ? turn.status : null;
15773
- if (status === "failed") {
15774
- return "failed";
16050
+ /**
16051
+ * Complete the reasoning section.
16052
+ * Returns true if reasoning was completed, false if there was nothing to complete.
16053
+ */
16054
+ completeReasoning(fullText) {
16055
+ const text = fullText ?? this.accumulator;
16056
+ if (!text.trim() && !this.toolCallStarted) {
16057
+ logger.debug(`${this.getLogPrefix()} Complete called but no content accumulated, skipping`);
16058
+ return false;
16059
+ }
16060
+ let title;
16061
+ let content = text;
16062
+ if (text.startsWith("**")) {
16063
+ const titleEndIndex = text.indexOf("**", 2);
16064
+ if (titleEndIndex !== -1) {
16065
+ title = text.substring(2, titleEndIndex);
16066
+ content = text.substring(titleEndIndex + 2).trim();
16067
+ }
16068
+ }
16069
+ logger.debug(`${this.getLogPrefix()} Complete reasoning - Title: "${title}", Has content: ${content.length > 0}`);
16070
+ if (title && !this.toolCallStarted) {
16071
+ this.currentCallId = this.currentCallId || randomUUID();
16072
+ this.sendToolCallStart(title);
16073
+ }
16074
+ if (this.toolCallStarted && this.currentCallId) {
16075
+ const toolResult = {
16076
+ type: "tool-call-result",
16077
+ callId: this.currentCallId,
16078
+ output: {
16079
+ content,
16080
+ status: "completed"
16081
+ },
16082
+ id: randomUUID()
16083
+ };
16084
+ logger.debug(`${this.getLogPrefix()} Sending tool call result`);
16085
+ this.onMessage?.(toolResult);
16086
+ } else if (content.trim()) {
16087
+ const reasoningMessage = {
16088
+ type: "reasoning",
16089
+ message: content,
16090
+ id: randomUUID()
16091
+ };
16092
+ logger.debug(`${this.getLogPrefix()} Sending reasoning message`);
16093
+ this.onMessage?.(reasoningMessage);
16094
+ }
16095
+ this.resetState();
16096
+ return true;
15775
16097
  }
15776
- if (status === "cancelled" || status === "canceled" || status === "aborted" || status === "interrupted") {
15777
- return "cancelled";
16098
+ /**
16099
+ * Abort the current reasoning section.
16100
+ */
16101
+ abort() {
16102
+ logger.debug(`${this.getLogPrefix()} Abort called`);
16103
+ this.finishCurrentToolCall("canceled");
16104
+ this.resetState();
15778
16105
  }
15779
- return "completed";
15780
- }
15781
- function emitHistoricalToolCall(envelopes, turn, item, name, title, args, output) {
15782
- const time = turnTimestampMs(turn);
15783
- const opts = { turn: turn.id, time, codexItemId: item.id };
15784
- envelopes.push(createEnvelope("agent", {
15785
- t: "tool-call-start",
15786
- call: item.id,
15787
- name,
15788
- title,
15789
- description: title,
15790
- args
15791
- }, {
15792
- ...opts,
15793
- id: `${item.id}:start`
15794
- }));
15795
- if (output && output.trim().length > 0) {
15796
- envelopes.push(createEnvelope("agent", {
15797
- t: "text",
15798
- text: output,
15799
- thinking: true
15800
- }, {
15801
- ...opts,
15802
- id: `${item.id}:output`
15803
- }));
16106
+ /**
16107
+ * Reset the processor state.
16108
+ */
16109
+ reset() {
16110
+ this.finishCurrentToolCall("canceled");
16111
+ this.resetState();
15804
16112
  }
15805
- envelopes.push(createEnvelope("agent", {
15806
- t: "tool-call-end",
15807
- call: item.id
15808
- }, {
15809
- ...opts,
15810
- id: `${item.id}:end`,
15811
- time: completedTimestampMs(turn)
15812
- }));
15813
- }
15814
- function mapCodexThreadToSessionEnvelopes(thread) {
15815
- const envelopes = [];
15816
- for (const turn of thread.turns ?? []) {
15817
- const startedAt = turnTimestampMs(turn);
15818
- const completedAt = completedTimestampMs(turn);
15819
- envelopes.push(createEnvelope("agent", { t: "turn-start" }, {
15820
- id: `${turn.id}:start`,
15821
- turn: turn.id,
15822
- time: startedAt
15823
- }));
15824
- for (const item of turn.items ?? []) {
15825
- switch (item.type) {
15826
- case "userMessage": {
15827
- const text = textFromInputItems(item.content);
15828
- if (text) {
15829
- envelopes.push(createEnvelope("user", { t: "text", text }, {
15830
- id: item.id,
15831
- time: startedAt,
15832
- codexItemId: item.id
15833
- }));
15834
- }
15835
- break;
15836
- }
15837
- case "agentMessage": {
15838
- const text = typeof item.text === "string" ? item.text.trim() : "";
15839
- if (text.length > 0) {
15840
- envelopes.push(createEnvelope("agent", { t: "text", text }, {
15841
- id: item.id,
15842
- turn: turn.id,
15843
- time: completedAt,
15844
- codexItemId: item.id
15845
- }));
15846
- }
15847
- break;
15848
- }
15849
- case "reasoning": {
15850
- const text = reasoningText(item);
15851
- if (text) {
15852
- envelopes.push(createEnvelope("agent", { t: "text", text, thinking: true }, {
15853
- id: item.id,
15854
- turn: turn.id,
15855
- time: startedAt,
15856
- codexItemId: item.id
15857
- }));
15858
- }
15859
- break;
15860
- }
15861
- case "commandExecution": {
15862
- const command = typeof item.command === "string" ? item.command : "";
15863
- emitHistoricalToolCall(
15864
- envelopes,
15865
- turn,
15866
- item,
15867
- "CodexBash",
15868
- commandToTitle(command),
15869
- { command, cwd: item.cwd },
15870
- typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : null
15871
- );
15872
- break;
15873
- }
15874
- case "fileChange": {
15875
- const title = "Apply patch";
15876
- emitHistoricalToolCall(
15877
- envelopes,
15878
- turn,
15879
- item,
15880
- "CodexPatch",
15881
- title,
15882
- { changes: item.changes, status: item.status },
15883
- null
15884
- );
15885
- break;
15886
- }
15887
- case "mcpToolCall": {
15888
- const title = `${item.server}.${item.tool}`;
15889
- const output = item.error !== void 0 && item.error !== null ? String(item.error) : item.result !== void 0 && item.result !== null ? String(item.result) : null;
15890
- emitHistoricalToolCall(
15891
- envelopes,
15892
- turn,
15893
- item,
15894
- "McpTool",
15895
- title,
15896
- {
15897
- server: item.server,
15898
- tool: item.tool,
15899
- arguments: item.arguments
15900
- },
15901
- output
15902
- );
15903
- break;
15904
- }
15905
- }
16113
+ /**
16114
+ * Finish current tool call if one is in progress.
16115
+ */
16116
+ finishCurrentToolCall(status) {
16117
+ if (this.toolCallStarted && this.currentCallId) {
16118
+ const toolResult = {
16119
+ type: "tool-call-result",
16120
+ callId: this.currentCallId,
16121
+ output: {
16122
+ content: this.contentBuffer || "",
16123
+ status
16124
+ },
16125
+ id: randomUUID()
16126
+ };
16127
+ logger.debug(`${this.getLogPrefix()} Sending tool call result with status: ${status}`);
16128
+ this.onMessage?.(toolResult);
15906
16129
  }
15907
- envelopes.push(createEnvelope("agent", { t: "turn-end", status: turnStatus(turn) }, {
15908
- id: `${turn.id}:end`,
15909
- turn: turn.id,
15910
- time: completedAt
15911
- }));
15912
16130
  }
15913
- return envelopes;
15914
- }
15915
- function patchDescription(changes) {
15916
- if (!changes || typeof changes !== "object") {
15917
- return "Applying patch";
16131
+ /**
16132
+ * Reset internal state.
16133
+ */
16134
+ resetState() {
16135
+ this.accumulator = "";
16136
+ this.inTitleCapture = false;
16137
+ this.titleBuffer = "";
16138
+ this.contentBuffer = "";
16139
+ this.hasTitle = false;
16140
+ this.currentCallId = null;
16141
+ this.toolCallStarted = false;
16142
+ this.currentTitle = null;
15918
16143
  }
15919
- const fileCount = Object.keys(changes).length;
15920
- if (fileCount === 1) {
15921
- return "Applying patch to 1 file";
16144
+ /**
16145
+ * Get the current call ID for tool result matching.
16146
+ */
16147
+ getCurrentCallId() {
16148
+ return this.currentCallId;
16149
+ }
16150
+ /**
16151
+ * Check if a tool call has been started.
16152
+ */
16153
+ hasStartedToolCall() {
16154
+ return this.toolCallStarted;
15922
16155
  }
15923
- return `Applying patch to ${fileCount} files`;
15924
16156
  }
15925
- function pickTurnEndStatus(message, type) {
15926
- const rawStatus = message.status;
15927
- if (rawStatus === "completed" || rawStatus === "failed" || rawStatus === "cancelled") {
15928
- return rawStatus;
16157
+
16158
+ class ReasoningProcessor extends BaseReasoningProcessor {
16159
+ getToolName() {
16160
+ return "CodexReasoning";
15929
16161
  }
15930
- if (rawStatus === "canceled") {
15931
- return "cancelled";
16162
+ getLogPrefix() {
16163
+ return "[ReasoningProcessor]";
15932
16164
  }
15933
- if (type === "turn_aborted") {
15934
- const reason = message.reason;
15935
- const error = message.error;
15936
- if (typeof reason === "string" && /(fail|error)/i.test(reason) || typeof error === "string" && error.length > 0 || error !== void 0 && error !== null && typeof error === "object") {
15937
- return "failed";
15938
- }
15939
- return "cancelled";
16165
+ /**
16166
+ * Process a reasoning delta and accumulate content.
16167
+ */
16168
+ processDelta(delta) {
16169
+ this.processInput(delta);
15940
16170
  }
15941
- if (message.error !== void 0 && message.error !== null) {
15942
- return "failed";
16171
+ /**
16172
+ * Complete the reasoning section with final text.
16173
+ */
16174
+ complete(fullText) {
16175
+ this.completeReasoning(fullText);
15943
16176
  }
15944
- return "completed";
15945
16177
  }
15946
- function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15947
- const type = message.type;
15948
- const startedSubagents = getStartedSubagents(state);
15949
- const activeSubagents = getActiveSubagents(state);
15950
- const providerSubagentToSessionSubagent = getProviderSubagentToSessionSubagent(state);
15951
- if (type === "task_started") {
15952
- const turnId = createId();
15953
- const turnStart = createEnvelope("agent", { t: "turn-start" }, { turn: turnId });
15954
- startedSubagents.clear();
15955
- activeSubagents.clear();
15956
- providerSubagentToSessionSubagent.clear();
15957
- return {
15958
- currentTurnId: turnId,
15959
- startedSubagents,
15960
- activeSubagents,
15961
- providerSubagentToSessionSubagent,
15962
- envelopes: [turnStart]
15963
- };
15964
- }
15965
- if (type === "task_complete" || type === "turn_aborted") {
15966
- if (!state.currentTurnId) {
15967
- return {
15968
- currentTurnId: null,
15969
- startedSubagents,
15970
- activeSubagents,
15971
- providerSubagentToSessionSubagent,
15972
- envelopes: []
15973
- };
15974
- }
15975
- const lifecycleOpts = { turn: state.currentTurnId };
15976
- providerSubagentToSessionSubagent.clear();
15977
- return {
15978
- currentTurnId: null,
15979
- startedSubagents,
15980
- activeSubagents,
15981
- providerSubagentToSessionSubagent,
15982
- envelopes: [
15983
- ...emitSubagentStops(lifecycleOpts, startedSubagents, activeSubagents),
15984
- createEnvelope("agent", {
15985
- t: "turn-end",
15986
- status: pickTurnEndStatus(message, type)
15987
- }, lifecycleOpts)
15988
- ]
15989
- };
15990
- }
15991
- if (type === "token_count") {
15992
- return {
15993
- currentTurnId: state.currentTurnId,
15994
- startedSubagents,
15995
- activeSubagents,
15996
- providerSubagentToSessionSubagent,
15997
- envelopes: []
15998
- };
16178
+
16179
+ class DiffProcessor {
16180
+ previousDiff = null;
16181
+ onMessage = null;
16182
+ constructor(onMessage) {
16183
+ this.onMessage = onMessage || null;
15999
16184
  }
16000
- const subagent = resolveSessionSubagent(message, providerSubagentToSessionSubagent);
16001
- const opts = buildEnvelopeOptions(state.currentTurnId, subagent);
16002
- if (type === "agent_message") {
16003
- if (typeof message.message !== "string") {
16004
- return {
16005
- currentTurnId: state.currentTurnId,
16006
- startedSubagents,
16007
- activeSubagents,
16008
- providerSubagentToSessionSubagent,
16009
- envelopes: []
16185
+ /**
16186
+ * Process a turn_diff message and check if the unified_diff has changed
16187
+ */
16188
+ processDiff(unifiedDiff) {
16189
+ if (this.previousDiff !== unifiedDiff) {
16190
+ logger.debug("[DiffProcessor] Unified diff changed, sending CodexDiff tool call");
16191
+ const callId = randomUUID();
16192
+ const toolCall = {
16193
+ type: "tool-call",
16194
+ name: "CodexDiff",
16195
+ callId,
16196
+ input: {
16197
+ unified_diff: unifiedDiff
16198
+ },
16199
+ id: randomUUID()
16010
16200
  };
16011
- }
16012
- const envelopes = [];
16013
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16014
- envelopes.push(createEnvelope("agent", { t: "text", text: message.message }, opts));
16015
- return {
16016
- currentTurnId: state.currentTurnId,
16017
- startedSubagents,
16018
- activeSubagents,
16019
- providerSubagentToSessionSubagent,
16020
- envelopes
16021
- };
16022
- }
16023
- if (type === "agent_reasoning" || type === "agent_reasoning_delta") {
16024
- const text = typeof message.text === "string" ? message.text : typeof message.delta === "string" ? message.delta : null;
16025
- if (!text) {
16026
- return {
16027
- currentTurnId: state.currentTurnId,
16028
- startedSubagents,
16029
- activeSubagents,
16030
- providerSubagentToSessionSubagent,
16031
- envelopes: []
16201
+ this.onMessage?.(toolCall);
16202
+ const toolResult = {
16203
+ type: "tool-call-result",
16204
+ callId,
16205
+ output: {
16206
+ status: "completed"
16207
+ },
16208
+ id: randomUUID()
16032
16209
  };
16210
+ this.onMessage?.(toolResult);
16033
16211
  }
16034
- const envelopes = [];
16035
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16036
- envelopes.push(createEnvelope("agent", { t: "text", text, thinking: true }, opts));
16037
- return {
16038
- currentTurnId: state.currentTurnId,
16039
- startedSubagents,
16040
- activeSubagents,
16041
- providerSubagentToSessionSubagent,
16042
- envelopes
16043
- };
16212
+ this.previousDiff = unifiedDiff;
16213
+ logger.debug("[DiffProcessor] Updated stored diff");
16044
16214
  }
16045
- if (type === "exec_command_begin") {
16046
- const call = pickCallId(message);
16047
- const { call_id: _callIdSnake, callId: _callIdCamel, type: _type, ...args } = message;
16048
- const command = summarizeCommand(args.command);
16049
- const description = typeof args.description === "string" ? args.description : command ?? "Execute command";
16050
- const envelopes = [];
16051
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16052
- envelopes.push(
16053
- createEnvelope("agent", {
16054
- t: "tool-call-start",
16055
- call,
16056
- name: "CodexBash",
16057
- title: commandToTitle(command),
16058
- description,
16059
- args
16060
- }, opts)
16061
- );
16062
- return {
16063
- currentTurnId: state.currentTurnId,
16064
- startedSubagents,
16065
- activeSubagents,
16066
- providerSubagentToSessionSubagent,
16067
- envelopes
16068
- };
16215
+ /**
16216
+ * Reset the processor state (called on task_complete or turn_aborted)
16217
+ */
16218
+ reset() {
16219
+ logger.debug("[DiffProcessor] Resetting diff state");
16220
+ this.previousDiff = null;
16069
16221
  }
16070
- if (type === "exec_command_end") {
16071
- const call = pickCallId(message);
16072
- const envelopes = [];
16073
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16074
- envelopes.push(createEnvelope("agent", { t: "tool-call-end", call }, opts));
16075
- return {
16076
- currentTurnId: state.currentTurnId,
16077
- startedSubagents,
16078
- activeSubagents,
16079
- providerSubagentToSessionSubagent,
16080
- envelopes
16081
- };
16222
+ /**
16223
+ * Set the message callback for sending messages directly
16224
+ */
16225
+ setMessageCallback(callback) {
16226
+ this.onMessage = callback;
16082
16227
  }
16083
- if (type === "patch_apply_begin") {
16084
- const call = pickCallId(message);
16085
- const autoApproved = message.auto_approved;
16086
- const changes = message.changes;
16087
- const envelopes = [];
16088
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16089
- envelopes.push(
16090
- createEnvelope("agent", {
16091
- t: "tool-call-start",
16092
- call,
16093
- name: "CodexPatch",
16094
- title: "Apply patch",
16095
- description: patchDescription(changes),
16096
- args: {
16097
- auto_approved: autoApproved,
16098
- changes
16099
- }
16100
- }, opts)
16101
- );
16102
- return {
16103
- currentTurnId: state.currentTurnId,
16104
- startedSubagents,
16105
- activeSubagents,
16106
- providerSubagentToSessionSubagent,
16107
- envelopes
16108
- };
16228
+ /**
16229
+ * Get the current diff value
16230
+ */
16231
+ getCurrentDiff() {
16232
+ return this.previousDiff;
16109
16233
  }
16110
- if (type === "patch_apply_end") {
16111
- const call = pickCallId(message);
16112
- const envelopes = [];
16113
- maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
16114
- envelopes.push(createEnvelope("agent", { t: "tool-call-end", call }, opts));
16115
- return {
16116
- currentTurnId: state.currentTurnId,
16117
- startedSubagents,
16118
- activeSubagents,
16119
- providerSubagentToSessionSubagent,
16120
- envelopes
16234
+ }
16235
+
16236
+ const CodexDisplay = ({ messageBuffer, logPath, onExit }) => {
16237
+ const [messages, setMessages] = useState([]);
16238
+ const [confirmationMode, setConfirmationMode] = useState(false);
16239
+ const [actionInProgress, setActionInProgress] = useState(false);
16240
+ const confirmationTimeoutRef = useRef(null);
16241
+ const { stdout } = useStdout();
16242
+ const terminalWidth = stdout.columns || 80;
16243
+ const terminalHeight = stdout.rows || 24;
16244
+ useEffect(() => {
16245
+ setMessages(messageBuffer.getMessages());
16246
+ const unsubscribe = messageBuffer.onUpdate((newMessages) => {
16247
+ setMessages(newMessages);
16248
+ });
16249
+ return () => {
16250
+ unsubscribe();
16251
+ if (confirmationTimeoutRef.current) {
16252
+ clearTimeout(confirmationTimeoutRef.current);
16253
+ }
16121
16254
  };
16122
- }
16255
+ }, [messageBuffer]);
16256
+ const resetConfirmation = useCallback(() => {
16257
+ setConfirmationMode(false);
16258
+ if (confirmationTimeoutRef.current) {
16259
+ clearTimeout(confirmationTimeoutRef.current);
16260
+ confirmationTimeoutRef.current = null;
16261
+ }
16262
+ }, []);
16263
+ const setConfirmationWithTimeout = useCallback(() => {
16264
+ setConfirmationMode(true);
16265
+ if (confirmationTimeoutRef.current) {
16266
+ clearTimeout(confirmationTimeoutRef.current);
16267
+ }
16268
+ confirmationTimeoutRef.current = setTimeout(() => {
16269
+ resetConfirmation();
16270
+ }, 15e3);
16271
+ }, [resetConfirmation]);
16272
+ useInput(useCallback(async (input, key) => {
16273
+ if (actionInProgress) return;
16274
+ if (key.ctrl && input === "c") {
16275
+ if (confirmationMode) {
16276
+ resetConfirmation();
16277
+ setActionInProgress(true);
16278
+ await new Promise((resolve) => setTimeout(resolve, 100));
16279
+ onExit?.();
16280
+ } else {
16281
+ setConfirmationWithTimeout();
16282
+ }
16283
+ return;
16284
+ }
16285
+ if (confirmationMode) {
16286
+ resetConfirmation();
16287
+ }
16288
+ }, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
16289
+ const getMessageColor = (type) => {
16290
+ switch (type) {
16291
+ case "user":
16292
+ return "magenta";
16293
+ case "assistant":
16294
+ return "cyan";
16295
+ case "system":
16296
+ return "blue";
16297
+ case "tool":
16298
+ return "yellow";
16299
+ case "result":
16300
+ return "green";
16301
+ case "status":
16302
+ return "gray";
16303
+ default:
16304
+ return "white";
16305
+ }
16306
+ };
16307
+ const formatMessage = (msg) => {
16308
+ const lines = msg.content.split("\n");
16309
+ const maxLineLength = terminalWidth - 10;
16310
+ return lines.map((line) => {
16311
+ if (line.length <= maxLineLength) return line;
16312
+ const chunks = [];
16313
+ for (let i = 0; i < line.length; i += maxLineLength) {
16314
+ chunks.push(line.slice(i, i + maxLineLength));
16315
+ }
16316
+ return chunks.join("\n");
16317
+ }).join("\n");
16318
+ };
16319
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
16320
+ Box,
16321
+ {
16322
+ flexDirection: "column",
16323
+ width: terminalWidth,
16324
+ height: terminalHeight - 4,
16325
+ borderStyle: "round",
16326
+ borderColor: "gray",
16327
+ paddingX: 1,
16328
+ overflow: "hidden"
16329
+ },
16330
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "\u{1F916} Codex Agent Messages"), /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
16331
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
16332
+ // Show only the last messages that fit in the available space
16333
+ messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
16334
+ ))
16335
+ ), /* @__PURE__ */ React.createElement(
16336
+ Box,
16337
+ {
16338
+ width: terminalWidth,
16339
+ borderStyle: "round",
16340
+ borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "green",
16341
+ paddingX: 2,
16342
+ justifyContent: "center",
16343
+ alignItems: "center",
16344
+ flexDirection: "column"
16345
+ },
16346
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", alignItems: "center" }, actionInProgress ? /* @__PURE__ */ React.createElement(Text, { color: "gray", bold: true }, "Exiting agent...") : confirmationMode ? /* @__PURE__ */ React.createElement(Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit the agent") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "green", bold: true }, "\u{1F916} Codex Agent Running \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
16347
+ ));
16348
+ };
16349
+
16350
+ function createOfflineSessionStub(sessionTag) {
16123
16351
  return {
16124
- currentTurnId: state.currentTurnId,
16125
- startedSubagents,
16126
- activeSubagents,
16127
- providerSubagentToSessionSubagent,
16128
- envelopes: []
16352
+ sessionId: `offline-${sessionTag}`,
16353
+ sendCodexMessage: () => {
16354
+ },
16355
+ sendAgentMessage: () => {
16356
+ },
16357
+ sendAgentUsageSnapshot: () => false,
16358
+ sendClaudeSessionMessage: () => {
16359
+ },
16360
+ keepAlive: () => {
16361
+ },
16362
+ sendSessionEvent: () => {
16363
+ },
16364
+ sendSessionDeath: () => {
16365
+ },
16366
+ updateLifecycleState: () => {
16367
+ },
16368
+ requestControlTransfer: async () => {
16369
+ },
16370
+ flush: async () => {
16371
+ },
16372
+ close: async () => {
16373
+ },
16374
+ updateMetadata: () => {
16375
+ },
16376
+ updateAgentState: () => {
16377
+ },
16378
+ onUserMessage: () => {
16379
+ },
16380
+ onFileEvent: () => {
16381
+ },
16382
+ trackAttachmentDownload: () => {
16383
+ },
16384
+ drainAttachmentsForUserMessage: async () => [],
16385
+ rpcHandlerManager: {
16386
+ registerHandler: () => {
16387
+ }
16388
+ }
16129
16389
  };
16130
16390
  }
16131
- function mapCodexProcessorMessageToSessionEnvelopes(message, state) {
16132
- const toolLikeMessage = message;
16133
- const opts = buildEnvelopeOptions(state.currentTurnId);
16134
- if (message.type === "reasoning") {
16135
- return [createEnvelope("agent", {
16136
- t: "text",
16137
- text: message.message,
16138
- thinking: true
16139
- }, opts)];
16391
+
16392
+ function setupOfflineReconnection(opts) {
16393
+ const { api, sessionTag, metadata, state, response, onSessionSwap } = opts;
16394
+ let session;
16395
+ let reconnectionHandle = null;
16396
+ if (!response) {
16397
+ session = createOfflineSessionStub(sessionTag);
16398
+ reconnectionHandle = startOfflineReconnection({
16399
+ serverUrl: configuration.serverUrl,
16400
+ onReconnected: async () => {
16401
+ const resp = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
16402
+ if (!resp) throw new Error("Server unavailable");
16403
+ const realSession = api.sessionSyncClient(resp);
16404
+ onSessionSwap(realSession);
16405
+ return realSession;
16406
+ },
16407
+ onNotify: (msg) => {
16408
+ console.log(msg);
16409
+ }
16410
+ });
16411
+ return { session, reconnectionHandle, isOffline: true };
16412
+ } else {
16413
+ session = api.sessionSyncClient(response, opts.sessionClientOptions);
16414
+ return { session, reconnectionHandle: null, isOffline: false };
16140
16415
  }
16141
- if (message.type === "tool-call") {
16142
- const title = typeof toolLikeMessage.input?.title === "string" ? toolLikeMessage.input.title : `${toolLikeMessage.name || "Tool"} call`;
16143
- return [createEnvelope("agent", {
16144
- t: "tool-call-start",
16145
- call: toolLikeMessage.callId,
16146
- name: toolLikeMessage.name || "unknown",
16147
- title,
16148
- description: title,
16149
- args: toolLikeMessage.input && typeof toolLikeMessage.input === "object" ? toolLikeMessage.input : {}
16150
- }, opts)];
16416
+ }
16417
+
16418
+ function resolveCodexExecutionPolicy(permissionMode, sandboxManagedByHappy) {
16419
+ if (sandboxManagedByHappy) {
16420
+ return {
16421
+ approvalPolicy: "never",
16422
+ sandbox: "danger-full-access"
16423
+ };
16151
16424
  }
16152
- if (message.type === "tool-call-result") {
16153
- const envelopes = [];
16154
- const content = toolLikeMessage.output?.content;
16155
- if (typeof content === "string" && content.trim().length > 0) {
16156
- envelopes.push(createEnvelope("agent", {
16157
- t: "text",
16158
- text: content,
16159
- thinking: true
16160
- }, opts));
16425
+ const approvalPolicy = (() => {
16426
+ switch (permissionMode) {
16427
+ // Codex native modes
16428
+ case "default":
16429
+ return "untrusted";
16430
+ // Ask for non-trusted commands
16431
+ case "read-only":
16432
+ return "never";
16433
+ // Never ask, read-only enforced by sandbox
16434
+ case "safe-yolo":
16435
+ return "on-request";
16436
+ // Workspace auto-run; request approval for escalation (0.153 removed on-failure)
16437
+ case "yolo":
16438
+ return "never";
16439
+ // Full YOLO: never interrupt for approvals
16440
+ // Defensive fallback for Claude-specific modes (backward compatibility)
16441
+ case "bypassPermissions":
16442
+ return "never";
16443
+ // Full access: map to yolo behavior
16444
+ case "acceptEdits":
16445
+ return "on-request";
16446
+ // Let model decide (closest to auto-approve edits)
16447
+ case "plan":
16448
+ return "untrusted";
16449
+ // Conservative: ask for non-trusted
16450
+ default:
16451
+ return "untrusted";
16161
16452
  }
16162
- envelopes.push(createEnvelope("agent", {
16163
- t: "tool-call-end",
16164
- call: toolLikeMessage.callId
16165
- }, opts));
16166
- return envelopes;
16167
- }
16168
- return [];
16453
+ })();
16454
+ const sandbox = (() => {
16455
+ switch (permissionMode) {
16456
+ // Codex native modes
16457
+ case "default":
16458
+ return "workspace-write";
16459
+ // Can write in workspace
16460
+ case "read-only":
16461
+ return "read-only";
16462
+ // Read-only filesystem
16463
+ case "safe-yolo":
16464
+ return "workspace-write";
16465
+ // Can write in workspace
16466
+ case "yolo":
16467
+ return "danger-full-access";
16468
+ // Full system access
16469
+ // Defensive fallback for Claude-specific modes
16470
+ case "bypassPermissions":
16471
+ return "danger-full-access";
16472
+ // Full access: map to yolo
16473
+ case "acceptEdits":
16474
+ return "workspace-write";
16475
+ // Can edit files in workspace
16476
+ case "plan":
16477
+ return "workspace-write";
16478
+ // Can write for planning
16479
+ default:
16480
+ return "workspace-write";
16481
+ }
16482
+ })();
16483
+ return { approvalPolicy, sandbox };
16169
16484
  }
16170
16485
 
16171
16486
  async function resumeExistingThread(opts) {
@@ -16637,6 +16952,13 @@ async function runCodex(opts) {
16637
16952
  process.exit(1);
16638
16953
  }
16639
16954
  };
16955
+ session.rpcHandlerManager.registerHandler("codex-child-read", async (args) => {
16956
+ try {
16957
+ return await readChildThread(client, args?.threadId);
16958
+ } catch (error) {
16959
+ return { error: error instanceof Error ? error.message : "Child read failed" };
16960
+ }
16961
+ });
16640
16962
  session.rpcHandlerManager.registerHandler("abort", handleAbort);
16641
16963
  const disposeTermination = registerKillSessionHandler(session.rpcHandlerManager, handleKillSession, session, { processSignals: true });
16642
16964
  const messageBuffer = new MessageBuffer();
@@ -17177,7 +17499,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17177
17499
  process.exit(0);
17178
17500
  } else if (subcommand === "install-terminal-hooks") {
17179
17501
  try {
17180
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-BT4Dq28Y.mjs');
17502
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-BWoHGCew.mjs');
17181
17503
  const command = parseTerminalHooksArgs(args.slice(1));
17182
17504
  if (command.action === "help") {
17183
17505
  console.log(TERMINAL_HOOKS_HELP);
@@ -17194,7 +17516,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17194
17516
  }
17195
17517
  } else if (subcommand === "spawn") {
17196
17518
  try {
17197
- const { handleSpawnCommand } = await import('./spawn-QtKlr8Ig.mjs');
17519
+ const { handleSpawnCommand } = await import('./spawn-CF1ZV-YH.mjs');
17198
17520
  await handleSpawnCommand(args.slice(1));
17199
17521
  } catch (error) {
17200
17522
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -17206,7 +17528,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17206
17528
  return;
17207
17529
  } else if (subcommand === "sessions") {
17208
17530
  try {
17209
- const { handleSessionsCommand } = await import('./sessions-CJuwvEr0.mjs');
17531
+ const { handleSessionsCommand } = await import('./sessions-5fJxQ2xj.mjs');
17210
17532
  await handleSessionsCommand(args.slice(1));
17211
17533
  } catch (error) {
17212
17534
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -17218,7 +17540,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17218
17540
  return;
17219
17541
  } else if (subcommand === "send") {
17220
17542
  try {
17221
- const { handleSendCommand } = await import('./send-DHShoBeq.mjs');
17543
+ const { handleSendCommand } = await import('./send-fWHOFJV9.mjs');
17222
17544
  await handleSendCommand(args.slice(1));
17223
17545
  } catch (error) {
17224
17546
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -17324,9 +17646,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17324
17646
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
17325
17647
  const projectId = args[3];
17326
17648
  try {
17327
- const { saveGoogleCloudProjectToConfig } = await import('./config-C4gWfnJX.mjs');
17328
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-BGeIBBE3.mjs').then(function (n) { return n.W; });
17329
- const { ApiClient: ApiClient2 } = await import('./types-Cq-tvv9t.mjs').then(function (n) { return n.$; });
17649
+ const { saveGoogleCloudProjectToConfig } = await import('./config-DzvbcL_x.mjs');
17650
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-CPTgiQ9A.mjs').then(function (n) { return n.W; });
17651
+ const { ApiClient: ApiClient2 } = await import('./types-BEXGahmj.mjs').then(function (n) { return n.a0; });
17330
17652
  let userEmail = void 0;
17331
17653
  try {
17332
17654
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -17357,7 +17679,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17357
17679
  }
17358
17680
  if (geminiSubcommand === "project" && args[2] === "get") {
17359
17681
  try {
17360
- const { readGeminiLocalConfig } = await import('./config-C4gWfnJX.mjs');
17682
+ const { readGeminiLocalConfig } = await import('./config-DzvbcL_x.mjs');
17361
17683
  const config = readGeminiLocalConfig();
17362
17684
  if (config.googleCloudProject) {
17363
17685
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -17397,7 +17719,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17397
17719
  process.exit(0);
17398
17720
  }
17399
17721
  try {
17400
- const { runGemini } = await import('./runGemini-DGTNwiLo.mjs');
17722
+ const { runGemini } = await import('./runGemini-BVJE7LZl.mjs');
17401
17723
  let startedBy = void 0;
17402
17724
  for (let i = 1; i < args.length; i++) {
17403
17725
  if (args[i] === "--started-by") {
@@ -17427,7 +17749,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17427
17749
  }
17428
17750
  };
17429
17751
  try {
17430
- const { runAcp, resolveAcpAgentConfig } = await import('./index-B6UvB88K.mjs');
17752
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-C33JB4xB.mjs');
17431
17753
  const parsed = parsePiRunnerArgs(args.slice(1));
17432
17754
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
17433
17755
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -17455,7 +17777,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17455
17777
  return;
17456
17778
  } else if (subcommand === "acp") {
17457
17779
  try {
17458
- const { runAcp, resolveAcpAgentConfig } = await import('./index-B6UvB88K.mjs');
17780
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-C33JB4xB.mjs');
17459
17781
  let startedBy = void 0;
17460
17782
  let verbose = false;
17461
17783
  const acpArgs = [];
@@ -17495,7 +17817,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17495
17817
  return;
17496
17818
  } else if (subcommand === "openclaw") {
17497
17819
  try {
17498
- const { runOpenClaw } = await import('./runOpenClaw-CxgTFOSM.mjs');
17820
+ const { runOpenClaw } = await import('./runOpenClaw-CJaIYpB7.mjs');
17499
17821
  let startedBy = void 0;
17500
17822
  let verbose = false;
17501
17823
  let gatewayUrl;
@@ -17546,7 +17868,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17546
17868
  return;
17547
17869
  } else if (subcommand === "todo") {
17548
17870
  try {
17549
- const { handleTodoCommand } = await import('./todo-CkJ5UjWs.mjs');
17871
+ const { handleTodoCommand } = await import('./todo-BAK2DVLV.mjs');
17550
17872
  await handleTodoCommand(args.slice(1));
17551
17873
  } catch (error) {
17552
17874
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -17555,7 +17877,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17555
17877
  return;
17556
17878
  } else if (subcommand === "teams") {
17557
17879
  try {
17558
- const { handleTeamsCommand } = await import('./teams-BF-GxUZc.mjs');
17880
+ const { handleTeamsCommand } = await import('./teams-gzvKrMfn.mjs');
17559
17881
  await handleTeamsCommand(args.slice(1));
17560
17882
  } catch (error) {
17561
17883
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -17564,7 +17886,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17564
17886
  return;
17565
17887
  } else if (subcommand === "mcp" && args.length === 1) {
17566
17888
  try {
17567
- const { handleMcpCommand } = await import('./mcp-BGu_dUaJ.mjs');
17889
+ const { handleMcpCommand } = await import('./mcp-DhuxpwTB.mjs');
17568
17890
  await handleMcpCommand();
17569
17891
  } catch (error) {
17570
17892
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}