@junctionpanel/server 0.1.62 → 0.1.63

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 (57) hide show
  1. package/dist/server/client/daemon-client.d.ts +24 -0
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +232 -2
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-manager.d.ts +5 -1
  6. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-manager.js +18 -2
  8. package/dist/server/server/agent/agent-manager.js.map +1 -1
  9. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  10. package/dist/server/server/agent/agent-projections.js +3 -0
  11. package/dist/server/server/agent/agent-projections.js.map +1 -1
  12. package/dist/server/server/agent/agent-sdk-types.d.ts +13 -0
  13. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  14. package/dist/server/server/agent/agent-storage.d.ts +3 -0
  15. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  16. package/dist/server/server/agent/agent-storage.js +1 -0
  17. package/dist/server/server/agent/agent-storage.js.map +1 -1
  18. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  19. package/dist/server/server/agent/mcp-server.js +2 -0
  20. package/dist/server/server/agent/mcp-server.js.map +1 -1
  21. package/dist/server/server/agent/providers/claude-agent.js +1 -0
  22. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  23. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -1
  24. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +59 -0
  25. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
  26. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +31 -0
  27. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  28. package/dist/server/server/agent/providers/codex-app-server-agent.js +1022 -50
  29. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  30. package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
  31. package/dist/server/server/agent/providers/gemini-agent.js +1 -0
  32. package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
  33. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  34. package/dist/server/server/agent/providers/opencode-agent.js +95 -14
  35. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  36. package/dist/server/server/bootstrap.d.ts +22 -0
  37. package/dist/server/server/bootstrap.d.ts.map +1 -1
  38. package/dist/server/server/bootstrap.js +10 -0
  39. package/dist/server/server/bootstrap.js.map +1 -1
  40. package/dist/server/server/persisted-config.d.ts +4 -4
  41. package/dist/server/server/session.d.ts +26 -0
  42. package/dist/server/server/session.d.ts.map +1 -1
  43. package/dist/server/server/session.js +439 -5
  44. package/dist/server/server/session.js.map +1 -1
  45. package/dist/server/server/voice-config.d.ts +10 -0
  46. package/dist/server/server/voice-config.d.ts.map +1 -0
  47. package/dist/server/server/voice-config.js +44 -0
  48. package/dist/server/server/voice-config.js.map +1 -0
  49. package/dist/server/server/websocket-server.d.ts +24 -1
  50. package/dist/server/server/websocket-server.d.ts.map +1 -1
  51. package/dist/server/server/websocket-server.js +61 -2
  52. package/dist/server/server/websocket-server.js.map +1 -1
  53. package/dist/server/shared/messages.d.ts +8394 -2300
  54. package/dist/server/shared/messages.d.ts.map +1 -1
  55. package/dist/server/shared/messages.js +135 -0
  56. package/dist/server/shared/messages.js.map +1 -1
  57. package/package.json +3 -3
@@ -58,6 +58,9 @@ const WORKSPACE_STATUS_WATCH_DEBOUNCE_MS = 250;
58
58
  const WORKSPACE_STATUS_GIT_REFRESH_MS = 3000;
59
59
  const WORKSPACE_STATUS_PR_ACTIVE_REFRESH_MS = 15000;
60
60
  const WORKSPACE_STATUS_PR_PASSIVE_REFRESH_MS = 60000;
61
+ const PROVIDER_CHILD_THREADS_REFRESH_DEBOUNCE_MS = 250;
62
+ const PROVIDER_CHILD_THREADS_PERSISTED_LIST_INITIAL_LIMIT = 500;
63
+ const PROVIDER_CHILD_THREADS_PERSISTED_LIST_MAX_LIMIT = 50000;
61
64
  const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
62
65
  const TERMINAL_STREAM_MAX_PENDING_BYTES = 2 * 1024 * 1024;
63
66
  const TERMINAL_STREAM_MAX_PENDING_CHUNKS = 2048;
