@adhdev/daemon-core 0.9.82-rc.117 → 0.9.82-rc.119

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.117",
3
+ "version": "0.9.82-rc.119",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,6 +26,11 @@ const RECENT_SEND_WINDOW_MS = 1200;
26
26
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
27
27
  const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
28
28
  const CLI_NATIVE_HISTORY_FRESH_MS = 5 * 60_000;
29
+ // Fallback list for supportsCliNativeTranscript() when the ProviderModule is
30
+ // unavailable (e.g. provider config not yet loaded). The authoritative check is
31
+ // provider.canonicalHistory via isNativeSourceCanonicalHistory(). New providers
32
+ // with native transcripts should set canonicalHistory in their provider.json
33
+ // rather than adding entries here.
29
34
  const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'hermes-cli', 'antigravity-cli']);
30
35
  const recentSendByTarget = new Map<string, number>();
31
36
 
@@ -402,11 +407,13 @@ function buildNativeHistoryFallbackReason(args: {
402
407
  provider?: ProviderModule;
403
408
  nativeSource?: string;
404
409
  nativeHistoryCoverage?: string;
410
+ unavailableReason?: string;
405
411
  nativeMessageCount: number;
406
412
  safeMapping: boolean;
407
413
  freshEnough: boolean;
408
414
  }): string {
409
415
  if (!supportsCliNativeTranscript(args.providerType, args.provider)) return 'provider_native_transcript_not_supported';
416
+ if (args.unavailableReason) return `native_history_unavailable:${args.unavailableReason}`;
410
417
  if (args.nativeSource === 'native-unavailable') return 'native_history_unavailable';
411
418
  if (args.nativeHistoryCoverage === 'partial') return 'native_history_partial';
412
419
  if (args.nativeHistoryCoverage === 'unavailable') return 'native_history_unavailable';
@@ -469,15 +476,20 @@ function hasSafeNativeHistoryMapping(args: {
469
476
  return messageSessionIds.some((id) => id === explicitSessionId);
470
477
  }
471
478
 
479
+ // Messages carry no historySessionId — cannot confirm they belong to the requested session.
480
+ // Only allow a coordinator transcript that is also confirmed by the PTY side; otherwise
481
+ // fail closed so a same-workspace session's history is never silently accepted.
472
482
  if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
473
483
  const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
474
484
  const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
475
485
  return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
476
486
  });
477
- if (!ptyHasCoordinator) {
478
- return false;
479
- }
487
+ return ptyHasCoordinator;
480
488
  }
489
+
490
+ // No historySessionId in messages and no coordinator cross-check: fail closed.
491
+ // Workspace-only matching must not override an explicit session identity.
492
+ return false;
481
493
  }
482
494
  const workspace = String(args.workspace || '').trim();
483
495
  if (!workspace) return false;
