@adhdev/daemon-core 0.9.82-rc.257 → 0.9.82-rc.259
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/boot/daemon-lifecycle.d.ts +3 -0
- package/dist/index.js +293 -51
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +293 -51
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-coordinator.d.ts +22 -0
- package/dist/mesh/mesh-events-pending.d.ts +7 -0
- package/dist/mesh/mesh-events-utils.d.ts +2 -0
- package/dist/mesh/mesh-events.d.ts +2 -2
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +3 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +8 -0
- package/src/commands/router.ts +28 -11
- package/src/mesh/mesh-events-coordinator.ts +110 -69
- package/src/mesh/mesh-events-pending.ts +57 -3
- package/src/mesh/mesh-events-utils.ts +20 -0
- package/src/mesh/mesh-events.ts +2 -0
- package/src/mesh/mesh-ledger.ts +1 -0
- package/src/mesh/mesh-routing.ts +272 -0
- package/src/mesh/mesh-runtime-store.ts +37 -0
|
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto';
|
|
|
4
4
|
import { LOG } from '../logging/logger.js';
|
|
5
5
|
import { getLedgerDir, readLedgerEntries } from './mesh-ledger.js';
|
|
6
6
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
7
|
-
import { buildMeshSystemMessage, readNonEmptyString, readRecord, resolveEventSessionId } from './mesh-events-utils.js';
|
|
7
|
+
import { buildMeshSystemMessage, canonicalDaemonId, readNonEmptyString, readRecord, resolveEventSessionId } from './mesh-events-utils.js';
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
|
|
@@ -83,6 +83,47 @@ export function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent)
|
|
|
83
83
|
].join('::');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// R3: TTL for the direct-delivered marker. A coordinator polls get_pending_mesh_events
|
|
87
|
+
// well within this window after a terminal event; after it expires the marker is swept and
|
|
88
|
+
// a late/duplicate drain would (harmlessly) re-surface — but by then the event is long gone
|
|
89
|
+
// from the queue too. 10 minutes mirrors the completion-fingerprint TTL.
|
|
90
|
+
const DIRECT_DELIVERED_TTL_MS = 10 * 60 * 1000;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* R3: record that an event was direct-injected into a live coordinator on `coordinatorDaemonId`.
|
|
94
|
+
* That coordinator's own drain (get_pending_mesh_events with the same coordinatorDaemonId) will
|
|
95
|
+
* skip the queued copy, so it receives the event exactly once instead of twice (PTY + poll).
|
|
96
|
+
* Other consumers (unscoped drainers, other daemons) are unaffected — they did not get the inject.
|
|
97
|
+
*/
|
|
98
|
+
export function markMeshCoordinatorEventDirectDelivered(
|
|
99
|
+
coordinatorDaemonId: string,
|
|
100
|
+
event: PendingMeshCoordinatorEvent,
|
|
101
|
+
): void {
|
|
102
|
+
// Canonicalize so the raw machineId used at inject time and the prefixed instanceId the
|
|
103
|
+
// coordinator drains with resolve to the same key.
|
|
104
|
+
const canonical = canonicalDaemonId(coordinatorDaemonId);
|
|
105
|
+
if (!canonical) return;
|
|
106
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
107
|
+
if (!fingerprint.trim()) return;
|
|
108
|
+
try {
|
|
109
|
+
const store = MeshRuntimeStore.getInstance();
|
|
110
|
+
store.recordDirectDelivered(canonical, fingerprint, DIRECT_DELIVERED_TTL_MS);
|
|
111
|
+
store.sweepExpiredDirectDelivered();
|
|
112
|
+
} catch { /* best-effort — a duplicate is preferable to a crash */ }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function wasDirectDeliveredToCoordinator(coordinatorDaemonId: string, event: PendingMeshCoordinatorEvent): boolean {
|
|
116
|
+
const canonical = canonicalDaemonId(coordinatorDaemonId);
|
|
117
|
+
if (!canonical) return false;
|
|
118
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
119
|
+
if (!fingerprint.trim()) return false;
|
|
120
|
+
try {
|
|
121
|
+
return MeshRuntimeStore.getInstance().wasDirectDelivered(canonical, fingerprint);
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
86
127
|
export function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
87
128
|
const fingerprint = buildPendingEventFingerprint(event);
|
|
88
129
|
if (!fingerprint.trim()) return false;
|
|
@@ -321,7 +362,14 @@ export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDa
|
|
|
321
362
|
for (const event of filtered) pushUnique(event);
|
|
322
363
|
}
|
|
323
364
|
if (merged.length === 0) return [];
|
|
324
|
-
|
|
365
|
+
// R3: when this drain is scoped to a coordinator daemon, exclude events that were already
|
|
366
|
+
// direct-injected into that coordinator's live CLI session. Unscoped drains (no daemon id)
|
|
367
|
+
// keep everything — they belong to consumers that never received the direct inject.
|
|
368
|
+
const deliverable = coordinatorDaemonId
|
|
369
|
+
? merged.filter(event => !wasDirectDeliveredToCoordinator(coordinatorDaemonId, event))
|
|
370
|
+
: merged;
|
|
371
|
+
if (deliverable.length === 0) return [];
|
|
372
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, deliverable);
|
|
325
373
|
}
|
|
326
374
|
|
|
327
375
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
@@ -356,7 +404,13 @@ export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaem
|
|
|
356
404
|
pushUnique(event);
|
|
357
405
|
}
|
|
358
406
|
|
|
359
|
-
|
|
407
|
+
// R3: hide events already direct-delivered to this coordinator from its status peek, so
|
|
408
|
+
// mesh_status doesn't report a "pending" event the coordinator has in fact already received.
|
|
409
|
+
const deliverable = coordinatorDaemonId
|
|
410
|
+
? merged.filter(event => !wasDirectDeliveredToCoordinator(coordinatorDaemonId, event))
|
|
411
|
+
: merged;
|
|
412
|
+
|
|
413
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, deliverable);
|
|
360
414
|
}
|
|
361
415
|
|
|
362
416
|
/**
|
|
@@ -10,6 +10,26 @@ export function readRecord(value: unknown): Record<string, unknown> | undefined
|
|
|
10
10
|
: undefined;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// A single daemon is identified three ways across the system: the raw machineId
|
|
14
|
+
// (`mach_X`, what loadConfig().machineId returns and what the core forwarder uses as
|
|
15
|
+
// localDaemonId), the cloud daemon id (`daemon_mach_X`), and the standalone daemon id
|
|
16
|
+
// (`standalone_mach_X`, the runtime instanceId the MCP coordinator reports as
|
|
17
|
+
// ctx.localDaemonId). Worker envelope stamps (meshCoordinatorDaemonId) and coordinator
|
|
18
|
+
// drains use the prefixed forms, while the forwarder compares against the raw id — so a
|
|
19
|
+
// naive `===` treats the same daemon as remote and breaks both coordinator matching and
|
|
20
|
+
// the R3 direct-delivered dedup. Canonicalize to the raw id before comparing.
|
|
21
|
+
export function canonicalDaemonId(value: unknown): string {
|
|
22
|
+
const id = readNonEmptyString(value);
|
|
23
|
+
if (!id) return '';
|
|
24
|
+
return id.replace(/^(?:daemon|standalone)_/, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sameDaemonId(a: unknown, b: unknown): boolean {
|
|
28
|
+
const ca = canonicalDaemonId(a);
|
|
29
|
+
const cb = canonicalDaemonId(b);
|
|
30
|
+
return ca !== '' && ca === cb;
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
export function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
14
34
|
return readNonEmptyString(event.targetSessionId)
|
|
15
35
|
|| readNonEmptyString(event.sessionId)
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -11,6 +11,7 @@ export {
|
|
|
11
11
|
drainPendingMeshCoordinatorEvents,
|
|
12
12
|
getPendingMeshCoordinatorEvents,
|
|
13
13
|
clearPendingMeshCoordinatorEvents,
|
|
14
|
+
markMeshCoordinatorEventDirectDelivered,
|
|
14
15
|
} from './mesh-events-pending.js';
|
|
15
16
|
|
|
16
17
|
export {
|
|
@@ -25,4 +26,5 @@ export {
|
|
|
25
26
|
setupMeshEventForwarding,
|
|
26
27
|
isMeshCoordinatorEvent,
|
|
27
28
|
__resetIdleAutoFastForwardForTests,
|
|
29
|
+
__resetMeshWorkspaceCacheForTests,
|
|
28
30
|
} from './mesh-events-coordinator.js';
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
|
+
import { getActiveDirectDispatches } from './mesh-work-queue.js';
|
|
3
|
+
import { hasUnterminalDirectDispatchLedgerEntry } from './mesh-events-stale.js';
|
|
4
|
+
import { appendLedgerEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
5
|
+
import { LOG } from '../logging/logger.js';
|
|
6
|
+
import { readNonEmptyString } from './mesh-events-utils.js';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// R1: single-source coordinator routing resolution
|
|
10
|
+
//
|
|
11
|
+
// Worker→node→mesh→coordinator routing used to be reconstructed inline inside
|
|
12
|
+
// setupMeshEventForwarding by reading six settings "stamp" fields
|
|
13
|
+
// (meshNodeFor / meshNodeId / meshCoordinatorFor / meshCoordinatorDaemonId /
|
|
14
|
+
// meshCoordinatorNodeId / launchedByCoordinator) plus two ledger queries plus a
|
|
15
|
+
// workspace fallback, scattered across ~50 lines. Any missing stamp silently
|
|
16
|
+
// changed the routing decision (delegate dropped, mesh unresolved) — design
|
|
17
|
+
// vulnerability #2.
|
|
18
|
+
//
|
|
19
|
+
// resolveWorkerDelegateRouting() is the ONE place that interprets those inputs.
|
|
20
|
+
// The stamps are inputs *internal to this function*; no external branch reads
|
|
21
|
+
// them to make a routing decision. The forwarder consumes the typed result only.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type WorkerDelegateRejectionReason =
|
|
25
|
+
| 'not_cli'
|
|
26
|
+
| 'no_workspace'
|
|
27
|
+
| 'no_worker_envelope'
|
|
28
|
+
| 'coordinator_not_dispatch_target'
|
|
29
|
+
| 'mesh_unresolved';
|
|
30
|
+
|
|
31
|
+
export interface WorkerDelegateRouting {
|
|
32
|
+
/** True when the source session is a mesh worker whose events must route to a coordinator. */
|
|
33
|
+
isDelegate: boolean;
|
|
34
|
+
/** Resolved mesh id (runtime stamp, direct-dispatch recovery, or workspace lookup). */
|
|
35
|
+
meshId: string;
|
|
36
|
+
/** Resolved node id within the mesh, or '' when it cannot be determined. */
|
|
37
|
+
nodeId: string;
|
|
38
|
+
/** Human-readable node label for the coordinator system message. */
|
|
39
|
+
nodeLabel: string;
|
|
40
|
+
/** Coordinator daemon anchor (prefixed id) when the worker carries one; '' otherwise. */
|
|
41
|
+
coordinatorDaemonId: string;
|
|
42
|
+
/** Source session workspace (best-effort; '' when the session/workspace is missing). */
|
|
43
|
+
workspace: string;
|
|
44
|
+
/** Source session id echoed back, so diagnostics can name the dropped event's origin. */
|
|
45
|
+
sessionId: string;
|
|
46
|
+
/** Why routing was rejected (isDelegate=false), for fail-loud diagnostics (R4). */
|
|
47
|
+
rejectionReason?: WorkerDelegateRejectionReason;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ResolveDeps {
|
|
51
|
+
/** Resolve a mesh by id (local config + inline cache). */
|
|
52
|
+
getMeshById: (meshId: string) => any | undefined;
|
|
53
|
+
/** Resolve a mesh by workspace path (cached repo lookup). */
|
|
54
|
+
getMeshByWorkspace: (workspace: string) => any | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readSettings(state: any): Record<string, unknown> {
|
|
58
|
+
return state?.settings && typeof state.settings === 'object'
|
|
59
|
+
? state.settings as Record<string, unknown>
|
|
60
|
+
: {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve how a source CLI session's mesh events should route to a coordinator.
|
|
65
|
+
*
|
|
66
|
+
* This is the single authoritative interpretation of the worker envelope. It folds
|
|
67
|
+
* together every signal that previously lived in inline branches:
|
|
68
|
+
* - runtime stamps (meshNodeFor / meshNodeId)
|
|
69
|
+
* - the coordinator anchor (meshCoordinatorDaemonId) and launch marker (launchedByCoordinator,
|
|
70
|
+
* meshCoordinatorNodeId) — any one proves delegation even if the node/mesh stamp was dropped
|
|
71
|
+
* - direct-dispatch recovery: a coordinator session that is itself a dispatch target
|
|
72
|
+
* - workspace→mesh fallback when no runtime mesh id survived
|
|
73
|
+
*
|
|
74
|
+
* Returns isDelegate=false (with a rejectionReason) instead of throwing, so the caller
|
|
75
|
+
* can either ignore non-delegate events or surface a diagnostic.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveWorkerDelegateRouting(
|
|
78
|
+
components: DaemonComponents,
|
|
79
|
+
instanceId: string,
|
|
80
|
+
deps: ResolveDeps,
|
|
81
|
+
): WorkerDelegateRouting {
|
|
82
|
+
const sessionId = readNonEmptyString(instanceId);
|
|
83
|
+
let workspace = '';
|
|
84
|
+
let coordinatorDaemonId = '';
|
|
85
|
+
const reject = (rejectionReason: WorkerDelegateRejectionReason): WorkerDelegateRouting => ({
|
|
86
|
+
isDelegate: false,
|
|
87
|
+
meshId: '',
|
|
88
|
+
nodeId: '',
|
|
89
|
+
nodeLabel: '',
|
|
90
|
+
coordinatorDaemonId,
|
|
91
|
+
workspace,
|
|
92
|
+
sessionId,
|
|
93
|
+
rejectionReason,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const sourceInstance = components.instanceManager.getInstance(instanceId);
|
|
97
|
+
if (!sourceInstance || sourceInstance.category !== 'cli') return reject('not_cli');
|
|
98
|
+
|
|
99
|
+
const state = sourceInstance.getState();
|
|
100
|
+
workspace = readNonEmptyString(state.workspace);
|
|
101
|
+
if (!workspace) return reject('no_workspace');
|
|
102
|
+
|
|
103
|
+
const settings = readSettings(state);
|
|
104
|
+
coordinatorDaemonId = readNonEmptyString(settings.meshCoordinatorDaemonId);
|
|
105
|
+
|
|
106
|
+
// A coordinator session (meshCoordinatorFor set) is only treated as a worker delegate
|
|
107
|
+
// when it is itself the target of an active direct dispatch — otherwise its own events
|
|
108
|
+
// must not be routed back to a coordinator (it IS the coordinator).
|
|
109
|
+
const coordinatorMeshId = readNonEmptyString(settings.meshCoordinatorFor);
|
|
110
|
+
let meshIdFromDirectDispatch = '';
|
|
111
|
+
if (coordinatorMeshId) {
|
|
112
|
+
let hasActiveDispatch = false;
|
|
113
|
+
try {
|
|
114
|
+
hasActiveDispatch =
|
|
115
|
+
getActiveDirectDispatches(coordinatorMeshId).some(d => d.sessionId === instanceId)
|
|
116
|
+
|| hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, instanceId);
|
|
117
|
+
} catch { /* best-effort */ }
|
|
118
|
+
if (!hasActiveDispatch) return reject('coordinator_not_dispatch_target');
|
|
119
|
+
meshIdFromDirectDispatch = coordinatorMeshId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor) || meshIdFromDirectDispatch;
|
|
123
|
+
|
|
124
|
+
// Worker-envelope proof of delegation. A worker can arrive carrying only the routing
|
|
125
|
+
// anchor (meshCoordinatorDaemonId) without meshNodeFor — e.g. when the node/mesh stamp
|
|
126
|
+
// was dropped on a direct dispatch or relaunch. Treat any envelope marker as proof and
|
|
127
|
+
// recover the mesh id by workspace when the runtime id is absent.
|
|
128
|
+
const hasWorkerEnvelope = Boolean(
|
|
129
|
+
meshIdFromRuntime
|
|
130
|
+
|| settings.launchedByCoordinator
|
|
131
|
+
|| coordinatorDaemonId
|
|
132
|
+
|| readNonEmptyString(settings.meshCoordinatorNodeId),
|
|
133
|
+
);
|
|
134
|
+
if (!hasWorkerEnvelope) return reject('no_worker_envelope');
|
|
135
|
+
|
|
136
|
+
const mesh = meshIdFromRuntime ? deps.getMeshById(meshIdFromRuntime) : deps.getMeshByWorkspace(workspace);
|
|
137
|
+
const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
|
|
138
|
+
if (!meshId) return reject('mesh_unresolved');
|
|
139
|
+
|
|
140
|
+
const targetNode = mesh?.nodes?.find((n: any) => n.workspace === workspace);
|
|
141
|
+
const runtimeNodeId = readNonEmptyString(settings.meshNodeId);
|
|
142
|
+
const nodeId = readNonEmptyString(targetNode?.id) || runtimeNodeId;
|
|
143
|
+
const nodeLabel = targetNode
|
|
144
|
+
? `Node '${targetNode.id}'`
|
|
145
|
+
: runtimeNodeId
|
|
146
|
+
? `Node '${runtimeNodeId}'`
|
|
147
|
+
: `Agent at ${workspace}`;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
isDelegate: true,
|
|
151
|
+
meshId,
|
|
152
|
+
nodeId,
|
|
153
|
+
nodeLabel,
|
|
154
|
+
coordinatorDaemonId,
|
|
155
|
+
workspace,
|
|
156
|
+
sessionId,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// R4: fail-loud routing diagnostics
|
|
162
|
+
//
|
|
163
|
+
// A rejection is only worth surfacing when the session DID present a worker
|
|
164
|
+
// envelope but routing still couldn't complete (`mesh_unresolved`). Before R4
|
|
165
|
+
// that event was dropped silently — setupMeshEventForwarding returned, the
|
|
166
|
+
// completion never reached a coordinator and never landed in the pending queue,
|
|
167
|
+
// and there was no trace of why. The benign rejections (not_cli / no_workspace /
|
|
168
|
+
// no_worker_envelope / coordinator_not_dispatch_target) are ordinary non-delegate
|
|
169
|
+
// traffic and must NOT spam the ledger.
|
|
170
|
+
//
|
|
171
|
+
// The diagnostic is keyed under a single shared stream id so all unroutable drops
|
|
172
|
+
// across meshes are discoverable in one place (the dropped event has no resolvable
|
|
173
|
+
// mesh by definition). A short per-(session+event) dedup window keeps a chatty
|
|
174
|
+
// session from flooding the ledger.
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
/** Ledger stream that collects delivery_unroutable diagnostics (no real mesh id exists for them). */
|
|
178
|
+
export const UNROUTABLE_DIAGNOSTIC_STREAM = '__unroutable__';
|
|
179
|
+
|
|
180
|
+
const UNROUTABLE_DIAGNOSTIC_DEDUP_MS = 60 * 1000;
|
|
181
|
+
const recentUnroutableDiagnostics = new Map<string, number>();
|
|
182
|
+
|
|
183
|
+
export function __resetUnroutableDiagnosticsForTests(): void {
|
|
184
|
+
recentUnroutableDiagnostics.clear();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** True only for rejections that represent a silently-dropped delegate event worth a diagnostic. */
|
|
188
|
+
export function isUnroutableDelegateRejection(routing: WorkerDelegateRouting): boolean {
|
|
189
|
+
return !routing.isDelegate && routing.rejectionReason === 'mesh_unresolved';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* R4: record that a worker event with a valid envelope could not be routed to a coordinator.
|
|
194
|
+
* Writes a `delivery_unroutable` ledger entry (deduped within a short window) so operators see
|
|
195
|
+
* WHY a completion never arrived instead of it vanishing. No-op for non-diagnostic rejections.
|
|
196
|
+
*/
|
|
197
|
+
export function recordUnroutableDelegateEvent(routing: WorkerDelegateRouting, eventName: string): boolean {
|
|
198
|
+
if (!isUnroutableDelegateRejection(routing)) return false;
|
|
199
|
+
|
|
200
|
+
const dedupKey = `${routing.sessionId}::${eventName}::${routing.workspace}`;
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
const last = recentUnroutableDiagnostics.get(dedupKey);
|
|
203
|
+
if (last !== undefined && now - last < UNROUTABLE_DIAGNOSTIC_DEDUP_MS) return false;
|
|
204
|
+
recentUnroutableDiagnostics.set(dedupKey, now);
|
|
205
|
+
// Opportunistic sweep so the map can't grow unbounded.
|
|
206
|
+
if (recentUnroutableDiagnostics.size > 256) {
|
|
207
|
+
for (const [key, ts] of recentUnroutableDiagnostics) {
|
|
208
|
+
if (now - ts >= UNROUTABLE_DIAGNOSTIC_DEDUP_MS) recentUnroutableDiagnostics.delete(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
appendLedgerEntry(UNROUTABLE_DIAGNOSTIC_STREAM, {
|
|
214
|
+
kind: 'delivery_unroutable',
|
|
215
|
+
sessionId: routing.sessionId || undefined,
|
|
216
|
+
payload: {
|
|
217
|
+
event: eventName,
|
|
218
|
+
reason: routing.rejectionReason,
|
|
219
|
+
workspace: routing.workspace || undefined,
|
|
220
|
+
coordinatorDaemonId: routing.coordinatorDaemonId || undefined,
|
|
221
|
+
detail: 'Worker envelope was present but no mesh could be resolved; the event could not be routed to a coordinator.',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
LOG.warn('MeshEvents', `delivery_unroutable: ${eventName} from session ${routing.sessionId || '(unknown)'} at ${routing.workspace || '(no workspace)'} — envelope present but mesh unresolved`);
|
|
225
|
+
return true;
|
|
226
|
+
} catch (e: any) {
|
|
227
|
+
LOG.warn('MeshEvents', `Failed to record delivery_unroutable diagnostic: ${e?.message || e}`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface UnroutableDeliveryDiagnostic {
|
|
233
|
+
timestamp: string;
|
|
234
|
+
event: string;
|
|
235
|
+
sessionId?: string;
|
|
236
|
+
workspace?: string;
|
|
237
|
+
coordinatorDaemonId?: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* R4 visibility: recent delivery_unroutable diagnostics, newest first. Surfaced in mesh_status
|
|
242
|
+
* so an operator/coordinator can see that completions were dropped (envelope present, mesh
|
|
243
|
+
* unresolved) rather than the drops being invisible in a ledger nobody reads. The diagnostics
|
|
244
|
+
* live in a single shared stream because an unroutable event has no resolvable mesh.
|
|
245
|
+
*/
|
|
246
|
+
export function getRecentUnroutableDeliveries(opts?: { sinceMs?: number; limit?: number }): UnroutableDeliveryDiagnostic[] {
|
|
247
|
+
const sinceMs = opts?.sinceMs ?? 60 * 60 * 1000; // last hour by default
|
|
248
|
+
const limit = opts?.limit ?? 20;
|
|
249
|
+
let entries: ReturnType<typeof readLedgerEntries>;
|
|
250
|
+
try {
|
|
251
|
+
entries = readLedgerEntries(UNROUTABLE_DIAGNOSTIC_STREAM, { kind: ['delivery_unroutable'], tail: 200 });
|
|
252
|
+
} catch {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
const cutoff = Date.now() - sinceMs;
|
|
256
|
+
const out: UnroutableDeliveryDiagnostic[] = [];
|
|
257
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
258
|
+
const entry = entries[i];
|
|
259
|
+
const ts = new Date(entry.timestamp).getTime();
|
|
260
|
+
if (!Number.isNaN(ts) && ts < cutoff) continue;
|
|
261
|
+
const payload = entry.payload && typeof entry.payload === 'object' ? entry.payload as Record<string, unknown> : {};
|
|
262
|
+
out.push({
|
|
263
|
+
timestamp: entry.timestamp,
|
|
264
|
+
event: readNonEmptyString(payload.event),
|
|
265
|
+
sessionId: readNonEmptyString(entry.sessionId) || readNonEmptyString(payload.sessionId) || undefined,
|
|
266
|
+
workspace: readNonEmptyString(payload.workspace) || undefined,
|
|
267
|
+
coordinatorDaemonId: readNonEmptyString(payload.coordinatorDaemonId) || undefined,
|
|
268
|
+
});
|
|
269
|
+
if (out.length >= limit) break;
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
@@ -117,6 +117,21 @@ export class MeshRuntimeStore {
|
|
|
117
117
|
expires_at INTEGER NOT NULL
|
|
118
118
|
);
|
|
119
119
|
|
|
120
|
+
-- R3: idempotent coordinator inbox. When a terminal/force-inject event is
|
|
121
|
+
-- direct-injected into a LIVE local CLI coordinator (coord.onEvent('send_message')),
|
|
122
|
+
-- we record (coordinator_daemon_id, fingerprint) here. That same coordinator also
|
|
123
|
+
-- polls get_pending_mesh_events, which would re-deliver the queued copy of the very
|
|
124
|
+
-- event it just received in its PTY → user sees the completion twice. The drain for
|
|
125
|
+
-- a coordinator daemon filters out events already direct-delivered to it, giving
|
|
126
|
+
-- exactly-once-per-coordinator while keeping the queue for other consumers (idle /
|
|
127
|
+
-- MCP-only / remote) that did NOT receive the direct inject.
|
|
128
|
+
CREATE TABLE IF NOT EXISTS mesh_direct_delivered_events (
|
|
129
|
+
coordinator_daemon_id TEXT NOT NULL,
|
|
130
|
+
fingerprint TEXT NOT NULL,
|
|
131
|
+
expires_at INTEGER NOT NULL,
|
|
132
|
+
PRIMARY KEY (coordinator_daemon_id, fingerprint)
|
|
133
|
+
);
|
|
134
|
+
|
|
120
135
|
CREATE TABLE IF NOT EXISTS mesh_direct_dispatches (
|
|
121
136
|
task_id TEXT PRIMARY KEY,
|
|
122
137
|
mesh_id TEXT NOT NULL,
|
|
@@ -281,6 +296,28 @@ export class MeshRuntimeStore {
|
|
|
281
296
|
this.db.prepare('DELETE FROM mesh_completion_fingerprints WHERE expires_at <= ?').run(Date.now());
|
|
282
297
|
}
|
|
283
298
|
|
|
299
|
+
// R3: record that an event (by pending-event fingerprint) was direct-injected into a live
|
|
300
|
+
// coordinator on the given daemon, so that coordinator's own drain skips the queued copy.
|
|
301
|
+
recordDirectDelivered(coordinatorDaemonId: string, fingerprint: string, ttlMs: number): void {
|
|
302
|
+
if (!coordinatorDaemonId || !fingerprint) return;
|
|
303
|
+
this.db.prepare(
|
|
304
|
+
'INSERT OR REPLACE INTO mesh_direct_delivered_events (coordinator_daemon_id, fingerprint, expires_at) VALUES (?, ?, ?)'
|
|
305
|
+
).run(coordinatorDaemonId, fingerprint, Date.now() + ttlMs);
|
|
306
|
+
this.maybeCheckpointWal();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
wasDirectDelivered(coordinatorDaemonId: string, fingerprint: string): boolean {
|
|
310
|
+
if (!coordinatorDaemonId || !fingerprint) return false;
|
|
311
|
+
const row = this.db.prepare(
|
|
312
|
+
'SELECT 1 FROM mesh_direct_delivered_events WHERE coordinator_daemon_id = ? AND fingerprint = ? AND expires_at > ?'
|
|
313
|
+
).get(coordinatorDaemonId, fingerprint, Date.now());
|
|
314
|
+
return row !== undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
sweepExpiredDirectDelivered(): void {
|
|
318
|
+
this.db.prepare('DELETE FROM mesh_direct_delivered_events WHERE expires_at <= ?').run(Date.now());
|
|
319
|
+
}
|
|
320
|
+
|
|
284
321
|
private maybeCheckpointWal(): void {
|
|
285
322
|
if (++this.walWriteCounter < MeshRuntimeStore.WAL_CHECK_INTERVAL) return;
|
|
286
323
|
this.walWriteCounter = 0;
|