@junctionpanel/server 0.1.61 → 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 +28 -1
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +236 -3
  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 +593 -80
  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 +8398 -2298
  54. package/dist/server/shared/messages.d.ts.map +1 -1
  55. package/dist/server/shared/messages.js +136 -0
  56. package/dist/server/shared/messages.js.map +1 -1
  57. package/package.json +3 -3
@@ -29,7 +29,7 @@ import { expandTilde } from '../utils/path.js';
29
29
  import { searchHomeDirectories, searchWorkspaceEntries, searchWorkspaceEntriesAtGitRef, searchGitRepositories, checkIsGitRepo, } from '../utils/directory-suggestions.js';
30
30
  import { cloneRepository } from '../utils/git-clone.js';
31
31
  import { initRepository } from '../utils/git-init.js';
32
- import { resolveClientMessageId } from './client-message-id.js';
32
+ import { normalizeClientMessageId, resolveClientMessageId } from './client-message-id.js';
33
33
  import { deriveProjectGroupingKey, deriveProjectGroupingName } from '../shared/project-grouping.js';
34
34
  import { DEFAULT_DAEMON_PACKAGE_NAME, resolveDaemonPackageVersion, } from './daemon-package-context.js';
35
35
  import { runDaemonDoctor } from './daemon-doctor.js';
