@adhdev/mesh-shared 1.0.60-rc.73 → 1.0.60-rc.74

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * turn-ipc — local records, queue composites and active work (C-W9a):
3
- * record_local, queue_query, queue_enqueue, queue_enqueue_graph, queue_cancel,
4
- * queue_requeue, direct_dispatch_record, graph_audit_record, active_work_query,
3
+ * record_local, queue_query, queue_enqueue, queue_enqueue_batch, queue_cancel,
4
+ * queue_requeue, direct_dispatch_record, active_work_query,
5
5
  * recovery_context_query. Part of the turn-ipc wire contract (./turn-ipc.ts).
6
6
  */
7
7
 
@@ -16,7 +16,7 @@
16
16
  //
17
17
  // CONTENT BOUNDARY: like `mission_upsert`'s `goal`, `note_upsert`'s `text` and
18
18
  // `ledger_query`'s entry payloads, several fields here are free text or
19
- // unbounded JSON (a task message, a MAGI synthesis, a queue row with its
19
+ // unbounded JSON (a task message, a queue row with its
20
20
  // message, an active-work record with its task summary). This is fine for the
21
21
  // same reason: local IPC between the mcp-server and the daemon on the
22
22
  // operator's own machine — never a cross-machine or server hop. Those fields
@@ -36,7 +36,6 @@ import {
36
36
  isOptionalRecord,
37
37
  isRecordArray,
38
38
  isOptionalString,
39
- makeGuard,
40
39
  } from './turn-ipc-guards';
41
40
 
42
41
  /** A queue row as the daemon's `getQueue` returns it — a JSON passthrough (see the section note). */
@@ -51,7 +50,7 @@ function isQueueEntryWire(value: unknown): value is QueueEntryWire {
51
50
  // `meshRecord(meshId, kind, scalars, { local: true })` over IPC: the scalar
52
51
  // projection goes to `mesh.<id>.events` (content-free), the FULL payload lands
53
52
  // in the daemon's `mesh_local_records` (local-only). Replaces every mcp-server
54
- // `appendLedgerEntry` (dispatch / MAGI question+synthesis / checkpoint message /
53
+ // `appendLedgerEntry` (dispatch / checkpoint message /
55
54
  // reconcile records). `payload` is free-form JSON (see the section note).
56
55
 
57
56
  export interface RecordLocalRequest {
@@ -116,7 +115,7 @@ export interface QueueQueryRequest {
116
115
  /** With `withCounts`: also count terminal rows whose `updated_at` is older than this age. */
117
116
  historicalOlderThanMs?: number
118
117
  /**
119
- * Also return `dependencyHeads`: id/status/blockedReason/cancelReason of every
118
+ * Also return `dependencyHeads`: id/status/cancelReason of every
120
119
  * row the returned entries list in `dependsOn` that is not itself returned —
121
120
  * enough to annotate dependency state without reading the whole queue.
122
121
  */
@@ -127,7 +126,6 @@ export interface QueueQueryRequest {
127
126
  export interface QueueDependencyHeadWire {
128
127
  id: string
129
128
  status: string
130
- blockedReason?: string
131
129
  cancelReason?: string
132
130
  }
133
131
 
@@ -155,9 +153,8 @@ export function decodeQueueQueryRequest(value: unknown): QueueQueryRequest | nul
155
153
  }
156
154
 
157
155
  function isQueueDependencyHeadWire(value: unknown): value is QueueDependencyHeadWire {
158
- return isRecord(value) && hasOnlyKeys(value, ['id', 'status', 'blockedReason', 'cancelReason'])
156
+ return isRecord(value) && hasOnlyKeys(value, ['id', 'status', 'cancelReason'])
159
157
  && typeof value.id === 'string' && typeof value.status === 'string'
160
- && (value.blockedReason === undefined || typeof value.blockedReason === 'string')
161
158
  && (value.cancelReason === undefined || typeof value.cancelReason === 'string')
162
159
  }
163
160
 
@@ -176,9 +173,8 @@ export function decodeQueueQueryResponse(value: unknown): QueueQueryResponse | n
176
173
 
177
174
  // ── queue_enqueue ──
178
175
  //
179
- // `enqueueTask(meshId, message, options)` + (when `decision` is given) the
180
- // single-surface `recordSingleEnqueueDecision` for the new task id, in the
181
- // daemon. `options` is `MeshEnqueueTaskOptions` (daemon-core) as a JSON
176
+ // `enqueueTask(meshId, message, options)` in the daemon. `options` is
177
+ // `MeshEnqueueTaskOptions` (daemon-core) as a JSON
182
178
  // passthrough; the daemon's enqueue guards (message, task-mode, difficulty,
183
179
  // dependency cycle) run unchanged and a refusal comes back as the error.
184
180
 
@@ -188,8 +184,6 @@ export interface QueueEnqueueRequest {
188
184
  /** Free text (the task message — see the section note). */
189
185
  message: string
190
186
  options?: Record<string, unknown>
191
- /** `recordSingleEnqueueDecision` args minus `taskId` (the daemon fills it). */
192
- decision?: Record<string, unknown>
193
187
  }
194
188
 
195
189
  export interface QueueEnqueueResponse {
@@ -197,10 +191,10 @@ export interface QueueEnqueueResponse {
197
191
  }
198
192
 
199
193
  function isQueueEnqueueRequest(value: unknown): value is QueueEnqueueRequest {
200
- return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'message', 'options', 'decision'])
194
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'message', 'options'])
201
195
  && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId)