@@ -498,6 +510,11 @@ function hasSafeNativeHistoryMapping(args: {
498
510
  return hasOverlappingVisibleConversationText(args.nativeMessages, args.ptyMessages || []);
499
511
  }
500
512
 
513
+ // Provenance boundary: workspace-only native history lookup is never safe
514
+ // because multiple concurrent sessions sharing the same cwd would alias each
515
+ // other. historySessionId (the provider-native session key) is required to
516
+ // establish ownership. hasSafeNativeHistoryMapping() enforces the same
517
+ // invariant after the read; both guards must hold for native history to be used.
501
518
  function readCliProviderNativeHistory(agentStr: string, args: {
502
519
  canonicalHistory?: ProviderModule['canonicalHistory'];
503
520
  historySessionId?: string;
@@ -507,8 +524,16 @@ function readCliProviderNativeHistory(agentStr: string, args: {
507
524
  excludeRecentCount: number;
508
525
  historyBehavior?: ProviderModule['historyBehavior'];
509
526
  scripts?: ProviderScripts;
510
- exactSessionScoped?: boolean;
511
527
  }): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
528
+ if (!args.historySessionId) {
529
+ return {
530
+ messages: [],
531
+ hasMore: false,
532
+ source: 'native-unavailable',
533
+ unavailableReason: 'native_history_workspace_only_lookup_unsafe',
534
+ lookup: 'session',
535
+ } as ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' };
536
+ }
512
537
  const sessionHistory = readProviderChatHistory(agentStr, {
513
538
  canonicalHistory: args.canonicalHistory,
514
539
  historySessionId: args.historySessionId,
@@ -519,24 +544,10 @@ function readCliProviderNativeHistory(agentStr: string, args: {
519
544
  historyBehavior: args.historyBehavior,
520
545
  scripts: args.scripts as any,
521
546
  });
522
- // Exact runtime/provider transcript reads must not silently fall back to the
523
- // workspace's active or most recent native transcript: multiple Hermes/Gemini/
524
- // Codex sessions can run in the same workspace, and workspace fallback can make
525
- // read_chat/completion evidence point at a different runtime's prompt.
526
- if ((sessionHistory as any).source !== 'native-unavailable' || args.exactSessionScoped || !args.historySessionId || !args.workspace) {
527
- return { ...(sessionHistory as any), lookup: args.historySessionId ? 'session' : 'workspace' };
528
- }
529
- const workspaceHistory = readProviderChatHistory(agentStr, {
530
- canonicalHistory: args.canonicalHistory,
531
- historySessionId: undefined,
532
- workspace: args.workspace,
533
- offset: args.offset,
534
- limit: args.limit,
535
- excludeRecentCount: args.excludeRecentCount,
536
- historyBehavior: args.historyBehavior,
537
- scripts: args.scripts as any,
538
- });
539
- return { ...(workspaceHistory as any), lookup: 'workspace' };
547
+ // Native transcripts are keyed by provider/runtime session identity. Falling
548
+ // back to workspace makes concurrent local Codex/Hermes sessions alias each
549
+ // other when they share the same cwd.
550
+ return { ...(sessionHistory as any), lookup: 'session' };
540
551
  }
541
552
 
542
553
  function isNativeHistoryFreshEnough(args: {
@@ -1223,7 +1234,6 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
1223
1234
  excludeRecentCount,
1224
1235
  historyBehavior: provider?.historyBehavior,
1225
1236
  scripts: provider?.scripts as any,
1226
- exactSessionScoped: exactNativeHistoryScope,
1227
1237
  })
1228
1238
  : readProviderChatHistory(agentStr, {
1229
1239
  canonicalHistory: provider?.canonicalHistory,
@@ -1363,7 +1373,6 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1363
1373
  excludeRecentCount: 0,
1364
1374
  historyBehavior: provider?.historyBehavior,
1365
1375
  scripts: provider?.scripts as any,
1366
- exactSessionScoped: exactNativeHistoryScope,
1367
1376
  });
1368
1377
  } catch (error: any) {
1369
1378
  const fallbackReason = `native_history_error:${error?.message || String(error)}`;
@@ -1446,11 +1455,12 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1446
1455
  const fallbackReason = buildNativeHistoryFallbackReason({
1447
1456
  providerType,
1448
1457
  provider,
1449
- nativeSource: (nativeHistory as any).source,
1450
- nativeHistoryCoverage,
1451
- nativeMessageCount: nativeMessages.length,
1452
- safeMapping,
1453
- freshEnough,
1458
+ nativeSource: (nativeHistory as any).source,
1459
+ nativeHistoryCoverage,
1460
+ unavailableReason,
1461
+ nativeMessageCount: nativeMessages.length,
1462
+ safeMapping,
1463
+ freshEnough,
1454
1464
  });
1455
1465
  messageSource = buildCliMessageSourceProvenance({
1456
1466
  selected: 'pty-parser',
@@ -1528,7 +1538,6 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1528
1538
  excludeRecentCount: 0,
1529
1539
  historyBehavior: provider?.historyBehavior,
1530
1540
  scripts: provider?.scripts as any,
1531
- exactSessionScoped: exactNativeHistoryScope,
1532
1541
  })
1533
1542
  : readProviderChatHistory(agentStr, {
1534
1543
  canonicalHistory: provider?.canonicalHistory,
@@ -1581,6 +1590,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1581
1590
  provider,
1582
1591
  nativeSource: (history as any).source,
1583
1592
  nativeHistoryCoverage,
1593
+ unavailableReason,
1584
1594
  nativeMessageCount: historyMessages.length,
1585
1595
  safeMapping,
1586
1596
  freshEnough: true,
@@ -1079,9 +1079,12 @@ function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
1079
1079
  };
1080
1080
  }
1081
1081
 
1082
- function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
1082
+ function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string, nodeWorkspace = '', nodeIsMissingLocalWorktree = false): boolean {
1083
1083
  const recordNodeId = readStringValue(record?.meta?.meshNodeId);
1084
1084
  if (!recordNodeId || recordNodeId !== nodeId) return false;
1085
+ if (nodeIsMissingLocalWorktree) return false;
1086
+ const recordWorkspace = readStringValue(record?.workspace);
1087
+ if (nodeWorkspace && recordWorkspace && recordWorkspace !== nodeWorkspace) return false;
1085
1088
  const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
1086
1089
  return !recordMeshId || recordMeshId === meshId;
1087
1090
  }
@@ -1130,9 +1133,15 @@ function collectLiveMeshSessionRecords(args: {
1130
1133
  liveSessionRecords: any[];
1131
1134
  allowCoordinatorSession?: boolean;
1132
1135
  }): any[] {
1136
+ const nodeWorkspace = readStringValue(args.node?.workspace);
1137
+ const nodeIsMissingLocalWorktree = args.node?.isLocalWorktree === true
1138
+ && !!nodeWorkspace
1139
+ && !fs.existsSync(nodeWorkspace);
1133
1140
  const matches = args.liveSessionRecords.filter((record) => {
1134
- const nodeWorkspace = readStringValue(args.node?.workspace);
1135
- if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)) return true;
1141
+ const recordNodeId = readStringValue(record?.meta?.meshNodeId);
1142
+ if (recordNodeId && recordNodeId !== args.nodeId) return false;
1143
+ if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId, nodeWorkspace || '', nodeIsMissingLocalWorktree)) return true;
1144
+ if (nodeIsMissingLocalWorktree) return false;
1136
1145
  return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
1137
1146
  });
