@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.
- package/dist/index.js +67 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +67 -13
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-stale.d.ts +12 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -1
- package/package.json +2 -2
- package/src/commands/med-family/mesh-crud.ts +12 -2
- package/src/mesh/mesh-events-coordinator.ts +19 -0
- package/src/mesh/mesh-events-stale.ts +23 -0
- package/src/mesh/mesh-events-utils.ts +1 -1
- package/src/mesh/mesh-reconcile-loop.ts +31 -9
- package/src/providers/chat-message-normalization.ts +1 -1
|
@@ -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 =
|
|
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.
|
|
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.
|
|
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
|
|
294
|
-
|| (!!selfMachineId && (nodeDaemonId
|
|
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 =
|
|
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
|
|
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
|
|
187
|
-
|| (nodeMachineId && drainDaemonIds
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
export const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 16_000;
|
|
5
5
|
|
|
6
6
|
export function extractFinalSummaryFromMessages(
|
|
7
7
|
messages: ChatMessage[] | null | undefined,
|