@loicngr/kobo 1.7.6 → 1.7.8

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 (110) hide show
  1. package/AGENTS.md +29 -0
  2. package/README.md +146 -4
  3. package/dist/mcp-server/kobo-tasks-server.js +27 -0
  4. package/dist/server/index.js +2 -0
  5. package/dist/server/routes/health.js +14 -0
  6. package/dist/server/routes/voice.js +149 -0
  7. package/dist/server/routes/workspaces.js +33 -9
  8. package/dist/server/services/agent/engines/claude-code/capabilities.js +7 -0
  9. package/dist/server/services/agent/engines/codex/capabilities.js +18 -0
  10. package/dist/server/services/agent/engines/codex/client.js +36 -0
  11. package/dist/server/services/agent/engines/codex/engine.js +276 -0
  12. package/dist/server/services/agent/engines/codex/event-mapper.js +473 -0
  13. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +60 -0
  14. package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +31 -0
  15. package/dist/server/services/agent/engines/codex/options-builder.js +81 -0
  16. package/dist/server/services/agent/engines/codex/protocol/types.js +11 -0
  17. package/dist/server/services/agent/engines/codex/server-requests.js +99 -0
  18. package/dist/server/services/agent/engines/codex/spawn.js +27 -0
  19. package/dist/server/services/agent/engines/registry.js +2 -0
  20. package/dist/server/services/agent/orchestrator.js +1 -1
  21. package/dist/server/services/settings-service.js +125 -6
  22. package/dist/server/services/transcription-service.js +206 -0
  23. package/dist/server/utils/paths.js +7 -0
  24. package/dist/shared/codex-models.js +43 -0
  25. package/package.json +13 -10
  26. package/src/client/dist/spa/assets/ActivityFeed-CPZdjJpH.js +8 -0
  27. package/src/client/dist/spa/assets/{ActivityFeed-tE4LVYck.css → ActivityFeed-WjiQ9716.css} +1 -1
  28. package/src/client/dist/spa/assets/{ClosePopup-D_UAdwkA.js → ClosePopup-C5JlH6Hy.js} +1 -1
  29. package/src/client/dist/spa/assets/CreatePage-CdfbFlXf.js +2 -0
  30. package/src/client/dist/spa/assets/CreatePage-ZyBHUbl0.css +1 -0
  31. package/src/client/dist/spa/assets/{DiffViewer-CblFgn8w.js → DiffViewer-DkiP6nWz.js} +3 -3
  32. package/src/client/dist/spa/assets/HealthPage-BHGZJTgS.js +1 -0
  33. package/src/client/dist/spa/assets/{MainLayout-DhaYycak.js → MainLayout-C0tClQZl.js} +17 -17
  34. package/src/client/dist/spa/assets/{MainLayout-drolsINz.css → MainLayout-DKnTGN_Q.css} +1 -1
  35. package/src/client/dist/spa/assets/{QBadge-DWH42dbo.js → QBadge-C7r6oPSi.js} +1 -1
  36. package/src/client/dist/spa/assets/{QBtn-a6jxWjmW.js → QBtn-DEuWKHbR.js} +1 -1
  37. package/src/client/dist/spa/assets/{QCheckbox-D5jfsxLV.js → QCheckbox-BvHfXBFY.js} +1 -1
  38. package/src/client/dist/spa/assets/{QChip-ByxK0Tuf.js → QChip-erWIZgxW.js} +1 -1
  39. package/src/client/dist/spa/assets/{QExpansionItem-CH1ipL9n.js → QExpansionItem-CW6sPoP9.js} +1 -1
  40. package/src/client/dist/spa/assets/QIcon-qfJNZLIW.js +1 -0
  41. package/src/client/dist/spa/assets/{QInput-Cm5-AGQ4.js → QInput-DCJEwE8V.js} +1 -1
  42. package/src/client/dist/spa/assets/{QItemLabel-DrTxqTqV.js → QItemLabel-CHkgkZVj.js} +1 -1
  43. package/src/client/dist/spa/assets/{QItemSection-5YpFpPDm.js → QItemSection-CQUDd0Vg.js} +1 -1
  44. package/src/client/dist/spa/assets/{QList-D0FtnQJI.js → QList-BbnN_oNX.js} +1 -1
  45. package/src/client/dist/spa/assets/{QMenu-B4xMxMGd.js → QMenu-CaVfoMu6.js} +1 -1
  46. package/src/client/dist/spa/assets/{QPage-DFi3K093.js → QPage-Co2h9wd_.js} +1 -1
  47. package/src/client/dist/spa/assets/{QRadio-B3aKjCVu.js → QRadio-DJxOyOA3.js} +1 -1
  48. package/src/client/dist/spa/assets/QSpace-DKIph84L.js +1 -0
  49. package/src/client/dist/spa/assets/{QSpinnerDots-CszPQQ9J.js → QSpinnerDots-Bfl2RMy4.js} +1 -1
  50. package/src/client/dist/spa/assets/{QTabPanels-D2ks0UIA.js → QTabPanels-E66qDYmr.js} +1 -1
  51. package/src/client/dist/spa/assets/{QToggle-1-N9qWq4.js → QToggle-DNOTC_3a.js} +1 -1
  52. package/src/client/dist/spa/assets/{QTooltip-fDNzBEfN.js → QTooltip-DYey0zHV.js} +1 -1
  53. package/src/client/dist/spa/assets/{SearchPage-cZTwP4Lf.js → SearchPage-BaI3iU58.js} +1 -1
  54. package/src/client/dist/spa/assets/SettingsPage-BqBOQKeM.js +9 -0
  55. package/src/client/dist/spa/assets/SettingsPage-Zeu2cZqi.css +1 -0
  56. package/src/client/dist/spa/assets/{TouchPan-DoE24Io3.js → TouchPan-DQILDzd3.js} +1 -1
  57. package/src/client/dist/spa/assets/WorkspacePage-C9eT5LAo.css +1 -0
  58. package/src/client/dist/spa/assets/WorkspacePage-DqMyUSFG.js +4 -0
  59. package/src/client/dist/spa/assets/{build-path-tree-B1Lvvqto.js → build-path-tree-BpcCBm9A.js} +1 -1
  60. package/src/client/dist/spa/assets/{cssMode-BFLYiiEw.js → cssMode-BaeNVqUm.js} +1 -1
  61. package/src/client/dist/spa/assets/{documents-kx0vLfSG.js → documents-soWtna0O.js} +1 -1
  62. package/src/client/dist/spa/assets/{editor.api-2asmmhth.js → editor.api-DMLl_PBy.js} +1 -1
  63. package/src/client/dist/spa/assets/{editor.main-ChCYZyez.js → editor.main-D2pRsQAX.js} +3 -3
  64. package/src/client/dist/spa/assets/{AutoLoopChip-w8D77bI5.js → engineFeatures-RffgP255.js} +1 -1
  65. package/src/client/dist/spa/assets/{expand-template-CXQFkQOJ.js → expand-template-z2wIJOD2.js} +1 -1
  66. package/src/client/dist/spa/assets/{formatters-DCAQ6ANJ.js → formatters-guwb-rzl.js} +1 -1
  67. package/src/client/dist/spa/assets/{freemarker2-BaBL9E9G.js → freemarker2-Bh6ItnVy.js} +1 -1
  68. package/src/client/dist/spa/assets/{handlebars-BxDour4L.js → handlebars-D8OXeysi.js} +1 -1
  69. package/src/client/dist/spa/assets/{html-C6hnkfIL.js → html-9Y1AHhvw.js} +1 -1
  70. package/src/client/dist/spa/assets/{htmlMode-9zT3-dmz.js → htmlMode-z00se0fQ.js} +1 -1
  71. package/src/client/dist/spa/assets/i18n-C-VMW7h5.js +1 -0
  72. package/src/client/dist/spa/assets/index-BLlWqEZC.js +2 -0
  73. package/src/client/dist/spa/assets/{javascript-C3YjvKbE.js → javascript-D0LSb7WU.js} +1 -1
  74. package/src/client/dist/spa/assets/{jsonMode-DcJDgMzf.js → jsonMode-BSmyaoX3.js} +1 -1
  75. package/src/client/dist/spa/assets/{liquid-CsT8SjJM.js → liquid-BsY5UXNl.js} +1 -1
  76. package/src/client/dist/spa/assets/{mdx-CT3yVSyc.js → mdx-BUcXih4e.js} +1 -1
  77. package/src/client/dist/spa/assets/{monaco.contribution-DKGNz1oQ.js → monaco.contribution-DrpufOT3.js} +2 -2
  78. package/src/client/dist/spa/assets/{notifications-OnPq4FrH.js → notifications-C255ApfS.js} +1 -1
  79. package/src/client/dist/spa/assets/permissionModes-BocOmzU8.js +1 -0
  80. package/src/client/dist/spa/assets/{purify.es-CPieV82n.js → purify.es-aV6SU8N4.js} +1 -1
  81. package/src/client/dist/spa/assets/{python-Ca5miKgj.js → python-C0PoB7M8.js} +1 -1
  82. package/src/client/dist/spa/assets/{razor-7qzusGRc.js → razor-Bu0-fwxD.js} +1 -1
  83. package/src/client/dist/spa/assets/{render-chat-markdown-Bqq2G-yI.js → render-chat-markdown-DALCdDVE.js} +1 -1
  84. package/src/client/dist/spa/assets/runtime-core.esm-bundler-9Z0QAO_7.js +1 -0
  85. package/src/client/dist/spa/assets/{tsMode-BdvO8jZ2.js → tsMode-Blc1d2dp.js} +1 -1
  86. package/src/client/dist/spa/assets/{typescript-BfVNzhgs.js → typescript-CV4ME9fo.js} +1 -1
  87. package/src/client/dist/spa/assets/{use-checkbox-D7zmRxGI.js → use-checkbox-y_fOkYZN.js} +1 -1
  88. package/src/client/dist/spa/assets/{use-id-CuaR1RiE.js → use-id-_7wiRcgb.js} +1 -1
  89. package/src/client/dist/spa/assets/{use-panel-D-8nAQns.js → use-panel-DCPiSURS.js} +1 -1
  90. package/src/client/dist/spa/assets/use-quasar-DQYS47mh.js +1 -0
  91. package/src/client/dist/spa/assets/{vue-i18n-BcfTCFFS.js → vue-i18n-DI-gS-CC.js} +1 -1
  92. package/src/client/dist/spa/assets/{xml-DGNXGqXL.js → xml-DLYRBBbI.js} +1 -1
  93. package/src/client/dist/spa/assets/{yaml-CtAtOyt5.js → yaml-QIBjI5Dl.js} +1 -1
  94. package/src/client/dist/spa/index.html +12 -12
  95. package/src/mcp-server/kobo-tasks-server.ts +27 -0
  96. package/src/client/dist/spa/assets/ActivityFeed-BboSPm4b.js +0 -7
  97. package/src/client/dist/spa/assets/CreatePage-BDObLDJc.js +0 -2
  98. package/src/client/dist/spa/assets/CreatePage-DssmsAsV.css +0 -1
  99. package/src/client/dist/spa/assets/HealthPage-CBSw7e5q.js +0 -1
  100. package/src/client/dist/spa/assets/QIcon-BJuyqdsT.js +0 -1
  101. package/src/client/dist/spa/assets/QSpace-CLtL3aPy.js +0 -1
  102. package/src/client/dist/spa/assets/SettingsPage-C1efO0VM.js +0 -1
  103. package/src/client/dist/spa/assets/SettingsPage-CMyeQ9_u.css +0 -1
  104. package/src/client/dist/spa/assets/WorkspacePage-3jcof896.js +0 -4
  105. package/src/client/dist/spa/assets/WorkspacePage-CCtIrBiR.css +0 -1
  106. package/src/client/dist/spa/assets/i18n-CLY0XI9-.js +0 -1
  107. package/src/client/dist/spa/assets/index-D6wj_wQ9.js +0 -2
  108. package/src/client/dist/spa/assets/models-BsjWUKqM.js +0 -1
  109. package/src/client/dist/spa/assets/runtime-core.esm-bundler-C3IgBgY5.js +0 -1
  110. package/src/client/dist/spa/assets/use-quasar-Sdcq6zzV.js +0 -1
