@adhdev/daemon-core 0.9.76-rc.36 → 0.9.76-rc.38
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/commands/router.d.ts +7 -0
- package/dist/index.js +164 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +164 -5
- package/dist/index.mjs.map +1 -1
- package/dist/repo-mesh-types.d.ts +7 -0
- package/package.json +1 -1
- package/src/commands/router.d.ts +1 -0
- package/src/commands/router.ts +181 -2
- package/src/config/mesh-config.ts +17 -2
- package/src/repo-mesh-types.ts +9 -0
|
@@ -40,6 +40,7 @@ export interface RepoMeshNode {
|
|
|
40
40
|
status: 'enabled' | 'disabled' | 'removed';
|
|
41
41
|
}
|
|
42
42
|
export type RepoMeshNodeHealth = 'online' | 'offline' | 'degraded' | 'dirty' | 'wrong_branch' | 'unknown';
|
|
43
|
+
export type RepoMeshSessionCleanupMode = 'preserve' | 'stop' | 'delete_stopped' | 'stop_and_delete';
|
|
43
44
|
export interface RepoMeshPolicy {
|
|
44
45
|
requirePreTaskCheckpoint: boolean;
|
|
45
46
|
requirePostTaskCheckpoint: boolean;
|
|
@@ -48,6 +49,12 @@ export interface RepoMeshPolicy {
|
|
|
48
49
|
dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
|
|
49
50
|
maxParallelTasks: number;
|
|
50
51
|
allowedProviders?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* What to do with delegated session-host records for a node when it is removed.
|
|
54
|
+
* Defaults to 'preserve' so completed work can be reviewed later and live
|
|
55
|
+
* runtimes are never stopped/deleted unless the mesh owner opts in.
|
|
56
|
+
*/
|
|
57
|
+
sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
|
|
51
58
|
}
|
|
52
59
|
export interface RepoMeshNodePolicy {
|
|
53
60
|
readOnly?: boolean;
|
package/package.json
CHANGED
package/src/commands/router.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface SessionHostControlPlane {
|
|
|
21
21
|
}): Promise<any>;
|
|
22
22
|
listSessions(): Promise<any[]>;
|
|
23
23
|
stopSession(sessionId: string): Promise<any>;
|
|
24
|
+
deleteSession(sessionId: string, opts?: { force?: boolean }): Promise<any>;
|
|
24
25
|
resumeSession(sessionId: string): Promise<any>;
|
|
25
26
|
restartSession(sessionId: string): Promise<any>;
|
|
26
27
|
sendSignal(sessionId: string, signal: string): Promise<any>;
|
package/src/commands/router.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { handleMeshForwardEvent } from '../mesh/mesh-events.js';
|
|
|
40
40
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
41
41
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
42
42
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
43
|
+
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
43
44
|
|
|
44
45
|
type ReleaseChannel = 'stable' | 'preview';
|
|
45
46
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
@@ -141,6 +142,7 @@ export interface SessionHostControlPlane {
|
|
|
141
142
|
getDiagnostics(payload?: { includeSessions?: boolean; limit?: number }): Promise<any>;
|
|
142
143
|
listSessions(): Promise<any[]>;
|
|
143
144
|
stopSession(sessionId: string): Promise<any>;
|
|
145
|
+
deleteSession(sessionId: string, opts?: { force?: boolean }): Promise<any>;
|
|
144
146
|
resumeSession(sessionId: string): Promise<any>;
|
|
145
147
|
restartSession(sessionId: string): Promise<any>;
|
|
146
148
|
sendSignal(sessionId: string, signal: string): Promise<any>;
|
|
@@ -335,6 +337,98 @@ export class DaemonCommandRouter {
|
|
|
335
337
|
return true;
|
|
336
338
|
}
|
|
337
339
|
|
|
340
|
+
private normalizeMeshSessionCleanupMode(value: unknown): RepoMeshSessionCleanupMode {
|
|
341
|
+
return value === 'stop'
|
|
342
|
+
|| value === 'delete_stopped'
|
|
343
|
+
|| value === 'stop_and_delete'
|
|
344
|
+
|| value === 'preserve'
|
|
345
|
+
? value
|
|
346
|
+
: 'preserve';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private sessionMatchesMeshNode(record: any, node: any, nodeId: string, sessionIds?: Set<string>): boolean {
|
|
350
|
+
const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : '';
|
|
351
|
+
if (!sessionId) return false;
|
|
352
|
+
if (sessionIds?.size) return sessionIds.has(sessionId);
|
|
353
|
+
const workspace = typeof node?.workspace === 'string' ? node.workspace : '';
|
|
354
|
+
if (workspace && record?.workspace === workspace) return true;
|
|
355
|
+
if (record?.meta?.meshNodeId === nodeId) return true;
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private isCompletedHostedSession(record: any): boolean {
|
|
360
|
+
return record?.lifecycle === 'stopped' || record?.lifecycle === 'failed' || record?.lifecycle === 'interrupted';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private async cleanupMeshSessions(args: {
|
|
364
|
+
meshId: string;
|
|
365
|
+
nodeId: string;
|
|
366
|
+
node: any;
|
|
367
|
+
mode: RepoMeshSessionCleanupMode;
|
|
368
|
+
sessionIds?: string[];
|
|
369
|
+
dryRun?: boolean;
|
|
370
|
+
}): Promise<{ success: boolean; [key: string]: unknown }> {
|
|
371
|
+
if (args.mode === 'preserve') {
|
|
372
|
+
return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
|
|
373
|
+
}
|
|
374
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
375
|
+
|
|
376
|
+
const requestedSessionIds = Array.isArray(args.sessionIds)
|
|
377
|
+
? new Set(args.sessionIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean))
|
|
378
|
+
: undefined;
|
|
379
|
+
const sessions = await this.deps.sessionHostControl.listSessions();
|
|
380
|
+
const matched = sessions.filter(record => this.sessionMatchesMeshNode(record, args.node, args.nodeId, requestedSessionIds));
|
|
381
|
+
const stoppedSessionIds: string[] = [];
|
|
382
|
+
const deletedSessionIds: string[] = [];
|
|
383
|
+
const skippedSessionIds: string[] = [];
|
|
384
|
+
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
385
|
+
|
|
386
|
+
for (const record of matched) {
|
|
387
|
+
const sessionId = String(record.sessionId);
|
|
388
|
+
const completed = this.isCompletedHostedSession(record);
|
|
389
|
+
try {
|
|
390
|
+
if (args.mode === 'stop') {
|
|
391
|
+
if (!completed) {
|
|
392
|
+
if (!args.dryRun) await this.deps.sessionHostControl.stopSession(sessionId);
|
|
393
|
+
stoppedSessionIds.push(sessionId);
|
|
394
|
+
} else {
|
|
395
|
+
skippedSessionIds.push(sessionId);
|
|
396
|
+
}
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (args.mode === 'delete_stopped') {
|
|
401
|
+
if (completed) {
|
|
402
|
+
if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: false });
|
|
403
|
+
deletedSessionIds.push(sessionId);
|
|
404
|
+
} else {
|
|
405
|
+
skippedSessionIds.push(sessionId);
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (args.mode === 'stop_and_delete') {
|
|
411
|
+
if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: true });
|
|
412
|
+
deletedSessionIds.push(sessionId);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
} catch (e: any) {
|
|
416
|
+
errors.push({ sessionId, error: e?.message || String(e) });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
success: errors.length === 0,
|
|
422
|
+
mode: args.mode,
|
|
423
|
+
dryRun: args.dryRun === true,
|
|
424
|
+
matchedCount: matched.length,
|
|
425
|
+
stoppedSessionIds,
|
|
426
|
+
deletedSessionIds,
|
|
427
|
+
skippedSessionIds,
|
|
428
|
+
...(errors.length ? { errors } : {}),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
338
432
|
private async traceSessionHostAction<T>(
|
|
339
433
|
action: string,
|
|
340
434
|
args: any,
|
|
@@ -1096,7 +1190,27 @@ export class DaemonCommandRouter {
|
|
|
1096
1190
|
if (!name) return { success: false, error: 'name required' };
|
|
1097
1191
|
try {
|
|
1098
1192
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
1099
|
-
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch });
|
|
1193
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy });
|
|
1194
|
+
return { success: true, mesh };
|
|
1195
|
+
} catch (e: any) {
|
|
1196
|
+
return { success: false, error: e.message };
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
case 'update_mesh': {
|
|
1201
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1202
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1203
|
+
try {
|
|
1204
|
+
const { updateMesh } = await import('../config/mesh-config.js');
|
|
1205
|
+
const patch: Record<string, unknown> = {};
|
|
1206
|
+
if (typeof args?.name === 'string') patch.name = args.name;
|
|
1207
|
+
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
1208
|
+
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
1209
|
+
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
1210
|
+
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
1211
|
+
const mesh = updateMesh(meshId, patch as any);
|
|
1212
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1213
|
+
this.inlineMeshCache.set(meshId, mesh);
|
|
1100
1214
|
return { success: true, mesh };
|
|
1101
1215
|
} catch (e: any) {
|
|
1102
1216
|
return { success: false, error: e.message };
|
|
@@ -1138,6 +1252,62 @@ export class DaemonCommandRouter {
|
|
|
1138
1252
|
}
|
|
1139
1253
|
}
|
|
1140
1254
|
|
|
1255
|
+
case 'update_mesh_node': {
|
|
1256
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1257
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1258
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
1259
|
+
try {
|
|
1260
|
+
const { updateNode } = await import('../config/mesh-config.js');
|
|
1261
|
+
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
1262
|
+
? { ...(args.policy as Record<string, unknown>) }
|
|
1263
|
+
: {};
|
|
1264
|
+
if (Array.isArray(args?.providerPriority)) {
|
|
1265
|
+
const providerPriority = args.providerPriority
|
|
1266
|
+
.map((type: any) => typeof type === 'string' ? type.trim() : '')
|
|
1267
|
+
.filter(Boolean);
|
|
1268
|
+
delete (policy as any).provider_priority;
|
|
1269
|
+
if (providerPriority.length) {
|
|
1270
|
+
(policy as any).providerPriority = providerPriority;
|
|
1271
|
+
} else {
|
|
1272
|
+
delete (policy as any).providerPriority;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const node = updateNode(meshId, nodeId, { policy: policy as any });
|
|
1276
|
+
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
1277
|
+
return { success: true, node };
|
|
1278
|
+
} catch (e: any) {
|
|
1279
|
+
return { success: false, error: e.message };
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
case 'cleanup_mesh_sessions': {
|
|
1284
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1285
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1286
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
1287
|
+
try {
|
|
1288
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
1289
|
+
const mesh = meshRecord?.mesh;
|
|
1290
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1291
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
1292
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
1293
|
+
const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
|
|
1294
|
+
const sessionIds = Array.isArray(args?.sessionIds)
|
|
1295
|
+
? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
|
|
1296
|
+
: undefined;
|
|
1297
|
+
const result = await this.cleanupMeshSessions({
|
|
1298
|
+
meshId,
|
|
1299
|
+
nodeId,
|
|
1300
|
+
node,
|
|
1301
|
+
mode,
|
|
1302
|
+
sessionIds,
|
|
1303
|
+
dryRun: args?.dryRun === true,
|
|
1304
|
+
});
|
|
1305
|
+
return result;
|
|
1306
|
+
} catch (e: any) {
|
|
1307
|
+
return { success: false, error: e.message };
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1141
1311
|
case 'remove_mesh_node': {
|
|
1142
1312
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1143
1313
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
@@ -1147,6 +1317,15 @@ export class DaemonCommandRouter {
|
|
|
1147
1317
|
const mesh = meshRecord?.mesh;
|
|
1148
1318
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
1149
1319
|
|
|
1320
|
+
const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
|
|
1321
|
+
args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
|
|
1322
|
+
);
|
|
1323
|
+
let sessionCleanup: Record<string, unknown> | undefined;
|
|
1324
|
+
if (node && sessionCleanupMode !== 'preserve') {
|
|
1325
|
+
sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode });
|
|
1326
|
+
if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1150
1329
|
// If this is a worktree node, clean up the git worktree first
|
|
1151
1330
|
if (node?.isLocalWorktree && node.workspace) {
|
|
1152
1331
|
try {
|
|
@@ -1171,7 +1350,7 @@ export class DaemonCommandRouter {
|
|
|
1171
1350
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
1172
1351
|
removed = removeNode(meshId, nodeId);
|
|
1173
1352
|
}
|
|
1174
|
-
return { success: true, removed };
|
|
1353
|
+
return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}) };
|
|
1175
1354
|
} catch (e: any) {
|
|
1176
1355
|
return { success: false, error: e.message };
|
|
1177
1356
|
}
|
|
@@ -74,6 +74,21 @@ export function normalizeRepoIdentity(remoteUrl: string): string {
|
|
|
74
74
|
|
|
75
75
|
// ─── CRUD Operations ────────────────────────────
|
|
76
76
|
|
|
77
|
+
const SESSION_CLEANUP_MODES = new Set(['preserve', 'stop', 'delete_stopped', 'stop_and_delete']);
|
|
78
|
+
|
|
79
|
+
function mergeMeshPolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMeshPolicy> | undefined): RepoMeshPolicy {
|
|
80
|
+
const policy: RepoMeshPolicy = { ...DEFAULT_MESH_POLICY, ...(base || {}), ...(patch || {}) };
|
|
81
|
+
if (!['block', 'warn', 'checkpoint_then_continue'].includes(policy.dirtyWorkspaceBehavior)) {
|
|
82
|
+
policy.dirtyWorkspaceBehavior = 'warn';
|
|
83
|
+
}
|
|
84
|
+
const maxParallelTasks = Number(policy.maxParallelTasks);
|
|
85
|
+
policy.maxParallelTasks = Number.isFinite(maxParallelTasks) ? Math.max(1, Math.min(8, Math.floor(maxParallelTasks))) : 2;
|
|
86
|
+
if (!SESSION_CLEANUP_MODES.has(String(policy.sessionCleanupOnNodeRemove))) {
|
|
87
|
+
policy.sessionCleanupOnNodeRemove = 'preserve';
|
|
88
|
+
}
|
|
89
|
+
return policy;
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
export function listMeshes(): LocalMeshEntry[] {
|
|
78
93
|
return loadMeshConfig().meshes;
|
|
79
94
|
}
|
|
@@ -112,7 +127,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
|
|
|
112
127
|
repoIdentity,
|
|
113
128
|
repoRemoteUrl: opts.repoRemoteUrl,
|
|
114
129
|
defaultBranch: opts.defaultBranch,
|
|
115
|
-
policy:
|
|
130
|
+
policy: mergeMeshPolicy(undefined, opts.policy),
|
|
116
131
|
coordinator: opts.coordinator || {},
|
|
117
132
|
nodes: [],
|
|
118
133
|
createdAt: now,
|
|
@@ -138,7 +153,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
|
|
|
138
153
|
|
|
139
154
|
if (opts.name !== undefined) mesh.name = opts.name.trim().slice(0, 100);
|
|
140
155
|
if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
|
|
141
|
-
if (opts.policy) mesh.policy =
|
|
156
|
+
if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
|
|
142
157
|
if (opts.coordinator) mesh.coordinator = opts.coordinator;
|
|
143
158
|
mesh.updatedAt = new Date().toISOString();
|
|
144
159
|
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -55,6 +55,8 @@ export type RepoMeshNodeHealth =
|
|
|
55
55
|
|
|
56
56
|
// ─── Policy Types ───────────────────────────────
|
|
57
57
|
|
|
58
|
+
export type RepoMeshSessionCleanupMode = 'preserve' | 'stop' | 'delete_stopped' | 'stop_and_delete';
|
|
59
|
+
|
|
58
60
|
export interface RepoMeshPolicy {
|
|
59
61
|
requirePreTaskCheckpoint: boolean;
|
|
60
62
|
requirePostTaskCheckpoint: boolean;
|
|
@@ -63,6 +65,12 @@ export interface RepoMeshPolicy {
|
|
|
63
65
|
dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
|
|
64
66
|
maxParallelTasks: number;
|
|
65
67
|
allowedProviders?: string[];
|
|
68
|
+
/**
|
|
69
|
+
* What to do with delegated session-host records for a node when it is removed.
|
|
70
|
+
* Defaults to 'preserve' so completed work can be reviewed later and live
|
|
71
|
+
* runtimes are never stopped/deleted unless the mesh owner opts in.
|
|
72
|
+
*/
|
|
73
|
+
sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
export interface RepoMeshNodePolicy {
|
|
@@ -80,6 +88,7 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
|
|
|
80
88
|
requireApprovalForDestructiveGit: true,
|
|
81
89
|
dirtyWorkspaceBehavior: 'warn',
|
|
82
90
|
maxParallelTasks: 2,
|
|
91
|
+
sessionCleanupOnNodeRemove: 'preserve',
|
|
83
92
|
};
|
|
84
93
|
|
|
85
94
|
// ─── Capabilities ───────────────────────────────
|