@adhdev/daemon-core 0.9.82-rc.172 → 0.9.82-rc.173
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/config/mesh-config.d.ts +1 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.js +775 -693
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +772 -692
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +1 -1
- package/dist/mesh/mesh-events.d.ts +1 -0
- package/dist/mesh/mesh-fast-forward.d.ts +2 -0
- package/dist/mesh/mesh-work-queue.d.ts +10 -1
- package/dist/repo-mesh-types.d.ts +2 -0
- package/package.json +1 -1
- package/src/config/mesh-config.ts +16 -0
- package/src/index.ts +1 -3
- package/src/mesh/beads-db.ts +20 -13
- package/src/mesh/mesh-events.ts +88 -10
- package/src/mesh/mesh-fast-forward.ts +11 -3
- package/src/mesh/mesh-work-queue.ts +49 -3
- package/src/repo-mesh-types.ts +2 -0
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/mesh/mesh-sync.ts +0 -111
package/dist/mesh/beads-db.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare class BeadsDB {
|
|
|
27
27
|
updateQueueEntry(entry: MeshWorkQueueEntry): void;
|
|
28
28
|
findQueueEntryById(meshId: string, id: string): MeshWorkQueueEntry | null;
|
|
29
29
|
hasActiveAssignment(meshId: string, sessionId: string, nodeId: string): boolean;
|
|
30
|
-
claimNextQueueTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null;
|
|
30
|
+
claimNextQueueTask(meshId: string, nodeId: string, sessionId: string, capabilityTags?: string[]): MeshWorkQueueEntry | null;
|
|
31
31
|
getQueueStatsByStatus(meshId: string): {
|
|
32
32
|
status: string;
|
|
33
33
|
count: number;
|
|
@@ -9,6 +9,7 @@ export interface MeshFastForwardNodeArgs {
|
|
|
9
9
|
updateSubmodules?: boolean;
|
|
10
10
|
submoduleIgnorePaths?: string[];
|
|
11
11
|
timeoutMs?: number;
|
|
12
|
+
trigger?: 'manual' | 'idle_auto' | string;
|
|
12
13
|
}
|
|
13
14
|
export interface MeshFastForwardPlannedStep {
|
|
14
15
|
operation: 'refresh_upstream' | 'verify_clean_worktree' | 'verify_fast_forward' | 'merge_ff_only' | 'submodule_update' | 'verify_post_status';
|
|
@@ -35,5 +36,6 @@ export interface MeshFastForwardResult {
|
|
|
35
36
|
finalBranchConvergenceState?: Record<string, unknown>;
|
|
36
37
|
operationError?: string;
|
|
37
38
|
ledgerError?: string;
|
|
39
|
+
trigger?: string;
|
|
38
40
|
}
|
|
39
41
|
export declare function fastForwardMeshNode(args: MeshFastForwardNodeArgs): Promise<MeshFastForwardResult>;
|
|
@@ -25,6 +25,8 @@ export interface MeshWorkQueueEntry {
|
|
|
25
25
|
targetNodeId?: string;
|
|
26
26
|
/** If specified, only this runtime session can claim the task */
|
|
27
27
|
targetSessionId?: string;
|
|
28
|
+
/** If specified, a node must expose all tags before it can claim the task. */
|
|
29
|
+
requiredTags?: string[];
|
|
28
30
|
/** The node that actually claimed and is executing the task */
|
|
29
31
|
assignedNodeId?: string;
|
|
30
32
|
/** The session currently executing the task */
|
|
@@ -53,6 +55,12 @@ export interface MeshWorkQueueEntry {
|
|
|
53
55
|
export interface MeshQueueMutationOptions {
|
|
54
56
|
ownerRole?: RepoMeshDaemonRole;
|
|
55
57
|
}
|
|
58
|
+
export declare function normalizeMeshCapabilityTags(value: unknown): string[];
|
|
59
|
+
export declare function buildMeshNodeCapabilityTags(node: {
|
|
60
|
+
capabilities?: unknown;
|
|
61
|
+
policy?: unknown;
|
|
62
|
+
} | undefined, providerType?: string): string[];
|
|
63
|
+
export declare function nodeSatisfiesRequiredTags(requiredTags: unknown, capabilityTags: unknown): boolean;
|
|
56
64
|
/**
|
|
57
65
|
* Add a new task to the mesh queue.
|
|
58
66
|
*/
|
|
@@ -60,6 +68,7 @@ export declare function enqueueTask(meshId: string, message: string, opts?: {
|
|
|
60
68
|
targetNodeId?: string;
|
|
61
69
|
targetSessionId?: string;
|
|
62
70
|
taskMode?: MeshTaskMode | string;
|
|
71
|
+
requiredTags?: string[];
|
|
63
72
|
} & MeshQueueMutationOptions): MeshWorkQueueEntry;
|
|
64
73
|
/**
|
|
65
74
|
* Get all tasks in the queue, optionally filtered by status.
|
|
@@ -71,7 +80,7 @@ export declare function getMeshQueueRevision(meshId: string): string;
|
|
|
71
80
|
/**
|
|
72
81
|
* Find the next pending task that this node is allowed to claim, and mark it as assigned.
|
|
73
82
|
*/
|
|
74
|
-
export declare function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null;
|
|
83
|
+
export declare function claimNextTask(meshId: string, nodeId: string, sessionId: string, capabilityTags?: string[]): MeshWorkQueueEntry | null;
|
|
75
84
|
/**
|
|
76
85
|
* Update the status of a specific task.
|
|
77
86
|
* Used when a session completes, fails, or stalls.
|
|
@@ -253,6 +253,8 @@ export interface LocalMeshNodeEntry {
|
|
|
253
253
|
daemonId?: string;
|
|
254
254
|
/** Machine registry ID that owns this workspace, when known. */
|
|
255
255
|
machineId?: string;
|
|
256
|
+
/** Operator-defined capability tags used by mesh queue matching. */
|
|
257
|
+
capabilities?: string[];
|
|
256
258
|
userOverrides: Partial<RepoMeshNodeCapabilities>;
|
|
257
259
|
policy: RepoMeshNodePolicy;
|
|
258
260
|
/**
|
package/package.json
CHANGED
|
@@ -42,6 +42,20 @@ function loadMeshConfig(): LocalMeshConfig {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function normalizeCapabilityTags(value: unknown): string[] | undefined {
|
|
46
|
+
if (!Array.isArray(value)) return undefined;
|
|
47
|
+
const seen = new Set<string>();
|
|
48
|
+
const tags = value
|
|
49
|
+
.map(tag => typeof tag === 'string' ? tag.trim() : '')
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.filter(tag => {
|
|
52
|
+
if (seen.has(tag)) return false;
|
|
53
|
+
seen.add(tag);
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
56
|
+
return tags.length ? tags : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
function saveMeshConfig(config: LocalMeshConfig): void {
|
|
46
60
|
const path = getMeshConfigPath();
|
|
47
61
|
writeFileSync(path, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
@@ -423,6 +437,7 @@ export interface AddNodeOptions {
|
|
|
423
437
|
repoRoot?: string;
|
|
424
438
|
daemonId?: string;
|
|
425
439
|
machineId?: string;
|
|
440
|
+
capabilities?: string[];
|
|
426
441
|
userOverrides?: Partial<RepoMeshNodeCapabilities>;
|
|
427
442
|
policy?: RepoMeshNodePolicy;
|
|
428
443
|
isLocalWorktree?: boolean;
|
|
@@ -452,6 +467,7 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
|
|
|
452
467
|
repoRoot: opts.repoRoot,
|
|
453
468
|
daemonId: opts.daemonId,
|
|
454
469
|
machineId: opts.machineId,
|
|
470
|
+
capabilities: normalizeCapabilityTags(opts.capabilities),
|
|
455
471
|
userOverrides: opts.userOverrides || {},
|
|
456
472
|
policy: opts.policy || {},
|
|
457
473
|
isLocalWorktree: opts.isLocalWorktree,
|
package/src/index.ts
CHANGED
|
@@ -201,8 +201,6 @@ export type {
|
|
|
201
201
|
RepoMeshRefineConfig,
|
|
202
202
|
RepoMeshRefineValidationCommandConfig,
|
|
203
203
|
} from './mesh/refine-config.js';
|
|
204
|
-
export { syncMeshes } from './mesh/mesh-sync.js';
|
|
205
|
-
export type { MeshSyncTransport, MeshSyncResult, RemoteMeshRecord } from './mesh/mesh-sync.js';
|
|
206
204
|
|
|
207
205
|
// ── Mesh Task Ledger ──
|
|
208
206
|
export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT } from './mesh/mesh-ledger.js';
|
|
@@ -213,7 +211,7 @@ export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence }
|
|
|
213
211
|
export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
|
|
214
212
|
|
|
215
213
|
// ── Mesh Work Queue (GUPP) ──
|
|
216
|
-
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches } from './mesh/mesh-work-queue.js';
|
|
214
|
+
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches } from './mesh/mesh-work-queue.js';
|
|
217
215
|
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult, DirectDispatchRecord } from './mesh/mesh-work-queue.js';
|
|
218
216
|
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary } from './mesh/mesh-active-work.js';
|
|
219
217
|
export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource, MeshStaleDirectWorkSummary } from './mesh/mesh-active-work.js';
|
package/src/mesh/beads-db.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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';
|
|
5
|
+
import { nodeSatisfiesRequiredTags } from './mesh-work-queue.js';
|
|
5
6
|
import type { MeshTaskStatus, MeshWorkQueueEntry } from './mesh-work-queue.js';
|
|
6
7
|
import type BetterSqlite3 from 'better-sqlite3';
|
|
7
8
|
import type { Database as DatabaseHandle } from 'better-sqlite3';
|
|
@@ -280,34 +281,40 @@ export class BeadsDB {
|
|
|
280
281
|
}
|
|
281
282
|
|
|
282
283
|
// O(1) claim: transaction ensures only one session claims a pending task
|
|
283
|
-
claimNextQueueTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
|
|
284
|
+
claimNextQueueTask(meshId: string, nodeId: string, sessionId: string, capabilityTags: string[] = []): MeshWorkQueueEntry | null {
|
|
284
285
|
return this.transaction(() => {
|
|
285
286
|
this.ensureLegacyQueueMigrated(meshId);
|
|
286
287
|
if (this.hasActiveAssignment(meshId, sessionId, nodeId)) return null;
|
|
287
288
|
|
|
288
289
|
// Priority: session-targeted > node-targeted (no session) > unconstrained
|
|
289
|
-
const
|
|
290
|
+
const rows = [
|
|
291
|
+
...(
|
|
290
292
|
this.db.prepare(`
|
|
291
293
|
SELECT payload FROM mesh_queue
|
|
292
294
|
WHERE mesh_id = ? AND status = 'pending' AND target_session_id = ?
|
|
293
|
-
ORDER BY created_at ASC
|
|
294
|
-
`).
|
|
295
|
-
|
|
295
|
+
ORDER BY created_at ASC
|
|
296
|
+
`).all(meshId, sessionId) as Array<{ payload: string }>
|
|
297
|
+
),
|
|
298
|
+
...(
|
|
296
299
|
this.db.prepare(`
|
|
297
300
|
SELECT payload FROM mesh_queue
|
|
298
301
|
WHERE mesh_id = ? AND status = 'pending' AND target_node_id = ? AND target_session_id IS NULL
|
|
299
|
-
ORDER BY created_at ASC
|
|
300
|
-
`).
|
|
301
|
-
|
|
302
|
+
ORDER BY created_at ASC
|
|
303
|
+
`).all(meshId, nodeId) as Array<{ payload: string }>
|
|
304
|
+
),
|
|
305
|
+
...(
|
|
302
306
|
this.db.prepare(`
|
|
303
307
|
SELECT payload FROM mesh_queue
|
|
304
308
|
WHERE mesh_id = ? AND status = 'pending' AND target_node_id IS NULL AND target_session_id IS NULL
|
|
305
|
-
ORDER BY created_at ASC
|
|
306
|
-
`).
|
|
307
|
-
|
|
308
|
-
|
|
309
|
+
ORDER BY created_at ASC
|
|
310
|
+
`).all(meshId) as Array<{ payload: string }>
|
|
311
|
+
),
|
|
312
|
+
];
|
|
313
|
+
const entry = rows
|
|
314
|
+
.map(row => JSON.parse(row.payload) as MeshWorkQueueEntry)
|
|
315
|
+
.find(candidate => nodeSatisfiesRequiredTags(candidate.requiredTags, capabilityTags));
|
|
316
|
+
if (!entry) return null;
|
|
309
317
|
|
|
310
|
-
const entry = JSON.parse(row.payload) as MeshWorkQueueEntry;
|
|
311
318
|
const now = new Date().toISOString();
|
|
312
319
|
entry.status = 'assigned';
|
|
313
320
|
entry.assignedNodeId = nodeId;
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { detectCLI } from '../detection/cli-detector.js';
|
|
|
7
7
|
import { LOG } from '../logging/logger.js';
|
|
8
8
|
import { appendLedgerEntry, buildTaskCompletionEvidence, getLedgerDir, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
9
9
|
import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
|
|
10
|
-
import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches } from './mesh-work-queue.js';
|
|
10
|
+
import { buildMeshNodeCapabilityTags, claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches } from './mesh-work-queue.js';
|
|
11
11
|
import { BeadsDB } from './beads-db.js';
|
|
12
|
+
import { fastForwardMeshNode } from './mesh-fast-forward.js';
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Remote Node Idle Session Tracking
|
|
@@ -33,6 +34,8 @@ const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${node
|
|
|
33
34
|
// meshNodeFor. Cache results for 5 seconds to avoid repeated config reads.
|
|
34
35
|
const meshByWorkspaceCache = new Map<string, { mesh: any; cachedAt: number }>();
|
|
35
36
|
const MESH_WORKSPACE_CACHE_TTL_MS = 5_000;
|
|
37
|
+
const IDLE_AUTO_FAST_FORWARD_THROTTLE_MS = 30 * 60 * 1000;
|
|
38
|
+
const idleAutoFastForwardLastAttempt = new Map<string, number>();
|
|
36
39
|
|
|
37
40
|
function getCachedMeshByWorkspace(workspace: string): any {
|
|
38
41
|
const now = Date.now();
|
|
@@ -47,6 +50,10 @@ function readWorkerResultMetadata(event: Record<string, unknown>): Record<string
|
|
|
47
50
|
return readRecord(event.workerResult) || readRecord(event.meshWorkerResult) || readRecord(event.structuredResult);
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
export function __resetIdleAutoFastForwardForTests(): void {
|
|
54
|
+
idleAutoFastForwardLastAttempt.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
function sweepExpiredRemoteIdleSessions(): void {
|
|
51
58
|
const now = Date.now();
|
|
52
59
|
for (const [key, session] of remoteIdleSessions) {
|
|
@@ -663,7 +670,10 @@ export function tryAssignQueueTask(
|
|
|
663
670
|
sessionId: string,
|
|
664
671
|
providerType: string
|
|
665
672
|
): boolean {
|
|
666
|
-
const
|
|
673
|
+
const mesh = getMeshWithCache(components, meshId);
|
|
674
|
+
const node = mesh?.nodes.find((n: any) => n.id === nodeId);
|
|
675
|
+
const capabilityTags = buildMeshNodeCapabilityTags(node, providerType);
|
|
676
|
+
const task = claimNextTask(meshId, nodeId, sessionId, capabilityTags);
|
|
667
677
|
if (!task) {
|
|
668
678
|
return false;
|
|
669
679
|
}
|
|
@@ -671,9 +681,6 @@ export function tryAssignQueueTask(
|
|
|
671
681
|
LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
|
|
672
682
|
|
|
673
683
|
// Check if the node is remote
|
|
674
|
-
const mesh = getMeshWithCache(components, meshId);
|
|
675
|
-
const node = mesh?.nodes.find((n: any) => n.id === nodeId);
|
|
676
|
-
|
|
677
684
|
// If the node is explicitly remote and we have a dispatch mechanism, route via P2P
|
|
678
685
|
if (node?.daemonId && components.dispatchMeshCommand) {
|
|
679
686
|
const isLocalNode = components.cliManager.adapters.has(sessionId);
|
|
@@ -1061,6 +1068,72 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
|
|
|
1061
1068
|
await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
|
|
1062
1069
|
}
|
|
1063
1070
|
|
|
1071
|
+
async function maybeAutoFastForwardIdleNode(components: DaemonComponents, args: {
|
|
1072
|
+
meshId: string;
|
|
1073
|
+
nodeId: string;
|
|
1074
|
+
sessionId?: string;
|
|
1075
|
+
providerType?: string;
|
|
1076
|
+
}): Promise<void> {
|
|
1077
|
+
const mesh = getMeshWithCache(components, args.meshId);
|
|
1078
|
+
const node = mesh?.nodes?.find((candidate: any) => candidate?.id === args.nodeId || candidate?.nodeId === args.nodeId);
|
|
1079
|
+
const workspace = readNonEmptyString(node?.workspace);
|
|
1080
|
+
if (!workspace) return;
|
|
1081
|
+
if (!existsSync(workspace)) return;
|
|
1082
|
+
|
|
1083
|
+
const throttleKey = `${args.meshId}:${args.nodeId}`;
|
|
1084
|
+
const now = Date.now();
|
|
1085
|
+
const lastAttempt = idleAutoFastForwardLastAttempt.get(throttleKey) || 0;
|
|
1086
|
+
if (now - lastAttempt < IDLE_AUTO_FAST_FORWARD_THROTTLE_MS) return;
|
|
1087
|
+
idleAutoFastForwardLastAttempt.set(throttleKey, now);
|
|
1088
|
+
|
|
1089
|
+
const submoduleIgnorePaths = Array.isArray(node?.policy?.submoduleIgnorePaths)
|
|
1090
|
+
? node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
1091
|
+
: undefined;
|
|
1092
|
+
try {
|
|
1093
|
+
const dryRun = await fastForwardMeshNode({
|
|
1094
|
+
meshId: args.meshId,
|
|
1095
|
+
nodeId: args.nodeId,
|
|
1096
|
+
workspace,
|
|
1097
|
+
execute: false,
|
|
1098
|
+
dryRun: true,
|
|
1099
|
+
updateSubmodules: false,
|
|
1100
|
+
submoduleIgnorePaths,
|
|
1101
|
+
trigger: 'idle_auto',
|
|
1102
|
+
});
|
|
1103
|
+
if (!dryRun || dryRun.code !== 'fast_forward_available' || dryRun.allowed !== true) return;
|
|
1104
|
+
await fastForwardMeshNode({
|
|
1105
|
+
meshId: args.meshId,
|
|
1106
|
+
nodeId: args.nodeId,
|
|
1107
|
+
workspace,
|
|
1108
|
+
execute: true,
|
|
1109
|
+
dryRun: false,
|
|
1110
|
+
updateSubmodules: false,
|
|
1111
|
+
submoduleIgnorePaths,
|
|
1112
|
+
trigger: 'idle_auto',
|
|
1113
|
+
});
|
|
1114
|
+
} catch (e: any) {
|
|
1115
|
+
LOG.warn('MeshFastForward', `Idle auto fast-forward check failed for ${args.nodeId}: ${e?.message || e}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function runIdleMaintenanceThenAssignQueue(components: DaemonComponents, args: {
|
|
1120
|
+
meshId: string;
|
|
1121
|
+
nodeId: string;
|
|
1122
|
+
sessionId: string;
|
|
1123
|
+
providerType: string;
|
|
1124
|
+
}): void {
|
|
1125
|
+
setImmediate(() => {
|
|
1126
|
+
maybeAutoFastForwardIdleNode(components, args)
|
|
1127
|
+
.finally(() => {
|
|
1128
|
+
try {
|
|
1129
|
+
tryAssignQueueTask(components, args.meshId, args.nodeId, args.sessionId, args.providerType);
|
|
1130
|
+
} catch (e: any) {
|
|
1131
|
+
LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${args.nodeId}: ${e?.message || e}`);
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1064
1137
|
function buildMeshSystemMessage(args: {
|
|
1065
1138
|
event: string;
|
|
1066
1139
|
nodeLabel: string;
|
|
@@ -1313,9 +1386,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1313
1386
|
updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
|
|
1314
1387
|
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
1315
1388
|
if (nodeId && providerType) {
|
|
1316
|
-
|
|
1317
|
-
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
1318
|
-
});
|
|
1389
|
+
runIdleMaintenanceThenAssignQueue(components, { meshId: args.meshId, nodeId, sessionId, providerType });
|
|
1319
1390
|
}
|
|
1320
1391
|
}
|
|
1321
1392
|
} else if (args.event === 'agent:ready') {
|
|
@@ -1370,8 +1441,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1370
1441
|
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
1371
1442
|
});
|
|
1372
1443
|
setImmediate(() => {
|
|
1373
|
-
|
|
1374
|
-
|
|
1444
|
+
maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
|
|
1445
|
+
.finally(() => {
|
|
1446
|
+
try {
|
|
1447
|
+
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
1448
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
1449
|
+
} catch (e: any) {
|
|
1450
|
+
LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1375
1453
|
});
|
|
1376
1454
|
}
|
|
1377
1455
|
} else if (args.event === 'agent:generating_started') {
|
|
@@ -12,6 +12,7 @@ export interface MeshFastForwardNodeArgs {
|
|
|
12
12
|
updateSubmodules?: boolean;
|
|
13
13
|
submoduleIgnorePaths?: string[];
|
|
14
14
|
timeoutMs?: number;
|
|
15
|
+
trigger?: 'manual' | 'idle_auto' | string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface MeshFastForwardPlannedStep {
|
|
@@ -40,11 +41,12 @@ export interface MeshFastForwardResult {
|
|
|
40
41
|
finalBranchConvergenceState?: Record<string, unknown>;
|
|
41
42
|
operationError?: string;
|
|
42
43
|
ledgerError?: string;
|
|
44
|
+
trigger?: string;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
type MeshFastForwardBase = Pick<
|
|
46
48
|
MeshFastForwardResult,
|
|
47
|
-
'workspace' | 'dryRun' | 'updateSubmodules' | 'plannedSteps'
|
|
49
|
+
'workspace' | 'dryRun' | 'updateSubmodules' | 'plannedSteps' | 'trigger'
|
|
48
50
|
> & Pick<Partial<MeshFastForwardResult>, 'nodeId' | 'meshId'>;
|
|
49
51
|
|
|
50
52
|
const STATUS_OPTIONS = { refreshUpstream: true, includeSubmodules: true, timeoutMs: 15_000 } as const;
|
|
@@ -54,6 +56,7 @@ export async function fastForwardMeshNode(args: MeshFastForwardNodeArgs): Promis
|
|
|
54
56
|
const nodeId = normalizeOptionalString(args.nodeId);
|
|
55
57
|
const meshId = normalizeOptionalString(args.meshId);
|
|
56
58
|
const requestedBranch = normalizeOptionalString(args.branch);
|
|
59
|
+
const trigger = normalizeOptionalString(args.trigger) || 'manual';
|
|
57
60
|
const updateSubmodules = args.updateSubmodules === true;
|
|
58
61
|
const dryRun = args.dryRun === true || args.execute !== true;
|
|
59
62
|
const plannedSteps = buildPlannedSteps(updateSubmodules);
|
|
@@ -64,6 +67,7 @@ export async function fastForwardMeshNode(args: MeshFastForwardNodeArgs): Promis
|
|
|
64
67
|
dryRun,
|
|
65
68
|
updateSubmodules,
|
|
66
69
|
plannedSteps,
|
|
70
|
+
trigger,
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
if (!workspace) {
|
|
@@ -78,11 +82,13 @@ export async function fastForwardMeshNode(args: MeshFastForwardNodeArgs): Promis
|
|
|
78
82
|
|
|
79
83
|
const earlyBlockers = collectPreflightBlockers(current, requestedBranch);
|
|
80
84
|
if (earlyBlockers.length > 0) {
|
|
81
|
-
|
|
85
|
+
const result: MeshFastForwardResult = {
|
|
82
86
|
...block(base, chooseBlockCode(current, earlyBlockers), earlyBlockers),
|
|
83
87
|
current,
|
|
84
88
|
finalBranchConvergenceState: buildConvergenceState(current, codeToConvergenceStatus(chooseBlockCode(current, earlyBlockers))),
|
|
85
89
|
};
|
|
90
|
+
await appendFastForwardLedger(result, 'blocked');
|
|
91
|
+
return result;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
if (current.behind === 0) {
|
|
@@ -129,6 +135,7 @@ export async function fastForwardMeshNode(args: MeshFastForwardNodeArgs): Promis
|
|
|
129
135
|
preStatus: current,
|
|
130
136
|
finalBranchConvergenceState: buildConvergenceState(current, 'fast_forward_available'),
|
|
131
137
|
};
|
|
138
|
+
await appendFastForwardLedger(result, 'dry_run');
|
|
132
139
|
return result;
|
|
133
140
|
}
|
|
134
141
|
|
|
@@ -393,7 +400,7 @@ function formatGitError(error: unknown): string {
|
|
|
393
400
|
return String(error);
|
|
394
401
|
}
|
|
395
402
|
|
|
396
|
-
async function appendFastForwardLedger(result: MeshFastForwardResult, outcome: 'noop' | 'blocked' | 'executed' | 'failed'): Promise<void> {
|
|
403
|
+
async function appendFastForwardLedger(result: MeshFastForwardResult, outcome: 'noop' | 'blocked' | 'dry_run' | 'executed' | 'failed'): Promise<void> {
|
|
397
404
|
if (!result.meshId) return;
|
|
398
405
|
try {
|
|
399
406
|
const { appendLedgerEntry } = await import('./mesh-ledger.js');
|
|
@@ -402,6 +409,7 @@ async function appendFastForwardLedger(result: MeshFastForwardResult, outcome: '
|
|
|
402
409
|
...(result.nodeId ? { nodeId: result.nodeId } : {}),
|
|
403
410
|
payload: {
|
|
404
411
|
operation: 'mesh_fast_forward_node',
|
|
412
|
+
trigger: result.trigger || 'manual',
|
|
405
413
|
outcome,
|
|
406
414
|
code: result.code,
|
|
407
415
|
workspace: result.workspace,
|
|
@@ -69,6 +69,8 @@ export interface MeshWorkQueueEntry {
|
|
|
69
69
|
targetNodeId?: string;
|
|
70
70
|
/** If specified, only this runtime session can claim the task */
|
|
71
71
|
targetSessionId?: string;
|
|
72
|
+
/** If specified, a node must expose all tags before it can claim the task. */
|
|
73
|
+
requiredTags?: string[];
|
|
72
74
|
/** The node that actually claimed and is executing the task */
|
|
73
75
|
assignedNodeId?: string;
|
|
74
76
|
/** The session currently executing the task */
|
|
@@ -99,6 +101,49 @@ export interface MeshQueueMutationOptions {
|
|
|
99
101
|
ownerRole?: RepoMeshDaemonRole;
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
export function normalizeMeshCapabilityTags(value: unknown): string[] {
|
|
105
|
+
if (!Array.isArray(value)) return [];
|
|
106
|
+
const seen = new Set<string>();
|
|
107
|
+
return value
|
|
108
|
+
.map(tag => typeof tag === 'string' ? tag.trim() : '')
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.filter(tag => {
|
|
111
|
+
if (seen.has(tag)) return false;
|
|
112
|
+
seen.add(tag);
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function firstProviderPriority(policy: unknown): string | undefined {
|
|
118
|
+
const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
119
|
+
? (policy as Record<string, unknown>).providerPriority
|
|
120
|
+
: undefined;
|
|
121
|
+
if (!Array.isArray(raw)) return undefined;
|
|
122
|
+
return raw.find(type => typeof type === 'string' && type.trim())?.trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildMeshNodeCapabilityTags(
|
|
126
|
+
node: { capabilities?: unknown; policy?: unknown } | undefined,
|
|
127
|
+
providerType?: string,
|
|
128
|
+
): string[] {
|
|
129
|
+
const provider = typeof providerType === 'string' && providerType.trim()
|
|
130
|
+
? providerType.trim()
|
|
131
|
+
: firstProviderPriority(node?.policy);
|
|
132
|
+
return normalizeMeshCapabilityTags([
|
|
133
|
+
...(Array.isArray(node?.capabilities) ? node.capabilities : []),
|
|
134
|
+
`os=${process.platform}`,
|
|
135
|
+
`arch=${process.arch}`,
|
|
136
|
+
...(provider ? [`provider=${provider}`] : []),
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function nodeSatisfiesRequiredTags(requiredTags: unknown, capabilityTags: unknown): boolean {
|
|
141
|
+
const required = normalizeMeshCapabilityTags(requiredTags);
|
|
142
|
+
if (required.length === 0) return true;
|
|
143
|
+
const available = new Set(normalizeMeshCapabilityTags(capabilityTags));
|
|
144
|
+
return required.every(tag => available.has(tag));
|
|
145
|
+
}
|
|
146
|
+
|
|
102
147
|
function withQueueLock<T>(_meshId: string, fn: () => T): T {
|
|
103
148
|
return BeadsDB.getInstance().transaction(fn);
|
|
104
149
|
}
|
|
@@ -117,7 +162,7 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
|
117
162
|
export function enqueueTask(
|
|
118
163
|
meshId: string,
|
|
119
164
|
message: string,
|
|
120
|
-
opts?: { targetNodeId?: string; targetSessionId?: string; taskMode?: MeshTaskMode | string } & MeshQueueMutationOptions,
|
|
165
|
+
opts?: { targetNodeId?: string; targetSessionId?: string; taskMode?: MeshTaskMode | string; requiredTags?: string[] } & MeshQueueMutationOptions,
|
|
121
166
|
): MeshWorkQueueEntry {
|
|
122
167
|
requireMeshHostQueueOwner(opts);
|
|
123
168
|
const modeValidation = validateMeshTaskModeRequest(opts?.taskMode, message);
|
|
@@ -132,6 +177,7 @@ export function enqueueTask(
|
|
|
132
177
|
taskMode: modeValidation.taskMode,
|
|
133
178
|
targetNodeId: opts?.targetNodeId,
|
|
134
179
|
targetSessionId: opts?.targetSessionId,
|
|
180
|
+
requiredTags: normalizeMeshCapabilityTags(opts?.requiredTags),
|
|
135
181
|
createdAt: new Date().toISOString(),
|
|
136
182
|
updatedAt: new Date().toISOString(),
|
|
137
183
|
};
|
|
@@ -153,8 +199,8 @@ export function getMeshQueueRevision(meshId: string): string {
|
|
|
153
199
|
/**
|
|
154
200
|
* Find the next pending task that this node is allowed to claim, and mark it as assigned.
|
|
155
201
|
*/
|
|
156
|
-
export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
|
|
157
|
-
return BeadsDB.getInstance().claimNextQueueTask(meshId, nodeId, sessionId);
|
|
202
|
+
export function claimNextTask(meshId: string, nodeId: string, sessionId: string, capabilityTags?: string[]): MeshWorkQueueEntry | null {
|
|
203
|
+
return BeadsDB.getInstance().claimNextQueueTask(meshId, nodeId, sessionId, capabilityTags);
|
|
158
204
|
}
|
|
159
205
|
|
|
160
206
|
/**
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -303,6 +303,8 @@ export interface LocalMeshNodeEntry {
|
|
|
303
303
|
daemonId?: string;
|
|
304
304
|
/** Machine registry ID that owns this workspace, when known. */
|
|
305
305
|
machineId?: string;
|
|
306
|
+
/** Operator-defined capability tags used by mesh queue matching. */
|
|
307
|
+
capabilities?: string[];
|
|
306
308
|
userOverrides: Partial<RepoMeshNodeCapabilities>;
|
|
307
309
|
policy: RepoMeshNodePolicy;
|
|
308
310
|
/**
|
package/dist/mesh/mesh-sync.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mesh Sync — Sync local mesh metadata to/from cloud D1
|
|
3
|
-
*
|
|
4
|
-
* When cloud is available, this module pushes local mesh config
|
|
5
|
-
* to the server and pulls remote meshes that were created from
|
|
6
|
-
* other machines. The local ~/.adhdev/meshes.json remains the
|
|
7
|
-
* canonical source; cloud is a membership/metadata layer only.
|
|
8
|
-
* Task/chat/ledger evidence remains local-first and must not be
|
|
9
|
-
* synchronized through Cloud/D1.
|
|
10
|
-
*
|
|
11
|
-
* This is called lazily (not on daemon startup) — only when the
|
|
12
|
-
* user explicitly opens the mesh page or runs `adhdev mesh sync`.
|
|
13
|
-
*/
|
|
14
|
-
export interface MeshSyncTransport {
|
|
15
|
-
/** GET /api/v1/repo-meshes */
|
|
16
|
-
listRemoteMeshes(): Promise<{
|
|
17
|
-
meshes: RemoteMeshRecord[];
|
|
18
|
-
}>;
|
|
19
|
-
/** POST /api/v1/repo-meshes */
|
|
20
|
-
createRemoteMesh(data: {
|
|
21
|
-
name: string;
|
|
22
|
-
repo_identity: string;
|
|
23
|
-
repo_remote_url?: string;
|
|
24
|
-
default_branch?: string;
|
|
25
|
-
policy?: string;
|
|
26
|
-
}): Promise<{
|
|
27
|
-
mesh: RemoteMeshRecord;
|
|
28
|
-
}>;
|
|
29
|
-
/** DELETE /api/v1/repo-meshes/:id */
|
|
30
|
-
deleteRemoteMesh(meshId: string): Promise<void>;
|
|
31
|
-
}
|
|
32
|
-
export interface RemoteMeshRecord {
|
|
33
|
-
id: string;
|
|
34
|
-
name: string;
|
|
35
|
-
repo_identity: string;
|
|
36
|
-
repo_remote_url: string | null;
|
|
37
|
-
default_branch: string | null;
|
|
38
|
-
policy: string;
|
|
39
|
-
status: string;
|
|
40
|
-
created_at: string;
|
|
41
|
-
updated_at: string;
|
|
42
|
-
}
|
|
43
|
-
export interface MeshSyncResult {
|
|
44
|
-
pushed: number;
|
|
45
|
-
pulled: number;
|
|
46
|
-
deleted: number;
|
|
47
|
-
errors: string[];
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Push local meshes to cloud (upsert by repo_identity).
|
|
51
|
-
* Pull remote meshes that don't exist locally.
|
|
52
|
-
*/
|
|
53
|
-
export declare function syncMeshes(transport: MeshSyncTransport): Promise<MeshSyncResult>;
|