@adhdev/daemon-core 0.9.82-rc.464 → 0.9.82-rc.466
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 +1022 -823
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1028 -829
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-pending.d.ts +18 -0
- package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
- package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +2 -14
- package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
- package/dist/mesh/mesh-runtime-store.d.ts +26 -0
- package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
- package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
- package/dist/providers/cli-provider-instance.d.ts +43 -35
- package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
- package/package.json +3 -3
- package/src/mesh/mesh-event-forwarding.ts +16 -1
- package/src/mesh/mesh-events-pending.ts +94 -5
- package/src/mesh/mesh-ledger.ts +10 -0
- package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
- package/src/mesh/mesh-reconcile-identity.ts +103 -0
- package/src/mesh/mesh-reconcile-loop.ts +28 -393
- package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
- package/src/mesh/mesh-runtime-store.ts +53 -0
- package/src/providers/cli-provider-history-dedup.ts +75 -0
- package/src/providers/cli-provider-input-prompt.ts +133 -0
- package/src/providers/cli-provider-instance.ts +139 -298
- package/src/providers/cli-provider-status-helpers.ts +123 -0
|
@@ -115,6 +115,14 @@ export declare function readRefineJobId(event: {
|
|
|
115
115
|
} | Record<string, unknown>): string;
|
|
116
116
|
export declare function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string;
|
|
117
117
|
export declare function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Retention sweep for the SQLite mesh_pending_events inbox. Deletes long-drained
|
|
120
|
+
* rows (past the idempotency-useful window) and long-orphaned undrained rows (a
|
|
121
|
+
* coordinator identity that never returned). Best-effort and idempotent: a store
|
|
122
|
+
* failure or an empty table is a cheap no-op. Called from the periodic mesh-event
|
|
123
|
+
* maintenance sweep. Returns the number of rows pruned (0 when nothing to do).
|
|
124
|
+
*/
|
|
125
|
+
export declare function prunePendingMeshCoordinatorEventsRetention(): number;
|
|
118
126
|
/**
|
|
119
127
|
* Stamp the v2 protocol envelope onto a pending event at emit time (B2a).
|
|
120
128
|
*
|
|
@@ -183,6 +191,16 @@ export declare function getPendingMeshCoordinatorEvents(meshId?: string, coordin
|
|
|
183
191
|
* (including drained fingerprint history) and JSONL files.
|
|
184
192
|
*/
|
|
185
193
|
export declare function __clearMeshPendingEventsForTests(meshId: string): void;
|
|
194
|
+
/**
|
|
195
|
+
* Test helper: persist a pending event VERBATIM, skipping the emit-time v2 stamp.
|
|
196
|
+
* The local emit path (queuePendingMeshCoordinatorEvent → stampPendingEventV2) now
|
|
197
|
+
* always mints a v2 envelope (self-daemon broadcast fallback when no coordinator
|
|
198
|
+
* identity is present), so a genuinely-unversioned (v1) row can no longer be produced
|
|
199
|
+
* through the normal queue. The drain-side v1 handling (accept-broadcast / enforce-
|
|
200
|
+
* quarantine) still matters for durable v1 rows written by a pre-v2 daemon and for
|
|
201
|
+
* version-skewed remote relays, so tests inject those rows directly through this.
|
|
202
|
+
*/
|
|
203
|
+
export declare function __persistUnstampedPendingEventForTests(event: PendingMeshCoordinatorEvent): boolean;
|
|
186
204
|
/** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
|
|
187
205
|
export declare function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void;
|
|
188
206
|
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const ACKED_DEATH_CONSECUTIVE_READ_FAILURES = 3;
|
|
2
|
+
export declare function resolveTunedReconcileMs(envName: string, def: number, min: number, max: number): number;
|
|
3
|
+
export declare function resolveAckedDeathDeadlineMs(): number;
|
|
4
|
+
export declare function resolveAckedTranscriptFastTrackGraceMs(): number;
|
|
5
|
+
export interface AckedHoldState {
|
|
6
|
+
liveConfirmedSinceAck: boolean;
|
|
7
|
+
consecutiveReadFailures: number;
|
|
8
|
+
transcriptIdleSinceMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const inFlightAckedHoldState: Map<string, AckedHoldState>;
|
|
11
|
+
export declare function inFlightSynthKey(meshId: string, taskId: string): string;
|
|
12
|
+
export declare function getHoldState(synthKey: string, meshId: string): AckedHoldState | undefined;
|
|
13
|
+
export declare function setHoldState(synthKey: string, meshId: string, state: AckedHoldState): void;
|
|
14
|
+
export declare function deleteHoldState(synthKey: string, meshId: string): void;
|
|
15
|
+
export declare function rehydrateAckedHoldsForMesh(meshId: string): void;
|
|
16
|
+
export declare function collectHeldSynthKeysForMesh(meshId: string): Set<string>;
|
|
17
|
+
export declare function __resetReconcileInFlightSynthDebounceForTests(): void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
|
+
import type { LocalMeshEntry } from '../repo-mesh-types.js';
|
|
3
|
+
export declare function resolveCoordinatorDaemonIds(components: DaemonComponents): string[];
|
|
4
|
+
export declare function daemonHostsMesh(mesh: LocalMeshEntry, daemonIds: string[]): boolean;
|
|
5
|
+
export declare function daemonIdListIncludes(ids: readonly string[], id: string | undefined): boolean;
|
|
6
|
+
export declare function resolveCoordinatorSelfIds(mesh: LocalMeshEntry, drainDaemonIds: string[]): string[];
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
phase4SynthesisFired: number;
|
|
5
|
-
/** Acked-hold transcript fast-track promoted a synth ahead of the death deadline. */
|
|
6
|
-
ackedHoldFastTrackFired: number;
|
|
7
|
-
/** Acked-hold death-deadline backstop released a held synth (the 8-min net). */
|
|
8
|
-
ackedHoldDeathDeadlineFired: number;
|
|
9
|
-
};
|
|
10
|
-
/** Test/observability accessor for the v2 last-resort backstop counters (snapshot). */
|
|
11
|
-
export declare function getMeshV2BackstopCounters(): Readonly<typeof meshV2BackstopCounters>;
|
|
12
|
-
/** Test helper: zero the backstop counters so a case starts from a clean slate. */
|
|
13
|
-
export declare function __resetMeshV2BackstopCountersForTests(): void;
|
|
14
|
-
export declare function __resetReconcileInFlightSynthDebounceForTests(): void;
|
|
2
|
+
export { getMeshV2BackstopCounters, __resetMeshV2BackstopCountersForTests } from './mesh-reconcile-v2-backstop.js';
|
|
3
|
+
export { __resetReconcileInFlightSynthDebounceForTests } from './mesh-reconcile-acked-hold.js';
|
|
15
4
|
/**
|
|
16
5
|
* DRAIN-WITHOUT-INJECT guard. Classify, for a mesh on THIS daemon, whether a
|
|
17
6
|
* queue-drain caller (the MCP `get_pending_mesh_events` poll) may safely consume
|
|
@@ -89,4 +78,3 @@ interface ReconcileLoopHandle {
|
|
|
89
78
|
stop(): void;
|
|
90
79
|
}
|
|
91
80
|
export declare function setupMeshReconcileLoop(components: DaemonComponents): ReconcileLoopHandle;
|
|
92
|
-
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare const meshV2BackstopCounters: {
|
|
2
|
+
/** PHASE-4 transcript synthesis actually reconciled a missing completion. */
|
|
3
|
+
phase4SynthesisFired: number;
|
|
4
|
+
/** Acked-hold transcript fast-track promoted a synth ahead of the death deadline. */
|
|
5
|
+
ackedHoldFastTrackFired: number;
|
|
6
|
+
/** Acked-hold death-deadline backstop released a held synth (the 8-min net). */
|
|
7
|
+
ackedHoldDeathDeadlineFired: number;
|
|
8
|
+
};
|
|
9
|
+
/** Test/observability accessor for the v2 last-resort backstop counters (snapshot). */
|
|
10
|
+
export declare function getMeshV2BackstopCounters(): Readonly<typeof meshV2BackstopCounters>;
|
|
11
|
+
/** Test helper: zero the backstop counters so a case starts from a clean slate. */
|
|
12
|
+
export declare function __resetMeshV2BackstopCountersForTests(): void;
|
|
13
|
+
/** Bump a backstop counter and, under enforce, WARN that a last-resort net fired —
|
|
14
|
+
* which under a healthy v2 contract should not happen (the real emit was lost). */
|
|
15
|
+
export declare function recordBackstopFire(kind: keyof typeof meshV2BackstopCounters, detail: string): void;
|
|
16
|
+
export {};
|
|
@@ -490,4 +490,30 @@ export declare class MeshRuntimeStore {
|
|
|
490
490
|
* for the same task could be re-queued later. Returns the number of rows deleted.
|
|
491
491
|
*/
|
|
492
492
|
deletePendingEventsById(ids: ReadonlyArray<string>): number;
|
|
493
|
+
/**
|
|
494
|
+
* Retention prune for mesh_pending_events. This table has no lifecycle GC of its
|
|
495
|
+
* own: a drained row is soft-marked (drained=1) and RETAINED — deliberately, so
|
|
496
|
+
* drainedEventIdsForMesh() has a durable v2-eventId dedup baseline — and an
|
|
497
|
+
* undrained row queued for a coordinator that never returned (a dead/evicted
|
|
498
|
+
* coordinator identity) stays drained=0 forever. Both accumulate without bound
|
|
499
|
+
* (observed: tens of thousands of rows, mostly stale). This is the missing
|
|
500
|
+
* retention step. Two independent windows:
|
|
501
|
+
*
|
|
502
|
+
* - drained rows older than `drainedOlderThanMs`: the coordinator consumed them
|
|
503
|
+
* long ago; the only thing they still back is the eventId re-delivery guard,
|
|
504
|
+
* which is only meaningful for the recent past (a re-delivery of a week-old
|
|
505
|
+
* event cannot occur — its producer session is long gone). Safe to delete.
|
|
506
|
+
* - UNDRAINED rows older than `undrainedOlderThanMs` (a much wider window):
|
|
507
|
+
* these are orphaned events for a coordinator identity that never drained
|
|
508
|
+
* them. Kept wide so a genuinely-offline-but-returning coordinator still
|
|
509
|
+
* receives its backlog; only genuinely unrecoverable orphans are swept.
|
|
510
|
+
*
|
|
511
|
+
* Both windows key off `queued_at` (always present) — `drained_at` can be NULL on
|
|
512
|
+
* legacy rows. Returns the number of rows deleted. Best-effort / idempotent:
|
|
513
|
+
* running it repeatedly with nothing to prune is a cheap no-op.
|
|
514
|
+
*/
|
|
515
|
+
prunePendingEvents(opts: {
|
|
516
|
+
drainedOlderThanMs: number;
|
|
517
|
+
undrainedOlderThanMs: number;
|
|
518
|
+
}): number;
|
|
493
519
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI provider persisted-history dedup — incremental append computation.
|
|
3
|
+
*
|
|
4
|
+
* Pure move out of cli-provider-instance.ts (no behavior change): the
|
|
5
|
+
* shared-prefix diff that turns a full parsed transcript into the newly-added
|
|
6
|
+
* tail to append to the persisted chat history. cli-provider-instance
|
|
7
|
+
* re-exports buildIncrementalHistoryAppendMessages so existing importers/tests
|
|
8
|
+
* keep their path.
|
|
9
|
+
*/
|
|
10
|
+
export type PersistableCliHistoryMessage = {
|
|
11
|
+
role: string;
|
|
12
|
+
content: string;
|
|
13
|
+
kind?: string;
|
|
14
|
+
senderName?: string;
|
|
15
|
+
receivedAt?: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function buildIncrementalHistoryAppendMessages(previousMessages: PersistableCliHistoryMessage[], currentMessages: PersistableCliHistoryMessage[]): PersistableCliHistoryMessage[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI provider structured-input helpers — image materialization + prompt build.
|
|
3
|
+
*
|
|
4
|
+
* Pure move out of cli-provider-instance.ts (no behavior change): the input
|
|
5
|
+
* envelope → CLI prompt string construction and its image-materialization
|
|
6
|
+
* support. cli-provider-instance re-exports buildCliStructuredInputPrompt so
|
|
7
|
+
* existing importers/tests keep their path.
|
|
8
|
+
*/
|
|
9
|
+
import { type InputEnvelope } from './contracts.js';
|
|
10
|
+
export declare function buildCliStructuredInputPrompt(input: InputEnvelope, options?: {
|
|
11
|
+
materializeDir?: string;
|
|
12
|
+
}): string;
|
|
@@ -9,40 +9,9 @@ import type { ProviderInstance, ProviderState, InstanceContext, HotChatSessionSt
|
|
|
9
9
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
10
10
|
import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
11
11
|
import type { ChatMessage } from '../types.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
kind?: string;
|
|
16
|
-
senderName?: string;
|
|
17
|
-
receivedAt?: number;
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* NOTIF Defect-2a: the REPORTED short-generating duration, anchored on the IMMUTABLE turn
|
|
21
|
-
* start. generatingStartedAt is reset to 0 on every mid-turn waiting_approval/idle blip and
|
|
22
|
-
* re-armed on the next →generating, so a long turn that blips would otherwise measure only the
|
|
23
|
-
* final 1.5-2.5s sliver. engine.currentTurnStartedAt (set once at onTurnStarted, surviving
|
|
24
|
-
* mid-turn blips until the next turn starts) is preferred; generatingStartedAt is the fallback
|
|
25
|
-
* for turns that never recorded an engine turn start. Returns 0 when neither anchor is set.
|
|
26
|
-
* Pure / unit-testable.
|
|
27
|
-
*/
|
|
28
|
-
export declare function computeTurnAnchoredDurationMs(engineTurnStartedAt: number | undefined, generatingStartedAt: number, now: number): {
|
|
29
|
-
durationMs: number;
|
|
30
|
-
anchor: 'turn-start' | 'generatingStartedAt' | 'none';
|
|
31
|
-
};
|
|
32
|
-
export declare function buildCliStructuredInputPrompt(input: InputEnvelope, options?: {
|
|
33
|
-
materializeDir?: string;
|
|
34
|
-
}): string;
|
|
35
|
-
export declare function buildIncrementalHistoryAppendMessages(previousMessages: PersistableCliHistoryMessage[], currentMessages: PersistableCliHistoryMessage[]): PersistableCliHistoryMessage[];
|
|
36
|
-
export declare function getForcedNewSessionScriptName(provider: ProviderModule | undefined, launchMode: 'new' | 'resume' | 'manual'): string | null;
|
|
37
|
-
export declare function waitForCliAdapterReady(adapter: {
|
|
38
|
-
isReady?: () => boolean;
|
|
39
|
-
getStatus?: () => {
|
|
40
|
-
status?: string;
|
|
41
|
-
};
|
|
42
|
-
}, options?: {
|
|
43
|
-
timeoutMs?: number;
|
|
44
|
-
pollMs?: number;
|
|
45
|
-
}): Promise<void>;
|
|
12
|
+
export { buildCliStructuredInputPrompt } from './cli-provider-input-prompt.js';
|
|
13
|
+
export { buildIncrementalHistoryAppendMessages } from './cli-provider-history-dedup.js';
|
|
14
|
+
export { computeTurnAnchoredDurationMs, getForcedNewSessionScriptName, waitForCliAdapterReady, } from './cli-provider-status-helpers.js';
|
|
46
15
|
export declare class CliProviderInstance implements ProviderInstance {
|
|
47
16
|
private provider;
|
|
48
17
|
private workingDir;
|
|
@@ -75,6 +44,33 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
75
44
|
* from scratch rather than firing on a stale timestamp.
|
|
76
45
|
*/
|
|
77
46
|
private static readonly AUTO_APPROVE_GATE_HYSTERESIS_MS;
|
|
47
|
+
/**
|
|
48
|
+
* AUTOAPPROVE-FLAP-RECUR (Fix B): extended busy-side continuity window for a
|
|
49
|
+
* DELEGATED-WORKER auto-approve episode that is genuinely still cycling.
|
|
50
|
+
*
|
|
51
|
+
* The default AUTO_APPROVE_GATE_HYSTERESIS_MS (1500) absorbs a *momentary*
|
|
52
|
+
* `generating` blip. But a delegated worker running a Bash approval observed
|
|
53
|
+
* the FSM cycle the FULL state waiting_approval → busy → waiting_approval on a
|
|
54
|
+
* 2–5s period (the button set scrolls in/out AND the modal question repaints,
|
|
55
|
+
* so the adapter genuinely reports status=generating for whole seconds between
|
|
56
|
+
* approval frames). Each busy phase outran the 1500ms hysteresis, so the
|
|
57
|
+
* settle clock was WIPED (the genuine-resolution branch), the 600ms settle
|
|
58
|
+
* window never accumulated across the flap, resolveModal never fired
|
|
59
|
+
* (resolveModal count 0), and the mask-stall clock instead tripped at 4500ms →
|
|
60
|
+
* coordinator nudge → the flap the coordinator observed.
|
|
61
|
+
*
|
|
62
|
+
* A genuine resolution and a flap both start with a busy phase; they diverge
|
|
63
|
+
* only in whether waiting_approval RETURNS. So we cannot simply lengthen the
|
|
64
|
+
* blanket hysteresis (that would make every real resolution hold the gate
|
|
65
|
+
* open for seconds). Instead this longer window applies ONLY while an active
|
|
66
|
+
* mask episode is alive (autoApproveMaskSince > 0) AND the session is a
|
|
67
|
+
* delegated worker — i.e. exactly the never-resolving-flap case. A foreground
|
|
68
|
+
* / attended session keeps the tight 1500ms window unchanged. The mask-stall
|
|
69
|
+
* bound below still caps the episode, so a worker whose approval truly never
|
|
70
|
+
* returns is surfaced to the coordinator within AUTO_APPROVE_MASK_STALL_MS
|
|
71
|
+
* rather than held forever.
|
|
72
|
+
*/
|
|
73
|
+
private static readonly AUTO_APPROVE_FLAP_CONTINUITY_MS;
|
|
78
74
|
/**
|
|
79
75
|
* STATUS-MISMATCH: upper bound on how long the auto-approve→`generating` SURFACE
|
|
80
76
|
* mask may hide a worker's `waiting_approval` (status + activeModal) before we give
|
|
@@ -114,6 +110,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
114
110
|
private pendingAutoApprovalSince;
|
|
115
111
|
private autoApproveSettleTimer;
|
|
116
112
|
private autoApproveInactiveSince;
|
|
113
|
+
private autoApproveLastModalSeenAt;
|
|
117
114
|
private autoApproveMaskSince;
|
|
118
115
|
private stalledApprovalNudgeEpisode;
|
|
119
116
|
private readonly manualAttendance;
|
|
@@ -303,6 +300,18 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
303
300
|
private approvalResolutionFinalizationBlock;
|
|
304
301
|
private scheduleCompletedDebounceFlush;
|
|
305
302
|
private isMeshWorkerSession;
|
|
303
|
+
/**
|
|
304
|
+
* AUTOAPPROVE-FLAP-RECUR (Fix A+B): how long a busy blip / modal scroll-out may
|
|
305
|
+
* persist before the in-progress settle gate is torn down. For a delegated
|
|
306
|
+
* worker whose auto-approve episode is genuinely still cycling (mask clock
|
|
307
|
+
* alive), the FSM's full waiting_approval → busy → waiting_approval flap runs
|
|
308
|
+
* on a multi-second period, so the settle continuity window is extended to
|
|
309
|
+
* AUTO_APPROVE_FLAP_CONTINUITY_MS to bridge it (still bounded, and still capped
|
|
310
|
+
* by AUTO_APPROVE_MASK_STALL_MS). Every other case — foreground/attended
|
|
311
|
+
* session, or no active mask episode — keeps the tight default hysteresis so a
|
|
312
|
+
* genuine resolution frees the gate promptly.
|
|
313
|
+
*/
|
|
314
|
+
private autoApproveContinuityWindowMs;
|
|
306
315
|
private isAutonomousMeshSession;
|
|
307
316
|
/**
|
|
308
317
|
* ARCH-REFACTOR R1: the taskId to attribute the CURRENTLY-completing turn to.
|
|
@@ -429,4 +438,3 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
429
438
|
private buildSqlPlaceholderList;
|
|
430
439
|
private querySqliteText;
|
|
431
440
|
}
|
|
432
|
-
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI provider status/launch pure helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure move out of cli-provider-instance.ts (no behavior change): the
|
|
5
|
+
* side-effect-free status predicates, the turn-anchored duration computation,
|
|
6
|
+
* the forced-new-session script resolver, the adapter-ready poll, and the lazy
|
|
7
|
+
* node:sqlite DatabaseSync loader. cli-provider-instance re-exports the
|
|
8
|
+
* public symbols (computeTurnAnchoredDurationMs, getForcedNewSessionScriptName,
|
|
9
|
+
* waitForCliAdapterReady) so existing importers/tests keep their path.
|
|
10
|
+
*/
|
|
11
|
+
import type { ProviderModule } from './contracts.js';
|
|
12
|
+
export declare function isIdleStatus(value: unknown): boolean;
|
|
13
|
+
export declare function getMessageTime(message: unknown): number;
|
|
14
|
+
export declare function hasNonEmptyCliModalButtons(activeModal: unknown): boolean;
|
|
15
|
+
export declare function isCliGeneratingLikeStatus(status: unknown): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* NOTIF Defect-2a: the REPORTED short-generating duration, anchored on the IMMUTABLE turn
|
|
18
|
+
* start. generatingStartedAt is reset to 0 on every mid-turn waiting_approval/idle blip and
|
|
19
|
+
* re-armed on the next →generating, so a long turn that blips would otherwise measure only the
|
|
20
|
+
* final 1.5-2.5s sliver. engine.currentTurnStartedAt (set once at onTurnStarted, surviving
|
|
21
|
+
* mid-turn blips until the next turn starts) is preferred; generatingStartedAt is the fallback
|
|
22
|
+
* for turns that never recorded an engine turn start. Returns 0 when neither anchor is set.
|
|
23
|
+
* Pure / unit-testable.
|
|
24
|
+
*/
|
|
25
|
+
export declare function computeTurnAnchoredDurationMs(engineTurnStartedAt: number | undefined, generatingStartedAt: number, now: number): {
|
|
26
|
+
durationMs: number;
|
|
27
|
+
anchor: 'turn-start' | 'generatingStartedAt' | 'none';
|
|
28
|
+
};
|
|
29
|
+
export declare function getDatabaseSync(): new (path: string, options?: {
|
|
30
|
+
readOnly?: boolean;
|
|
31
|
+
}) => {
|
|
32
|
+
prepare(sql: string): {
|
|
33
|
+
get(...params: Array<string | number>): unknown;
|
|
34
|
+
};
|
|
35
|
+
close(): void;
|
|
36
|
+
};
|
|
37
|
+
export declare function getForcedNewSessionScriptName(provider: ProviderModule | undefined, launchMode: 'new' | 'resume' | 'manual'): string | null;
|
|
38
|
+
export declare function waitForCliAdapterReady(adapter: {
|
|
39
|
+
isReady?: () => boolean;
|
|
40
|
+
getStatus?: () => {
|
|
41
|
+
status?: string;
|
|
42
|
+
};
|
|
43
|
+
}, options?: {
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
pollMs?: number;
|
|
46
|
+
}): Promise<void>;
|
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.466",
|
|
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",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"author": "vilmire",
|
|
48
48
|
"license": "AGPL-3.0-or-later",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
51
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "0.9.82-rc.466",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.466",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -7,7 +7,7 @@ import type { SessionRecoveryContext } from './mesh-ledger.js';
|
|
|
7
7
|
import { updateSessionTaskStatus, enqueueTask, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches, hasPendingDependents, getQueue } from './mesh-work-queue.js';
|
|
8
8
|
import { markSessionDeliveriesTerminal, updateSessionDeliveryStatus, recordCompletionConflict } from './mesh-delivery-policy.js';
|
|
9
9
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
10
|
-
import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, readV2EnvelopeFromWire, type PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
10
|
+
import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, prunePendingMeshCoordinatorEventsRetention, readV2EnvelopeFromWire, type PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
11
11
|
import type { ProviderInstance } from '../providers/provider-instance.js';
|
|
12
12
|
import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent, isUnroutableDelegateRejection } from './mesh-routing.js';
|
|
13
13
|
import { resolveMeshHostStatus } from './mesh-host-ownership.js';
|
|
@@ -178,10 +178,25 @@ export function __resetMeshWorkspaceCacheForTests(): void {
|
|
|
178
178
|
meshByWorkspaceCache.clear();
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// Throttle the pending-event retention DELETE so it runs at most hourly — the
|
|
182
|
+
// remote-idle sweep below fires on every completion/idle transition, but a table
|
|
183
|
+
// scan + DELETE should not. In-process timestamp; a restart re-arms it, which only
|
|
184
|
+
// means one prune shortly after boot (harmless, idempotent).
|
|
185
|
+
let lastPendingEventsPruneAt = 0;
|
|
186
|
+
const PENDING_EVENTS_PRUNE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
187
|
+
|
|
181
188
|
function sweepExpiredRemoteIdleSessions(): void {
|
|
182
189
|
try {
|
|
183
190
|
MeshRuntimeStore.getInstance().pruneExpiredRemoteIdleSessions();
|
|
184
191
|
} catch { /* best-effort */ }
|
|
192
|
+
// Piggyback the pending-event retention prune on the same periodic sweep, but
|
|
193
|
+
// hourly — this is the maintenance hook that keeps mesh_pending_events from
|
|
194
|
+
// accumulating stale drained/orphaned rows without bound.
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
if (now - lastPendingEventsPruneAt >= PENDING_EVENTS_PRUNE_INTERVAL_MS) {
|
|
197
|
+
lastPendingEventsPruneAt = now;
|
|
198
|
+
prunePendingMeshCoordinatorEventsRetention();
|
|
199
|
+
}
|
|
185
200
|
}
|
|
186
201
|
|
|
187
202
|
const INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS = 30 * 60 * 1000;
|
|
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, readFileSync, renameSync, statSync, unlinkS
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { LOG } from '../logging/logger.js';
|
|
5
|
+
import { loadConfig } from '../config/config.js';
|
|
5
6
|
import { getLedgerDir, readLedgerEntries, appendLedgerEntry } from './mesh-ledger.js';
|
|
6
7
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
7
8
|
import { buildMeshSystemMessage, readNonEmptyString, readRecord, resolveEventSessionId, readMeshCompletionSummary, isWeakCompletionMetadata } from './mesh-events-utils.js';
|
|
@@ -674,6 +675,42 @@ function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMe
|
|
|
674
675
|
const MAX_PENDING_EVENTS_BYTES = 100 * 1024; // 100 KB — keep the pending file small
|
|
675
676
|
const MAX_PENDING_EVENTS_KEEP = 50; // keep the last 50 events when trimming
|
|
676
677
|
|
|
678
|
+
// ─── SQLite pending-event retention ─────────────────────────────────────────
|
|
679
|
+
// mesh_pending_events had no lifecycle GC: drained rows are retained forever (the
|
|
680
|
+
// durable v2-eventId dedup baseline drainedEventIdsForMesh reads them), and an
|
|
681
|
+
// undrained row for a coordinator identity that never returns stays queued forever.
|
|
682
|
+
// Both accumulate without bound. These windows bound that growth while preserving
|
|
683
|
+
// the two things the rows exist for — recent-re-delivery idempotency and delivery
|
|
684
|
+
// to a returning coordinator. A drained event older than the drained window cannot
|
|
685
|
+
// be re-delivered (its producer session is long gone), so keeping it buys nothing;
|
|
686
|
+
// an undrained event is kept far longer so a genuinely-offline coordinator's backlog
|
|
687
|
+
// survives, and only unrecoverable orphans are swept.
|
|
688
|
+
const PENDING_EVENTS_DRAINED_RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
689
|
+
const PENDING_EVENTS_UNDRAINED_RETENTION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Retention sweep for the SQLite mesh_pending_events inbox. Deletes long-drained
|
|
693
|
+
* rows (past the idempotency-useful window) and long-orphaned undrained rows (a
|
|
694
|
+
* coordinator identity that never returned). Best-effort and idempotent: a store
|
|
695
|
+
* failure or an empty table is a cheap no-op. Called from the periodic mesh-event
|
|
696
|
+
* maintenance sweep. Returns the number of rows pruned (0 when nothing to do).
|
|
697
|
+
*/
|
|
698
|
+
export function prunePendingMeshCoordinatorEventsRetention(): number {
|
|
699
|
+
try {
|
|
700
|
+
const removed = MeshRuntimeStore.getInstance().prunePendingEvents({
|
|
701
|
+
drainedOlderThanMs: PENDING_EVENTS_DRAINED_RETENTION_MS,
|
|
702
|
+
undrainedOlderThanMs: PENDING_EVENTS_UNDRAINED_RETENTION_MS,
|
|
703
|
+
});
|
|
704
|
+
if (removed > 0) {
|
|
705
|
+
LOG.info('MeshEvents', `Pruned ${removed} stale pending-event row(s) (drained >7d / undrained >30d)`);
|
|
706
|
+
}
|
|
707
|
+
return removed;
|
|
708
|
+
} catch (e: any) {
|
|
709
|
+
LOG.warn('MeshEvents', `Pending-event retention prune failed: ${e?.message || e}`);
|
|
710
|
+
return 0;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
677
714
|
function trimPendingEventsIfNeeded(path: string): void {
|
|
678
715
|
try {
|
|
679
716
|
if (!existsSync(path)) return;
|
|
@@ -746,23 +783,51 @@ export function stampPendingEventV2(
|
|
|
746
783
|
return event;
|
|
747
784
|
}
|
|
748
785
|
|
|
749
|
-
const
|
|
786
|
+
const coordinatorIdentity = hint?.dispatchedBy ?? coordinatorIdentityFromEmitFields({
|
|
750
787
|
daemonId: event.targetCoordinatorDaemonId,
|
|
751
788
|
coordinatorRunId: hint?.coordinatorRunId,
|
|
752
789
|
sessionId: event.targetCoordinatorSessionId,
|
|
753
790
|
});
|
|
791
|
+
|
|
792
|
+
// A/C ROOT FIX: an emit site with NO coordinator identity (direct-dispatch /
|
|
793
|
+
// refine notification / any path where the worker session never carried a
|
|
794
|
+
// meshCoordinatorDaemonId) used to leave the event UNVERSIONED (v1). Under v2
|
|
795
|
+
// enforce (default ON) routeV2EventsForDrainer QUARANTINES every unversioned
|
|
796
|
+
// event — so a summary-less completion (agent:generating_completed) and every
|
|
797
|
+
// refine terminal notification (refine:accepted/completed/failed) were held
|
|
798
|
+
// back and never reached the coordinator; only the backstop papered over it.
|
|
799
|
+
//
|
|
800
|
+
// Fall back to THIS daemon's own id as the dispatcher so a v2 envelope can
|
|
801
|
+
// still be minted. There is no addressable coordinator, so we intentionally
|
|
802
|
+
// leave intendedFor empty and let buildPendingEventEmitStamp downgrade the
|
|
803
|
+
// (unicast-defaulting) terminal event to a BROADCAST — deliverable to whatever
|
|
804
|
+
// coordinator drains on this machine, instead of an undeliverable v1 event.
|
|
805
|
+
// When a real coordinator identity DOES exist the unicast path below is
|
|
806
|
+
// unchanged (no regression). loadConfig().machineId is the same self-id source
|
|
807
|
+
// resolveCoordinatorDaemonIds / the local queue-assignment stamp use, so the
|
|
808
|
+
// broadcast dispatcher matches the drainer's own identity form.
|
|
809
|
+
const selfFallback = !coordinatorIdentity;
|
|
810
|
+
const dispatchedBy = coordinatorIdentity ?? coordinatorIdentityFromEmitFields({
|
|
811
|
+
daemonId: readNonEmptyString(loadConfig().machineId),
|
|
812
|
+
});
|
|
754
813
|
// The unicast target is, by default, the same coordinator the event is already
|
|
755
|
-
// routed to (its originating coordinator). A hint may override it.
|
|
756
|
-
|
|
814
|
+
// routed to (its originating coordinator). A hint may override it. In the
|
|
815
|
+
// self-fallback case there is no originating coordinator to address, so leave
|
|
816
|
+
// it empty → broadcast (never a self-unicast that a sibling session's drainer
|
|
817
|
+
// would skip).
|
|
818
|
+
const intendedFor: CoordinatorIdentity | undefined = hint?.intendedFor
|
|
819
|
+
?? (selfFallback ? undefined : coordinatorIdentity);
|
|
757
820
|
|
|
758
821
|
const stamp = buildPendingEventEmitStamp({
|
|
759
822
|
eventName: event.event,
|
|
760
823
|
eventId: randomUUID(),
|
|
761
824
|
dispatchedBy,
|
|
762
825
|
intendedFor,
|
|
763
|
-
|
|
826
|
+
// Force broadcast for the self-fallback so a unicast-defaulting terminal
|
|
827
|
+
// event isn't addressed to this daemon alone; an explicit hint still wins.
|
|
828
|
+
scope: hint?.scope ?? (selfFallback ? 'broadcast' : undefined),
|
|
764
829
|
});
|
|
765
|
-
if (!stamp) return event; // no coordinator identity → stays a v1 event
|
|
830
|
+
if (!stamp) return event; // no coordinator identity at all (no self id) → stays a v1 event
|
|
766
831
|
|
|
767
832
|
return {
|
|
768
833
|
...event,
|
|
@@ -849,6 +914,17 @@ export function queuePendingMeshCoordinatorEvent(
|
|
|
849
914
|
// B2a: stamp the v2 envelope before dedup/persist so the eventId/scope ride
|
|
850
915
|
// into both stores and the fingerprint/dedup logic sees the final shape.
|
|
851
916
|
const event = stampPendingEventV2(rawEvent, hint);
|
|
917
|
+
return persistPendingMeshCoordinatorEvent(event);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Persist an ALREADY-STAMPED pending event to both stores (dedup + SQLite + JSONL),
|
|
922
|
+
* without re-running the emit stamp. queuePendingMeshCoordinatorEvent stamps then
|
|
923
|
+
* calls this; the only other caller is the test helper below, which needs to inject
|
|
924
|
+
* a genuinely-unversioned (v1) row to exercise the drain-side v1 handling now that
|
|
925
|
+
* the emit path never produces one (self-daemon fallback stamps every local emit).
|
|
926
|
+
*/
|
|
927
|
+
function persistPendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
852
928
|
try {
|
|
853
929
|
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
854
930
|
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
|
|
@@ -1212,6 +1288,19 @@ export function __clearMeshPendingEventsForTests(meshId: string): void {
|
|
|
1212
1288
|
clearPendingMeshCoordinatorEvents(meshId);
|
|
1213
1289
|
}
|
|
1214
1290
|
|
|
1291
|
+
/**
|
|
1292
|
+
* Test helper: persist a pending event VERBATIM, skipping the emit-time v2 stamp.
|
|
1293
|
+
* The local emit path (queuePendingMeshCoordinatorEvent → stampPendingEventV2) now
|
|
1294
|
+
* always mints a v2 envelope (self-daemon broadcast fallback when no coordinator
|
|
1295
|
+
* identity is present), so a genuinely-unversioned (v1) row can no longer be produced
|
|
1296
|
+
* through the normal queue. The drain-side v1 handling (accept-broadcast / enforce-
|
|
1297
|
+
* quarantine) still matters for durable v1 rows written by a pre-v2 daemon and for
|
|
1298
|
+
* version-skewed remote relays, so tests inject those rows directly through this.
|
|
1299
|
+
*/
|
|
1300
|
+
export function __persistUnstampedPendingEventForTests(event: PendingMeshCoordinatorEvent): boolean {
|
|
1301
|
+
return persistPendingMeshCoordinatorEvent(event);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1215
1304
|
/** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
|
|
1216
1305
|
export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
|
|
1217
1306
|
if (!meshId) return;
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -255,11 +255,21 @@ const RECENT_FAILURE_WINDOW_MS = 30 * 60 * 1000; // 30 minutes
|
|
|
255
255
|
|
|
256
256
|
// Kinds that accumulate indefinitely and are safe to archive after ARCHIVE_TERMINAL_OLDER_THAN_MS.
|
|
257
257
|
// Non-terminal kinds (dispatched, sessions, nodes, checkpoints) are always kept in the active file.
|
|
258
|
+
//
|
|
259
|
+
// session_auto_launch is telemetry, NOT audit: it records the queue-assignment
|
|
260
|
+
// decision on each tick (mostly phase:'skipped' — nothing was launched). No reader
|
|
261
|
+
// consults it back from the ledger (unlike task_dispatched, which getSessionRecoveryContext
|
|
262
|
+
// scans and which therefore MUST stay non-archivable). It is the single largest
|
|
263
|
+
// avoidable ledger consumer after node/dispatch history, so archiving it after the
|
|
264
|
+
// 7-day window is the volume fix — recent entries still surface for diagnosis (and the
|
|
265
|
+
// per-tick dedup keeps live volume bounded), old ones move to the archive JSONL and out
|
|
266
|
+
// of the runtime store. This preserves observability while removing the standing bulk.
|
|
258
267
|
const ARCHIVABLE_KINDS: ReadonlySet<MeshLedgerKind> = new Set([
|
|
259
268
|
'task_completed',
|
|
260
269
|
'task_failed',
|
|
261
270
|
'task_stalled',
|
|
262
271
|
'recovery_attempted',
|
|
272
|
+
'session_auto_launch',
|
|
263
273
|
] as MeshLedgerKind[]);
|
|
264
274
|
const DEFAULT_LEDGER_SLICE_LIMIT = 100;
|
|
265
275
|
export const MAX_LEDGER_SLICE_LIMIT = 500;
|