@adhdev/mesh-shared 1.0.60-rc.5 → 1.0.60-rc.50

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.
Files changed (41) hide show
  1. package/dist/index.d.ts +10 -0
  2. package/dist/index.js +2788 -17
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +2464 -17
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/mesh-tool-names.d.ts +2 -2
  7. package/dist/mesh-vocabulary.d.ts +70 -0
  8. package/dist/mission-brief.d.ts +62 -0
  9. package/dist/outbound-message.d.ts +202 -0
  10. package/dist/path-ownership.d.ts +101 -0
  11. package/dist/protocol/daemon-server.d.ts +388 -0
  12. package/dist/protocol/dashboard-daemon-p2p.d.ts +212 -0
  13. package/dist/protocol/dashboard-server.d.ts +240 -0
  14. package/dist/protocol/envelope.d.ts +55 -0
  15. package/dist/protocol/index.d.ts +15 -0
  16. package/dist/session-launch.d.ts +113 -0
  17. package/dist/session-status.d.ts +61 -0
  18. package/dist/turn-evidence.d.ts +438 -0
  19. package/dist/turn-ipc.d.ts +974 -0
  20. package/dist/types.d.ts +11 -0
  21. package/dist/worker-protocol.d.ts +69 -0
  22. package/dist/ws-protocol.d.ts +71 -41
  23. package/package.json +1 -1
  24. package/src/index.ts +10 -0
  25. package/src/mesh-tool-names.ts +0 -1
  26. package/src/mesh-vocabulary.ts +98 -0
  27. package/src/mission-brief.ts +153 -0
  28. package/src/outbound-message.ts +320 -0
  29. package/src/path-ownership.ts +271 -0
  30. package/src/protocol/daemon-server.ts +552 -0
  31. package/src/protocol/dashboard-daemon-p2p.ts +272 -0
  32. package/src/protocol/dashboard-server.ts +340 -0
  33. package/src/protocol/envelope.ts +75 -0
  34. package/src/protocol/index.ts +16 -0
  35. package/src/session-launch.ts +247 -0
  36. package/src/session-status.ts +140 -0
  37. package/src/turn-evidence.ts +602 -0
  38. package/src/turn-ipc.ts +2231 -0
  39. package/src/types.ts +8 -0
  40. package/src/worker-protocol.ts +164 -0
  41. package/src/ws-protocol.ts +102 -93
