@jingyi0605/codingns 0.9.6 → 0.9.7

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 (81) hide show
  1. package/dist/public/assets/{AdaptiveButlerPage-khJQh6a_.js → AdaptiveButlerPage-DclGPzEx.js} +2 -2
  2. package/dist/public/assets/{App-If9gThKM.js → App-BxX5mm9o.js} +3 -3
  3. package/dist/public/assets/{BootstrapPage-DcfYtoLC.js → BootstrapPage-Bl21SsuW.js} +1 -1
  4. package/dist/public/assets/{ConversationPage-Bfb7GLTM.js → ConversationPage-CmiVCV0q.js} +7 -7
  5. package/dist/public/assets/{DesktopDetachPreviewPage-CXUPMcBz.js → DesktopDetachPreviewPage-uaOHVsjV.js} +1 -1
  6. package/dist/public/assets/{DesktopModal-bMdI1jEe.js → DesktopModal-BxsogpLf.js} +1 -1
  7. package/dist/public/assets/{DesktopWindowPage-D1xwgS-7.js → DesktopWindowPage-Bubfw1nC.js} +1 -1
  8. package/dist/public/assets/{FileContextPanel-C4syif3B.js → FileContextPanel-DrYWcTkp.js} +1 -1
  9. package/dist/public/assets/{GitSidebar-DduL9aTV.js → GitSidebar-CQsfJqun.js} +1 -1
  10. package/dist/public/assets/{MobileCreateSessionSheet-DWPBsEx8.js → MobileCreateSessionSheet-CqtmfVNo.js} +1 -1
  11. package/dist/public/assets/{MobileSheet-BXvQPkxt.js → MobileSheet-C5IVmUsO.js} +1 -1
  12. package/dist/public/assets/{MobileTopHeaderFrame-vdYOyaaB.js → MobileTopHeaderFrame-EoBm3-X0.js} +1 -1
  13. package/dist/public/assets/{MobileWorkspaceSwitcherHeader-DT330cAx.js → MobileWorkspaceSwitcherHeader-BC8MMQs_.js} +1 -1
  14. package/dist/public/assets/{PluginAccessOverview-C77TeZTK.js → PluginAccessOverview-CrQiQxxZ.js} +1 -1
  15. package/dist/public/assets/{PluginContainerPage-DdSwOCw-.js → PluginContainerPage-_2u-9thM.js} +1 -1
  16. package/dist/public/assets/{PluginDetailPage-BK1yTzvO.js → PluginDetailPage-F9cKjSCp.js} +1 -1
  17. package/dist/public/assets/{PluginsListPage-DAAwSc6W.js → PluginsListPage-BOhcua_4.js} +1 -1
  18. package/dist/public/assets/{RelayConnectEntryPage-4Yyo2p8b.js → RelayConnectEntryPage-Ck_uZLzj.js} +1 -1
  19. package/dist/public/assets/{ServerSettingsModal-C_DEisHs.js → ServerSettingsModal-ICd82a_x.js} +1 -1
  20. package/dist/public/assets/SessionIndexPage-Cox6P6dw.js +1 -0
  21. package/dist/public/assets/{SettingsPage-CDAVsPr3.js → SettingsPage-BI5cM3j5.js} +1 -1
  22. package/dist/public/assets/{TerminalManagerPanel-4OR47vcf.js → TerminalManagerPanel-DiVhBQhf.js} +1 -1
  23. package/dist/public/assets/{TerminalPage-Pvx396YX.js → TerminalPage-DUMUO7Ng.js} +1 -1
  24. package/dist/public/assets/{TerminalRuntimeFallbackModal-KvG6k4AQ.js → TerminalRuntimeFallbackModal-DGPR_CMh.js} +1 -1
  25. package/dist/public/assets/{ToolFilesPage-DrYHk0N-.js → ToolFilesPage-m88CAp-r.js} +1 -1
  26. package/dist/public/assets/{ToolGitPage-Dz1q-Ns_.js → ToolGitPage-DCYpfTsb.js} +1 -1
  27. package/dist/public/assets/{ToolProcessesPage-CRhphOmM.js → ToolProcessesPage-Bk5ulsyq.js} +1 -1
  28. package/dist/public/assets/{ToolsHomePage-BJSDLR6T.js → ToolsHomePage-BrTwfjI7.js} +1 -1
  29. package/dist/public/assets/{WorkbenchLandingPage-BlkxdOLC.js → WorkbenchLandingPage-q4AAmdMV.js} +1 -1
  30. package/dist/public/assets/{WorkbenchLayout-D-U7ghT0.js → WorkbenchLayout-DD8b-m2G.js} +66 -61
  31. package/dist/public/assets/{WorkbenchModal-xbx1o6MO.js → WorkbenchModal-CsZeLArg.js} +1 -1
  32. package/dist/public/assets/WorkbenchShellRoute-BaiW_vfb.js +1 -0
  33. package/dist/public/assets/WorkbenchShellRoute-CxKYZ6uF.css +1 -0
  34. package/dist/public/assets/{WorkspaceDebugDetailPage-B4ol2_a5.js → WorkspaceDebugDetailPage-BcUUDEyw.js} +1 -1
  35. package/dist/public/assets/WorkspaceDetailPage-zEZ1VARA.js +1 -0
  36. package/dist/public/assets/{WorkspaceHomePage-tmCafatd.js → WorkspaceHomePage-CR0rqS4e.js} +1 -1
  37. package/dist/public/assets/{client-runtime-manager-Bwau7p1v.js → client-runtime-manager-BgGugw8T.js} +1 -1
  38. package/dist/public/assets/{index-_OCkVmfl.js → index-CFyk1rgJ.js} +5 -5
  39. package/dist/public/assets/index-CrU73EIV.css +1 -0
  40. package/dist/public/assets/{login-direct-candidate-resolver-CKUQ07IA.js → login-direct-candidate-resolver-ty2uOY5y.js} +1 -1
  41. package/dist/public/assets/{plugin-permission-copy-DIVk5jNp.js → plugin-permission-copy-iK3faI3n.js} +1 -1
  42. package/dist/public/assets/{plugins-api-DHJVvPZw.js → plugins-api-CmV7aDGA.js} +1 -1
  43. package/dist/public/assets/{preferences-service-CyxxeBmS.js → preferences-service-BoSmT_ny.js} +1 -1
  44. package/dist/public/assets/{relay-entry-B5GmiOrR.js → relay-entry-C8k5qsl5.js} +1 -1
  45. package/dist/public/assets/styles-BhKoKfQ_.css +1 -0
  46. package/dist/public/assets/{terminal-runtime-meta-DBsyT35T.js → terminal-runtime-meta-okQIDzfA.js} +1 -1
  47. package/dist/public/assets/{useRegisteredDebugTemplates-wCGD2SLW.js → useRegisteredDebugTemplates-B_vXUtSe.js} +1 -1
  48. package/dist/public/assets/{workbench-navigation-RyUjchbD.js → workbench-navigation-DoDaQR4z.js} +1 -1
  49. package/dist/public/index.html +2 -2
  50. package/dist/server/modules/provider/provider-discovery-runtime.js +16 -1
  51. package/dist/server/modules/provider/provider-discovery-runtime.js.map +1 -1
  52. package/dist/server/modules/sessions/codex-app-server-helper-client.js +14 -0
  53. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  54. package/dist/server/modules/sessions/codex-app-server-helper-process.js +59 -0
  55. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  56. package/dist/server/modules/sessions/codex-session-title-generator.d.ts +19 -0
  57. package/dist/server/modules/sessions/codex-session-title-generator.js +295 -0
  58. package/dist/server/modules/sessions/codex-session-title-generator.js.map +1 -0
  59. package/dist/server/modules/sessions/session-history-service.d.ts +5 -0
  60. package/dist/server/modules/sessions/session-history-service.js +183 -0
  61. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  62. package/dist/server/modules/tasks/task-types.d.ts +1 -0
  63. package/dist/server/modules/tasks/task-types.js +1 -0
  64. package/dist/server/modules/tasks/task-types.js.map +1 -1
  65. package/dist/server/modules/workspace/affairs-lightweight-session-service.js +13 -9
  66. package/dist/server/modules/workspace/affairs-lightweight-session-service.js.map +1 -1
  67. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +7 -0
  68. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +596 -6
  69. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  70. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +1 -0
  71. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +249 -6
  72. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  73. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +11 -0
  74. package/package.json +1 -1
  75. package/dist/public/assets/SessionIndexPage-DyMikN_x.js +0 -1
  76. package/dist/public/assets/WorkbenchShellRoute-DyWSCHz_.js +0 -1
  77. package/dist/public/assets/WorkbenchShellRoute-htbkGbtW.css +0 -1
  78. package/dist/public/assets/WorkspaceDetailPage-DMakfmHR.js +0 -1
  79. package/dist/public/assets/index-DmUJ8tIw.css +0 -1
  80. package/dist/public/assets/styles-DkbkRgWw.css +0 -1
  81. /package/dist/public/assets/{styles-JKFlsYFv.js → styles-DwSuZo1w.js} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { performance } from "node:perf_hooks";
