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

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