@adhdev/daemon-core 0.9.82-rc.136 → 0.9.82-rc.138
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/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +169 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +72 -74
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +5 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3507 -2288
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3515 -2301
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +54 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/mesh-active-work.d.ts +7 -1
- package/dist/mesh/mesh-events.d.ts +10 -4
- package/dist/mesh/mesh-ledger.d.ts +21 -1
- package/dist/mesh/mesh-refine-status.d.ts +2 -3
- package/dist/mesh/mesh-work-queue.d.ts +17 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
- package/dist/providers/contracts.d.ts +19 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/repo-mesh-types.d.ts +5 -0
- package/dist/shared-types.d.ts +7 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +9 -0
- package/src/cli-adapters/cli-script-runner.ts +145 -0
- package/src/cli-adapters/cli-state-engine.ts +1054 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +0 -1
- package/src/cli-adapters/provider-cli-adapter.ts +413 -1399
- package/src/cli-adapters/provider-cli-parse.ts +3 -0
- package/src/cli-adapters/provider-cli-shared.ts +17 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +715 -368
- package/src/commands/router.ts +22 -2
- package/src/config/chat-history.ts +43 -16
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +3 -2
- package/src/mesh/beads-db.ts +305 -2
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +12 -17
- package/src/mesh/mesh-active-work.ts +162 -59
- package/src/mesh/mesh-events.ts +198 -53
- package/src/mesh/mesh-ledger.ts +321 -105
- package/src/mesh/mesh-refine-status.ts +2 -3
- package/src/mesh/mesh-work-queue.ts +116 -120
- package/src/mesh/worktree-bootstrap-config.ts +17 -4
- package/src/providers/contracts.ts +19 -0
- package/src/providers/provider-loader.ts +21 -7
- package/src/providers/provider-schema.ts +12 -0
- package/src/providers/read-chat-contract.ts +74 -14
- package/src/providers/transcript-v2.ts +567 -0
- package/src/repo-mesh-types.ts +10 -0
- package/src/shared-types.ts +7 -0
- package/src/status/snapshot.ts +35 -11
- package/src/types.ts +5 -0
package/src/commands/router.ts
CHANGED
|
@@ -1434,10 +1434,18 @@ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReach
|
|
|
1434
1434
|
|
|
1435
1435
|
function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
1436
1436
|
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1437
|
+
process.stderr.write(
|
|
1438
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via mesh.policy. `
|
|
1439
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
1440
|
+
);
|
|
1437
1441
|
return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
|
|
1438
1442
|
}
|
|
1439
1443
|
const loaded = loadMeshRefineConfig(mesh, workspace);
|
|
1440
1444
|
if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1445
|
+
process.stderr.write(
|
|
1446
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via ${loaded.path || loaded.source}. `
|
|
1447
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
1448
|
+
);
|
|
1441
1449
|
return { enabled: true, source: loaded.path || loaded.source };
|
|
1442
1450
|
}
|
|
1443
1451
|
return { enabled: false };
|
|
@@ -3637,7 +3645,13 @@ export class DaemonCommandRouter {
|
|
|
3637
3645
|
|
|
3638
3646
|
case 'get_pending_mesh_events': {
|
|
3639
3647
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3640
|
-
|
|
3648
|
+
// (B3) Respect coordinatorDaemonId when the caller declares it
|
|
3649
|
+
// so unicast events route to the right coordinator instead of
|
|
3650
|
+
// being silently consumed by the first drainer.
|
|
3651
|
+
const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
3652
|
+
? args.coordinatorDaemonId.trim()
|
|
3653
|
+
: undefined;
|
|
3654
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
|
|
3641
3655
|
return { success: true, events };
|
|
3642
3656
|
}
|
|
3643
3657
|
|
|
@@ -5879,7 +5893,13 @@ export class DaemonCommandRouter {
|
|
|
5879
5893
|
nodeStatuses.push(status);
|
|
5880
5894
|
}
|
|
5881
5895
|
|
|
5882
|
-
|
|
5896
|
+
// (B3) Pass coordinatorDaemonId when the caller declares
|
|
5897
|
+
// it so v1.5 unicast routing (targetCoordinatorDaemonId)
|
|
5898
|
+
// delivers events to the right coordinator.
|
|
5899
|
+
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
5900
|
+
? args.coordinatorDaemonId.trim()
|
|
5901
|
+
: undefined;
|
|
5902
|
+
const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
|
|
5883
5903
|
const previewFreshness = (() => {
|
|
5884
5904
|
const localRepoRoot = nodeStatuses
|
|
5885
5905
|
.map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
|
|
@@ -1375,19 +1375,42 @@ function normalizeProviderNativeHistoryRecords(agentType: string, historySession
|
|
|
1375
1375
|
if (!Array.isArray(records)) return [];
|
|
1376
1376
|
const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
|
|
1377
1377
|
return records
|
|
1378
|
-
.map((record: any) =>
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1378
|
+
.map((record: any) => {
|
|
1379
|
+
const base: HistoryMessage = {
|
|
1380
|
+
ts: typeof record?.ts === 'string' ? record.ts : new Date(Number(record?.receivedAt) || Date.now()).toISOString(),
|
|
1381
|
+
receivedAt: Number(record?.receivedAt) || Date.parse(record?.ts || '') || Date.now(),
|
|
1382
|
+
role: record?.role,
|
|
1383
|
+
content: String(record?.content || ''),
|
|
1384
|
+
kind: record?.kind || (record?.role === 'system' ? 'session_start' : 'standard'),
|
|
1385
|
+
senderName: record?.senderName,
|
|
1386
|
+
agent: agentType,
|
|
1387
|
+
instanceId: record?.instanceId,
|
|
1388
|
+
historySessionId: normalizeSavedHistorySessionId(record?.historySessionId || normalizedSessionId),
|
|
1389
|
+
sessionTitle: record?.sessionTitle,
|
|
1390
|
+
workspace: record?.workspace,
|
|
1391
|
+
} as HistoryMessage;
|
|
1392
|
+
// (A2.3 v2 identity passthrough) — if the producer (native_history.js)
|
|
1393
|
+
// emitted v2 stable identity, keep it across the sanitize layer so
|
|
1394
|
+
// downstream (chat-commands.ts normalizeNativeHistoryMessages) sees
|
|
1395
|
+
// the producer's contract output instead of recomputing from index
|
|
1396
|
+
// and content hash. v1 producers without these fields are unaffected.
|
|
1397
|
+
if (typeof record?.providerUnitKey === 'string' && record.providerUnitKey) {
|
|
1398
|
+
(base as any).providerUnitKey = record.providerUnitKey;
|
|
1399
|
+
}
|
|
1400
|
+
if (typeof record?.bubbleId === 'string' && record.bubbleId) {
|
|
1401
|
+
(base as any).bubbleId = record.bubbleId;
|
|
1402
|
+
}
|
|
1403
|
+
if (typeof record?.sequence === 'number' && Number.isFinite(record.sequence)) {
|
|
1404
|
+
(base as any).sequence = record.sequence;
|
|
1405
|
+
}
|
|
1406
|
+
if (typeof record?._turnKey === 'string' && record._turnKey) {
|
|
1407
|
+
(base as any)._turnKey = record._turnKey;
|
|
1408
|
+
}
|
|
1409
|
+
if (typeof record?.bubbleState === 'string' && record.bubbleState) {
|
|
1410
|
+
(base as any).bubbleState = record.bubbleState;
|
|
1411
|
+
}
|
|
1412
|
+
return sanitizeHistoryMessage(agentType, base);
|
|
1413
|
+
})
|
|
1391
1414
|
.filter(Boolean) as HistoryMessage[];
|
|
1392
1415
|
}
|
|
1393
1416
|
|
|
@@ -1397,6 +1420,7 @@ function callProviderNativeHistoryRead(
|
|
|
1397
1420
|
scripts: ProviderNativeHistoryScripts | undefined,
|
|
1398
1421
|
historySessionId: string | undefined,
|
|
1399
1422
|
workspace?: string,
|
|
1423
|
+
excludeInProgressTurn?: boolean,
|
|
1400
1424
|
): ProviderNativeHistoryReadResult | null {
|
|
1401
1425
|
const fn = getProviderNativeHistoryScript(scripts, canonicalHistory, 'readSession');
|
|
1402
1426
|
if (!fn) return null;
|
|
@@ -1408,7 +1432,8 @@ function callProviderNativeHistoryRead(
|
|
|
1408
1432
|
workspace,
|
|
1409
1433
|
format: canonicalHistory?.format,
|
|
1410
1434
|
watchPath: canonicalHistory?.watchPath,
|
|
1411
|
-
|
|
1435
|
+
excludeInProgressTurn: excludeInProgressTurn === true,
|
|
1436
|
+
args: { sessionId: normalizedSessionId, historySessionId: normalizedSessionId, workspace, excludeInProgressTurn: excludeInProgressTurn === true },
|
|
1412
1437
|
});
|
|
1413
1438
|
if (!result || typeof result !== 'object') return null;
|
|
1414
1439
|
const records = normalizeProviderNativeHistoryRecords(agentType, normalizedSessionId, (result as any).messages || (result as any).records);
|
|
@@ -1430,11 +1455,12 @@ function buildNativeHistoryReadResult(
|
|
|
1430
1455
|
scripts: ProviderNativeHistoryScripts | undefined,
|
|
1431
1456
|
historySessionId: string | undefined,
|
|
1432
1457
|
workspace?: string,
|
|
1458
|
+
excludeInProgressTurn?: boolean,
|
|
1433
1459
|
): ProviderNativeHistoryReadResult | null {
|
|
1434
1460
|
const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
|
|
1435
1461
|
const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
|
|
1436
1462
|
if (!canonicalHistory || (!normalizedSessionId && !normalizedWorkspace) || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
|
|
1437
|
-
return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace);
|
|
1463
|
+
return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace, excludeInProgressTurn);
|
|
1438
1464
|
}
|
|
1439
1465
|
|
|
1440
1466
|
function materializeNativeHistoryToMirror(
|
|
@@ -1490,6 +1516,7 @@ export function readProviderChatHistory(
|
|
|
1490
1516
|
excludeRecentCount?: number;
|
|
1491
1517
|
historyBehavior?: ProviderHistoryBehavior;
|
|
1492
1518
|
scripts?: ProviderNativeHistoryScripts;
|
|
1519
|
+
excludeInProgressTurn?: boolean;
|
|
1493
1520
|
} = {},
|
|
1494
1521
|
): {
|
|
1495
1522
|
messages: HistoryMessage[];
|
|
@@ -1503,7 +1530,7 @@ export function readProviderChatHistory(
|
|
|
1503
1530
|
unavailableReason?: string;
|
|
1504
1531
|
} {
|
|
1505
1532
|
if (isNativeSourceCanonicalHistory(options.canonicalHistory) && (options.historySessionId || options.workspace)) {
|
|
1506
|
-
const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace);
|
|
1533
|
+
const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace, options.excludeInProgressTurn);
|
|
1507
1534
|
if (!nativeResult) return { messages: [], hasMore: false, source: 'native-unavailable' };
|
|
1508
1535
|
return {
|
|
1509
1536
|
...pageHistoryRecords(agentType, nativeResult.records, options.offset || 0, options.limit || 30, options.excludeRecentCount || 0, options.historyBehavior),
|
package/src/git/git-worktree.ts
CHANGED
|
@@ -116,8 +116,11 @@ export async function createWorktree(opts: WorktreeCreateOptions): Promise<Workt
|
|
|
116
116
|
});
|
|
117
117
|
} catch (error: any) {
|
|
118
118
|
const stderr = typeof error.stderr === 'string' ? error.stderr : '';
|
|
119
|
-
// Clean error messages for common failures
|
|
120
119
|
if (/already exists/i.test(stderr)) {
|
|
120
|
+
// Distinguish directory-collision (TOCTOU race) from branch-already-exists
|
|
121
|
+
if (existsSync(targetDir)) {
|
|
122
|
+
throw new Error(`Worktree target directory was created concurrently: ${targetDir}`);
|
|
123
|
+
}
|
|
121
124
|
throw new Error(`Branch '${branch}' already exists or is checked out in another worktree`);
|
|
122
125
|
}
|
|
123
126
|
throw new Error(`git worktree add failed: ${stderr.trim() || error.message}`);
|
|
@@ -170,6 +173,10 @@ export async function removeWorktree(repoRoot: string, worktreePath: string, opt
|
|
|
170
173
|
const stdout = typeof error.stdout === 'string' ? error.stdout : '';
|
|
171
174
|
const detail = `${stderr}\n${stdout}\n${error.message || ''}`;
|
|
172
175
|
if (opts.allowSubmoduleForceFallback && SUBMODULE_WORKTREE_REMOVE_RE.test(detail)) {
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`[adhdev-mesh] WARNING: git worktree remove --force fallback for submodule worktree '${worktreePath}'. `
|
|
178
|
+
+ `Any uncommitted changes inside submodules will be lost.\n`,
|
|
179
|
+
);
|
|
173
180
|
try {
|
|
174
181
|
await execFileAsync('git', ['worktree', 'remove', '--force', worktreePath], {
|
|
175
182
|
cwd: repoRoot,
|
package/src/index.ts
CHANGED
|
@@ -115,6 +115,7 @@ export type {
|
|
|
115
115
|
RepoMeshLedgerEntryStatus,
|
|
116
116
|
RepoMeshLedgerSummaryStatus,
|
|
117
117
|
RepoMeshLedgerStatus,
|
|
118
|
+
MeshAsyncJobLifecycle,
|
|
118
119
|
} from './repo-mesh-types.js';
|
|
119
120
|
export { DEFAULT_MESH_POLICY } from './repo-mesh-types.js';
|
|
120
121
|
|
|
@@ -195,8 +196,8 @@ export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence }
|
|
|
195
196
|
export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
|
|
196
197
|
|
|
197
198
|
// ── Mesh Work Queue (GUPP) ──
|
|
198
|
-
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest } from './mesh/mesh-work-queue.js';
|
|
199
|
-
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult } from './mesh/mesh-work-queue.js';
|
|
199
|
+
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches } from './mesh/mesh-work-queue.js';
|
|
200
|
+
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult, DirectDispatchRecord } from './mesh/mesh-work-queue.js';
|
|
200
201
|
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary } from './mesh/mesh-active-work.js';
|
|
201
202
|
export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource, MeshStaleDirectWorkSummary } from './mesh/mesh-active-work.js';
|
|
202
203
|
export { buildMeshAsyncRefineJobs } from './mesh/mesh-refine-status.js';
|
package/src/mesh/beads-db.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from 'fs';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { getLedgerDir } from './mesh-ledger.js';
|
|
@@ -28,12 +28,18 @@ function legacyQueuePath(meshId: string): string {
|
|
|
28
28
|
export class BeadsDB {
|
|
29
29
|
private static instance: BeadsDB | undefined;
|
|
30
30
|
private readonly db: DatabaseHandle;
|
|
31
|
+
private readonly dbPath: string;
|
|
31
32
|
private readonly migratedMeshIds = new Set<string>();
|
|
33
|
+
private fingerprintSweepCounter = 0;
|
|
34
|
+
private walWriteCounter = 0;
|
|
35
|
+
private static readonly WAL_CHECK_INTERVAL = 500;
|
|
36
|
+
private static readonly WAL_MAX_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
32
37
|
|
|
33
38
|
private constructor(dbPath: string) {
|
|
34
39
|
const dir = dirname(dbPath);
|
|
35
40
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
36
41
|
|
|
42
|
+
this.dbPath = dbPath;
|
|
37
43
|
this.db = new (loadDatabaseCtor())(dbPath);
|
|
38
44
|
this.db.pragma('journal_mode = WAL');
|
|
39
45
|
this.db.pragma('synchronous = NORMAL');
|
|
@@ -81,9 +87,72 @@ export class BeadsDB {
|
|
|
81
87
|
ON mesh_queue(mesh_id, status, created_at);
|
|
82
88
|
CREATE INDEX IF NOT EXISTS idx_mesh_queue_assignment
|
|
83
89
|
ON mesh_queue(mesh_id, assigned_node_id, assigned_session_id, status);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS mesh_completion_fingerprints (
|
|
92
|
+
fingerprint TEXT PRIMARY KEY,
|
|
93
|
+
expires_at INTEGER NOT NULL
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS mesh_direct_dispatches (
|
|
97
|
+
task_id TEXT PRIMARY KEY,
|
|
98
|
+
mesh_id TEXT NOT NULL,
|
|
99
|
+
node_id TEXT,
|
|
100
|
+
session_id TEXT,
|
|
101
|
+
provider_type TEXT,
|
|
102
|
+
message TEXT NOT NULL,
|
|
103
|
+
task_mode TEXT,
|
|
104
|
+
via TEXT NOT NULL,
|
|
105
|
+
status TEXT NOT NULL DEFAULT 'dispatched',
|
|
106
|
+
dispatched_to_idle_session INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
dispatched_at TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT NOT NULL
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_direct_dispatches_mesh_session
|
|
112
|
+
ON mesh_direct_dispatches(mesh_id, session_id, status);
|
|
84
113
|
`);
|
|
85
114
|
}
|
|
86
115
|
|
|
116
|
+
hasCompletionFingerprint(fingerprint: string): boolean {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const row = this.db
|
|
119
|
+
.prepare('SELECT 1 FROM mesh_completion_fingerprints WHERE fingerprint = ? AND expires_at > ?')
|
|
120
|
+
.get(fingerprint, now) as { 1: number } | undefined;
|
|
121
|
+
// Sweep expired fingerprints every 100 reads so stale rows don't accumulate
|
|
122
|
+
// even during read-heavy (non-write) periods when recordFingerprintSeen is idle.
|
|
123
|
+
if (++this.fingerprintSweepCounter >= 100) {
|
|
124
|
+
this.fingerprintSweepCounter = 0;
|
|
125
|
+
this.sweepExpiredFingerprints();
|
|
126
|
+
}
|
|
127
|
+
return row !== undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
recordCompletionFingerprint(fingerprint: string, ttlMs: number): void {
|
|
131
|
+
const expiresAt = Date.now() + ttlMs;
|
|
132
|
+
this.db.prepare('INSERT OR REPLACE INTO mesh_completion_fingerprints (fingerprint, expires_at) VALUES (?, ?)')
|
|
133
|
+
.run(fingerprint, expiresAt);
|
|
134
|
+
this.maybeCheckpointWal();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
sweepExpiredFingerprints(): void {
|
|
138
|
+
this.db.prepare('DELETE FROM mesh_completion_fingerprints WHERE expires_at <= ?').run(Date.now());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private maybeCheckpointWal(): void {
|
|
142
|
+
if (++this.walWriteCounter < BeadsDB.WAL_CHECK_INTERVAL) return;
|
|
143
|
+
this.walWriteCounter = 0;
|
|
144
|
+
try {
|
|
145
|
+
const walPath = `${this.dbPath}-wal`;
|
|
146
|
+
if (!existsSync(walPath)) return;
|
|
147
|
+
const size = statSync(walPath).size;
|
|
148
|
+
if (size < BeadsDB.WAL_MAX_BYTES) return;
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
`[adhdev-mesh] WAL file ${Math.round(size / 1024 / 1024)}MB exceeds threshold; forcing checkpoint\n`,
|
|
151
|
+
);
|
|
152
|
+
this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
153
|
+
} catch { /* best-effort */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
87
156
|
private ensureLegacyQueueMigrated(meshId: string): void {
|
|
88
157
|
if (this.migratedMeshIds.has(meshId)) return;
|
|
89
158
|
this.migratedMeshIds.add(meshId);
|
|
@@ -136,7 +205,8 @@ export class BeadsDB {
|
|
|
136
205
|
const rows = this.db
|
|
137
206
|
.prepare('SELECT id, status, updated_at FROM mesh_queue WHERE mesh_id = ? ORDER BY id ASC')
|
|
138
207
|
.all(meshId) as Array<{ id: string; status: string; updated_at: string }>;
|
|
139
|
-
|
|
208
|
+
// Tab as field delimiter (UUIDs and ISO timestamps never contain tabs).
|
|
209
|
+
return rows.map(row => `${row.id}\t${row.status}\t${row.updated_at}`).join('\n');
|
|
140
210
|
}
|
|
141
211
|
|
|
142
212
|
replaceQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
@@ -152,6 +222,7 @@ export class BeadsDB {
|
|
|
152
222
|
`);
|
|
153
223
|
deleteStmt.run(meshId);
|
|
154
224
|
for (const entry of queue) insert.run(this.toRow(entry));
|
|
225
|
+
this.maybeCheckpointWal();
|
|
155
226
|
}
|
|
156
227
|
|
|
157
228
|
deleteQueue(meshId: string): void {
|
|
@@ -159,6 +230,134 @@ export class BeadsDB {
|
|
|
159
230
|
this.migratedMeshIds.delete(meshId);
|
|
160
231
|
}
|
|
161
232
|
|
|
233
|
+
insertQueueEntry(entry: MeshWorkQueueEntry): void {
|
|
234
|
+
this.db.prepare(`
|
|
235
|
+
INSERT INTO mesh_queue (
|
|
236
|
+
id, mesh_id, status, target_node_id, target_session_id,
|
|
237
|
+
assigned_node_id, assigned_session_id, created_at, updated_at, payload
|
|
238
|
+
) VALUES (
|
|
239
|
+
@id, @meshId, @status, @targetNodeId, @targetSessionId,
|
|
240
|
+
@assignedNodeId, @assignedSessionId, @createdAt, @updatedAt, @payload
|
|
241
|
+
)
|
|
242
|
+
`).run(this.toRow(entry));
|
|
243
|
+
this.maybeCheckpointWal();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
updateQueueEntry(entry: MeshWorkQueueEntry): void {
|
|
247
|
+
const now = new Date().toISOString();
|
|
248
|
+
entry.updatedAt = now;
|
|
249
|
+
this.db.prepare(`
|
|
250
|
+
UPDATE mesh_queue SET
|
|
251
|
+
status = @status,
|
|
252
|
+
target_node_id = @targetNodeId,
|
|
253
|
+
target_session_id = @targetSessionId,
|
|
254
|
+
assigned_node_id = @assignedNodeId,
|
|
255
|
+
assigned_session_id = @assignedSessionId,
|
|
256
|
+
updated_at = @updatedAt,
|
|
257
|
+
payload = @payload
|
|
258
|
+
WHERE id = @id AND mesh_id = @meshId
|
|
259
|
+
`).run(this.toRow(entry));
|
|
260
|
+
this.maybeCheckpointWal();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
findQueueEntryById(meshId: string, id: string): MeshWorkQueueEntry | null {
|
|
264
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
265
|
+
const row = this.db.prepare(
|
|
266
|
+
'SELECT payload FROM mesh_queue WHERE id = ? AND mesh_id = ?'
|
|
267
|
+
).get(id, meshId) as { payload: string } | undefined;
|
|
268
|
+
return row ? JSON.parse(row.payload) as MeshWorkQueueEntry : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
hasActiveAssignment(meshId: string, sessionId: string, nodeId: string): boolean {
|
|
272
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
273
|
+
const row = this.db.prepare(`
|
|
274
|
+
SELECT 1 FROM mesh_queue
|
|
275
|
+
WHERE mesh_id = ? AND status = 'assigned'
|
|
276
|
+
AND (assigned_session_id = ? OR assigned_node_id = ?)
|
|
277
|
+
LIMIT 1
|
|
278
|
+
`).get(meshId, sessionId, nodeId);
|
|
279
|
+
return row !== undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// O(1) claim: transaction ensures only one session claims a pending task
|
|
283
|
+
claimNextQueueTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
|
|
284
|
+
return this.transaction(() => {
|
|
285
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
286
|
+
if (this.hasActiveAssignment(meshId, sessionId, nodeId)) return null;
|
|
287
|
+
|
|
288
|
+
// Priority: session-targeted > node-targeted (no session) > unconstrained
|
|
289
|
+
const row = (
|
|
290
|
+
this.db.prepare(`
|
|
291
|
+
SELECT payload FROM mesh_queue
|
|
292
|
+
WHERE mesh_id = ? AND status = 'pending' AND target_session_id = ?
|
|
293
|
+
ORDER BY created_at ASC LIMIT 1
|
|
294
|
+
`).get(meshId, sessionId) as { payload: string } | undefined
|
|
295
|
+
) || (
|
|
296
|
+
this.db.prepare(`
|
|
297
|
+
SELECT payload FROM mesh_queue
|
|
298
|
+
WHERE mesh_id = ? AND status = 'pending' AND target_node_id = ? AND target_session_id IS NULL
|
|
299
|
+
ORDER BY created_at ASC LIMIT 1
|
|
300
|
+
`).get(meshId, nodeId) as { payload: string } | undefined
|
|
301
|
+
) || (
|
|
302
|
+
this.db.prepare(`
|
|
303
|
+
SELECT payload FROM mesh_queue
|
|
304
|
+
WHERE mesh_id = ? AND status = 'pending' AND target_node_id IS NULL AND target_session_id IS NULL
|
|
305
|
+
ORDER BY created_at ASC LIMIT 1
|
|
306
|
+
`).get(meshId) as { payload: string } | undefined
|
|
307
|
+
);
|
|
308
|
+
if (!row) return null;
|
|
309
|
+
|
|
310
|
+
const entry = JSON.parse(row.payload) as MeshWorkQueueEntry;
|
|
311
|
+
const now = new Date().toISOString();
|
|
312
|
+
entry.status = 'assigned';
|
|
313
|
+
entry.assignedNodeId = nodeId;
|
|
314
|
+
entry.assignedSessionId = sessionId;
|
|
315
|
+
entry.dispatchTimestamp = now;
|
|
316
|
+
entry.updatedAt = now;
|
|
317
|
+
|
|
318
|
+
this.db.prepare(`
|
|
319
|
+
UPDATE mesh_queue SET
|
|
320
|
+
status = 'assigned', assigned_node_id = ?, assigned_session_id = ?,
|
|
321
|
+
updated_at = ?, payload = ?
|
|
322
|
+
WHERE id = ? AND mesh_id = ?
|
|
323
|
+
`).run(nodeId, sessionId, now, JSON.stringify(entry), entry.id, meshId);
|
|
324
|
+
|
|
325
|
+
this.maybeCheckpointWal();
|
|
326
|
+
return entry;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getQueueStatsByStatus(meshId: string): { status: string; count: number }[] {
|
|
331
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
332
|
+
return this.db.prepare(
|
|
333
|
+
`SELECT status, COUNT(*) as count FROM mesh_queue WHERE mesh_id = ? GROUP BY status`
|
|
334
|
+
).all(meshId) as { status: string; count: number }[];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
getActiveAssignmentDetails(meshId: string): Array<{ id: string; nodeId?: string; sessionId?: string; message: string }> {
|
|
338
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
339
|
+
const rows = this.db.prepare(`
|
|
340
|
+
SELECT assigned_node_id, assigned_session_id, payload
|
|
341
|
+
FROM mesh_queue WHERE mesh_id = ? AND status = 'assigned'
|
|
342
|
+
`).all(meshId) as Array<{ assigned_node_id: string | null; assigned_session_id: string | null; payload: string }>;
|
|
343
|
+
return rows.map(r => {
|
|
344
|
+
let id = '', message = '';
|
|
345
|
+
try { const e = JSON.parse(r.payload) as MeshWorkQueueEntry; id = e.id; message = e.message; } catch { /* ignore */ }
|
|
346
|
+
return { id, nodeId: r.assigned_node_id ?? undefined, sessionId: r.assigned_session_id ?? undefined, message };
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
findAssignedBySession(meshId: string, sessionId: string, occurredAtIso?: string): MeshWorkQueueEntry | null {
|
|
351
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
352
|
+
// Use updated_at (≈ dispatchTimestamp when status='assigned') for the occurredAt filter.
|
|
353
|
+
const sql = occurredAtIso
|
|
354
|
+
? `SELECT payload FROM mesh_queue WHERE mesh_id = ? AND assigned_session_id = ? AND status = 'assigned' AND updated_at <= ? ORDER BY updated_at DESC LIMIT 1`
|
|
355
|
+
: `SELECT payload FROM mesh_queue WHERE mesh_id = ? AND assigned_session_id = ? AND status = 'assigned' ORDER BY updated_at DESC LIMIT 1`;
|
|
356
|
+
const args: string[] = occurredAtIso ? [meshId, sessionId, occurredAtIso] : [meshId, sessionId];
|
|
357
|
+
const row = this.db.prepare(sql).get(...args as [string, string, string?]) as { payload: string } | undefined;
|
|
358
|
+
return row ? JSON.parse(row.payload) as MeshWorkQueueEntry : null;
|
|
359
|
+
}
|
|
360
|
+
|
|
162
361
|
private toRow(entry: MeshWorkQueueEntry): Record<string, unknown> {
|
|
163
362
|
return {
|
|
164
363
|
id: entry.id,
|
|
@@ -173,4 +372,108 @@ export class BeadsDB {
|
|
|
173
372
|
payload: JSON.stringify(entry),
|
|
174
373
|
};
|
|
175
374
|
}
|
|
375
|
+
|
|
376
|
+
// ── Direct Dispatch Tracking ─────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
insertDirectDispatch(entry: {
|
|
379
|
+
taskId: string;
|
|
380
|
+
meshId: string;
|
|
381
|
+
nodeId?: string;
|
|
382
|
+
sessionId?: string;
|
|
383
|
+
providerType?: string;
|
|
384
|
+
message: string;
|
|
385
|
+
taskMode?: string;
|
|
386
|
+
via: string;
|
|
387
|
+
dispatchedToIdleSession?: boolean;
|
|
388
|
+
dispatchedAt: string;
|
|
389
|
+
}): void {
|
|
390
|
+
const now = new Date().toISOString();
|
|
391
|
+
this.db.prepare(`
|
|
392
|
+
INSERT OR REPLACE INTO mesh_direct_dispatches
|
|
393
|
+
(task_id, mesh_id, node_id, session_id, provider_type, message, task_mode, via,
|
|
394
|
+
status, dispatched_to_idle_session, dispatched_at, updated_at)
|
|
395
|
+
VALUES
|
|
396
|
+
(@taskId, @meshId, @nodeId, @sessionId, @providerType, @message, @taskMode, @via,
|
|
397
|
+
'dispatched', @dispatchedToIdle, @dispatchedAt, @updatedAt)
|
|
398
|
+
`).run({
|
|
399
|
+
taskId: entry.taskId,
|
|
400
|
+
meshId: entry.meshId,
|
|
401
|
+
nodeId: entry.nodeId ?? null,
|
|
402
|
+
sessionId: entry.sessionId ?? null,
|
|
403
|
+
providerType: entry.providerType ?? null,
|
|
404
|
+
message: entry.message,
|
|
405
|
+
taskMode: entry.taskMode ?? null,
|
|
406
|
+
via: entry.via,
|
|
407
|
+
dispatchedToIdle: entry.dispatchedToIdleSession ? 1 : 0,
|
|
408
|
+
dispatchedAt: entry.dispatchedAt,
|
|
409
|
+
updatedAt: now,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
getActiveDirectDispatches(meshId: string): Array<{
|
|
414
|
+
taskId: string;
|
|
415
|
+
meshId: string;
|
|
416
|
+
nodeId: string | null;
|
|
417
|
+
sessionId: string | null;
|
|
418
|
+
providerType: string | null;
|
|
419
|
+
message: string;
|
|
420
|
+
taskMode: string | null;
|
|
421
|
+
via: string;
|
|
422
|
+
status: string;
|
|
423
|
+
dispatchedToIdleSession: boolean;
|
|
424
|
+
dispatchedAt: string;
|
|
425
|
+
updatedAt: string;
|
|
426
|
+
}> {
|
|
427
|
+
const rows = this.db.prepare(`
|
|
428
|
+
SELECT task_id, mesh_id, node_id, session_id, provider_type, message, task_mode, via,
|
|
429
|
+
status, dispatched_to_idle_session, dispatched_at, updated_at
|
|
430
|
+
FROM mesh_direct_dispatches
|
|
431
|
+
WHERE mesh_id = ? AND status NOT IN ('completed', 'failed', 'stale')
|
|
432
|
+
ORDER BY dispatched_at ASC
|
|
433
|
+
`).all(meshId) as Array<Record<string, unknown>>;
|
|
434
|
+
return rows.map(r => ({
|
|
435
|
+
taskId: r.task_id as string,
|
|
436
|
+
meshId: r.mesh_id as string,
|
|
437
|
+
nodeId: r.node_id as string | null,
|
|
438
|
+
sessionId: r.session_id as string | null,
|
|
439
|
+
providerType: r.provider_type as string | null,
|
|
440
|
+
message: r.message as string,
|
|
441
|
+
taskMode: r.task_mode as string | null,
|
|
442
|
+
via: r.via as string,
|
|
443
|
+
status: r.status as string,
|
|
444
|
+
dispatchedToIdleSession: (r.dispatched_to_idle_session as number) === 1,
|
|
445
|
+
dispatchedAt: r.dispatched_at as string,
|
|
446
|
+
updatedAt: r.updated_at as string,
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale'): void {
|
|
451
|
+
if (!sessionId) return; // never update rows without a session binding
|
|
452
|
+
const now = new Date().toISOString();
|
|
453
|
+
this.db.prepare(`
|
|
454
|
+
UPDATE mesh_direct_dispatches
|
|
455
|
+
SET status = @status, updated_at = @updatedAt
|
|
456
|
+
WHERE mesh_id = @meshId AND session_id = @sessionId
|
|
457
|
+
AND session_id IS NOT NULL
|
|
458
|
+
AND status NOT IN ('completed', 'failed')
|
|
459
|
+
`).run({ status, meshId, sessionId, updatedAt: now });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
cleanupTerminalDirectDispatches(olderThanMs: number): void {
|
|
463
|
+
const cutoff = new Date(Date.now() - olderThanMs).toISOString();
|
|
464
|
+
this.db.prepare(`
|
|
465
|
+
DELETE FROM mesh_direct_dispatches
|
|
466
|
+
WHERE status IN ('completed', 'failed', 'stale') AND updated_at < ?
|
|
467
|
+
`).run(cutoff);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
markStaleDirectDispatches(meshId: string, olderThanMs: number): void {
|
|
471
|
+
const cutoff = new Date(Date.now() - olderThanMs).toISOString();
|
|
472
|
+
const now = new Date().toISOString();
|
|
473
|
+
this.db.prepare(`
|
|
474
|
+
UPDATE mesh_direct_dispatches
|
|
475
|
+
SET status = 'stale', updated_at = ?
|
|
476
|
+
WHERE mesh_id = ? AND status = 'dispatched' AND dispatched_at < ?
|
|
477
|
+
`).run(now, meshId, cutoff);
|
|
478
|
+
}
|
|
176
479
|
}
|