@@ -0,0 +1,2231 @@
1
+ /**
2
+ * turn-ipc — typed IPC command contracts for the six commands that replace
3
+ * mcp-server's direct `mesh-runtime.db` access (design §5 C2 "MCP server").
4
+ *
5
+ * Wiring-unification Phase C, workstream C-W6
6
+ * (docs/design/2026-09-23-wiring-unification.md §5 C2/C3, C9 row C-W6).
7
+ *
8
+ * WHY THIS FILE EXISTS
9
+ * ---------------------
10
+ * mcp-server is a second, in-process writer of `mesh-runtime.db` (~45-69
11
+ * direct-mutation call sites, ~54 reads, per the C-W6 brief). C2 moves every
12
+ * one of those call sites behind IPC, executed in the daemon that owns the
13
+ * turn ledger and the seqscribe node, so mcp-server never opens the SQLite
14
+ * file itself. This file is the WIRE CONTRACT for that IPC surface — request/
15
+ * response shapes, closed enums, runtime decoders — shared by the daemon-core
16
+ * command handlers (added with C-W6 proper; NOT this file) and the mcp-server
17
+ * client (`mcp-server/src/ipc/turn-commands.ts`, this workstream).
18
+ *
19
+ * The six commands (§5 C2):
20
+ * - turn_observe — submit one TurnEvidence; returns the reducer verdict.
21
+ * - mesh_record — append a content-free scalar record (`mesh.record`
22
+ * topic entry), replacing `appendLedgerEntry`.
23
+ * - turn_cancel — cancel an attempt (`cancel` evidence, source `mcp_probe`
24
+ * or `operator`).
25
+ * - operator_status — record a fire-and-forget operator/tool-call status
26
+ * (`operator_status` evidence + the former
27
+ * `recordMeshCoordinatorToolCall` log write).
28
+ * - turn_query — read attempt/event rows (own or fleet scope).
29
+ * - mesh_index_query — read `mesh_topic_index` rows (fleet-wide, replacing
30
+ * the in-memory read model / parity reads).
31
+ *
32
+ * CONTENT BOUNDARY
33
+ * -----------------
34
+ * Every request/response field is an identifier, a closed enum, a boolean, a
35
+ * counter or a timestamp. `turn_observe` carries a `TurnEvidence`, which is
36
+ * content-free by construction (`turn-evidence.ts`). `mesh_record` carries a
37
+ * `ProjectedScalars` payload restricted to the SAME allow-list keys
38
+ * `seqscribe/mesh-event-projection.ts` already reviews and ships
39
+ * (`PROJECTED_PAYLOAD_KEYS`, 35 keys) — this file pins a STRUCTURAL COPY of
40
+ * that key list (`MESH_RECORD_PAYLOAD_KEYS`) so mesh-shared does not import
41
+ * daemon-core (leaf constraint), with a test asserting the two lists agree.
42
+ * Summaries, report bodies, progress notes and modal text never travel in any
43
+ * of these payloads — they go to the content-class `mesh.<id>.handoff` topic
44
+ * and are referenced by `SummaryRef {topic, writer, seq}` (already declared
45
+ * in `./turn-evidence.ts`).
46
+ *
47
+ * VERSIONING
48
+ * -----------
49
+ * Every request carries `v: 1` so a future incompatible change can be
50
+ * detected by the responder before it tries to interpret the args.
51
+ */
52
+
53
+ import {
54
+ isEvidenceIdentifier,
55
+ isTurnEvidence,
56
+ isTurnReason,
57
+ type ProjectedScalars,
58
+ type SummaryRef,
59
+ type TurnAttemptRef,
60
+ type TurnEvidence,
61
+ type TurnEvidenceKind,
62
+ type TurnOutcome,
63
+ type TurnReason,
64
+ } from './turn-evidence'
65
+ import { isRecord } from './protocol/envelope'
66
+
67
+ // ─── shared primitives ─────────────────────────────────────────────────────
68
+
69
+ /** Every request in this file is versioned so a responder can reject a shape it predates. */
70
+ export const TURN_IPC_PROTOCOL_VERSION = 1 as const
71
+
72
+ function isFiniteNumber(value: unknown): value is number {
73
+ return typeof value === 'number' && Number.isFinite(value)
74
+ }
75
+
76
+ function isNonNegativeInt(value: unknown): value is number {
77
+ return Number.isSafeInteger(value) && (value as number) >= 0
78
+ }
79
+
80
+ function isOptionalId(value: unknown): boolean {
81
+ return value === undefined || isEvidenceIdentifier(value)
82
+ }
83
+
84
+ function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
85
+ for (const key of Object.keys(value)) {
86
+ if (!allowed.includes(key)) return false
87
+ }
88
+ return true
89
+ }
90
+
91
+ // ─── error codes ────────────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Closed error-code union every command response may carry instead of (or in
95
+ * addition to, for the client-side wrapper) a result.
96
+ *
97
+ * daemon_required — no daemon reachable (no IPC/local transport
98
+ * connection could be established). Distinct from
99
+ * every other failure: it means "there is nobody to
100
+ * ask", not "the ask failed."
101
+ * turn_ledger_unavailable — a daemon answered but its turn ledger is not
102
+ * armed yet (mid-boot, mid-migration).
103
+ * ledger_not_owner — the daemon that answered does not own the
104
+ * attempt/mesh addressed by the request (routing
105
+ * mistake, or the owner changed after a reclaim).
106
+ */
107
+ export const TURN_IPC_ERROR_CODES = ['daemon_required', 'turn_ledger_unavailable', 'ledger_not_owner'] as const
108
+ export type TurnIpcErrorCode = typeof TURN_IPC_ERROR_CODES[number]
109
+
110
+ function makeGuard<T extends readonly string[]>(values: T): (value: unknown) => value is T[number] {
111
+ const set: ReadonlySet<string> = new Set(values)
112
+ return (value: unknown): value is T[number] => typeof value === 'string' && set.has(value)
113
+ }
114
+
115
+ export const isTurnIpcErrorCode = makeGuard(TURN_IPC_ERROR_CODES)
116
+
117
+ /** Structured failure shape a responder returns instead of a result. Never thrown across the wire — the client maps it to a typed error (see mcp-server/src/ipc/turn-commands.ts). */
118
+ export interface TurnIpcError {
119
+ code: TurnIpcErrorCode
120
+ /** Short machine-oriented detail, e.g. which daemon was expected. Never free text describing content. */
121
+ detail?: string
122
+ }
123
+
124
+ export function isTurnIpcError(value: unknown): value is TurnIpcError {
125
+ return isRecord(value) && hasOnlyKeys(value, ['code', 'detail']) && isTurnIpcErrorCode(value.code)
126
+ && (value.detail === undefined || typeof value.detail === 'string')
127
+ }
128
+
129
+ // ─── turn_observe ───────────────────────────────────────────────────────────
130
+
131
+ /** Reducer verdict, mirrored from the C1 reducer's `reduce()` return shape. */
132
+ export const TURN_OBSERVE_VERDICTS = ['applied', 'recorded', 'rejected'] as const
133
+ export type TurnObserveVerdict = typeof TURN_OBSERVE_VERDICTS[number]
134
+ export const isTurnObserveVerdict = makeGuard(TURN_OBSERVE_VERDICTS)
135
+
136
+ export interface TurnObserveRequest {
137
+ v: typeof TURN_IPC_PROTOCOL_VERSION
138
+ evidence: TurnEvidence
139
+ }
140
+
141
+ export interface TurnObserveResponse {
142
+ verdict: TurnObserveVerdict
143
+ attemptRef: TurnAttemptRef
144
+ /** Present only when the evidence committed the attempt (verdict 'applied' + a terminal-class kind). */
145
+ outcome?: TurnOutcome
146
+ }
147
+
148
+ export function isTurnObserveRequest(value: unknown): value is TurnObserveRequest {
149
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'evidence'])
150
+ && value.v === TURN_IPC_PROTOCOL_VERSION
151
+ && isTurnEvidence(value.evidence)
152
+ }
153
+
154
+ export function decodeTurnObserveRequest(value: unknown): TurnObserveRequest | null {
155
+ return isTurnObserveRequest(value) ? value : null
156
+ }
157
+
158
+ function isTurnAttemptRefValue(value: unknown): value is TurnAttemptRef {
159
+ return isRecord(value) && hasOnlyKeys(value, ['attemptId', 'generation'])
160
+ && isEvidenceIdentifier(value.attemptId) && isNonNegativeInt(value.generation)
161
+ }
162
+
163
+ export function isTurnObserveResponse(value: unknown): value is TurnObserveResponse {
164
+ return isRecord(value) && hasOnlyKeys(value, ['verdict', 'attemptRef', 'outcome'])
165
+ && isTurnObserveVerdict(value.verdict)
166
+ && isTurnAttemptRefValue(value.attemptRef)
167
+ && (value.outcome === undefined || isTurnOutcomeValue(value.outcome))
168
+ }
169
+
170
+ const TURN_OUTCOMES_LOCAL = ['completed', 'failed', 'cancelled'] as const
171
+ function isTurnOutcomeValue(value: unknown): value is TurnOutcome {
172
+ return typeof value === 'string' && (TURN_OUTCOMES_LOCAL as readonly string[]).includes(value)
173
+ }
174
+
175
+ export function decodeTurnObserveResponse(value: unknown): TurnObserveResponse | null {
176
+ return isTurnObserveResponse(value) ? value : null
177
+ }
178
+
179
+ // ─── mesh_record ────────────────────────────────────────────────────────────
180
+
181
+ /**
182
+ * Structural pin of `seqscribe/mesh-event-projection.ts` `PROJECTED_PAYLOAD_KEYS`
183
+ * (daemon-core; this leaf cannot import it). A test in this package's test
184
+ * suite AND a daemon-core test each assert their list equals this one — see
185
+ * `mesh-shared/test/turn-ipc.test.ts` and the C-W6 report's break-once table.
186
+ * Adding a key here is the SAME assertion as adding it there: non-content
187
+ * (identifier, enum, boolean, or counter), never free text.
188
+ */
189
+ export const MESH_RECORD_PAYLOAD_KEYS = [
190
+ // Identifiers
191
+ 'taskId', 'deliveryId', 'attemptId', 'missionId', 'checkpointId', 'promptId', 'providerSessionId', 'targetNoteId',
192
+ // Routing / addressing
193
+ 'nodeId', 'sessionId', 'providerType', 'transport', 'attemptedSessionId', 'holderSessionId',
194
+ // Outcome enums
195
+ 'reason', 'status', 'outcome', 'terminalKind', 'event',
196
+ 'source', 'intentionalStopReason',
197
+ // Booleans
198
+ 'retryable', 'rebound', 'forced', 'fallback', 'membershipRemoved', 'requestedForce', 'removedByRemoteDaemon',
199
+ 'completedViaReady', 'intentional', 'weak',
200
+ // Counters
201
+ 'attempt', 'attemptCount', 'count',
202
+ ] as const
203
+ export type MeshRecordPayloadKey = typeof MESH_RECORD_PAYLOAD_KEYS[number]
204
+
205
+ const MESH_RECORD_PAYLOAD_KEY_SET: ReadonlySet<string> = new Set(MESH_RECORD_PAYLOAD_KEYS)
206
+
207
+ /** Maximum length of any string value in a `mesh_record` payload (mirrors `MAX_PROJECTED_STRING`). */
208
+ export const MAX_MESH_RECORD_STRING = 200
209
+
210
+ export interface MeshRecordRequest {
211
+ v: typeof TURN_IPC_PROTOCOL_VERSION
212
+ meshId: string
213
+ /** The original ledger `kind` string (daemon-core `MeshLedgerKind`; mesh-shared cannot import that union, so it is an identifier here). */
214
+ ledgerKind: string
215
+ nodeId?: string
216
+ sessionId?: string
217
+ taskId?: string
218
+ /** Restricted to `MESH_RECORD_PAYLOAD_KEYS` — an unknown key is a decode failure, not a silently dropped field. */
219
+ payload: ProjectedScalars
220
+ }
221
+
222
+ export interface MeshRecordResponse {
223
+ /** `mesh.<id>.events` append coordinates, for callers that need to reference the entry (e.g. a `ref`'d handoff). */
224
+ eventId: string
225
+ seq: number
226
+ }
227
+
228
+ function isProjectedScalarsValue(value: unknown): value is ProjectedScalars {
229
+ if (!isRecord(value)) return false
230
+ for (const v of Object.values(value)) {
231
+ if (v === null) continue
232
+ if (typeof v === 'boolean' || typeof v === 'number') continue
233
+ if (typeof v === 'string') { if (v.length > MAX_MESH_RECORD_STRING) return false; continue }
234
+ return false
235
+ }
236
+ return true
237
+ }
238
+
239
+ /** Every payload key must be a member of `MESH_RECORD_PAYLOAD_KEYS` — the allow-list boundary at the wire. */
240
+ export function isMeshRecordPayload(value: unknown): value is ProjectedScalars {
241
+ if (!isProjectedScalarsValue(value)) return false
242
+ for (const key of Object.keys(value as Record<string, unknown>)) {
243
+ if (!MESH_RECORD_PAYLOAD_KEY_SET.has(key)) return false
244
+ }
245
+ return true
246
+ }
247
+
248
+ export function isMeshRecordRequest(value: unknown): value is MeshRecordRequest {
249
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'ledgerKind', 'nodeId', 'sessionId', 'taskId', 'payload'])
250
+ && value.v === TURN_IPC_PROTOCOL_VERSION
251
+ && isEvidenceIdentifier(value.meshId)
252
+ && isEvidenceIdentifier(value.ledgerKind)
253
+ && isOptionalId(value.nodeId) && isOptionalId(value.sessionId) && isOptionalId(value.taskId)
254
+ && isMeshRecordPayload(value.payload)
255
+ }
256
+
257
+ export function decodeMeshRecordRequest(value: unknown): MeshRecordRequest | null {
258
+ return isMeshRecordRequest(value) ? value : null
259
+ }
260
+
261
+ export function isMeshRecordResponse(value: unknown): value is MeshRecordResponse {
262
+ return isRecord(value) && hasOnlyKeys(value, ['eventId', 'seq'])
263
+ && isEvidenceIdentifier(value.eventId) && isNonNegativeInt(value.seq)
264
+ }
265
+
266
+ export function decodeMeshRecordResponse(value: unknown): MeshRecordResponse | null {
267
+ return isMeshRecordResponse(value) ? value : null
268
+ }
269
+
270
+ // ─── turn_cancel ────────────────────────────────────────────────────────────
271
+
272
+ export interface TurnCancelRequest {
273
+ v: typeof TURN_IPC_PROTOCOL_VERSION
274
+ /** Exactly one of the two must be present — the decoder rejects both-or-neither. */
275
+ attemptId?: string
276
+ taskId?: string
277
+ reason: TurnReason
278
+ }
279
+
280
+ export interface TurnCancelResponse {
281
+ attemptRef: TurnAttemptRef
282
+ verdict: TurnObserveVerdict
283
+ }
284
+
285
+ export function isTurnCancelRequest(value: unknown): value is TurnCancelRequest {
286
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'attemptId', 'taskId', 'reason'])) return false
287
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
288
+ const hasAttempt = value.attemptId !== undefined
289
+ const hasTask = value.taskId !== undefined
290
+ if (hasAttempt === hasTask) return false // exactly one of the two
291
+ if (hasAttempt && !isEvidenceIdentifier(value.attemptId)) return false
292
+ if (hasTask && !isEvidenceIdentifier(value.taskId)) return false
293
+ return isTurnReason(value.reason)
294
+ }
295
+
296
+ export function decodeTurnCancelRequest(value: unknown): TurnCancelRequest | null {
297
+ return isTurnCancelRequest(value) ? value : null
298
+ }
299
+
300
+ export function isTurnCancelResponse(value: unknown): value is TurnCancelResponse {
301
+ return isRecord(value) && hasOnlyKeys(value, ['attemptRef', 'verdict'])
302
+ && isTurnAttemptRefValue(value.attemptRef) && isTurnObserveVerdict(value.verdict)
303
+ }
304
+
305
+ export function decodeTurnCancelResponse(value: unknown): TurnCancelResponse | null {
306
+ return isTurnCancelResponse(value) ? value : null
307
+ }
308
+
309
+ // ─── operator_status ────────────────────────────────────────────────────────
310
+
311
+ /**
312
+ * Fire-and-forget operator/tool-call status record — replaces
313
+ * `recordMeshCoordinatorToolCall` (10 sites, `mesh_tool_call_log`) and doubles
314
+ * as the `operator_status` evidence kind's wire shape.
315
+ *
316
+ * NAMING NOTE (flagged for the C-W6 implementer, see report §"mismatch"):
317
+ * the C-W6 brief's DELIVERABLES describes this request as
318
+ * `{taskId, status: MeshTaskStatus, reason}`, but C1's `operator_status`
319
+ * evidence kind (`turn-evidence.ts` `OPERATOR_STATUSES`) is the closed
320
+ * 2-value set `'completed' | 'failed'` — a verdict on an operator ACTION, not
321
+ * a `MeshTaskStatus` (5-value queue-row status: pending/assigned/completed/
322
+ * failed/cancelled). This contract follows `turn-evidence.ts` because that is
323
+ * what the C1 reducer actually consumes for this evidence kind; using the
324
+ * 5-value `MeshTaskStatus` here would let this request carry `pending` or
325
+ * `assigned`, which the reducer has no transition for.
326
+ */
327
+ export const OPERATOR_STATUS_VALUES = ['completed', 'failed'] as const
328
+ export type OperatorStatusValue = typeof OPERATOR_STATUS_VALUES[number]
329
+ export const isOperatorStatusValue = makeGuard(OPERATOR_STATUS_VALUES)
330
+
331
+ export const OPERATOR_STATUS_REASON_VALUES = ['operator_update', 'refine_terminal', 'validation_terminal'] as const
332
+ export type OperatorStatusReasonValue = typeof OPERATOR_STATUS_REASON_VALUES[number]
333
+ export const isOperatorStatusReasonValue = makeGuard(OPERATOR_STATUS_REASON_VALUES)
334
+
335
+ export interface OperatorStatusRequest {
336
+ v: typeof TURN_IPC_PROTOCOL_VERSION
337
+ taskId: string
338
+ status: OperatorStatusValue
339
+ reason: OperatorStatusReasonValue
340
+ }
341
+
342
+ /** Acknowledgement only — this command is fire-and-forget by design (the daemon owns rate-limiting the underlying log). */
343
+ export interface OperatorStatusResponse {
344
+ accepted: true
345
+ }
346
+
347
+ export function isOperatorStatusRequest(value: unknown): value is OperatorStatusRequest {
348
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'taskId', 'status', 'reason'])
349
+ && value.v === TURN_IPC_PROTOCOL_VERSION
350
+ && isEvidenceIdentifier(value.taskId)
351
+ && isOperatorStatusValue(value.status)
352
+ && isOperatorStatusReasonValue(value.reason)
353
+ }
354
+
355
+ export function decodeOperatorStatusRequest(value: unknown): OperatorStatusRequest | null {
356
+ return isOperatorStatusRequest(value) ? value : null
357
+ }
358
+
359
+ export function isOperatorStatusResponse(value: unknown): value is OperatorStatusResponse {
360
+ return isRecord(value) && hasOnlyKeys(value, ['accepted']) && value.accepted === true
361
+ }
362
+
363
+ export function decodeOperatorStatusResponse(value: unknown): OperatorStatusResponse | null {
364
+ return isOperatorStatusResponse(value) ? value : null
365
+ }
366
+
367
+ // ─── turn_query ─────────────────────────────────────────────────────────────
368
+
369
+ /** Row shape returned by `turn_query` — scalar projection of a `turn_attempts` row (C3 schema), never `data_json`/`payload_json` content. */
370
+ export interface TurnQueryAttemptRow {
371
+ attemptId: string
372
+ generation: number
373
+ meshId?: string
374
+ taskId?: string
375
+ sessionId: string
376
+ nodeId?: string
377
+ providerType?: string
378
+ state: string
379
+ terminalOutcome?: TurnOutcome
380
+ terminalReason?: TurnReason
381
+ acceptedAt: number
382
+ terminalAt?: number
383
+ }
384
+
385
+ /** Row shape returned by `turn_query` for the event stream — scalar projection of a `turn_events` row, `payload_json` NEVER included. */
386
+ export interface TurnQueryEventRow {
387
+ eventId: string
388
+ attemptId?: string
389
+ generation?: number
390
+ sessionId: string
391
+ kind: TurnEvidenceKind
392
+ source: string
393
+ verdict: TurnObserveVerdict | 'forwarded'
394
+ atMs: number
395
+ }
396
+
397
+ export interface TurnQueryRequest {
398
+ v: typeof TURN_IPC_PROTOCOL_VERSION
399
+ meshId: string
400
+ taskId?: string
401
+ attemptId?: string
402
+ sessionId?: string
403
+ state?: string
404
+ since?: number
405
+ tail?: number
406
+ }
407
+
408
+ export interface TurnQueryResponse {
409
+ attempts: readonly TurnQueryAttemptRow[]
410
+ events: readonly TurnQueryEventRow[]
411
+ /** C7-5: set while `mesh.<id>.events` replication is behind for this mesh — the caller must not treat a fleet-scope answer as complete. */
412
+ replicationPending?: boolean
413
+ }
414
+
415
+ export function isTurnQueryRequest(value: unknown): value is TurnQueryRequest {
416
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'attemptId', 'sessionId', 'state', 'since', 'tail'])
417
+ && value.v === TURN_IPC_PROTOCOL_VERSION
418
+ && isEvidenceIdentifier(value.meshId)
419
+ && isOptionalId(value.taskId) && isOptionalId(value.attemptId) && isOptionalId(value.sessionId)
420
+ && (value.state === undefined || isEvidenceIdentifier(value.state))
421
+ && (value.since === undefined || isFiniteNumber(value.since))
422
+ && (value.tail === undefined || isNonNegativeInt(value.tail))
423
+ }
424
+
425
+ export function decodeTurnQueryRequest(value: unknown): TurnQueryRequest | null {
426
+ return isTurnQueryRequest(value) ? value : null
427
+ }
428
+
429
+ const TURN_QUERY_ATTEMPT_ROW_KEYS = [
430
+ 'attemptId', 'generation', 'meshId', 'taskId', 'sessionId', 'nodeId', 'providerType',
431
+ 'state', 'terminalOutcome', 'terminalReason', 'acceptedAt', 'terminalAt',
432
+ ] as const
433
+
434
+ function isTurnQueryAttemptRow(value: unknown): value is TurnQueryAttemptRow {
435
+ if (!isRecord(value) || !hasOnlyKeys(value, TURN_QUERY_ATTEMPT_ROW_KEYS)) return false
436
+ if (!isEvidenceIdentifier(value.attemptId) || !isNonNegativeInt(value.generation)) return false
437
+ if (!isOptionalId(value.meshId) || !isOptionalId(value.taskId)) return false
438
+ if (!isEvidenceIdentifier(value.sessionId)) return false
439
+ if (!isOptionalId(value.nodeId) || !isOptionalId(value.providerType)) return false
440
+ if (typeof value.state !== 'string') return false
441
+ if (value.terminalOutcome !== undefined && !isTurnOutcomeValue(value.terminalOutcome)) return false
442
+ if (value.terminalReason !== undefined && !isTurnReason(value.terminalReason)) return false
443
+ if (!isFiniteNumber(value.acceptedAt)) return false
444
+ if (value.terminalAt !== undefined && !isFiniteNumber(value.terminalAt)) return false
445
+ return true
446
+ }
447
+
448
+ const TURN_QUERY_EVENT_VERDICTS = ['applied', 'recorded', 'rejected', 'forwarded'] as const
449
+ const TURN_QUERY_EVENT_ROW_KEYS = ['eventId', 'attemptId', 'generation', 'sessionId', 'kind', 'source', 'verdict', 'atMs'] as const
450
+
451
+ function isTurnQueryEventRow(value: unknown): value is TurnQueryEventRow {
452
+ if (!isRecord(value) || !hasOnlyKeys(value, TURN_QUERY_EVENT_ROW_KEYS)) return false
453
+ if (!isEvidenceIdentifier(value.eventId)) return false
454
+ if (!isOptionalId(value.attemptId)) return false
455
+ if (value.generation !== undefined && !isNonNegativeInt(value.generation)) return false
456
+ if (!isEvidenceIdentifier(value.sessionId)) return false
457
+ if (typeof value.kind !== 'string') return false
458
+ if (typeof value.source !== 'string') return false
459
+ if (!(TURN_QUERY_EVENT_VERDICTS as readonly string[]).includes(value.verdict as string)) return false
460
+ if (!isFiniteNumber(value.atMs)) return false
461
+ return true
462
+ }
463
+
464
+ export function isTurnQueryResponse(value: unknown): value is TurnQueryResponse {
465
+ if (!isRecord(value) || !hasOnlyKeys(value, ['attempts', 'events', 'replicationPending'])) return false
466
+ if (!Array.isArray(value.attempts) || !value.attempts.every(isTurnQueryAttemptRow)) return false
467
+ if (!Array.isArray(value.events) || !value.events.every(isTurnQueryEventRow)) return false
468
+ if (value.replicationPending !== undefined && typeof value.replicationPending !== 'boolean') return false
469
+ return true
470
+ }
471
+
472
+ export function decodeTurnQueryResponse(value: unknown): TurnQueryResponse | null {
473
+ return isTurnQueryResponse(value) ? value : null
474
+ }
475
+
476
+ // ─── mesh_index_query ───────────────────────────────────────────────────────
477
+
478
+ export const MESH_INDEX_QUERY_WRITER_SCOPES = ['own', 'fleet'] as const
479
+ export type MeshIndexQueryWriterScope = typeof MESH_INDEX_QUERY_WRITER_SCOPES[number]
480
+ export const isMeshIndexQueryWriterScope = makeGuard(MESH_INDEX_QUERY_WRITER_SCOPES)
481
+
482
+ export interface MeshIndexQueryRequest {
483
+ v: typeof TURN_IPC_PROTOCOL_VERSION
484
+ meshId: string
485
+ ledgerKind?: string
486
+ sessionId?: string
487
+ taskId?: string
488
+ writer?: MeshIndexQueryWriterScope
489
+ since?: number
490
+ tail?: number
491
+ }
492
+
493
+ /** Row shape returned by `mesh_index_query` — a `mesh_topic_index` row (C3 schema). */
494
+ export interface MeshTopicIndexRow {
495
+ writer: string
496
+ seq: number
497
+ meshId: string
498
+ eventId: string
499
+ kind: string
500
+ ledgerKind?: string
501
+ taskId?: string
502
+ sessionId?: string
503
+ nodeId?: string
504
+ atMs: number
505
+ payload: ProjectedScalars
506
+ }
507
+
508
+ export interface MeshIndexQueryResponse {
509
+ rows: readonly MeshTopicIndexRow[]
510
+ /** C7-5: true while `mesh.<id>.events` replication is behind for this mesh. */
511
+ replicationPending?: boolean
512
+ }
513
+
514
+ export function isMeshIndexQueryRequest(value: unknown): value is MeshIndexQueryRequest {
515
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'ledgerKind', 'sessionId', 'taskId', 'writer', 'since', 'tail'])
516
+ && value.v === TURN_IPC_PROTOCOL_VERSION
517
+ && isEvidenceIdentifier(value.meshId)
518
+ && isOptionalId(value.ledgerKind) && isOptionalId(value.sessionId) && isOptionalId(value.taskId)
519
+ && (value.writer === undefined || isMeshIndexQueryWriterScope(value.writer))
520
+ && (value.since === undefined || isFiniteNumber(value.since))
521
+ && (value.tail === undefined || isNonNegativeInt(value.tail))
522
+ }
523
+
524
+ export function decodeMeshIndexQueryRequest(value: unknown): MeshIndexQueryRequest | null {
525
+ return isMeshIndexQueryRequest(value) ? value : null
526
+ }
527
+
528
+ const MESH_TOPIC_INDEX_ROW_KEYS = [
529
+ 'writer', 'seq', 'meshId', 'eventId', 'kind', 'ledgerKind', 'taskId', 'sessionId', 'nodeId', 'atMs', 'payload',
530
+ ] as const
531
+
532
+ function isMeshTopicIndexRow(value: unknown): value is MeshTopicIndexRow {
533
+ if (!isRecord(value) || !hasOnlyKeys(value, MESH_TOPIC_INDEX_ROW_KEYS)) return false
534
+ if (!isEvidenceIdentifier(value.writer) || !isNonNegativeInt(value.seq)) return false
535
+ if (!isEvidenceIdentifier(value.meshId) || !isEvidenceIdentifier(value.eventId)) return false
536
+ if (typeof value.kind !== 'string') return false
537
+ if (!isOptionalId(value.ledgerKind) || !isOptionalId(value.taskId) || !isOptionalId(value.sessionId) || !isOptionalId(value.nodeId)) return false
538
+ if (!isFiniteNumber(value.atMs)) return false
539
+ // Rows replay mesh.record entries, so the payload is held to the SAME
540
+ // allow-list as a mesh_record request — not just "any scalar map" — to
541
+ // stop a fleet-wide read from surfacing a key that was never reviewed.
542
+ if (!isMeshRecordPayload(value.payload)) return false
543
+ return true
544
+ }
545
+
546
+ export function isMeshIndexQueryResponse(value: unknown): value is MeshIndexQueryResponse {
547
+ if (!isRecord(value) || !hasOnlyKeys(value, ['rows', 'replicationPending'])) return false
548
+ if (!Array.isArray(value.rows) || !value.rows.every(isMeshTopicIndexRow)) return false
549
+ if (value.replicationPending !== undefined && typeof value.replicationPending !== 'boolean') return false
550
+ return true
551
+ }
552
+
553
+ export function decodeMeshIndexQueryResponse(value: unknown): MeshIndexQueryResponse | null {
554
+ return isMeshIndexQueryResponse(value) ? value : null
555
+ }
556
+
557
+ // ─── mission_upsert / mission_query ────────────────────────────────────────
558
+ //
559
+ // Decision (2026-09-23, design doc §5 C2 update): missions and graph gates
560
+ // have no home in the original six commands — `upsertMeshMission`/
561
+ // `listMeshMissionsForTool` write/read structured state with a free-text
562
+ // `goal`/`title` that cannot fit `mesh_record`'s scalar `ProjectedScalars`
563
+ // allow-list (§ "MESH_RECORD_PAYLOAD_KEYS" above has no text field, on
564
+ // purpose). Two more commands, not a widened allow-list:
565
+ //
566
+ // - mission_upsert — create/update a mission (mesh-missions.ts
567
+ // `upsertMeshMission`'s shape).
568
+ // - mission_query — list missions for a mesh, optionally filtered by
569
+ // status (`getMeshMissions`'s shape).
570
+ //
571
+ // WHY FREE TEXT IS FINE HERE, UNLIKE mesh_record's payload: this is LOCAL
572
+ // IPC between mcp-server and the daemon that owns the mission table — both
573
+ // processes run on the operator's own machine. The content boundary this
574
+ // program protects is "the SERVER never receives chat content" (CLAUDE.md);
575
+ // IPC between two local processes was never inside that boundary, the same
576
+ // way a direct in-process function call wasn't. Cross-machine mission text
577
+ // (a worker learning about a mission a coordinator defined) travels as a
578
+ // `mesh.<id>.handoff` ref per C10-1's precedent — mission_upsert/query never
579
+ // themselves cross a machine boundary.
580
+
581
+ export const MESH_MISSION_STATUSES = ['active', 'paused', 'completed', 'abandoned'] as const
582
+ export type MeshMissionStatusValue = typeof MESH_MISSION_STATUSES[number]
583
+ export const isMeshMissionStatusValue = makeGuard(MESH_MISSION_STATUSES)
584
+
585
+ export const MESH_MISSION_SOURCES = ['magi', 'coordinator'] as const
586
+ export type MeshMissionSourceValue = typeof MESH_MISSION_SOURCES[number]
587
+ export const isMeshMissionSourceValue = makeGuard(MESH_MISSION_SOURCES)
588
+
589
+ // H2 (mission brief, wiring-unification Phase H — docs/design/2026-09-23-wiring-
590
+ // unification.md §7c): free text, same "local IPC is inside the boundary" rationale
591
+ // as `goal` above (section note). `MissionBriefWire` is a structural mirror of
592
+ // mesh-shared's own `MissionBrief` (mission-brief.ts) rather than importing it —
593
+ // this file's guards are all hand-rolled structural checks, and importing the type
594
+ // only (not the class) would still couple this wire contract to that module's
595
+ // internal shape; a mirror keeps the two independently reviewable, same as every
596
+ // other wire type in this file.
597
+ export interface MissionBriefWire {
598
+ goal: string
599
+ constraints?: readonly string[]
600
+ doneCriteria?: readonly string[]
601
+ handoffNotes?: readonly string[]
602
+ ownedPaths?: readonly string[]
603
+ }
604
+
605
+ function isStringArray(value: unknown): value is readonly string[] {
606
+ return Array.isArray(value) && value.every((v) => typeof v === 'string')
607
+ }
608
+
609
+ function isMissionBriefWire(value: unknown): value is MissionBriefWire {
610
+ if (!isRecord(value) || !hasOnlyKeys(value, ['goal', 'constraints', 'doneCriteria', 'handoffNotes', 'ownedPaths'])) return false
611
+ if (typeof value.goal !== 'string' || value.goal.trim().length === 0) return false
612
+ for (const k of ['constraints', 'doneCriteria', 'handoffNotes', 'ownedPaths'] as const) {
613
+ if (value[k] !== undefined && !isStringArray(value[k])) return false
614
+ }
615
+ return true
616
+ }
617
+
618
+ export interface MissionUpsertRequest {
619
+ v: typeof TURN_IPC_PROTOCOL_VERSION
620
+ meshId: string
621
+ /** Omitted = create a new mission; present = update (must resolve to an existing mission — see mesh-missions.ts MISSION-UPSERT-SILENT-CREATE). */
622
+ id?: string
623
+ title: string
624
+ /** Free text — see file-header note on why this is fine over local IPC. */
625
+ goal?: string
626
+ status?: MeshMissionStatusValue
627
+ source?: MeshMissionSourceValue
628
+ /**
629
+ * H2: `undefined` = do not touch the stored brief. `null` = explicitly clear it.
630
+ * A present object with no usable `goal` is treated by the daemon's own
631
+ * `normalizeMissionBrief` as "no brief" (see mesh-missions.ts upsertMeshMission) —
632
+ * this wire guard only checks SHAPE, not that decision.
633
+ */
634
+ brief?: MissionBriefWire | null
635
+ }
636
+
637
+ export interface MeshMissionRecordWire {
638
+ id: string
639
+ meshId: string
640
+ title: string
641
+ goal: string
642
+ status: MeshMissionStatusValue
643
+ source?: MeshMissionSourceValue
644
+ /** H2: absent = no brief attached to this mission. */
645
+ brief?: MissionBriefWire
646
+ }
647
+
648
+ export interface MissionUpsertResponse {
649
+ mission: MeshMissionRecordWire
650
+ }
651
+
652
+ function isMeshMissionRecordWire(value: unknown): value is MeshMissionRecordWire {
653
+ if (!isRecord(value) || !hasOnlyKeys(value, ['id', 'meshId', 'title', 'goal', 'status', 'source', 'brief'])) return false
654
+ if (!isEvidenceIdentifier(value.id) || !isEvidenceIdentifier(value.meshId)) return false
655
+ if (typeof value.title !== 'string' || typeof value.goal !== 'string') return false
656
+ if (!isMeshMissionStatusValue(value.status)) return false
657
+ if (value.source !== undefined && !isMeshMissionSourceValue(value.source)) return false
658
+ if (value.brief !== undefined && !isMissionBriefWire(value.brief)) return false
659
+ return true
660
+ }
661
+
662
+ export function isMissionUpsertRequest(value: unknown): value is MissionUpsertRequest {
663
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'id', 'title', 'goal', 'status', 'source', 'brief'])) return false
664
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
665
+ if (!isEvidenceIdentifier(value.meshId)) return false
666
+ if (value.id !== undefined && !isEvidenceIdentifier(value.id)) return false
667
+ if (typeof value.title !== 'string' || value.title.trim().length === 0) return false
668
+ if (value.goal !== undefined && typeof value.goal !== 'string') return false
669
+ if (value.status !== undefined && !isMeshMissionStatusValue(value.status)) return false
670
+ if (value.source !== undefined && !isMeshMissionSourceValue(value.source)) return false
671
+ if (value.brief !== undefined && value.brief !== null && !isMissionBriefWire(value.brief)) return false
672
+ return true
673
+ }
674
+
675
+ export function decodeMissionUpsertRequest(value: unknown): MissionUpsertRequest | null {
676
+ return isMissionUpsertRequest(value) ? value : null
677
+ }
678
+
679
+ export function isMissionUpsertResponse(value: unknown): value is MissionUpsertResponse {
680
+ return isRecord(value) && hasOnlyKeys(value, ['mission']) && isMeshMissionRecordWire(value.mission)
681
+ }
682
+
683
+ export function decodeMissionUpsertResponse(value: unknown): MissionUpsertResponse | null {
684
+ return isMissionUpsertResponse(value) ? value : null
685
+ }
686
+
687
+ export interface MissionQueryRequest {
688
+ v: typeof TURN_IPC_PROTOCOL_VERSION
689
+ meshId: string
690
+ /** Omitted = every status. */
691
+ statuses?: readonly MeshMissionStatusValue[]
692
+ /** Single-mission lookup (mesh-missions.ts `getMeshMission`'s shape) — mutually exclusive with `statuses`. */
693
+ id?: string
694
+ }
695
+
696
+ export interface MissionQueryResponse {
697
+ missions: readonly MeshMissionRecordWire[]
698
+ }
699
+
700
+ export function isMissionQueryRequest(value: unknown): value is MissionQueryRequest {
701
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'id'])) return false
702
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
703
+ if (!isEvidenceIdentifier(value.meshId)) return false
704
+ if (value.id !== undefined && !isEvidenceIdentifier(value.id)) return false
705
+ if (value.statuses !== undefined) {
706
+ if (!Array.isArray(value.statuses) || value.statuses.length === 0) return false
707
+ if (!value.statuses.every(isMeshMissionStatusValue)) return false
708
+ }
709
+ return true
710
+ }
711
+
712
+ export function decodeMissionQueryRequest(value: unknown): MissionQueryRequest | null {
713
+ return isMissionQueryRequest(value) ? value : null
714
+ }
715
+
716
+ export function isMissionQueryResponse(value: unknown): value is MissionQueryResponse {
717
+ return isRecord(value) && hasOnlyKeys(value, ['missions'])
718
+ && Array.isArray(value.missions) && value.missions.every(isMeshMissionRecordWire)
719
+ }
720
+
721
+ export function decodeMissionQueryResponse(value: unknown): MissionQueryResponse | null {
722
+ return isMissionQueryResponse(value) ? value : null
723
+ }
724
+
725
+ // ─── mission_list_query ─────────────────────────────────────────────────────
726
+ //
727
+ // Wiring-unification Phase C, workstream C-W9b.
728
+ //
729
+ // `mesh-tools-mission.ts`'s own file-header note (2026-09-24, C-W6) flagged
730
+ // this exact gap: `mission_query`'s landed response is `{missions:
731
+ // MeshMissionRecordWire[]}` only — no `verbose`/`includeMagi`/`withStats`/
732
+ // `limit`/`truncated`/`overflowIds`/`historyFold`, all of which
733
+ // `listMeshMissionsForTool` (daemon-core `mesh-missions.ts`) computes for the
734
+ // `mesh_mission_list` tool. Rather than widen `mission_query` itself (an
735
+ // already-landed, already-tested contract other callers rely on for its
736
+ // narrow shape), this is an ADDITIVE sibling command carrying
737
+ // `listMeshMissionsForTool`'s full output shape — request/response fields
738
+ // mirror its options/`MeshMissionListResult` one-for-one (see
739
+ // mesh-missions.ts). `goal`/`goalPreview` are free text, same local-IPC
740
+ // rationale as `mission_upsert`'s `goal` above.
741
+
742
+ export interface MissionListQueryRequest {
743
+ v: typeof TURN_IPC_PROTOCOL_VERSION
744
+ meshId: string
745
+ statuses?: readonly MeshMissionStatusValue[]
746
+ verbose?: boolean
747
+ includeMagi?: boolean
748
+ withStats?: boolean
749
+ limit?: number
750
+ historyIdLimit?: number
751
+ }
752
+
753
+ export interface MeshMissionTaskAggregateWire {
754
+ total: number
755
+ pending: number
756
+ assigned: number
757
+ completed: number
758
+ failed: number
759
+ cancelled: number
760
+ blocked: number
761
+ lastActivityAt: string | null
762
+ }
763
+
764
+ export interface MeshMissionStatsWire {
765
+ missionId: string
766
+ taskCount: number
767
+ completed: number
768
+ failed: number
769
+ totalDurationMs: number
770
+ wallClockMs: number | null
771
+ retries: number
772
+ incompleteTaskIds: readonly string[]
773
+ }
774
+
775
+ /** Verbose (full-goal) mission row. `goal` present, `goalPreview`/`goalTruncated` absent. */
776
+ export interface MissionListSummaryVerboseWire {
777
+ id: string
778
+ meshId: string
779
+ title: string
780
+ goal: string
781
+ status: MeshMissionStatusValue
782
+ source?: MeshMissionSourceValue
783
+ tasks: MeshMissionTaskAggregateWire
784
+ stats?: MeshMissionStatsWire
785
+ /** H2: absent = no brief attached. */
786
+ brief?: MissionBriefWire
787
+ }
788
+
789
+ /** Compact (default) mission row. `goalPreview`/`goalTruncated` present, `goal` absent. */
790
+ export interface MissionListSummarySlimWire {
791
+ id: string
792
+ meshId: string
793
+ title: string
794
+ goalPreview: string
795
+ goalTruncated: boolean
796
+ status: MeshMissionStatusValue
797
+ source?: MeshMissionSourceValue
798
+ tasks: MeshMissionTaskAggregateWire
799
+ stats?: MeshMissionStatsWire
800
+ /** H2: absent = no brief attached. */
801
+ brief?: MissionBriefWire
802
+ }
803
+
804
+ export type MissionListSummaryWire = MissionListSummaryVerboseWire | MissionListSummarySlimWire
805
+
806
+ export interface MeshMissionHistoryFoldWire {
807
+ count: number
808
+ byStatus: Record<string, number>
809
+ missionIds: readonly string[]
810
+ note: string
811
+ }
812
+
813
+ export interface MissionListQueryResponse {
814
+ missions: readonly MissionListSummaryWire[]
815
+ historyFold: MeshMissionHistoryFoldWire | null
816
+ truncated: boolean
817
+ matched: number
818
+ overflowIds?: readonly string[]
819
+ }
820
+
821
+ export function isMissionListQueryRequest(value: unknown): value is MissionListQueryRequest {
822
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'verbose', 'includeMagi', 'withStats', 'limit', 'historyIdLimit'])) return false
823
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
824
+ if (!isEvidenceIdentifier(value.meshId)) return false
825
+ if (value.statuses !== undefined) {
826
+ if (!Array.isArray(value.statuses) || value.statuses.length === 0) return false
827
+ if (!value.statuses.every(isMeshMissionStatusValue)) return false
828
+ }
829
+ if (value.verbose !== undefined && typeof value.verbose !== 'boolean') return false
830
+ if (value.includeMagi !== undefined && typeof value.includeMagi !== 'boolean') return false
831
+ if (value.withStats !== undefined && typeof value.withStats !== 'boolean') return false
832
+ if (value.limit !== undefined && !isNonNegativeInt(value.limit)) return false
833
+ if (value.historyIdLimit !== undefined && !isNonNegativeInt(value.historyIdLimit)) return false
834
+ return true
835
+ }
836
+
837
+ export function decodeMissionListQueryRequest(value: unknown): MissionListQueryRequest | null {
838
+ return isMissionListQueryRequest(value) ? value : null
839
+ }
840
+
841
+ function isMeshMissionTaskAggregateWire(value: unknown): value is MeshMissionTaskAggregateWire {
842
+ const keys = ['total', 'pending', 'assigned', 'completed', 'failed', 'cancelled', 'blocked', 'lastActivityAt'] as const
843
+ if (!isRecord(value) || !hasOnlyKeys(value, keys)) return false
844
+ for (const k of ['total', 'pending', 'assigned', 'completed', 'failed', 'cancelled', 'blocked'] as const) {
845
+ if (!isNonNegativeInt(value[k])) return false
846
+ }
847
+ return value.lastActivityAt === null || typeof value.lastActivityAt === 'string'
848
+ }
849
+
850
+ function isMeshMissionStatsWire(value: unknown): value is MeshMissionStatsWire {
851
+ const keys = ['missionId', 'taskCount', 'completed', 'failed', 'totalDurationMs', 'wallClockMs', 'retries', 'incompleteTaskIds'] as const
852
+ if (!isRecord(value) || !hasOnlyKeys(value, keys)) return false
853
+ if (!isEvidenceIdentifier(value.missionId)) return false
854
+ for (const k of ['taskCount', 'completed', 'failed', 'totalDurationMs', 'retries'] as const) {
855
+ if (!isNonNegativeInt(value[k])) return false
856
+ }
857
+ if (value.wallClockMs !== null && !isNonNegativeInt(value.wallClockMs)) return false
858
+ if (!Array.isArray(value.incompleteTaskIds) || !value.incompleteTaskIds.every(isEvidenceIdentifier)) return false
859
+ return true
860
+ }
861
+
862
+ function isMissionListSummaryWire(value: unknown): value is MissionListSummaryWire {
863
+ if (!isRecord(value)) return false
864
+ const hasGoal = 'goal' in value
865
+ const hasPreview = 'goalPreview' in value && 'goalTruncated' in value
866
+ if (hasGoal === hasPreview) return false // exactly one of the two shapes
867
+ const baseKeys = ['id', 'meshId', 'title', 'status', 'source', 'tasks', 'stats', 'brief'] as const
868
+ const allowed = hasGoal ? [...baseKeys, 'goal'] : [...baseKeys, 'goalPreview', 'goalTruncated']
869
+ if (!hasOnlyKeys(value, allowed)) return false
870
+ if (!isEvidenceIdentifier(value.id) || !isEvidenceIdentifier(value.meshId)) return false
871
+ if (typeof value.title !== 'string') return false
872
+ if (!isMeshMissionStatusValue(value.status)) return false
873
+ if (value.source !== undefined && !isMeshMissionSourceValue(value.source)) return false
874
+ if (!isMeshMissionTaskAggregateWire(value.tasks)) return false
875
+ if (value.stats !== undefined && !isMeshMissionStatsWire(value.stats)) return false
876
+ if (value.brief !== undefined && !isMissionBriefWire(value.brief)) return false
877
+ if (hasGoal && typeof value.goal !== 'string') return false
878
+ if (hasPreview && (typeof value.goalPreview !== 'string' || typeof value.goalTruncated !== 'boolean')) return false
879
+ return true
880
+ }
881
+
882
+ function isMeshMissionHistoryFoldWire(value: unknown): value is MeshMissionHistoryFoldWire {
883
+ if (!isRecord(value) || !hasOnlyKeys(value, ['count', 'byStatus', 'missionIds', 'note'])) return false
884
+ if (!isNonNegativeInt(value.count)) return false
885
+ if (!isRecord(value.byStatus) || !Object.values(value.byStatus).every((n) => isNonNegativeInt(n))) return false
886
+ if (!Array.isArray(value.missionIds) || !value.missionIds.every(isEvidenceIdentifier)) return false
887
+ return typeof value.note === 'string'
888
+ }
889
+
890
+ export function isMissionListQueryResponse(value: unknown): value is MissionListQueryResponse {
891
+ if (!isRecord(value) || !hasOnlyKeys(value, ['missions', 'historyFold', 'truncated', 'matched', 'overflowIds'])) return false
892
+ if (!Array.isArray(value.missions) || !value.missions.every(isMissionListSummaryWire)) return false
893
+ if (value.historyFold !== null && !isMeshMissionHistoryFoldWire(value.historyFold)) return false
894
+ if (typeof value.truncated !== 'boolean') return false
895
+ if (!isNonNegativeInt(value.matched)) return false
896
+ if (value.overflowIds !== undefined && (!Array.isArray(value.overflowIds) || !value.overflowIds.every(isEvidenceIdentifier))) return false
897
+ return true
898
+ }
899
+
900
+ export function decodeMissionListQueryResponse(value: unknown): MissionListQueryResponse | null {
901
+ return isMissionListQueryResponse(value) ? value : null
902
+ }
903
+
904
+ // ─── note_upsert / note_forget ──────────────────────────────────────────────
905
+ //
906
+ // Decision (2026-09-24, C-W6b; implemented C-W8): coordinator operating notes
907
+ // get their own two local-IPC commands. A note's text is coordinator-authored
908
+ // free text, so — exactly like mission_upsert's `goal` — it cannot ride
909
+ // `mesh_record`'s scalar allow-list, and it never needs to: notes live in the
910
+ // owning daemon's `mesh_operating_notes` table (local-only; never on
911
+ // `mesh.<id>.events`). The same "local IPC is inside the boundary" rationale as
912
+ // the mission commands above applies.
913
+
914
+ export const OPERATING_NOTE_CATEGORIES = ['provider_quirk', 'pattern_to_avoid', 'recovery_lesson'] as const
915
+ export type OperatingNoteCategory = typeof OPERATING_NOTE_CATEGORIES[number]
916
+ export const isOperatingNoteCategory = makeGuard(OPERATING_NOTE_CATEGORIES)
917
+
918
+ export interface NoteUpsertRequest {
919
+ v: typeof TURN_IPC_PROTOCOL_VERSION
920
+ meshId: string
921
+ /** Free text (local IPC — see the section note). */
922
+ text: string
923
+ category?: OperatingNoteCategory
924
+ pinned?: boolean
925
+ /** Explicit expiry, ISO-8601. */
926
+ expiresAt?: string
927
+ /** Note id (or subject key) this note supersedes. */
928
+ supersedes?: string
929
+ subjectKey?: string
930
+ /** Best-effort identity of the recording coordinator (session id / daemon id / host). */
931
+ sourceCoordinator?: string
932
+ }
933
+
934
+ export interface NoteUpsertResponse {
935
+ noteId: string
936
+ /** True when the same text was already among the recent live notes (no new note). */
937
+ deduped: boolean
938
+ createdAt: string
939
+ }
940
+
941
+ function isOptionalShortString(value: unknown, max = 512): boolean {
942
+ return value === undefined || (typeof value === 'string' && value.trim().length > 0 && value.length <= max)
943
+ }
944
+
945
+ export function isNoteUpsertRequest(value: unknown): value is NoteUpsertRequest {
946
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'text', 'category', 'pinned', 'expiresAt', 'supersedes', 'subjectKey', 'sourceCoordinator'])) return false
947
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
948
+ if (!isEvidenceIdentifier(value.meshId)) return false
949
+ if (typeof value.text !== 'string' || value.text.trim().length === 0) return false
950
+ if (value.category !== undefined && !isOperatingNoteCategory(value.category)) return false
951
+ if (value.pinned !== undefined && typeof value.pinned !== 'boolean') return false
952
+ if (value.expiresAt !== undefined && (typeof value.expiresAt !== 'string' || Number.isNaN(Date.parse(value.expiresAt)))) return false
953
+ return isOptionalShortString(value.supersedes) && isOptionalShortString(value.subjectKey) && isOptionalShortString(value.sourceCoordinator)
954
+ }
955
+
956
+ export function decodeNoteUpsertRequest(value: unknown): NoteUpsertRequest | null {
957
+ return isNoteUpsertRequest(value) ? value : null
958
+ }
959
+
960
+ export function isNoteUpsertResponse(value: unknown): value is NoteUpsertResponse {
961
+ return isRecord(value) && hasOnlyKeys(value, ['noteId', 'deduped', 'createdAt'])
962
+ && isEvidenceIdentifier(value.noteId) && typeof value.deduped === 'boolean' && typeof value.createdAt === 'string'
963
+ }
964
+
965
+ export function decodeNoteUpsertResponse(value: unknown): NoteUpsertResponse | null {
966
+ return isNoteUpsertResponse(value) ? value : null
967
+ }
968
+
969
+ export interface NoteForgetRequest {
970
+ v: typeof TURN_IPC_PROTOCOL_VERSION
971
+ meshId: string
972
+ /** At least one of `noteId` / `text` (a text forget retracts every note with that exact text, and any recorded later). */
973
+ noteId?: string
974
+ text?: string
975
+ reason?: string
976
+ }
977
+
978
+ export interface NoteForgetResponse {
979
+ /** Live notes this forget hid. */
980
+ matched: number
981
+ tombstoneId: string
982
+ }
983
+
984
+ export function isNoteForgetRequest(value: unknown): value is NoteForgetRequest {
985
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'noteId', 'text', 'reason'])) return false
986
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
987
+ if (!isEvidenceIdentifier(value.meshId)) return false
988
+ if (value.noteId !== undefined && !isEvidenceIdentifier(value.noteId)) return false
989
+ if (value.text !== undefined && (typeof value.text !== 'string' || value.text.trim().length === 0)) return false
990
+ if (value.noteId === undefined && value.text === undefined) return false
991
+ return value.reason === undefined || typeof value.reason === 'string'
992
+ }
993
+
994
+ export function decodeNoteForgetRequest(value: unknown): NoteForgetRequest | null {
995
+ return isNoteForgetRequest(value) ? value : null
996
+ }
997
+
998
+ export function isNoteForgetResponse(value: unknown): value is NoteForgetResponse {
999
+ return isRecord(value) && hasOnlyKeys(value, ['matched', 'tombstoneId'])
1000
+ && typeof value.matched === 'number' && Number.isInteger(value.matched) && value.matched >= 0
1001
+ && isEvidenceIdentifier(value.tombstoneId)
1002
+ }
1003
+
1004
+ export function decodeNoteForgetResponse(value: unknown): NoteForgetResponse | null {
1005
+ return isNoteForgetResponse(value) ? value : null
1006
+ }
1007
+
1008
+ // ─── tool_call_record ───────────────────────────────────────────────────────
1009
+ //
1010
+ // Wiring-unification Phase C, workstream C-W9b
1011
+ // (docs/design/2026-09-23-wiring-unification.md §5 C2 "MCP server" paragraph,
1012
+ // 2026-09-24 14:00 stamp "C-W9").
1013
+ //
1014
+ // Replaces `recordMeshToolCall`/`recordMeshCoordinatorToolCall`
1015
+ // (`daemon-core/src/mesh/mesh-direct-dispatch.ts`) — an in-process,
1016
+ // per-mesh-tool-call rate-limit/advisory bump mcp-server called directly
1017
+ // against its own store handle. Every field is an identifier, a closed enum,
1018
+ // a boolean or a counter — squarely inside `mesh_record`'s content boundary,
1019
+ // but this is its own command (not a `mesh_record` payload) because it is a
1020
+ // COUNTER MUTATION with a computed return value (`rateLimitExceeded`,
1021
+ // `callsInWindow`, `advisory`), not an append-only scalar record.
1022
+
1023
+ export const TOOL_CALLER_ROLES = ['coordinator', 'unknown'] as const
1024
+ export type ToolCallerRole = typeof TOOL_CALLER_ROLES[number]
1025
+ export const isToolCallerRole = makeGuard(TOOL_CALLER_ROLES)
1026
+
1027
+ export interface ToolCallRecordRequest {
1028
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1029
+ meshId: string
1030
+ /** The mesh tool name, e.g. `mesh_status` (identifier, not free text — always a fixed tool-name string). */
1031
+ tool: string
1032
+ sessionId?: string
1033
+ callerRole: ToolCallerRole
1034
+ }
1035
+
1036
+ export interface ToolCallRecordResponse {
1037
+ rateLimitExceeded: boolean
1038
+ callsInWindow: number
1039
+ /** Short machine-oriented advisory string, or null. Never free text describing content. */
1040
+ advisory: string | null
1041
+ }
1042
+
1043
+ export function isToolCallRecordRequest(value: unknown): value is ToolCallRecordRequest {
1044
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'tool', 'sessionId', 'callerRole'])
1045
+ && value.v === TURN_IPC_PROTOCOL_VERSION
1046
+ && isEvidenceIdentifier(value.meshId)
1047
+ && isEvidenceIdentifier(value.tool)
1048
+ && isOptionalId(value.sessionId)
1049
+ && isToolCallerRole(value.callerRole)
1050
+ }
1051
+
1052
+ export function decodeToolCallRecordRequest(value: unknown): ToolCallRecordRequest | null {
1053
+ return isToolCallRecordRequest(value) ? value : null
1054
+ }
1055
+
1056
+ export function isToolCallRecordResponse(value: unknown): value is ToolCallRecordResponse {
1057
+ return isRecord(value) && hasOnlyKeys(value, ['rateLimitExceeded', 'callsInWindow', 'advisory'])
1058
+ && typeof value.rateLimitExceeded === 'boolean'
1059
+ && isNonNegativeInt(value.callsInWindow)
1060
+ && (value.advisory === null || typeof value.advisory === 'string')
1061
+ }
1062
+
1063
+ export function decodeToolCallRecordResponse(value: unknown): ToolCallRecordResponse | null {
1064
+ return isToolCallRecordResponse(value) ? value : null
1065
+ }
1066
+
1067
+ // ─── ledger_query ───────────────────────────────────────────────────────────
1068
+ //
1069
+ // Wiring-unification Phase C, workstream C-W9b.
1070
+ //
1071
+ // Replaces the remaining bare `readLedgerEntries(meshId, opts)` /
1072
+ // `getLedgerSummary(meshId)` in-process reads that `turn_query` cannot serve
1073
+ // (§5 C2's `mesh-tools-mission.ts` file-header note, 2026-09-24: "general
1074
+ // ledger browsing by arbitrary `kind`/`since`/`node` filters —
1075
+ // `turn_query`'s shape... has no free-form kind-list or node filter, so this
1076
+ // is a real API mismatch, not a mechanical rename"). `turn_query` stays
1077
+ // scoped to the turn-ledger's own attempt/event rows; `ledger_query` is the
1078
+ // general record browse surface (C-W9a: the daemon's `mesh_local_records` plus
1079
+ // the turn ledger's task outcomes, `readLocalRecords`) `meshTaskHistory` /
1080
+ // `meshLedgerQuery` / the MAGI ledger-scan helpers need.
1081
+ //
1082
+ // CONTENT BOUNDARY: `MeshLedgerEntry.payload` (daemon-core `mesh-ledger.ts`)
1083
+ // is `Record<string, unknown>` — genuinely unbounded JSON (a MAGI synthesis
1084
+ // object, a graph plan, a free-text checkpoint message), NOT constrained to
1085
+ // `mesh_record`'s scalar allow-list. This is fine for the SAME reason
1086
+ // `mission_upsert`'s `goal` and `note_upsert`'s `text` are fine (see that
1087
+ // section's note above): this is local IPC between mcp-server and the
1088
+ // daemon that owns the ledger, both on the operator's own machine — never a
1089
+ // cross-machine or server hop. `entryPayload` below is therefore an
1090
+ // intentionally-unvalidated `Record<string, unknown>` passthrough, exactly
1091
+ // like `MissionUpsertRequest.goal` is unvalidated free text.
1092
+
1093
+ export interface LedgerQueryRequest {
1094
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1095
+ meshId: string
1096
+ /** One or more ledger `kind` strings (daemon-core `MeshLedgerKind`; mesh-shared cannot import that union, so these are identifiers here). Omitted = every kind. */
1097
+ kind?: readonly string[]
1098
+ /** ISO-8601 or epoch-ms-as-string — passed through to `readLedgerEntries`'s `since` (parsed via `new Date()`). */
1099
+ since?: string
1100
+ /** Filter to entries whose nodeId is equivalent to this daemon id (matched via `daemonIdsEquivalent` daemon-side, not raw `===`). */
1101
+ node?: string
1102
+ tail?: number
1103
+ /** Also return `getLedgerSummary(meshId)` alongside the entries — saves a second round trip for callers that want both (the common case: every existing call site fetches both together). */
1104
+ includeSummary?: boolean
1105
+ }
1106
+
1107
+ export interface LedgerQueryEntryWire {
1108
+ id: string
1109
+ meshId: string
1110
+ timestamp: string
1111
+ kind: string
1112
+ nodeId?: string
1113
+ sessionId?: string
1114
+ providerType?: string
1115
+ taskId?: string
1116
+ /** Unbounded JSON passthrough — see file section's CONTENT BOUNDARY note. */
1117
+ payload: Record<string, unknown>
1118
+ }
1119
+
1120
+ export interface LedgerQuerySummaryWire {
1121
+ meshId: string
1122
+ totalEntries: number
1123
+ taskDispatched: number
1124
+ taskCompleted: number
1125
+ taskFailed: number
1126
+ taskStalled: number
1127
+ sessionLaunched: number
1128
+ checkpointCreated: number
1129
+ lastActivityAt: string | null
1130
+ recentFailures: number
1131
+ }
1132
+
1133
+ export interface LedgerQueryResponse {
1134
+ entries: readonly LedgerQueryEntryWire[]
1135
+ summary?: LedgerQuerySummaryWire
1136
+ }
1137
+
1138
+ export function isLedgerQueryRequest(value: unknown): value is LedgerQueryRequest {
1139
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'kind', 'since', 'node', 'tail', 'includeSummary'])) return false
1140
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
1141
+ if (!isEvidenceIdentifier(value.meshId)) return false
1142
+ if (value.kind !== undefined && (!Array.isArray(value.kind) || value.kind.length === 0 || !value.kind.every(isEvidenceIdentifier))) return false
1143
+ if (value.since !== undefined && typeof value.since !== 'string') return false
1144
+ if (!isOptionalId(value.node)) return false
1145
+ if (value.tail !== undefined && !isNonNegativeInt(value.tail)) return false
1146
+ if (value.includeSummary !== undefined && typeof value.includeSummary !== 'boolean') return false
1147
+ return true
1148
+ }
1149
+
1150
+ export function decodeLedgerQueryRequest(value: unknown): LedgerQueryRequest | null {
1151
+ return isLedgerQueryRequest(value) ? value : null
1152
+ }
1153
+
1154
+ const LEDGER_QUERY_ENTRY_KEYS = ['id', 'meshId', 'timestamp', 'kind', 'nodeId', 'sessionId', 'providerType', 'taskId', 'payload'] as const
1155
+
1156
+ function isLedgerQueryEntryWire(value: unknown): value is LedgerQueryEntryWire {
1157
+ if (!isRecord(value) || !hasOnlyKeys(value, LEDGER_QUERY_ENTRY_KEYS)) return false
1158
+ if (!isEvidenceIdentifier(value.id) || !isEvidenceIdentifier(value.meshId)) return false
1159
+ if (typeof value.timestamp !== 'string' || typeof value.kind !== 'string') return false
1160
+ if (!isOptionalId(value.nodeId) || !isOptionalId(value.sessionId) || !isOptionalId(value.providerType) || !isOptionalId(value.taskId)) return false
1161
+ if (!isRecord(value.payload)) return false
1162
+ return true
1163
+ }
1164
+
1165
+ function isLedgerQuerySummaryWire(value: unknown): value is LedgerQuerySummaryWire {
1166
+ const keys = ['meshId', 'totalEntries', 'taskDispatched', 'taskCompleted', 'taskFailed', 'taskStalled', 'sessionLaunched', 'checkpointCreated', 'lastActivityAt', 'recentFailures'] as const
1167
+ if (!isRecord(value) || !hasOnlyKeys(value, keys)) return false
1168
+ if (!isEvidenceIdentifier(value.meshId)) return false
1169
+ for (const k of ['totalEntries', 'taskDispatched', 'taskCompleted', 'taskFailed', 'taskStalled', 'sessionLaunched', 'checkpointCreated', 'recentFailures'] as const) {
1170
+ if (!isNonNegativeInt(value[k])) return false
1171
+ }
1172
+ if (value.lastActivityAt !== null && typeof value.lastActivityAt !== 'string') return false
1173
+ return true
1174
+ }
1175
+
1176
+ export function isLedgerQueryResponse(value: unknown): value is LedgerQueryResponse {
1177
+ if (!isRecord(value) || !hasOnlyKeys(value, ['entries', 'summary'])) return false
1178
+ if (!Array.isArray(value.entries) || !value.entries.every(isLedgerQueryEntryWire)) return false
1179
+ if (value.summary !== undefined && !isLedgerQuerySummaryWire(value.summary)) return false
1180
+ return true
1181
+ }
1182
+
1183
+ export function decodeLedgerQueryResponse(value: unknown): LedgerQueryResponse | null {
1184
+ return isLedgerQueryResponse(value) ? value : null
1185
+ }
1186
+
1187
+ // ─── C-W9a: local records, queue composites, active work ────────────────────
1188
+ //
1189
+ // Wiring-unification Phase C, workstream C-W9a (the 2026-09-24 14:00 stamp
1190
+ // "Left for C-W9"): the event ledger and its JSONL mirror are gone, and every
1191
+ // mcp-server call that still reached the daemon's `mesh-runtime.db` in-process
1192
+ // — record appends, queue mutations, the queue/active-work reads that fed
1193
+ // `buildMeshActiveWork`, recovery hints — moves behind the commands below,
1194
+ // executed in the daemon that owns the store.
1195
+ //
1196
+ // CONTENT BOUNDARY: like `mission_upsert`'s `goal`, `note_upsert`'s `text` and
1197
+ // `ledger_query`'s entry payloads, several fields here are free text or
1198
+ // unbounded JSON (a task message, a MAGI synthesis, a queue row with its
1199
+ // message, an active-work record with its task summary). This is fine for the
1200
+ // same reason: local IPC between the mcp-server and the daemon on the
1201
+ // operator's own machine — never a cross-machine or server hop. Those fields
1202
+ // are documented as passthroughs and validated only for shape (object/array),
1203
+ // exactly like `LedgerQueryEntryWire.payload`. Nothing here is ever published
1204
+ // to a topic: `record_local`'s topic leg is the daemon's `mesh.record`
1205
+ // allow-list projection, which drops free text by construction.
1206
+
1207
+ function isRecordArray(value: unknown): value is readonly Record<string, unknown>[] {
1208
+ return Array.isArray(value) && value.every(isRecord)
1209
+ }
1210
+
1211
+ function isOptionalRecord(value: unknown): boolean {
1212
+ return value === undefined || isRecord(value)
1213
+ }
1214
+
1215
+ function isOptionalBoolean(value: unknown): boolean {
1216
+ return value === undefined || typeof value === 'boolean'
1217
+ }
1218
+
1219
+ function isOptionalString(value: unknown): boolean {
1220
+ return value === undefined || typeof value === 'string'
1221
+ }
1222
+
1223
+ /** A queue row as the daemon's `getQueue` returns it — a JSON passthrough (see the section note). */
1224
+ export type QueueEntryWire = Record<string, unknown> & { id: string; status: string }
1225
+
1226
+ function isQueueEntryWire(value: unknown): value is QueueEntryWire {
1227
+ return isRecord(value) && isEvidenceIdentifier(value.id) && typeof value.status === 'string'
1228
+ }
1229
+
1230
+ // ── record_local ──
1231
+ //
1232
+ // `meshRecord(meshId, kind, scalars, { local: true })` over IPC: the scalar
1233
+ // projection goes to `mesh.<id>.events` (content-free), the FULL payload lands
1234
+ // in the daemon's `mesh_local_records` (local-only). Replaces every mcp-server
1235
+ // `appendLedgerEntry` (dispatch / MAGI question+synthesis / checkpoint message /
1236
+ // reconcile records). `payload` is free-form JSON (see the section note).
1237
+
1238
+ export interface RecordLocalRequest {
1239
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1240
+ meshId: string
1241
+ /** A `MeshLedgerKind` (daemon-core); an identifier here (mesh-shared cannot import that union). */
1242
+ kind: string
1243
+ nodeId?: string
1244
+ sessionId?: string
1245
+ providerType?: string
1246
+ taskId?: string
1247
+ payload: Record<string, unknown>
1248
+ }
1249
+
1250
+ export interface RecordLocalResponse {
1251
+ eventId: string
1252
+ timestamp: string
1253
+ storedLocally: boolean
1254
+ published: boolean
1255
+ }
1256
+
1257
+ export function isRecordLocalRequest(value: unknown): value is RecordLocalRequest {
1258
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'kind', 'nodeId', 'sessionId', 'providerType', 'taskId', 'payload'])
1259
+ && value.v === TURN_IPC_PROTOCOL_VERSION
1260
+ && isEvidenceIdentifier(value.meshId)
1261
+ && isEvidenceIdentifier(value.kind)
1262
+ && isOptionalId(value.nodeId) && isOptionalId(value.sessionId) && isOptionalId(value.providerType) && isOptionalId(value.taskId)
1263
+ && isRecord(value.payload)
1264
+ }
1265
+
1266
+ export function decodeRecordLocalRequest(value: unknown): RecordLocalRequest | null {
1267
+ return isRecordLocalRequest(value) ? value : null
1268
+ }
1269
+
1270
+ export function isRecordLocalResponse(value: unknown): value is RecordLocalResponse {
1271
+ return isRecord(value) && hasOnlyKeys(value, ['eventId', 'timestamp', 'storedLocally', 'published'])
1272
+ && isEvidenceIdentifier(value.eventId) && typeof value.timestamp === 'string'
1273
+ && typeof value.storedLocally === 'boolean' && typeof value.published === 'boolean'
1274
+ }
1275
+
1276
+ export function decodeRecordLocalResponse(value: unknown): RecordLocalResponse | null {
1277
+ return isRecordLocalResponse(value) ? value : null
1278
+ }
1279
+
1280
+ // ── queue_query ──
1281
+
1282
+ export interface QueueQueryRequest {
1283
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1284
+ meshId: string
1285
+ /** Status filter (queue statuses); omitted = every row. */
1286
+ statuses?: readonly string[]
1287
+ /** One row by id (returns 0 or 1 entries). */
1288
+ taskId?: string
1289
+ /** Project for a VIEW surface: the persisted input envelope → `inputSummary` (summarizeQueueEntryInputForView). */
1290
+ view?: boolean
1291
+ }
1292
+
1293
+ export interface QueueQueryResponse {
1294
+ entries: readonly QueueEntryWire[]
1295
+ }
1296
+
1297
+ export function isQueueQueryRequest(value: unknown): value is QueueQueryRequest {
1298
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'taskId', 'view'])) return false
1299
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
1300
+ if (value.statuses !== undefined && (!Array.isArray(value.statuses) || !value.statuses.every(isEvidenceIdentifier))) return false
1301
+ return isOptionalId(value.taskId) && isOptionalBoolean(value.view)
1302
+ }
1303
+
1304
+ export function decodeQueueQueryRequest(value: unknown): QueueQueryRequest | null {
1305
+ return isQueueQueryRequest(value) ? value : null
1306
+ }
1307
+
1308
+ export function isQueueQueryResponse(value: unknown): value is QueueQueryResponse {
1309
+ return isRecord(value) && hasOnlyKeys(value, ['entries']) && Array.isArray(value.entries) && value.entries.every(isQueueEntryWire)
1310
+ }
1311
+
1312
+ export function decodeQueueQueryResponse(value: unknown): QueueQueryResponse | null {
1313
+ return isQueueQueryResponse(value) ? value : null
1314
+ }
1315
+
1316
+ // ── queue_enqueue ──
1317
+ //
1318
+ // `enqueueTask(meshId, message, options)` + (when `decision` is given) the
1319
+ // single-surface `recordSingleEnqueueDecision` for the new task id, in the
1320
+ // daemon. `options` is `MeshEnqueueTaskOptions` (daemon-core) as a JSON
1321
+ // passthrough; the daemon's enqueue guards (message, task-mode, difficulty,
1322
+ // dependency cycle) run unchanged and a refusal comes back as the error.
1323
+
1324
+ export interface QueueEnqueueRequest {
1325
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1326
+ meshId: string
1327
+ /** Free text (the task message — see the section note). */
1328
+ message: string
1329
+ options?: Record<string, unknown>
1330
+ /** `recordSingleEnqueueDecision` args minus `taskId` (the daemon fills it). */
1331
+ decision?: Record<string, unknown>
1332
+ }
1333
+
1334
+ export interface QueueEnqueueResponse {
1335
+ entry: QueueEntryWire
1336
+ }
1337
+
1338
+ export function isQueueEnqueueRequest(value: unknown): value is QueueEnqueueRequest {
1339
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'message', 'options', 'decision'])
1340
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId)
1341
+ && typeof value.message === 'string'
1342
+ && isOptionalRecord(value.options) && isOptionalRecord(value.decision)
1343
+ }
1344
+
1345
+ export function decodeQueueEnqueueRequest(value: unknown): QueueEnqueueRequest | null {
1346
+ return isQueueEnqueueRequest(value) ? value : null
1347
+ }
1348
+
1349
+ export function isQueueEnqueueResponse(value: unknown): value is QueueEnqueueResponse {
1350
+ return isRecord(value) && hasOnlyKeys(value, ['entry']) && isQueueEntryWire(value.entry)
1351
+ }
1352
+
1353
+ export function decodeQueueEnqueueResponse(value: unknown): QueueEnqueueResponse | null {
1354
+ return isQueueEnqueueResponse(value) ? value : null
1355
+ }
1356
+
1357
+ // ── queue_enqueue_graph ──
1358
+ //
1359
+ // The atomic batch enqueue, both paths, with its audit trail — in the daemon:
1360
+ // - `mode: 'compat'` → `enqueueTaskGraph(meshId, specs)`;
1361
+ // - `mode: 'graph'` → `commitMeshGraphPlan(plan)` + `recordGraphEnqueueCommitted`.
1362
+ // A failure records `recordGraphEnqueueRolledBack` (graph) or
1363
+ // `recordGraphEnqueueValidationFailed` (compat) from the catch — the daemon
1364
+ // writes the audit AFTER the failed transaction, exactly as design :752-753
1365
+ // requires — and answers `{ ok: false, refusalCode?, message, extra? }` (a
1366
+ // domain refusal is a RESULT here, not a transport error, so the caller keeps
1367
+ // its code/extra for the tool response). NOT `code`/`error`: those two keys
1368
+ // belong to the command envelope (`{ success, error, code }`) the client
1369
+ // unwraps, so a result must never reuse them.
1370
+
1371
+ export interface QueueEnqueueGraphRequest {
1372
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1373
+ meshId: string
1374
+ mode: 'compat' | 'graph'
1375
+ /** compat: `MeshTaskGraphEntrySpec[]` (JSON passthrough). */
1376
+ specs?: readonly Record<string, unknown>[]
1377
+ /** graph: `commitMeshGraphPlan` input minus `meshId` (JSON passthrough). */
1378
+ plan?: Record<string, unknown>
1379
+ /** Audit context: batchId / missionId / coordinatorSessionId / onDependencyFailure / orchestrationDecision / taskCount. */
1380
+ audit?: Record<string, unknown>
1381
+ }
1382
+
1383
+ export type QueueEnqueueGraphResponse =
1384
+ | { ok: true; tasks: readonly QueueEntryWire[]; graph?: Record<string, unknown> }
1385
+ | { ok: false; refusalCode?: string; message: string; extra?: Record<string, unknown> }
1386
+
1387
+ export function isQueueEnqueueGraphRequest(value: unknown): value is QueueEnqueueGraphRequest {
1388
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'mode', 'specs', 'plan', 'audit'])) return false
1389
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
1390
+ if (value.mode === 'compat') return isRecordArray(value.specs) && value.plan === undefined && isOptionalRecord(value.audit)
1391
+ if (value.mode === 'graph') return isRecord(value.plan) && value.specs === undefined && isOptionalRecord(value.audit)
1392
+ return false
1393
+ }
1394
+
1395
+ export function decodeQueueEnqueueGraphRequest(value: unknown): QueueEnqueueGraphRequest | null {
1396
+ return isQueueEnqueueGraphRequest(value) ? value : null
1397
+ }
1398
+
1399
+ export function isQueueEnqueueGraphResponse(value: unknown): value is QueueEnqueueGraphResponse {
1400
+ if (!isRecord(value)) return false
1401
+ if (value.ok === true) {
1402
+ return hasOnlyKeys(value, ['ok', 'tasks', 'graph']) && Array.isArray(value.tasks) && value.tasks.every(isQueueEntryWire)
1403
+ && isOptionalRecord(value.graph)
1404
+ }
1405
+ if (value.ok === false) {
1406
+ return hasOnlyKeys(value, ['ok', 'refusalCode', 'message', 'extra']) && typeof value.message === 'string'
1407
+ && isOptionalString(value.refusalCode) && isOptionalRecord(value.extra)
1408
+ }
1409
+ return false
1410
+ }
1411
+
1412
+ export function decodeQueueEnqueueGraphResponse(value: unknown): QueueEnqueueGraphResponse | null {
1413
+ return isQueueEnqueueGraphResponse(value) ? value : null
1414
+ }
1415
+
1416
+ // ── queue_cancel ──
1417
+
1418
+ export interface QueueCancelRequest {
1419
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1420
+ meshId: string
1421
+ taskId: string
1422
+ /** Operator reason (free text, local IPC). */
1423
+ reason?: string
1424
+ }
1425
+
1426
+ export interface QueueCancelResponse {
1427
+ /** The cancelled row, or null when no such task. */
1428
+ task: QueueEntryWire | null
1429
+ /** The row as it was BEFORE the cancel (its assignment is what a caller stops). */
1430
+ before: QueueEntryWire | null
1431
+ }
1432
+
1433
+ export function isQueueCancelRequest(value: unknown): value is QueueCancelRequest {
1434
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'reason'])
1435
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId) && isEvidenceIdentifier(value.taskId)
1436
+ && isOptionalString(value.reason)
1437
+ }
1438
+
1439
+ export function decodeQueueCancelRequest(value: unknown): QueueCancelRequest | null {
1440
+ return isQueueCancelRequest(value) ? value : null
1441
+ }
1442
+
1443
+ export function isQueueCancelResponse(value: unknown): value is QueueCancelResponse {
1444
+ return isRecord(value) && hasOnlyKeys(value, ['task', 'before'])
1445
+ && (value.task === null || isQueueEntryWire(value.task))
1446
+ && (value.before === null || isQueueEntryWire(value.before))
1447
+ }
1448
+
1449
+ export function decodeQueueCancelResponse(value: unknown): QueueCancelResponse | null {
1450
+ return isQueueCancelResponse(value) ? value : null
1451
+ }
1452
+
1453
+ // ── queue_requeue ──
1454
+
1455
+ export interface QueueRequeueRequest {
1456
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1457
+ meshId: string
1458
+ taskId: string
1459
+ /** `requeueTask` options (reason / targetNodeId / targetSessionId / clear* / force / message / maxRetries / notBefore), JSON passthrough. */
1460
+ options?: Record<string, unknown>
1461
+ }
1462
+
1463
+ export interface QueueRequeueResponse {
1464
+ task: QueueEntryWire | null
1465
+ }
1466
+
1467
+ export function isQueueRequeueRequest(value: unknown): value is QueueRequeueRequest {
1468
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'options'])
1469
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId) && isEvidenceIdentifier(value.taskId)
1470
+ && isOptionalRecord(value.options)
1471
+ }
1472
+
1473
+ export function decodeQueueRequeueRequest(value: unknown): QueueRequeueRequest | null {
1474
+ return isQueueRequeueRequest(value) ? value : null
1475
+ }
1476
+
1477
+ export function isQueueRequeueResponse(value: unknown): value is QueueRequeueResponse {
1478
+ return isRecord(value) && hasOnlyKeys(value, ['task']) && (value.task === null || isQueueEntryWire(value.task))
1479
+ }
1480
+
1481
+ export function decodeQueueRequeueResponse(value: unknown): QueueRequeueResponse | null {
1482
+ return isQueueRequeueResponse(value) ? value : null
1483
+ }
1484
+
1485
+ // ── direct_dispatch_record ──
1486
+ //
1487
+ // The post-dispatch bookkeeping of `mesh_send_task`'s direct arms, in the
1488
+ // daemon: `recordDirectDispatchTask` (materialise the queue row, stamped with
1489
+ // the already-open `mesh_direct` attempt) and `recordDirectDispatchDecision`
1490
+ // (GRAPH-MEASUREMENT-DIRECT). Each step is best-effort and reported separately
1491
+ // — a dispatch that already happened must never fail on its bookkeeping.
1492
+
1493
+ export interface DirectDispatchRecordRequest {
1494
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1495
+ meshId: string
1496
+ taskId: string
1497
+ /** Free text (the authored task message — the materialised row keeps it). */
1498
+ message: string
1499
+ /** `recordDirectDispatchTask` options minus `id` (JSON passthrough). */
1500
+ task?: Record<string, unknown>
1501
+ /** `recordDirectDispatchDecision` args minus `taskId` (JSON passthrough). */
1502
+ decision?: Record<string, unknown>
1503
+ }
1504
+
1505
+ export interface DirectDispatchRecordResponse {
1506
+ taskRecorded: boolean
1507
+ decisionRecorded: boolean
1508
+ }
1509
+
1510
+ export function isDirectDispatchRecordRequest(value: unknown): value is DirectDispatchRecordRequest {
1511
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'message', 'task', 'decision'])
1512
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId) && isEvidenceIdentifier(value.taskId)
1513
+ && typeof value.message === 'string' && isOptionalRecord(value.task) && isOptionalRecord(value.decision)
1514
+ }
1515
+
1516
+ export function decodeDirectDispatchRecordRequest(value: unknown): DirectDispatchRecordRequest | null {
1517
+ return isDirectDispatchRecordRequest(value) ? value : null
1518
+ }
1519
+
1520
+ export function isDirectDispatchRecordResponse(value: unknown): value is DirectDispatchRecordResponse {
1521
+ return isRecord(value) && hasOnlyKeys(value, ['taskRecorded', 'decisionRecorded'])
1522
+ && typeof value.taskRecorded === 'boolean' && typeof value.decisionRecorded === 'boolean'
1523
+ }
1524
+
1525
+ export function decodeDirectDispatchRecordResponse(value: unknown): DirectDispatchRecordResponse | null {
1526
+ return isDirectDispatchRecordResponse(value) ? value : null
1527
+ }
1528
+
1529
+ // ── graph_audit_record ──
1530
+ //
1531
+ // The coordinator-gate / node-patch provenance records the `mesh_graph_*`
1532
+ // tools write after a gate action (design :740-750), written by the daemon's
1533
+ // allow-listed recorders (mesh-graph-provenance.ts) — the mcp-server never
1534
+ // builds the record payload itself. `fields` is the recorder's argument object.
1535
+
1536
+ export const GRAPH_AUDIT_EVENTS = ['gate_claimed', 'gate_released', 'gate_abandoned', 'node_patched'] as const
1537
+ export type GraphAuditEvent = typeof GRAPH_AUDIT_EVENTS[number]
1538
+ export const isGraphAuditEvent = makeGuard(GRAPH_AUDIT_EVENTS)
1539
+
1540
+ export interface GraphAuditRecordRequest {
1541
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1542
+ meshId: string
1543
+ event: GraphAuditEvent
1544
+ fields: Record<string, unknown>
1545
+ }
1546
+
1547
+ export interface GraphAuditRecordResponse {
1548
+ recorded: boolean
1549
+ }
1550
+
1551
+ export function isGraphAuditRecordRequest(value: unknown): value is GraphAuditRecordRequest {
1552
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'event', 'fields'])
1553
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId)
1554
+ && isGraphAuditEvent(value.event) && isRecord(value.fields)
1555
+ }
1556
+
1557
+ export function decodeGraphAuditRecordRequest(value: unknown): GraphAuditRecordRequest | null {
1558
+ return isGraphAuditRecordRequest(value) ? value : null
1559
+ }
1560
+
1561
+ export function isGraphAuditRecordResponse(value: unknown): value is GraphAuditRecordResponse {
1562
+ return isRecord(value) && hasOnlyKeys(value, ['recorded']) && typeof value.recorded === 'boolean'
1563
+ }
1564
+
1565
+ export function decodeGraphAuditRecordResponse(value: unknown): GraphAuditRecordResponse | null {
1566
+ return isGraphAuditRecordResponse(value) ? value : null
1567
+ }
1568
+
1569
+ // ── active_work_query ──
1570
+ //
1571
+ // The active-work view, COMPUTED in the daemon: it reads the queue, the open
1572
+ // direct dispatches (`mesh_direct` attempts) and its local records (+ turn
1573
+ // outcomes), and runs `buildMeshActiveWork` over them with the caller's live
1574
+ // node probe results. `includeInputs` also returns the records + dispatches it
1575
+ // used (the transcript-reconcile pass and the stale-direct prune read them);
1576
+ // `compute: false` returns only those inputs. `includeSchedulingRuntime` adds
1577
+ // `buildMeshSchedulingRuntime(mesh, queue)` for the caller's `mesh` snapshot.
1578
+ // `queue` lets a caller substitute its own annotated queue view.
1579
+
1580
+ export interface ActiveWorkQueryRequest {
1581
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1582
+ meshId: string
1583
+ /** Live node probe results (`buildMeshActiveWork`'s `nodes`), JSON passthrough. */
1584
+ nodes?: readonly Record<string, unknown>[]
1585
+ /** Substitute queue rows (default: the daemon's `getQueue`). */
1586
+ queue?: readonly Record<string, unknown>[]
1587
+ /** Record tail fed to active work (default 200). */
1588
+ recordTail?: number
1589
+ includeTerminalDirect?: boolean
1590
+ /** Default true. */
1591
+ compute?: boolean
1592
+ includeInputs?: boolean
1593
+ includeSummary?: boolean
1594
+ includeSchedulingRuntime?: boolean
1595
+ /** The caller's mesh snapshot (required for the scheduling runtime), JSON passthrough. */
1596
+ mesh?: Record<string, unknown>
1597
+ }
1598
+
1599
+ export interface ActiveWorkQueryResponse {
1600
+ /** `MeshActiveWorkEvidence` (daemon-core), JSON passthrough; absent when `compute: false`. */
1601
+ activeWork?: Record<string, unknown>
1602
+ records?: readonly Record<string, unknown>[]
1603
+ directDispatches?: readonly Record<string, unknown>[]
1604
+ /** `MeshLedgerSummary`-shaped record summary. */
1605
+ summary?: Record<string, unknown>
1606
+ schedulingRuntime?: Record<string, unknown>
1607
+ }
1608
+
1609
+ export function isActiveWorkQueryRequest(value: unknown): value is ActiveWorkQueryRequest {
1610
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'nodes', 'queue', 'recordTail', 'includeTerminalDirect', 'compute', 'includeInputs', 'includeSummary', 'includeSchedulingRuntime', 'mesh'])) return false
1611
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
1612
+ if (value.nodes !== undefined && !isRecordArray(value.nodes)) return false
1613
+ if (value.queue !== undefined && !isRecordArray(value.queue)) return false
1614
+ if (value.recordTail !== undefined && !isNonNegativeInt(value.recordTail)) return false
1615
+ if (value.includeSchedulingRuntime === true && !isRecord(value.mesh)) return false
1616
+ return isOptionalBoolean(value.includeTerminalDirect) && isOptionalBoolean(value.compute) && isOptionalBoolean(value.includeInputs)
1617
+ && isOptionalBoolean(value.includeSummary) && isOptionalBoolean(value.includeSchedulingRuntime) && isOptionalRecord(value.mesh)
1618
+ }
1619
+
1620
+ export function decodeActiveWorkQueryRequest(value: unknown): ActiveWorkQueryRequest | null {
1621
+ return isActiveWorkQueryRequest(value) ? value : null
1622
+ }
1623
+
1624
+ export function isActiveWorkQueryResponse(value: unknown): value is ActiveWorkQueryResponse {
1625
+ if (!isRecord(value) || !hasOnlyKeys(value, ['activeWork', 'records', 'directDispatches', 'summary', 'schedulingRuntime'])) return false
1626
+ if (value.records !== undefined && !isRecordArray(value.records)) return false
1627
+ if (value.directDispatches !== undefined && !isRecordArray(value.directDispatches)) return false
1628
+ return isOptionalRecord(value.activeWork) && isOptionalRecord(value.summary) && isOptionalRecord(value.schedulingRuntime)
1629
+ }
1630
+
1631
+ export function decodeActiveWorkQueryResponse(value: unknown): ActiveWorkQueryResponse | null {
1632
+ return isActiveWorkQueryResponse(value) ? value : null
1633
+ }
1634
+
1635
+ // ── recovery_context_query ──
1636
+ //
1637
+ // `getSessionRecoveryContext` (daemon-core `mesh-local-records.ts`) in the
1638
+ // daemon. `lastTaskMessage` / `advice` are free text (local IPC).
1639
+
1640
+ export interface RecoveryContextQueryRequest {
1641
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1642
+ meshId: string
1643
+ nodeId?: string
1644
+ sessionId?: string
1645
+ maxRetries?: number
1646
+ }
1647
+
1648
+ export interface RecoveryContextQueryResponse {
1649
+ context: Record<string, unknown>
1650
+ }
1651
+
1652
+ export function isRecoveryContextQueryRequest(value: unknown): value is RecoveryContextQueryRequest {
1653
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'nodeId', 'sessionId', 'maxRetries'])
1654
+ && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId)
1655
+ && isOptionalId(value.nodeId) && isOptionalId(value.sessionId)
1656
+ && (value.maxRetries === undefined || isNonNegativeInt(value.maxRetries))
1657
+ && (value.nodeId !== undefined || value.sessionId !== undefined)
1658
+ }
1659
+
1660
+ export function decodeRecoveryContextQueryRequest(value: unknown): RecoveryContextQueryRequest | null {
1661
+ return isRecoveryContextQueryRequest(value) ? value : null
1662
+ }
1663
+
1664
+ export function isRecoveryContextQueryResponse(value: unknown): value is RecoveryContextQueryResponse {
1665
+ return isRecord(value) && hasOnlyKeys(value, ['context']) && isRecord(value.context)
1666
+ && typeof value.context.consecutiveNodeFailures === 'number'
1667
+ }
1668
+
1669
+ export function decodeRecoveryContextQueryResponse(value: unknown): RecoveryContextQueryResponse | null {
1670
+ return isRecoveryContextQueryResponse(value) ? value : null
1671
+ }
1672
+
1673
+ // ─── graph_gate_claim / graph_gate_release / graph_gate_abandon / graph_node_patch / graph_view_query ──
1674
+ //
1675
+ // Wiring-unification Phase C, workstream C-W9c (2026-09-24 19:00 stamp
1676
+ // "mcp-server graph gates/plan/patch ... still call daemon-core in-process").
1677
+ //
1678
+ // `mesh-tools-graph.ts`'s five graph-orchestration tools (`mesh_graph_gate_claim`
1679
+ // / `_release` / `_abandon`, `mesh_graph_node_patch`, `mesh_graph_view`) called
1680
+ // daemon-core's MeshRuntimeStore-backed graph module in-process
1681
+ // (`claimMeshGraphGate` / `releaseMeshGraphGate` / `abandonMeshGraphGate` /
1682
+ // `patchGraphNodeAndRetry` / `buildMeshGraphViews`, all in `mesh-graph-gates.ts` /
1683
+ // `mesh-graph-transition-runner.ts` / `mesh-graph-view.ts`), plus the read-only
1684
+ // git-based `collectGateConvergenceEvidence` probe. These five commands move
1685
+ // every one of those calls into the daemon that owns the graph rows.
1686
+ //
1687
+ // CONTENT BOUNDARY: a gate/node-patch result is exposed to the CALLER as
1688
+ // identifiers, enums, counts and a `result`/`evidence`/`patches` passthrough
1689
+ // the coordinator itself supplied on the request (release/patch echo back only
1690
+ // what they were given) — never anything the daemon derives from chat content.
1691
+ // `MeshGraphGateRow` / `MeshGraphView` are large, evolving daemon-core shapes;
1692
+ // rather than duplicate their structure here (the same risk `active_work_query`'s
1693
+ // `activeWork` avoided), the gate/graph payload is a JSON passthrough
1694
+ // (`Record<string, unknown>`), exactly like `ActiveWorkQueryResponse.activeWork`.
1695
+
1696
+ export interface GraphGateClaimRequest {
1697
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1698
+ meshId: string
1699
+ gateId: string
1700
+ coordinatorSessionId: string
1701
+ leaseSeconds?: number
1702
+ extendDeadlineSeconds?: number
1703
+ /** Also collect `collectGateConvergenceEvidence` on a successful claim (mesh_graph_gate_claim's own behavior). */
1704
+ probeConvergenceEvidence?: boolean
1705
+ }
1706
+
1707
+ export interface GraphGateClaimResponse {
1708
+ claimed: boolean
1709
+ /** gate_not_found / gate_not_eligible / gate_not_awaiting / gate_lease_held / gate_claim_race / gate_terminal:<state> / gate_not_claimable, when `claimed: false`. */
1710
+ reason?: string
1711
+ /** `MeshGraphGateRow`, JSON passthrough; present whenever the core found the gate. */
1712
+ gate?: Record<string, unknown>
1713
+ leaseGeneration?: number
1714
+ fencingToken?: string
1715
+ leaseExpiresAt?: string
1716
+ deadlineAt?: string
1717
+ ambiguousExternalOutcome?: boolean
1718
+ previousLeaseOwnerSessionId?: string
1719
+ /** `GateConvergenceEvidence`, JSON passthrough; present only when requested and found. */
1720
+ convergenceEvidence?: Record<string, unknown>
1721
+ }
1722
+
1723
+ export function isGraphGateClaimRequest(value: unknown): value is GraphGateClaimRequest {
1724
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'gateId', 'coordinatorSessionId', 'leaseSeconds', 'extendDeadlineSeconds', 'probeConvergenceEvidence'])) return false
1725
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId) || !isEvidenceIdentifier(value.gateId) || !isEvidenceIdentifier(value.coordinatorSessionId)) return false
1726
+ if (value.leaseSeconds !== undefined && !isFiniteNumber(value.leaseSeconds)) return false
1727
+ if (value.extendDeadlineSeconds !== undefined && !isFiniteNumber(value.extendDeadlineSeconds)) return false
1728
+ return isOptionalBoolean(value.probeConvergenceEvidence)
1729
+ }
1730
+
1731
+ export function decodeGraphGateClaimRequest(value: unknown): GraphGateClaimRequest | null {
1732
+ return isGraphGateClaimRequest(value) ? value : null
1733
+ }
1734
+
1735
+ export function isGraphGateClaimResponse(value: unknown): value is GraphGateClaimResponse {
1736
+ if (!isRecord(value) || !hasOnlyKeys(value, ['claimed', 'reason', 'gate', 'leaseGeneration', 'fencingToken', 'leaseExpiresAt', 'deadlineAt', 'ambiguousExternalOutcome', 'previousLeaseOwnerSessionId', 'convergenceEvidence'])) return false
1737
+ if (typeof value.claimed !== 'boolean') return false
1738
+ return isOptionalRecord(value.gate) && isOptionalRecord(value.convergenceEvidence) && isOptionalBoolean(value.ambiguousExternalOutcome)
1739
+ }
1740
+
1741
+ export function decodeGraphGateClaimResponse(value: unknown): GraphGateClaimResponse | null {
1742
+ return isGraphGateClaimResponse(value) ? value : null
1743
+ }
1744
+
1745
+ export interface GraphGateReleaseRequest {
1746
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1747
+ meshId: string
1748
+ gateId: string
1749
+ fencingToken: string
1750
+ leaseGeneration: number
1751
+ idempotencyKey: string
1752
+ outcome: string
1753
+ /** Action-specific structured result/evidence the CALLER supplied — JSON passthrough, echoed back unmodified. */
1754
+ result?: unknown
1755
+ evidence?: unknown
1756
+ patches?: readonly { node: string, baseSpecPatch: Record<string, unknown> }[]
1757
+ }
1758
+
1759
+ // `releaseMeshGraphGate` THROWS for every rejection (its whole transaction must
1760
+ // roll back — mesh-graph-gates.ts file header). The daemon handler catches
1761
+ // that and reports it as a RESULT, not an IPC-level failure (the same
1762
+ // success:true-carries-a-refusal shape `queue_enqueue_graph` already uses) —
1763
+ // `mesh_graph_gate_release`'s tool layer classifies `refusalCode` by prefix
1764
+ // exactly as it did the in-process thrown message.
1765
+ export type GraphGateReleaseResponse =
1766
+ | {
1767
+ released: true
1768
+ duplicate: boolean
1769
+ gate?: Record<string, unknown>
1770
+ materializedNodeIds: readonly string[]
1771
+ downstreamNodeCount?: number
1772
+ graphCompleted?: boolean
1773
+ }
1774
+ | { released: false, refusalCode?: string, message: string }
1775
+
1776
+ export function isGraphGateReleaseRequest(value: unknown): value is GraphGateReleaseRequest {
1777
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'gateId', 'fencingToken', 'leaseGeneration', 'idempotencyKey', 'outcome', 'result', 'evidence', 'patches'])) return false
1778
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId) || !isEvidenceIdentifier(value.gateId)) return false
1779
+ if (typeof value.fencingToken !== 'string' || !value.fencingToken) return false
1780
+ if (!isFiniteNumber(value.leaseGeneration)) return false
1781
+ if (typeof value.idempotencyKey !== 'string' || !value.idempotencyKey) return false
1782
+ if (typeof value.outcome !== 'string' || !value.outcome) return false
1783
+ if (value.patches !== undefined) {
1784
+ if (!Array.isArray(value.patches)) return false
1785
+ for (const p of value.patches) {
1786
+ if (!isRecord(p) || typeof p.node !== 'string' || !p.node || !isRecord(p.baseSpecPatch)) return false
1787
+ }
1788
+ }
1789
+ return true
1790
+ }
1791
+
1792
+ export function decodeGraphGateReleaseRequest(value: unknown): GraphGateReleaseRequest | null {
1793
+ return isGraphGateReleaseRequest(value) ? value : null
1794
+ }
1795
+
1796
+ export function isGraphGateReleaseResponse(value: unknown): value is GraphGateReleaseResponse {
1797
+ if (!isRecord(value)) return false
1798
+ if (value.released === true) {
1799
+ if (!hasOnlyKeys(value, ['released', 'duplicate', 'gate', 'materializedNodeIds', 'downstreamNodeCount', 'graphCompleted'])) return false
1800
+ if (typeof value.duplicate !== 'boolean') return false
1801
+ if (!Array.isArray(value.materializedNodeIds) || !value.materializedNodeIds.every((n) => typeof n === 'string')) return false
1802
+ if (value.downstreamNodeCount !== undefined && !isNonNegativeInt(value.downstreamNodeCount)) return false
1803
+ return isOptionalRecord(value.gate) && isOptionalBoolean(value.graphCompleted)
1804
+ }
1805
+ if (value.released === false) {
1806
+ return hasOnlyKeys(value, ['released', 'refusalCode', 'message']) && typeof value.message === 'string' && isOptionalString(value.refusalCode)
1807
+ }
1808
+ return false
1809
+ }
1810
+
1811
+ export function decodeGraphGateReleaseResponse(value: unknown): GraphGateReleaseResponse | null {
1812
+ return isGraphGateReleaseResponse(value) ? value : null
1813
+ }
1814
+
1815
+ export interface GraphGateAbandonRequest {
1816
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1817
+ meshId: string
1818
+ gateId: string
1819
+ reason: string
1820
+ coordinatorSessionId?: string
1821
+ force?: boolean
1822
+ }
1823
+
1824
+ export interface GraphGateAbandonResponse {
1825
+ abandoned: boolean
1826
+ /** gate_not_found / gate_lease_held / gate_abandon_race / gate_terminal:<state> / gate_not_abandonable, when `abandoned: false`. */
1827
+ reason?: string
1828
+ gate?: Record<string, unknown>
1829
+ cancelledNodeIds: readonly string[]
1830
+ cancelledTaskIds: readonly string[]
1831
+ graphStatus?: string
1832
+ }
1833
+
1834
+ export function isGraphGateAbandonRequest(value: unknown): value is GraphGateAbandonRequest {
1835
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'gateId', 'reason', 'coordinatorSessionId', 'force'])) return false
1836
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId) || !isEvidenceIdentifier(value.gateId)) return false
1837
+ if (typeof value.reason !== 'string' || !value.reason) return false
1838
+ if (value.coordinatorSessionId !== undefined && !isEvidenceIdentifier(value.coordinatorSessionId)) return false
1839
+ return isOptionalBoolean(value.force)
1840
+ }
1841
+
1842
+ export function decodeGraphGateAbandonRequest(value: unknown): GraphGateAbandonRequest | null {
1843
+ return isGraphGateAbandonRequest(value) ? value : null
1844
+ }
1845
+
1846
+ export function isGraphGateAbandonResponse(value: unknown): value is GraphGateAbandonResponse {
1847
+ if (!isRecord(value) || !hasOnlyKeys(value, ['abandoned', 'reason', 'gate', 'cancelledNodeIds', 'cancelledTaskIds', 'graphStatus'])) return false
1848
+ if (typeof value.abandoned !== 'boolean') return false
1849
+ if (!Array.isArray(value.cancelledNodeIds) || !value.cancelledNodeIds.every((n) => typeof n === 'string')) return false
1850
+ if (!Array.isArray(value.cancelledTaskIds) || !value.cancelledTaskIds.every((n) => typeof n === 'string')) return false
1851
+ return isOptionalRecord(value.gate)
1852
+ }
1853
+
1854
+ export function decodeGraphGateAbandonResponse(value: unknown): GraphGateAbandonResponse | null {
1855
+ return isGraphGateAbandonResponse(value) ? value : null
1856
+ }
1857
+
1858
+ export interface GraphNodePatchRequest {
1859
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1860
+ meshId: string
1861
+ node: string
1862
+ graphId?: string
1863
+ /** Only {@link MESH_NODE_PATCH_KEYS}-shaped keys are permitted; the daemon enforces the allow-list. */
1864
+ baseSpecPatch: Record<string, unknown>
1865
+ }
1866
+
1867
+ // `patchGraphNodeAndRetry` THROWS for every rejection (mesh-graph-transition-runner.ts) —
1868
+ // same released:false treatment as `GraphGateReleaseResponse` above.
1869
+ export type GraphNodePatchResponse =
1870
+ | {
1871
+ patched: true
1872
+ graphId: string
1873
+ nodeId: string
1874
+ ref?: string
1875
+ queueTaskId?: string
1876
+ materializationVersion: number
1877
+ state: string
1878
+ /** `SettleOutcome['kind']` — materialized / deferred / skipped / error. */
1879
+ outcomeKind: string
1880
+ /** Present when `outcomeKind === 'skipped'`. */
1881
+ skippedReason?: string
1882
+ blockedReason?: string
1883
+ }
1884
+ | { patched: false, refusalCode?: string, message: string }
1885
+
1886
+ export function isGraphNodePatchRequest(value: unknown): value is GraphNodePatchRequest {
1887
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'node', 'graphId', 'baseSpecPatch'])) return false
1888
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
1889
+ if (typeof value.node !== 'string' || !value.node) return false
1890
+ if (value.graphId !== undefined && !isEvidenceIdentifier(value.graphId)) return false
1891
+ return isRecord(value.baseSpecPatch) && Object.keys(value.baseSpecPatch).length > 0
1892
+ }
1893
+
1894
+ export function decodeGraphNodePatchRequest(value: unknown): GraphNodePatchRequest | null {
1895
+ return isGraphNodePatchRequest(value) ? value : null
1896
+ }
1897
+
1898
+ export function isGraphNodePatchResponse(value: unknown): value is GraphNodePatchResponse {
1899
+ if (!isRecord(value)) return false
1900
+ if (value.patched === true) {
1901
+ if (!hasOnlyKeys(value, ['patched', 'graphId', 'nodeId', 'ref', 'queueTaskId', 'materializationVersion', 'state', 'outcomeKind', 'skippedReason', 'blockedReason'])) return false
1902
+ if (!isEvidenceIdentifier(value.graphId) || !isEvidenceIdentifier(value.nodeId)) return false
1903
+ if (!isNonNegativeInt(value.materializationVersion)) return false
1904
+ return typeof value.state === 'string' && typeof value.outcomeKind === 'string'
1905
+ }
1906
+ if (value.patched === false) {
1907
+ return hasOnlyKeys(value, ['patched', 'refusalCode', 'message']) && typeof value.message === 'string' && isOptionalString(value.refusalCode)
1908
+ }
1909
+ return false
1910
+ }
1911
+
1912
+ export function decodeGraphNodePatchResponse(value: unknown): GraphNodePatchResponse | null {
1913
+ return isGraphNodePatchResponse(value) ? value : null
1914
+ }
1915
+
1916
+ export interface GraphViewQueryRequest {
1917
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1918
+ meshId: string
1919
+ graphId?: string
1920
+ batchId?: string
1921
+ /** Default true: only graphs that still need attention. */
1922
+ activeOnly?: boolean
1923
+ limit?: number
1924
+ /** Attach `collectGateConvergenceEvidence` to the first few waiting gates (mesh_graph_view's G4 opt-in probe). */
1925
+ probeGateEvidence?: boolean
1926
+ }
1927
+
1928
+ export interface GraphViewQueryResponse {
1929
+ /** `MeshGraphView[]`, JSON passthrough — see this section's content-boundary note. */
1930
+ graphs: readonly Record<string, unknown>[]
1931
+ }
1932
+
1933
+ export function isGraphViewQueryRequest(value: unknown): value is GraphViewQueryRequest {
1934
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'graphId', 'batchId', 'activeOnly', 'limit', 'probeGateEvidence'])) return false
1935
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
1936
+ if (value.graphId !== undefined && !isEvidenceIdentifier(value.graphId)) return false
1937
+ if (value.batchId !== undefined && !isEvidenceIdentifier(value.batchId)) return false
1938
+ if (value.limit !== undefined && !isNonNegativeInt(value.limit)) return false
1939
+ return isOptionalBoolean(value.activeOnly) && isOptionalBoolean(value.probeGateEvidence)
1940
+ }
1941
+
1942
+ export function decodeGraphViewQueryRequest(value: unknown): GraphViewQueryRequest | null {
1943
+ return isGraphViewQueryRequest(value) ? value : null
1944
+ }
1945
+
1946
+ export function isGraphViewQueryResponse(value: unknown): value is GraphViewQueryResponse {
1947
+ return isRecord(value) && hasOnlyKeys(value, ['graphs']) && isRecordArray(value.graphs)
1948
+ }
1949
+
1950
+ export function decodeGraphViewQueryResponse(value: unknown): GraphViewQueryResponse | null {
1951
+ return isGraphViewQueryResponse(value) ? value : null
1952
+ }
1953
+
1954
+ // ─── task_stats_query ───────────────────────────────────────────────────────
1955
+ //
1956
+ // C-W9c: replaces the in-process `computeMeshTaskStats` / `computeMeshMissionStats`
1957
+ // (daemon-core `mesh-task-stats.ts`) — both scan the daemon's queue + local
1958
+ // records, so both move behind one command. Exactly one of `taskIds` /
1959
+ // `missionId` selects the per-task mode; `missionId` alone with `rollup: true`
1960
+ // additionally returns the mission-level aggregate.
1961
+
1962
+ export interface TaskStatsQueryRequest {
1963
+ v: typeof TURN_IPC_PROTOCOL_VERSION
1964
+ meshId: string
1965
+ taskIds?: readonly string[]
1966
+ missionId?: string
1967
+ tail?: number
1968
+ /** Also compute `computeMeshMissionStats` — requires `missionId`. */
1969
+ rollup?: boolean
1970
+ }
1971
+
1972
+ export interface TaskStatsQueryResponse {
1973
+ /** `MeshTaskStats[]`, JSON passthrough. */
1974
+ tasks: readonly Record<string, unknown>[]
1975
+ /** `MeshMissionStats`, JSON passthrough; present only when `rollup: true` was requested and satisfiable. */
1976
+ mission?: Record<string, unknown>
1977
+ }
1978
+
1979
+ export function isTaskStatsQueryRequest(value: unknown): value is TaskStatsQueryRequest {
1980
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'taskIds', 'missionId', 'tail', 'rollup'])) return false
1981
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
1982
+ if (value.taskIds !== undefined) {
1983
+ if (!Array.isArray(value.taskIds) || value.taskIds.length === 0 || !value.taskIds.every(isEvidenceIdentifier)) return false
1984
+ }
1985
+ if (value.missionId !== undefined && !isEvidenceIdentifier(value.missionId)) return false
1986
+ if (value.tail !== undefined && !isNonNegativeInt(value.tail)) return false
1987
+ if (value.rollup === true && value.missionId === undefined) return false
1988
+ return isOptionalBoolean(value.rollup)
1989
+ }
1990
+
1991
+ export function decodeTaskStatsQueryRequest(value: unknown): TaskStatsQueryRequest | null {
1992
+ return isTaskStatsQueryRequest(value) ? value : null
1993
+ }
1994
+
1995
+ export function isTaskStatsQueryResponse(value: unknown): value is TaskStatsQueryResponse {
1996
+ return isRecord(value) && hasOnlyKeys(value, ['tasks', 'mission']) && isRecordArray(value.tasks) && isOptionalRecord(value.mission)
1997
+ }
1998
+
1999
+ export function decodeTaskStatsQueryResponse(value: unknown): TaskStatsQueryResponse | null {
2000
+ return isTaskStatsQueryResponse(value) ? value : null
2001
+ }
2002
+
2003
+ // ─── prune_stale_direct ─────────────────────────────────────────────────────
2004
+ //
2005
+ // C-W9c: replaces the in-process `pruneStaleDirectDispatches` call
2006
+ // (`mesh_prune_stale_direct` tool, `mesh-tools-session.ts`). Unlike the other
2007
+ // C-W9 commands this one does NOT take its inputs (queue/records/direct
2008
+ // dispatches) on the wire — the daemon already reads them itself
2009
+ // (`getQueue`/`readLocalRecords`/`getActiveDirectDispatches`), so the request
2010
+ // carries only the decision knobs. `closeDispatches` (the C-W8 `turn_cancel`
2011
+ // callback) also runs daemon-side now, since it is the same process.
2012
+
2013
+ export interface PruneStaleDirectRequest {
2014
+ v: typeof TURN_IPC_PROTOCOL_VERSION
2015
+ meshId: string
2016
+ execute?: boolean
2017
+ includeTerminal?: boolean
2018
+ source?: string
2019
+ }
2020
+
2021
+ export interface PruneStaleDirectResponse {
2022
+ mode: 'execute' | 'dry_run'
2023
+ includeTerminal: boolean
2024
+ candidateCount: number
2025
+ /** `MeshActiveWorkRecord[]`, JSON passthrough. */
2026
+ prunable: readonly Record<string, unknown>[]
2027
+ prunedCount: number
2028
+ preservedUnacknowledged: readonly Record<string, unknown>[]
2029
+ preservedLedgerOnly: readonly Record<string, unknown>[]
2030
+ preservedNotOrphan: readonly Record<string, unknown>[]
2031
+ }
2032
+
2033
+ export function isPruneStaleDirectRequest(value: unknown): value is PruneStaleDirectRequest {
2034
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'execute', 'includeTerminal', 'source'])) return false
2035
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
2036
+ if (value.source !== undefined && typeof value.source !== 'string') return false
2037
+ return isOptionalBoolean(value.execute) && isOptionalBoolean(value.includeTerminal)
2038
+ }
2039
+
2040
+ export function decodePruneStaleDirectRequest(value: unknown): PruneStaleDirectRequest | null {
2041
+ return isPruneStaleDirectRequest(value) ? value : null
2042
+ }
2043
+
2044
+ export function isPruneStaleDirectResponse(value: unknown): value is PruneStaleDirectResponse {
2045
+ if (!isRecord(value) || !hasOnlyKeys(value, ['mode', 'includeTerminal', 'candidateCount', 'prunable', 'prunedCount', 'preservedUnacknowledged', 'preservedLedgerOnly', 'preservedNotOrphan'])) return false
2046
+ if (value.mode !== 'execute' && value.mode !== 'dry_run') return false
2047
+ if (typeof value.includeTerminal !== 'boolean') return false
2048
+ if (!isNonNegativeInt(value.candidateCount) || !isNonNegativeInt(value.prunedCount)) return false
2049
+ return isRecordArray(value.prunable) && isRecordArray(value.preservedUnacknowledged)
2050
+ && isRecordArray(value.preservedLedgerOnly) && isRecordArray(value.preservedNotOrphan)
2051
+ }
2052
+
2053
+ export function decodePruneStaleDirectResponse(value: unknown): PruneStaleDirectResponse | null {
2054
+ return isPruneStaleDirectResponse(value) ? value : null
2055
+ }
2056
+
2057
+ // ─── orphaned_pin_notify ────────────────────────────────────────────────────
2058
+ //
2059
+ // C-W9c: replaces the in-process `notifyCoordinatorOfOrphanedPins` call
2060
+ // (`mesh_queue_cancel` tool, `mesh-tools-queue.ts`, CANCEL-ORPHANS-PINNED-TASK).
2061
+ // The core reads the daemon's live queue (`getQueue`) and, when it finds
2062
+ // orphans, calls `notifyMeshCoordinator` to page the coordinator — both are
2063
+ // daemon-only concerns, so the whole call moves, not just its queue read.
2064
+ // `title` is a task-message-derived label (first line, truncated) — free text,
2065
+ // hence local IPC only, same rationale as `record_local`'s payload.
2066
+
2067
+ export interface OrphanedPinNotifyRequest {
2068
+ v: typeof TURN_IPC_PROTOCOL_VERSION
2069
+ meshId: string
2070
+ stoppedSessionId: string
2071
+ excludeTaskId?: string
2072
+ cause?: string
2073
+ nodeId?: string
2074
+ coordinatorSessionId?: string
2075
+ }
2076
+
2077
+ export interface OrphanedPinnedTaskWire {
2078
+ taskId: string
2079
+ title: string
2080
+ targetSessionId: string
2081
+ targetNodeId?: string
2082
+ missionId?: string
2083
+ }
2084
+
2085
+ export interface OrphanedPinNotifyResponse {
2086
+ orphans: readonly OrphanedPinnedTaskWire[]
2087
+ }
2088
+
2089
+ function isOrphanedPinnedTaskWire(value: unknown): value is OrphanedPinnedTaskWire {
2090
+ if (!isRecord(value) || !hasOnlyKeys(value, ['taskId', 'title', 'targetSessionId', 'targetNodeId', 'missionId'])) return false
2091
+ if (!isEvidenceIdentifier(value.taskId) || !isEvidenceIdentifier(value.targetSessionId)) return false
2092
+ if (typeof value.title !== 'string') return false
2093
+ if (value.targetNodeId !== undefined && !isEvidenceIdentifier(value.targetNodeId)) return false
2094
+ return value.missionId === undefined || isEvidenceIdentifier(value.missionId)
2095
+ }
2096
+
2097
+ export function isOrphanedPinNotifyRequest(value: unknown): value is OrphanedPinNotifyRequest {
2098
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'stoppedSessionId', 'excludeTaskId', 'cause', 'nodeId', 'coordinatorSessionId'])) return false
2099
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId) || !isEvidenceIdentifier(value.stoppedSessionId)) return false
2100
+ if (value.excludeTaskId !== undefined && !isEvidenceIdentifier(value.excludeTaskId)) return false
2101
+ if (value.cause !== undefined && typeof value.cause !== 'string') return false
2102
+ if (value.nodeId !== undefined && !isEvidenceIdentifier(value.nodeId)) return false
2103
+ return value.coordinatorSessionId === undefined || isEvidenceIdentifier(value.coordinatorSessionId)
2104
+ }
2105
+
2106
+ export function decodeOrphanedPinNotifyRequest(value: unknown): OrphanedPinNotifyRequest | null {
2107
+ return isOrphanedPinNotifyRequest(value) ? value : null
2108
+ }
2109
+
2110
+ export function isOrphanedPinNotifyResponse(value: unknown): value is OrphanedPinNotifyResponse {
2111
+ return isRecord(value) && hasOnlyKeys(value, ['orphans']) && Array.isArray(value.orphans) && value.orphans.every(isOrphanedPinnedTaskWire)
2112
+ }
2113
+
2114
+ export function decodeOrphanedPinNotifyResponse(value: unknown): OrphanedPinNotifyResponse | null {
2115
+ return isOrphanedPinNotifyResponse(value) ? value : null
2116
+ }
2117
+
2118
+ // ─── command registry ───────────────────────────────────────────────────────
2119
+
2120
+ /** The complete, closed set of IPC command names this contract defines. */
2121
+ export const TURN_IPC_COMMANDS = [
2122
+ 'turn_observe',
2123
+ 'mesh_record',
2124
+ 'turn_cancel',
2125
+ 'operator_status',
2126
+ 'turn_query',
2127
+ 'mesh_index_query',
2128
+ 'mission_upsert',
2129
+ 'mission_query',
2130
+ 'note_upsert',
2131
+ 'note_forget',
2132
+ // C-W9b additions (2026-09-24 14:00 stamp "C-W9"):
2133
+ 'tool_call_record',
2134
+ 'ledger_query',
2135
+ 'mission_list_query',
2136
+ // C-W9a additions (the event ledger retired; the queue behind IPC):
2137
+ 'record_local',
2138
+ 'queue_query',
2139
+ 'queue_enqueue',
2140
+ 'queue_enqueue_graph',
2141
+ 'queue_cancel',
2142
+ 'queue_requeue',
2143
+ 'direct_dispatch_record',
2144
+ 'graph_audit_record',
2145
+ 'active_work_query',
2146
+ 'recovery_context_query',
2147
+ // C-W9c additions (2026-09-24 19:00 stamp — the last mcp-server in-process
2148
+ // daemon-core paths: graph gates/plan/patch, mission reads (MAGI), task/
2149
+ // mission stats, orphaned-pin helpers, one prune audit):
2150
+ 'graph_gate_claim',
2151
+ 'graph_gate_release',
2152
+ 'graph_gate_abandon',
2153
+ 'graph_node_patch',
2154
+ 'graph_view_query',
2155
+ 'task_stats_query',
2156
+ 'prune_stale_direct',
2157
+ 'orphaned_pin_notify',
2158
+ ] as const
2159
+ export type TurnIpcCommand = typeof TURN_IPC_COMMANDS[number]
2160
+ export const isTurnIpcCommand = makeGuard(TURN_IPC_COMMANDS)
2161
+
2162
+ /** Request type keyed by command name — for a generically-typed dispatcher on either end. */
2163
+ export interface TurnIpcRequestByCommand {
2164
+ turn_observe: TurnObserveRequest
2165
+ mesh_record: MeshRecordRequest
2166
+ turn_cancel: TurnCancelRequest
2167
+ operator_status: OperatorStatusRequest
2168
+ turn_query: TurnQueryRequest
2169
+ mesh_index_query: MeshIndexQueryRequest
2170
+ mission_upsert: MissionUpsertRequest
2171
+ mission_query: MissionQueryRequest
2172
+ note_upsert: NoteUpsertRequest
2173
+ note_forget: NoteForgetRequest
2174
+ tool_call_record: ToolCallRecordRequest
2175
+ ledger_query: LedgerQueryRequest
2176
+ mission_list_query: MissionListQueryRequest
2177
+ record_local: RecordLocalRequest
2178
+ queue_query: QueueQueryRequest
2179
+ queue_enqueue: QueueEnqueueRequest
2180
+ queue_enqueue_graph: QueueEnqueueGraphRequest
2181
+ queue_cancel: QueueCancelRequest
2182
+ queue_requeue: QueueRequeueRequest
2183
+ direct_dispatch_record: DirectDispatchRecordRequest
2184
+ graph_audit_record: GraphAuditRecordRequest
2185
+ active_work_query: ActiveWorkQueryRequest
2186
+ recovery_context_query: RecoveryContextQueryRequest
2187
+ graph_gate_claim: GraphGateClaimRequest
2188
+ graph_gate_release: GraphGateReleaseRequest
2189
+ graph_gate_abandon: GraphGateAbandonRequest
2190
+ graph_node_patch: GraphNodePatchRequest
2191
+ graph_view_query: GraphViewQueryRequest
2192
+ task_stats_query: TaskStatsQueryRequest
2193
+ prune_stale_direct: PruneStaleDirectRequest
2194
+ orphaned_pin_notify: OrphanedPinNotifyRequest
2195
+ }
2196
+
2197
+ /** Response type keyed by command name — for a generically-typed dispatcher on either end. */
2198
+ export interface TurnIpcResponseByCommand {
2199
+ turn_observe: TurnObserveResponse
2200
+ mesh_record: MeshRecordResponse
2201
+ turn_cancel: TurnCancelResponse
2202
+ operator_status: OperatorStatusResponse
2203
+ turn_query: TurnQueryResponse
2204
+ mission_upsert: MissionUpsertResponse
2205
+ mission_query: MissionQueryResponse
2206
+ mesh_index_query: MeshIndexQueryResponse
2207
+ note_upsert: NoteUpsertResponse
2208
+ note_forget: NoteForgetResponse
2209
+ tool_call_record: ToolCallRecordResponse
2210
+ ledger_query: LedgerQueryResponse
2211
+ mission_list_query: MissionListQueryResponse
2212
+ record_local: RecordLocalResponse
2213
+ queue_query: QueueQueryResponse
2214
+ queue_enqueue: QueueEnqueueResponse
2215
+ queue_enqueue_graph: QueueEnqueueGraphResponse
2216
+ queue_cancel: QueueCancelResponse
2217
+ queue_requeue: QueueRequeueResponse
2218
+ direct_dispatch_record: DirectDispatchRecordResponse
2219
+ graph_audit_record: GraphAuditRecordResponse
2220
+ active_work_query: ActiveWorkQueryResponse
2221
+ recovery_context_query: RecoveryContextQueryResponse
2222
+ graph_gate_claim: GraphGateClaimResponse
2223
+ graph_gate_release: GraphGateReleaseResponse
2224
+ graph_gate_abandon: GraphGateAbandonResponse
2225
+ graph_node_patch: GraphNodePatchResponse
2226
+ graph_view_query: GraphViewQueryResponse
2227
+ task_stats_query: TaskStatsQueryResponse
2228
+ prune_stale_direct: PruneStaleDirectResponse
2229
+ orphaned_pin_notify: OrphanedPinNotifyResponse
2230
+ }
2231
+