@@ -168,6 +171,12 @@ export class Session {
168
171
  this.checkoutDiffSubscriptions = new Map();
169
172
  this.checkoutDiffTargets = new Map();
170
173
  this.workspaceGitOperationStates = sharedWorkspaceGitOperationStates;
174
+ this.providerChildThreadEntries = [];
175
+ this.providerChildThreadEntriesFingerprint = null;
176
+ this.providerChildThreadEntriesHydrated = false;
177
+ this.providerChildThreadRefreshTimer = null;
178
+ this.providerChildThreadRefreshPromise = null;
179
+ this.providerChildThreadRefreshQueued = false;
171
180
  const { clientId, userId, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, junctionHome, agentManager, agentStorage, createAgentMcpTransport, terminalManager, agentProviderRuntimeSettings, connectionContext, } = options;
172
181
  this.clientId = clientId;
173
182
  this.userId = userId;
@@ -393,8 +402,14 @@ export class Session {
393
402
  this.unsubscribeAgentEvents = this.agentManager.subscribe((event) => {
394
403
  if (event.type === 'agent_state') {
395
404
  void this.forwardAgentUpdate(event.agent);
405
+ if (event.agent.provider === 'codex') {
406
+ this.scheduleProviderChildThreadRefresh();
407
+ }
396
408
  return;
397
409
  }
410
+ if (this.shouldRefreshProviderChildThreadsForStreamEvent(event.event)) {
411
+ this.scheduleProviderChildThreadRefresh();
412
+ }
398
413
  // Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
399
414
  // for the focused agent, with a short grace window while backgrounded.
400
415
  // History catch-up is handled via pull-based `fetch_agent_timeline_request`.
@@ -597,12 +612,16 @@ export class Session {
597
612
  snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, {
598
613
  ...extractTimestamps(record),
599
614
  ...extractTimelineSnapshot(record),
615
+ parentAgentId: record.parentAgentId ?? null,
600
616
  });
601
617
  this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent resumed from persistence');
602
618
  }
603
619
  else {
604
620
  const config = buildSessionConfig(record);
605
- snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
621
+ snapshot = await this.agentManager.createAgent(config, agentId, {
622
+ labels: record.labels,
623
+ parentAgentId: record.parentAgentId ?? null,
624
+ });
606
625
  this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent created from stored config');
607
626
  }
608
627
  await this.agentManager.hydrateTimelineFromProvider(agentId);
@@ -788,6 +807,112 @@ export class Session {
788
807
  checkout,
789
808
  };
790
809
  }
810
+ shouldRefreshProviderChildThreadsForStreamEvent(event) {
811
+ if (event.provider !== 'codex') {
812
+ return false;
813
+ }
814
+ if (event.type === 'turn_completed') {
815
+ return true;
816
+ }
817
+ return (event.type === 'timeline' &&
818
+ event.item.type === 'tool_call' &&
819
+ event.item.name === 'spawn_agent' &&
820
+ event.item.detail.type === 'sub_agent');
821
+ }
822
+ serializeProviderChildThreadEntries(entries) {
823
+ return JSON.stringify(entries.map(({ thread, project }) => ({
824
+ id: thread.id,
825
+ threadId: thread.threadId,
826
+ parentThreadId: thread.parentThreadId ?? null,
827
+ rootThreadId: thread.rootThreadId ?? null,
828
+ parentAgentId: thread.parentAgentId,
829
+ cwd: thread.cwd,
830
+ title: thread.title,
831
+ status: thread.status,
832
+ model: thread.model ?? null,
833
+ createdAt: thread.createdAt,
834
+ updatedAt: thread.updatedAt,
835
+ persistence: {
836
+ provider: thread.persistence?.provider ?? null,
837
+ sessionId: thread.persistence?.sessionId ?? null,
838
+ nativeHandle: thread.persistence?.nativeHandle ?? null,
839
+ },
840
+ agentId: thread.agentId ?? null,
841
+ agentNickname: thread.agentNickname ?? null,
842
+ agentRole: thread.agentRole ?? null,
843
+ projectKey: project.projectKey,
844
+ projectName: project.projectName,
845
+ checkout: {
846
+ cwd: project.checkout.cwd,
847
+ isGit: project.checkout.isGit,
848
+ currentBranch: project.checkout.currentBranch,
849
+ remoteUrl: project.checkout.remoteUrl,
850
+ isJunctionOwnedWorktree: project.checkout.isJunctionOwnedWorktree,
851
+ mainRepoRoot: project.checkout.mainRepoRoot,
852
+ },
853
+ })));
854
+ }
855
+ emitProviderChildThreadsChanged(payload) {
856
+ this.emit({
857
+ type: 'provider_child_threads_changed',
858
+ payload,
859
+ });
860
+ }
861
+ setProviderChildThreadEntries(entries, options) {
862
+ const fingerprint = this.serializeProviderChildThreadEntries(entries);
863
+ this.providerChildThreadEntriesHydrated = true;
864
+ if (this.providerChildThreadEntriesFingerprint === fingerprint) {
865
+ return;
866
+ }
867
+ this.providerChildThreadEntries = entries;
868
+ this.providerChildThreadEntriesFingerprint = fingerprint;
869
+ if (options?.emit) {
870
+ this.emitProviderChildThreadsChanged({ entries });
871
+ }
872
+ }
873
+ async refreshProviderChildThreadEntries(options) {
874
+ if (this.providerChildThreadRefreshPromise) {
875
+ if (options?.emit) {
876
+ this.providerChildThreadRefreshQueued = true;
877
+ }
878
+ await this.providerChildThreadRefreshPromise;
879
+ return;
880
+ }
881
+ const refreshPromise = (async () => {
882
+ const entries = await this.listProviderChildThreadEntries();
883
+ this.setProviderChildThreadEntries(entries, { emit: options?.emit });
884
+ })();
885
+ this.providerChildThreadRefreshPromise = refreshPromise;
886
+ try {
887
+ await refreshPromise;
888
+ }
889
+ finally {
890
+ if (this.providerChildThreadRefreshPromise === refreshPromise) {
891
+ this.providerChildThreadRefreshPromise = null;
892
+ }
893
+ if (this.providerChildThreadRefreshQueued) {
894
+ this.providerChildThreadRefreshQueued = false;
895
+ await this.refreshProviderChildThreadEntries({ emit: true });
896
+ }
897
+ }
898
+ }
899
+ scheduleProviderChildThreadRefresh() {
900
+ if (this.providerChildThreadRefreshTimer) {
901
+ return;
902
+ }
903
+ this.providerChildThreadRefreshTimer = setTimeout(() => {
904
+ this.providerChildThreadRefreshTimer = null;
905
+ void this.refreshProviderChildThreadEntries({ emit: true }).catch((error) => {
906
+ this.sessionLogger.debug({ err: error }, 'Failed to refresh provider child thread cache');
907
+ });
908
+ }, PROVIDER_CHILD_THREADS_REFRESH_DEBOUNCE_MS);
909
+ }
910
+ async ensureProviderChildThreadEntriesHydrated() {
911
+ if (this.providerChildThreadEntriesHydrated) {
912
+ return;
913
+ }
914
+ await this.refreshProviderChildThreadEntries({ emit: false });
915
+ }
791
916
  async forwardAgentUpdate(agent) {
792
917
  try {
793
918
  const subscription = this.agentUpdatesSubscription;
@@ -820,6 +945,9 @@ export class Session {
820
945
  }
821
946
  async forwardStoredAgentRecordUpdate(record) {
822
947
  try {
948
+ if (record.provider === 'codex') {
949
+ this.scheduleProviderChildThreadRefresh();
950
+ }
823
951
  const subscription = this.agentUpdatesSubscription;
824
952
  if (!subscription || record.internal) {
825
953
  return;
@@ -863,6 +991,12 @@ export class Session {
863
991
  case 'fetch_agent_request':
864
992
  await this.handleFetchAgent(msg.agentId, msg.requestId);
865
993
  break;
994
+ case 'fetch_provider_child_threads_request':
995
+ await this.handleFetchProviderChildThreads(msg);
996
+ break;
997
+ case 'fetch_provider_thread_timeline_request':
998
+ await this.handleFetchProviderThreadTimeline(msg);
999
+ break;
866
1000
  case 'delete_agent_request':
867
1001
  await this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
868
1002
  break;
@@ -1251,6 +1385,9 @@ export class Session {
1251
1385
  }
1252
1386
  async handleDeleteAgentRequest(agentId, requestId) {
1253
1387
  this.sessionLogger.info({ agentId }, `Deleting agent ${agentId} from registry`);
1388
+ const storedRecord = await this.agentStorage.get(agentId);
1389
+ const liveAgent = this.agentManager.getAgent(agentId);
1390
+ const shouldRefreshProviderChildThreads = storedRecord?.provider === 'codex' || liveAgent?.provider === 'codex';
1254
1391
  // Prevent the persistence hook from re-creating the record while we close/delete.
1255
1392
  this.agentStorage.beginDelete(agentId);
1256
1393
  try {
@@ -1278,6 +1415,9 @@ export class Session {
1278
1415
  agentId,
1279
1416
  });
1280
1417
  }
1418
+ if (shouldRefreshProviderChildThreads) {
1419
+ this.scheduleProviderChildThreadRefresh();
1420
+ }
1281
1421
  }
1282
1422
  async handleArchiveAgentRequest(agentId, requestId, dirtyWorktreeBehavior) {
1283
1423
  this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
@@ -1316,6 +1456,9 @@ export class Session {
1316
1456
  archivedWorktree: await this.buildArchivedWorktreeState(archivedRecord.cwd, archivedAt),
1317
1457
  };
1318
1458
  await this.agentStorage.upsert(archivedRecord);
1459
+ if (archivedRecord.provider === 'codex') {
1460
+ this.scheduleProviderChildThreadRefresh();
1461
+ }
1319
1462
  if (liveAgent) {
1320
1463
  this.agentManager.notifyAgentState(agentId);
1321
1464
  }
@@ -1604,7 +1747,7 @@ export class Session {
1604
1747
  * Handle create agent request
1605
1748
  */
1606
1749
  async handleCreateAgentRequest(msg) {
1607
- const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, initialRunOptions, git, bootstrapSetupOverride, generalPreferencesApplied, images, labels, } = msg;
1750
+ const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, initialRunOptions, git, bootstrapSetupOverride, generalPreferencesApplied, images, labels, parentAgentId, } = msg;
1608
1751
  this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ''}`);
1609
1752
  const trimmedPrompt = initialPrompt?.trim() ?? '';
1610
1753
  const hasInitialMessage = trimmedPrompt.length > 0 || Boolean(images?.length);
@@ -1618,10 +1761,36 @@ export class Session {
1618
1761
  const mergedLabels = autoWorkspaceName
1619
1762
  ? { ...labels, 'junction:workspace': autoWorkspaceName }
1620
1763
  : labels;
1621
- const snapshot = await this.agentManager.createAgent(sessionConfig, undefined, {
1622
- labels: applyNotificationRelayOwnerLabel(mergedLabels, this.userId),
1623
- });
1764
+ const resumeHandle = toAgentPersistenceHandle(this.sessionLogger, initialRunOptions?.resumeFrom ?? null);
1765
+ const agentLabels = applyNotificationRelayOwnerLabel(mergedLabels, this.userId);
1766
+ const existingLiveAgent = resumeHandle
1767
+ ? this.findLiveAgentByPersistenceHandle({
1768
+ handle: resumeHandle,
1769
+ parentAgentId: parentAgentId ?? null,
1770
+ })
1771
+ : null;
1772
+ let snapshot = existingLiveAgent
1773
+ ? existingLiveAgent
1774
+ : resumeHandle
1775
+ ? await this.agentManager.resumeAgentFromPersistence(resumeHandle, sessionConfig, undefined, {
1776
+ labels: agentLabels,
1777
+ parentAgentId: parentAgentId ?? null,
1778
+ })
1779
+ : await this.agentManager.createAgent(sessionConfig, undefined, {
1780
+ labels: agentLabels,
1781
+ parentAgentId: parentAgentId ?? null,
1782
+ });
1783
+ if (resumeHandle) {
1784
+ if (existingLiveAgent && sessionConfig.title?.trim()) {
1785
+ await this.agentManager.setTitle(existingLiveAgent.id, sessionConfig.title.trim());
1786
+ }
1787
+ await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
1788
+ snapshot = this.agentManager.getAgent(snapshot.id) ?? snapshot;
1789
+ }
1624
1790
  await this.forwardAgentUpdate(snapshot);
1791
+ if (snapshot.provider === 'codex') {
1792
+ this.scheduleProviderChildThreadRefresh();
1793
+ }
1625
1794
  const runInitialPrompt = async () => {
1626
1795
  if (!hasInitialMessage) {
1627
1796
  return;
@@ -1791,6 +1960,9 @@ export class Session {
1791
1960
  const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
1792
1961
  await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
1793
1962
  await this.forwardAgentUpdate(snapshot);
1963
+ if (snapshot.provider === 'codex') {
1964
+ this.scheduleProviderChildThreadRefresh();
1965
+ }
1794
1966
  const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
1795
1967
  if (requestId) {
1796
1968
  const agentPayload = await this.getAgentPayloadById(snapshot.id);
@@ -1849,10 +2021,14 @@ export class Session {
1849
2021
  snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, {
1850
2022
  ...extractTimestamps(record),
1851
2023
  ...extractTimelineSnapshot(record),
2024
+ parentAgentId: record.parentAgentId ?? null,
1852
2025
  });
1853
2026
  }
1854
2027
  await this.agentManager.hydrateTimelineFromProvider(agentId);
1855
2028
  await this.forwardAgentUpdate(snapshot);
2029
+ if (snapshot.provider === 'codex') {
2030
+ this.scheduleProviderChildThreadRefresh();
2031
+ }
1856
2032
  const timelineSize = this.agentManager.getTimeline(agentId).length;
1857
2033
  if (requestId) {
1858
2034
  this.emit({
@@ -5429,6 +5605,260 @@ export class Session {
5429
5605
  });
5430
5606
  }
5431
5607
  }
5608
+ resolveAgentSessionId(agent) {
5609
+ return agent.runtimeInfo?.sessionId ?? agent.persistence?.sessionId ?? null;
5610
+ }
5611
+ findLiveAgentByPersistenceHandle(input) {
5612
+ const targetSessionId = input.handle.sessionId.trim();
5613
+ if (!targetSessionId) {
5614
+ return null;
5615
+ }
5616
+ for (const agent of this.agentManager.listAgents()) {
5617
+ if (agent.provider !== input.handle.provider) {
5618
+ continue;
5619
+ }
5620
+ if (input.parentAgentId !== undefined
5621
+ && (agent.parentAgentId ?? null) !== (input.parentAgentId ?? null)) {
5622
+ continue;
5623
+ }
5624
+ const agentSessionId = agent.persistence?.sessionId ?? agent.runtimeInfo?.sessionId ?? null;
5625
+ if (agentSessionId === targetSessionId) {
5626
+ return agent;
5627
+ }
5628
+ }
5629
+ return null;
5630
+ }
5631
+ buildProviderChildThreadId(input) {
5632
+ return `${input.provider}:thread:${input.threadId}`;
5633
+ }
5634
+ resolveProviderChildThreadStatus(input) {
5635
+ return input.parentAgent.status === 'running' ? 'running' : 'idle';
5636
+ }
5637
+ buildProviderThreadTimelineEpoch(threadId) {
5638
+ return `provider-thread:${threadId}`;
5639
+ }
5640
+ normalizeTimelineItemForInheritedPrefixComparison(item) {
5641
+ if (item.type !== 'tool_call') {
5642
+ return item;
5643
+ }
5644
+ if (item.name === 'spawn_agent' && item.detail.type === 'sub_agent') {
5645
+ return {
5646
+ type: item.type,
5647
+ name: item.name,
5648
+ detail: {
5649
+ type: item.detail.type,
5650
+ subAgentType: item.detail.subAgentType ?? null,
5651
+ description: item.detail.description ?? null,
5652
+ },
5653
+ };
5654
+ }
5655
+ return item;
5656
+ }
5657
+ serializeTimelineItemFingerprint(item) {
5658
+ return JSON.stringify(this.normalizeTimelineItemForInheritedPrefixComparison(item));
5659
+ }
5660
+ countInheritedTimelinePrefix(parentTimeline, childTimeline) {
5661
+ let index = 0;
5662
+ while (index < parentTimeline.length && index < childTimeline.length) {
5663
+ if (this.serializeTimelineItemFingerprint(parentTimeline[index])
5664
+ !== this.serializeTimelineItemFingerprint(childTimeline[index])) {
5665
+ break;
5666
+ }
5667
+ index += 1;
5668
+ }
5669
+ return index;
5670
+ }
5671
+ buildProviderThreadTimelineRows(input) {
5672
+ const createdAtMs = input.createdAt.getTime();
5673
+ return input.timeline.map((item, index) => ({
5674
+ seq: index,
5675
+ timestamp: new Date(createdAtMs + index).toISOString(),
5676
+ item,
5677
+ }));
5678
+ }
5679
+ async listPersistedProviderChildThreadCandidates() {
5680
+ let limit = PROVIDER_CHILD_THREADS_PERSISTED_LIST_INITIAL_LIMIT;
5681
+ let previousCount = -1;
5682
+ while (true) {
5683
+ const descriptors = await this.agentManager.listPersistedAgents({
5684
+ provider: 'codex',
5685
+ limit,
5686
+ includeTimeline: false,
5687
+ });
5688
+ if (descriptors.length < limit) {
5689
+ return descriptors;
5690
+ }
5691
+ if (descriptors.length === previousCount || limit >= PROVIDER_CHILD_THREADS_PERSISTED_LIST_MAX_LIMIT) {
5692
+ return descriptors;
5693
+ }
5694
+ previousCount = descriptors.length;
5695
+ limit = Math.min(limit * 2, PROVIDER_CHILD_THREADS_PERSISTED_LIST_MAX_LIMIT);
5696
+ }
5697
+ }
5698
+ async listProviderChildThreadEntries() {
5699
+ const agents = await this.listAgentPayloads();
5700
+ const parentAgentBySessionId = new Map();
5701
+ for (const agent of agents) {
5702
+ const sessionId = this.resolveAgentSessionId(agent);
5703
+ if (!sessionId) {
5704
+ continue;
5705
+ }
5706
+ if (!parentAgentBySessionId.has(sessionId)) {
5707
+ parentAgentBySessionId.set(sessionId, agent);
5708
+ }
5709
+ }
5710
+ const placementByCwd = new Map();
5711
+ const getPlacement = (cwd) => {
5712
+ const existing = placementByCwd.get(cwd);
5713
+ if (existing) {
5714
+ return existing;
5715
+ }
5716
+ const next = this.buildProjectPlacement(cwd);
5717
+ placementByCwd.set(cwd, next);
5718
+ return next;
5719
+ };
5720
+ const persistedThreads = await this.listPersistedProviderChildThreadCandidates();
5721
+ const entries = [];
5722
+ const seenKeys = new Set();
5723
+ const existingEntryByThreadId = new Map(this.providerChildThreadEntries.map((entry) => [entry.thread.threadId, entry]));
5724
+ for (const descriptor of persistedThreads) {
5725
+ const threadId = descriptor.sessionId.trim();
5726
+ const parentSessionId = descriptor.parentSessionId?.trim() ?? '';
5727
+ if (!threadId || !parentSessionId) {
5728
+ continue;
5729
+ }
5730
+ const parentAgent = parentAgentBySessionId.get(parentSessionId);
5731
+ if (!parentAgent) {
5732
+ const existingEntry = existingEntryByThreadId.get(threadId);
5733
+ if (existingEntry) {
5734
+ entries.push(existingEntry);
5735
+ seenKeys.add(`codex:${threadId}`);
5736
+ }
5737
+ continue;
5738
+ }
5739
+ const dedupeKey = `codex:${threadId}`;
5740
+ if (seenKeys.has(dedupeKey)) {
5741
+ continue;
5742
+ }
5743
+ seenKeys.add(dedupeKey);
5744
+ entries.push({
5745
+ thread: {
5746
+ id: this.buildProviderChildThreadId({
5747
+ provider: 'codex',
5748
+ threadId,
5749
+ }),
5750
+ provider: 'codex',
5751
+ threadId,
5752
+ parentThreadId: descriptor.parentSessionId ?? null,
5753
+ rootThreadId: descriptor.rootSessionId ?? null,
5754
+ parentAgentId: parentAgent.id,
5755
+ cwd: descriptor.cwd,
5756
+ title: descriptor.title,
5757
+ status: this.resolveProviderChildThreadStatus({ parentAgent }),
5758
+ model: descriptor.model ?? null,
5759
+ createdAt: descriptor.createdAt.toISOString(),
5760
+ updatedAt: descriptor.lastActivityAt.toISOString(),
5761
+ persistence: descriptor.persistence,
5762
+ agentId: descriptor.agentId ?? null,
5763
+ agentNickname: descriptor.agentNickname ?? null,
5764
+ agentRole: descriptor.agentRole ?? null,
5765
+ },
5766
+ project: await getPlacement(descriptor.cwd),
5767
+ });
5768
+ }
5769
+ entries.sort((left, right) => new Date(right.thread.updatedAt).getTime() - new Date(left.thread.updatedAt).getTime());
5770
+ return entries;
5771
+ }
5772
+ async handleFetchProviderThreadTimeline(request) {
5773
+ const detailMode = request.detailMode ?? 'full';
5774
+ try {
5775
+ await this.ensureProviderChildThreadEntriesHydrated();
5776
+ const isKnownChildThread = this.providerChildThreadEntries.some((entry) => entry.thread.provider === 'codex' && entry.thread.threadId === request.threadId);
5777
+ if (!isKnownChildThread) {
5778
+ throw new SessionRequestError('provider_thread_not_found', `Provider thread not found: ${request.threadId}`);
5779
+ }
5780
+ const descriptor = await this.agentManager.readPersistedAgent('codex', {
5781
+ sessionId: request.threadId,
5782
+ includeTimeline: true,
5783
+ });
5784
+ if (!descriptor) {
5785
+ throw new SessionRequestError('provider_thread_not_found', `Provider thread not found: ${request.threadId}`);
5786
+ }
5787
+ let inheritedPrefixCount = 0;
5788
+ if (descriptor.parentSessionId) {
5789
+ const parentDescriptor = await this.agentManager.readPersistedAgent('codex', {
5790
+ sessionId: descriptor.parentSessionId,
5791
+ includeTimeline: true,
5792
+ });
5793
+ if (parentDescriptor) {
5794
+ inheritedPrefixCount = this.countInheritedTimelinePrefix(parentDescriptor.timeline, descriptor.timeline);
5795
+ }
5796
+ }
5797
+ const timelineRows = this.buildProviderThreadTimelineRows({
5798
+ timeline: descriptor.timeline.slice(inheritedPrefixCount),
5799
+ createdAt: descriptor.createdAt,
5800
+ });
5801
+ const entries = projectTimelineRows(timelineRows, 'codex', 'projected').map((entry) => ({
5802
+ ...entry,
5803
+ item: sanitizeTimelineItemForTransport(entry.item, detailMode),
5804
+ }));
5805
+ this.emit({
5806
+ type: 'fetch_provider_thread_timeline_response',
5807
+ payload: {
5808
+ requestId: request.requestId,
5809
+ provider: 'codex',
5810
+ threadId: descriptor.sessionId,
5811
+ epoch: this.buildProviderThreadTimelineEpoch(descriptor.sessionId),
5812
+ inheritedPrefixCount,
5813
+ entries,
5814
+ error: null,
5815
+ },
5816
+ });
5817
+ }
5818
+ catch (error) {
5819
+ this.sessionLogger.error({ err: error, threadId: request.threadId }, 'Failed to handle fetch_provider_thread_timeline_request');
5820
+ this.emit({
5821
+ type: 'fetch_provider_thread_timeline_response',
5822
+ payload: {
5823
+ requestId: request.requestId,
5824
+ provider: request.provider,
5825
+ threadId: request.threadId,
5826
+ epoch: this.buildProviderThreadTimelineEpoch(request.threadId),
5827
+ inheritedPrefixCount: 0,
5828
+ entries: [],
5829
+ error: error instanceof Error ? error.message : 'Failed to fetch provider thread timeline',
5830
+ },
5831
+ });
5832
+ }
5833
+ }
5834
+ async handleFetchProviderChildThreads(request) {
5835
+ try {
5836
+ await this.ensureProviderChildThreadEntriesHydrated();
5837
+ this.emit({
5838
+ type: 'fetch_provider_child_threads_response',
5839
+ payload: {
5840
+ requestId: request.requestId,
5841
+ entries: this.providerChildThreadEntries,
5842
+ },
5843
+ });
5844
+ }
5845
+ catch (error) {
5846
+ const code = error instanceof SessionRequestError
5847
+ ? error.code
5848
+ : 'fetch_provider_child_threads_failed';
5849
+ const message = error instanceof Error ? error.message : 'Failed to fetch provider child threads';
5850
+ this.sessionLogger.error({ err: error }, 'Failed to handle fetch_provider_child_threads_request');
5851
+ this.emit({
5852
+ type: 'rpc_error',
5853
+ payload: {
5854
+ requestId: request.requestId,
5855
+ requestType: request.type,
5856
+ error: message,
5857
+ code,
5858
+ },
5859
+ });
5860
+ }
5861
+ }
5432
5862
  async handleFetchAgent(agentIdOrIdentifier, requestId) {
5433
5863
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
5434
5864
  if (!resolved.ok) {
@@ -5823,6 +6253,10 @@ export class Session {
5823
6253
  this.unsubscribeAgentEvents();
5824
6254
  this.unsubscribeAgentEvents = null;
5825
6255
  }
6256
+ if (this.providerChildThreadRefreshTimer) {
6257
+ clearTimeout(this.providerChildThreadRefreshTimer);
6258
+ this.providerChildThreadRefreshTimer = null;
6259
+ }
5826
6260
  // Abort any ongoing operations
5827
6261
  this.abortController.abort();
5828
6262
  // Close MCP clients