202
196
  && typeof value.message === 'string'
203
- && isOptionalRecord(value.options) && isOptionalRecord(value.decision)
197
+ && isOptionalRecord(value.options)
204
198
  }
205
199
 
206
200
  export function decodeQueueEnqueueRequest(value: unknown): QueueEnqueueRequest | null {
@@ -215,53 +209,41 @@ export function decodeQueueEnqueueResponse(value: unknown): QueueEnqueueResponse
215
209
  return isQueueEnqueueResponse(value) ? value : null
216
210
  }
217
211
 
218
- // ── queue_enqueue_graph ──
212
+ // ── queue_enqueue_batch ──
219
213
  //
220
- // The atomic batch enqueue, both paths, with its audit trail — in the daemon:
221
- // - `mode: 'compat'` → `enqueueTaskGraph(meshId, specs)`;
222
- // - `mode: 'graph'` → `commitMeshGraphPlan(plan)` + `recordGraphEnqueueCommitted`.
223
- // A failure records `recordGraphEnqueueRolledBack` (graph) or
224
- // `recordGraphEnqueueValidationFailed` (compat) from the catch — the daemon
225
- // writes the audit AFTER the failed transaction, exactly as design :752-753
226
- // requires — and answers `{ ok: false, refusalCode?, message, extra? }` (a
227
- // domain refusal is a RESULT here, not a transport error, so the caller keeps
228
- // its code/extra for the tool response). NOT `code`/`error`: those two keys
229
- // belong to the command envelope (`{ success, error, code }`) the client
230
- // unwraps, so a result must never reuse them.
231
-
232
- export interface QueueEnqueueGraphRequest {
214
+ // The atomic multi-task enqueue in the daemon: `enqueueTaskBatch(meshId, specs)`
215
+ // — every task inserts or none does. A refusal answers
216
+ // `{ ok: false, refusalCode?, message, extra? }` (a domain refusal is a RESULT
217
+ // here, not a transport error, so the caller keeps its code/extra for the tool
218
+ // response). NOT `code`/`error`: those two keys belong to the command envelope
219
+ // (`{ success, error, code }`) the client unwraps, so a result must never
220
+ // reuse them.
221
+
222
+ export interface QueueEnqueueBatchRequest {
233
223
  v: typeof TURN_IPC_PROTOCOL_VERSION
234
224
  meshId: string
235
- mode: 'compat' | 'graph'
236
- /** compat: `MeshTaskGraphEntrySpec[]` (JSON passthrough). */
237
- specs?: readonly Record<string, unknown>[]
238
- /** graph: `commitMeshGraphPlan` input minus `meshId` (JSON passthrough). */
239
- plan?: Record<string, unknown>
240
- /** Audit context: batchId / missionId / coordinatorSessionId / onDependencyFailure / orchestrationDecision / taskCount. */
241
- audit?: Record<string, unknown>
225
+ /** `MeshTaskBatchEntrySpec[]` (daemon-core, JSON passthrough). */
226
+ specs: readonly Record<string, unknown>[]
242
227
  }
243
228
 
244
- export type QueueEnqueueGraphResponse =
245
- | { ok: true; tasks: readonly QueueEntryWire[]; graph?: Record<string, unknown> }
229
+ export type QueueEnqueueBatchResponse =
230
+ | { ok: true; tasks: readonly QueueEntryWire[] }
246
231
  | { ok: false; refusalCode?: string; message: string; extra?: Record<string, unknown> }
247
232
 
248
- function isQueueEnqueueGraphRequest(value: unknown): value is QueueEnqueueGraphRequest {
249
- if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'mode', 'specs', 'plan', 'audit'])) return false
250
- if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
251
- if (value.mode === 'compat') return isRecordArray(value.specs) && value.plan === undefined && isOptionalRecord(value.audit)
252
- if (value.mode === 'graph') return isRecord(value.plan) && value.specs === undefined && isOptionalRecord(value.audit)
253
- return false
233
+ function isQueueEnqueueBatchRequest(value: unknown): value is QueueEnqueueBatchRequest {
234
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'specs'])
235
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId)
236
+ && isRecordArray(value.specs)
254
237
  }
