@adhdev/daemon-core 0.9.82-rc.48 → 0.9.82-rc.49

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.
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync } from 'fs';
10
10
  import { join } from 'path';
11
- import { randomUUID } from 'crypto';
11
+ import { createHash, randomBytes, randomUUID } from 'crypto';
12
12
  import { getConfigDir } from './config.js';
13
13
  import type {
14
14
  LocalMeshConfig,
@@ -18,8 +18,11 @@ import type {
18
18
  RepoMeshNodePolicy,
19
19
  RepoMeshNodeCapabilities,
20
20
  RepoMeshCoordinatorConfig,
21
+ RepoMeshHostMetadata,
22
+ RepoMeshDaemonRole,
21
23
  } from '../repo-mesh-types.js';
22
24
  import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
25
+ import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
23
26
 
24
27
  // ─── Persistence ────────────────────────────────
25
28
 
@@ -112,6 +115,7 @@ export interface CreateMeshOptions {
112
115
  defaultBranch?: string;
113
116
  policy?: Partial<RepoMeshPolicy>;
114
117
  coordinator?: RepoMeshCoordinatorConfig;
118
+ meshHost?: RepoMeshHostMetadata;
115
119
  }
116
120
 
117
121
  export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
@@ -133,6 +137,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
133
137
  defaultBranch: opts.defaultBranch,
134
138
  policy: mergeMeshPolicy(undefined, opts.policy),
135
139
  coordinator: opts.coordinator || {},
140
+ meshHost: opts.meshHost || createDefaultMeshHostMetadata(),
136
141
  nodes: [],
137
142
  createdAt: now,
138
143
  updatedAt: now,
@@ -148,6 +153,7 @@ export interface UpdateMeshOptions {
148
153
  defaultBranch?: string;
149
154
  policy?: Partial<RepoMeshPolicy>;
150
155
  coordinator?: RepoMeshCoordinatorConfig;
156
+ meshHost?: RepoMeshHostMetadata;
151
157
  }
152
158
 
153
159
  export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEntry | undefined {
@@ -159,6 +165,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
159
165
  if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
160
166
  if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
161
167
  if (opts.coordinator) mesh.coordinator = opts.coordinator;
168
+ if (opts.meshHost) mesh.meshHost = opts.meshHost;
162
169
  mesh.updatedAt = new Date().toISOString();
163
170
 
164
171
  saveMeshConfig(config);
@@ -174,6 +181,240 @@ export function deleteMesh(meshId: string): boolean {
174
181
  return true;
175
182
  }
176
183
 
184
+ function normalizeManualHostAddress(hostAddress: string): string {
185
+ const normalized = hostAddress.trim().replace(/\/+$/, '');
186
+ if (!normalized) throw new Error('hostAddress required');
187
+ let parsed: URL;
188
+ try {
189
+ parsed = new URL(normalized);
190
+ } catch {
191
+ throw new Error('hostAddress must be a valid http(s) or ws(s) URL');
192
+ }
193
+ if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
194
+ throw new Error('hostAddress must use http, https, ws, or wss');
195
+ }
196
+ return normalized;
197
+ }
198
+
199
+ export function tokenIdForManualPairing(token: string): string {
200
+ return `tok_${createHash('sha256').update(token).digest('hex').slice(0, 16)}`;
201
+ }
202
+
203
+ function normalizeTokenExpiry(value: unknown): string | undefined {
204
+ if (typeof value !== 'string' || !value.trim()) return undefined;
205
+ const date = new Date(value);
206
+ if (Number.isNaN(date.getTime())) throw new Error('expiresAt must be a valid ISO date');
207
+ return date.toISOString();
208
+ }
209
+
210
+ function assertPairingTokenValid(pairing: RepoMeshHostMetadata['pairing'], rawToken: string, nowIso: string): { ok: true; tokenId: string } | { ok: false; reason: string; expectedTokenId?: string; presentedTokenId?: string } {
211
+ const token = rawToken.trim();
212
+ if (!token) return { ok: false, reason: 'token required' };
213
+ const presentedTokenId = tokenIdForManualPairing(token);
214
+ const expectedTokenId = pairing?.tokenId;
215
+ if (!expectedTokenId || pairing?.status === 'not_configured' || pairing?.status === 'revoked') {
216
+ return { ok: false, reason: 'host pairing token is not configured', presentedTokenId };
217
+ }
218
+ if (pairing.expiresAt && new Date(pairing.expiresAt).getTime() <= new Date(nowIso).getTime()) {
219
+ return { ok: false, reason: 'host pairing token expired', expectedTokenId, presentedTokenId };
220
+ }
221
+ if (presentedTokenId !== expectedTokenId) {
222
+ return { ok: false, reason: 'invalid pairing token', expectedTokenId, presentedTokenId };
223
+ }
224
+ return { ok: true, tokenId: presentedTokenId };
225
+ }
226
+
227
+ export interface ConfigureMeshHostPairingOptions {
228
+ hostAddress: string;
229
+ token: string;
230
+ now?: string;
231
+ }
232
+
233
+ export function configureMeshHostPairing(
234
+ meshId: string,
235
+ opts: ConfigureMeshHostPairingOptions,
236
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; hostAddress: string } | undefined {
237
+ const hostAddress = normalizeManualHostAddress(opts.hostAddress);
238
+ const token = opts.token.trim();
239
+ if (!token) throw new Error('token required');
240
+
241
+ const config = loadMeshConfig();
242
+ const mesh = config.meshes.find(m => m.id === meshId);
243
+ if (!mesh) return undefined;
244
+
245
+ const now = opts.now || new Date().toISOString();
246
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
247
+ const meshHost: RepoMeshHostMetadata = {
248
+ ...previous,
249
+ role: 'member',
250
+ hostAddress,
251
+ pairing: {
252
+ status: 'pairing',
253
+ tokenId: tokenIdForManualPairing(token),
254
+ lastPairedAt: now,
255
+ },
256
+ };
257
+
258
+ mesh.meshHost = meshHost;
259
+ mesh.updatedAt = now;
260
+ saveMeshConfig(config);
261
+ return { mesh, meshHost, hostAddress };
262
+ }
263
+
264
+ export interface CreateMeshHostPairingTokenOptions {
265
+ token?: string;
266
+ expiresAt?: string;
267
+ now?: string;
268
+ }
269
+
270
+ export function createMeshHostPairingToken(
271
+ meshId: string,
272
+ opts: CreateMeshHostPairingTokenOptions = {},
273
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; token: string; tokenId: string; expiresAt?: string } | undefined {
274
+ const config = loadMeshConfig();
275
+ const mesh = config.meshes.find(m => m.id === meshId);
276
+ if (!mesh) return undefined;
277
+ const now = opts.now || new Date().toISOString();
278
+ const token = (opts.token || `mhj_${randomBytes(24).toString('base64url')}`).trim();
279
+ if (!token) throw new Error('token required');
280
+ const tokenId = tokenIdForManualPairing(token);
281
+ const expiresAt = normalizeTokenExpiry(opts.expiresAt);
282
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
283
+ if (previous.role === 'member') {
284
+ throw new Error('Mesh Host daemon required to create host pairing tokens; member daemons cannot mint host join tokens.');
285
+ }
286
+ const meshHost: RepoMeshHostMetadata = {
287
+ ...previous,
288
+ role: 'host',
289
+ pairing: {
290
+ status: 'pairing',
291
+ tokenId,
292
+ lastPairedAt: now,
293
+ ...(expiresAt ? { expiresAt } : {}),
294
+ },
295
+ };
296
+ mesh.meshHost = meshHost;
297
+ mesh.updatedAt = now;
298
+ saveMeshConfig(config);
299
+ return { mesh, meshHost, token, tokenId, ...(expiresAt ? { expiresAt } : {}) };
300
+ }
301
+
302
+ export interface MeshHostJoinMemberNodeInput {
303
+ id?: string;
304
+ workspace: string;
305
+ repoRoot?: string;
306
+ daemonId?: string;
307
+ machineId?: string;
308
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
309
+ policy?: RepoMeshNodePolicy;
310
+ role?: RepoMeshDaemonRole;
311
+ }
312
+
313
+ export interface ApplyMeshHostJoinOptions {
314
+ token: string;
315
+ memberNode: MeshHostJoinMemberNodeInput;
316
+ memberMeshId?: string;
317
+ now?: string;
318
+ }
319
+
320
+ export function applyMeshHostJoinRequest(
321
+ meshId: string,
322
+ opts: ApplyMeshHostJoinOptions,
323
+ ): { accepted: true; mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; node: LocalMeshNodeEntry; tokenId: string } | { accepted: false; mesh?: LocalMeshEntry; meshHost?: RepoMeshHostMetadata; tokenId?: string; reason: string } | undefined {
324
+ const config = loadMeshConfig();
325
+ const mesh = config.meshes.find(m => m.id === meshId);
326
+ if (!mesh) return undefined;
327
+ const now = opts.now || new Date().toISOString();
328
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
329
+ if (previous.role === 'member') {
330
+ return { accepted: false, mesh, meshHost: previous, reason: 'Mesh Host daemon required to accept join requests' };
331
+ }
332
+ const meshHost: RepoMeshHostMetadata = { ...previous, role: 'host' };
333
+ const validation = assertPairingTokenValid(meshHost.pairing, opts.token, now);
334
+ if (!validation.ok) {
335
+ mesh.meshHost = {
336
+ ...meshHost,
337
+ pairing: {
338
+ ...(meshHost.pairing || { status: 'not_configured' as const }),
339
+ status: 'rejected',
340
+ lastRejectedAt: now,
341
+ },
342
+ };
343
+ mesh.updatedAt = now;
344
+ saveMeshConfig(config);
345
+ return { accepted: false, mesh, meshHost: mesh.meshHost, tokenId: validation.presentedTokenId, reason: validation.reason };
346
+ }
347
+
348
+ const workspace = opts.memberNode.workspace.trim();
349
+ if (!workspace) throw new Error('memberNode.workspace required');
350
+ const memberId = opts.memberNode.id?.trim();
351
+ let node = mesh.nodes.find(n => (memberId && n.id === memberId) || n.workspace === workspace);
352
+ if (node) {
353
+ node.workspace = workspace;
354
+ node.repoRoot = opts.memberNode.repoRoot;
355
+ node.daemonId = opts.memberNode.daemonId;
356
+ node.machineId = opts.memberNode.machineId;
357
+ node.userOverrides = opts.memberNode.userOverrides || node.userOverrides || {};
358
+ node.policy = { ...(node.policy || {}), ...(opts.memberNode.policy || {}) };
359
+ node.role = 'member';
360
+ } else {
361
+ if (mesh.nodes.length >= 10) throw new Error('Maximum 10 nodes per mesh');
362
+ node = {
363
+ id: memberId || `node_${randomUUID().replace(/-/g, '')}`,
364
+ workspace,
365
+ repoRoot: opts.memberNode.repoRoot,
366
+ daemonId: opts.memberNode.daemonId,
367
+ machineId: opts.memberNode.machineId,
368
+ userOverrides: opts.memberNode.userOverrides || {},
369
+ policy: opts.memberNode.policy || {},
370
+ role: 'member',
371
+ };
372
+ mesh.nodes.push(node);
373
+ }
374
+ mesh.meshHost = {
375
+ ...meshHost,
376
+ pairing: {
377
+ ...(meshHost.pairing || {}),
378
+ status: 'paired',
379
+ tokenId: validation.tokenId,
380
+ joinedAt: now,
381
+ lastPairedAt: meshHost.pairing?.lastPairedAt || now,
382
+ ...(meshHost.pairing?.expiresAt ? { expiresAt: meshHost.pairing.expiresAt } : {}),
383
+ },
384
+ };
385
+ mesh.updatedAt = now;
386
+ saveMeshConfig(config);
387
+ return { accepted: true, mesh, meshHost: mesh.meshHost, node, tokenId: validation.tokenId };
388
+ }
389
+
390
+ export function markMeshHostPairingJoined(
391
+ meshId: string,
392
+ opts: { hostDaemonId?: string; hostNodeId?: string; joinedAt?: string; token?: string; tokenId?: string },
393
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata } | undefined {
394
+ const config = loadMeshConfig();
395
+ const mesh = config.meshes.find(m => m.id === meshId);
396
+ if (!mesh) return undefined;
397
+ const now = opts.joinedAt || new Date().toISOString();
398
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
399
+ const tokenId = opts.tokenId || (opts.token ? tokenIdForManualPairing(opts.token) : previous.pairing?.tokenId);
400
+ mesh.meshHost = {
401
+ ...previous,
402
+ role: 'member',
403
+ ...(opts.hostDaemonId ? { hostDaemonId: opts.hostDaemonId } : {}),
404
+ ...(opts.hostNodeId ? { hostNodeId: opts.hostNodeId } : {}),
405
+ pairing: {
406
+ ...(previous.pairing || {}),
407
+ status: 'paired',
408
+ ...(tokenId ? { tokenId } : {}),
409
+ joinedAt: now,
410
+ lastPairedAt: previous.pairing?.lastPairedAt || now,
411
+ },
412
+ };
413
+ mesh.updatedAt = now;
414
+ saveMeshConfig(config);
415
+ return { mesh, meshHost: mesh.meshHost };
416
+ }
417
+
177
418
  // ─── Node Operations ────────────────────────────
178
419
 
179
420
  export interface AddNodeOptions {
@@ -186,6 +427,7 @@ export interface AddNodeOptions {
186
427
  isLocalWorktree?: boolean;
187
428
  worktreeBranch?: string;
188
429
  clonedFromNodeId?: string;
430
+ role?: RepoMeshDaemonRole;
189
431
  }
190
432
 
191
433
  export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
@@ -213,6 +455,7 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
213
455
  isLocalWorktree: opts.isLocalWorktree,
214
456
  worktreeBranch: opts.worktreeBranch,
215
457
  clonedFromNodeId: opts.clonedFromNodeId,
458
+ role: opts.role,
216
459
  };
217
460
 
218
461
  mesh.nodes.push(node);
package/src/index.ts CHANGED
@@ -88,6 +88,10 @@ export type {
88
88
  // ── Repo Mesh Types (cross-package) ──
89
89
  export type {
90
90
  RepoMesh,
91
+ RepoMeshDaemonRole,
92
+ RepoMeshHostMetadata,
93
+ RepoMeshHostPairingMetadata,
94
+ RepoMeshHostStatus,
91
95
  RepoMeshNode,
92
96
  RepoMeshNodeHealth,
93
97
  RepoMeshPolicy,
@@ -155,6 +159,21 @@ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './con
155
159
  // ── Mesh Coordinator ──
156
160
  export { buildCoordinatorSystemPrompt } from './mesh/coordinator-prompt.js';
157
161
  export type { CoordinatorPromptContext } from './mesh/coordinator-prompt.js';
162
+ export {
163
+ MESH_REFINE_CONFIG_LOCATIONS,
164
+ MESH_REFINE_CONFIG_SCHEMA,
165
+ loadMeshRefineConfig,
166
+ resolveMeshRefineValidationPlan,
167
+ suggestMeshRefineConfig,
168
+ validateMeshRefineConfig,
169
+ } from './mesh/refine-config.js';
170
+ export type {
171
+ MeshRefineValidationCategory,
172
+ MeshRefineValidationCommandPlan,
173
+ MeshRefineValidationPlan,
174
+ RepoMeshRefineConfig,
175
+ RepoMeshRefineValidationCommandConfig,
176
+ } from './mesh/refine-config.js';
158
177
  export { syncMeshes } from './mesh/mesh-sync.js';
159
178
  export type { MeshSyncTransport, MeshSyncResult, RemoteMeshRecord } from './mesh/mesh-sync.js';
160
179
 
@@ -166,7 +185,10 @@ export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshL
166
185
 
167
186
  // ── Mesh Work Queue (GUPP) ──
168
187
  export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats } from './mesh/mesh-work-queue.js';
169
- export type { MeshWorkQueueEntry, MeshTaskStatus, MeshWorkQueueStats } from './mesh/mesh-work-queue.js';
188
+ export type { MeshWorkQueueEntry, MeshTaskStatus, MeshWorkQueueStats, MeshQueueMutationOptions } from './mesh/mesh-work-queue.js';
189
+
190
+ // ── Mesh Host Ownership ──
191
+ export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHostOwner, normalizeMeshDaemonRole, requireMeshHostQueueOwner, resolveMeshHostStatus } from './mesh/mesh-host-ownership.js';
170
192
 
171
193
  // ── Mesh Visualization ──
172
194
  // buildMeshGraph and MeshGraph types moved to @adhdev/web-core to avoid
@@ -0,0 +1,73 @@
1
+ import type { RepoMeshDaemonRole, RepoMeshHostMetadata, RepoMeshHostStatus } from '../repo-mesh-types.js';
2
+
3
+ function readObject(value: unknown): Record<string, unknown> | null {
4
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
5
+ }
6
+
7
+ function readString(value: unknown): string | undefined {
8
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
9
+ }
10
+
11
+ export function normalizeMeshDaemonRole(value: unknown): RepoMeshDaemonRole | undefined {
12
+ return value === 'host' || value === 'member' ? value : undefined;
13
+ }
14
+
15
+ export function resolveMeshHostStatus(mesh: unknown): RepoMeshHostStatus {
16
+ const meshRecord = readObject(mesh);
17
+ const raw = readObject(meshRecord?.meshHost);
18
+ const role = normalizeMeshDaemonRole(raw?.role) ?? 'host';
19
+ const pairing = readObject(raw?.pairing);
20
+ const normalized: RepoMeshHostStatus = {
21
+ role,
22
+ canOwnCoordinator: role === 'host',
23
+ canOwnQueue: role === 'host',
24
+ defaulted: !raw,
25
+ };
26
+ const hostDaemonId = readString(raw?.hostDaemonId);
27
+ const hostNodeId = readString(raw?.hostNodeId);
28
+ const hostAddress = readString(raw?.hostAddress);
29
+ if (hostDaemonId) normalized.hostDaemonId = hostDaemonId;
30
+ if (hostNodeId) normalized.hostNodeId = hostNodeId;
31
+ if (hostAddress) normalized.hostAddress = hostAddress;
32
+ if (pairing) {
33
+ const status = pairing.status === 'pairing' || pairing.status === 'paired' || pairing.status === 'rejected' || pairing.status === 'revoked'
34
+ ? pairing.status
35
+ : 'not_configured';
36
+ normalized.pairing = {
37
+ status,
38
+ ...(readString(pairing.tokenId) ? { tokenId: readString(pairing.tokenId) } : {}),
39
+ ...(readString(pairing.joinedAt) ? { joinedAt: readString(pairing.joinedAt) } : {}),
40
+ ...(readString(pairing.lastPairedAt) ? { lastPairedAt: readString(pairing.lastPairedAt) } : {}),
41
+ ...(readString(pairing.lastRejectedAt) ? { lastRejectedAt: readString(pairing.lastRejectedAt) } : {}),
42
+ ...(readString(pairing.expiresAt) ? { expiresAt: readString(pairing.expiresAt) } : {}),
43
+ };
44
+ }
45
+ return normalized;
46
+ }
47
+
48
+ export function isMeshHostOwner(mesh: unknown): boolean {
49
+ return resolveMeshHostStatus(mesh).role === 'host';
50
+ }
51
+
52
+ export function buildMeshHostRequiredFailure(mesh: unknown, operation: string): Record<string, unknown> {
53
+ const meshHost = resolveMeshHostStatus(mesh);
54
+ return {
55
+ success: false,
56
+ code: 'mesh_host_required',
57
+ error: `Mesh Host daemon required for ${operation}; member daemons must pair with the host and cannot own coordinator/queue mutations.`,
58
+ meshHost,
59
+ };
60
+ }
61
+
62
+ export function requireMeshHostQueueOwner(opts?: { ownerRole?: RepoMeshDaemonRole }): void {
63
+ if (opts?.ownerRole === 'member') {
64
+ throw new Error('Mesh Host daemon required to mutate mesh queue; member daemons must use the host-owned queue.');
65
+ }
66
+ }
67
+
68
+ export function createDefaultMeshHostMetadata(): RepoMeshHostMetadata {
69
+ return {
70
+ role: 'host',
71
+ pairing: { status: 'not_configured' },
72
+ };
73
+ }
@@ -31,6 +31,7 @@ export type MeshLedgerKind =
31
31
  | 'session_stopped'
32
32
  | 'checkpoint_created'
33
33
  | 'node_cloned'
34
+ | 'node_joined'
34
35
  | 'node_removed'
35
36
  | 'coordinator_started'
36
37
  | 'recovery_attempted'
@@ -2,6 +2,8 @@ import { existsSync, writeFileSync, readFileSync, openSync, closeSync, unlinkSyn
2
2
  import { join } from 'path';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { getLedgerDir } from './mesh-ledger.js';
5
+ import { requireMeshHostQueueOwner } from './mesh-host-ownership.js';
6
+ import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
5
7
 
6
8
  export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
7
9
  export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
@@ -45,6 +47,10 @@ export interface MeshWorkQueueEntry {
45
47
  updatedAt: string;
46
48
  }
47
49
 
50
+ export interface MeshQueueMutationOptions {
51
+ ownerRole?: RepoMeshDaemonRole;
52
+ }
53
+
48
54
  function getQueuePath(meshId: string): string {
49
55
  const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
50
56
  return join(getLedgerDir(), `${safe}.queue.json`);
@@ -97,8 +103,9 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
97
103
  export function enqueueTask(
98
104
  meshId: string,
99
105
  message: string,
100
- opts?: { targetNodeId?: string; targetSessionId?: string }
106
+ opts?: { targetNodeId?: string; targetSessionId?: string } & MeshQueueMutationOptions,
101
107
  ): MeshWorkQueueEntry {
108
+ requireMeshHostQueueOwner(opts);
102
109
  return withQueueLock(meshId, () => {
103
110
  const queue = readQueue(meshId);
104
111
  const entry: MeshWorkQueueEntry = {
@@ -166,7 +173,9 @@ export function updateTaskStatus(
166
173
  meshId: string,
167
174
  taskId: string,
168
175
  status: MeshTaskStatus,
176
+ opts?: MeshQueueMutationOptions,
169
177
  ): MeshWorkQueueEntry | null {
178
+ requireMeshHostQueueOwner(opts);
170
179
  return withQueueLock(meshId, () => {
171
180
  const queue = readQueue(meshId);
172
181
  const idx = queue.findIndex(q => q.id === taskId);
@@ -201,8 +210,9 @@ export function recordTaskAutoLaunch(
201
210
  export function cancelTask(
202
211
  meshId: string,
203
212
  taskId: string,
204
- opts?: { reason?: string },
213
+ opts?: { reason?: string } & MeshQueueMutationOptions,
205
214
  ): MeshWorkQueueEntry | null {
215
+ requireMeshHostQueueOwner(opts);
206
216
  return withQueueLock(meshId, () => {
207
217
  const queue = readQueue(meshId);
208
218
  const idx = queue.findIndex(q => q.id === taskId);
@@ -230,8 +240,9 @@ export function requeueTask(
230
240
  targetSessionId?: string;
231
241
  clearTargetNode?: boolean;
232
242
  clearTargetSession?: boolean;
233
- },
243
+ } & MeshQueueMutationOptions,
234
244
  ): MeshWorkQueueEntry | null {
245
+ requireMeshHostQueueOwner(opts);
235
246
  return withQueueLock(meshId, () => {
236
247
  const queue = readQueue(meshId);
237
248
  const idx = queue.findIndex(q => q.id === taskId);