@@ -48,6 +48,8 @@ const READ_ONLY_GIT_ENV = {
48
48
  const DEFAULT_STORED_TIMELINE_FETCH_LIMIT = 200;
49
49
  const pendingAgentInitializations = new Map();
50
50
  const pendingAgentMessageExecutions = new Map();
51
+ const CREATE_AGENT_REPLAY_WINDOW_MS = 30000;
52
+ const pendingCreateAgentRequests = new Map();
51
53
  const sharedWorkspaceGitOperationStates = new Map();
52
54
  const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
53
55
  const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
@@ -56,9 +58,36 @@ const WORKSPACE_STATUS_WATCH_DEBOUNCE_MS = 250;
56
58
  const WORKSPACE_STATUS_GIT_REFRESH_MS = 3000;
57
59
  const WORKSPACE_STATUS_PR_ACTIVE_REFRESH_MS = 15000;
58
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;
59
64
  const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
60
65
  const TERMINAL_STREAM_MAX_PENDING_BYTES = 2 * 1024 * 1024;
61
66
  const TERMINAL_STREAM_MAX_PENDING_CHUNKS = 2048;
67
+ function buildCreateAgentReplayKey(clientId, clientMessageId) {
68
+ return JSON.stringify([clientId, clientMessageId]);
69
+ }
70
+ function deletePendingCreateAgentRequest(replayKey, expectedEntry) {
71
+ const existingEntry = pendingCreateAgentRequests.get(replayKey);
72
+ if (!existingEntry || (expectedEntry && existingEntry !== expectedEntry)) {
73
+ return;
74
+ }
75
+ clearTimeout(existingEntry.evictionTimer);
76
+ pendingCreateAgentRequests.delete(replayKey);
77
+ }
78
+ function storePendingCreateAgentRequest(replayKey, promise) {
79
+ const entry = {
80
+ promise,
81
+ evictionTimer: setTimeout(() => {
82
+ deletePendingCreateAgentRequest(replayKey, entry);
83
+ }, CREATE_AGENT_REPLAY_WINDOW_MS),
84
+ };
85
+ pendingCreateAgentRequests.set(replayKey, entry);
86
+ void promise.catch(() => {
87
+ deletePendingCreateAgentRequest(replayKey, entry);
88
+ });
89
+ return entry;
90
+ }
62
91
  const DIRTY_WORKTREE_CONFIRMATION_REQUIRED = 'dirty_worktree_confirmation_required';
63
92
  class SessionRequestError extends Error {
64
93
  constructor(code, message) {
@@ -142,6 +171,12 @@ export class Session {
142
171
  this.checkoutDiffSubscriptions = new Map();
143
172
  this.checkoutDiffTargets = new Map();
144
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;
145
180
  const { clientId, userId, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, junctionHome, agentManager, agentStorage, createAgentMcpTransport, terminalManager, agentProviderRuntimeSettings, connectionContext, } = options;
146
181
  this.clientId = clientId;
147
182
  this.userId = userId;
@@ -367,8 +402,14 @@ export class Session {
367
402
  this.unsubscribeAgentEvents = this.agentManager.subscribe((event) => {
368
403
  if (event.type === 'agent_state') {
369
404
  void this.forwardAgentUpdate(event.agent);
405
+ if (event.agent.provider === 'codex') {
406
+ this.scheduleProviderChildThreadRefresh();
407
+ }
370
408
  return;
371
409
  }
410
+ if (this.shouldRefreshProviderChildThreadsForStreamEvent(event.event)) {
411
+ this.scheduleProviderChildThreadRefresh();
412
+ }
372
413
  // Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
373
414
  // for the focused agent, with a short grace window while backgrounded.
374
415
  // History catch-up is handled via pull-based `fetch_agent_timeline_request`.
@@ -571,12 +612,16 @@ export class Session {
571
612
  snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, {
572
613
  ...extractTimestamps(record),
573
614
  ...extractTimelineSnapshot(record),
615
+ parentAgentId: record.parentAgentId ?? null,
574
616
  });
575
617
  this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent resumed from persistence');
576
618
  }
577
619
  else {
578
620
  const config = buildSessionConfig(record);
579
- 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
+ });
580
625
  this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent created from stored config');
581
626
  }
582
627
  await this.agentManager.hydrateTimelineFromProvider(agentId);
@@ -762,6 +807,112 @@ export class Session {
762
807
  checkout,
763
808
  };
764
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
+ }
765
916
  async forwardAgentUpdate(agent) {
766
917
  try {
767
918
  const subscription = this.agentUpdatesSubscription;
@@ -794,6 +945,9 @@ export class Session {
794
945
  }
795
946
  async forwardStoredAgentRecordUpdate(record) {
796
947
  try {
948
+ if (record.provider === 'codex') {
949
+ this.scheduleProviderChildThreadRefresh();
950
+ }
797
951
  const subscription = this.agentUpdatesSubscription;
798
952
  if (!subscription || record.internal) {
799
953
  return;
@@ -837,6 +991,12 @@ export class Session {
837
991
  case 'fetch_agent_request':
838
992
  await this.handleFetchAgent(msg.agentId, msg.requestId);
839
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;
840
1000
  case 'delete_agent_request':
841
1001
  await this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
842
1002
  break;
@@ -1225,6 +1385,9 @@ export class Session {
1225
1385
  }
1226
1386
  async handleDeleteAgentRequest(agentId, requestId) {
1227
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';
1228
1391
  // Prevent the persistence hook from re-creating the record while we close/delete.
1229
1392
  this.agentStorage.beginDelete(agentId);
1230
1393
  try {
@@ -1252,6 +1415,9 @@ export class Session {
1252
1415
  agentId,
1253
1416
  });
1254
1417
  }
1418
+ if (shouldRefreshProviderChildThreads) {
1419
+ this.scheduleProviderChildThreadRefresh();
1420
+ }
1255
1421
  }
1256
1422
  async handleArchiveAgentRequest(agentId, requestId, dirtyWorktreeBehavior) {
1257
1423
  this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
@@ -1290,6 +1456,9 @@ export class Session {
1290
1456
  archivedWorktree: await this.buildArchivedWorktreeState(archivedRecord.cwd, archivedAt),
1291
1457
  };
1292
1458
  await this.agentStorage.upsert(archivedRecord);
1459
+ if (archivedRecord.provider === 'codex') {
1460
+ this.scheduleProviderChildThreadRefresh();
1461
+ }
1293
1462
  if (liveAgent) {
1294
1463
  this.agentManager.notifyAgentState(agentId);
1295
1464
  }
@@ -1557,7 +1726,7 @@ export class Session {
1557
1726
  const archivedAt = await this.getArchivedAt(agentId);
1558
1727
  if (archivedAt) {
1559
1728
  this.handleAgentRunError(agentId, new Error(`Agent ${agentId} is archived`), 'Refusing to send prompt to archived agent');
1560
- return;
1729
+ return false;
1561
1730
  }
1562
1731
  try {
1563
1732
  await this.performAgentMessageSend({
@@ -1567,107 +1736,186 @@ export class Session {
1567
1736
  images,
1568
1737
  runOptions,
1569
1738
  });
1739
+ return true;
1570
1740
  }
1571
1741
  catch (error) {
1572
1742
  this.handleAgentRunError(agentId, error, 'Failed to send prompt to agent');
1743
+ return false;
1573
1744
  }
1574
1745
  }
1575
1746
  /**
1576
1747
  * Handle create agent request
1577
1748
  */
1578
1749
  async handleCreateAgentRequest(msg) {
1579
- 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;
1580
1751
  this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ''}`);
1752
+ const trimmedPrompt = initialPrompt?.trim() ?? '';
1753
+ const hasInitialMessage = trimmedPrompt.length > 0 || Boolean(images?.length);
1754
+ const normalizedClientMessageId = hasInitialMessage ? normalizeClientMessageId(clientMessageId) : undefined;
1755
+ const createReplayKey = normalizedClientMessageId
1756
+ ? buildCreateAgentReplayKey(this.clientId, normalizedClientMessageId)
1757
+ : null;
1581
1758
  try {
1582
- const { sessionConfig, worktreeConfig, autoWorkspaceName } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
1583
- const mergedLabels = autoWorkspaceName
1584
- ? { ...labels, 'junction:workspace': autoWorkspaceName }
1585
- : labels;
1586
- const snapshot = await this.agentManager.createAgent(sessionConfig, undefined, {
1587
- labels: applyNotificationRelayOwnerLabel(mergedLabels, this.userId),
1588
- });
1589
- await this.forwardAgentUpdate(snapshot);
1590
- if (requestId) {
1591
- const agentPayload = await this.getAgentPayloadById(snapshot.id);
1592
- if (!agentPayload) {
1593
- throw new Error(`Agent ${snapshot.id} not found after creation`);
1759
+ const createAgentLaunch = async (clearReplayEntryOnDeferredFailure) => {
1760
+ const { sessionConfig, worktreeConfig, autoWorkspaceName } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
1761
+ const mergedLabels = autoWorkspaceName
1762
+ ? { ...labels, 'junction:workspace': autoWorkspaceName }
1763
+ : labels;
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;
1594
1789
  }
1595
- this.emit({
1596
- type: 'status',
1597
- payload: {
1598
- status: 'agent_created',
1599
- agentId: snapshot.id,
1600
- requestId,
1601
- agent: agentPayload,
1602
- },
1603
- });
1604
- }
1605
- const trimmedPrompt = initialPrompt?.trim() ?? '';
1606
- const hasInitialMessage = trimmedPrompt.length > 0 || Boolean(images?.length);
1607
- const runInitialPrompt = async () => {
1608
- if (!hasInitialMessage) {
1609
- return;
1790
+ await this.forwardAgentUpdate(snapshot);
1791
+ if (snapshot.provider === 'codex') {
1792
+ this.scheduleProviderChildThreadRefresh();
1793
+ }
1794
+ const runInitialPrompt = async () => {
1795
+ if (!hasInitialMessage) {
1796
+ return;
1797
+ }
1798
+ if (trimmedPrompt.length > 0) {
1799
+ scheduleAgentMetadataGeneration({
1800
+ agentManager: this.agentManager,
1801
+ agentId: snapshot.id,
1802
+ cwd: snapshot.cwd,
1803
+ initialPrompt: trimmedPrompt,
1804
+ explicitTitle: snapshot.config.title,
1805
+ junctionHome: this.junctionHome,
1806
+ logger: this.sessionLogger,
1807
+ });
1808
+ }
1809
+ const sendSucceeded = await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, normalizedClientMessageId ?? resolveClientMessageId(clientMessageId), images, normalizeAgentRunOptions({
1810
+ ...(outputSchema ? { outputSchema } : {}),
1811
+ ...(initialRunOptions ?? {}),
1812
+ }));
1813
+ if (!sendSucceeded) {
1814
+ clearReplayEntryOnDeferredFailure?.();
1815
+ }
1816
+ };
1817
+ const handleInitialPromptError = (promptError) => {
1818
+ clearReplayEntryOnDeferredFailure?.();
1819
+ this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
1820
+ this.emit({
1821
+ type: 'activity_log',
1822
+ payload: {
1823
+ id: uuidv4(),
1824
+ timestamp: new Date(),
1825
+ type: 'error',
1826
+ content: `Initial prompt failed: ${promptError?.message ?? promptError}`,
1827
+ },
1828
+ });
1829
+ };
1830
+ if (hasInitialMessage && !worktreeConfig) {
1831
+ void runInitialPrompt().catch(handleInitialPromptError);
1610
1832
  }
1611
- if (trimmedPrompt.length > 0) {
1612
- scheduleAgentMetadataGeneration({
1613
- agentManager: this.agentManager,
1833
+ if (worktreeConfig) {
1834
+ void runAsyncWorktreeBootstrap({
1614
1835
  agentId: snapshot.id,
1615
- cwd: snapshot.cwd,
1616
- initialPrompt: trimmedPrompt,
1617
- explicitTitle: snapshot.config.title,
1618
- junctionHome: this.junctionHome,
1836
+ worktree: worktreeConfig,
1837
+ setupOverride: bootstrapSetupOverride,
1838
+ generalPreferencesApplied,
1839
+ terminalManager: this.terminalManager,
1840
+ appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1841
+ agentManager: this.agentManager,
1842
+ agentId: snapshot.id,
1843
+ item,
1844
+ }),
1845
+ emitLiveTimelineItem: (item) => emitLiveTimelineItemIfAgentKnown({
1846
+ agentManager: this.agentManager,
1847
+ agentId: snapshot.id,
1848
+ item,
1849
+ }),
1850
+ onSetupSettled: async (result) => {
1851
+ if (!hasInitialMessage || result.setupStatus === 'failed') {
1852
+ if (hasInitialMessage && result.setupStatus === 'failed') {
1853
+ clearReplayEntryOnDeferredFailure?.();
1854
+ }
1855
+ return;
1856
+ }
1857
+ await runInitialPrompt().catch(handleInitialPromptError);
1858
+ },
1619
1859
  logger: this.sessionLogger,
1860
+ }).catch((bootstrapError) => {
1861
+ clearReplayEntryOnDeferredFailure?.();
1862
+ this.sessionLogger.error({ err: bootstrapError, agentId: snapshot.id }, `Failed to bootstrap worktree for agent ${snapshot.id}`);
1620
1863
  });
1621
1864
  }
1622
- await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, resolveClientMessageId(clientMessageId), images, normalizeAgentRunOptions({
1623
- ...(outputSchema ? { outputSchema } : {}),
1624
- ...(initialRunOptions ?? {}),
1625
- }));
1865
+ this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1866
+ return snapshot;
1626
1867
  };
1627
- const handleInitialPromptError = (promptError) => {
1628
- this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
1868
+ let reusedExisting = false;
1869
+ let snapshot;
1870
+ let agentPayload;
1871
+ if (createReplayKey) {
1872
+ const existingEntry = pendingCreateAgentRequests.get(createReplayKey);
1873
+ if (existingEntry) {
1874
+ reusedExisting = true;
1875
+ snapshot = await existingEntry.promise;
1876
+ }
1877
+ else {
1878
+ reusedExisting = false;
1879
+ let clearReplayEntryOnDeferredFailure = () => { };
1880
+ const nextPromise = createAgentLaunch(() => {
1881
+ clearReplayEntryOnDeferredFailure();
1882
+ });
1883
+ const nextEntry = storePendingCreateAgentRequest(createReplayKey, nextPromise);
1884
+ clearReplayEntryOnDeferredFailure = () => {
1885
+ deletePendingCreateAgentRequest(createReplayKey, nextEntry);
1886
+ };
1887
+ try {
1888
+ snapshot = await nextPromise;
1889
+ }
1890
+ catch (error) {
1891
+ deletePendingCreateAgentRequest(createReplayKey, nextEntry);
1892
+ throw error;
1893
+ }
1894
+ }
1895
+ agentPayload = await this.getAgentPayloadById(snapshot.id);
1896
+ if (!agentPayload) {
1897
+ agentPayload = await this.buildAgentPayload(snapshot);
1898
+ }
1899
+ }
1900
+ else {
1901
+ snapshot = await createAgentLaunch();
1902
+ agentPayload = await this.getAgentPayloadById(snapshot.id);
1903
+ if (!agentPayload) {
1904
+ throw new Error(`Agent ${snapshot.id} not found after creation`);
1905
+ }
1906
+ }
1907
+ if (requestId) {
1629
1908
  this.emit({
1630
- type: 'activity_log',
1909
+ type: 'status',
1631
1910
  payload: {
1632
- id: uuidv4(),
1633
- timestamp: new Date(),
1634
- type: 'error',
1635
- content: `Initial prompt failed: ${promptError?.message ?? promptError}`,
1636
- },
1637
- });
1638
- };
1639
- if (hasInitialMessage && !worktreeConfig) {
1640
- void runInitialPrompt().catch(handleInitialPromptError);
1641
- }
1642
- if (worktreeConfig) {
1643
- void runAsyncWorktreeBootstrap({
1644
- agentId: snapshot.id,
1645
- worktree: worktreeConfig,
1646
- setupOverride: bootstrapSetupOverride,
1647
- generalPreferencesApplied,
1648
- terminalManager: this.terminalManager,
1649
- appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1650
- agentManager: this.agentManager,
1651
- agentId: snapshot.id,
1652
- item,
1653
- }),
1654
- emitLiveTimelineItem: (item) => emitLiveTimelineItemIfAgentKnown({
1655
- agentManager: this.agentManager,
1911
+ status: 'agent_created',
1912
+ reusedExisting,
1656
1913
  agentId: snapshot.id,
1657
- item,
1658
- }),
1659
- onSetupSettled: async (result) => {
1660
- if (!hasInitialMessage || result.setupStatus === 'failed') {
1661
- return;
1662
- }
1663
- await runInitialPrompt().catch(handleInitialPromptError);
1914
+ requestId,
1915
+ agent: agentPayload,
1664
1916
  },
1665
- logger: this.sessionLogger,
1666
- }).catch((bootstrapError) => {
1667
- this.sessionLogger.error({ err: bootstrapError, agentId: snapshot.id }, `Failed to bootstrap worktree for agent ${snapshot.id}`);
1668
1917
  });
1669
1918
  }
1670
- this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1671
1919
  }
1672
1920
  catch (error) {
1673
1921
  this.sessionLogger.error({ err: error }, 'Failed to create agent');
@@ -1712,6 +1960,9 @@ export class Session {
1712
1960
  const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
1713
1961
  await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
1714
1962
  await this.forwardAgentUpdate(snapshot);
1963
+ if (snapshot.provider === 'codex') {
1964
+ this.scheduleProviderChildThreadRefresh();
1965
+ }
1715
1966
  const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
1716
1967
  if (requestId) {
1717
1968
  const agentPayload = await this.getAgentPayloadById(snapshot.id);
@@ -1770,10 +2021,14 @@ export class Session {
1770
2021
  snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, {
1771
2022
  ...extractTimestamps(record),
1772
2023
  ...extractTimelineSnapshot(record),
2024
+ parentAgentId: record.parentAgentId ?? null,
1773
2025
  });
1774
2026
  }
1775
2027
  await this.agentManager.hydrateTimelineFromProvider(agentId);
1776
2028
  await this.forwardAgentUpdate(snapshot);
2029
+ if (snapshot.provider === 'codex') {
2030
+ this.scheduleProviderChildThreadRefresh();
2031
+ }
1777
2032
  const timelineSize = this.agentManager.getTimeline(agentId).length;
1778
2033
  if (requestId) {
1779
2034
  this.emit({
@@ -5350,6 +5605,260 @@ export class Session {
5350
5605
  });
5351
5606
  }
5352
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
+ }
5353
5862
  async handleFetchAgent(agentIdOrIdentifier, requestId) {
5354
5863
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
5355
5864
  if (!resolved.ok) {
@@ -5744,6 +6253,10 @@ export class Session {
5744
6253
  this.unsubscribeAgentEvents();
5745
6254
  this.unsubscribeAgentEvents = null;
5746
6255
  }
6256
+ if (this.providerChildThreadRefreshTimer) {
6257
+ clearTimeout(this.providerChildThreadRefreshTimer);
6258
+ this.providerChildThreadRefreshTimer = null;
6259
+ }
5747
6260
  // Abort any ongoing operations
5748
6261
  this.abortController.abort();
5749
6262
  // Close MCP clients