6
6
  import { basename, dirname, join, resolve } from "node:path";
@@ -13,6 +13,9 @@ import { loadDatabaseSync } from "../sqlite/node-sqlite.js";
13
13
  import { createCodexThreadPermissionOptions } from "./codex-permissions.js";
14
14
  const CODEX_RUNTIME_DEBUG_ENABLED = /^(1|true|yes)$/i.test(process.env.CODINGNS_PERF_DEBUG?.trim() ?? "");
15
15
  const CODEX_APP_SERVER_REQUEST_TIMEOUT_MS = 20_000;
16
+ const CODEX_APP_SERVER_SPAWN_AGENT_GRACE_MS = 6 * 60 * 60 * 1000;
17
+ const CODEX_SPAWN_AGENT_RAW_SCAN_BYTES = 2 * 1024 * 1024;
18
+ const CODEX_SPAWN_AGENT_POLL_INTERVAL_MS = 2_000;
16
19
  function logCodexRuntimeStep(scope, startedAtMs, detail = {}) {
17
20
  if (!CODEX_RUNTIME_DEBUG_ENABLED) {
18
21
  return;
@@ -21,6 +24,182 @@ function logCodexRuntimeStep(scope, startedAtMs, detail = {}) {
21
24
  const suffix = formatCodexRuntimeDebugDetail(detail);
22
25
  console.info(`[perf][codex-runtime] ${scope} ${durationMs}ms${suffix ? ` ${suffix}` : ""}`);
23
26
  }
27
+ function closeCodexTransportAfterTurn(transport, lifecycle, rawStoreRef) {
28
+ if (!shouldKeepCodexTransportAliveAfterTurn(lifecycle, rawStoreRef)) {
29
+ transport.close();
30
+ return;
31
+ }
32
+ // 子 Agent 依附在 Codex app-server 进程上。父 turn 完成后立即 close
33
+ // 会把刚 spawn 出来的子 Agent 一起 SIGTERM 掉。这里给一个足够长的
34
+ // 宽限期,让子 Agent 自己跑完;宽限期到了再兜底回收,避免进程永久泄漏。
35
+ const timer = setTimeout(() => {
36
+ transport.close();
37
+ }, CODEX_APP_SERVER_SPAWN_AGENT_GRACE_MS);
38
+ if (typeof timer.unref === "function") {
39
+ timer.unref();
40
+ }
41
+ }
42
+ function shouldKeepCodexTransportAliveAfterTurn(lifecycle, rawStoreRef) {
43
+ return lifecycle.keepTransportAliveAfterTurn || codexRawStoreContainsSpawnAgentCall(rawStoreRef);
44
+ }
45
+ function codexRawStoreContainsSpawnAgentCall(rawStoreRef) {
46
+ const text = readCodexRawStoreTail(rawStoreRef);
47
+ if (!text || !text.includes("spawn_agent")) {
48
+ return false;
49
+ }
50
+ for (const line of text.split("\n")) {
51
+ if (!line.includes("spawn_agent")) {
52
+ continue;
53
+ }
54
+ try {
55
+ const record = toRecord(JSON.parse(line));
56
+ const payload = toRecord(readProp(record, "payload"));
57
+ if (isCodexSpawnAgentItem(record)
58
+ || isCodexSpawnAgentItem(payload)
59
+ || isCodexSpawnAgentItem(toRecord(readProp(record, "item")))
60
+ || isCodexSpawnAgentItem(toRecord(readProp(payload, "item")))) {
61
+ return true;
62
+ }
63
+ }
64
+ catch {
65
+ // 单行坏掉不影响判断,继续看下一行。
66
+ }
67
+ }
68
+ return false;
69
+ }
70
+ function extractCodexSpawnedAgentIdsFromRawStore(rawStoreRef) {
71
+ const text = readCodexRawStoreTail(rawStoreRef);
72
+ if (!text || !text.includes("spawn_agent")) {
73
+ return [];
74
+ }
75
+ const spawnCallIds = new Set();
76
+ const agentIds = new Set();
77
+ for (const line of text.split("\n")) {
78
+ if (!line.trim()) {
79
+ continue;
80
+ }
81
+ try {
82
+ const record = toRecord(JSON.parse(line));
83
+ const payload = toRecord(readProp(record, "payload"));
84
+ if (!payload) {
85
+ continue;
86
+ }
87
+ if (isCodexSpawnAgentItem(payload)) {
88
+ const callId = ensureText(readProp(payload, "call_id")).trim();
89
+ if (callId) {
90
+ spawnCallIds.add(callId);
91
+ }
92
+ continue;
93
+ }
94
+ if (ensureText(readProp(payload, "type")).trim() !== "function_call_output") {
95
+ continue;
96
+ }
97
+ const callId = ensureText(readProp(payload, "call_id")).trim();
98
+ if (!callId || !spawnCallIds.has(callId)) {
99
+ continue;
100
+ }
101
+ const parsedOutput = parseStructuredJson(ensureText(readProp(payload, "output")));
102
+ const agentId = ensureText(readProp(parsedOutput, "agent_id")).trim();
103
+ if (looksLikeCodexThreadId(agentId)) {
104
+ agentIds.add(agentId);
105
+ }
106
+ }
107
+ catch {
108
+ // 单行坏掉不影响判断,继续看下一行。
109
+ }
110
+ }
111
+ return [...agentIds];
112
+ }
113
+ function isCodexRawStoreTerminal(rawStoreRef) {
114
+ const text = readCodexRawStoreTail(rawStoreRef);
115
+ if (!text) {
116
+ return false;
117
+ }
118
+ for (const line of text.split("\n")) {
119
+ if (!line.includes("task_complete")
120
+ && !line.includes("turn_aborted")
121
+ && !line.includes("turn_failed")) {
122
+ continue;
123
+ }
124
+ try {
125
+ const record = toRecord(JSON.parse(line));
126
+ const payload = toRecord(readProp(record, "payload"));
127
+ const recordType = ensureText(readProp(record, "type")).trim();
128
+ const payloadType = ensureText(readProp(payload, "type")).trim();
129
+ if ((recordType === "event_msg" && payloadType === "task_complete")
130
+ || (recordType === "event_msg" && payloadType === "turn_aborted")
131
+ || (recordType === "event_msg" && payloadType === "turn_failed")) {
132
+ return true;
133
+ }
134
+ }
135
+ catch {
136
+ // 单行坏掉不影响判断,继续看下一行。
137
+ }
138
+ }
139
+ return false;
140
+ }
141
+ function readCodexRawStoreTail(rawStoreRef) {
142
+ try {
143
+ if (!rawStoreRef.trim() || !existsSync(rawStoreRef)) {
144
+ return "";
145
+ }
146
+ const stat = statSync(rawStoreRef);
147
+ if (!stat.isFile()) {
148
+ return "";
149
+ }
150
+ if (stat.size <= CODEX_SPAWN_AGENT_RAW_SCAN_BYTES) {
151
+ return readFileSync(rawStoreRef, "utf8");
152
+ }
153
+ const fd = openSync(rawStoreRef, "r");
154
+ try {
155
+ const length = CODEX_SPAWN_AGENT_RAW_SCAN_BYTES;
156
+ const buffer = Buffer.allocUnsafe(length);
157
+ const bytesRead = readSync(fd, buffer, 0, length, stat.size - length);
158
+ return buffer.subarray(0, bytesRead).toString("utf8");
159
+ }
160
+ finally {
161
+ closeSync(fd);
162
+ }
163
+ }
164
+ catch {
165
+ return "";
166
+ }
167
+ }
168
+ function isCodexSpawnAgentEvent(event) {
169
+ const eventRecord = toRecord(event);
170
+ if (!eventRecord) {
171
+ return false;
172
+ }
173
+ // app-server 通常发的是 { type: "item.completed", item: {...} }。
174
+ // 但真实 3002 路径里,父 turn 的 transcript 也会出现已经展开的
175
+ // { type: "function_call", name: "spawn_agent" } 记录。两种都必须识别,
176
+ // 否则父 turn 完成后会 close app-server,把子 Agent 一起中断。
177
+ return isCodexSpawnAgentItem(toRecord(readProp(eventRecord, "item")) ?? eventRecord);
178
+ }
179
+ function markCodexSpawnAgentLifecycleFromEvents(lifecycle, events) {
180
+ if (lifecycle.keepTransportAliveAfterTurn) {
181
+ return;
182
+ }
183
+ if (events.some((event) => isCodexSpawnAgentEvent(event))) {
184
+ lifecycle.keepTransportAliveAfterTurn = true;
185
+ }
186
+ }
187
+ function isCodexSpawnAgentItem(item) {
188
+ if (!item) {
189
+ return false;
190
+ }
191
+ const itemType = ensureText(readProp(item, "type")).trim();
192
+ if (itemType === "function_call"
193
+ || itemType === "functionCall"
194
+ || itemType === "custom_tool_call") {
195
+ return (ensureText(readProp(item, "name")).trim() === "spawn_agent"
196
+ || ensureText(readProp(item, "tool")).trim() === "spawn_agent");
197
+ }
198
+ if (itemType === "dynamicToolCall" || itemType === "mcpToolCall") {
199
+ return ensureText(readProp(item, "tool")).trim() === "spawn_agent";
200
+ }
201
+ return false;
202
+ }
24
203
  function formatCodexRuntimeDebugDetail(detail) {
25
204
  const entries = Object.entries(detail).filter(([, value]) => value !== undefined);
26
205
  if (entries.length === 0) {
@@ -75,6 +254,9 @@ export class CodexRuntimeAdapter {
75
254
  });
76
255
  const abortController = new AbortController();
77
256
  const eventQueue = createAsyncEventQueue();
257
+ const lifecycle = {
258
+ keepTransportAliveAfterTurn: false
259
+ };
78
260
  const translateNotification = createCodexAppServerNotificationTranslator();
79
261
  const forwardTranslatedNotification = createCodexTranslatedNotificationForwarder(eventQueue);
80
262
  const resumedSyntheticSession = await this.resumeSyntheticThreadFromHistory(transport, request);
@@ -115,6 +297,7 @@ export class CodexRuntimeAdapter {
115
297
  });
116
298
  }
117
299
  const translated = translateNotification(notification);
300
+ markCodexSpawnAgentLifecycleFromEvents(lifecycle, translated.events);
118
301
  forwardTranslatedNotification(translated);
119
302
  });