1138
1147
 
@@ -1155,11 +1164,15 @@ function buildHistoricalMeshSessions(args: {
1155
1164
  }): { count: number; sessions: Record<string, unknown>[]; instruction: string } | undefined {
1156
1165
  const liveNodeIds = new Set<string>();
1157
1166
  const liveWorkspaces = new Set<string>();
1167
+ const missingLocalWorktreeNodeIds = new Set<string>();
1158
1168
  for (const node of args.nodes || []) {
1159
1169
  const nodeId = readStringValue(node?.id, node?.nodeId);
1160
1170
  const workspace = readStringValue(node?.workspace);
1161
1171
  if (nodeId) liveNodeIds.add(nodeId);
1162
1172
  if (workspace) liveWorkspaces.add(workspace);
1173
+ if (nodeId && node?.isLocalWorktree === true && workspace && !fs.existsSync(workspace)) {
1174
+ missingLocalWorktreeNodeIds.add(nodeId);
1175
+ }
1163
1176
  }
1164
1177
 
1165
1178
  const sessions: Record<string, unknown>[] = [];
@@ -1169,7 +1182,7 @@ function buildHistoricalMeshSessions(args: {
1169
1182
  if (recordMeshId !== args.meshId) continue;
1170
1183
  const recordNodeId = readStringValue(meta.meshNodeId);
1171
1184
  const workspace = readStringValue(record?.workspace);
1172
- const removedNode = !!recordNodeId && !liveNodeIds.has(recordNodeId);
1185
+ const removedNode = !!recordNodeId && (!liveNodeIds.has(recordNodeId) || missingLocalWorktreeNodeIds.has(recordNodeId));
1173
1186
  const orphanedWorkspace = !!workspace && !liveWorkspaces.has(workspace) && meta.meshCoordinatorFor !== args.meshId;
1174
1187
  if (!removedNode && !orphanedWorkspace) continue;
1175
1188
  sessions.push({
@@ -5636,12 +5649,22 @@ export class DaemonCommandRouter {
5636
5649
  for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
5637
5650
  const nodeId = String(node.id || node.nodeId || '');
5638
5651
  const daemonId = readStringValue(node.daemonId);
5652
+ const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
5653
+ const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
5639
5654
  const providerPriority = readProviderPriorityFromPolicy(node.policy);
5655
+ const configuredCoordinatorNode = Boolean(
5656
+ nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
5657
+ );
5658
+ const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
5659
+ && !daemonId
5660
+ && !nodeMachineId
5661
+ && !nodeHostname;
5640
5662
  const isSelfNode = Boolean(
5641
5663
  nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
5642
5664
  ) || Boolean(
5643
5665
  daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
5644
- ) || Boolean(meshRecord?.inline && nodeIndex === 0);
5666
+ ) || Boolean(meshRecord?.inline && nodeIndex === 0)
5667
+ || sparseConfiguredCoordinatorNode;
5645
5668
  const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
5646
5669
  localMachineId,
5647
5670
  localDaemonId: this.deps.statusInstanceId,
@@ -5660,7 +5683,7 @@ export class DaemonCommandRouter {
5660
5683
  worktreeBranch: node.worktreeBranch,
5661
5684
  role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
5662
5685
  daemonId,
5663
- machineId: readMeshNodeMachineId(node as Record<string, unknown>) || node.machineId,
5686
+ machineId: nodeMachineId || node.machineId,
5664
5687
  machine: machineIdentity,
5665
5688
  machineStatus: node.machineStatus,
5666
5689
  health: 'unknown',