255
238
 
256
- export function decodeQueueEnqueueGraphRequest(value: unknown): QueueEnqueueGraphRequest | null {
257
- return isQueueEnqueueGraphRequest(value) ? value : null
239
+ export function decodeQueueEnqueueBatchRequest(value: unknown): QueueEnqueueBatchRequest | null {
240
+ return isQueueEnqueueBatchRequest(value) ? value : null
258
241
  }
259
242
 
260
- function isQueueEnqueueGraphResponse(value: unknown): value is QueueEnqueueGraphResponse {
243
+ function isQueueEnqueueBatchResponse(value: unknown): value is QueueEnqueueBatchResponse {
261
244
  if (!isRecord(value)) return false
262
245
  if (value.ok === true) {
263
- return hasOnlyKeys(value, ['ok', 'tasks', 'graph']) && Array.isArray(value.tasks) && value.tasks.every(isQueueEntryWire)
264
- && isOptionalRecord(value.graph)
246
+ return hasOnlyKeys(value, ['ok', 'tasks']) && Array.isArray(value.tasks) && value.tasks.every(isQueueEntryWire)
265
247
  }
266
248
  if (value.ok === false) {
267
249
  return hasOnlyKeys(value, ['ok', 'refusalCode', 'message', 'extra']) && typeof value.message === 'string'
@@ -270,8 +252,8 @@ function isQueueEnqueueGraphResponse(value: unknown): value is QueueEnqueueGraph
270
252
  return false
271
253
  }
272
254
 
273
- export function decodeQueueEnqueueGraphResponse(value: unknown): QueueEnqueueGraphResponse | null {
274
- return isQueueEnqueueGraphResponse(value) ? value : null
255
+ export function decodeQueueEnqueueBatchResponse(value: unknown): QueueEnqueueBatchResponse | null {
256
+ return isQueueEnqueueBatchResponse(value) ? value : null
275
257
  }
276
258
 
277
259
  // ── queue_cancel ──
@@ -347,9 +329,8 @@ export function decodeQueueRequeueResponse(value: unknown): QueueRequeueResponse
347
329
  //
348
330
  // The post-dispatch bookkeeping of `mesh_send_task`'s direct arms, in the
349
331
  // daemon: `recordDirectDispatchTask` (materialise the queue row, stamped with
350
- // the already-open `mesh_direct` attempt) and `recordDirectDispatchDecision`
351
- // (GRAPH-MEASUREMENT-DIRECT). Each step is best-effort and reported separately
352
- // — a dispatch that already happened must never fail on its bookkeeping.
332
+ // the already-open `mesh_direct` attempt). Best-effort and reported — a
333
+ // dispatch that already happened must never fail on its bookkeeping.
353
334
 
354
335
  export interface DirectDispatchRecordRequest {
355
336
  v: typeof TURN_IPC_PROTOCOL_VERSION
@@ -359,19 +340,16 @@ export interface DirectDispatchRecordRequest {
359
340
  message: string
360
341
  /** `recordDirectDispatchTask` options minus `id` (JSON passthrough). */
361
342
  task?: Record<string, unknown>
362
- /** `recordDirectDispatchDecision` args minus `taskId` (JSON passthrough). */
363
- decision?: Record<string, unknown>
364
343
  }
365
344
 
366
345
  export interface DirectDispatchRecordResponse {
367
346
  taskRecorded: boolean
368
- decisionRecorded: boolean
369
347
  }
370
348
 
371
349
  function isDirectDispatchRecordRequest(value: unknown): value is DirectDispatchRecordRequest {
372
- return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'message', 'task', 'decision'])
350
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'message', 'task'])
373
351
  && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId) && isEvidenceIdentifier(value.taskId)
374
- && typeof value.message === 'string' && isOptionalRecord(value.task) && isOptionalRecord(value.decision)
352
+ && typeof value.message === 'string' && isOptionalRecord(value.task)
375
353
  }