@@ -42,18 +42,16 @@ function isAgentPermissionMode(value) {
42
42
  *
43
43
  * Cascade: explicit body field → global default (validated) → 'bypass'.
44
44
  *
45
- * Legacy `defaultPermissionMode` values ('plan' / 'auto-accept') are honored:
46
- * 'plan' stays 'plan'; 'auto-accept' falls through to 'bypass' (the safest
47
- * non-plan default matches the pre-refactor "skip prompts" behaviour).
45
+ * The per-engine default lives in `defaultPermissionModeByEngine[engineId]`
46
+ * (added in settings v20). If the engine id is missing or its entry is invalid,
47
+ * we fall back to 'bypass' (the safest non-plan default).
48
48
  */
49
- function resolveCreateAgentPermissionMode(bodyValue, _projectPath, globalSettings) {
49
+ function resolveCreateAgentPermissionMode(bodyValue, _projectPath, globalSettings, engineId) {
50
50
  if (isAgentPermissionMode(bodyValue))
51
51
  return bodyValue;
52
- const global = globalSettings.defaultPermissionMode;
52
+ const global = globalSettings.defaultPermissionModeByEngine?.[engineId];
53
53
  if (isAgentPermissionMode(global))
54
54
  return global;
55
- if (global === 'plan')
56
- return 'plan';
57
55
  return 'bypass';
58
56
  }
59
57
  app.get('/', (c) => {
@@ -82,10 +80,25 @@ app.post('/', migrationGuard, async (c) => {
82
80
  // engine is rejected up-front so we don't create orphan workspaces that
83
81
  // can't spawn an agent.
84
82
  if (body.engine) {
85
- const validEngineIds = listEngines().map((e) => e.id);
83
+ const engines = listEngines();
84
+ const validEngineIds = engines.map((e) => e.id);
86
85
  if (!validEngineIds.includes(body.engine)) {
87
86
  return c.json({ error: `Unknown engine '${body.engine}'. Valid engines: ${validEngineIds.join(', ')}` }, 400);
88
87
  }
88
+ // Cross-validate engine × permission mode: each engine declares which
89
+ // modes it supports via `capabilities.permissionModes`. The UI already
90
+ // filters, but API consumers can still send any combo. Reject up-front
91
+ // so we don't park workspaces in a permanently broken state (e.g. Codex
92
+ // workspaces with `interactive` mode hang on the first tool call).
93
+ if (body.agentPermissionMode) {
94
+ const engine = engines.find((e) => e.id === body.engine);
95
+ const supported = engine?.capabilities.permissionModes ?? [];
96
+ if (!supported.includes(body.agentPermissionMode)) {
97
+ return c.json({
98
+ error: `Engine '${body.engine}' does not support agentPermissionMode '${body.agentPermissionMode}'. Supported: ${supported.join(', ')}`,
99
+ }, 400);
100
+ }
101
+ }
89
102
  }
90
103
  // Fetch the source branch from origin first — if this fails, block creation
91
104
  // immediately (no DB records created, user stays on the create page).
@@ -222,7 +235,7 @@ app.post('/', migrationGuard, async (c) => {
222
235
  worktreeOwned: !useReusedWorktree,
223
236
  model: body.model,
224
237
  reasoningEffort: body.reasoningEffort,
225
- agentPermissionMode: resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, globalSettings),
238
+ agentPermissionMode: resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, globalSettings, body.engine ?? 'claude-code'),
226
239
  engine: body.engine,
227
240
  ...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
228
241
  });
@@ -1451,6 +1464,17 @@ app.patch('/:id', migrationGuard, async (c) => {
1451
1464
  if (!isAgentPermissionMode(body.agentPermissionMode)) {
1452
1465
  return c.json({ error: `Invalid agentPermissionMode. Must be one of: ${VALID_AGENT_PERMISSION_MODES.join(', ')}` }, 400);
1453
1466
  }
1467
+ // Cross-validate against the engine's declared capabilities — see the
1468
+ // POST route for the same guard. Prevents parking a workspace in a mode
1469
+ // its engine cannot honour.
1470
+ const engineId = workspace.engine;
1471
+ const engine = listEngines().find((e) => e.id === engineId);
1472
+ const supported = engine?.capabilities.permissionModes ?? [];
1473
+ if (engine && !supported.includes(body.agentPermissionMode)) {
1474
+ return c.json({
1475
+ error: `Engine '${engineId}' does not support agentPermissionMode '${body.agentPermissionMode}'. Supported: ${supported.join(', ')}`,
1476
+ }, 400);
1477
+ }
1454
1478
  updated = workspaceService.updateAgentPermissionMode(id, body.agentPermissionMode);
1455
1479
  }
1456
1480
  if (body.status) {
@@ -4,14 +4,21 @@ export const CLAUDE_CODE_CAPABILITIES = {
4
4
  // ONE source of truth, consumed both by this file (for /api/engines and
5
5
  // for validation in POST /api/workspaces) and by the frontend selectors.
6
6
  models: CLAUDE_MODELS.map((m) => ({ id: m.id, label: m.label })),
7
+ // Reasoning effort values passed verbatim via `options.extraArgs.effort` to
8
+ // the Claude SDK. `auto` is a Kōbō sentinel meaning "let the SDK pick its
9
+ // default" (we omit the arg).
7
10
  effortLevels: [
8
11
  { id: 'auto', label: 'Auto' },
9
12
  { id: 'low', label: 'Low' },
10
13
  { id: 'medium', label: 'Medium' },
11
14
  { id: 'high', label: 'High' },
15
+ { id: 'xhigh', label: 'Extra High' },
16
+ { id: 'max', label: 'Max' },
12
17
  ],
13
18
  permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
14
19
  supportsResume: true,
15
20
  supportsMcp: true,
16
21
  supportsSkills: true,
22
+ supportsSubagents: true,
23
+ supportsQuotaStatus: true,
17
24
  };
@@ -0,0 +1,18 @@
1
+ import { CODEX_MODELS } from '../../../../../shared/codex-models.js';
2
+ export const CODEX_CAPABILITIES = {
3
+ models: CODEX_MODELS.map((m) => ({ id: m.id, label: m.label })),
4
+ effortLevels: [
5
+ { id: 'auto', label: 'Auto' },
6
+ { id: 'minimal', label: 'Minimal' },
7
+ { id: 'low', label: 'Low' },
8
+ { id: 'medium', label: 'Medium' },
9
+ { id: 'high', label: 'High' },
10
+ { id: 'xhigh', label: 'Extra High' },
11
+ ],
12
+ permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
13
+ supportsResume: true,
14
+ supportsMcp: true,
15
+ supportsSkills: false,
16
+ supportsSubagents: true,
17
+ supportsQuotaStatus: true,
18
+ };
@@ -0,0 +1,36 @@
1
+ import { createJsonRpcPeer } from './jsonrpc/peer.js';
2
+ export function createAppServerClient(opts) {
3
+ const peer = createJsonRpcPeer({
4
+ stdin: opts.stdin,
5
+ stdout: opts.stdout,
6
+ onNotification: opts.onNotification ?? (() => { }),
7
+ onServerRequest: opts.onServerRequest ?? (() => { }),
8
+ onError: opts.onError,
9
+ });
10
+ return {
11
+ peer,
12
+ async connect() {
13
+ // Without experimentalApi the server rejects collaborationMode (-32600).
14
+ const params = {
15
+ clientInfo: opts.clientInfo,
16
+ capabilities: { experimentalApi: true },
17
+ };
18
+ return peer.request('initialize', params);
19
+ },
20
+ startThread(params) {
21
+ return peer.request('thread/start', params);
22
+ },
23
+ resumeThread(params) {
24
+ return peer.request('thread/resume', params);
25
+ },
26
+ startTurn(params) {
27
+ return peer.request('turn/start', params);
28
+ },
29
+ async interruptTurn(params) {
30
+ await peer.request('turn/interrupt', params);
31
+ },
32
+ close() {
33
+ peer.close();
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,276 @@
1
+ import { getPackageVersion } from '../../../../utils/paths.js';
2
+ import { CODEX_CAPABILITIES } from './capabilities.js';
3
+ import { createAppServerClient } from './client.js';
4
+ import { createMapperState, emitSessionStarted, handleAgentMessageDelta, handleItemCompleted, handleItemStarted, handleRateLimitsUpdated, handleTurnCompleted, QUOTA_PATTERN, tryEmitQuota, } from './event-mapper.js';
5
+ import { buildCodexOptions } from './options-builder.js';
6
+ import { buildResponseForResolve, handleServerRequest } from './server-requests.js';
7
+ import { spawnAppServer } from './spawn.js';
8
+ /**
9
+ * Heuristic for detecting a stale/expired thread id on `thread/resume`.
10
+ * Canonical wording isn't captured yet — when matched, the engine emits
11
+ * `error/resume_failed` so the orchestrator can restart with a fresh thread.
12
+ */
13
+ export const RESUME_FAILED_PATTERN = /(thread\b.*\bnot found|session\b.*\bnot found|no\s+(such\s+)?thread|thread.*expired|conversation\b.*\bnot found|invalid\s+thread\s+id)/i;
14
+ export function createCodexEngine() {
15
+ return {
16
+ id: 'codex',
17
+ displayName: 'OpenAI Codex',
18
+ capabilities: CODEX_CAPABILITIES,
19
+ async start(options, onEvent) {
20
+ const { threadParams, input, isResume, collaborationMode } = buildCodexOptions({
21
+ prompt: options.prompt,
22
+ model: options.model,
23
+ effort: options.effort,
24
+ agentPermissionMode: options.agentPermissionMode ?? 'bypass',
25
+ resumeFromEngineSessionId: options.resumeFromEngineSessionId,
26
+ workingDir: options.workingDir,
27
+ mcpServers: options.mcpServers,
28
+ });
29
+ const mapperState = createMapperState();
30
+ const abortController = new AbortController();
31
+ const pendingByCallId = new Map();
32
+ let iteratorRunning = false;
33
+ let userInterrupted = false;
34
+ let discoveredSessionId = options.resumeFromEngineSessionId;
35
+ const safeEmit = (ev) => {
36
+ try {
37
+ onEvent(ev);
38
+ }
39
+ catch (err) {
40
+ console.error('[codex-engine] onEvent handler threw:', err);
41
+ }
42
+ };
43
+ const child = spawnAppServer({ cwd: options.workingDir, signal: abortController.signal });
44
+ if (child.stderr) {
45
+ child.stderr.setEncoding('utf8');
46
+ child.stderr.on('data', (chunk) => {
47
+ const text = chunk.toString();
48
+ if (QUOTA_PATTERN.test(text)) {
49
+ tryEmitQuota(mapperState, safeEmit, text.trim());
50
+ }
51
+ else {
52
+ console.warn('[codex] stderr:', text.trimEnd());
53
+ }
54
+ });
55
+ }
56
+ let resolveTurnDone;
57
+ let rejectTurnDone;
58
+ const turnDonePromise = new Promise((resolve, reject) => {
59
+ resolveTurnDone = resolve;
60
+ rejectTurnDone = reject;
61
+ });
62
+ abortController.signal.addEventListener('abort', () => {
63
+ const err = new Error('AbortError');
64
+ err.name = 'AbortError';
65
+ rejectTurnDone(err);
66
+ });
67
+ const client = createAppServerClient({
68
+ stdin: child.stdin,
69
+ stdout: child.stdout,
70
+ clientInfo: { name: 'kobo', version: getPackageVersion() },
71
+ onNotification(method, params) {
72
+ // Ignored notifications — harmless bookkeeping by the server
73
+ if (method === 'mcpServer/startupStatus/updated' ||
74
+ method === 'thread/started' ||
75
+ method === 'thread/status/changed' ||
76
+ method === 'remoteControl/status/changed' ||
77
+ method === 'turn/started') {
78
+ return;
79
+ }
80
+ if (method === 'item/started') {
81
+ const n = params;
82
+ for (const ev of handleItemStarted(n.item, mapperState))
83
+ safeEmit(ev);
84
+ return;
85
+ }
86
+ if (method === 'item/completed') {
87
+ const n = params;
88
+ for (const ev of handleItemCompleted(n.item, mapperState))
89
+ safeEmit(ev);
90
+ return;
91
+ }
92
+ if (method === 'item/agentMessage/delta') {
93
+ const n = params;
94
+ for (const ev of handleAgentMessageDelta(n, mapperState))
95
+ safeEmit(ev);
96
+ return;
97
+ }
98
+ if (method === 'turn/completed') {
99
+ const n = params;
100
+ for (const ev of handleTurnCompleted(n, mapperState))
101
+ safeEmit(ev);
102
+ resolveTurnDone();
103
+ return;
104
+ }
105
+ if (method === 'thread/tokenUsage/updated') {
106
+ const p = params;
107
+ if (p?.tokenUsage?.last) {
108
+ const last = p.tokenUsage.last;
109
+ safeEmit({
110
+ kind: 'usage',
111
+ inputTokens: last.inputTokens,
112
+ outputTokens: last.outputTokens + last.reasoningOutputTokens,
113
+ cacheRead: last.cachedInputTokens,
114
+ });
115
+ }
116
+ return;
117
+ }
118
+ if (method === 'account/rateLimits/updated') {
119
+ for (const ev of handleRateLimitsUpdated(params, mapperState))
120
+ safeEmit(ev);
121
+ return;
122
+ }
123
+ if (method === 'error') {
124
+ const n = params;
125
+ const msg = n?.message ?? 'unknown error';
126
+ if (QUOTA_PATTERN.test(msg)) {
127
+ tryEmitQuota(mapperState, safeEmit, msg);
128
+ }
129
+ else {
130
+ mapperState.sawErrorResult = true;
131
+ safeEmit({ kind: 'error', category: 'other', message: msg });
132
+ }
133
+ return;
134
+ }
135
+ },
136
+ onServerRequest(id, method, params) {
137
+ handleServerRequest({
138
+ requestId: id,
139
+ method,
140
+ params,
141
+ emit: safeEmit,
142
+ register(callId, pending) {
143
+ pendingByCallId.set(callId, pending);
144
+ },
145
+ respondError: (reqId, code, message) => client.peer.respondError(reqId, code, message),
146
+ });
147
+ },
148
+ onError(err) {
149
+ console.error('[codex] JSON-RPC transport error:', err);
150
+ rejectTurnDone(err);
151
+ },
152
+ });
153
+ const iteratorPromise = (async () => {
154
+ iteratorRunning = true;
155
+ try {
156
+ await client.connect();
157
+ if (isResume && options.resumeFromEngineSessionId) {
158
+ await client.resumeThread({
159
+ threadId: options.resumeFromEngineSessionId,
160
+ cwd: options.workingDir,
161
+ persistExtendedHistory: false,
162
+ ...(threadParams.model != null ? { model: threadParams.model } : {}),
163
+ ...(threadParams.approvalPolicy != null ? { approvalPolicy: threadParams.approvalPolicy } : {}),
164
+ ...(threadParams.sandbox != null ? { sandbox: threadParams.sandbox } : {}),
165
+ ...(threadParams.modelReasoningEffort != null
166
+ ? { modelReasoningEffort: threadParams.modelReasoningEffort }
167
+ : {}),
168
+ ...(threadParams.config != null ? { config: threadParams.config } : {}),
169
+ });
170
+ }
171
+ else {
172
+ const startResp = await client.startThread(threadParams);
173
+ discoveredSessionId = startResp.thread.id;
174
+ }
175
+ for (const ev of emitSessionStarted(discoveredSessionId, mapperState))
176
+ safeEmit(ev);
177
+ // collaborationMode is sticky server-side — always send it explicitly,
178
+ // never omit (would leave a Bypass turn stuck in a previous Plan mode).
179
+ await client.startTurn({
180
+ threadId: discoveredSessionId,
181
+ input,
182
+ collaborationMode,
183
+ });
184
+ await turnDonePromise;
185
+ const reason = mapperState.sawErrorResult
186
+ ? 'error'
187
+ : mapperState.sawTurnInterrupted
188
+ ? 'killed'
189
+ : 'completed';
190
+ safeEmit({
191
+ kind: 'session:ended',
192
+ reason,
193
+ exitCode: reason === 'completed' ? 0 : null,
194
+ });
195
+ }
196
+ catch (err) {
197
+ const error = err;
198
+ const message = error.message ?? String(err);
199
+ const isAbort = userInterrupted || error.name === 'AbortError' || abortController.signal.aborted;
200
+ const isResumeAttempt = options.resumeFromEngineSessionId !== undefined;
201
+ if (isAbort) {
202
+ safeEmit({ kind: 'session:ended', reason: 'killed', exitCode: null });
203
+ }
204
+ else if (QUOTA_PATTERN.test(message)) {
205
+ tryEmitQuota(mapperState, safeEmit, message);
206
+ safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
207
+ }
208
+ else if (isResumeAttempt && RESUME_FAILED_PATTERN.test(message)) {
209
+ safeEmit({ kind: 'error', category: 'resume_failed', message });
210
+ safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
211
+ }
212
+ else {
213
+ safeEmit({ kind: 'error', category: 'spawn_failed', message });
214
+ safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
215
+ }
216
+ }
217
+ finally {
218
+ iteratorRunning = false;
219
+ client.close();
220
+ try {
221
+ child.kill('SIGTERM');
222
+ }
223
+ catch {
224
+ // best-effort
225
+ }
226
+ }
227
+ })();
228
+ const engineProcess = {
229
+ get pid() {
230
+ return child.pid;
231
+ },
232
+ get engineSessionId() {
233
+ return discoveredSessionId;
234
+ },
235
+ isAlive() {
236
+ return iteratorRunning;
237
+ },
238
+ sendMessage() {
239
+ throw new Error('sendMessage not supported in Codex app-server single-shot mode');
240
+ },
241
+ interrupt() {
242
+ userInterrupted = true;
243
+ abortController.abort();
244
+ if (discoveredSessionId) {
245
+ client.interruptTurn({ threadId: discoveredSessionId }).catch(() => { });
246
+ }
247
+ },
248
+ async stop() {
249
+ abortController.abort();
250
+ try {
251
+ await iteratorPromise;
252
+ }
253
+ catch {
254
+ // swallow — best effort
255
+ }
256
+ try {
257
+ child.stdin?.end();
258
+ }
259
+ catch {
260
+ // swallow
261
+ }
262
+ },
263
+ resolvePendingUserInput(callId, response) {
264
+ const pending = pendingByCallId.get(callId);
265
+ if (!pending)
266
+ return false;
267
+ pendingByCallId.delete(callId);
268
+ const result = buildResponseForResolve(pending, response);
269
+ client.peer.respond(pending.requestId, result);
270
+ return true;
271
+ },
272
+ };
273
+ return engineProcess;
274
+ },
275
+ };
276
+ }