@adhdev/daemon-core 0.9.82-rc.195 → 0.9.82-rc.197
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/cli-adapter-types.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +594 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +593 -83
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/contracts.d.ts +1 -1
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-delivery-policy.d.ts +126 -0
- package/dist/mesh/{beads-db.d.ts → mesh-runtime-store.d.ts} +68 -2
- package/dist/mesh/mesh-work-queue.d.ts +3 -3
- package/dist/providers/provider-instance.d.ts +1 -1
- package/dist/providers/spec/driver.d.ts +4 -1
- package/dist/providers/spec/schema.gen.d.ts +46 -0
- package/dist/providers/spec/types.d.ts +39 -0
- package/dist/shared-types-extra.d.ts +1 -1
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cli-adapter-types.ts +1 -0
- package/src/cli-adapters/cli-state-engine.ts +44 -2
- package/src/index.ts +4 -0
- package/src/mesh/contracts.ts +1 -1
- package/src/mesh/mesh-active-work.ts +8 -8
- package/src/mesh/mesh-delivery-policy.ts +298 -0
- package/src/mesh/mesh-events.ts +64 -15
- package/src/mesh/{beads-db.ts → mesh-runtime-store.ts} +249 -7
- package/src/mesh/mesh-work-queue.ts +33 -33
- package/src/providers/cli-provider-instance.ts +31 -8
- package/src/providers/provider-instance.ts +1 -1
- package/src/providers/spec/driver.ts +34 -3
- package/src/providers/spec/evaluator.ts +32 -3
- package/src/providers/spec/schema.gen.ts +22 -2
- package/src/providers/spec/schema.json +1 -0
- package/src/providers/spec/types.ts +39 -0
- package/src/providers/types/interactive-prompt.ts +21 -7
- package/src/shared-types-extra.ts +1 -1
- package/src/status/normalize.ts +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, statSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from 'fs';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { getLedgerDir } from './mesh-ledger.js';
|
|
@@ -26,8 +26,31 @@ function legacyQueuePath(meshId: string): string {
|
|
|
26
26
|
return join(getLedgerDir(), `${safeMeshId(meshId)}.queue.json`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
function meshRuntimeStorePath(): string {
|
|
30
|
+
const dir = getLedgerDir();
|
|
31
|
+
const nextPath = join(dir, 'mesh-runtime.db');
|
|
32
|
+
if (existsSync(nextPath)) return nextPath;
|
|
33
|
+
|
|
34
|
+
const legacyPath = join(dir, 'beads.db');
|
|
35
|
+
if (!existsSync(legacyPath)) return nextPath;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
renameSync(legacyPath, nextPath);
|
|
39
|
+
for (const suffix of ['-wal', '-shm']) {
|
|
40
|
+
const legacyCompanion = `${legacyPath}${suffix}`;
|
|
41
|
+
if (existsSync(legacyCompanion)) {
|
|
42
|
+
renameSync(legacyCompanion, `${nextPath}${suffix}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Best-effort compatibility for existing installs. If migration fails,
|
|
47
|
+
// opening the new store will create a clean DB instead of blocking boot.
|
|
48
|
+
}
|
|
49
|
+
return nextPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class MeshRuntimeStore {
|
|
53
|
+
private static instance: MeshRuntimeStore | undefined;
|
|
31
54
|
private readonly db: DatabaseHandle;
|
|
32
55
|
private readonly dbPath: string;
|
|
33
56
|
private readonly migratedMeshIds = new Set<string>();
|
|
@@ -49,9 +72,9 @@ export class BeadsDB {
|
|
|
49
72
|
this.migrate();
|
|
50
73
|
}
|
|
51
74
|
|
|
52
|
-
static getInstance():
|
|
75
|
+
static getInstance(): MeshRuntimeStore {
|
|
53
76
|
if (!this.instance) {
|
|
54
|
-
this.instance = new
|
|
77
|
+
this.instance = new MeshRuntimeStore(meshRuntimeStorePath());
|
|
55
78
|
}
|
|
56
79
|
return this.instance;
|
|
57
80
|
}
|
|
@@ -120,6 +143,49 @@ export class BeadsDB {
|
|
|
120
143
|
metadata TEXT,
|
|
121
144
|
PRIMARY KEY (node_id, session_id)
|
|
122
145
|
);
|
|
146
|
+
|
|
147
|
+
CREATE TABLE IF NOT EXISTS mesh_session_delivery (
|
|
148
|
+
id TEXT PRIMARY KEY,
|
|
149
|
+
mesh_id TEXT NOT NULL,
|
|
150
|
+
node_id TEXT,
|
|
151
|
+
session_id TEXT,
|
|
152
|
+
provider_type TEXT,
|
|
153
|
+
task_id TEXT,
|
|
154
|
+
kind TEXT NOT NULL,
|
|
155
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
156
|
+
message TEXT NOT NULL,
|
|
157
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
158
|
+
deliver_after TEXT,
|
|
159
|
+
expires_at TEXT,
|
|
160
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
161
|
+
source_coordinator_session_id TEXT,
|
|
162
|
+
source_coordinator_daemon_id TEXT,
|
|
163
|
+
last_error TEXT,
|
|
164
|
+
created_at TEXT NOT NULL,
|
|
165
|
+
updated_at TEXT NOT NULL
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_mesh_session_delivery_mesh_status
|
|
169
|
+
ON mesh_session_delivery(mesh_id, status, created_at);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_mesh_session_delivery_session
|
|
171
|
+
ON mesh_session_delivery(mesh_id, session_id, status);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_mesh_session_delivery_task
|
|
173
|
+
ON mesh_session_delivery(mesh_id, task_id);
|
|
174
|
+
|
|
175
|
+
CREATE TABLE IF NOT EXISTS mesh_completion_conflicts (
|
|
176
|
+
id TEXT PRIMARY KEY,
|
|
177
|
+
mesh_id TEXT NOT NULL,
|
|
178
|
+
fingerprint TEXT NOT NULL,
|
|
179
|
+
conflicting_task_id TEXT,
|
|
180
|
+
conflicting_session_id TEXT,
|
|
181
|
+
original_task_id TEXT,
|
|
182
|
+
original_session_id TEXT,
|
|
183
|
+
event TEXT NOT NULL,
|
|
184
|
+
created_at TEXT NOT NULL
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_mesh_completion_conflicts_mesh
|
|
188
|
+
ON mesh_completion_conflicts(mesh_id, created_at);
|
|
123
189
|
`);
|
|
124
190
|
}
|
|
125
191
|
|
|
@@ -149,13 +215,13 @@ export class BeadsDB {
|
|
|
149
215
|
}
|
|
150
216
|
|
|
151
217
|
private maybeCheckpointWal(): void {
|
|
152
|
-
if (++this.walWriteCounter <
|
|
218
|
+
if (++this.walWriteCounter < MeshRuntimeStore.WAL_CHECK_INTERVAL) return;
|
|
153
219
|
this.walWriteCounter = 0;
|
|
154
220
|
try {
|
|
155
221
|
const walPath = `${this.dbPath}-wal`;
|
|
156
222
|
if (!existsSync(walPath)) return;
|
|
157
223
|
const size = statSync(walPath).size;
|
|
158
|
-
if (size <
|
|
224
|
+
if (size < MeshRuntimeStore.WAL_MAX_BYTES) return;
|
|
159
225
|
process.stderr.write(
|
|
160
226
|
`[adhdev-mesh] WAL file ${Math.round(size / 1024 / 1024)}MB exceeds threshold; forcing checkpoint\n`,
|
|
161
227
|
);
|
|
@@ -524,4 +590,180 @@ export class BeadsDB {
|
|
|
524
590
|
pruneExpiredRemoteIdleSessions(): void {
|
|
525
591
|
this.db.prepare('DELETE FROM remote_idle_sessions WHERE expires_at <= ?').run(Date.now());
|
|
526
592
|
}
|
|
593
|
+
|
|
594
|
+
// ── Session Delivery Queue ───────────────────────────────────────────────
|
|
595
|
+
|
|
596
|
+
insertSessionDelivery(entry: {
|
|
597
|
+
id: string;
|
|
598
|
+
meshId: string;
|
|
599
|
+
nodeId?: string;
|
|
600
|
+
sessionId?: string;
|
|
601
|
+
providerType?: string;
|
|
602
|
+
taskId?: string;
|
|
603
|
+
kind: string;
|
|
604
|
+
priority?: number;
|
|
605
|
+
message: string;
|
|
606
|
+
status: string;
|
|
607
|
+
deliverAfter?: string;
|
|
608
|
+
expiresAt?: string;
|
|
609
|
+
sourceCoordinatorSessionId?: string;
|
|
610
|
+
sourceCoordinatorDaemonId?: string;
|
|
611
|
+
createdAt: string;
|
|
612
|
+
updatedAt: string;
|
|
613
|
+
}): void {
|
|
614
|
+
this.db.prepare(`
|
|
615
|
+
INSERT OR REPLACE INTO mesh_session_delivery (
|
|
616
|
+
id, mesh_id, node_id, session_id, provider_type, task_id, kind, priority,
|
|
617
|
+
message, status, deliver_after, expires_at, attempt_count,
|
|
618
|
+
source_coordinator_session_id, source_coordinator_daemon_id,
|
|
619
|
+
last_error, created_at, updated_at
|
|
620
|
+
) VALUES (
|
|
621
|
+
@id, @meshId, @nodeId, @sessionId, @providerType, @taskId, @kind, @priority,
|
|
622
|
+
@message, @status, @deliverAfter, @expiresAt, 0,
|
|
623
|
+
@sourceCoordinatorSessionId, @sourceCoordinatorDaemonId,
|
|
624
|
+
NULL, @createdAt, @updatedAt
|
|
625
|
+
)
|
|
626
|
+
`).run({
|
|
627
|
+
id: entry.id,
|
|
628
|
+
meshId: entry.meshId,
|
|
629
|
+
nodeId: entry.nodeId ?? null,
|
|
630
|
+
sessionId: entry.sessionId ?? null,
|
|
631
|
+
providerType: entry.providerType ?? null,
|
|
632
|
+
taskId: entry.taskId ?? null,
|
|
633
|
+
kind: entry.kind,
|
|
634
|
+
priority: entry.priority ?? 0,
|
|
635
|
+
message: entry.message,
|
|
636
|
+
status: entry.status,
|
|
637
|
+
deliverAfter: entry.deliverAfter ?? null,
|
|
638
|
+
expiresAt: entry.expiresAt ?? null,
|
|
639
|
+
sourceCoordinatorSessionId: entry.sourceCoordinatorSessionId ?? null,
|
|
640
|
+
sourceCoordinatorDaemonId: entry.sourceCoordinatorDaemonId ?? null,
|
|
641
|
+
createdAt: entry.createdAt,
|
|
642
|
+
updatedAt: entry.updatedAt,
|
|
643
|
+
});
|
|
644
|
+
this.maybeCheckpointWal();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
updateSessionDeliveryStatus(id: string, status: string, opts?: { lastError?: string; incrementAttempt?: boolean }): void {
|
|
648
|
+
const now = new Date().toISOString();
|
|
649
|
+
if (opts?.incrementAttempt) {
|
|
650
|
+
this.db.prepare(`
|
|
651
|
+
UPDATE mesh_session_delivery
|
|
652
|
+
SET status = @status, last_error = @lastError, attempt_count = attempt_count + 1, updated_at = @updatedAt
|
|
653
|
+
WHERE id = @id
|
|
654
|
+
`).run({ id, status, lastError: opts?.lastError ?? null, updatedAt: now });
|
|
655
|
+
} else {
|
|
656
|
+
this.db.prepare(`
|
|
657
|
+
UPDATE mesh_session_delivery
|
|
658
|
+
SET status = @status, last_error = @lastError, updated_at = @updatedAt
|
|
659
|
+
WHERE id = @id
|
|
660
|
+
`).run({ id, status, lastError: opts?.lastError ?? null, updatedAt: now });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
getActiveSessionDeliveries(meshId: string, sessionId?: string): Array<{
|
|
665
|
+
id: string; meshId: string; nodeId: string | null; sessionId: string | null;
|
|
666
|
+
providerType: string | null; taskId: string | null; kind: string; priority: number;
|
|
667
|
+
message: string; status: string; deliverAfter: string | null; expiresAt: string | null;
|
|
668
|
+
attemptCount: number; sourceCoordinatorSessionId: string | null;
|
|
669
|
+
sourceCoordinatorDaemonId: string | null; lastError: string | null;
|
|
670
|
+
createdAt: string; updatedAt: string;
|
|
671
|
+
}> {
|
|
672
|
+
const now = new Date().toISOString();
|
|
673
|
+
const sql = sessionId
|
|
674
|
+
? `SELECT * FROM mesh_session_delivery WHERE mesh_id = ? AND session_id = ? AND status NOT IN ('delivered','completed','failed','expired','cancelled') AND (expires_at IS NULL OR expires_at > ?) ORDER BY priority DESC, created_at ASC`
|
|
675
|
+
: `SELECT * FROM mesh_session_delivery WHERE mesh_id = ? AND status NOT IN ('delivered','completed','failed','expired','cancelled') AND (expires_at IS NULL OR expires_at > ?) ORDER BY priority DESC, created_at ASC`;
|
|
676
|
+
const rows = sessionId
|
|
677
|
+
? this.db.prepare(sql).all(meshId, sessionId, now) as Array<Record<string, unknown>>
|
|
678
|
+
: this.db.prepare(sql).all(meshId, now) as Array<Record<string, unknown>>;
|
|
679
|
+
return rows.map(r => ({
|
|
680
|
+
id: r.id as string,
|
|
681
|
+
meshId: r.mesh_id as string,
|
|
682
|
+
nodeId: r.node_id as string | null,
|
|
683
|
+
sessionId: r.session_id as string | null,
|
|
684
|
+
providerType: r.provider_type as string | null,
|
|
685
|
+
taskId: r.task_id as string | null,
|
|
686
|
+
kind: r.kind as string,
|
|
687
|
+
priority: r.priority as number,
|
|
688
|
+
message: r.message as string,
|
|
689
|
+
status: r.status as string,
|
|
690
|
+
deliverAfter: r.deliver_after as string | null,
|
|
691
|
+
expiresAt: r.expires_at as string | null,
|
|
692
|
+
attemptCount: r.attempt_count as number,
|
|
693
|
+
sourceCoordinatorSessionId: r.source_coordinator_session_id as string | null,
|
|
694
|
+
sourceCoordinatorDaemonId: r.source_coordinator_daemon_id as string | null,
|
|
695
|
+
lastError: r.last_error as string | null,
|
|
696
|
+
createdAt: r.created_at as string,
|
|
697
|
+
updatedAt: r.updated_at as string,
|
|
698
|
+
}));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
expireStaleSessionDeliveries(meshId: string): void {
|
|
702
|
+
const now = new Date().toISOString();
|
|
703
|
+
this.db.prepare(`
|
|
704
|
+
UPDATE mesh_session_delivery
|
|
705
|
+
SET status = 'expired', updated_at = ?
|
|
706
|
+
WHERE mesh_id = ? AND expires_at IS NOT NULL AND expires_at <= ?
|
|
707
|
+
AND status NOT IN ('delivered','completed','failed','expired','cancelled')
|
|
708
|
+
`).run(now, meshId, now);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
deleteSessionDeliveries(meshId: string): void {
|
|
712
|
+
this.db.prepare('DELETE FROM mesh_session_delivery WHERE mesh_id = ?').run(meshId);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ── Completion Conflict Diagnostics ──────────────────────────────────────
|
|
716
|
+
|
|
717
|
+
recordCompletionConflict(entry: {
|
|
718
|
+
id: string;
|
|
719
|
+
meshId: string;
|
|
720
|
+
fingerprint: string;
|
|
721
|
+
conflictingTaskId?: string;
|
|
722
|
+
conflictingSessionId?: string;
|
|
723
|
+
originalTaskId?: string;
|
|
724
|
+
originalSessionId?: string;
|
|
725
|
+
event: string;
|
|
726
|
+
createdAt: string;
|
|
727
|
+
}): void {
|
|
728
|
+
this.db.prepare(`
|
|
729
|
+
INSERT OR IGNORE INTO mesh_completion_conflicts
|
|
730
|
+
(id, mesh_id, fingerprint, conflicting_task_id, conflicting_session_id,
|
|
731
|
+
original_task_id, original_session_id, event, created_at)
|
|
732
|
+
VALUES (@id, @meshId, @fingerprint, @conflictingTaskId, @conflictingSessionId,
|
|
733
|
+
@originalTaskId, @originalSessionId, @event, @createdAt)
|
|
734
|
+
`).run({
|
|
735
|
+
id: entry.id,
|
|
736
|
+
meshId: entry.meshId,
|
|
737
|
+
fingerprint: entry.fingerprint,
|
|
738
|
+
conflictingTaskId: entry.conflictingTaskId ?? null,
|
|
739
|
+
conflictingSessionId: entry.conflictingSessionId ?? null,
|
|
740
|
+
originalTaskId: entry.originalTaskId ?? null,
|
|
741
|
+
originalSessionId: entry.originalSessionId ?? null,
|
|
742
|
+
event: entry.event,
|
|
743
|
+
createdAt: entry.createdAt,
|
|
744
|
+
});
|
|
745
|
+
this.maybeCheckpointWal();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
getRecentCompletionConflicts(meshId: string, limitMs: number = 60 * 60 * 1000): Array<{
|
|
749
|
+
id: string; meshId: string; fingerprint: string; conflictingTaskId: string | null;
|
|
750
|
+
conflictingSessionId: string | null; originalTaskId: string | null;
|
|
751
|
+
originalSessionId: string | null; event: string; createdAt: string;
|
|
752
|
+
}> {
|
|
753
|
+
const cutoff = new Date(Date.now() - limitMs).toISOString();
|
|
754
|
+
const rows = this.db.prepare(
|
|
755
|
+
'SELECT * FROM mesh_completion_conflicts WHERE mesh_id = ? AND created_at >= ? ORDER BY created_at DESC LIMIT 50'
|
|
756
|
+
).all(meshId, cutoff) as Array<Record<string, unknown>>;
|
|
757
|
+
return rows.map(r => ({
|
|
758
|
+
id: r.id as string,
|
|
759
|
+
meshId: r.mesh_id as string,
|
|
760
|
+
fingerprint: r.fingerprint as string,
|
|
761
|
+
conflictingTaskId: r.conflicting_task_id as string | null,
|
|
762
|
+
conflictingSessionId: r.conflicting_session_id as string | null,
|
|
763
|
+
originalTaskId: r.original_task_id as string | null,
|
|
764
|
+
originalSessionId: r.original_session_id as string | null,
|
|
765
|
+
event: r.event as string,
|
|
766
|
+
createdAt: r.created_at as string,
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
527
769
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { requireMeshHostQueueOwner } from './mesh-host-ownership.js';
|
|
3
3
|
import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
|
|
4
|
-
import {
|
|
4
|
+
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
5
5
|
|
|
6
6
|
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
|
|
7
7
|
export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
|
|
@@ -145,15 +145,15 @@ export function nodeSatisfiesRequiredTags(requiredTags: unknown, capabilityTags:
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
function withQueueLock<T>(_meshId: string, fn: () => T): T {
|
|
148
|
-
return
|
|
148
|
+
return MeshRuntimeStore.getInstance().transaction(fn);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
function readQueue(meshId: string): MeshWorkQueueEntry[] {
|
|
152
|
-
return
|
|
152
|
+
return MeshRuntimeStore.getInstance().getQueueEntries(meshId);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
156
|
-
|
|
156
|
+
MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
@@ -181,7 +181,7 @@ export function enqueueTask(
|
|
|
181
181
|
createdAt: new Date().toISOString(),
|
|
182
182
|
updatedAt: new Date().toISOString(),
|
|
183
183
|
};
|
|
184
|
-
|
|
184
|
+
MeshRuntimeStore.getInstance().insertQueueEntry(entry);
|
|
185
185
|
return entry;
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -189,18 +189,18 @@ export function enqueueTask(
|
|
|
189
189
|
* Get all tasks in the queue, optionally filtered by status.
|
|
190
190
|
*/
|
|
191
191
|
export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }): MeshWorkQueueEntry[] {
|
|
192
|
-
return
|
|
192
|
+
return MeshRuntimeStore.getInstance().getQueueEntries(meshId, opts?.status?.length ? opts.status : undefined);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
export function getMeshQueueRevision(meshId: string): string {
|
|
196
|
-
return
|
|
196
|
+
return MeshRuntimeStore.getInstance().getQueueRevision(meshId);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
200
|
* Find the next pending task that this node is allowed to claim, and mark it as assigned.
|
|
201
201
|
*/
|
|
202
202
|
export function claimNextTask(meshId: string, nodeId: string, sessionId: string, capabilityTags?: string[]): MeshWorkQueueEntry | null {
|
|
203
|
-
return
|
|
203
|
+
return MeshRuntimeStore.getInstance().claimNextQueueTask(meshId, nodeId, sessionId, capabilityTags);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
@@ -215,10 +215,10 @@ export function updateTaskStatus(
|
|
|
215
215
|
): MeshWorkQueueEntry | null {
|
|
216
216
|
requireMeshHostQueueOwner(opts);
|
|
217
217
|
return withQueueLock(meshId, () => {
|
|
218
|
-
const entry =
|
|
218
|
+
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
219
219
|
if (!entry) return null;
|
|
220
220
|
entry.status = status;
|
|
221
|
-
|
|
221
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
222
222
|
return entry;
|
|
223
223
|
});
|
|
224
224
|
}
|
|
@@ -229,11 +229,11 @@ export function recordTaskAutoLaunch(
|
|
|
229
229
|
autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
|
|
230
230
|
): MeshWorkQueueEntry | null {
|
|
231
231
|
return withQueueLock(meshId, () => {
|
|
232
|
-
const entry =
|
|
232
|
+
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
233
233
|
if (!entry) return null;
|
|
234
234
|
const now = new Date().toISOString();
|
|
235
235
|
entry.autoLaunch = { ...autoLaunch, updatedAt: now };
|
|
236
|
-
|
|
236
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
237
237
|
return entry;
|
|
238
238
|
});
|
|
239
239
|
}
|
|
@@ -248,13 +248,13 @@ export function cancelTask(
|
|
|
248
248
|
): MeshWorkQueueEntry | null {
|
|
249
249
|
requireMeshHostQueueOwner(opts);
|
|
250
250
|
return withQueueLock(meshId, () => {
|
|
251
|
-
const entry =
|
|
251
|
+
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
252
252
|
if (!entry) return null;
|
|
253
253
|
const now = new Date().toISOString();
|
|
254
254
|
entry.status = 'cancelled';
|
|
255
255
|
entry.cancelledAt = now;
|
|
256
256
|
if (opts?.reason) entry.cancelReason = opts.reason;
|
|
257
|
-
|
|
257
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
258
258
|
return entry;
|
|
259
259
|
});
|
|
260
260
|
}
|
|
@@ -276,7 +276,7 @@ export function requeueTask(
|
|
|
276
276
|
): MeshWorkQueueEntry | null {
|
|
277
277
|
requireMeshHostQueueOwner(opts);
|
|
278
278
|
return withQueueLock(meshId, () => {
|
|
279
|
-
const entry =
|
|
279
|
+
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
280
280
|
if (!entry) return null;
|
|
281
281
|
entry.status = 'pending';
|
|
282
282
|
delete entry.assignedNodeId;
|
|
@@ -290,7 +290,7 @@ export function requeueTask(
|
|
|
290
290
|
entry.requeuedAt = new Date().toISOString();
|
|
291
291
|
entry.requeueCount = (entry.requeueCount || 0) + 1;
|
|
292
292
|
if (opts?.reason) entry.requeueReason = opts.reason;
|
|
293
|
-
|
|
293
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
294
294
|
return entry;
|
|
295
295
|
});
|
|
296
296
|
}
|
|
@@ -306,10 +306,10 @@ export function updateSessionTaskStatus(
|
|
|
306
306
|
): MeshWorkQueueEntry | null {
|
|
307
307
|
return withQueueLock(meshId, () => {
|
|
308
308
|
const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
|
|
309
|
-
const entry =
|
|
309
|
+
const entry = MeshRuntimeStore.getInstance().findAssignedBySession(meshId, sessionId, occurredAtIso);
|
|
310
310
|
if (!entry) return null;
|
|
311
311
|
entry.status = status;
|
|
312
|
-
|
|
312
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
313
313
|
return entry;
|
|
314
314
|
});
|
|
315
315
|
}
|
|
@@ -339,7 +339,7 @@ export interface MeshWorkQueueStats {
|
|
|
339
339
|
* Return aggregate queue statistics for the given mesh.
|
|
340
340
|
*/
|
|
341
341
|
export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
|
|
342
|
-
const rows =
|
|
342
|
+
const rows = MeshRuntimeStore.getInstance().getQueueStatsByStatus(meshId);
|
|
343
343
|
const counts: Record<string, number> = {};
|
|
344
344
|
for (const r of rows) counts[r.status] = r.count;
|
|
345
345
|
const pending = counts['pending'] ?? 0;
|
|
@@ -358,33 +358,33 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
|
|
|
358
358
|
cancelled,
|
|
359
359
|
activeCounts: { pending, assigned },
|
|
360
360
|
historicalCounts: { completed, failed, cancelled },
|
|
361
|
-
activeAssignments:
|
|
361
|
+
activeAssignments: MeshRuntimeStore.getInstance().getActiveAssignmentDetails(meshId),
|
|
362
362
|
};
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
export function __replaceMeshQueueForTests(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
MeshRuntimeStore.getInstance().transaction(() => {
|
|
367
|
+
MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
|
|
368
368
|
});
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
export function __clearMeshQueueForTests(meshId: string): void {
|
|
372
|
-
|
|
372
|
+
MeshRuntimeStore.getInstance().deleteQueue(meshId);
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
export function __clearDirectDispatchesForTests(meshId: string): void {
|
|
376
|
-
|
|
376
|
+
MeshRuntimeStore.getInstance().deleteDirectDispatches(meshId);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
export function
|
|
380
|
-
|
|
379
|
+
export function __resetMeshRuntimeStoreForTests(): void {
|
|
380
|
+
MeshRuntimeStore.resetForTests();
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
// ── Direct Dispatch Tracking ─────────────────────────────────────────────────
|
|
384
384
|
// Persists direct (non-queue) task dispatches so buildMeshActiveWork can read
|
|
385
|
-
// active work from
|
|
385
|
+
// active work from MeshRuntimeStore instead of scanning ledger JSONL entries.
|
|
386
386
|
|
|
387
|
-
export type DirectDispatchRecord = ReturnType<
|
|
387
|
+
export type DirectDispatchRecord = ReturnType<MeshRuntimeStore['getActiveDirectDispatches']>[number];
|
|
388
388
|
|
|
389
389
|
export function insertDirectDispatch(
|
|
390
390
|
meshId: string,
|
|
@@ -401,7 +401,7 @@ export function insertDirectDispatch(
|
|
|
401
401
|
},
|
|
402
402
|
): void {
|
|
403
403
|
try {
|
|
404
|
-
|
|
404
|
+
MeshRuntimeStore.getInstance().insertDirectDispatch({ ...data, meshId });
|
|
405
405
|
} catch (e: any) {
|
|
406
406
|
process.stderr.write(`[adhdev-mesh] insertDirectDispatch failed for task ${data.taskId}: ${e?.message || e}\n`);
|
|
407
407
|
}
|
|
@@ -409,7 +409,7 @@ export function insertDirectDispatch(
|
|
|
409
409
|
|
|
410
410
|
export function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[] {
|
|
411
411
|
try {
|
|
412
|
-
return
|
|
412
|
+
return MeshRuntimeStore.getInstance().getActiveDirectDispatches(meshId);
|
|
413
413
|
} catch {
|
|
414
414
|
return [];
|
|
415
415
|
}
|
|
@@ -421,18 +421,18 @@ export function updateDirectDispatchStatus(
|
|
|
421
421
|
status: 'acked' | 'completed' | 'failed' | 'stale',
|
|
422
422
|
): void {
|
|
423
423
|
try {
|
|
424
|
-
|
|
424
|
+
MeshRuntimeStore.getInstance().updateDirectDispatchStatus(meshId, sessionId, status);
|
|
425
425
|
} catch { /* best-effort */ }
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
export function cleanupTerminalDirectDispatches(olderThanMs = 7 * 24 * 60 * 60_000): void {
|
|
429
429
|
try {
|
|
430
|
-
|
|
430
|
+
MeshRuntimeStore.getInstance().cleanupTerminalDirectDispatches(olderThanMs);
|
|
431
431
|
} catch { /* best-effort */ }
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
export function markStaleDirectDispatches(meshId: string, olderThanMs = 60 * 60_000): void {
|
|
435
435
|
try {
|
|
436
|
-
|
|
436
|
+
MeshRuntimeStore.getInstance().markStaleDirectDispatches(meshId, olderThanMs);
|
|
437
437
|
} catch { /* best-effort */ }
|
|
438
438
|
}
|
|
@@ -724,16 +724,24 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
724
724
|
? visibleStatus
|
|
725
725
|
: (suppressStaleParsedBusyStatus ? visibleStatus : (parsedChatStatus || visibleStatus)));
|
|
726
726
|
|
|
727
|
+
// If an AskUserQuestion prompt is awaiting user input, overlay status as
|
|
728
|
+
// waiting_choice. This is distinct from waiting_approval (tool-use consent)
|
|
729
|
+
// — the engine's isWaitingForResponse state is unchanged, so completion
|
|
730
|
+
// tracking continues normally once the user responds.
|
|
731
|
+
const hasInteractivePrompt = !!this.activeInteractivePrompt;
|
|
732
|
+
const finalStatus = hasInteractivePrompt ? 'waiting_choice' : visibleStatus;
|
|
733
|
+
const finalChatStatus = hasInteractivePrompt ? 'waiting_choice' : activeChatStatus;
|
|
734
|
+
|
|
727
735
|
return {
|
|
728
736
|
type: this.type,
|
|
729
737
|
name: this.provider.name,
|
|
730
738
|
category: 'cli',
|
|
731
|
-
status:
|
|
739
|
+
status: finalStatus,
|
|
732
740
|
mode: this.presentationMode,
|
|
733
741
|
activeChat: {
|
|
734
742
|
id: activeChatId,
|
|
735
743
|
title: parsedStatus?.title || dirName,
|
|
736
|
-
status:
|
|
744
|
+
status: finalChatStatus,
|
|
737
745
|
messages: statusMessages,
|
|
738
746
|
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
739
747
|
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
@@ -1265,6 +1273,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1265
1273
|
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1266
1274
|
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1267
1275
|
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1276
|
+
LOG.debug('CLI', `[${this.type}] finalAssistantEvidence: present=${finalAssistantEvidence.present} source=${finalAssistantEvidence.source} adapterOwnsMessagesElsewhere=${adapterOwnsMessagesElsewhere} parsedStatus=${parsedStatus}`);
|
|
1268
1277
|
if (!finalAssistantEvidence.present) {
|
|
1269
1278
|
if (adapterOwnsMessagesElsewhere) {
|
|
1270
1279
|
if (finalAssistantEvidence.source === 'external-native') {
|
|
@@ -1273,19 +1282,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1273
1282
|
LOG.info('CLI', `[${this.type}] external transcript probe: msgCount=${probe.msgCount} lastRole=${probe.lastRole || 'none'} lastKind=${probe.lastKind || 'none'} contentLen=${probe.contentLen} sourceMtime=${probe.sourceMtimeMs ?? 'unknown'} mtimeAge=${probe.mtimeAgeMs ?? 'unknown'}ms`);
|
|
1274
1283
|
pending.loggedTranscriptProbe = true;
|
|
1275
1284
|
}
|
|
1285
|
+
LOG.debug('CLI', `[${this.type}] external-native probe result: lastRole=${probe?.lastRole} contentLen=${probe?.contentLen}`);
|
|
1286
|
+
if (probe?.lastRole === 'assistant' && (probe.contentLen ?? 0) > 0) {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1276
1289
|
if (this.type === 'antigravity-cli') {
|
|
1277
1290
|
return null;
|
|
1278
1291
|
}
|
|
1279
1292
|
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1280
1293
|
}
|
|
1281
|
-
// SpecCliAdapter never populates parsed.messages — chat history flows
|
|
1282
|
-
// through the daemon's native-history pipeline, not the status hook.
|
|
1283
|
-
// If that pipeline is unavailable, keep the old skip behavior for
|
|
1284
|
-
// providers that have not opted into strict final-assistant evidence.
|
|
1285
1294
|
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1286
1295
|
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1287
1296
|
}
|
|
1288
1297
|
} else {
|
|
1298
|
+
LOG.debug('CLI', `[${this.type}] missing_final_assistant (not ownsExternal) requiresFinalAssistant=${!!(this.provider as any).requiresFinalAssistantBeforeIdle}`);
|
|
1289
1299
|
return {
|
|
1290
1300
|
reason: 'missing_final_assistant',
|
|
1291
1301
|
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
@@ -1331,6 +1341,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1331
1341
|
const latestVisibleStatus = externalNativeFinal && isCliGeneratingLikeStatus(latestStatus.status)
|
|
1332
1342
|
? 'idle'
|
|
1333
1343
|
: (latestAutoApproveActive ? 'generating' : latestStatus.status);
|
|
1344
|
+
LOG.debug('CLI', `[${this.type}] flush attempt: adapterStatus=${latestStatus.status} latestVisible=${latestVisibleStatus} externalNativeFinal=${!!externalNativeFinal} generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse} hasPartial=${!!this.adapter.getPartialResponse?.()}`);
|
|
1334
1345
|
if (latestVisibleStatus !== 'idle') {
|
|
1335
1346
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
1336
1347
|
this.completedDebouncePending = null;
|
|
@@ -1342,6 +1353,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1342
1353
|
if (block) {
|
|
1343
1354
|
const blockReason = block.reason;
|
|
1344
1355
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
1356
|
+
LOG.debug('CLI', `[${this.type}] finalization block: reason=${blockReason} terminal=${block.terminal} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
|
|
1345
1357
|
if ((block.terminal && !block.allowTimeout) || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
1346
1358
|
if (pending.loggedBlockReason !== blockReason) {
|
|
1347
1359
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
@@ -1485,10 +1497,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1485
1497
|
if (newStatus !== this.lastStatus) {
|
|
1486
1498
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
1487
1499
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
1500
|
+
// If a completion event is already pending and the turn has ended
|
|
1501
|
+
// (generatingStartedAt===0), the PTY is painting its prompt area
|
|
1502
|
+
// after completing. Ignore this blip — do not cancel the pending
|
|
1503
|
+
// completion and do not advance lastStatus to generating.
|
|
1504
|
+
if (this.completedDebouncePending && this.generatingStartedAt === 0) {
|
|
1505
|
+
LOG.debug('CLI', `[${this.type}] ignoring post-completion PTY generating blip (generatingStartedAt=0)`);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1488
1508
|
this.suppressIdleHistoryReplay = false;
|
|
1489
1509
|
// Cancel any pending completed event (multi-step: idle→generating resume)
|
|
1490
1510
|
if (this.completedDebouncePending) {
|
|
1491
|
-
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating)`);
|
|
1511
|
+
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating) generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse}`);
|
|
1492
1512
|
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1493
1513
|
this.completedDebouncePending = null;
|
|
1494
1514
|
}
|
|
@@ -1586,7 +1606,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1586
1606
|
firstObservedAt: now,
|
|
1587
1607
|
previousStatus: this.lastStatus,
|
|
1588
1608
|
};
|
|
1589
|
-
this.
|
|
1609
|
+
const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
|
|
1610
|
+
const flushDelay = ownsExternalHistory ? 0 : 3000;
|
|
1611
|
+
LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
|
|
1612
|
+
this.scheduleCompletedDebounceFlush(flushDelay);
|
|
1590
1613
|
}
|
|
1591
1614
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
1592
1615
|
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
@@ -16,7 +16,7 @@ import type { InteractivePrompt } from './types/interactive-prompt.js';
|
|
|
16
16
|
|
|
17
17
|
// ─── ProviderState — Discriminated union by category ─────────────
|
|
18
18
|
|
|
19
|
-
export type ProviderStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
|
|
19
|
+
export type ProviderStatus = 'idle' | 'generating' | 'waiting_approval' | 'waiting_choice' | 'error' | 'stopped' | 'starting';
|
|
20
20
|
|
|
21
21
|
export interface ProviderRuntimeWriteOwner {
|
|
22
22
|
clientId: string;
|