376
354
 
377
355
  export function decodeDirectDispatchRecordRequest(value: unknown): DirectDispatchRecordRequest | null {
@@ -379,54 +357,14 @@ export function decodeDirectDispatchRecordRequest(value: unknown): DirectDispatc
379
357
  }
380
358
 
381
359
  function isDirectDispatchRecordResponse(value: unknown): value is DirectDispatchRecordResponse {
382
- return isRecord(value) && hasOnlyKeys(value, ['taskRecorded', 'decisionRecorded'])
383
- && typeof value.taskRecorded === 'boolean' && typeof value.decisionRecorded === 'boolean'
360
+ return isRecord(value) && hasOnlyKeys(value, ['taskRecorded'])
361
+ && typeof value.taskRecorded === 'boolean'
384
362
  }
385
363
 
386
364
  export function decodeDirectDispatchRecordResponse(value: unknown): DirectDispatchRecordResponse | null {
387
365
  return isDirectDispatchRecordResponse(value) ? value : null
388
366
  }
389
367
 
390
- // ── graph_audit_record ──
391
- //
392
- // The coordinator-gate / node-patch provenance records the `mesh_graph_*`
393
- // tools write after a gate action (design :740-750), written by the daemon's
394
- // allow-listed recorders (mesh-graph-provenance.ts) — the mcp-server never
395
- // builds the record payload itself. `fields` is the recorder's argument object.
396
-
397
- export const GRAPH_AUDIT_EVENTS = ['gate_claimed', 'gate_released', 'gate_abandoned', 'node_patched'] as const
398
- export type GraphAuditEvent = typeof GRAPH_AUDIT_EVENTS[number]
399
- const isGraphAuditEvent = makeGuard(GRAPH_AUDIT_EVENTS)
400
-
401
- export interface GraphAuditRecordRequest {
402
- v: typeof TURN_IPC_PROTOCOL_VERSION
403
- meshId: string
404
- event: GraphAuditEvent
405
- fields: Record<string, unknown>
406
- }
407
-
408
- export interface GraphAuditRecordResponse {
409
- recorded: boolean
410
- }
411
-
412
- function isGraphAuditRecordRequest(value: unknown): value is GraphAuditRecordRequest {
413
- return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'event', 'fields'])
414
- && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId)
415
- && isGraphAuditEvent(value.event) && isRecord(value.fields)
416
- }
417
-
418
- export function decodeGraphAuditRecordRequest(value: unknown): GraphAuditRecordRequest | null {
419
- return isGraphAuditRecordRequest(value) ? value : null
420
- }
421
-
422
- function isGraphAuditRecordResponse(value: unknown): value is GraphAuditRecordResponse {
423
- return isRecord(value) && hasOnlyKeys(value, ['recorded']) && typeof value.recorded === 'boolean'
424
- }
425
-
426
- export function decodeGraphAuditRecordResponse(value: unknown): GraphAuditRecordResponse | null {
427
- return isGraphAuditRecordResponse(value) ? value : null
428
- }
429
-
430
368
  // ── active_work_query ──
431
369
  //
432
370
  // The active-work view, COMPUTED in the daemon: it reads the queue, the open
