@adhdev/daemon-core 0.9.82-rc.185 → 0.9.82-rc.187

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.
@@ -608,6 +608,166 @@ function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: strin
608
608
  return false;
609
609
  }
610
610
 
611
+ function findDirectDispatchLedgerEntry(args: {
612
+ meshId: string;
613
+ taskId: string;
614
+ sessionId?: string;
615
+ }): { id: string; timestamp: string; nodeId?: string; sessionId?: string; providerType?: string; payload: Record<string, unknown> } | null {
616
+ const entries = readLedgerEntries(args.meshId, { tail: 500 });
617
+ for (let i = entries.length - 1; i >= 0; i--) {
618
+ const entry = entries[i];
619
+ if (entry.kind !== 'task_dispatched') continue;
620
+ const payloadTaskId = readNonEmptyString(entry.payload?.taskId);
621
+ if (payloadTaskId !== args.taskId) continue;
622
+ if (args.sessionId && entry.sessionId && entry.sessionId !== args.sessionId) continue;
623
+ return {
624
+ id: entry.id,
625
+ timestamp: entry.timestamp,
626
+ nodeId: entry.nodeId,
627
+ sessionId: entry.sessionId,
628
+ providerType: entry.providerType,
629
+ payload: entry.payload || {},
630
+ };
631
+ }
632
+ return null;
633
+ }
634
+
635
+ function hasTerminalLedgerAfterDispatch(args: {
636
+ meshId: string;
637
+ taskId: string;
638
+ sessionId?: string;
639
+ dispatchEntryId?: string;
640
+ dispatchTimestamp?: string;
641
+ }): boolean {
642
+ const entries = readLedgerEntries(args.meshId, { tail: 500 });
643
+ let afterDispatch = !args.dispatchEntryId && !args.dispatchTimestamp;
644
+ const dispatchTime = args.dispatchTimestamp ? new Date(args.dispatchTimestamp).getTime() : Number.NaN;
645
+ for (const entry of entries) {
646
+ if (!afterDispatch) {
647
+ if (args.dispatchEntryId && entry.id === args.dispatchEntryId) {
648
+ afterDispatch = true;
649
+ continue;
650
+ }
651
+ if (!args.dispatchEntryId && Number.isFinite(dispatchTime)) {
652
+ const entryTime = new Date(entry.timestamp).getTime();
653
+ if (Number.isFinite(entryTime) && entryTime >= dispatchTime) afterDispatch = true;
654
+ }
655
+ if (!afterDispatch) continue;
656
+ }
657
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
658
+ const terminalTaskId = readNonEmptyString(entry.payload?.taskId);
659
+ if (terminalTaskId && terminalTaskId === args.taskId) return true;
660
+ if (terminalTaskId && terminalTaskId !== args.taskId) continue;
661
+ if (args.sessionId && entry.sessionId === args.sessionId) return true;
662
+ }
663
+ return false;
664
+ }
665
+
666
+ export function reconcileDirectDispatchCompletionFromTranscript(args: {
667
+ meshId: string;
668
+ nodeId?: string;
669
+ sessionId: string;
670
+ providerType?: string;
671
+ providerSessionId?: string;
672
+ taskId: string;
673
+ finalSummary: string;
674
+ transcriptMessageAt?: string;
675
+ completedAt?: string;
676
+ targetCoordinatorDaemonId?: string;
677
+ source?: string;
678
+ }): { reconciled: boolean; kind?: MeshLedgerKind; alreadyTerminal?: boolean; workerResult?: unknown; ledgerEntryId?: string; reason?: string } {
679
+ const finalSummary = readNonEmptyString(args.finalSummary);
680
+ if (!args.meshId || !args.taskId || !args.sessionId || !finalSummary) {
681
+ return { reconciled: false, reason: 'missing_required_completion_evidence' };
682
+ }
683
+
684
+ const dispatch = findDirectDispatchLedgerEntry({
685
+ meshId: args.meshId,
686
+ taskId: args.taskId,
687
+ sessionId: args.sessionId,
688
+ });
689
+ if (hasTerminalLedgerAfterDispatch({
690
+ meshId: args.meshId,
691
+ taskId: args.taskId,
692
+ sessionId: args.sessionId,
693
+ dispatchEntryId: dispatch?.id,
694
+ dispatchTimestamp: dispatch?.timestamp,
695
+ })) {
696
+ return { reconciled: false, alreadyTerminal: true, reason: 'terminal_ledger_entry_exists' };
697
+ }
698
+
699
+ const nodeId = readNonEmptyString(args.nodeId) || dispatch?.nodeId;
700
+ const providerType = readNonEmptyString(args.providerType) || dispatch?.providerType || readNonEmptyString(dispatch?.payload.providerType);
701
+ const completedAt = args.completedAt || new Date().toISOString();
702
+ const evidence = buildTaskCompletionEvidence({
703
+ event: 'agent:generating_completed',
704
+ nodeId: nodeId || 'unknown',
705
+ sessionId: args.sessionId,
706
+ providerType,
707
+ providerSessionId: readNonEmptyString(args.providerSessionId),
708
+ finalSummary,
709
+ completedAt,
710
+ });
711
+ const workerResult = evidence.workerResult;
712
+ const dispatchTime = dispatch?.timestamp ? new Date(dispatch.timestamp).getTime() : Number.NaN;
713
+ const transcriptTime = args.transcriptMessageAt ? new Date(args.transcriptMessageAt).getTime() : Number.NaN;
714
+ const transcriptAfterDispatch = Number.isFinite(dispatchTime) && Number.isFinite(transcriptTime) && transcriptTime >= dispatchTime;
715
+ if (workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
716
+ return { reconciled: false, reason: 'transcript_not_proven_after_dispatch' };
717
+ }
718
+ const workerFailed = workerResult.status === 'failed' || (workerResult.status !== 'completed' && workerResult.errors.length > 0);
719
+ const kind: MeshLedgerKind = workerFailed ? 'task_failed' : 'task_completed';
720
+
721
+ const entry = appendLedgerEntry(args.meshId, {
722
+ kind,
723
+ nodeId: nodeId || undefined,
724
+ sessionId: args.sessionId,
725
+ providerType: providerType || undefined,
726
+ payload: {
727
+ event: 'agent:generating_completed',
728
+ source: args.source || 'direct_task_transcript_reconciliation',
729
+ taskId: args.taskId,
730
+ providerSessionId: readNonEmptyString(args.providerSessionId),
731
+ finalSummary,
732
+ workerResult,
733
+ completionDiagnostic: {
734
+ reason: 'direct_task_transcript_reconciliation',
735
+ dispatchEntryId: dispatch?.id,
736
+ dispatchTimestamp: dispatch?.timestamp,
737
+ transcriptMessageAt: readNonEmptyString(args.transcriptMessageAt),
738
+ transcriptFinalAssistantPresent: true,
739
+ },
740
+ evidence,
741
+ },
742
+ });
743
+ updateDirectDispatchStatus(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
744
+ setImmediate(() => cleanupTerminalDirectDispatches());
745
+ queuePendingMeshCoordinatorEvent({
746
+ event: kind === 'task_completed' ? 'agent:generating_completed' : 'agent:stopped',
747
+ meshId: args.meshId,
748
+ nodeLabel: nodeId ? `Node '${nodeId}'` : 'Remote agent',
749
+ nodeId: nodeId || undefined,
750
+ metadataEvent: {
751
+ targetSessionId: args.sessionId,
752
+ providerType: providerType || undefined,
753
+ providerSessionId: readNonEmptyString(args.providerSessionId),
754
+ finalSummary,
755
+ taskId: args.taskId,
756
+ workerResult,
757
+ completionDiagnostic: {
758
+ reason: 'direct_task_transcript_reconciliation',
759
+ terminalLedgerKind: kind,
760
+ terminalLedgerId: entry.id,
761
+ },
762
+ },
763
+ coordinatorMessage: undefined,
764
+ queuedAt: Date.now(),
765
+ ...(readNonEmptyString(args.targetCoordinatorDaemonId) ? { targetCoordinatorDaemonId: readNonEmptyString(args.targetCoordinatorDaemonId) } : {}),
766
+ });
767
+
768
+ return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
769
+ }
770
+
611
771
  function buildLongGeneratingCompletionReconciliation(args: {
612
772
  meshId: string;
613
773
  nodeId?: string;
@@ -1019,13 +1179,69 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
1019
1179
  return false;
1020
1180
  }
1021
1181
 
1182
+ export interface MeshQueueTriggerResult {
1183
+ success: true;
1184
+ meshId: string;
1185
+ pendingBefore: number;
1186
+ assignedBefore: number;
1187
+ pendingAfter: number;
1188
+ assignedAfter: number;
1189
+ claimed: boolean;
1190
+ newlyAssignedTasks: Array<{
1191
+ id: string;
1192
+ nodeId?: string;
1193
+ sessionId?: string;
1194
+ }>;
1195
+ localIdleSessionsChecked: number;
1196
+ remoteIdleSessionsChecked: number;
1197
+ skippedSessions: Array<{
1198
+ nodeId?: string;
1199
+ sessionId?: string;
1200
+ reason: string;
1201
+ status?: string;
1202
+ }>;
1203
+ autoLaunchStarted: boolean;
1204
+ noIdleMeshSessionAvailable?: boolean;
1205
+ }
1206
+
1207
+ function countQueueStatus(meshId: string, status: 'pending' | 'assigned'): number {
1208
+ return getQueue(meshId, { status: [status] as any }).length;
1209
+ }
1210
+
1211
+ function getQueueStatusById(meshId: string): Map<string, string> {
1212
+ return new Map(getQueue(meshId).map(task => [task.id, task.status]));
1213
+ }
1214
+
1022
1215
  /**
1023
1216
  * Triggers a queue check for all nodes in the mesh.
1024
1217
  * Called when a new task is enqueued, in case nodes are already idle.
1025
1218
  */
1026
- export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<void> {
1219
+ export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<MeshQueueTriggerResult> {
1027
1220
  const mesh = getMeshWithCache(components, meshId);
1028
- if (!mesh) return;
1221
+ const pendingBefore = countQueueStatus(meshId, 'pending');
1222
+ const assignedBefore = countQueueStatus(meshId, 'assigned');
1223
+ const beforeStatus = getQueueStatusById(meshId);
1224
+ const skippedSessions: MeshQueueTriggerResult['skippedSessions'] = [];
1225
+ let localIdleSessionsChecked = 0;
1226
+ let remoteIdleSessionsChecked = 0;
1227
+ let autoLaunchStarted = false;
1228
+ if (!mesh) {
1229
+ return {
1230
+ success: true,
1231
+ meshId,
1232
+ pendingBefore,
1233
+ assignedBefore,
1234
+ pendingAfter: pendingBefore,
1235
+ assignedAfter: assignedBefore,
1236
+ claimed: false,
1237
+ newlyAssignedTasks: [],
1238
+ localIdleSessionsChecked,
1239
+ remoteIdleSessionsChecked,
1240
+ skippedSessions: [{ reason: 'mesh_not_found' }],
1241
+ autoLaunchStarted,
1242
+ noIdleMeshSessionAvailable: true,
1243
+ };
1244
+ }
1029
1245
 
1030
1246
  // Find all CLI instances that belong to this mesh and are idle
1031
1247
  const cliInstances = components.instanceManager.getByCategory('cli');
@@ -1042,14 +1258,30 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
1042
1258
  // Only genuinely idle live sessions can pull work. Restored/stopped
1043
1259
  // records are kept for transcript/recovery visibility, but assigning
1044
1260
  // queue items to them strands tasks in assigned/pending without chat.
1045
- if (!isIdleSessionState(state)) continue;
1261
+ if (!isIdleSessionState(state)) {
1262
+ const status = readNonEmptyString(state.status).toLowerCase();
1263
+ skippedSessions.push({
1264
+ nodeId,
1265
+ sessionId: readNonEmptyString(state.instanceId),
1266
+ reason: isTerminalSessionStatus(status) ? 'terminal_session' : 'session_not_idle',
1267
+ status: status || undefined,
1268
+ });
1269
+ continue;
1270
+ }
1046
1271
 
1047
1272
  const sessionId = state.instanceId;
1048
1273
  const providerType = state.type || readNonEmptyString(settings.providerType);
1049
1274
 
1050
1275
  if (providerType) {
1051
1276
  // Try to assign a task to this idle node
1277
+ localIdleSessionsChecked += 1;
1052
1278
  tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType);
1279
+ } else {
1280
+ skippedSessions.push({
1281
+ nodeId,
1282
+ sessionId,
1283
+ reason: 'provider_type_missing',
1284
+ });
1053
1285
  }
1054
1286
  }
1055
1287
 
@@ -1058,6 +1290,7 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
1058
1290
  // Find if this node is in the same mesh
1059
1291
  const node = mesh.nodes.find((n: any) => n.id === idle.nodeId);
1060
1292
  if (node) {
1293
+ remoteIdleSessionsChecked += 1;
1061
1294
  const assigned = tryAssignQueueTask(components, meshId, idle.nodeId, idle.sessionId, idle.providerType);
1062
1295
  if (assigned) {
1063
1296
  remoteIdleSessions.delete(key);
@@ -1065,7 +1298,34 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
1065
1298
  }
1066
1299
  }
1067
1300
 
1068
- await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
1301
+ autoLaunchStarted = await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
1302
+ const afterQueue = getQueue(meshId);
1303
+ const pendingAfter = afterQueue.filter(task => task.status === 'pending').length;
1304
+ const assignedAfter = afterQueue.filter(task => task.status === 'assigned').length;
1305
+ const newlyAssignedTasks = afterQueue
1306
+ .filter(task => task.status === 'assigned' && beforeStatus.get(task.id) !== 'assigned')
1307
+ .map(task => ({
1308
+ id: task.id,
1309
+ nodeId: task.assignedNodeId,
1310
+ sessionId: task.assignedSessionId,
1311
+ }));
1312
+ return {
1313
+ success: true,
1314
+ meshId,
1315
+ pendingBefore,
1316
+ assignedBefore,
1317
+ pendingAfter,
1318
+ assignedAfter,
1319
+ claimed: newlyAssignedTasks.length > 0,
1320
+ newlyAssignedTasks,
1321
+ localIdleSessionsChecked,
1322
+ remoteIdleSessionsChecked,
1323
+ skippedSessions,
1324
+ autoLaunchStarted,
1325
+ ...(pendingAfter > 0 && newlyAssignedTasks.length === 0 && localIdleSessionsChecked === 0 && remoteIdleSessionsChecked === 0 && !autoLaunchStarted
1326
+ ? { noIdleMeshSessionAvailable: true }
1327
+ : {}),
1328
+ };
1069
1329
  }
1070
1330
 
1071
1331
  async function maybeAutoFastForwardIdleNode(components: DaemonComponents, args: {
@@ -372,6 +372,10 @@ export function __clearMeshQueueForTests(meshId: string): void {
372
372
  BeadsDB.getInstance().deleteQueue(meshId);
373
373
  }
374
374
 
375
+ export function __clearDirectDispatchesForTests(meshId: string): void {
376
+ BeadsDB.getInstance().deleteDirectDispatches(meshId);
377
+ }
378
+
375
379
  export function __resetBeadsDBForTests(): void {
376
380
  BeadsDB.resetForTests();
377
381
  }
@@ -64,6 +64,7 @@ function getMessageTime(message: unknown): number {
64
64
  type CompletedFinalizationBlock = {
65
65
  reason: string;
66
66
  terminal?: boolean;
67
+ allowTimeout?: boolean;
67
68
  };
68
69
 
69
70
  type CompletionFinalAssistantEvidence = {
@@ -613,7 +614,12 @@ export class CliProviderInstance implements ProviderInstance {
613
614
  if (suppressFreshLaunchStartupReplay) {
614
615
  parsedMessages = [];
615
616
  }
616
- const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
617
+ // Adapter runtime metadata is transport-owned and is not guaranteed to
618
+ // identify this conversation. Spec adapters historically exposed the
619
+ // provider spec id (for example "codex-cli") as runtimeId, which made
620
+ // concurrent sessions share one activeChat identity until their native
621
+ // provider session ids were discovered.
622
+ const activeChatId = this.providerSessionId || this.instanceId;
617
623
  const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
618
624
  ? Math.max(0, Number(parsedStatus.historyMessageCount))
619
625
  : null;
@@ -1164,6 +1170,7 @@ export class CliProviderInstance implements ProviderInstance {
1164
1170
  if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
1165
1171
  const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
1166
1172
  const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
1173
+ const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
1167
1174
  if (!finalAssistantEvidence.present) {
1168
1175
  if (adapterOwnsMessagesElsewhere) {
1169
1176
  if (finalAssistantEvidence.source === 'external-native') {
@@ -1172,19 +1179,20 @@ export class CliProviderInstance implements ProviderInstance {
1172
1179
  LOG.info('CLI', `[${this.type}] external transcript probe: msgCount=${probe.msgCount} lastRole=${probe.lastRole || 'none'} lastKind=${probe.lastKind || 'none'} contentLen=${probe.contentLen} sourceMtime=${probe.sourceMtimeMs ?? 'unknown'} mtimeAge=${probe.mtimeAgeMs ?? 'unknown'}ms`);
1173
1180
  pending.loggedTranscriptProbe = true;
1174
1181
  }
1175
- return { reason: 'missing_final_assistant', terminal: true };
1182
+ return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
1176
1183
  }
1177
1184
  // SpecCliAdapter never populates parsed.messages — chat history flows
1178
1185
  // through the daemon's native-history pipeline, not the status hook.
1179
1186
  // If that pipeline is unavailable, keep the old skip behavior for
1180
1187
  // providers that have not opted into strict final-assistant evidence.
1181
1188
  if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
1182
- return { reason: 'missing_final_assistant', terminal: true };
1189
+ return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
1183
1190
  }
1184
1191
  } else {
1185
1192
  return {
1186
1193
  reason: 'missing_final_assistant',
1187
1194
  terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
1195
+ allowTimeout: allowMissingAssistantTimeout,
1188
1196
  };
1189
1197
  }
1190
1198
  }
@@ -1234,7 +1242,7 @@ export class CliProviderInstance implements ProviderInstance {
1234
1242
  if (block) {
1235
1243
  const blockReason = block.reason;
1236
1244
  const waitedMs = Date.now() - pending.firstObservedAt;
1237
- if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
1245
+ if ((block.terminal && !block.allowTimeout) || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
1238
1246
  if (pending.loggedBlockReason !== blockReason) {
1239
1247
  LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
1240
1248
  pending.loggedBlockReason = blockReason;
@@ -390,7 +390,7 @@ export interface ProviderMeshCoordinatorConfig {
390
390
  requiresRestart?: boolean;
391
391
  /** User-facing setup explanation for manual modes. */
392
392
  instructions?: string;
393
- /** Copyable setup template. Supports {{meshId}}, {{adhdevMcpCommand}}, {{workspace}}, {{serverName}}. */
393
+ /** Copyable setup template. Supports {{meshId}}, {{adhdevMcpCommand}}, {{adhdevMcpArgs}}, {{workspace}}, {{serverName}}. */
394
394
  template?: string;
395
395
  };
396
396
  /**
@@ -27,6 +27,7 @@ export interface NativeHistoryInput {
27
27
  providerSessionId?: string;
28
28
  historySessionId?: string;
29
29
  workspace?: string;
30
+ sessionStartedAtMs?: number;
30
31
  format?: string;
31
32
  watchPath?: string;
32
33
  forceRefresh?: boolean;
@@ -34,7 +35,7 @@ export interface NativeHistoryInput {
34
35
  }
35
36
 
36
37
  export interface NativeHistoryResult {
37
- messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string }>;
38
+ messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string; workspace?: string }>;
38
39
  providerSessionId?: string;
39
40
  sourcePath: string;
40
41
  sourceMtimeMs: number;
@@ -53,7 +54,12 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
53
54
  // shows up before I type anything" on every provider).
54
55
  const requestedProviderSid = input.providerSessionId || '';
55
56
 
56
- const sourcePath = resolveSourcePath(reader, workspace, sessionId);
57
+ const sessionStartedAtMs = typeof input.sessionStartedAtMs === 'number'
58
+ ? input.sessionStartedAtMs
59
+ : typeof input.args?.sessionStartedAtMs === 'number'
60
+ ? input.args.sessionStartedAtMs
61
+ : 0;
62
+ const sourcePath = resolveSourcePath(reader, workspace, sessionId, sessionStartedAtMs);
57
63
  if (!sourcePath) return null;
58
64
  if (input.forceRefresh === true || input.args?.forceRefresh === true) {
59
65
  try { fs.statSync(sourcePath); } catch { /* best-effort metadata refresh */ }
@@ -72,6 +78,7 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
72
78
  content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
73
79
  receivedAt: typeof m.receivedAt === 'number' ? m.receivedAt : Date.parse(m.timestamp || '') || Date.now(),
74
80
  kind: typeof m.kind === 'string' ? m.kind : 'standard',
81
+ workspace: typeof m.workspace === 'string' ? m.workspace : workspace || undefined,
75
82
  })),
76
83
  providerSessionId: session.providerSessionId,
77
84
  sourcePath: session.sourcePath,
@@ -85,10 +92,10 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
85
92
  // Per-provider path resolution
86
93
  // ────────────────────────────────────────────────────────────────────────────
87
94
 
88
- function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string): string | null {
95
+ function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string, sessionStartedAtMs: number): string | null {
89
96
  switch (reader) {
90
97
  case 'claude-cli': return resolveClaudePath(workspace, sessionId);
91
- case 'codex-cli': return resolveCodexPath(workspace);
98
+ case 'codex-cli': return resolveCodexPath(workspace, sessionId, sessionStartedAtMs);
92
99
  case 'antigravity-cli': return resolveAntigravityPath(workspace);
93
100
  case 'hermes-cli': return resolveHermesPath(workspace, sessionId);
94
101
  }
@@ -112,21 +119,111 @@ function resolveClaudePath(workspace: string, sessionId: string): string | null
112
119
  return null;
113
120
  }
114
121
 
115
- function resolveCodexPath(workspace: string): string | null {
116
- void workspace;
122
+ function resolveCodexPath(workspace: string, sessionId: string, sessionStartedAtMs: number): string | null {
117
123
  // codex stores by UTC date: ~/.codex/sessions/<year>/<month>/<day>/<file>.jsonl
118
- const now = new Date();
119
- const dir = path.join(
120
- os.homedir(), '.codex', 'sessions',
121
- String(now.getUTCFullYear()),
122
- String(now.getUTCMonth() + 1).padStart(2, '0'),
123
- String(now.getUTCDate()).padStart(2, '0'),
124
- );
125
- if (fs.existsSync(dir)) {
126
- const f = newestRecentFile(dir, /\.jsonl$/);
127
- if (f) return f;
124
+ const root = codexSessionsRoot();
125
+ if (sessionId && isUuidLikeSessionId(sessionId)) {
126
+ return findCodexPathBySessionId(root, sessionId);
128
127
  }
129
- return null;
128
+ return findCodexPathByRuntime(root, workspace, sessionStartedAtMs);
129
+ }
130
+
131
+ function findCodexPathBySessionId(root: string, sessionId: string): string | null {
132
+ if (!fs.existsSync(root)) return null;
133
+ const needle = sessionId.toLowerCase();
134
+ const matches: Array<{ p: string; mtime: number }> = [];
135
+ const stack: string[] = [root];
136
+ while (stack.length > 0) {
137
+ const current = stack.pop()!;
138
+ let entries: fs.Dirent[] = [];
139
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
140
+ for (const entry of entries) {
141
+ const entryPath = path.join(current, entry.name);
142
+ if (entry.isDirectory()) {
143
+ stack.push(entryPath);
144
+ continue;
145
+ }
146
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
147
+ if (!entry.name.toLowerCase().includes(needle)) continue;
148
+ if (!isSafeFilename(entry.name.replace('.jsonl', ''))) continue;
149
+ matches.push({ p: entryPath, mtime: safeMtime(entryPath) });
150
+ }
151
+ }
152
+ matches.sort((a, b) => b.mtime - a.mtime);
153
+ return matches[0]?.p ?? null;
154
+ }
155
+
156
+ const CODEX_SPAWN_BIND_GRACE_MS = 10_000;
157
+
158
+ function findCodexPathByRuntime(root: string, workspace: string, sessionStartedAtMs: number): string | null {
159
+ if (!fs.existsSync(root) || !workspace) return null;
160
+ const workspaceResolved = resolveRealPath(workspace);
161
+ const cutoff = Date.now() - RECENT_WINDOW_MS;
162
+ const matches: Array<{ p: string; mtime: number; diff: number }> = [];
163
+ const stack: string[] = [root];
164
+
165
+ while (stack.length > 0) {
166
+ const current = stack.pop()!;
167
+ let entries: fs.Dirent[] = [];
168
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
169
+ for (const entry of entries) {
170
+ const entryPath = path.join(current, entry.name);
171
+ if (entry.isDirectory()) {
172
+ stack.push(entryPath);
173
+ continue;
174
+ }
175
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
176
+ const mtime = safeMtime(entryPath);
177
+ if (mtime < cutoff) continue;
178
+ const meta = readCodexSessionMeta(entryPath);
179
+ if (!meta?.cwd || resolveRealPath(meta.cwd) !== workspaceResolved) continue;
180
+ const diff = sessionStartedAtMs > 0 && meta.timestampMs != null
181
+ ? Math.abs(meta.timestampMs - sessionStartedAtMs)
182
+ : 0;
183
+ if (sessionStartedAtMs > 0 && (meta.timestampMs == null || diff > CODEX_SPAWN_BIND_GRACE_MS)) continue;
184
+ matches.push({ p: entryPath, mtime, diff });
185
+ }
186
+ }
187
+
188
+ matches.sort((a, b) => sessionStartedAtMs > 0
189
+ ? a.diff - b.diff || b.mtime - a.mtime
190
+ : b.mtime - a.mtime);
191
+ return matches[0]?.p ?? null;
192
+ }
193
+
194
+ function readCodexSessionMeta(filePath: string): { cwd?: string; timestampMs?: number } | null {
195
+ try {
196
+ const fd = fs.openSync(filePath, 'r');
197
+ try {
198
+ const buffer = Buffer.alloc(8192);
199
+ const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
200
+ if (bytes <= 0) return null;
201
+ const text = buffer.subarray(0, bytes).toString('utf8');
202
+ const firstLine = text.slice(0, text.indexOf('\n') >= 0 ? text.indexOf('\n') : text.length).trim();
203
+ if (!firstLine) return null;
204
+ const record = JSON.parse(firstLine) as Record<string, unknown>;
205
+ if (record.type !== 'session_meta' || !record.payload || typeof record.payload !== 'object') return null;
206
+ const payload = record.payload as Record<string, unknown>;
207
+ const timestampRaw = payload.timestamp;
208
+ const timestampMs = typeof timestampRaw === 'string'
209
+ ? Date.parse(timestampRaw)
210
+ : typeof timestampRaw === 'number'
211
+ ? (timestampRaw < 1e12 ? timestampRaw * 1000 : timestampRaw)
212
+ : NaN;
213
+ return {
214
+ cwd: typeof payload.cwd === 'string' ? payload.cwd : undefined,
215
+ timestampMs: Number.isFinite(timestampMs) ? timestampMs : undefined,
216
+ };
217
+ } finally {
218
+ fs.closeSync(fd);
219
+ }
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+
225
+ function resolveRealPath(value: string): string {
226
+ try { return fs.realpathSync(value); } catch { return value; }
130
227
  }
131
228
 
132
229
  function resolveAntigravityPath(workspace: string): string | null {
@@ -187,6 +284,18 @@ function cwdAsDashes(cwd: string): string {
187
284
  return cwd.replace(/\//g, '-');
188
285
  }
189
286
 
287
+ function codexSessionsRoot(): string {
288
+ return path.join(os.homedir(), '.codex', 'sessions');
289
+ }
290
+
291
+ function isUuidLikeSessionId(sessionId: string): boolean {
292
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
293
+ }
294
+
295
+ function isSafeFilename(name: string): boolean {
296
+ return /^[A-Za-z0-9._:-]+$/.test(name) && !name.includes('..');
297
+ }
298
+
190
299
  function newestFile(dir: string, pattern: RegExp): string | null {
191
300
  try {
192
301
  const entries = fs.readdirSync(dir, { withFileTypes: true })
@@ -36,6 +36,9 @@ import {
36
36
  } from './external-sources.js';
37
37
  import type { ProviderSourceMode } from '../config/config.js';
38
38
  import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
39
+ import { loadSpec } from './spec/loader.js';
40
+ import { executeNativeHistory } from './spec/native-history-executor.js';
41
+ import { createNativeHistoryDispatcher, type ReaderId } from './native-history/dispatcher.js';
39
42
 
40
43
  /**
41
44
  * Adds a provider-script root to the require whitelist. Wrapped in a
@@ -1244,8 +1247,6 @@ export class ProviderLoader {
1244
1247
  // Hand the resolved spec path off to route.ts via a hidden field
1245
1248
  // so the routing layer doesn't have to repeat the candidate walk.
1246
1249
  (resolved as any)._resolvedSpecPath = specPath;
1247
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1248
- const { loadSpec } = require('./spec/loader.js');
1249
1250
  const r = loadSpec(specPath);
1250
1251
  // Stub each control_bar entry as a provider.scripts.<id>. The
1251
1252
  // upstream invoke_provider_script gate checks that the script
@@ -1274,8 +1275,6 @@ export class ProviderLoader {
1274
1275
  let format = 'spec';
1275
1276
 
1276
1277
  if (nh.source) {
1277
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1278
- const { executeNativeHistory } = require('./spec/native-history-executor.js');
1279
1278
  format = `spec-${nh.source.kind}`;
1280
1279
  reader = (input: any) => executeNativeHistory(nh, input);
1281
1280
  } else if (nh.override_path) {
@@ -1294,9 +1293,7 @@ export class ProviderLoader {
1294
1293
  } catch { /* fall through — leave native unavailable */ }
1295
1294
  }
1296
1295
  } else if (nh.reader) {
1297
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1298
- const { createNativeHistoryDispatcher } = require('./native-history/dispatcher.js');
1299
- const dispatch = createNativeHistoryDispatcher(nh.reader);
1296
+ const dispatch = createNativeHistoryDispatcher(nh.reader as ReaderId);
1300
1297
  format = nh.reader;
1301
1298
  reader = (input: any) => dispatch(input);
1302
1299
  }