120
303
  transport.setServerRequestHandler(async (serverRequest) => {
@@ -142,6 +325,7 @@ export class CodexRuntimeAdapter {
142
325
  const startTurnNotification = startTurnResult?.notification ?? null;
143
326
  if (startTurnNotification) {
144
327
  const translated = translateNotification(startTurnNotification);
328
+ markCodexSpawnAgentLifecycleFromEvents(lifecycle, translated.events);
145
329
  forwardTranslatedNotification(translated);
146
330
  }
147
331
  logCodexRuntimeStep("start_session.turn_start", startTurnStartedAtMs, {
@@ -166,8 +350,8 @@ export class CodexRuntimeAdapter {
166
350
  transport.close();
167
351
  },
168
352
  isAlive: () => transport.isClosed() === false,
169
- completed: this.runTurn(null, request, sink, providerSessionId, rawStoreRef, abortController, eventQueue.iterator, [], launchedAtMs, launchPerfStartedAtMs).finally(() => {
170
- transport.close();
353
+ completed: this.runTurn(null, request, sink, providerSessionId, rawStoreRef, abortController, eventQueue.iterator, [], launchedAtMs, launchPerfStartedAtMs, lifecycle).finally(() => {
354
+ closeCodexTransportAfterTurn(transport, lifecycle, rawStoreRef);
171
355
  })
172
356
  };
173
357
  }
@@ -261,6 +445,9 @@ export class CodexRuntimeAdapter {
261
445
  : pickedRawStoreRef;
262
446
  const abortController = new AbortController();
263
447
  const eventQueue = createAsyncEventQueue();
448
+ const lifecycle = {
449
+ keepTransportAliveAfterTurn: false
450
+ };
264
451
  const translateNotification = createCodexAppServerNotificationTranslator();
265
452
  const forwardTranslatedNotification = createCodexTranslatedNotificationForwarder(eventQueue);
266
453
  logCodexRuntimeStep("continue_session.raw_store_ref_ready", runtimeStartedAtMs, {
@@ -286,6 +473,7 @@ export class CodexRuntimeAdapter {
286
473
  });
287
474
  }
288
475
  const translated = translateNotification(notification);
476
+ markCodexSpawnAgentLifecycleFromEvents(lifecycle, translated.events);
289
477
  forwardTranslatedNotification(translated);
290
478
  });
291
479
  transport.setServerRequestHandler(async (serverRequest) => {
@@ -313,6 +501,7 @@ export class CodexRuntimeAdapter {
313
501
  const startTurnNotification = startTurnResult?.notification ?? null;
314
502
  if (startTurnNotification) {
315
503
  const translated = translateNotification(startTurnNotification);
504
+ markCodexSpawnAgentLifecycleFromEvents(lifecycle, translated.events);
316
505
  forwardTranslatedNotification(translated);
317
506
  }
318
507
  logCodexRuntimeStep("continue_session.turn_start", startTurnStartedAtMs, {
@@ -337,8 +526,8 @@ export class CodexRuntimeAdapter {
337
526
  transport.close();
338
527
  },
339
528
  isAlive: () => transport.isClosed() === false,
340
- completed: this.runTurn(null, request, sink, resolvedSessionId, rawStoreRef, abortController, eventQueue.iterator, [], Date.now()).finally(() => {
341
- transport.close();
529
+ completed: this.runTurn(null, request, sink, resolvedSessionId, rawStoreRef, abortController, eventQueue.iterator, [], Date.now(), performance.now(), lifecycle).finally(() => {
530
+ closeCodexTransportAfterTurn(transport, lifecycle, rawStoreRef);
342
531
  })
343
532
  };
344
533
  }
@@ -382,13 +571,16 @@ export class CodexRuntimeAdapter {
382
571
  }
383
572
  return fallbackMatch;
384
573
  }
385
- async runTurn(thread, request, sink, providerSessionId, rawStoreRef, abortController, preparedEvents, bufferedEvents = [], launchedAtMs = Date.now(), launchPerfStartedAtMs = performance.now()) {
574
+ async runTurn(thread, request, sink, providerSessionId, rawStoreRef, abortController, preparedEvents, bufferedEvents = [], launchedAtMs = Date.now(), launchPerfStartedAtMs = performance.now(), lifecycle = {
575
+ keepTransportAliveAfterTurn: false
576
+ }) {
386
577
  const context = {
387
578
  providerSessionId,
388
579
  rawStoreRef,
389
580
  // 运行时消息必须接在历史消息后面,不能每轮都从 1 重新编号,
390
581
  // 否则前端会把新 assistant/tool 消息排到旧消息前面,表现成用户消息一直挂在底部。
391
582
  sequence: Math.max(0, request.sequenceBase ?? 0),
583
+ lifecycle,
392
584
  toolNameByCallId: new Map(),
393
585
  stableMessageRefByIdentity: new Map(),
394
586
  lastSignatureByIdentity: new Map(),
@@ -418,6 +610,7 @@ export class CodexRuntimeAdapter {
418
610
  while (true) {
419
611
  const next = await events.next();
420
612
  if (next.done) {
613
+ await this.waitForSpawnedCodexAgentsIfNeeded(context, abortController.signal);
421
614
  return;
422
615
  }
423
616
  await this.refreshSessionBindingIfNeeded(context);
@@ -450,11 +643,37 @@ export class CodexRuntimeAdapter {
450
643
  });
451
644
  }
452
645
  }
646
+ async waitForSpawnedCodexAgentsIfNeeded(context, signal) {
647
+ if (!shouldKeepCodexTransportAliveAfterTurn(context.lifecycle, context.rawStoreRef)) {
648
+ return;
649
+ }
650
+ const agentIds = extractCodexSpawnedAgentIdsFromRawStore(context.rawStoreRef);
651
+ if (agentIds.length === 0) {
652
+ return;
653
+ }
654
+ const deadline = Date.now() + CODEX_APP_SERVER_SPAWN_AGENT_GRACE_MS;
655
+ const remainingAgentIds = new Set(agentIds);
656
+ while (remainingAgentIds.size > 0 && Date.now() < deadline && !signal.aborted) {
657
+ for (const agentId of [...remainingAgentIds]) {
658
+ const rawStoreRef = this.findRawStoreRefOnce(agentId, context.workspacePath, context.homeDir);
659
+ if (rawStoreRef && isCodexRawStoreTerminal(rawStoreRef)) {
660
+ remainingAgentIds.delete(agentId);
661
+ }
662
+ }
663
+ if (remainingAgentIds.size === 0 || Date.now() >= deadline || signal.aborted) {
664
+ break;
665
+ }
666
+ await sleep(CODEX_SPAWN_AGENT_POLL_INTERVAL_MS);
667
+ }
668
+ }
453
669
  async handleEvent(event, request, context, interrupted) {
454
670
  const eventType = ensureText(readProp(event, "type")).trim();
455
671
  if (eventType.length === 0) {
456
672
  return;
457
673
  }
674
+ if (isCodexSpawnAgentEvent(event)) {
675
+ context.lifecycle.keepTransportAliveAfterTurn = true;
676
+ }
458
677
  if (context.lastSignatureByIdentity.size === 0 && eventType.startsWith("item.")) {
459
678
  logCodexRuntimeStep("turn.first_item_event", context.launchPerfStartedAtMs, {
460
679
  sessionId: request.sessionId,
@@ -1686,6 +1905,17 @@ function translateCodexAppServerItem(item) {
1686
1905
  status: normalizeCodexItemStatus(item.status)
1687
1906
  };
1688
1907
  }
1908
+ if (itemType === "functionCall" || itemType === "function_call") {
1909
+ return {
1910
+ type: "function_call",
1911
+ id: item.id,
1912
+ name: item.name,
1913
+ arguments: readProp(item, "arguments") ?? readProp(item, "input"),
1914
+ output: item.output,
1915
+ error: item.error,
1916
+ status: normalizeCodexItemStatus(item.status)
1917
+ };
1918
+ }
1689
1919
  if (itemType === "dynamicToolCall") {
1690
1920
  const toolName = ensureText(item.tool).trim();
1691
1921
  const patchText = isCodexExecCommandToolName(toolName)
@@ -2118,6 +2348,19 @@ function normalizeText(value) {
2118
2348
  const normalized = ensureText(value).trim();
2119
2349
  return normalized.length > 0 ? normalized : null;
2120
2350
  }
2351
+ function parseStructuredJson(value) {
2352
+ const normalized = value.trim();
2353
+ if (!normalized) {
2354
+ return null;
2355
+ }
2356
+ try {
2357
+ const parsed = JSON.parse(normalized);
2358
+ return toRecord(parsed);
2359
+ }
2360
+ catch {
2361
+ return null;
2362
+ }
2363
+ }
2121
2364
  function readThreadIdFromRawStore(rawStoreRef) {
2122
2365
  const filePath = ensureText(rawStoreRef).trim();
2123
2366
  if (!filePath || !existsSync(filePath)) {