@@ -0,0 +1,193 @@
1
+ /**
2
+ * turn-ipc — task stats and maintenance commands: task_stats_query,
3
+ * prune_stale_direct, orphaned_pin_notify. Part of the turn-ipc wire contract
4
+ * (./turn-ipc.ts).
5
+ */
6
+
7
+ import {
8
+ TURN_IPC_PROTOCOL_VERSION,
9
+ hasOnlyKeys,
10
+ isOptionalBoolean,
11
+ isOptionalRecord,
12
+ isNonNegativeInt,
13
+ isRecordArray,
14
+ } from './turn-ipc-guards';
15
+ import { isRecord } from './protocol/envelope';
16
+ import { isEvidenceIdentifier } from './turn-evidence';
17
+
18
+ // ─── task_stats_query ───────────────────────────────────────────────────────
19
+ //
20
+ // C-W9c: replaces the in-process `computeMeshTaskStats` / `computeMeshMissionStats`
21
+ // (daemon-core `mesh-task-stats.ts`) — both scan the daemon's queue + local
22
+ // records, so both move behind one command. Exactly one of `taskIds` /
23
+ // `missionId` selects the per-task mode; `missionId` alone with `rollup: true`
24
+ // additionally returns the mission-level aggregate.
25
+
26
+ export interface TaskStatsQueryRequest {
27
+ v: typeof TURN_IPC_PROTOCOL_VERSION
28
+ meshId: string
29
+ taskIds?: readonly string[]
30
+ missionId?: string
31
+ tail?: number
32
+ /** Also compute `computeMeshMissionStats` — requires `missionId`. */
33
+ rollup?: boolean
34
+ /**
35
+ * Batch rollups: `computeMeshMissionStats` for every id, over ONE queue read and
36
+ * ONE record read (answers `missions`, `tasks` empty). Exclusive with
37
+ * `taskIds`/`missionId`. An older daemon rejects the key.
38
+ */
39
+ missionIds?: readonly string[]
40
+ }
41
+
42
+ export interface TaskStatsQueryResponse {
43
+ /** `MeshTaskStats[]`, JSON passthrough. */
44
+ tasks: readonly Record<string, unknown>[]
45
+ /** `MeshMissionStats`, JSON passthrough; present only when `rollup: true` was requested and satisfiable. */
46
+ mission?: Record<string, unknown>
47
+ /** Batch mode: `MeshMissionStats` per requested mission id, JSON passthrough. */
48
+ missions?: Record<string, Record<string, unknown>>
49
+ }
50
+
51
+ function isTaskStatsQueryRequest(value: unknown): value is TaskStatsQueryRequest {
52
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'taskIds', 'missionId', 'tail', 'rollup', 'missionIds'])) return false
53
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
54
+ if (value.taskIds !== undefined) {
55
+ if (!Array.isArray(value.taskIds) || value.taskIds.length === 0 || !value.taskIds.every(isEvidenceIdentifier)) return false
56
+ }
57
+ if (value.missionId !== undefined && !isEvidenceIdentifier(value.missionId)) return false
58
+ if (value.tail !== undefined && !isNonNegativeInt(value.tail)) return false
59
+ if (value.missionIds !== undefined) {
60
+ if (!Array.isArray(value.missionIds) || value.missionIds.length === 0 || !value.missionIds.every(isEvidenceIdentifier)) return false
61
+ if (value.taskIds !== undefined || value.missionId !== undefined) return false
62
+ }
63
+ if (value.rollup === true && value.missionId === undefined) return false
64
+ return isOptionalBoolean(value.rollup)
65
+ }
66
+
67
+ export function decodeTaskStatsQueryRequest(value: unknown): TaskStatsQueryRequest | null {
68
+ return isTaskStatsQueryRequest(value) ? value : null
69
+ }
70
+
71
+ function isTaskStatsQueryResponse(value: unknown): value is TaskStatsQueryResponse {
72
+ return isRecord(value) && hasOnlyKeys(value, ['tasks', 'mission', 'missions']) && isRecordArray(value.tasks) && isOptionalRecord(value.mission)
73
+ && (value.missions === undefined || (isRecord(value.missions) && Object.values(value.missions).every(isRecord)))
74
+ }
75
+
76
+ export function decodeTaskStatsQueryResponse(value: unknown): TaskStatsQueryResponse | null {
77
+ return isTaskStatsQueryResponse(value) ? value : null
78
+ }
79
+
80
+ // ─── prune_stale_direct ─────────────────────────────────────────────────────
81
+ //
82
+ // C-W9c: replaces the in-process `pruneStaleDirectDispatches` call
83
+ // (`mesh_cleanup_sessions` mode=prune_stale_direct, `mesh-tools-session.ts`). Unlike the other
84
+ // C-W9 commands this one does NOT take its inputs (queue/records/direct
85
+ // dispatches) on the wire — the daemon already reads them itself
86
+ // (`getQueue`/`readLocalRecords`/`getActiveDirectDispatches`), so the request
87
+ // carries only the decision knobs. `closeDispatches` (the C-W8 `turn_cancel`
88
+ // callback) also runs daemon-side now, since it is the same process.
89
+
90
+ export interface PruneStaleDirectRequest {
91
+ v: typeof TURN_IPC_PROTOCOL_VERSION
92
+ meshId: string
93
+ execute?: boolean
94
+ includeTerminal?: boolean
95
+ source?: string
96
+ }
97
+
98
+ export interface PruneStaleDirectResponse {
99
+ mode: 'execute' | 'dry_run'
100
+ includeTerminal: boolean
101
+ candidateCount: number
102
+ /** `MeshActiveWorkRecord[]`, JSON passthrough. */
103
+ prunable: readonly Record<string, unknown>[]
104
+ prunedCount: number
105
+ preservedUnacknowledged: readonly Record<string, unknown>[]
106
+ preservedLedgerOnly: readonly Record<string, unknown>[]
107
+ preservedNotOrphan: readonly Record<string, unknown>[]
108
+ }
109
+
110
+ function isPruneStaleDirectRequest(value: unknown): value is PruneStaleDirectRequest {
111
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'execute', 'includeTerminal', 'source'])) return false
112
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
113
+ if (value.source !== undefined && typeof value.source !== 'string') return false
114
+ return isOptionalBoolean(value.execute) && isOptionalBoolean(value.includeTerminal)
115
+ }
116
+
117
+ export function decodePruneStaleDirectRequest(value: unknown): PruneStaleDirectRequest | null {
118
+ return isPruneStaleDirectRequest(value) ? value : null
119
+ }
120
+
121
+ function isPruneStaleDirectResponse(value: unknown): value is PruneStaleDirectResponse {
122
+ if (!isRecord(value) || !hasOnlyKeys(value, ['mode', 'includeTerminal', 'candidateCount', 'prunable', 'prunedCount', 'preservedUnacknowledged', 'preservedLedgerOnly', 'preservedNotOrphan'])) return false
123
+ if (value.mode !== 'execute' && value.mode !== 'dry_run') return false
124
+ if (typeof value.includeTerminal !== 'boolean') return false
125
+ if (!isNonNegativeInt(value.candidateCount) || !isNonNegativeInt(value.prunedCount)) return false
126
+ return isRecordArray(value.prunable) && isRecordArray(value.preservedUnacknowledged)
127
+ && isRecordArray(value.preservedLedgerOnly) && isRecordArray(value.preservedNotOrphan)
128
+ }
129
+
130
+ export function decodePruneStaleDirectResponse(value: unknown): PruneStaleDirectResponse | null {
131
+ return isPruneStaleDirectResponse(value) ? value : null
132
+ }
133
+
134
+ // ─── orphaned_pin_notify ────────────────────────────────────────────────────
135
+ //
136
+ // C-W9c: replaces the in-process `notifyCoordinatorOfOrphanedPins` call
137
+ // (`mesh_queue_cancel` tool, `mesh-tools-queue.ts`, CANCEL-ORPHANS-PINNED-TASK).
138
+ // The core reads the daemon's live queue (`getQueue`) and, when it finds
139
+ // orphans, calls `notifyMeshCoordinator` to page the coordinator — both are
140
+ // daemon-only concerns, so the whole call moves, not just its queue read.
141
+ // `title` is a task-message-derived label (first line, truncated) — free text,
142
+ // hence local IPC only, same rationale as `record_local`'s payload.
143
+
144
+ export interface OrphanedPinNotifyRequest {
145
+ v: typeof TURN_IPC_PROTOCOL_VERSION
146
+ meshId: string
147
+ stoppedSessionId: string
148
+ excludeTaskId?: string
149
+ cause?: string
150
+ nodeId?: string
151
+ coordinatorSessionId?: string
152
+ }
153
+
154
+ export interface OrphanedPinnedTaskWire {
155
+ taskId: string
156
+ title: string
157
+ targetSessionId: string
158
+ targetNodeId?: string
159
+ missionId?: string
160
+ }
161
+
162
+ export interface OrphanedPinNotifyResponse {
163
+ orphans: readonly OrphanedPinnedTaskWire[]
164
+ }
165
+
166
+ function isOrphanedPinnedTaskWire(value: unknown): value is OrphanedPinnedTaskWire {
167
+ if (!isRecord(value) || !hasOnlyKeys(value, ['taskId', 'title', 'targetSessionId', 'targetNodeId', 'missionId'])) return false
168
+ if (!isEvidenceIdentifier(value.taskId) || !isEvidenceIdentifier(value.targetSessionId)) return false
169
+ if (typeof value.title !== 'string') return false
170
+ if (value.targetNodeId !== undefined && !isEvidenceIdentifier(value.targetNodeId)) return false
171
+ return value.missionId === undefined || isEvidenceIdentifier(value.missionId)
172
+ }
173
+
174
+ function isOrphanedPinNotifyRequest(value: unknown): value is OrphanedPinNotifyRequest {
175
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'stoppedSessionId', 'excludeTaskId', 'cause', 'nodeId', 'coordinatorSessionId'])) return false
176
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId) || !isEvidenceIdentifier(value.stoppedSessionId)) return false
177
+ if (value.excludeTaskId !== undefined && !isEvidenceIdentifier(value.excludeTaskId)) return false
178
+ if (value.cause !== undefined && typeof value.cause !== 'string') return false
179
+ if (value.nodeId !== undefined && !isEvidenceIdentifier(value.nodeId)) return false
180
+ return value.coordinatorSessionId === undefined || isEvidenceIdentifier(value.coordinatorSessionId)
181
+ }
182
+
183
+ export function decodeOrphanedPinNotifyRequest(value: unknown): OrphanedPinNotifyRequest | null {
184
+ return isOrphanedPinNotifyRequest(value) ? value : null
185
+ }
186
+
187
+ function isOrphanedPinNotifyResponse(value: unknown): value is OrphanedPinNotifyResponse {
188
+ return isRecord(value) && hasOnlyKeys(value, ['orphans']) && Array.isArray(value.orphans) && value.orphans.every(isOrphanedPinnedTaskWire)
189
+ }
190
+
191
+ export function decodeOrphanedPinNotifyResponse(value: unknown): OrphanedPinNotifyResponse | null {
192
+ return isOrphanedPinNotifyResponse(value) ? value : null
193
+ }
package/src/turn-ipc.ts CHANGED
@@ -52,7 +52,7 @@
52
52
  * LAYOUT
