@adhdev/daemon-core 0.9.82-rc.116 → 0.9.82-rc.118

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.116",
3
+ "version": "0.9.82-rc.118",
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",
@@ -402,11 +402,13 @@ function buildNativeHistoryFallbackReason(args: {
402
402
  provider?: ProviderModule;
403
403
  nativeSource?: string;
404
404
  nativeHistoryCoverage?: string;
405
+ unavailableReason?: string;
405
406
  nativeMessageCount: number;
406
407
  safeMapping: boolean;
407
408
  freshEnough: boolean;
408
409
  }): string {
409
410
  if (!supportsCliNativeTranscript(args.providerType, args.provider)) return 'provider_native_transcript_not_supported';
411
+ if (args.unavailableReason) return `native_history_unavailable:${args.unavailableReason}`;
410
412
  if (args.nativeSource === 'native-unavailable') return 'native_history_unavailable';
411
413
  if (args.nativeHistoryCoverage === 'partial') return 'native_history_partial';
412
414
  if (args.nativeHistoryCoverage === 'unavailable') return 'native_history_unavailable';
@@ -509,6 +511,15 @@ function readCliProviderNativeHistory(agentStr: string, args: {
509
511
  scripts?: ProviderScripts;
510
512
  exactSessionScoped?: boolean;
511
513
  }): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
514
+ if (!args.historySessionId) {
515
+ return {
516
+ messages: [],
517
+ hasMore: false,
518
+ source: 'native-unavailable',
519
+ unavailableReason: 'native_history_workspace_only_lookup_unsafe',
520
+ lookup: 'session',
521
+ } as ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' };
522
+ }
512
523
  const sessionHistory = readProviderChatHistory(agentStr, {
513
524
  canonicalHistory: args.canonicalHistory,
514
525
  historySessionId: args.historySessionId,
@@ -519,24 +530,10 @@ function readCliProviderNativeHistory(agentStr: string, args: {
519
530
  historyBehavior: args.historyBehavior,
520
531
  scripts: args.scripts as any,
521
532
  });
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' };
533
+ // Native transcripts are keyed by provider/runtime session identity. Falling
534
+ // back to workspace makes concurrent local Codex/Hermes sessions alias each
535
+ // other when they share the same cwd.
536
+ return { ...(sessionHistory as any), lookup: 'session' };
540
537
  }
541
538
 
542
539
  function isNativeHistoryFreshEnough(args: {
@@ -1446,11 +1443,12 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1446
1443
  const fallbackReason = buildNativeHistoryFallbackReason({
1447
1444
  providerType,
1448
1445
  provider,
1449
- nativeSource: (nativeHistory as any).source,
1450
- nativeHistoryCoverage,
1451
- nativeMessageCount: nativeMessages.length,
1452
- safeMapping,
1453
- freshEnough,
1446
+ nativeSource: (nativeHistory as any).source,
1447
+ nativeHistoryCoverage,
1448
+ unavailableReason,
1449
+ nativeMessageCount: nativeMessages.length,
1450
+ safeMapping,
1451
+ freshEnough,
1454
1452
  });
1455
1453
  messageSource = buildCliMessageSourceProvenance({
1456
1454
  selected: 'pty-parser',
@@ -1581,6 +1579,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1581
1579
  provider,
1582
1580
  nativeSource: (history as any).source,
1583
1581
  nativeHistoryCoverage,
1582
+ unavailableReason,
1584
1583
  nativeMessageCount: historyMessages.length,
1585
1584
  safeMapping,
1586
1585
  freshEnough: true,
@@ -55,6 +55,7 @@ import {
55
55
  import {
56
56
  MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
57
57
  MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
58
+ loadMeshWorktreeBootstrapConfig,
58
59
  runMeshWorktreeBootstrap,
59
60
  type WorktreeBootstrapState,
60
61
  } from '../mesh/worktree-bootstrap-config.js';
@@ -934,6 +935,12 @@ function finalizeMeshNodeStatus(args: {
934
935
  || 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
935
936
  return;
936
937
  }
938
+ if (bootstrap.status === 'running' && bootstrap.required !== false) {
939
+ status.launchReady = false;
940
+ status.launchBlockedReason = 'worktree_bootstrap_running';
941
+ status.launchBlockedMessage = 'Required worktree bootstrap is still running; wait for it to finish before launching an agent into this node.';
942
+ return;
943
+ }
937
944
  }
938
945
  const connectionState = readStringValue(readObjectRecord(status.connection).state);
939
946
  status.launchReady = !!daemonId && (
@@ -1072,9 +1079,12 @@ function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
1072
1079
  };
1073
1080
  }
1074
1081
 
1075
- function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
1082
+ function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string, nodeWorkspace = '', nodeIsMissingLocalWorktree = false): boolean {
1076
1083
  const recordNodeId = readStringValue(record?.meta?.meshNodeId);
1077
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;
1078
1088
  const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
1079
1089
  return !recordMeshId || recordMeshId === meshId;
1080
1090
  }
@@ -1123,9 +1133,15 @@ function collectLiveMeshSessionRecords(args: {
1123
1133
  liveSessionRecords: any[];
1124
1134
  allowCoordinatorSession?: boolean;
1125
1135
  }): any[] {
1136
+ const nodeWorkspace = readStringValue(args.node?.workspace);
1137
+ const nodeIsMissingLocalWorktree = args.node?.isLocalWorktree === true
1138
+ && !!nodeWorkspace
1139
+ && !fs.existsSync(nodeWorkspace);
1126
1140
  const matches = args.liveSessionRecords.filter((record) => {
1127
- const nodeWorkspace = readStringValue(args.node?.workspace);
1128
- 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;
1129
1145
  return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
1130
1146
  });
1131
1147
 
@@ -1148,11 +1164,15 @@ function buildHistoricalMeshSessions(args: {
1148
1164
  }): { count: number; sessions: Record<string, unknown>[]; instruction: string } | undefined {
1149
1165
  const liveNodeIds = new Set<string>();
1150
1166
  const liveWorkspaces = new Set<string>();
1167
+ const missingLocalWorktreeNodeIds = new Set<string>();
1151
1168
  for (const node of args.nodes || []) {
1152
1169
  const nodeId = readStringValue(node?.id, node?.nodeId);
1153
1170
  const workspace = readStringValue(node?.workspace);
1154
1171
  if (nodeId) liveNodeIds.add(nodeId);
1155
1172
  if (workspace) liveWorkspaces.add(workspace);
1173
+ if (nodeId && node?.isLocalWorktree === true && workspace && !fs.existsSync(workspace)) {
1174
+ missingLocalWorktreeNodeIds.add(nodeId);
1175
+ }
1156
1176
  }
1157
1177
 
1158
1178
  const sessions: Record<string, unknown>[] = [];
@@ -1162,7 +1182,7 @@ function buildHistoricalMeshSessions(args: {
1162
1182
  if (recordMeshId !== args.meshId) continue;
1163
1183
  const recordNodeId = readStringValue(meta.meshNodeId);
1164
1184
  const workspace = readStringValue(record?.workspace);
1165
- const removedNode = !!recordNodeId && !liveNodeIds.has(recordNodeId);
1185
+ const removedNode = !!recordNodeId && (!liveNodeIds.has(recordNodeId) || missingLocalWorktreeNodeIds.has(recordNodeId));
1166
1186
  const orphanedWorkspace = !!workspace && !liveWorkspaces.has(workspace) && meta.meshCoordinatorFor !== args.meshId;
1167
1187
  if (!removedNode && !orphanedWorkspace) continue;
1168
1188
  sessions.push({
@@ -4971,60 +4991,120 @@ export class DaemonCommandRouter {
4971
4991
  this.invalidateAggregateMeshStatus(meshId);
4972
4992
  }
4973
4993
 
4974
- // Initialize submodules if policy allows (default: true)
4975
- const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
4976
- if (initSubmodules) {
4977
- try {
4978
- const { runGit } = await import('../git/git-executor.js');
4979
- await runGit(
4980
- { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
4981
- ['submodule', 'update', '--init', '--recursive'],
4982
- { timeoutMs: 120000 },
4983
- );
4984
- } catch (subErr: any) {
4985
- // Submodule init is best-effort; don't fail the clone
4986
- console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
4994
+ const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
4995
+ node.worktreeBootstrap = bootstrapState;
4996
+ if (meshRecord.inline) {
4997
+ this.updateInlineMeshNode(meshId, mesh, node);
4998
+ return;
4987
4999
  }
4988
- }
4989
-
4990
- const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
4991
- node.worktreeBootstrap = bootstrapState;
4992
- if (!meshRecord.inline) {
4993
5000
  try {
4994
5001
  const { updateNode } = await import('../config/mesh-config.js');
4995
5002
  updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
4996
5003
  this.invalidateAggregateMeshStatus(meshId);
4997
5004
  } catch { /* bootstrap status persistence is best-effort */ }
4998
- }
5005
+ };
4999
5006
 
5000
- // Record in task ledger
5001
- try {
5002
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
5003
- appendLedgerEntry(meshId, {
5004
- kind: 'node_cloned',
5005
- nodeId: node.id,
5006
- payload: {
5007
- sourceNodeId,
5008
- branch: result.branch,
5009
- worktreePath: result.worktreePath,
5010
- submodulesInitialized: initSubmodules,
5011
- worktreeBootstrap: {
5012
- status: bootstrapState.status,
5013
- required: bootstrapState.required,
5014
- configSource: bootstrapState.configSource,
5015
- configSourceType: bootstrapState.configSourceType,
5016
- lastCommand: bootstrapState.lastCommand,
5017
- exitCode: bootstrapState.exitCode,
5007
+ const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
5008
+ try {
5009
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
5010
+ appendLedgerEntry(meshId, {
5011
+ kind: 'node_cloned',
5012
+ nodeId: node.id,
5013
+ payload: {
5014
+ sourceNodeId,
5015
+ branch: result.branch,
5016
+ worktreePath: result.worktreePath,
5017
+ submodulesInitialized: initSubmodules,
5018
+ worktreeBootstrap: {
5019
+ status: bootstrapState.status,
5020
+ required: bootstrapState.required,
5021
+ configSource: bootstrapState.configSource,
5022
+ configSourceType: bootstrapState.configSourceType,
5023
+ lastCommand: bootstrapState.lastCommand,
5024
+ exitCode: bootstrapState.exitCode,
5025
+ },
5018
5026
  },
5019
- },
5027
+ });
5028
+ } catch { /* ledger append is best-effort */ }
5029
+ };
5030
+
5031
+ const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
5032
+ const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
5033
+ const runningBootstrapState: WorktreeBootstrapState = {
5034
+ status: 'running',
5035
+ required: loadedBootstrap.config?.required !== false,
5036
+ configSource: loadedBootstrap.path || loadedBootstrap.source,
5037
+ configSourceType: loadedBootstrap.sourceType,
5038
+ startedAt: new Date().toISOString(),
5039
+ };
5040
+ await persistWorktreeSetupState(runningBootstrapState);
5041
+
5042
+ const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
5043
+ let submodulesInitialized = false;
5044
+ if (initSubmodules) {
5045
+ try {
5046
+ const { runGit } = await import('../git/git-executor.js');
5047
+ await runGit(
5048
+ { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
5049
+ ['submodule', 'update', '--init', '--recursive'],
5050
+ { timeoutMs: 120000 },
5051
+ );
5052
+ submodulesInitialized = true;
5053
+ } catch (subErr: any) {
5054
+ // Submodule init is best-effort; don't fail the clone
5055
+ console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
5056
+ }
5057
+ }
5058
+ const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
5059
+ await persistWorktreeSetupState(bootstrapState);
5060
+ await appendCloneLedger(submodulesInitialized, bootstrapState);
5061
+ return { submodulesInitialized, bootstrapState };
5062
+ };
5063
+
5064
+ const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
5065
+ const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
5066
+ ? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
5067
+ : 8000;
5068
+ const setupPromise = finishWorktreeSetup();
5069
+ const setupResult = await Promise.race([
5070
+ setupPromise.then((value) => ({ completed: true as const, value })),
5071
+ new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
5072
+ ]);
5073
+
5074
+ if (!setupResult.completed) {
5075
+ setupPromise.catch((error: any) => {
5076
+ const failedState: WorktreeBootstrapState = {
5077
+ ...runningBootstrapState,
5078
+ status: 'failed',
5079
+ completedAt: new Date().toISOString(),
5080
+ error: error?.message || String(error),
5081
+ };
5082
+ void persistWorktreeSetupState(failedState);
5083
+ void appendCloneLedger(false, failedState);
5020
5084
  });
5021
- } catch { /* ledger append is best-effort */ }
5085
+ return {
5086
+ success: true,
5087
+ async: true,
5088
+ status: 'accepted',
5089
+ node,
5090
+ worktreePath: result.worktreePath,
5091
+ branch: result.branch,
5092
+ worktreeBootstrap: runningBootstrapState,
5093
+ worktreeSetup: {
5094
+ status: 'running',
5095
+ setupWaitMs,
5096
+ message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
5097
+ },
5098
+ };
5099
+ }
5022
5100
 
5101
+ const { submodulesInitialized, bootstrapState } = setupResult.value;
5023
5102
  return {
5024
5103
  success: true,
5025
5104
  node,
5026
5105
  worktreePath: result.worktreePath,
5027
5106
  branch: result.branch,
5107
+ submodulesInitialized,
5028
5108
  worktreeBootstrap: bootstrapState,
5029
5109
  };
5030
5110
  } catch (e: any) {
@@ -5569,12 +5649,22 @@ export class DaemonCommandRouter {
5569
5649
  for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
5570
5650
  const nodeId = String(node.id || node.nodeId || '');
5571
5651
  const daemonId = readStringValue(node.daemonId);
5652
+ const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
5653
+ const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
5572
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;
5573
5662
  const isSelfNode = Boolean(
5574
5663
  nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
5575
5664
  ) || Boolean(
5576
5665
  daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
5577
- ) || Boolean(meshRecord?.inline && nodeIndex === 0);
5666
+ ) || Boolean(meshRecord?.inline && nodeIndex === 0)
5667
+ || sparseConfiguredCoordinatorNode;
5578
5668
  const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
5579
5669
  localMachineId,
5580
5670
  localDaemonId: this.deps.statusInstanceId,
@@ -5593,7 +5683,7 @@ export class DaemonCommandRouter {
5593
5683
  worktreeBranch: node.worktreeBranch,
5594
5684
  role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
5595
5685
  daemonId,
5596
- machineId: readMeshNodeMachineId(node as Record<string, unknown>) || node.machineId,
5686
+ machineId: nodeMachineId || node.machineId,
5597
5687
  machine: machineIdentity,
5598
5688
  machineStatus: node.machineStatus,
5599
5689
  health: 'unknown',
@@ -24,6 +24,13 @@ function normalizeApprovalLabel(value: string): string {
24
24
  .trim();
25
25
  }
26
26
 
27
+ function isNegativeApprovalLabel(value: string): boolean {
28
+ const label = normalizeApprovalLabel(value);
29
+ return /^(no|deny|reject|cancel|skip|exit|stop)\b/.test(label)
30
+ || /\bwithout\b/.test(label)
31
+ || /\bdo not\b/.test(label);
32
+ }
33
+
27
34
  export function getApprovalPositiveHints(provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null): string[] {
28
35
  const customHints = Array.isArray(provider?.approvalPositiveHints)
29
36
  ? provider.approvalPositiveHints
@@ -39,24 +46,24 @@ export function pickApprovalButton(
39
46
  ): { index: number; label: string } {
40
47
  const labels = (buttons || []).map((button) => String(button || '').trim()).filter(Boolean);
41
48
  if (labels.length === 0) {
42
- return { index: 0, label: 'Approve' };
49
+ return { index: -1, label: '' };
43
50
  }
44
51
 
45
52
  const normalizedButtons = labels.map((label) => normalizeApprovalLabel(label));
46
53
  const hints = getApprovalPositiveHints(provider);
47
54
 
48
55
  for (const hint of hints) {
49
- const exactIndex = normalizedButtons.findIndex((label) => label === hint);
56
+ const exactIndex = normalizedButtons.findIndex((label, index) => label === hint && !isNegativeApprovalLabel(labels[index]));
50
57
  if (exactIndex >= 0) return { index: exactIndex, label: labels[exactIndex] };
51
58
 
52
- const prefixIndex = normalizedButtons.findIndex((label) => label.startsWith(hint));
59
+ const prefixIndex = normalizedButtons.findIndex((label, index) => label.startsWith(hint) && !isNegativeApprovalLabel(labels[index]));
53
60
  if (prefixIndex >= 0) return { index: prefixIndex, label: labels[prefixIndex] };
54
61
 
55
- const includeIndex = normalizedButtons.findIndex((label) => label.includes(hint));
62
+ const includeIndex = normalizedButtons.findIndex((label, index) => label.includes(hint) && !isNegativeApprovalLabel(labels[index]));
56
63
  if (includeIndex >= 0) return { index: includeIndex, label: labels[includeIndex] };
57
64
  }
58
65
 
59
- return { index: 0, label: labels[0] };
66
+ return { index: -1, label: '' };
60
67
  }
61
68
 
62
69
  export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string {