@adhdev/daemon-core 0.9.82-rc.373 → 0.9.82-rc.374

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.
@@ -11,6 +11,18 @@ export declare function findRecentTerminalLedgerEvidence(args: {
11
11
  } | null;
12
12
  export declare function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId: string): boolean;
13
13
  export declare function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: string): boolean;
14
+ export declare function findTerminalLedgerEvidenceForTask(args: {
15
+ meshId: string;
16
+ taskId?: string;
17
+ sessionId?: string;
18
+ nodeId?: string;
19
+ tail?: number;
20
+ }): {
21
+ id: string;
22
+ kind: Extract<MeshLedgerKind, 'task_completed' | 'task_failed' | 'task_stalled'>;
23
+ payload: Record<string, unknown>;
24
+ timestamp: string;
25
+ } | null;
14
26
  export declare function reconcileDirectDispatchCompletionFromTranscript(args: {
15
27
  meshId: string;
16
28
  nodeId?: string;
@@ -1,5 +1,5 @@
1
1
  import type { ChatMessage } from '../types.js';
2
- export declare const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 4000;
2
+ export declare const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 16000;
3
3
  export declare function extractFinalSummaryFromMessages(messages: ChatMessage[] | null | undefined, maxChars?: number): string;
4
4
  /**
5
5
  * Like extractFinalSummaryFromMessages but also returns the ISO timestamp of the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.373",
3
+ "version": "0.9.82-rc.374",
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",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.373",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.374",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -289,9 +289,19 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
289
289
  const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>) || '';
290
290
  const selfDaemonId = ctx.deps.statusInstanceId || '';
291
291
  const selfMachineId = (() => { try { return loadConfig().machineId || ''; } catch { return ''; } })();
292
+ // Identity match is form-safe: a daemon answers to the same machine
293
+ // under interchangeable id forms (bare `mach_X`, cloud `daemon_mach_X`,
294
+ // standalone `standalone_mach_X`). statusInstanceId/loadConfig().machineId
295
+ // and the node's stored daemonId/machineId frequently hold DIFFERENT forms
296
+ // of the same machine, so a raw `===` would miss the self-match and let the
297
+ // coordinator delete its own live base node (the very accident this guard
298
+ // exists to prevent). daemonIdsEquivalent collapses every form to its
299
+ // machine core before comparing, so a same-machine match is caught
300
+ // regardless of which form each side carries. This only widens matches
301
+ // (every raw-`===` hit still matches) — fail-open → fail-closed.
292
302
  const isCoordinatorBaseNode =
293
- (!!selfDaemonId && (nodeDaemonId === selfDaemonId || nodeMachineId === selfDaemonId))
294
- || (!!selfMachineId && (nodeDaemonId === selfMachineId || nodeMachineId === selfMachineId));
303
+ (!!selfDaemonId && (daemonIdsEquivalent(nodeDaemonId, selfDaemonId) || daemonIdsEquivalent(nodeMachineId, selfDaemonId)))
304
+ || (!!selfMachineId && (daemonIdsEquivalent(nodeDaemonId, selfMachineId) || daemonIdsEquivalent(nodeMachineId, selfMachineId)));
295
305
  if (isCoordinatorBaseNode) {
296
306
  return {
297
307
  success: false,
@@ -23,6 +23,7 @@ import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
23
23
  import { normalizeMeshNodeId, meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, normalizeMeshWorkspaceForCompare, meshWorkspacesEquivalent, type MeshNodeIdentified } from '@adhdev/mesh-shared';
24
24
  import {
25
25
  findRecentTerminalLedgerEvidence,
26
+ findTerminalLedgerEvidenceForTask,
26
27
  hasDispatchAfterTerminal,
27
28
  hasUnterminalDirectDispatchLedgerEntry,
28
29
  buildNoProgressCompletionReconciliation,
@@ -654,6 +655,24 @@ export function tryAssignQueueTask(
654
655
  return false;
655
656
  }
656
657
 
658
+ const terminal = findTerminalLedgerEvidenceForTask({
659
+ meshId,
660
+ taskId: task.id,
661
+ });
662
+ if (terminal) {
663
+ const status = terminal.kind === 'task_completed' ? 'completed' : 'failed';
664
+ updateTaskStatus(meshId, task.id, status);
665
+ LOG.info('MeshQueue', `Skipped dispatch for terminal task ${task.id} on mesh ${meshId}; ${terminal.kind} ledger evidence already exists`);
666
+ traceMeshEventDrop('dispatch_terminal_ledger', {
667
+ taskId: task.id,
668
+ sessionId,
669
+ nodeId,
670
+ meshId,
671
+ event: 'agent_command',
672
+ }, terminal.kind);
673
+ return false;
674
+ }
675
+
657
676
  LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
658
677
 
659
678
  if (node?.daemonId && components.dispatchMeshCommand) {
@@ -85,6 +85,29 @@ function isWeakCompletionLedgerPayload(payload: Record<string, unknown> | undefi
85
85
  return diag?.finalAssistantPresent === false || diag?.blockReason === 'missing_final_assistant';
86
86
  }
87
87
 
88
+ export function findTerminalLedgerEvidenceForTask(args: {
89
+ meshId: string;
90
+ taskId?: string;
91
+ sessionId?: string;
92
+ nodeId?: string;
93
+ tail?: number;
94
+ }): { id: string; kind: Extract<MeshLedgerKind, 'task_completed' | 'task_failed' | 'task_stalled'>; payload: Record<string, unknown>; timestamp: string } | null {
95
+ const taskId = readNonEmptyString(args.taskId);
96
+ if (!taskId) return null;
97
+ const entries = readLedgerEntries(args.meshId, { tail: args.tail ?? 500 });
98
+ for (let i = entries.length - 1; i >= 0; i--) {
99
+ const entry = entries[i];
100
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
101
+ const terminalTaskId = readNonEmptyString(entry.payload?.taskId);
102
+ if (terminalTaskId !== taskId) continue;
103
+ if (entry.kind === 'task_completed' && isWeakCompletionLedgerPayload(entry.payload)) continue;
104
+ if (args.sessionId && entry.sessionId && entry.sessionId !== args.sessionId) continue;
105
+ if (!args.sessionId && args.nodeId && entry.nodeId && !meshNodeIdMatches(entry as unknown as MeshNodeIdentified, args.nodeId)) continue;
106
+ return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
107
+ }
108
+ return null;
109
+ }
110
+
88
111
  function findDirectDispatchLedgerEntry(args: {
89
112
  meshId: string;
90
113
  taskId: string;
@@ -109,7 +109,7 @@ const MESH_SURFACED_PREVIEW_MAX_CHARS = 512;
109
109
  // coordinator-facing payload that replaces a "go call mesh_read_chat" instruction —
110
110
  // it should carry enough of the worker's result to act on without a second round-trip,
111
111
  // while still bounding what is written into the coordinator PTY.
112
- const MESH_COMPLETION_SURFACE_MAX_CHARS = 4000;
112
+ const MESH_COMPLETION_SURFACE_MAX_CHARS = 16000;
113
113
 
114
114
  /**
115
115
  * The worker's final assistant text carried on a completion event — read from
@@ -58,10 +58,10 @@ import {
58
58
  import { readNonEmptyString, readMeshCompletionSummary } from './mesh-events-utils.js';
59
59
  import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
60
60
  import { expandDaemonIdForms, daemonIdsEquivalent } from '@adhdev/mesh-shared';
61
- import { getActiveDirectDispatches, getQueue, reclaimStrandedAssignedTask } from './mesh-work-queue.js';
61
+ import { getActiveDirectDispatches, getQueue, reclaimStrandedAssignedTask, updateTaskStatus } from './mesh-work-queue.js';
62
62
  import { readLedgerEntries } from './mesh-ledger.js';
63
63
  import { pruneStaleDirectDispatches } from './mesh-active-work.js';
64
- import { reconcileDirectDispatchCompletionFromTranscript } from './mesh-events-stale.js';
64
+ import { findTerminalLedgerEvidenceForTask, reconcileDirectDispatchCompletionFromTranscript } from './mesh-events-stale.js';
65
65
  import { extractFinalAssistantSummaryEvidence } from '../providers/chat-message-normalization.js';
66
66
  import type { ChatMessage } from '../types.js';
67
67
 
@@ -164,7 +164,12 @@ function daemonHostsMesh(mesh: LocalMeshEntry, daemonIds: string[]): boolean {
164
164
  const hostDaemonId = readNonEmptyString(host.hostDaemonId);
165
165
  // Host role but no pinned hostDaemonId → treat as host (single-daemon / legacy).
166
166
  if (!hostDaemonId) return true;
167
- return daemonIds.includes(hostDaemonId);
167
+ return daemonIdListIncludes(daemonIds, hostDaemonId);
168
+ }
169
+
170
+ function daemonIdListIncludes(ids: readonly string[], id: string | undefined): boolean {
171
+ if (!id) return false;
172
+ return ids.some(candidate => candidate === id || daemonIdsEquivalent(candidate, id));
168
173
  }
169
174
 
170
175
  // Resolve EVERY id-form this daemon answers to FOR A GIVEN MESH: the runtime drain
@@ -183,8 +188,8 @@ function resolveCoordinatorSelfIds(mesh: LocalMeshEntry, drainDaemonIds: string[
183
188
  for (const node of mesh.nodes) {
184
189
  const nodeDaemonId = readNonEmptyString(node.daemonId);
185
190
  const nodeMachineId = readNonEmptyString(node.machineId);
186
- const isSelf = (nodeDaemonId && drainDaemonIds.includes(nodeDaemonId))
187
- || (nodeMachineId && drainDaemonIds.includes(nodeMachineId));
191
+ const isSelf = (nodeDaemonId && daemonIdListIncludes(drainDaemonIds, nodeDaemonId))
192
+ || (nodeMachineId && daemonIdListIncludes(drainDaemonIds, nodeMachineId));
188
193
  if (!isSelf) continue;
189
194
  if (nodeDaemonId) ids.add(nodeDaemonId);
190
195
  if (nodeMachineId) ids.add(nodeMachineId);
@@ -196,7 +201,7 @@ function resolveCoordinatorSelfIds(mesh: LocalMeshEntry, drainDaemonIds: string[
196
201
  // this daemon does not make this daemon the host; daemonHostsMesh still honours a
197
202
  // foreign hostDaemonId and rejects ownership.
198
203
  const hostDaemonId = readNonEmptyString(mesh.meshHost?.hostDaemonId);
199
- if (hostDaemonId && ids.has(hostDaemonId)) ids.add(hostDaemonId);
204
+ if (hostDaemonId && daemonIdListIncludes([...ids], hostDaemonId)) ids.add(hostDaemonId);
200
205
  return [...ids];
201
206
  }
202
207
 
@@ -358,6 +363,23 @@ function recoverStrandedAssignedDispatches(meshId: string, store: MeshRuntimeSto
358
363
  const dispatchedAtMs = Date.parse(row.dispatchTimestamp ?? '');
359
364
  if (!Number.isFinite(dispatchedAtMs)) continue; // no dispatch ts → can't age it
360
365
  if (nowMs - dispatchedAtMs < ASSIGNED_STRANDED_DEADLINE_MS) continue; // still in confirm window
366
+ const terminal = findTerminalLedgerEvidenceForTask({
367
+ meshId,
368
+ taskId: row.id,
369
+ });
370
+ if (terminal) {
371
+ const status = terminal.kind === 'task_completed' ? 'completed' : 'failed';
372
+ updateTaskStatus(meshId, row.id, status);
373
+ LOG.warn('MeshReconcile', `Skipped stranded reclaim redispatch for terminal task ${row.id} on mesh ${meshId}; ${terminal.kind} ledger evidence already exists`);
374
+ traceMeshEventDrop('assigned_stranded_terminal_ledger', {
375
+ taskId: row.id,
376
+ sessionId: row.assignedSessionId,
377
+ nodeId: row.assignedNodeId,
378
+ meshId,
379
+ event: 'agent:generating_completed',
380
+ }, terminal.kind);
381
+ continue;
382
+ }
361
383
  if (store.taskHasConfirmedDelivery(meshId, row.id)) continue; // dispatched → PHASE 4's job
362
384
  const reclaimed = reclaimStrandedAssignedTask(meshId, row.id, {
363
385
  reason: 'assigned_stranded_dispatch_unconfirmed',
@@ -896,7 +918,7 @@ async function pullRemoteNodeQueues(
896
918
  // from ourselves over P2P is both wasteful and a self-dispatch hazard.
897
919
  if (!nodeDaemonId) continue;
898
920
  if (daemonIdsEquivalent(nodeDaemonId, localDaemonId)) continue;
899
- if (candidateDaemonIds.includes(nodeDaemonId)) continue;
921
+ if (daemonIdListIncludes(candidateDaemonIds, nodeDaemonId)) continue;
900
922
 
901
923
  for (const pendingEventArgs of pulls) {
902
924
  let events: unknown;
@@ -972,7 +994,7 @@ async function reconcileUnterminatedDirectDispatches(
972
994
  // A node is local when it has no daemonId, names this daemon, or actually
973
995
  // has a live instance here. Anything else is reached over P2P.
974
996
  const isLocalNode = !nodeDaemonId
975
- || selfIds.includes(nodeDaemonId)
997
+ || daemonIdListIncludes(selfIds, nodeDaemonId)
976
998
  || daemonIdsEquivalent(nodeDaemonId, localDaemonId)
977
999
  || !!components.instanceManager.getInstance(sessionId);
978
1000
 
@@ -1092,7 +1114,7 @@ async function collectLiveNodesWithSessions(
1092
1114
  return Promise.all(mesh.nodes.map(async (node) => {
1093
1115
  const nodeDaemonId = readNonEmptyString(node.daemonId);
1094
1116
  const isLocalNode = !nodeDaemonId
1095
- || selfIds.includes(nodeDaemonId)
1117
+ || daemonIdListIncludes(selfIds, nodeDaemonId)
1096
1118
  || daemonIdsEquivalent(nodeDaemonId, localDaemonId);
1097
1119
  let statusResult: unknown;
1098
1120
  try {
@@ -1,7 +1,7 @@
1
1
  import type { ChatMessage } from '../types.js';
2
2
  import { flattenContent } from './contracts.js';
3
3
 
4
- export const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 4_000;
4
+ export const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 16_000;
5
5
 
6
6
  export function extractFinalSummaryFromMessages(
7
7
  messages: ChatMessage[] | null | undefined,