53
53
  * -------
54
54
  * This module holds the error codes and the command registry; the per-command
55
- * contracts live in ./turn-ipc-{turn,mission,queue,graph}.ts (re-exported here)
55
+ * contracts live in ./turn-ipc-{turn,mission,queue,stats}.ts (re-exported here)
56
56
  * over the shared guard primitives in ./turn-ipc-guards.ts.
57
57
  */
58
58
 
@@ -63,7 +63,7 @@ export { TURN_IPC_PROTOCOL_VERSION } from './turn-ipc-guards';
63
63
  export * from './turn-ipc-turn';
64
64
  export * from './turn-ipc-mission';
65
65
  export * from './turn-ipc-queue';
66
- export * from './turn-ipc-graph';
66
+ export * from './turn-ipc-stats';
67
67
 
68
68
  // ─── error codes ────────────────────────────────────────────────────────────
69
69
 
@@ -120,21 +120,15 @@ export const TURN_IPC_COMMANDS = [
120
120
  'record_local',
121
121
  'queue_query',
122
122
  'queue_enqueue',
123
- 'queue_enqueue_graph',
123
+ 'queue_enqueue_batch',
124
124
  'queue_cancel',
125
125
  'queue_requeue',
126
126
  'direct_dispatch_record',
127
- 'graph_audit_record',
128
127
  'active_work_query',
129
128
  'recovery_context_query',
130
129
  // C-W9c additions (2026-09-24 19:00 stamp — the last mcp-server in-process
131
- // daemon-core paths: graph gates/plan/patch, mission reads (MAGI), task/
132
- // mission stats, orphaned-pin helpers, one prune audit):
133
- 'graph_gate_claim',
134
- 'graph_gate_release',
135
- 'graph_gate_abandon',
136
- 'graph_node_patch',
137
- 'graph_view_query',
130
+ // daemon-core paths: mission reads, task/mission stats, orphaned-pin
131
+ // helpers, one prune audit):
138
132
  'task_stats_query',
139
133
  'prune_stale_direct',
140
134
  'orphaned_pin_notify',