@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/dist/index.js +33 -157
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +33 -90
- package/dist/index.mjs.map +1 -1
- package/dist/turn-ipc-graph.d.ts +195 -0
- package/dist/turn-ipc-guards.d.ts +18 -0
- package/dist/turn-ipc-mission.d.ts +257 -0
- package/dist/turn-ipc-queue.d.ts +221 -0
- package/dist/turn-ipc-turn.d.ts +176 -0
- package/dist/turn-ipc.d.ts +11 -961
- package/package.json +1 -1
- package/src/turn-ipc-graph.ts +477 -0
- package/src/turn-ipc-guards.ts +61 -0
- package/src/turn-ipc-mission.ts +661 -0
- package/src/turn-ipc-queue.ts +555 -0
- package/src/turn-ipc-turn.ts +453 -0
- package/src/turn-ipc.ts +40 -2227
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
2177
|
-
|
|
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
|
|
2185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
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
|
|
2206
|
-
return isOrphanedPinNotifyRequest(value) ? value : null
|
|
2207
|
-
}
|
|
87
|
+
export const isTurnIpcErrorCode = makeGuard(TURN_IPC_ERROR_CODES)
|
|
2208
88
|
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
|
2214
|
-
return
|
|
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
|
-
|