@adhdev/daemon-core 0.9.82-rc.354 → 0.9.82-rc.355
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 +571 -202
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +571 -202
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-runtime-store.d.ts +1 -1
- package/dist/mesh/mesh-work-queue.d.ts +1 -1
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/dist/providers/spec/adapter.d.ts +22 -0
- package/dist/providers/spec/fsm-driver.d.ts +49 -7
- package/dist/providers/spec/fsm-evaluator.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +9 -5
- package/package.json +2 -2
- package/src/commands/router.ts +19 -6
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +117 -12
- package/src/mesh/mesh-events-pending.ts +33 -0
- package/src/mesh/mesh-events-stale.ts +3 -1
- package/src/mesh/mesh-reconcile-loop.ts +47 -0
- package/src/mesh/mesh-runtime-store.ts +18 -2
- package/src/mesh/mesh-work-queue.ts +8 -1
- package/src/providers/cli-provider-instance.ts +69 -4
- package/src/providers/spec/adapter.ts +67 -0
- package/src/providers/spec/cli-adapter.ts +6 -0
- package/src/providers/spec/evaluator.ts +24 -9
- package/src/providers/spec/fsm-driver.ts +135 -13
- package/src/providers/spec/fsm-evaluator.ts +19 -2
- package/src/providers/spec/types.ts +9 -5
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface MeshEventTraceCtx {
|
|
2
|
+
/** Primary correlation anchor — the mesh task id (meshActiveTaskId / metadataEvent.taskId). */
|
|
3
|
+
taskId?: unknown;
|
|
4
|
+
/** Optional per-event id when the producer assigns one. */
|
|
5
|
+
eventId?: unknown;
|
|
6
|
+
/** Worker session id — the fallback anchor when no task is attached. */
|
|
7
|
+
sessionId?: unknown;
|
|
8
|
+
nodeId?: unknown;
|
|
9
|
+
meshId?: unknown;
|
|
10
|
+
event?: unknown;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Stable, greppable correlation key. `task=` and `sess=` are ALWAYS rendered (as `-`
|
|
14
|
+
* when absent) so the key shape is uniform across stages and a single grep alternation
|
|
15
|
+
* (`task=<id>\|sess=<id>`) follows the event end-to-end.
|
|
16
|
+
*/
|
|
17
|
+
export declare function meshEventTraceKey(ctx: MeshEventTraceCtx): string;
|
|
18
|
+
/** Lifecycle progress (INFO). One line per stage the event clears. */
|
|
19
|
+
export declare function traceMeshEventStage(stage: string, ctx: MeshEventTraceCtx, detail?: string): void;
|
|
20
|
+
/** Event did not advance — rejected / held / skipped / deduped (WARN). */
|
|
21
|
+
export declare function traceMeshEventDrop(reason: string, ctx: MeshEventTraceCtx, detail?: string): void;
|
|
@@ -129,7 +129,7 @@ export declare class MeshRuntimeStore {
|
|
|
129
129
|
dispatchedAt: string;
|
|
130
130
|
updatedAt: string;
|
|
131
131
|
}>;
|
|
132
|
-
updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale'): void;
|
|
132
|
+
updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale', taskId?: string): void;
|
|
133
133
|
cleanupTerminalDirectDispatches(olderThanMs: number): void;
|
|
134
134
|
deleteDirectDispatches(meshId: string): void;
|
|
135
135
|
/**
|
|
@@ -320,7 +320,7 @@ export declare function insertDirectDispatch(meshId: string, data: {
|
|
|
320
320
|
dispatchedAt: string;
|
|
321
321
|
}): void;
|
|
322
322
|
export declare function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[];
|
|
323
|
-
export declare function updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale'): void;
|
|
323
|
+
export declare function updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale', taskId?: string): void;
|
|
324
324
|
export declare function cleanupTerminalDirectDispatches(olderThanMs?: number): void;
|
|
325
325
|
export declare function markStaleDirectDispatches(meshId: string, olderThanMs?: number): void;
|
|
326
326
|
/**
|
|
@@ -184,6 +184,8 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
184
184
|
private shouldSuppressStaleParsedBusyStatus;
|
|
185
185
|
private getCompletedFinalizationBlock;
|
|
186
186
|
private scheduleCompletedDebounceFlush;
|
|
187
|
+
private isMeshWorkerSession;
|
|
188
|
+
private meshTraceCtx;
|
|
187
189
|
private flushCompletedDebounceIfFinalized;
|
|
188
190
|
private maybeAutoApproveStatus;
|
|
189
191
|
/**
|
|
@@ -33,6 +33,21 @@ export interface TerminalAdapterHandlers {
|
|
|
33
33
|
}): void;
|
|
34
34
|
tick?(): void;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* One entry in the PTY input/output event timeline (debug-only). Captured at
|
|
38
|
+
* the single common point every spec@4 provider funnels through — this adapter
|
|
39
|
+
* — so the Spec Debug Snapshot can answer "what input / output preceded a status
|
|
40
|
+
* transition?". Observation only; nothing here feeds the FSM decision.
|
|
41
|
+
*/
|
|
42
|
+
export interface SpecPtyEvent {
|
|
43
|
+
/** Wall-clock ms. */
|
|
44
|
+
ts: number;
|
|
45
|
+
kind: 'spawn' | 'input' | 'output' | 'resize' | 'cursor' | 'exit';
|
|
46
|
+
/** Human-readable, control-char-escaped, length-capped preview. */
|
|
47
|
+
content: string;
|
|
48
|
+
/** Raw byte length before truncation (output/input only). */
|
|
49
|
+
bytes?: number;
|
|
50
|
+
}
|
|
36
51
|
export declare class TerminalAdapter {
|
|
37
52
|
private readonly opts;
|
|
38
53
|
private readonly handlers;
|
|
@@ -46,6 +61,9 @@ export declare class TerminalAdapter {
|
|
|
46
61
|
private screenTimer;
|
|
47
62
|
private tickTimer;
|
|
48
63
|
private lastScreen;
|
|
64
|
+
/** Debug-only ring buffer of PTY input/output/resize/cursor events. */
|
|
65
|
+
private events;
|
|
66
|
+
private lastCursorKey;
|
|
49
67
|
constructor(opts: TerminalAdapterOpts, handlers: TerminalAdapterHandlers);
|
|
50
68
|
start(): void;
|
|
51
69
|
resize(cols: number, rows: number): void;
|
|
@@ -62,6 +80,10 @@ export declare class TerminalAdapter {
|
|
|
62
80
|
col: number;
|
|
63
81
|
};
|
|
64
82
|
send_keys(text: string): void;
|
|
83
|
+
/** Debug-only: most-recent PTY input/output/resize/cursor events, oldest
|
|
84
|
+
* first. Pure observation — never consulted by the FSM. */
|
|
85
|
+
getEventTimeline(limit?: number): SpecPtyEvent[];
|
|
86
|
+
private recordEvent;
|
|
65
87
|
kill(): void;
|
|
66
88
|
private onChunk;
|
|
67
89
|
private computeScreen;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SpecPtyEvent } from './adapter.js';
|
|
1
2
|
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
2
3
|
import { type TraceEntry } from './evaluator.js';
|
|
3
4
|
import { type TransitionEval } from './fsm-evaluator.js';
|
|
@@ -139,6 +140,7 @@ export interface ISpecDriver {
|
|
|
139
140
|
} | null;
|
|
140
141
|
getFsmDebug?(): unknown;
|
|
141
142
|
getFsmSnapshotHistory?(): ReadonlyArray<FsmSnapshotEntry>;
|
|
143
|
+
getEventTimeline?(limit?: number): ReadonlyArray<SpecPtyEvent>;
|
|
142
144
|
}
|
|
143
145
|
export interface SpecDriverOpts {
|
|
144
146
|
specPath: string;
|
|
@@ -152,6 +154,10 @@ export interface SpecDriverOpts {
|
|
|
152
154
|
extraCliArgs?: string[];
|
|
153
155
|
}
|
|
154
156
|
export declare function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number;
|
|
157
|
+
/** Split `text` into chunks of at most `size` UTF-16 units without ever cutting
|
|
158
|
+
* between a high and low surrogate (which would corrupt an astral char — emoji,
|
|
159
|
+
* etc. — on the UTF-8 PTY write). */
|
|
160
|
+
export declare function chunkPreservingSurrogates(text: string, size: number): string[];
|
|
155
161
|
export declare function guessExt(mime: string): string;
|
|
156
162
|
type HistoryEntry = DriverHistoryEntry;
|
|
157
163
|
export declare class FsmDriver implements ISpecDriver {
|
|
@@ -181,6 +187,16 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
181
187
|
* WIN32_SUBMIT_* and scheduleWin32Submit). Re-arms itself until the FSM
|
|
182
188
|
* leaves idle (submitted) or the resend budget is spent. */
|
|
183
189
|
private win32SubmitTimer;
|
|
190
|
+
/** Wall-clock (ms) of the most recent raw PTY output chunk. Advances on every
|
|
191
|
+
* on_pty_data — including the echo of text written into the composer — so the
|
|
192
|
+
* win32 submit settle-gate can tell when input has finished landing. */
|
|
193
|
+
private lastPtyDataAt;
|
|
194
|
+
/** Wall-clock (ms) of the most recent win32 message-body input write. Bridges
|
|
195
|
+
* the gap between writing a chunk and its echo so the settle-gate does not
|
|
196
|
+
* declare "quiet" mid-write. */
|
|
197
|
+
private lastWin32WriteAt;
|
|
198
|
+
/** Pending paced chunk-write timer for a large win32 body (see writeWin32Body). */
|
|
199
|
+
private win32WriteTimer;
|
|
184
200
|
private currentEval;
|
|
185
201
|
private stateHistory;
|
|
186
202
|
private prevStateAt;
|
|
@@ -230,6 +246,8 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
230
246
|
};
|
|
231
247
|
getStateHistory(): ReadonlyArray<HistoryEntry>;
|
|
232
248
|
getFsmSnapshotHistory(): ReadonlyArray<FsmSnapshotEntry>;
|
|
249
|
+
/** Debug-only PTY input/output/resize/cursor timeline from the adapter. */
|
|
250
|
+
getEventTimeline(limit?: number): ReadonlyArray<SpecPtyEvent>;
|
|
233
251
|
getSections(): Array<{
|
|
234
252
|
id: string;
|
|
235
253
|
text: string;
|
|
@@ -300,14 +318,38 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
300
318
|
private actuallySendMessage;
|
|
301
319
|
/** The agent's current coarse status, derived from the FSM node we're in. */
|
|
302
320
|
private currentStatus;
|
|
321
|
+
/** Record a win32 body write so the settle-gate counts it as input activity
|
|
322
|
+
* even before the echo arrives. */
|
|
323
|
+
private markWin32Write;
|
|
324
|
+
/** Most recent win32 input activity — a write we issued OR a PTY output chunk
|
|
325
|
+
* (echo). The submit settle-gate waits for this to go quiet. */
|
|
326
|
+
private lastWin32InputActivityAt;
|
|
303
327
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* a
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
|
|
328
|
+
* Write the message body to the PTY for win32, paced into bounded chunks. A
|
|
329
|
+
* single unbounded ConPTY write can overflow the input pipe and drop leading
|
|
330
|
+
* bytes; splitting it with a short inter-chunk gap keeps the console input
|
|
331
|
+
* buffer from overflowing. Small bodies still go out in a single write. Each
|
|
332
|
+
* chunk advances lastWin32WriteAt so the submit settle-gate keeps waiting until
|
|
333
|
+
* the final chunk is out and echoed.
|
|
334
|
+
*/
|
|
335
|
+
private writeWin32Body;
|
|
336
|
+
/**
|
|
337
|
+
* win32 submit. Two phases:
|
|
338
|
+
*
|
|
339
|
+
* Phase 1 (settle-gate): hold the first CR until the PTY output has been quiet
|
|
340
|
+
* for WIN32_SUBMIT_SETTLE_MS after the last input write — i.e. the full
|
|
341
|
+
* (possibly multi-KB / multiline) body has finished arriving in the composer
|
|
342
|
+
* and echoing. Honors an initial minimum delay and is bounded by
|
|
343
|
+
* WIN32_SUBMIT_MAX_SETTLE_WAIT_MS so a noisy screen can never hang the submit.
|
|
344
|
+
* This is what stops a long message from being submitted half-arrived (its
|
|
345
|
+
* leading lines lost). A short message settles almost immediately.
|
|
346
|
+
*
|
|
347
|
+
* Phase 2 (verified resend — unchanged): send the submit key, wait a gap, and
|
|
348
|
+
* if the FSM is still 'idle' (the CR was absorbed as a multiline-paste
|
|
349
|
+
* newline) resend, up to WIN32_SUBMIT_MAX_RESENDS. The first CR always fires
|
|
350
|
+
* (a stale/edge status never suppresses it); resends are gated on still being
|
|
351
|
+
* idle and stop the instant the agent leaves idle (submitted → generating /
|
|
352
|
+
* approval). This preserves the win32 lone-CR-swallow handling.
|
|
311
353
|
*/
|
|
312
354
|
private scheduleWin32Submit;
|
|
313
355
|
private handleClickControl;
|
|
@@ -19,6 +19,10 @@ export interface CondResult {
|
|
|
19
19
|
/** Remaining ms until a time-based condition would flip to true. 0 if
|
|
20
20
|
* already true or not applicable. Lets the UI show a countdown. */
|
|
21
21
|
remainingMs?: number;
|
|
22
|
+
/** Debug-only: the actual substring a TRUE regex condition matched, so the
|
|
23
|
+
* snapshot shows WHAT text the rule fired on — not just which regex. Never
|
|
24
|
+
* read by the FSM; purely for the Spec Debug Snapshot. */
|
|
25
|
+
matchedText?: string;
|
|
22
26
|
children?: CondResult[];
|
|
23
27
|
}
|
|
24
28
|
/** One evaluated transition with its full reasoning. */
|
|
@@ -123,11 +123,15 @@ export interface SectionDef {
|
|
|
123
123
|
until?: string;
|
|
124
124
|
/**
|
|
125
125
|
* Anchor regex(es). A single string anchors on the first/last matching line
|
|
126
|
-
* (per `anchor_last`). An array is an OR-set
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
126
|
+
* (per `anchor_last`). An array is an OR-set of candidate shapes: each
|
|
127
|
+
* candidate resolves its OWN anchor line independently (anchor_last → that
|
|
128
|
+
* candidate's last matching line, else its first), then the TOPMOST resolved
|
|
129
|
+
* line across candidates wins — a section's anchor marks the top of its
|
|
130
|
+
* block, so the highest recognized landmark bounds the whole block. This
|
|
131
|
+
* lets one section capture two shapes — e.g. a box-divider modal AND a
|
|
132
|
+
* divider-less modal whose only stable landmark is the question line above
|
|
133
|
+
* its numbered choices — while preventing a stray LOWER landmark (e.g. an
|
|
134
|
+
* input-box `────` rule below the choices) from clipping the block.
|
|
131
135
|
*/
|
|
132
136
|
anchor?: string | string[];
|
|
133
137
|
anchor_flags?: string;
|
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.355",
|
|
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.355",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
package/src/commands/router.ts
CHANGED
|
@@ -8110,7 +8110,10 @@ export class DaemonCommandRouter {
|
|
|
8110
8110
|
// _meshDirectDispatch prevents re-forwarding (and P2P self-dial) when the stored
|
|
8111
8111
|
// daemonId uses a legacy format that doesn't match the receiving daemon's identity.
|
|
8112
8112
|
const selfDaemonId = this.deps.statusInstanceId;
|
|
8113
|
-
|
|
8113
|
+
// daemonIdsEquivalent: a legacy-form stored daemonId that resolves to THIS
|
|
8114
|
+
// machine's core must be treated as local (not remote) so it is not forwarded /
|
|
8115
|
+
// P2P self-dialed. Equivalent → local.
|
|
8116
|
+
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
8114
8117
|
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
8115
8118
|
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'fast_forward_mesh_node', {
|
|
8116
8119
|
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
@@ -8154,7 +8157,9 @@ export class DaemonCommandRouter {
|
|
|
8154
8157
|
// once the call lands on the owning daemon — that daemon then reads
|
|
8155
8158
|
// its own logs even if the stored daemonId uses a legacy form.
|
|
8156
8159
|
const selfDaemonId = this.deps.statusInstanceId;
|
|
8157
|
-
|
|
8160
|
+
// daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core is
|
|
8161
|
+
// local — read locally instead of forwarding. Equivalent → local.
|
|
8162
|
+
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
8158
8163
|
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
8159
8164
|
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'get_mesh_node_logs', {
|
|
8160
8165
|
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
@@ -8230,7 +8235,9 @@ export class DaemonCommandRouter {
|
|
|
8230
8235
|
const forwardNode = meshRecordForForward?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
8231
8236
|
const nodeDaemonId = typeof forwardNode?.daemonId === 'string' ? forwardNode.daemonId.trim() : undefined;
|
|
8232
8237
|
const selfDaemonId = this.deps.statusInstanceId;
|
|
8233
|
-
|
|
8238
|
+
// daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core
|
|
8239
|
+
// is local — execute locally instead of forwarding. Equivalent → local.
|
|
8240
|
+
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
8234
8241
|
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
8235
8242
|
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
8236
8243
|
? args.coordinatorDaemonId.trim()
|
|
@@ -8347,7 +8354,9 @@ export class DaemonCommandRouter {
|
|
|
8347
8354
|
let worktreeCleanup: Record<string, unknown> | undefined;
|
|
8348
8355
|
if (node?.isLocalWorktree) {
|
|
8349
8356
|
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
8350
|
-
|
|
8357
|
+
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
8358
|
+
// clean up locally, do not forward. Equivalent → local.
|
|
8359
|
+
const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
8351
8360
|
&& !args?._meshDirectDispatch;
|
|
8352
8361
|
if (isRemoteWorktree) {
|
|
8353
8362
|
// Worktree lives on a different machine — ask that daemon to clean it up.
|
|
@@ -8473,7 +8482,9 @@ export class DaemonCommandRouter {
|
|
|
8473
8482
|
// _meshDirectDispatch prevents infinite re-forwarding when the stored daemonId
|
|
8474
8483
|
// uses a legacy format that doesn't match the receiving daemon's statusInstanceId.
|
|
8475
8484
|
const sourceDaemonId = typeof sourceNode.daemonId === 'string' ? sourceNode.daemonId.trim() : undefined;
|
|
8476
|
-
|
|
8485
|
+
// daemonIdsEquivalent: an equivalent-form source daemonId is this machine —
|
|
8486
|
+
// clone locally, do not forward. Equivalent → local.
|
|
8487
|
+
if (sourceDaemonId && !daemonIdsEquivalent(sourceDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
8477
8488
|
&& !args?._meshDirectDispatch) {
|
|
8478
8489
|
const forwarded = await this.deps.dispatchMeshCommand(sourceDaemonId, 'clone_mesh_node', {
|
|
8479
8490
|
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
@@ -8748,7 +8759,9 @@ export class DaemonCommandRouter {
|
|
|
8748
8759
|
// Bootstrap runs scripts in the worktree path — forward to the node's daemon if remote.
|
|
8749
8760
|
// _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
|
|
8750
8761
|
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
8751
|
-
|
|
8762
|
+
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
8763
|
+
// bootstrap locally, do not forward. Equivalent → local.
|
|
8764
|
+
if (nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
8752
8765
|
&& !args?._meshDirectDispatch) {
|
|
8753
8766
|
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId, 'retry_mesh_node_bootstrap', {
|
|
8754
8767
|
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVTTRACE — observation-only lifecycle tracing for mesh completion events.
|
|
3
|
+
*
|
|
4
|
+
* Pure logging. This module adds NO decision logic: every call site is a bare log
|
|
5
|
+
* statement inserted ALONGSIDE (never replacing) the existing control flow. Its only
|
|
6
|
+
* job is to make a single completion event greppable across its whole lifecycle by a
|
|
7
|
+
* stable correlation key, and to mark — with one uniform anchor — every point where
|
|
8
|
+
* such an event is rejected / held / skipped / deduped.
|
|
9
|
+
*
|
|
10
|
+
* grep anchors:
|
|
11
|
+
* [EvtTrace] [stage:<name>] — lifecycle progressed a step (INFO)
|
|
12
|
+
* [EvtTrace] [drop:<reason>] — event did NOT advance here, with the reason (WARN)
|
|
13
|
+
*
|
|
14
|
+
* Follow one completion: grep the daemon log for its `task=<id>` (or `sess=<id>`)
|
|
15
|
+
* across the [stage:*] lines; the line with [drop:*] is where it died.
|
|
16
|
+
*
|
|
17
|
+
* Dependency-light on purpose (only the logger) so both providers/ and mesh/ can
|
|
18
|
+
* import it without any cycle risk.
|
|
19
|
+
*/
|
|
20
|
+
import { LOG } from '../logging/logger.js';
|
|
21
|
+
|
|
22
|
+
const CAT = 'EvtTrace';
|
|
23
|
+
|
|
24
|
+
function s(v: unknown): string {
|
|
25
|
+
return typeof v === 'string' && v.trim() ? v.trim() : '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MeshEventTraceCtx {
|
|
29
|
+
/** Primary correlation anchor — the mesh task id (meshActiveTaskId / metadataEvent.taskId). */
|
|
30
|
+
taskId?: unknown;
|
|
31
|
+
/** Optional per-event id when the producer assigns one. */
|
|
32
|
+
eventId?: unknown;
|
|
33
|
+
/** Worker session id — the fallback anchor when no task is attached. */
|
|
34
|
+
sessionId?: unknown;
|
|
35
|
+
nodeId?: unknown;
|
|
36
|
+
meshId?: unknown;
|
|
37
|
+
event?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Stable, greppable correlation key. `task=` and `sess=` are ALWAYS rendered (as `-`
|
|
42
|
+
* when absent) so the key shape is uniform across stages and a single grep alternation
|
|
43
|
+
* (`task=<id>\|sess=<id>`) follows the event end-to-end.
|
|
44
|
+
*/
|
|
45
|
+
export function meshEventTraceKey(ctx: MeshEventTraceCtx): string {
|
|
46
|
+
const segs = [`task=${s(ctx.taskId) || '-'}`];
|
|
47
|
+
const eventId = s(ctx.eventId);
|
|
48
|
+
if (eventId) segs.push(`evt=${eventId}`);
|
|
49
|
+
segs.push(`sess=${s(ctx.sessionId) || '-'}`);
|
|
50
|
+
const nodeId = s(ctx.nodeId);
|
|
51
|
+
if (nodeId) segs.push(`node=${nodeId}`);
|
|
52
|
+
const meshId = s(ctx.meshId);
|
|
53
|
+
if (meshId) segs.push(`mesh=${meshId}`);
|
|
54
|
+
const event = s(ctx.event);
|
|
55
|
+
if (event) segs.push(`event=${event}`);
|
|
56
|
+
return segs.join(' ');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Lifecycle progress (INFO). One line per stage the event clears. */
|
|
60
|
+
export function traceMeshEventStage(stage: string, ctx: MeshEventTraceCtx, detail?: string): void {
|
|
61
|
+
LOG.info(CAT, `[stage:${stage}] ${meshEventTraceKey(ctx)}${detail ? ` — ${detail}` : ''}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Event did not advance — rejected / held / skipped / deduped (WARN). */
|
|
65
|
+
export function traceMeshEventDrop(reason: string, ctx: MeshEventTraceCtx, detail?: string): void {
|
|
66
|
+
LOG.warn(CAT, `[drop:${reason}] ${meshEventTraceKey(ctx)}${detail ? ` — ${detail}` : ''}`);
|
|
67
|
+
}
|