@jingyi0605/codingns 0.9.6 → 0.9.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 (170) hide show
  1. package/dist/public/assets/{AdaptiveButlerPage-khJQh6a_.js → AdaptiveButlerPage-D-gXre7Y.js} +2 -2
  2. package/dist/public/assets/{App-CcDXqFl1.css → App-7zrCMhE-.css} +1 -1
  3. package/dist/public/assets/App-Dl-mcdqy.js +30 -0
  4. package/dist/public/assets/{BootstrapPage-DcfYtoLC.js → BootstrapPage-B-yMdfpQ.js} +1 -1
  5. package/dist/public/assets/ConversationPage-DRQ5Sg_d.js +9 -0
  6. package/dist/public/assets/{DesktopDetachPreviewPage-CXUPMcBz.js → DesktopDetachPreviewPage-D1DMaGcy.js} +1 -1
  7. package/dist/public/assets/{DesktopModal-bMdI1jEe.js → DesktopModal-BnfGW2gk.js} +1 -1
  8. package/dist/public/assets/DesktopWindowPage-2SWAi0xz.js +2 -0
  9. package/dist/public/assets/FileContextPanel-fbPuE9dO.js +1 -0
  10. package/dist/public/assets/GitSidebar-BkmesJJR.js +6 -0
  11. package/dist/public/assets/{MobileCreateSessionSheet-DWPBsEx8.js → MobileCreateSessionSheet-CEJcDBZJ.js} +1 -1
  12. package/dist/public/assets/{MobileSheet-BXvQPkxt.js → MobileSheet-rkn_CUOY.js} +1 -1
  13. package/dist/public/assets/{MobileTopHeaderFrame-vdYOyaaB.js → MobileTopHeaderFrame-CU0wsYSS.js} +1 -1
  14. package/dist/public/assets/MobileWorkspaceSwitcherHeader-idl8o1OB.js +1 -0
  15. package/dist/public/assets/{PluginAccessOverview-C77TeZTK.js → PluginAccessOverview-BBgM6tb0.js} +1 -1
  16. package/dist/public/assets/{PluginContainerPage-DdSwOCw-.js → PluginContainerPage-D-ly3i3H.js} +1 -1
  17. package/dist/public/assets/{PluginDetailPage-BK1yTzvO.js → PluginDetailPage-CWAHYyyG.js} +1 -1
  18. package/dist/public/assets/{PluginsListPage-DAAwSc6W.js → PluginsListPage-Cte3vBgR.js} +1 -1
  19. package/dist/public/assets/{RelayConnectEntryPage-4Yyo2p8b.js → RelayConnectEntryPage-sRJlstx9.js} +1 -1
  20. package/dist/public/assets/{ServerSettingsModal-C_DEisHs.js → ServerSettingsModal-BBft9KEC.js} +1 -1
  21. package/dist/public/assets/SessionIndexPage-CN7cEdl9.js +1 -0
  22. package/dist/public/assets/SettingsPage-BGT-YqG2.js +2 -0
  23. package/dist/public/assets/TerminalManagerPanel-6-ZJ8vGn.js +1 -0
  24. package/dist/public/assets/TerminalPage-CUXXQYU2.js +55 -0
  25. package/dist/public/assets/{TerminalRuntimeFallbackModal-KvG6k4AQ.js → TerminalRuntimeFallbackModal-zc3qqMKJ.js} +1 -1
  26. package/dist/public/assets/ToolFilesPage-QzsZyr0F.js +1 -0
  27. package/dist/public/assets/ToolGitPage-CXg4ncuT.js +1 -0
  28. package/dist/public/assets/ToolProcessesPage-BPsOsg4w.js +1 -0
  29. package/dist/public/assets/ToolsHomePage-D1n4FU1s.js +1 -0
  30. package/dist/public/assets/WorkbenchLandingPage-BaU_dXls.js +1 -0
  31. package/dist/public/assets/WorkbenchLayout-DViAJhHz.js +1027 -0
  32. package/dist/public/assets/{WorkbenchModal-xbx1o6MO.js → WorkbenchModal-DWsNm2B2.js} +1 -1
  33. package/dist/public/assets/WorkbenchShellRoute-BGfRqBUa.js +1 -0
  34. package/dist/public/assets/WorkbenchShellRoute-f2jWjHWu.css +1 -0
  35. package/dist/public/assets/WorkspaceDebugDetailPage-BX0zVSsI.js +1 -0
  36. package/dist/public/assets/WorkspaceDetailPage-Dx6JX4jx.js +1 -0
  37. package/dist/public/assets/WorkspaceHomePage-DQVJ042Z.js +1 -0
  38. package/dist/public/assets/{client-runtime-manager-Bwau7p1v.js → client-runtime-manager-D9VbgJZ_.js} +1 -1
  39. package/dist/public/assets/host-alias-0TfFnYxR.js +1 -0
  40. package/dist/public/assets/index-DREvg1Yu.css +1 -0
  41. package/dist/public/assets/index-FOhyOpGY.js +50 -0
  42. package/dist/public/assets/{login-direct-candidate-resolver-CKUQ07IA.js → login-direct-candidate-resolver-17wEvjhh.js} +1 -1
  43. package/dist/public/assets/peer-host-config-sync-vYkmqzNz.js +1 -0
  44. package/dist/public/assets/{plugin-permission-copy-DIVk5jNp.js → plugin-permission-copy-apDn8EWG.js} +1 -1
  45. package/dist/public/assets/{plugins-api-DHJVvPZw.js → plugins-api-CnZYRKoS.js} +1 -1
  46. package/dist/public/assets/{preferences-service-CyxxeBmS.js → preferences-service-PZlLLAWH.js} +1 -1
  47. package/dist/public/assets/relay-entry-DhHwflXl.js +1 -0
  48. package/dist/public/assets/styles-BhKoKfQ_.css +1 -0
  49. package/dist/public/assets/terminal-runtime-meta-2zvacxvM.js +1 -0
  50. package/dist/public/assets/{useRegisteredDebugTemplates-wCGD2SLW.js → useRegisteredDebugTemplates-CJ-o4tFl.js} +1 -1
  51. package/dist/public/assets/workbench-navigation-aqJ1ay4M.js +1 -0
  52. package/dist/public/index.html +2 -2
  53. package/dist/server/middlewares/auth-guard.js +1 -0
  54. package/dist/server/middlewares/auth-guard.js.map +1 -1
  55. package/dist/server/modules/peer-host/host-api-proxy-service.d.ts +9 -0
  56. package/dist/server/modules/peer-host/host-api-proxy-service.js +174 -0
  57. package/dist/server/modules/peer-host/host-api-proxy-service.js.map +1 -0
  58. package/dist/server/modules/peer-host/host-handshake-controller.d.ts +7 -0
  59. package/dist/server/modules/peer-host/host-handshake-controller.js +10 -0
  60. package/dist/server/modules/peer-host/host-handshake-controller.js.map +1 -0
  61. package/dist/server/modules/peer-host/host-handshake.d.ts +15 -0
  62. package/dist/server/modules/peer-host/host-handshake.js +20 -0
  63. package/dist/server/modules/peer-host/host-handshake.js.map +1 -0
  64. package/dist/server/modules/peer-host/host-ws-proxy-service.d.ts +14 -0
  65. package/dist/server/modules/peer-host/host-ws-proxy-service.js +256 -0
  66. package/dist/server/modules/peer-host/host-ws-proxy-service.js.map +1 -0
  67. package/dist/server/modules/peer-host/peer-host-controller.d.ts +55 -0
  68. package/dist/server/modules/peer-host/peer-host-controller.js +62 -0
  69. package/dist/server/modules/peer-host/peer-host-controller.js.map +1 -0
  70. package/dist/server/modules/peer-host/peer-host-service.d.ts +77 -0
  71. package/dist/server/modules/peer-host/peer-host-service.js +529 -0
  72. package/dist/server/modules/peer-host/peer-host-service.js.map +1 -0
  73. package/dist/server/modules/provider/provider-discovery-runtime.js +16 -1
  74. package/dist/server/modules/provider/provider-discovery-runtime.js.map +1 -1
  75. package/dist/server/modules/sessions/codex-app-server-helper-client.js +14 -0
  76. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  77. package/dist/server/modules/sessions/codex-app-server-helper-process.js +59 -0
  78. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  79. package/dist/server/modules/sessions/codex-session-title-generator.d.ts +19 -0
  80. package/dist/server/modules/sessions/codex-session-title-generator.js +308 -0
  81. package/dist/server/modules/sessions/codex-session-title-generator.js.map +1 -0
  82. package/dist/server/modules/sessions/session-history-service.d.ts +16 -0
  83. package/dist/server/modules/sessions/session-history-service.js +208 -12
  84. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  85. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +6 -0
  86. package/dist/server/modules/sessions/session-live-runtime-service.js +181 -80
  87. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  88. package/dist/server/modules/sessions/session-title-utils.d.ts +3 -0
  89. package/dist/server/modules/sessions/session-title-utils.js +25 -0
  90. package/dist/server/modules/sessions/session-title-utils.js.map +1 -0
  91. package/dist/server/modules/sessions/workspace-office-mcp-config.d.ts +2 -1
  92. package/dist/server/modules/sessions/workspace-office-mcp-config.js +44 -3
  93. package/dist/server/modules/sessions/workspace-office-mcp-config.js.map +1 -1
  94. package/dist/server/modules/tasks/task-manager.d.ts +2 -1
  95. package/dist/server/modules/tasks/task-manager.js +3 -0
  96. package/dist/server/modules/tasks/task-manager.js.map +1 -1
  97. package/dist/server/modules/tasks/task-scheduler.d.ts +2 -1
  98. package/dist/server/modules/tasks/task-scheduler.js +21 -0
  99. package/dist/server/modules/tasks/task-scheduler.js.map +1 -1
  100. package/dist/server/modules/tasks/task-types.d.ts +6 -0
  101. package/dist/server/modules/tasks/task-types.js +1 -0
  102. package/dist/server/modules/tasks/task-types.js.map +1 -1
  103. package/dist/server/modules/workbench/workbench-controller.js +3 -2
  104. package/dist/server/modules/workbench/workbench-controller.js.map +1 -1
  105. package/dist/server/modules/workspace/affairs-library-service.d.ts +1 -0
  106. package/dist/server/modules/workspace/affairs-library-service.js +80 -0
  107. package/dist/server/modules/workspace/affairs-library-service.js.map +1 -1
  108. package/dist/server/modules/workspace/affairs-lightweight-session-service.js +13 -9
  109. package/dist/server/modules/workspace/affairs-lightweight-session-service.js.map +1 -1
  110. package/dist/server/routes/peer-hosts.d.ts +3 -0
  111. package/dist/server/routes/peer-hosts.js +18 -0
  112. package/dist/server/routes/peer-hosts.js.map +1 -0
  113. package/dist/server/routes/public.d.ts +2 -1
  114. package/dist/server/routes/public.js +2 -1
  115. package/dist/server/routes/public.js.map +1 -1
  116. package/dist/server/server/create-server.d.ts +4 -0
  117. package/dist/server/server/create-server.js +30 -2
  118. package/dist/server/server/create-server.js.map +1 -1
  119. package/dist/server/shared/http/error-handler.js +12 -0
  120. package/dist/server/shared/http/error-handler.js.map +1 -1
  121. package/dist/server/storage/repositories/peer-host-repository.d.ts +44 -0
  122. package/dist/server/storage/repositories/peer-host-repository.js +271 -0
  123. package/dist/server/storage/repositories/peer-host-repository.js.map +1 -0
  124. package/dist/server/storage/sqlite/client.js +81 -0
  125. package/dist/server/storage/sqlite/client.js.map +1 -1
  126. package/dist/server/storage/sqlite/schema.sql +64 -0
  127. package/dist/server/types/domain.d.ts +43 -0
  128. package/dist/server/ws/workbench-ws-hub.js +5 -14
  129. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  130. package/dist/server/ws/ws-server.d.ts +2 -1
  131. package/dist/server/ws/ws-server.js +5 -1
  132. package/dist/server/ws/ws-server.js.map +1 -1
  133. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +7 -0
  134. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +623 -9
  135. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  136. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js +8 -1
  137. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js.map +1 -1
  138. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +4 -0
  139. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +375 -15
  140. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  141. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +11 -0
  142. package/package.json +1 -1
  143. package/dist/public/assets/App-If9gThKM.js +0 -30
  144. package/dist/public/assets/ConversationPage-Bfb7GLTM.js +0 -9
  145. package/dist/public/assets/DesktopWindowPage-D1xwgS-7.js +0 -2
  146. package/dist/public/assets/FileContextPanel-C4syif3B.js +0 -1
  147. package/dist/public/assets/GitSidebar-DduL9aTV.js +0 -6
  148. package/dist/public/assets/MobileWorkspaceSwitcherHeader-DT330cAx.js +0 -1
  149. package/dist/public/assets/SessionIndexPage-DyMikN_x.js +0 -1
  150. package/dist/public/assets/SettingsPage-CDAVsPr3.js +0 -2
  151. package/dist/public/assets/TerminalManagerPanel-4OR47vcf.js +0 -1
  152. package/dist/public/assets/TerminalPage-Pvx396YX.js +0 -55
  153. package/dist/public/assets/ToolFilesPage-DrYHk0N-.js +0 -1
  154. package/dist/public/assets/ToolGitPage-Dz1q-Ns_.js +0 -1
  155. package/dist/public/assets/ToolProcessesPage-CRhphOmM.js +0 -1
  156. package/dist/public/assets/ToolsHomePage-BJSDLR6T.js +0 -1
  157. package/dist/public/assets/WorkbenchLandingPage-BlkxdOLC.js +0 -1
  158. package/dist/public/assets/WorkbenchLayout-D-U7ghT0.js +0 -1022
  159. package/dist/public/assets/WorkbenchShellRoute-DyWSCHz_.js +0 -1
  160. package/dist/public/assets/WorkbenchShellRoute-htbkGbtW.css +0 -1
  161. package/dist/public/assets/WorkspaceDebugDetailPage-B4ol2_a5.js +0 -1
  162. package/dist/public/assets/WorkspaceDetailPage-DMakfmHR.js +0 -1
  163. package/dist/public/assets/WorkspaceHomePage-tmCafatd.js +0 -1
  164. package/dist/public/assets/index-DmUJ8tIw.css +0 -1
  165. package/dist/public/assets/index-_OCkVmfl.js +0 -50
  166. package/dist/public/assets/relay-entry-B5GmiOrR.js +0 -1
  167. package/dist/public/assets/styles-DkbkRgWw.css +0 -1
  168. package/dist/public/assets/terminal-runtime-meta-DBsyT35T.js +0 -1
  169. package/dist/public/assets/workbench-navigation-RyUjchbD.js +0 -1
  170. /package/dist/public/assets/{styles-JKFlsYFv.js → styles-DwSuZo1w.js} +0 -0
