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

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.
@@ -0,0 +1,555 @@
1
+ /**
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,
5
+ * recovery_context_query. Part of the turn-ipc wire contract (./turn-ipc.ts).
6
+ */
7
+
8
+ // ─── C-W9a: local records, queue composites, active work ────────────────────
9
+ //
10
+ // Wiring-unification Phase C, workstream C-W9a (the 2026-09-24 14:00 stamp
11
+ // "Left for C-W9"): the event ledger and its JSONL mirror are gone, and every
12
+ // mcp-server call that still reached the daemon's `mesh-runtime.db` in-process
13
+ // — record appends, queue mutations, the queue/active-work reads that fed
14
+ // `buildMeshActiveWork`, recovery hints — moves behind the commands below,
15
+ // executed in the daemon that owns the store.
16
+ //
17
+ // CONTENT BOUNDARY: like `mission_upsert`'s `goal`, `note_upsert`'s `text` and
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
20
+ // message, an active-work record with its task summary). This is fine for the
21
+ // same reason: local IPC between the mcp-server and the daemon on the
22
+ // operator's own machine — never a cross-machine or server hop. Those fields
23
+ // are documented as passthroughs and validated only for shape (object/array),
24
+ // exactly like `LedgerQueryEntryWire.payload`. Nothing here is ever published
25
+ // to a topic: `record_local`'s topic leg is the daemon's `mesh.record`
26
+ // allow-list projection, which drops free text by construction.
27
+
28
+ import { isRecord } from './protocol/envelope';
29
+ import { isEvidenceIdentifier } from './turn-evidence';
30
+ import {
31
+ TURN_IPC_PROTOCOL_VERSION,
32
+ hasOnlyKeys,
33
+ isOptionalId,
34
+ isNonNegativeInt,
35
+ isOptionalBoolean,
36
+ isOptionalRecord,
37
+ isRecordArray,
38
+ isOptionalString,
39
+ makeGuard,
40
+ } from './turn-ipc-guards';
41
+
42
+ /** A queue row as the daemon's `getQueue` returns it — a JSON passthrough (see the section note). */
43
+ export type QueueEntryWire = Record<string, unknown> & { id: string; status: string }
44
+
45
+ function isQueueEntryWire(value: unknown): value is QueueEntryWire {
46
+ return isRecord(value) && isEvidenceIdentifier(value.id) && typeof value.status === 'string'
47
+ }
48
+
49
+ // ── record_local ──
50
+ //
51
+ // `meshRecord(meshId, kind, scalars, { local: true })` over IPC: the scalar
52
+ // projection goes to `mesh.<id>.events` (content-free), the FULL payload lands
53
+ // in the daemon's `mesh_local_records` (local-only). Replaces every mcp-server
54
+ // `appendLedgerEntry` (dispatch / MAGI question+synthesis / checkpoint message /
55
+ // reconcile records). `payload` is free-form JSON (see the section note).
56
+
57
+ export interface RecordLocalRequest {
58
+ v: typeof TURN_IPC_PROTOCOL_VERSION
59
+ meshId: string
60
+ /** A `MeshLedgerKind` (daemon-core); an identifier here (mesh-shared cannot import that union). */
61
+ kind: string
62
+ nodeId?: string
63
+ sessionId?: string
64
+ providerType?: string
65
+ taskId?: string
66
+ payload: Record<string, unknown>
67
+ }
68
+
69
+ export interface RecordLocalResponse {
70
+ eventId: string
71
+ timestamp: string
72
+ storedLocally: boolean
73
+ published: boolean
74
+ }
75
+
76
+ function isRecordLocalRequest(value: unknown): value is RecordLocalRequest {
77
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'kind', 'nodeId', 'sessionId', 'providerType', 'taskId', 'payload'])
78
+ && value.v === TURN_IPC_PROTOCOL_VERSION
79
+ && isEvidenceIdentifier(value.meshId)
80
+ && isEvidenceIdentifier(value.kind)
81
+ && isOptionalId(value.nodeId) && isOptionalId(value.sessionId) && isOptionalId(value.providerType) && isOptionalId(value.taskId)
82
+ && isRecord(value.payload)
83
+ }
84
+
85
+ export function decodeRecordLocalRequest(value: unknown): RecordLocalRequest | null {
86
+ return isRecordLocalRequest(value) ? value : null
87
+ }
88
+
89
+ function isRecordLocalResponse(value: unknown): value is RecordLocalResponse {
90
+ return isRecord(value) && hasOnlyKeys(value, ['eventId', 'timestamp', 'storedLocally', 'published'])
91
+ && isEvidenceIdentifier(value.eventId) && typeof value.timestamp === 'string'
92
+ && typeof value.storedLocally === 'boolean' && typeof value.published === 'boolean'
93
+ }
94
+
95
+ export function decodeRecordLocalResponse(value: unknown): RecordLocalResponse | null {
96
+ return isRecordLocalResponse(value) ? value : null
97
+ }
98
+
99
+ // ── queue_query ──
100
+
101
+ export interface QueueQueryRequest {
102
+ v: typeof TURN_IPC_PROTOCOL_VERSION
103
+ meshId: string
104
+ /** Status filter (queue statuses); omitted = every row. */
105
+ statuses?: readonly string[]
106
+ /** One row by id (returns 0 or 1 entries). */
107
+ taskId?: string
108
+ /** Project for a VIEW surface: the persisted input envelope → `inputSummary` (summarizeQueueEntryInputForView). */
109
+ view?: boolean
110
+ /**
111
+ * Also return the per-status row counts of the WHOLE mesh queue (columns only,
112
+ * no payload parse) — lets a view read only the rows it shows yet report
113
+ * mesh-wide counts. An older daemon rejects the key.
114
+ */
115
+ withCounts?: boolean
116
+ /** With `withCounts`: also count terminal rows whose `updated_at` is older than this age. */
117
+ historicalOlderThanMs?: number
118
+ /**
119
+ * Also return `dependencyHeads`: id/status/blockedReason/cancelReason of every
120
+ * row the returned entries list in `dependsOn` that is not itself returned —
121
+ * enough to annotate dependency state without reading the whole queue.
122
+ */
123
+ withDependencyHeads?: boolean
124
+ }
125
+
126
+ /** A dependency row reduced to the fields dependency-state annotation reads. */
127
+ export interface QueueDependencyHeadWire {
128
+ id: string
129
+ status: string
130
+ blockedReason?: string
131
+ cancelReason?: string
132
+ }
133
+
134
+ export interface QueueQueryResponse {
135
+ entries: readonly QueueEntryWire[]
136
+ /** `withCounts`: row count per status over the whole mesh queue. */
137
+ counts?: Record<string, number>
138
+ /** `withCounts` + `historicalOlderThanMs`: terminal rows older than that age. */
139
+ oldHistoricalCount?: number
140
+ /** `withDependencyHeads`. */
141
+ dependencyHeads?: readonly QueueDependencyHeadWire[]
142
+ }
143
+
144
+ function isQueueQueryRequest(value: unknown): value is QueueQueryRequest {
145
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'taskId', 'view', 'withCounts', 'historicalOlderThanMs', 'withDependencyHeads'])) return false
146
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
147
+ if (value.statuses !== undefined && (!Array.isArray(value.statuses) || !value.statuses.every(isEvidenceIdentifier))) return false
148
+ if (value.historicalOlderThanMs !== undefined && !isNonNegativeInt(value.historicalOlderThanMs)) return false
149
+ return isOptionalId(value.taskId) && isOptionalBoolean(value.view)
150
+ && isOptionalBoolean(value.withCounts) && isOptionalBoolean(value.withDependencyHeads)
151
+ }
152
+
153
+ export function decodeQueueQueryRequest(value: unknown): QueueQueryRequest | null {
154
+ return isQueueQueryRequest(value) ? value : null
155
+ }
156
+
157
+ function isQueueDependencyHeadWire(value: unknown): value is QueueDependencyHeadWire {
158
+ return isRecord(value) && hasOnlyKeys(value, ['id', 'status', 'blockedReason', 'cancelReason'])
159
+ && typeof value.id === 'string' && typeof value.status === 'string'
160
+ && (value.blockedReason === undefined || typeof value.blockedReason === 'string')
161
+ && (value.cancelReason === undefined || typeof value.cancelReason === 'string')
162
+ }
163
+
164
+ function isQueueQueryResponse(value: unknown): value is QueueQueryResponse {
165
+ if (!isRecord(value) || !hasOnlyKeys(value, ['entries', 'counts', 'oldHistoricalCount', 'dependencyHeads'])) return false
166
+ if (!Array.isArray(value.entries) || !value.entries.every(isQueueEntryWire)) return false
167
+ if (value.counts !== undefined && (!isRecord(value.counts) || !Object.values(value.counts).every(isNonNegativeInt))) return false
168
+ if (value.oldHistoricalCount !== undefined && !isNonNegativeInt(value.oldHistoricalCount)) return false
169
+ if (value.dependencyHeads !== undefined && (!Array.isArray(value.dependencyHeads) || !value.dependencyHeads.every(isQueueDependencyHeadWire))) return false
170
+ return true
171
+ }
172
+
173
+ export function decodeQueueQueryResponse(value: unknown): QueueQueryResponse | null {
174
+ return isQueueQueryResponse(value) ? value : null
175
+ }
176
+
177
+ // ── queue_enqueue ──
178
+ //
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
182
+ // passthrough; the daemon's enqueue guards (message, task-mode, difficulty,
183
+ // dependency cycle) run unchanged and a refusal comes back as the error.
184
+
185
+ export interface QueueEnqueueRequest {
186
+ v: typeof TURN_IPC_PROTOCOL_VERSION
187
+ meshId: string
188
+ /** Free text (the task message — see the section note). */
189
+ message: string
190
+ options?: Record<string, unknown>
191
+ /** `recordSingleEnqueueDecision` args minus `taskId` (the daemon fills it). */
192
+ decision?: Record<string, unknown>
193
+ }
194
+
195
+ export interface QueueEnqueueResponse {
196
+ entry: QueueEntryWire
197
+ }
198
+
199
+ function isQueueEnqueueRequest(value: unknown): value is QueueEnqueueRequest {
200
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'message', 'options', 'decision'])
201
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId)
202
+ && typeof value.message === 'string'
203
+ && isOptionalRecord(value.options) && isOptionalRecord(value.decision)
204
+ }
205
+
206
+ export function decodeQueueEnqueueRequest(value: unknown): QueueEnqueueRequest | null {
207
+ return isQueueEnqueueRequest(value) ? value : null
208
+ }
209
+
210
+ function isQueueEnqueueResponse(value: unknown): value is QueueEnqueueResponse {
211
+ return isRecord(value) && hasOnlyKeys(value, ['entry']) && isQueueEntryWire(value.entry)
212
+ }
213
+
214
+ export function decodeQueueEnqueueResponse(value: unknown): QueueEnqueueResponse | null {
215
+ return isQueueEnqueueResponse(value) ? value : null
216
+ }
217
+
218
+ // ── queue_enqueue_graph ──
219
+ //
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 {
233
+ v: typeof TURN_IPC_PROTOCOL_VERSION
234
+ 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>
242
+ }
243
+
244
+ export type QueueEnqueueGraphResponse =
245
+ | { ok: true; tasks: readonly QueueEntryWire[]; graph?: Record<string, unknown> }
246
+ | { ok: false; refusalCode?: string; message: string; extra?: Record<string, unknown> }
247
+
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
254
+ }
255
+
256
+ export function decodeQueueEnqueueGraphRequest(value: unknown): QueueEnqueueGraphRequest | null {
257
+ return isQueueEnqueueGraphRequest(value) ? value : null
258
+ }
259
+
260
+ function isQueueEnqueueGraphResponse(value: unknown): value is QueueEnqueueGraphResponse {
261
+ if (!isRecord(value)) return false
262
+ if (value.ok === true) {
263
+ return hasOnlyKeys(value, ['ok', 'tasks', 'graph']) && Array.isArray(value.tasks) && value.tasks.every(isQueueEntryWire)
264
+ && isOptionalRecord(value.graph)
265
+ }
266
+ if (value.ok === false) {
267
+ return hasOnlyKeys(value, ['ok', 'refusalCode', 'message', 'extra']) && typeof value.message === 'string'
268
+ && isOptionalString(value.refusalCode) && isOptionalRecord(value.extra)
269
+ }
270
+ return false
271
+ }
272
+
273
+ export function decodeQueueEnqueueGraphResponse(value: unknown): QueueEnqueueGraphResponse | null {
274
+ return isQueueEnqueueGraphResponse(value) ? value : null
275
+ }
276
+
277
+ // ── queue_cancel ──
278
+
279
+ export interface QueueCancelRequest {
280
+ v: typeof TURN_IPC_PROTOCOL_VERSION
281
+ meshId: string
282
+ taskId: string
283
+ /** Operator reason (free text, local IPC). */
284
+ reason?: string
285
+ }
286
+
287
+ export interface QueueCancelResponse {
288
+ /** The cancelled row, or null when no such task. */
289
+ task: QueueEntryWire | null
290
+ /** The row as it was BEFORE the cancel (its assignment is what a caller stops). */
291
+ before: QueueEntryWire | null
292
+ }
293
+
294
+ function isQueueCancelRequest(value: unknown): value is QueueCancelRequest {
295
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'reason'])
296
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId) && isEvidenceIdentifier(value.taskId)
297
+ && isOptionalString(value.reason)
298
+ }
299
+
300
+ export function decodeQueueCancelRequest(value: unknown): QueueCancelRequest | null {
301
+ return isQueueCancelRequest(value) ? value : null
302
+ }
303
+
304
+ function isQueueCancelResponse(value: unknown): value is QueueCancelResponse {
305
+ return isRecord(value) && hasOnlyKeys(value, ['task', 'before'])
306
+ && (value.task === null || isQueueEntryWire(value.task))
307
+ && (value.before === null || isQueueEntryWire(value.before))
308
+ }
309
+
310
+ export function decodeQueueCancelResponse(value: unknown): QueueCancelResponse | null {
311
+ return isQueueCancelResponse(value) ? value : null
312
+ }
313
+
314
+ // ── queue_requeue ──
315
+
316
+ export interface QueueRequeueRequest {
317
+ v: typeof TURN_IPC_PROTOCOL_VERSION
318
+ meshId: string
319
+ taskId: string
320
+ /** `requeueTask` options (reason / targetNodeId / targetSessionId / clear* / force / message / maxRetries / notBefore), JSON passthrough. */
321
+ options?: Record<string, unknown>
322
+ }
323
+
324
+ export interface QueueRequeueResponse {
325
+ task: QueueEntryWire | null
326
+ }
327
+
328
+ function isQueueRequeueRequest(value: unknown): value is QueueRequeueRequest {
329
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'options'])
330
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId) && isEvidenceIdentifier(value.taskId)
331
+ && isOptionalRecord(value.options)
332
+ }
333
+
334
+ export function decodeQueueRequeueRequest(value: unknown): QueueRequeueRequest | null {
335
+ return isQueueRequeueRequest(value) ? value : null
336
+ }
337
+
338
+ function isQueueRequeueResponse(value: unknown): value is QueueRequeueResponse {
339
+ return isRecord(value) && hasOnlyKeys(value, ['task']) && (value.task === null || isQueueEntryWire(value.task))
340
+ }
341
+
342
+ export function decodeQueueRequeueResponse(value: unknown): QueueRequeueResponse | null {
343
+ return isQueueRequeueResponse(value) ? value : null
344
+ }
345
+
346
+ // ── direct_dispatch_record ──
347
+ //
348
+ // The post-dispatch bookkeeping of `mesh_send_task`'s direct arms, in the
349
+ // 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.
353
+
354
+ export interface DirectDispatchRecordRequest {
355
+ v: typeof TURN_IPC_PROTOCOL_VERSION
356
+ meshId: string
357
+ taskId: string
358
+ /** Free text (the authored task message — the materialised row keeps it). */
359
+ message: string
360
+ /** `recordDirectDispatchTask` options minus `id` (JSON passthrough). */
361
+ task?: Record<string, unknown>
362
+ /** `recordDirectDispatchDecision` args minus `taskId` (JSON passthrough). */
363
+ decision?: Record<string, unknown>
364
+ }
365
+
366
+ export interface DirectDispatchRecordResponse {
367
+ taskRecorded: boolean
368
+ decisionRecorded: boolean
369
+ }
370
+
371
+ function isDirectDispatchRecordRequest(value: unknown): value is DirectDispatchRecordRequest {
372
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'message', 'task', 'decision'])
373
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId) && isEvidenceIdentifier(value.taskId)
374
+ && typeof value.message === 'string' && isOptionalRecord(value.task) && isOptionalRecord(value.decision)
375
+ }
376
+
377
+ export function decodeDirectDispatchRecordRequest(value: unknown): DirectDispatchRecordRequest | null {
378
+ return isDirectDispatchRecordRequest(value) ? value : null
379
+ }
380
+
381
+ function isDirectDispatchRecordResponse(value: unknown): value is DirectDispatchRecordResponse {
382
+ return isRecord(value) && hasOnlyKeys(value, ['taskRecorded', 'decisionRecorded'])
383
+ && typeof value.taskRecorded === 'boolean' && typeof value.decisionRecorded === 'boolean'
384
+ }
385
+
386
+ export function decodeDirectDispatchRecordResponse(value: unknown): DirectDispatchRecordResponse | null {
387
+ return isDirectDispatchRecordResponse(value) ? value : null
388
+ }
389
+
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
+ // ── active_work_query ──
431
+ //
432
+ // The active-work view, COMPUTED in the daemon: it reads the queue, the open
433
+ // direct dispatches (`mesh_direct` attempts) and its local records (+ turn
434
+ // outcomes), and runs `buildMeshActiveWork` over them with the caller's live
435
+ // node probe results. `includeInputs` also returns the records + dispatches it
436
+ // used (the transcript-reconcile pass and the stale-direct prune read them);
437
+ // `compute: false` returns only those inputs. `includeSchedulingRuntime` adds
438
+ // `buildMeshSchedulingRuntime(mesh, queue)` for the caller's `mesh` snapshot.
439
+ // `queue` lets a caller substitute its own annotated queue view.
440
+
441
+ export interface ActiveWorkQueryRequest {
442
+ v: typeof TURN_IPC_PROTOCOL_VERSION
443
+ meshId: string
444
+ /** Live node probe results (`buildMeshActiveWork`'s `nodes`), JSON passthrough. */
445
+ nodes?: readonly Record<string, unknown>[]
446
+ /** Substitute queue rows (default: the daemon's `getQueue`). */
447
+ queue?: readonly Record<string, unknown>[]
448
+ /** Record tail fed to active work (default 200). */
449
+ recordTail?: number
450
+ includeTerminalDirect?: boolean
451
+ /** Default true. */
452
+ compute?: boolean
453
+ includeInputs?: boolean
454
+ includeSummary?: boolean
455
+ includeSchedulingRuntime?: boolean
456
+ /**
457
+ * The caller's mesh snapshot for the scheduling runtime, JSON passthrough.
458
+ * Optional since the daemon resolves its own mesh record when it is absent;
459
+ * an older daemon rejects `includeSchedulingRuntime` without it (the caller's
460
+ * feature-detect signal to resend with the mesh).
461
+ */
462
+ mesh?: Record<string, unknown>
463
+ }
464
+
465
+ export interface ActiveWorkQueryResponse {
466
+ /** `MeshActiveWorkEvidence` (daemon-core), JSON passthrough; absent when `compute: false`. */
467
+ activeWork?: Record<string, unknown>
468
+ records?: readonly Record<string, unknown>[]
469
+ directDispatches?: readonly Record<string, unknown>[]
470
+ /** `MeshLedgerSummary`-shaped record summary. */
471
+ summary?: Record<string, unknown>
472
+ schedulingRuntime?: Record<string, unknown>
473
+ }
474
+
475
+ function isActiveWorkQueryRequest(value: unknown): value is ActiveWorkQueryRequest {
476
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'nodes', 'queue', 'recordTail', 'includeTerminalDirect', 'compute', 'includeInputs', 'includeSummary', 'includeSchedulingRuntime', 'mesh'])) return false
477
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
478
+ if (value.nodes !== undefined && !isRecordArray(value.nodes)) return false
479
+ if (value.queue !== undefined && !isRecordArray(value.queue)) return false
480
+ if (value.recordTail !== undefined && !isNonNegativeInt(value.recordTail)) return false
481
+ return isOptionalBoolean(value.includeTerminalDirect) && isOptionalBoolean(value.compute) && isOptionalBoolean(value.includeInputs)
482
+ && isOptionalBoolean(value.includeSummary) && isOptionalBoolean(value.includeSchedulingRuntime) && isOptionalRecord(value.mesh)
483
+ }
484
+
485
+ export function decodeActiveWorkQueryRequest(value: unknown): ActiveWorkQueryRequest | null {
486
+ return isActiveWorkQueryRequest(value) ? value : null
487
+ }
488
+
489
+ function isActiveWorkQueryResponse(value: unknown): value is ActiveWorkQueryResponse {
490
+ if (!isRecord(value) || !hasOnlyKeys(value, ['activeWork', 'records', 'directDispatches', 'summary', 'schedulingRuntime'])) return false
491
+ if (value.records !== undefined && !isRecordArray(value.records)) return false
492
+ if (value.directDispatches !== undefined && !isRecordArray(value.directDispatches)) return false
493
+ return isOptionalRecord(value.activeWork) && isOptionalRecord(value.summary) && isOptionalRecord(value.schedulingRuntime)
494
+ }
495
+
496
+ export function decodeActiveWorkQueryResponse(value: unknown): ActiveWorkQueryResponse | null {
497
+ return isActiveWorkQueryResponse(value) ? value : null
498
+ }
499
+
500
+ // ── recovery_context_query ──
501
+ //
502
+ // `getSessionRecoveryContext` (daemon-core `mesh-local-records.ts`) in the
503
+ // daemon. `lastTaskMessage` / `advice` are free text (local IPC).
504
+
505
+ export interface RecoveryContextQueryRequest {
506
+ v: typeof TURN_IPC_PROTOCOL_VERSION
507
+ meshId: string
508
+ nodeId?: string
509
+ sessionId?: string
510
+ maxRetries?: number
511
+ /**
512
+ * Batch: one context per node id, computed over ONE record read (answers
513
+ * `contexts`). Exclusive with `nodeId`/`sessionId`. An older daemon rejects it.
514
+ */
515
+ nodeIds?: readonly string[]
516
+ }
517
+
518
+ export interface RecoveryContextQueryResponse {
519
+ /** Single mode (nodeId/sessionId). */
520
+ context?: Record<string, unknown>
521
+ /** Batch mode (`nodeIds`): context per node id. */
522
+ contexts?: Record<string, Record<string, unknown>>
523
+ }
524
+
525
+ function isRecoveryContextQueryRequest(value: unknown): value is RecoveryContextQueryRequest {
526
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'nodeId', 'sessionId', 'maxRetries', 'nodeIds'])) return false
527
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
528
+ if (!isOptionalId(value.nodeId) || !isOptionalId(value.sessionId)) return false
529
+ if (value.maxRetries !== undefined && !isNonNegativeInt(value.maxRetries)) return false
530
+ if (value.nodeIds !== undefined) {
531
+ if (!Array.isArray(value.nodeIds) || value.nodeIds.length === 0 || !value.nodeIds.every(isEvidenceIdentifier)) return false
532
+ return value.nodeId === undefined && value.sessionId === undefined
533
+ }
534
+ return value.nodeId !== undefined || value.sessionId !== undefined
535
+ }
536
+
537
+ export function decodeRecoveryContextQueryRequest(value: unknown): RecoveryContextQueryRequest | null {
538
+ return isRecoveryContextQueryRequest(value) ? value : null
539
+ }
540
+
541
+ function isRecoveryContextWire(value: unknown): boolean {
542
+ return isRecord(value) && typeof value.consecutiveNodeFailures === 'number'
543
+ }
544
+
545
+ function isRecoveryContextQueryResponse(value: unknown): value is RecoveryContextQueryResponse {
546
+ if (!isRecord(value) || !hasOnlyKeys(value, ['context', 'contexts'])) return false
547
+ if (value.context === undefined && value.contexts === undefined) return false
548
+ if (value.context !== undefined && !isRecoveryContextWire(value.context)) return false
549
+ if (value.contexts !== undefined && (!isRecord(value.contexts) || !Object.values(value.contexts).every(isRecoveryContextWire))) return false
550
+ return true
551
+ }
552
+
553
+ export function decodeRecoveryContextQueryResponse(value: unknown): RecoveryContextQueryResponse | null {
554
+ return isRecoveryContextQueryResponse(value) ? value : null
555
+ }