@@ -5,7 +5,7 @@ import { appendJsonLine, createRawRef, encodeCursor, ensureDirectory, extractTex
5
5
  import { buildCodexResumeHistoryFromRawStore } from "../codex-resume-history.js";
6
6
  import { buildApplyPatchFromCodexCommandLikeValue } from "../patch-builder.js";
7
7
  import { loadDatabaseSync } from "../sqlite/node-sqlite.js";
8
- const CODEX_SESSION_TITLE_MAX_LENGTH = 48;
8
+ const CODEX_SESSION_TITLE_MAX_LENGTH = 72;
9
9
  const HISTORY_CACHE_LIMIT = 6;
10
10
  const SESSION_SUMMARY_CACHE_LIMIT = 512;
11
11
  const SPAWN_RELATION_SCAN_CACHE_LIMIT = 512;
@@ -35,7 +35,7 @@ export class CodexAdapter {
35
35
  const startedAt = Date.now();
36
36
  const targetPath = normalizeWorkspacePath(workspacePath);
37
37
  const knownSessions = (options?.knownSessions ?? []).filter((session) => session.provider === this.providerId);
38
- const threadMetadataIndex = this.readThreadMetadataIndex();
38
+ const threadMetadataIndex = await this.readThreadMetadataIndexForWorkspace(targetPath);
39
39
  const files = this.listSessionFiles(targetPath, threadMetadataIndex, knownSessions);
40
40
  const knownByRawStoreRef = new Map(knownSessions.map((session) => [session.rawStoreRef, session]));
41
41
  const knownByProviderSessionId = new Map(knownSessions.map((session) => [session.providerSessionId, session]));
@@ -239,6 +239,7 @@ export class CodexAdapter {
239
239
  resolveCodexFallbackTitle(messages) ??
240
240
  fileSessionId;
241
241
  const lastMessageAt = messages.at(-1)?.timestamp ?? (ensureText(metaPayload.timestamp) || null);
242
+ const fileActivityObservation = resolveCodexJsonlActivityObservation(filterInheritedCodexSubagentRecords(records, codexSessionId));
242
243
  const summary = this.hydrateSessionSummary({
243
244
  provider: this.providerId,
244
245
  providerSessionId: codexSessionId,
@@ -247,7 +248,8 @@ export class CodexAdapter {
247
248
  rawStoreRef: filePath,
248
249
  lastMessageAt,
249
250
  messageCount: messages.length,
250
- isArchived: false
251
+ isArchived: false,
252
+ activityObservation: fileActivityObservation
251
253
  }, filePath, stats, currentThreadMetadata, currentSpawnRelation);
252
254
  sessionsByProviderSessionId.set(codexSessionId, summary);
253
255
  this.touchSessionSummaryCache(filePath, {
@@ -556,6 +558,19 @@ export class CodexAdapter {
556
558
  }
557
559
  async renameSessionTitle(providerSessionId, rawStoreRef, title) {
558
560
  const nextTitle = title.trim();
561
+ const transport = this.options.threadControlTransportFactory?.();
562
+ if (transport) {
563
+ try {
564
+ await transport.initialize();
565
+ await transport.setThreadName?.(providerSessionId, nextTitle);
566
+ }
567
+ catch {
568
+ // app-server 的 thread/name/set 是首选,但失败时不能破坏原有本地改名能力。
569
+ }
570
+ finally {
571
+ transport.close();
572
+ }
573
+ }
559
574
  const resolvedStoreRef = this.resolveSessionFilePath(rawStoreRef, providerSessionId);
560
575
  const indexPath = join(this.options.homeDir, "session_index.jsonl");
561
576
  const stateDbPath = findLatestCodexStateDatabase(this.options.homeDir);
@@ -794,11 +809,15 @@ export class CodexAdapter {
794
809
  title: normalizeCodexIndexedTitle(ensureText(record.thread_name)) || null,
795
810
  cwd: null,
796
811
  createdAtMs: null,
812
+ updatedAtMs: null,
797
813
  firstUserMessage: null,
798
814
  agentNickname: null,
799
815
  agentRole: null,
816
+ parentProviderSessionId: null,
817
+ parentRelationKind: null,
800
818
  isArchived: null,
801
- rolloutPath: null
819
+ rolloutPath: null,
820
+ activityObservation: null
802
821
  });
803
822
  }
804
823
  catch {
@@ -844,15 +863,19 @@ export class CodexAdapter {
844
863
  title: current?.title ?? dbTitle,
845
864
  cwd: ensureText(row.cwd).trim() || (current?.cwd ?? null),
846
865
  createdAtMs: Number.isFinite(createdAtSeconds) ? createdAtSeconds * 1000 : null,
866
+ updatedAtMs: current?.updatedAtMs ?? null,
847
867
  firstUserMessage: ensureText(row.first_user_message).trim() || (current?.firstUserMessage ?? null),
848
868
  agentNickname: ensureText(row.agent_nickname).trim() || (current?.agentNickname ?? null),
849
869
  agentRole: ensureText(row.agent_role).trim() || (current?.agentRole ?? null),
870
+ parentProviderSessionId: current?.parentProviderSessionId ?? null,
871
+ parentRelationKind: current?.parentRelationKind ?? null,
850
872
  rolloutPath: ensureText(row.rollout_path).trim() || (current?.rolloutPath ?? null),
851
873
  isArchived: typeof row.archived === "number"
852
874
  ? row.archived === 1
853
875
  : ensureText(row.rollout_path).includes("archived_sessions")
854
876
  ? true
855
- : (current?.isArchived ?? null)
877
+ : (current?.isArchived ?? null),
878
+ activityObservation: current?.activityObservation ?? null
856
879
  });
857
880
  }
858
881
  }
@@ -876,6 +899,91 @@ export class CodexAdapter {
876
899
  };
877
900
  return index;
878
901
  }
902
+ async readThreadMetadataIndexForWorkspace(workspacePath) {
903
+ const index = new Map(this.readThreadMetadataIndex());
904
+ await this.mergeAppServerThreadMetadata(index, workspacePath);
905
+ return index;
906
+ }
907
+ async mergeAppServerThreadMetadata(index, workspacePath) {
908
+ const createTransport = this.options.threadControlTransportFactory;
909
+ if (!createTransport) {
910
+ return;
911
+ }
912
+ const transport = createTransport();
913
+ try {
914
+ await transport.initialize();
915
+ const threads = await transport.listThreads?.({ workspacePath });
916
+ if (!threads || threads.length === 0) {
917
+ return;
918
+ }
919
+ for (const thread of threads) {
920
+ const metadata = normalizeCodexAppServerThreadMetadata(thread);
921
+ if (!metadata) {
922
+ continue;
923
+ }
924
+ const current = index.get(metadata.threadId);
925
+ index.set(metadata.threadId, {
926
+ title: metadata.title ?? current?.title ?? null,
927
+ cwd: metadata.cwd ?? current?.cwd ?? null,
928
+ createdAtMs: metadata.createdAtMs ?? current?.createdAtMs ?? null,
929
+ updatedAtMs: metadata.updatedAtMs ?? current?.updatedAtMs ?? null,
930
+ firstUserMessage: metadata.firstUserMessage ?? current?.firstUserMessage ?? null,
931
+ agentNickname: metadata.agentNickname ?? current?.agentNickname ?? null,
932
+ agentRole: metadata.agentRole ?? current?.agentRole ?? null,
933
+ parentProviderSessionId: metadata.parentProviderSessionId ?? current?.parentProviderSessionId ?? null,
934
+ parentRelationKind: metadata.parentRelationKind ?? current?.parentRelationKind ?? null,
935
+ isArchived: metadata.isArchived ?? current?.isArchived ?? null,
936
+ rolloutPath: metadata.rolloutPath ?? current?.rolloutPath ?? null,
937
+ activityObservation: metadata.activityObservation ?? current?.activityObservation ?? null
938
+ });
939
+ }
940
+ await this.mergeAppServerSubagentActivityFromParentThreads(index, threads, transport);
941
+ }
942
+ catch {
943
+ // app-server 是增强信息来源,失败时退回 JSONL/state DB,不能拖垮会话列表。
944
+ }
945
+ finally {
946
+ transport.close();
947
+ }
948
+ }
949
+ async mergeAppServerSubagentActivityFromParentThreads(index, threads, transport) {
950
+ const childrenByParentThreadId = new Map();
951
+ for (const thread of threads) {
952
+ const metadata = normalizeCodexAppServerThreadMetadata(thread);
953
+ if (!metadata
954
+ || metadata.parentRelationKind !== "spawn"
955
+ || !metadata.parentProviderSessionId) {
956
+ continue;
957
+ }
958
+ const children = childrenByParentThreadId.get(metadata.parentProviderSessionId) ?? [];
959
+ children.push(metadata.threadId);
960
+ childrenByParentThreadId.set(metadata.parentProviderSessionId, children);
961
+ }
962
+ for (const [parentThreadId, childThreadIds] of childrenByParentThreadId) {
963
+ let parentThread = null;
964
+ try {
965
+ const result = await transport.readThread(parentThreadId);
966
+ parentThread = asCodexRecord(result.thread) ?? result;
967
+ }
968
+ catch {
969
+ continue;
970
+ }
971
+ for (const childThreadId of childThreadIds) {
972
+ const observation = resolveLatestCodexCollabAgentActivityObservation(parentThread, childThreadId);
973
+ if (!observation) {
974
+ continue;
975
+ }
976
+ const current = index.get(childThreadId);
977
+ if (!current) {
978
+ continue;
979
+ }
980
+ index.set(childThreadId, {
981
+ ...current,
982
+ activityObservation: observation
983
+ });
984
+ }
985
+ }
986
+ }
879
987
  listSessionFiles(targetPath, threadMetadataIndex, knownSessions) {
880
988
  const activeFiles = walkJsonlFiles(join(this.options.homeDir, "sessions"));
881
989
  const archivedFiles = this.listArchivedSessionFiles(targetPath, threadMetadataIndex, knownSessions);
@@ -1151,12 +1259,20 @@ export class CodexAdapter {
1151
1259
  const indexedTitle = normalizeCodexIndexedTitle(metadata?.title);
1152
1260
  const normalizedFirstUserMessage = normalizeCodexIndexedTitle(metadata?.firstUserMessage);
1153
1261
  if (indexedTitle) {
1262
+ if (isCodexSubagentThread(metadata, null)
1263
+ && normalizedFirstUserMessage
1264
+ && indexedTitle === normalizedFirstUserMessage) {
1265
+ return null;
1266
+ }
1154
1267
  // Codex 有时会把第一条用户消息原样回填成 title,这种脏标题仍然按统一长度预算裁掉。
1155
1268
  if (normalizedFirstUserMessage && indexedTitle === normalizedFirstUserMessage) {
1156
1269
  return indexedTitle.slice(0, CODEX_SESSION_TITLE_MAX_LENGTH);
1157
1270
  }
1158
1271
  return indexedTitle;
1159
1272
  }
1273
+ if (isCodexSubagentThread(metadata, null)) {
1274
+ return null;
1275
+ }
1160
1276
  return normalizeCodexMessageTitle(metadata?.firstUserMessage);
1161
1277
  }
1162
1278
  readSpawnedAgentRelationIndex(files, targetPath, threadMetadataIndex, candidateThreadIds) {
@@ -1283,6 +1399,13 @@ export class CodexAdapter {
1283
1399
  if (directRelations.has(threadId)) {
1284
1400
  continue;
1285
1401
  }
1402
+ if (metadata.parentProviderSessionId) {
1403
+ directRelations.set(threadId, {
1404
+ parentProviderSessionId: metadata.parentProviderSessionId,
1405
+ kind: metadata.parentRelationKind ?? "spawn"
1406
+ });
1407
+ continue;
1408
+ }
1286
1409
  if (!metadata.agentRole && !metadata.agentNickname) {
1287
1410
  continue;
1288
1411
  }
@@ -1331,11 +1454,13 @@ export class CodexAdapter {
1331
1454
  hydrateSessionSummary(summary, filePath, stats, metadata, relation) {
1332
1455
  const resolvedRelation = relation ?? null;
1333
1456
  const resolvedMetadata = metadata ?? null;
1457
+ const metadataTitle = resolveCodexMetadataTitle(resolvedMetadata);
1334
1458
  const isSubagent = resolvedMetadata || resolvedRelation
1335
1459
  ? isCodexSubagentThread(resolvedMetadata, resolvedRelation)
1336
1460
  : Boolean(summary.isSubagent);
1337
1461
  return {
1338
1462
  ...summary,
1463
+ title: metadataTitle ?? summary.title,
1339
1464
  rawStoreRef: filePath,
1340
1465
  isArchived: resolveCodexArchivedState(resolvedMetadata, filePath),
1341
1466
  parentProviderSessionId: resolvedRelation?.parentProviderSessionId ?? summary.parentProviderSessionId ?? null,
@@ -1344,14 +1469,15 @@ export class CodexAdapter {
1344
1469
  ? buildCodexSubagentLabel(resolvedMetadata)
1345
1470
  : summary.subagentLabel ?? null,
1346
1471
  sourceMtimeMs: stats.mtimeMs,
1347
- sourceSizeBytes: stats.size
1472
+ sourceSizeBytes: stats.size,
1473
+ activityObservation: mergeCodexActivityObservation(resolvedMetadata?.activityObservation ?? null, summary.activityObservation ?? null)
1348
1474
  };
1349
1475
  }
1350
1476
  parseMessages(filePath, records, providerSessionId) {
1351
1477
  return this.parseMessagesFromEntries(filePath, records, providerSessionId);
1352
1478
  }
1353
1479
  parseMessagesFromEntries(filePath, records, providerSessionId) {
1354
- const effectiveRecords = filterRolledBackCodexRecords(records);
1480
+ const effectiveRecords = filterRolledBackCodexRecords(filterInheritedCodexSubagentRecords(records, providerSessionId));
1355
1481
  const messages = [];
1356
1482
  const messageIndexesByKey = new Map();
1357
1483
  const toolNameById = new Map();
@@ -1699,6 +1825,105 @@ function filterRolledBackCodexRecords(records) {
1699
1825
  }
1700
1826
  return filteredRecords;
1701
1827
  }
1828
+ function filterInheritedCodexSubagentRecords(records, providerSessionId) {
1829
+ const boundary = resolveCodexSessionInheritanceBoundary(records, providerSessionId);
1830
+ if (!boundary.inheritedParentThreadId || boundary.startLineNumber === null) {
1831
+ return records;
1832
+ }
1833
+ const startLineNumber = boundary.startLineNumber;
1834
+ return records.filter((record) => {
1835
+ if (record.lineNumber < startLineNumber) {
1836
+ return shouldKeepCodexRecordBeforeSubagentBoundary(record, boundary.threadId);
1837
+ }
1838
+ return true;
1839
+ });
1840
+ }
1841
+ function resolveCodexSessionInheritanceBoundary(records, providerSessionId) {
1842
+ const fallbackThreadId = ensureText(providerSessionId).trim();
1843
+ let currentThreadId = fallbackThreadId;
1844
+ let inheritedParentThreadId = null;
1845
+ let startLineNumber = null;
1846
+ for (const record of records) {
1847
+ if (record.data.type !== "session_meta") {
1848
+ continue;
1849
+ }
1850
+ const payload = (record.data.payload ?? {});
1851
+ const metaId = ensureText(payload.id).trim();
1852
+ const threadId = looksLikeCodexThreadId(metaId) ? metaId : fallbackThreadId || metaId;
1853
+ const parentRelation = resolveCodexParentThreadRelation(payload);
1854
+ const isSubagent = parentRelation.kind === "spawn"
1855
+ || ensureText(payload.thread_source).trim() === "subagent"
1856
+ || ensureText(payload.agent_nickname).trim().length > 0
1857
+ || ensureText(payload.agent_role).trim().length > 0;
1858
+ if (isSubagent
1859
+ && parentRelation.parentThreadId
1860
+ && (!fallbackThreadId || threadId === fallbackThreadId)) {
1861
+ currentThreadId = threadId;
1862
+ inheritedParentThreadId = parentRelation.parentThreadId;
1863
+ continue;
1864
+ }
1865
+ if (inheritedParentThreadId && threadId === inheritedParentThreadId) {
1866
+ continue;
1867
+ }
1868
+ if (inheritedParentThreadId && threadId === currentThreadId && startLineNumber === null) {
1869
+ startLineNumber = record.lineNumber;
1870
+ }
1871
+ }
1872
+ if (inheritedParentThreadId && startLineNumber === null) {
1873
+ startLineNumber = findFirstOwnCodexSubagentTurnLineNumber(records, currentThreadId, inheritedParentThreadId);
1874
+ }
1875
+ return {
1876
+ threadId: currentThreadId,
1877
+ inheritedParentThreadId,
1878
+ startLineNumber
1879
+ };
1880
+ }
1881
+ function findFirstOwnCodexSubagentTurnLineNumber(records, threadId, inheritedParentThreadId) {
1882
+ for (const record of records) {
1883
+ if (record.data.type === "turn_context") {
1884
+ const payload = (record.data.payload ?? {});
1885
+ const turnId = ensureText(payload.turn_id).trim();
1886
+ if (isCodexOwnTurnId(turnId, threadId, inheritedParentThreadId)) {
1887
+ return record.lineNumber;
1888
+ }
1889
+ continue;
1890
+ }
1891
+ if (record.data.type !== "event_msg") {
1892
+ continue;
1893
+ }
1894
+ const payload = (record.data.payload ?? {});
1895
+ const eventType = ensureText(payload.type).trim();
1896
+ const turnId = ensureText(payload.turn_id).trim();
1897
+ if (eventType === "task_started"
1898
+ && turnId.length > 0
1899
+ && isCodexOwnTurnId(turnId, threadId, inheritedParentThreadId)) {
1900
+ return record.lineNumber;
1901
+ }
1902
+ }
1903
+ return null;
1904
+ }
1905
+ function isCodexOwnTurnId(turnId, threadId, inheritedParentThreadId) {
1906
+ if (!looksLikeCodexThreadId(turnId)) {
1907
+ return false;
1908
+ }
1909
+ if (turnId === inheritedParentThreadId) {
1910
+ return false;
1911
+ }
1912
+ if (looksLikeCodexThreadId(threadId)) {
1913
+ // Codex 的 thread id / turn id 都是 UUIDv7。子 Agent 自己的 turn 会在子 thread 创建之后;
1914
+ // fork 继承来的父会话 turn 一定早于子 thread。这里用时间有序 ID 切掉继承前缀。
1915
+ return turnId.localeCompare(threadId) >= 0;
1916
+ }
1917
+ return !turnId.startsWith(inheritedParentThreadId.slice(0, 8));
1918
+ }
1919
+ function shouldKeepCodexRecordBeforeSubagentBoundary(record, threadId) {
1920
+ if (record.data.type !== "session_meta") {
1921
+ return false;
1922
+ }
1923
+ const payload = (record.data.payload ?? {});
1924
+ const metaId = ensureText(payload.id).trim();
1925
+ return metaId === threadId;
1926
+ }
1702
1927
  function buildRecentHistoryPage(messages, totalMessageCount, limit) {
1703
1928
  const effectiveTotal = Math.max(totalMessageCount, messages.length);
1704
1929
  const pageMessages = messages.slice(-Math.min(limit, messages.length)).map((message, index, items) => ({
@@ -1952,6 +2177,13 @@ function readNonNegativeInteger(value) {
1952
2177
  }
1953
2178
  return null;
1954
2179
  }
2180
+ function readFiniteNumber(value) {
2181
+ if (typeof value === "number" && Number.isFinite(value)) {
2182
+ return value;
2183
+ }
2184
+ const parsed = Number.parseFloat(ensureText(value));
2185
+ return Number.isFinite(parsed) ? parsed : null;
2186
+ }
1955
2187
  function clampUsageRatio(promptTokens, contextWindow) {
1956
2188
  if (contextWindow <= 0) {
1957
2189
  return 0;
@@ -2019,6 +2251,362 @@ function resolveCodexParentThreadRelation(payload) {
2019
2251
  kind: null
2020
2252
  };
2021
2253
  }
2254
+ function normalizeCodexAppServerThreadMetadata(thread) {
2255
+ const threadId = ensureText(thread.id).trim();
2256
+ if (!looksLikeCodexThreadId(threadId)) {
2257
+ return null;
2258
+ }
2259
+ const source = thread.source;
2260
+ const sourceRecord = typeof source === "object" && source !== null
2261
+ ? source
2262
+ : null;
2263
+ const subAgentSource = typeof sourceRecord?.subAgent === "object" && sourceRecord.subAgent !== null
2264
+ ? sourceRecord.subAgent
2265
+ : typeof sourceRecord?.subagent === "object" && sourceRecord.subagent !== null
2266
+ ? sourceRecord.subagent
2267
+ : null;
2268
+ const threadSpawn = typeof subAgentSource?.thread_spawn === "object" && subAgentSource.thread_spawn !== null
2269
+ ? subAgentSource.thread_spawn
2270
+ : typeof subAgentSource?.threadSpawn === "object" && subAgentSource.threadSpawn !== null
2271
+ ? subAgentSource.threadSpawn
2272
+ : null;
2273
+ const spawnParentThreadId = ensureText(threadSpawn?.parent_thread_id).trim()
2274
+ || ensureText(threadSpawn?.parentThreadId).trim();
2275
+ const forkedFromId = ensureText(thread.forkedFromId ?? thread.forked_from_id).trim();
2276
+ const parentProviderSessionId = spawnParentThreadId || forkedFromId || null;
2277
+ const agentNickname = ensureText(thread.agentNickname ?? thread.agent_nickname).trim()
2278
+ || ensureText(threadSpawn?.agent_nickname ?? threadSpawn?.agentNickname).trim()
2279
+ || null;
2280
+ const agentRole = ensureText(thread.agentRole ?? thread.agent_role).trim()
2281
+ || ensureText(threadSpawn?.agent_role ?? threadSpawn?.agentRole).trim()
2282
+ || null;
2283
+ const createdAtSeconds = readFiniteNumber(thread.createdAt ?? thread.created_at);
2284
+ const updatedAtSeconds = readFiniteNumber(thread.updatedAt ?? thread.updated_at);
2285
+ const preview = ensureText(thread.preview).trim();
2286
+ const name = normalizeCodexIndexedTitle(ensureText(thread.name));
2287
+ const path = ensureText(thread.path).trim();
2288
+ const cwd = ensureText(thread.cwd).trim();
2289
+ const activityObservation = resolveCodexThreadActivityObservation(thread);
2290
+ return {
2291
+ threadId,
2292
+ title: name || null,
2293
+ cwd: cwd || null,
2294
+ createdAtMs: createdAtSeconds !== null ? createdAtSeconds * 1000 : null,
2295
+ updatedAtMs: updatedAtSeconds !== null ? updatedAtSeconds * 1000 : null,
2296
+ firstUserMessage: preview || null,
2297
+ agentNickname,
2298
+ agentRole,
2299
+ parentProviderSessionId,
2300
+ parentRelationKind: spawnParentThreadId.length > 0
2301
+ ? "spawn"
2302
+ : forkedFromId.length > 0
2303
+ ? "fork"
2304
+ : null,
2305
+ isArchived: null,
2306
+ rolloutPath: path || null,
2307
+ activityObservation
2308
+ };
2309
+ }
2310
+ function resolveCodexThreadActivityObservation(thread) {
2311
+ const direct = resolveCodexThreadStatusActivityObservation(thread);
2312
+ const threadId = ensureText(thread.id).trim();
2313
+ const parent = resolveLatestCodexCollabAgentActivityObservation(thread, threadId);
2314
+ if (!direct) {
2315
+ return parent;
2316
+ }
2317
+ if (!parent) {
2318
+ return direct;
2319
+ }
2320
+ if (isTerminalProviderSessionObservation(parent)
2321
+ && (direct.runningState === "starting" || direct.runningState === "running")) {
2322
+ return parent;
2323
+ }
2324
+ const directAt = direct.observedAt ?? "";
2325
+ const parentAt = parent.observedAt ?? "";
2326
+ if (parentAt && (!directAt || parentAt.localeCompare(directAt) >= 0)) {
2327
+ return parent;
2328
+ }
2329
+ return direct;
2330
+ }
2331
+ function resolveCodexJsonlActivityObservation(records) {
2332
+ let latest = null;
2333
+ for (const record of records) {
2334
+ if (record.data.type !== "event_msg") {
2335
+ continue;
2336
+ }
2337
+ const payload = (record.data.payload ?? {});
2338
+ const eventType = ensureText(payload.type).trim();
2339
+ const turnId = ensureText(payload.turn_id).trim() || null;
2340
+ const observedAt = (codexSecondsToIso(payload.completed_at ?? payload.completedAt)
2341
+ ?? codexSecondsToIso(payload.started_at ?? payload.startedAt)
2342
+ ?? ensureText(record.data.timestamp).trim())
2343
+ || null;
2344
+ let observation = null;
2345
+ if (eventType === "task_started") {
2346
+ observation = {
2347
+ runningState: "running",
2348
+ confidence: "strong",
2349
+ observedAt,
2350
+ detail: null,
2351
+ errorCode: null,
2352
+ runId: turnId
2353
+ };
2354
+ }
2355
+ else if (eventType === "task_complete") {
2356
+ observation = {
2357
+ runningState: "completed",
2358
+ confidence: "strong",
2359
+ observedAt,
2360
+ detail: null,
2361
+ errorCode: null,
2362
+ runId: turnId
2363
+ };
2364
+ }
2365
+ else if (eventType === "task_failed") {
2366
+ observation = {
2367
+ runningState: "failed",
2368
+ confidence: "strong",
2369
+ observedAt,
2370
+ detail: ensureText(payload.error).trim()
2371
+ || ensureText(payload.message).trim()
2372
+ || "Codex task failed",
2373
+ errorCode: "CODEX_TASK_FAILED",
2374
+ runId: turnId
2375
+ };
2376
+ }
2377
+ if (!observation) {
2378
+ continue;
2379
+ }
2380
+ if (!latest || compareNullableIso(observation.observedAt, latest.observedAt) >= 0) {
2381
+ latest = observation;
2382
+ }
2383
+ }
2384
+ return latest;
2385
+ }
2386
+ function mergeCodexActivityObservation(primary, fallback) {
2387
+ if (!primary) {
2388
+ return fallback;
2389
+ }
2390
+ if (!fallback) {
2391
+ return primary;
2392
+ }
2393
+ const primaryAt = primary.observedAt ?? "";
2394
+ const fallbackAt = fallback.observedAt ?? "";
2395
+ const order = compareNullableIso(fallback.observedAt, primary.observedAt);
2396
+ // 同一份 Codex 记录里,后面的 task_started 才代表当前状态。
2397
+ // 旧逻辑无脑保留 terminal,会把上一轮 task_complete 错当成整个父会话已经结束。
2398
+ if (order > 0) {
2399
+ return fallback;
2400
+ }
2401
+ if (order < 0) {
2402
+ return primary;
2403
+ }
2404
+ if (isTerminalProviderSessionObservation(primary)) {
2405
+ return primary;
2406
+ }
2407
+ if (isTerminalProviderSessionObservation(fallback)) {
2408
+ return fallback;
2409
+ }
2410
+ if (primary.runningState === "starting" || primary.runningState === "running") {
2411
+ return primary;
2412
+ }
2413
+ if (fallback.runningState === "starting" || fallback.runningState === "running") {
2414
+ return fallback;
2415
+ }
2416
+ return fallbackAt && (!primaryAt || fallbackAt.localeCompare(primaryAt) > 0)
2417
+ ? fallback
2418
+ : primary;
2419
+ }
2420
+ function isTerminalProviderSessionObservation(observation) {
2421
+ return observation.runningState === "completed"
2422
+ || observation.runningState === "interrupted"
2423
+ || observation.runningState === "failed";
2424
+ }
2425
+ function resolveCodexThreadStatusActivityObservation(thread) {
2426
+ const status = asCodexRecord(thread.status);
2427
+ const statusType = ensureText(status?.type).trim();
2428
+ const observedAt = codexSecondsToIso(thread.updatedAt ?? thread.updated_at);
2429
+ if (statusType === "active") {
2430
+ return {
2431
+ runningState: "running",
2432
+ confidence: "authoritative",
2433
+ observedAt,
2434
+ detail: null,
2435
+ errorCode: null,
2436
+ runId: null
2437
+ };
2438
+ }
2439
+ if (statusType === "idle" || statusType === "notLoaded") {
2440
+ return {
2441
+ runningState: "idle",
2442
+ confidence: "strong",
2443
+ observedAt,
2444
+ detail: null,
2445
+ errorCode: null,
2446
+ runId: null
2447
+ };
2448
+ }
2449
+ if (statusType === "systemError") {
2450
+ return {
2451
+ runningState: "failed",
2452
+ confidence: "strong",
2453
+ observedAt,
2454
+ detail: "Codex app-server reported a system error",
2455
+ errorCode: "CODEX_APP_SERVER_SYSTEM_ERROR",
2456
+ runId: null
2457
+ };
2458
+ }
2459
+ return null;
2460
+ }
2461
+ function resolveLatestCodexCollabAgentActivityObservation(thread, threadId) {
2462
+ if (!threadId) {
2463
+ return null;
2464
+ }
2465
+ let latest = null;
2466
+ for (const turn of collectCodexThreadTurns(thread)) {
2467
+ const turnRecord = asCodexRecord(turn);
2468
+ const observedAt = codexSecondsToIso(turnRecord?.completedAt ?? turnRecord?.completed_at)
2469
+ ?? codexSecondsToIso(turnRecord?.startedAt ?? turnRecord?.started_at)
2470
+ ?? codexSecondsToIso(thread.updatedAt ?? thread.updated_at);
2471
+ const runId = ensureText(turnRecord?.id).trim() || null;
2472
+ const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
2473
+ for (const item of items) {
2474
+ const itemRecord = asCodexRecord(item);
2475
+ if (!itemRecord || ensureText(itemRecord.type).trim() !== "collabAgentToolCall") {
2476
+ continue;
2477
+ }
2478
+ const receivers = Array.isArray(itemRecord.receiverThreadIds)
2479
+ ? itemRecord.receiverThreadIds.map((value) => ensureText(value).trim())
2480
+ : [];
2481
+ if (!receivers.includes(threadId)) {
2482
+ continue;
2483
+ }
2484
+ const agentsStates = asCodexRecord(itemRecord.agentsStates);
2485
+ const agentState = asCodexRecord(agentsStates?.[threadId]);
2486
+ const agentStatus = ensureText(agentState?.status).trim();
2487
+ const observation = mapCodexCollabAgentStatusToActivityObservation({
2488
+ status: agentStatus,
2489
+ message: ensureText(agentState?.message).trim() || null,
2490
+ tool: ensureText(itemRecord.tool).trim(),
2491
+ callStatus: ensureText(itemRecord.status).trim(),
2492
+ observedAt,
2493
+ runId
2494
+ });
2495
+ if (!observation) {
2496
+ continue;
2497
+ }
2498
+ if (!latest || compareNullableIso(observation.observedAt, latest.observedAt) >= 0) {
2499
+ latest = observation;
2500
+ }
2501
+ }
2502
+ }
2503
+ return latest;
2504
+ }
2505
+ function mapCodexCollabAgentStatusToActivityObservation(input) {
2506
+ const terminalDetail = input.message ?? null;
2507
+ switch (input.status) {
2508
+ case "pendingInit":
2509
+ return {
2510
+ runningState: "starting",
2511
+ confidence: "authoritative",
2512
+ observedAt: input.observedAt,
2513
+ detail: input.tool ? `Codex sub-agent ${input.tool} is pending` : null,
2514
+ errorCode: null,
2515
+ runId: input.runId
2516
+ };
2517
+ case "running":
2518
+ return {
2519
+ runningState: "running",
2520
+ confidence: "authoritative",
2521
+ observedAt: input.observedAt,
2522
+ detail: null,
2523
+ errorCode: null,
2524
+ runId: input.runId
2525
+ };
2526
+ case "completed":
2527
+ return {
2528
+ runningState: "completed",
2529
+ confidence: "strong",
2530
+ observedAt: input.observedAt,
2531
+ detail: terminalDetail,
2532
+ errorCode: null,
2533
+ runId: input.runId
2534
+ };
2535
+ case "interrupted":
2536
+ return {
2537
+ runningState: "interrupted",
2538
+ confidence: "strong",
2539
+ observedAt: input.observedAt,
2540
+ detail: terminalDetail,
2541
+ errorCode: null,
2542
+ runId: input.runId
2543
+ };
2544
+ case "errored":
2545
+ return {
2546
+ runningState: "failed",
2547
+ confidence: "strong",
2548
+ observedAt: input.observedAt,
2549
+ detail: terminalDetail ?? "Codex sub-agent failed",
2550
+ errorCode: "CODEX_SUBAGENT_FAILED",
2551
+ runId: input.runId
2552
+ };
2553
+ case "shutdown":
2554
+ case "notFound":
2555
+ return {
2556
+ runningState: "completed",
2557
+ confidence: "strong",
2558
+ observedAt: input.observedAt,
2559
+ detail: terminalDetail,
2560
+ errorCode: null,
2561
+ runId: input.runId
2562
+ };
2563
+ default:
2564
+ if (input.tool === "closeAgent" && input.callStatus === "completed") {
2565
+ return {
2566
+ runningState: "completed",
2567
+ confidence: "strong",
2568
+ observedAt: input.observedAt,
2569
+ detail: terminalDetail,
2570
+ errorCode: null,
2571
+ runId: input.runId
2572
+ };
2573
+ }
2574
+ return null;
2575
+ }
2576
+ }
2577
+ function collectCodexThreadTurns(thread) {
2578
+ if (Array.isArray(thread.turns)) {
2579
+ return thread.turns;
2580
+ }
2581
+ const nestedThread = asCodexRecord(thread.thread);
2582
+ if (Array.isArray(nestedThread?.turns)) {
2583
+ return nestedThread.turns;
2584
+ }
2585
+ return [];
2586
+ }
2587
+ function asCodexRecord(value) {
2588
+ return value && typeof value === "object" ? value : null;
2589
+ }
2590
+ function codexSecondsToIso(value) {
2591
+ const seconds = readFiniteNumber(value);
2592
+ if (seconds === null) {
2593
+ return null;
2594
+ }
2595
+ const date = new Date(seconds * 1000);
2596
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
2597
+ }
2598
+ function compareNullableIso(left, right) {
2599
+ if (left === right) {
2600
+ return 0;
2601
+ }
2602
+ if (!left) {
2603
+ return -1;
2604
+ }
2605
+ if (!right) {
2606
+ return 1;
2607
+ }
2608
+ return left.localeCompare(right);
2609
+ }
2022
2610
  function toTimestampMs(value) {
2023
2611
  const timestampMs = Date.parse(ensureText(value).trim());
2024
2612
  return Number.isFinite(timestampMs) ? timestampMs : null;
@@ -2043,6 +2631,17 @@ function pickClosestCodexSpawnRecord(spawnRecords, workspacePath, message, creat
2043
2631
  function isCodexSubagentThread(metadata, relation) {
2044
2632
  return Boolean(relation?.kind === "spawn" || metadata?.agentRole || metadata?.agentNickname);
2045
2633
  }
2634
+ function resolveCodexMetadataTitle(metadata) {
2635
+ const title = normalizeCodexIndexedTitle(metadata?.title);
2636
+ if (!title) {
2637
+ return null;
2638
+ }
2639
+ const firstUserMessage = normalizeCodexIndexedTitle(metadata?.firstUserMessage);
2640
+ if (firstUserMessage && title === firstUserMessage) {
2641
+ return null;
2642
+ }
2643
+ return title;
2644
+ }
2046
2645
  function buildCodexSubagentLabel(metadata) {
2047
2646
  const agentRole = metadata?.agentRole?.trim() || "";
2048
2647
  const agentNickname = metadata?.agentNickname?.trim() || "";
@@ -2081,8 +2680,8 @@ function hasUsableCodexTitle(title) {
2081
2680
  return normalizeCodexIndexedTitle(title) !== null;
2082
2681
  }
2083
2682
  function normalizeCodexIndexedTitle(title) {
2084
- const normalized = ensureText(title).trim();
2085
- if (normalized.length === 0 || looksLikeCodexRulesMessage(normalized)) {
2683
+ const normalized = normalizeCodexTitleText(title);
2684
+ if (!normalized || looksLikeCodexRulesMessage(normalized)) {
2086
2685
  return null;
2087
2686
  }
2088
2687
  return normalized;
@@ -2091,6 +2690,21 @@ function normalizeCodexMessageTitle(content) {
2091
2690
  const normalized = normalizeCodexIndexedTitle(content);
2092
2691
  return normalized ? normalized.slice(0, CODEX_SESSION_TITLE_MAX_LENGTH) : null;
2093
2692
  }
2693
+ function normalizeCodexTitleText(content) {
2694
+ const normalized = ensureText(content).trim().replace(/\s+/g, " ");
2695
+ if (normalized.length === 0) {
2696
+ return null;
2697
+ }
2698
+ return extractCodexSubagentTaskTitle(normalized) ?? normalized;
2699
+ }
2700
+ function extractCodexSubagentTaskTitle(title) {
2701
+ const match = title.match(/^你是\s*Agent\s*[A-Za-z0-9_-]+\s*[,,。::;;\s]*负责\s*(.+)$/i);
2702
+ const task = match?.[1]?.trim().replace(/^[::,,。;;\s]+/, "").trim();
2703
+ if (!task) {
2704
+ return null;
2705
+ }
2706
+ return task;
2707
+ }
2094
2708
  function extractCodexThreadHistory(result) {
2095
2709
  const snapshot = extractCodexThreadHistorySnapshot(result);
2096
2710
  return snapshot.value;