@adhdev/mesh-shared 1.0.60-rc.9 → 1.0.61-rc.1
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/brain-routing.d.ts +6 -8
- package/dist/git-diff.d.ts +21 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +2876 -77
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2625 -72
- package/dist/index.mjs.map +1 -1
- package/dist/interpolation.d.ts +0 -9
- package/dist/json.d.ts +4 -0
- package/dist/keyed-doc-delta.d.ts +105 -0
- package/dist/mesh-tool-names.d.ts +31 -2
- package/dist/mesh-vocabulary.d.ts +66 -0
- package/dist/mission-brief.d.ts +62 -0
- package/dist/outbound-message.d.ts +201 -0
- package/dist/path-ownership.d.ts +101 -0
- package/dist/protocol/daemon-server.d.ts +356 -0
- package/dist/protocol/dashboard-daemon-p2p.d.ts +213 -0
- package/dist/protocol/dashboard-server.d.ts +240 -0
- package/dist/protocol/dashboard-wire-version.d.ts +21 -0
- package/dist/protocol/envelope.d.ts +55 -0
- package/dist/protocol/index.d.ts +16 -0
- package/dist/session-launch.d.ts +105 -0
- package/dist/session-status.d.ts +75 -0
- package/dist/slot-proposal.d.ts +2 -28
- package/dist/turn-evidence.d.ts +434 -0
- package/dist/turn-ipc-guards.d.ts +18 -0
- package/dist/turn-ipc-mission.d.ts +250 -0
- package/dist/turn-ipc-queue.d.ts +196 -0
- package/dist/turn-ipc-stats.d.ts +72 -0
- package/dist/turn-ipc-turn.d.ts +176 -0
- package/dist/turn-ipc.d.ts +90 -0
- package/dist/types.d.ts +11 -0
- package/dist/worker-protocol.d.ts +69 -0
- package/dist/ws-protocol.d.ts +69 -41
- package/package.json +1 -1
- package/src/brain-routing.ts +9 -11
- package/src/git-diff.ts +48 -0
- package/src/index.ts +12 -1
- package/src/interpolation.ts +0 -18
- package/src/json.ts +10 -0
- package/src/keyed-doc-delta.ts +413 -0
- package/src/mesh-tool-names.ts +66 -25
- package/src/mesh-vocabulary.ts +94 -0
- package/src/mission-brief.ts +153 -0
- package/src/outbound-message.ts +319 -0
- package/src/path-ownership.ts +271 -0
- package/src/protocol/daemon-server.ts +510 -0
- package/src/protocol/dashboard-daemon-p2p.ts +273 -0
- package/src/protocol/dashboard-server.ts +340 -0
- package/src/protocol/dashboard-wire-version.ts +27 -0
- package/src/protocol/envelope.ts +75 -0
- package/src/protocol/index.ts +17 -0
- package/src/session-launch.ts +245 -0
- package/src/session-status.ts +164 -0
- package/src/slot-proposal.ts +2 -48
- package/src/turn-evidence.ts +596 -0
- package/src/turn-ipc-guards.ts +61 -0
- package/src/turn-ipc-mission.ts +647 -0
- package/src/turn-ipc-queue.ts +493 -0
- package/src/turn-ipc-stats.ts +193 -0
- package/src/turn-ipc-turn.ts +453 -0
- package/src/turn-ipc.ts +137 -0
- package/src/types.ts +8 -0
- package/src/worker-protocol.ts +164 -0
- package/src/ws-protocol.ts +98 -93
- package/dist/magi.d.ts +0 -281
- package/src/magi.ts +0 -310
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn-ipc — mission, note, tool-call and ledger commands: mission_upsert,
|
|
3
|
+
* mission_query, mission_list_query, note_upsert, note_forget, tool_call_record,
|
|
4
|
+
* ledger_query. Part of the turn-ipc wire contract (./turn-ipc.ts).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
makeGuard,
|
|
9
|
+
hasOnlyKeys,
|
|
10
|
+
isStringArray,
|
|
11
|
+
TURN_IPC_PROTOCOL_VERSION,
|
|
12
|
+
isNonNegativeInt,
|
|
13
|
+
isOptionalShortString,
|
|
14
|
+
isOptionalId,
|
|
15
|
+
} from './turn-ipc-guards';
|
|
16
|
+
import { isRecord } from './protocol/envelope';
|
|
17
|
+
import { isEvidenceIdentifier } from './turn-evidence';
|
|
18
|
+
|
|
19
|
+
// ─── mission_upsert / mission_query ────────────────────────────────────────
|
|
20
|
+
//
|
|
21
|
+
// Decision (2026-09-23, design doc §5 C2 update): missions and graph gates
|
|
22
|
+
// have no home in the original six commands — `upsertMeshMission`/
|
|
23
|
+
// `listMeshMissionsForTool` write/read structured state with a free-text
|
|
24
|
+
// `goal`/`title` that cannot fit `mesh_record`'s scalar `ProjectedScalars`
|
|
25
|
+
// allow-list (§ "MESH_RECORD_PAYLOAD_KEYS" above has no text field, on
|
|
26
|
+
// purpose). Two more commands, not a widened allow-list:
|
|
27
|
+
//
|
|
28
|
+
// - mission_upsert — create/update a mission (mesh-missions.ts
|
|
29
|
+
// `upsertMeshMission`'s shape).
|
|
30
|
+
// - mission_query — list missions for a mesh, optionally filtered by
|
|
31
|
+
// status (`getMeshMissions`'s shape).
|
|
32
|
+
//
|
|
33
|
+
// WHY FREE TEXT IS FINE HERE, UNLIKE mesh_record's payload: this is LOCAL
|
|
34
|
+
// IPC between mcp-server and the daemon that owns the mission table — both
|
|
35
|
+
// processes run on the operator's own machine. The content boundary this
|
|
36
|
+
// program protects is "the SERVER never receives chat content" (CLAUDE.md);
|
|
37
|
+
// IPC between two local processes was never inside that boundary, the same
|
|
38
|
+
// way a direct in-process function call wasn't. Cross-machine mission text
|
|
39
|
+
// (a worker learning about a mission a coordinator defined) travels as a
|
|
40
|
+
// `mesh.<id>.handoff` ref per C10-1's precedent — mission_upsert/query never
|
|
41
|
+
// themselves cross a machine boundary.
|
|
42
|
+
|
|
43
|
+
export const MESH_MISSION_STATUSES = ['active', 'paused', 'completed', 'abandoned'] as const
|
|
44
|
+
export type MeshMissionStatusValue = typeof MESH_MISSION_STATUSES[number]
|
|
45
|
+
export const isMeshMissionStatusValue = makeGuard(MESH_MISSION_STATUSES)
|
|
46
|
+
|
|
47
|
+
// H2 (mission brief, wiring-unification Phase H — docs/design/2026-09-23-wiring-
|
|
48
|
+
// unification.md §7c): free text, same "local IPC is inside the boundary" rationale
|
|
49
|
+
// as `goal` above (section note). `MissionBriefWire` is a structural mirror of
|
|
50
|
+
// mesh-shared's own `MissionBrief` (mission-brief.ts) rather than importing it —
|
|
51
|
+
// this file's guards are all hand-rolled structural checks, and importing the type
|
|
52
|
+
// only (not the class) would still couple this wire contract to that module's
|
|
53
|
+
// internal shape; a mirror keeps the two independently reviewable, same as every
|
|
54
|
+
// other wire type in this file.
|
|
55
|
+
export interface MissionBriefWire {
|
|
56
|
+
goal: string
|
|
57
|
+
constraints?: readonly string[]
|
|
58
|
+
doneCriteria?: readonly string[]
|
|
59
|
+
handoffNotes?: readonly string[]
|
|
60
|
+
ownedPaths?: readonly string[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isMissionBriefWire(value: unknown): value is MissionBriefWire {
|
|
64
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['goal', 'constraints', 'doneCriteria', 'handoffNotes', 'ownedPaths'])) return false
|
|
65
|
+
if (typeof value.goal !== 'string' || value.goal.trim().length === 0) return false
|
|
66
|
+
for (const k of ['constraints', 'doneCriteria', 'handoffNotes', 'ownedPaths'] as const) {
|
|
67
|
+
if (value[k] !== undefined && !isStringArray(value[k])) return false
|
|
68
|
+
}
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MissionUpsertRequest {
|
|
73
|
+
v: typeof TURN_IPC_PROTOCOL_VERSION
|
|
74
|
+
meshId: string
|
|
75
|
+
/** Omitted = create a new mission; present = update (must resolve to an existing mission — see mesh-missions.ts MISSION-UPSERT-SILENT-CREATE). */
|
|
76
|
+
id?: string
|
|
77
|
+
title: string
|
|
78
|
+
/** Free text — see file-header note on why this is fine over local IPC. */
|
|
79
|
+
goal?: string
|
|
80
|
+
status?: MeshMissionStatusValue
|
|
81
|
+
/**
|
|
82
|
+
* H2: `undefined` = do not touch the stored brief. `null` = explicitly clear it.
|
|
83
|
+
* A present object with no usable `goal` is treated by the daemon's own
|
|
84
|
+
* `normalizeMissionBrief` as "no brief" (see mesh-missions.ts upsertMeshMission) —
|
|
85
|
+
* this wire guard only checks SHAPE, not that decision.
|
|
86
|
+
*/
|
|
87
|
+
brief?: MissionBriefWire | null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface MeshMissionRecordWire {
|
|
91
|
+
id: string
|
|
92
|
+
meshId: string
|
|
93
|
+
title: string
|
|
94
|
+
goal: string
|
|
95
|
+
status: MeshMissionStatusValue
|
|
96
|
+
/** H2: absent = no brief attached to this mission. */
|
|
97
|
+
brief?: MissionBriefWire
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface MissionUpsertResponse {
|
|
101
|
+
mission: MeshMissionRecordWire
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isMeshMissionRecordWire(value: unknown): value is MeshMissionRecordWire {
|
|
105
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['id', 'meshId', 'title', 'goal', 'status', 'brief'])) return false
|
|
106
|
+
if (!isEvidenceIdentifier(value.id) || !isEvidenceIdentifier(value.meshId)) return false
|
|
107
|
+
if (typeof value.title !== 'string' || typeof value.goal !== 'string') return false
|
|
108
|
+
if (!isMeshMissionStatusValue(value.status)) return false
|
|
109
|
+
if (value.brief !== undefined && !isMissionBriefWire(value.brief)) return false
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isMissionUpsertRequest(value: unknown): value is MissionUpsertRequest {
|
|
114
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'id', 'title', 'goal', 'status', 'brief'])) return false
|
|
115
|
+
if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
|
|
116
|
+
if (!isEvidenceIdentifier(value.meshId)) return false
|
|
117
|
+
if (value.id !== undefined && !isEvidenceIdentifier(value.id)) return false
|
|
118
|
+
if (typeof value.title !== 'string' || value.title.trim().length === 0) return false
|
|
119
|
+
if (value.goal !== undefined && typeof value.goal !== 'string') return false
|
|
120
|
+
if (value.status !== undefined && !isMeshMissionStatusValue(value.status)) return false
|
|
121
|
+
if (value.brief !== undefined && value.brief !== null && !isMissionBriefWire(value.brief)) return false
|
|
122
|
+
return true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function decodeMissionUpsertRequest(value: unknown): MissionUpsertRequest | null {
|
|
126
|
+
return isMissionUpsertRequest(value) ? value : null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isMissionUpsertResponse(value: unknown): value is MissionUpsertResponse {
|
|
130
|
+
return isRecord(value) && hasOnlyKeys(value, ['mission']) && isMeshMissionRecordWire(value.mission)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function decodeMissionUpsertResponse(value: unknown): MissionUpsertResponse | null {
|
|
134
|
+
return isMissionUpsertResponse(value) ? value : null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface MissionQueryRequest {
|
|
138
|
+
v: typeof TURN_IPC_PROTOCOL_VERSION
|
|
139
|
+
meshId: string
|
|
140
|
+
/** Omitted = every status. */
|
|
141
|
+
statuses?: readonly MeshMissionStatusValue[]
|
|
142
|
+
/** Single-mission lookup (mesh-missions.ts `getMeshMission`'s shape) — mutually exclusive with `statuses`. */
|
|
143
|
+
id?: string
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface MissionQueryResponse {
|
|
147
|
+
missions: readonly MeshMissionRecordWire[]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isMissionQueryRequest(value: unknown): value is MissionQueryRequest {
|
|
151
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'id'])) return false
|
|
152
|
+
if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
|
|
153
|
+
if (!isEvidenceIdentifier(value.meshId)) return false
|
|
154
|
+
if (value.id !== undefined && !isEvidenceIdentifier(value.id)) return false
|
|
155
|
+
if (value.statuses !== undefined) {
|
|
156
|
+
if (!Array.isArray(value.statuses) || value.statuses.length === 0) return false
|
|
157
|
+
if (!value.statuses.every(isMeshMissionStatusValue)) return false
|
|
158
|
+
}
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function decodeMissionQueryRequest(value: unknown): MissionQueryRequest | null {
|
|
163
|
+
return isMissionQueryRequest(value) ? value : null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isMissionQueryResponse(value: unknown): value is MissionQueryResponse {
|
|
167
|
+
return isRecord(value) && hasOnlyKeys(value, ['missions'])
|
|
168
|
+
&& Array.isArray(value.missions) && value.missions.every(isMeshMissionRecordWire)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function decodeMissionQueryResponse(value: unknown): MissionQueryResponse | null {
|
|
172
|
+
return isMissionQueryResponse(value) ? value : null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── mission_list_query ─────────────────────────────────────────────────────
|
|
176
|
+
//
|
|
177
|
+
// Wiring-unification Phase C, workstream C-W9b.
|
|
178
|
+
//
|
|
179
|
+
// `mesh-tools-mission.ts`'s own file-header note (2026-09-24, C-W6) flagged
|
|
180
|
+
// this exact gap: `mission_query`'s landed response is `{missions:
|
|
181
|
+
// MeshMissionRecordWire[]}` only — no `verbose`/`withStats`/
|
|
182
|
+
// `limit`/`truncated`/`overflowIds`/`historyFold`, all of which
|
|
183
|
+
// `listMeshMissionsForTool` (daemon-core `mesh-missions.ts`) computes for the
|
|
184
|
+
// `mesh_mission_list` tool. Rather than widen `mission_query` itself (an
|
|
185
|
+
// already-landed, already-tested contract other callers rely on for its
|
|
186
|
+
// narrow shape), this is an ADDITIVE sibling command carrying
|
|
187
|
+
// `listMeshMissionsForTool`'s full output shape — request/response fields
|
|
188
|
+
// mirror its options/`MeshMissionListResult` one-for-one (see
|
|
189
|
+
// mesh-missions.ts). `goal`/`goalPreview` are free text, same local-IPC
|
|
190
|
+
// rationale as `mission_upsert`'s `goal` above.
|
|
191
|
+
|
|
192
|
+
export interface MissionListQueryRequest {
|
|
193
|
+
v: typeof TURN_IPC_PROTOCOL_VERSION
|
|
194
|
+
meshId: string
|
|
195
|
+
statuses?: readonly MeshMissionStatusValue[]
|
|
196
|
+
verbose?: boolean
|
|
197
|
+
withStats?: boolean
|
|
198
|
+
limit?: number
|
|
199
|
+
historyIdLimit?: number
|
|
200
|
+
/**
|
|
201
|
+
* The mesh_status mission projection, computed in the daemon (the MCP no longer
|
|
202
|
+
* opens the daemon's store for it): 'compact' = `getMeshStatusMissionsCompact`
|
|
203
|
+
* (live missions goal-elided + folded history), 'verbose' =
|
|
204
|
+
* `getMeshStatusMissionSummaries({verbose: true})` (live + capped history, full
|
|
205
|
+
* goal, no stats — stats ride one batched `task_stats_query`). Rows then also
|
|
206
|
+
* carry `createdAt`/`updatedAt`/`closeCandidateEmittedAt`. When set, every other
|
|
207
|
+
* option except `historyIdLimit` is ignored. An older daemon rejects the key.
|
|
208
|
+
*/
|
|
209
|
+
meshStatusView?: 'compact' | 'verbose'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface MeshMissionTaskAggregateWire {
|
|
213
|
+
total: number
|
|
214
|
+
pending: number
|
|
215
|
+
assigned: number
|
|
216
|
+
completed: number
|
|
217
|
+
failed: number
|
|
218
|
+
cancelled: number
|
|
219
|
+
blocked: number
|
|
220
|
+
lastActivityAt: string | null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface MeshMissionStatsWire {
|
|
224
|
+
missionId: string
|
|
225
|
+
taskCount: number
|
|
226
|
+
completed: number
|
|
227
|
+
failed: number
|
|
228
|
+
totalDurationMs: number
|
|
229
|
+
wallClockMs: number | null
|
|
230
|
+
retries: number
|
|
231
|
+
incompleteTaskIds: readonly string[]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Verbose (full-goal) mission row. `goal` present, `goalPreview`/`goalTruncated` absent. */
|
|
235
|
+
export interface MissionListSummaryVerboseWire {
|
|
236
|
+
id: string
|
|
237
|
+
meshId: string
|
|
238
|
+
title: string
|
|
239
|
+
goal: string
|
|
240
|
+
status: MeshMissionStatusValue
|
|
241
|
+
tasks: MeshMissionTaskAggregateWire
|
|
242
|
+
stats?: MeshMissionStatsWire
|
|
243
|
+
/** H2: absent = no brief attached. */
|
|
244
|
+
brief?: MissionBriefWire
|
|
245
|
+
/** `meshStatusView` rows only. */
|
|
246
|
+
createdAt?: string
|
|
247
|
+
updatedAt?: string
|
|
248
|
+
closeCandidateEmittedAt?: string | null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Compact (default) mission row. `goalPreview`/`goalTruncated` present, `goal` absent. */
|
|
252
|
+
export interface MissionListSummarySlimWire {
|
|
253
|
+
id: string
|
|
254
|
+
meshId: string
|
|
255
|
+
title: string
|
|
256
|
+
goalPreview: string
|
|
257
|
+
goalTruncated: boolean
|
|
258
|
+
status: MeshMissionStatusValue
|
|
259
|
+
tasks: MeshMissionTaskAggregateWire
|
|
260
|
+
stats?: MeshMissionStatsWire
|
|
261
|
+
/** H2: absent = no brief attached. */
|
|
262
|
+
brief?: MissionBriefWire
|
|
263
|
+
/** `meshStatusView` rows only. */
|
|
264
|
+
createdAt?: string
|
|
265
|
+
updatedAt?: string
|
|
266
|
+
closeCandidateEmittedAt?: string | null
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export type MissionListSummaryWire = MissionListSummaryVerboseWire | MissionListSummarySlimWire
|
|
270
|
+
|
|
271
|
+
export interface MeshMissionHistoryFoldWire {
|
|
272
|
+
count: number
|
|
273
|
+
byStatus: Record<string, number>
|
|
274
|
+
missionIds: readonly string[]
|
|
275
|
+
note: string
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface MissionListQueryResponse {
|
|
279
|
+
missions: readonly MissionListSummaryWire[]
|
|
280
|
+
historyFold: MeshMissionHistoryFoldWire | null
|
|
281
|
+
truncated: boolean
|
|
282
|
+
matched: number
|
|
283
|
+
overflowIds?: readonly string[]
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isMissionListQueryRequest(value: unknown): value is MissionListQueryRequest {
|
|
287
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'verbose', 'withStats', 'limit', 'historyIdLimit', 'meshStatusView'])) return false
|
|
288
|
+
if (value.meshStatusView !== undefined && value.meshStatusView !== 'compact' && value.meshStatusView !== 'verbose') return false
|
|
289
|
+
if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
|
|
290
|
+
if (!isEvidenceIdentifier(value.meshId)) return false
|
|
291
|
+
if (value.statuses !== undefined) {
|
|
292
|
+
if (!Array.isArray(value.statuses) || value.statuses.length === 0) return false
|
|
293
|
+
if (!value.statuses.every(isMeshMissionStatusValue)) return false
|
|
294
|
+
}
|
|
295
|
+
if (value.verbose !== undefined && typeof value.verbose !== 'boolean') return false
|
|
296
|
+
if (value.withStats !== undefined && typeof value.withStats !== 'boolean') return false
|
|
297
|
+
if (value.limit !== undefined && !isNonNegativeInt(value.limit)) return false
|
|
298
|
+
if (value.historyIdLimit !== undefined && !isNonNegativeInt(value.historyIdLimit)) return false
|
|
299
|
+
return true
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function decodeMissionListQueryRequest(value: unknown): MissionListQueryRequest | null {
|
|
303
|
+
return isMissionListQueryRequest(value) ? value : null
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isMeshMissionTaskAggregateWire(value: unknown): value is MeshMissionTaskAggregateWire {
|
|
307
|
+
const keys = ['total', 'pending', 'assigned', 'completed', 'failed', 'cancelled', 'blocked', 'lastActivityAt'] as const
|
|
308
|
+
if (!isRecord(value) || !hasOnlyKeys(value, keys)) return false
|
|
309
|
+
for (const k of ['total', 'pending', 'assigned', 'completed', 'failed', 'cancelled', 'blocked'] as const) {
|
|
310
|
+
if (!isNonNegativeInt(value[k])) return false
|
|
311
|
+
}
|
|
312
|
+
return value.lastActivityAt === null || typeof value.lastActivityAt === 'string'
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isMeshMissionStatsWire(value: unknown): value is MeshMissionStatsWire {
|
|
316
|
+
const keys = ['missionId', 'taskCount', 'completed', 'failed', 'totalDurationMs', 'wallClockMs', 'retries', 'incompleteTaskIds'] as const
|
|
317
|
+
if (!isRecord(value) || !hasOnlyKeys(value, keys)) return false
|
|
318
|
+
if (!isEvidenceIdentifier(value.missionId)) return false
|
|
319
|
+
for (const k of ['taskCount', 'completed', 'failed', 'totalDurationMs', 'retries'] as const) {
|
|
320
|
+
if (!isNonNegativeInt(value[k])) return false
|
|
321
|
+
}
|
|
322
|
+
if (value.wallClockMs !== null && !isNonNegativeInt(value.wallClockMs)) return false
|
|
323
|
+
if (!Array.isArray(value.incompleteTaskIds) || !value.incompleteTaskIds.every(isEvidenceIdentifier)) return false
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isMissionListSummaryWire(value: unknown): value is MissionListSummaryWire {
|
|
328
|
+
if (!isRecord(value)) return false
|
|
329
|
+
const hasGoal = 'goal' in value
|
|
330
|
+
const hasPreview = 'goalPreview' in value && 'goalTruncated' in value
|
|
331
|
+
if (hasGoal === hasPreview) return false // exactly one of the two shapes
|
|
332
|
+
const baseKeys = ['id', 'meshId', 'title', 'status', 'tasks', 'stats', 'brief', 'createdAt', 'updatedAt', 'closeCandidateEmittedAt'] as const
|
|
333
|
+
const allowed = hasGoal ? [...baseKeys, 'goal'] : [...baseKeys, 'goalPreview', 'goalTruncated']
|
|
334
|
+
if (!hasOnlyKeys(value, allowed)) return false
|
|
335
|
+
if (!isEvidenceIdentifier(value.id) || !isEvidenceIdentifier(value.meshId)) return false
|
|
336
|
+
if (typeof value.title !== 'string') return false
|
|
337
|
+
if (!isMeshMissionStatusValue(value.status)) return false
|
|
338
|
+
if (!isMeshMissionTaskAggregateWire(value.tasks)) return false
|
|
339
|
+
if (value.stats !== undefined && !isMeshMissionStatsWire(value.stats)) return false
|
|
340
|
+
if (value.brief !== undefined && !isMissionBriefWire(value.brief)) return false
|
|
341
|
+
if (hasGoal && typeof value.goal !== 'string') return false
|
|
342
|
+
if (hasPreview && (typeof value.goalPreview !== 'string' || typeof value.goalTruncated !== 'boolean')) return false
|
|
343
|
+
if (value.createdAt !== undefined && typeof value.createdAt !== 'string') return false
|
|
344
|
+
if (value.updatedAt !== undefined && typeof value.updatedAt !== 'string') return false
|
|
345
|
+
if (value.closeCandidateEmittedAt !== undefined && value.closeCandidateEmittedAt !== null && typeof value.closeCandidateEmittedAt !== 'string') return false
|
|
346
|
+
return true
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isMeshMissionHistoryFoldWire(value: unknown): value is MeshMissionHistoryFoldWire {
|
|
350
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['count', 'byStatus', 'missionIds', 'note'])) return false
|
|
351
|
+
if (!isNonNegativeInt(value.count)) return false
|
|
352
|
+
if (!isRecord(value.byStatus) || !Object.values(value.byStatus).every((n) => isNonNegativeInt(n))) return false
|
|
353
|
+
if (!Array.isArray(value.missionIds) || !value.missionIds.every(isEvidenceIdentifier)) return false
|
|
354
|
+
return typeof value.note === 'string'
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isMissionListQueryResponse(value: unknown): value is MissionListQueryResponse {
|
|
358
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['missions', 'historyFold', 'truncated', 'matched', 'overflowIds'])) return false
|
|
359
|
+
if (!Array.isArray(value.missions) || !value.missions.every(isMissionListSummaryWire)) return false
|
|
360
|
+
if (value.historyFold !== null && !isMeshMissionHistoryFoldWire(value.historyFold)) return false
|
|
361
|
+
if (typeof value.truncated !== 'boolean') return false
|
|
362
|
+
if (!isNonNegativeInt(value.matched)) return false
|
|
363
|
+
if (value.overflowIds !== undefined && (!Array.isArray(value.overflowIds) || !value.overflowIds.every(isEvidenceIdentifier))) return false
|
|
364
|
+
return true
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function decodeMissionListQueryResponse(value: unknown): MissionListQueryResponse | null {
|
|
368
|
+
return isMissionListQueryResponse(value) ? value : null
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── note_upsert / note_forget ──────────────────────────────────────────────
|
|
372
|
+
//
|
|
373
|
+
// Decision (2026-09-24, C-W6b; implemented C-W8): coordinator operating notes
|
|
374
|
+
// get their own two local-IPC commands. A note's text is coordinator-authored
|
|
375
|
+
// free text, so — exactly like mission_upsert's `goal` — it cannot ride
|
|
376
|
+
// `mesh_record`'s scalar allow-list, and it never needs to: notes live in the
|
|
377
|
+
// owning daemon's `mesh_operating_notes` table (local-only; never on
|
|
378
|
+
// `mesh.<id>.events`). The same "local IPC is inside the boundary" rationale as
|
|
379
|
+
// the mission commands above applies.
|
|
380
|
+
|
|
381
|
+
export const OPERATING_NOTE_CATEGORIES = ['provider_quirk', 'pattern_to_avoid', 'recovery_lesson'] as const
|
|
382
|
+
export type OperatingNoteCategory = typeof OPERATING_NOTE_CATEGORIES[number]
|
|
383
|
+
const isOperatingNoteCategory = makeGuard(OPERATING_NOTE_CATEGORIES)
|
|
384
|
+
|
|
385
|
+
export interface NoteUpsertRequest {
|
|
386
|
+
v: typeof TURN_IPC_PROTOCOL_VERSION
|
|
387
|
+
meshId: string
|
|
388
|
+
/** Free text (local IPC — see the section note). */
|
|
389
|
+
text: string
|
|
390
|
+
category?: OperatingNoteCategory
|
|
391
|
+
pinned?: boolean
|
|
392
|
+
/** Explicit expiry, ISO-8601. */
|
|
393
|
+
expiresAt?: string
|
|
394
|
+
/** Note id (or subject key) this note supersedes. */
|
|
395
|
+
supersedes?: string
|
|
396
|
+
subjectKey?: string
|
|
397
|
+
/** Best-effort identity of the recording coordinator (session id / daemon id / host). */
|
|
398
|
+
sourceCoordinator?: string
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface NoteUpsertResponse {
|
|
402
|
+
noteId: string
|
|
403
|
+
/** True when the same text was already among the recent live notes (no new note). */
|
|
404
|
+
deduped: boolean
|
|
405
|
+
createdAt: string
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function isNoteUpsertRequest(value: unknown): value is NoteUpsertRequest {
|
|
409
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'text', 'category', 'pinned', 'expiresAt', 'supersedes', 'subjectKey', 'sourceCoordinator'])) return false
|
|
410
|
+
if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
|
|
411
|
+
if (!isEvidenceIdentifier(value.meshId)) return false
|
|
412
|
+
if (typeof value.text !== 'string' || value.text.trim().length === 0) return false
|
|
413
|
+
if (value.category !== undefined && !isOperatingNoteCategory(value.category)) return false
|
|
414
|
+
if (value.pinned !== undefined && typeof value.pinned !== 'boolean') return false
|
|
415
|
+
if (value.expiresAt !== undefined && (typeof value.expiresAt !== 'string' || Number.isNaN(Date.parse(value.expiresAt)))) return false
|
|
416
|
+
return isOptionalShortString(value.supersedes) && isOptionalShortString(value.subjectKey) && isOptionalShortString(value.sourceCoordinator)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function decodeNoteUpsertRequest(value: unknown): NoteUpsertRequest | null {
|
|
420
|
+
return isNoteUpsertRequest(value) ? value : null
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function isNoteUpsertResponse(value: unknown): value is NoteUpsertResponse {
|
|
424
|
+
return isRecord(value) && hasOnlyKeys(value, ['noteId', 'deduped', 'createdAt'])
|
|
425
|
+
&& isEvidenceIdentifier(value.noteId) && typeof value.deduped === 'boolean' && typeof value.createdAt === 'string'
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function decodeNoteUpsertResponse(value: unknown): NoteUpsertResponse | null {
|
|
429
|
+
return isNoteUpsertResponse(value) ? value : null
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export interface NoteForgetRequest {
|
|
433
|
+
v: typeof TURN_IPC_PROTOCOL_VERSION
|
|
434
|
+
meshId: string
|
|
435
|
+
/** At least one of `noteId` / `text` (a text forget retracts every note with that exact text, and any recorded later). */
|
|
436
|
+
noteId?: string
|
|
437
|
+
text?: string
|
|
438
|
+
reason?: string
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export interface NoteForgetResponse {
|
|
442
|
+
/** Live notes this forget hid. */
|
|
443
|
+
matched: number
|
|
444
|
+
tombstoneId: string
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function isNoteForgetRequest(value: unknown): value is NoteForgetRequest {
|
|
448
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'noteId', 'text', 'reason'])) return false
|
|
449
|
+
if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
|
|
450
|
+
if (!isEvidenceIdentifier(value.meshId)) return false
|
|
451
|
+
if (value.noteId !== undefined && !isEvidenceIdentifier(value.noteId)) return false
|
|
452
|
+
if (value.text !== undefined && (typeof value.text !== 'string' || value.text.trim().length === 0)) return false
|
|
453
|
+
if (value.noteId === undefined && value.text === undefined) return false
|
|
454
|
+
return value.reason === undefined || typeof value.reason === 'string'
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function decodeNoteForgetRequest(value: unknown): NoteForgetRequest | null {
|
|
458
|
+
return isNoteForgetRequest(value) ? value : null
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function isNoteForgetResponse(value: unknown): value is NoteForgetResponse {
|
|
462
|
+
return isRecord(value) && hasOnlyKeys(value, ['matched', 'tombstoneId'])
|
|
463
|
+
&& typeof value.matched === 'number' && Number.isInteger(value.matched) && value.matched >= 0
|
|
464
|
+
&& isEvidenceIdentifier(value.tombstoneId)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function decodeNoteForgetResponse(value: unknown): NoteForgetResponse | null {
|
|
468
|
+
return isNoteForgetResponse(value) ? value : null
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─── tool_call_record ───────────────────────────────────────────────────────
|
|
472
|
+
//
|
|
473
|
+
// Wiring-unification Phase C, workstream C-W9b
|
|
474
|
+
// (docs/design/2026-09-23-wiring-unification.md §5 C2 "MCP server" paragraph,
|
|
475
|
+
// 2026-09-24 14:00 stamp "C-W9").
|
|
476
|
+
//
|
|
477
|
+
// Replaces `recordMeshToolCall`/`recordMeshCoordinatorToolCall`
|
|
478
|
+
// (`daemon-core/src/mesh/mesh-direct-dispatch.ts`) — an in-process,
|
|
479
|
+
// per-mesh-tool-call rate-limit/advisory bump mcp-server called directly
|
|
480
|
+
// against its own store handle. Every field is an identifier, a closed enum,
|
|
481
|
+
// a boolean or a counter — squarely inside `mesh_record`'s content boundary,
|
|
482
|
+
// but this is its own command (not a `mesh_record` payload) because it is a
|
|
483
|
+
// COUNTER MUTATION with a computed return value (`rateLimitExceeded`,
|
|
484
|
+
// `callsInWindow`, `advisory`), not an append-only scalar record.
|
|
485
|
+
|
|
486
|
+
export const TOOL_CALLER_ROLES = ['coordinator', 'unknown'] as const
|
|
487
|
+
export type ToolCallerRole = typeof TOOL_CALLER_ROLES[number]
|
|
488
|
+
const isToolCallerRole = makeGuard(TOOL_CALLER_ROLES)
|
|
489
|
+
|
|
490
|
+
export interface ToolCallRecordRequest {
|
|
491
|
+
v: typeof TURN_IPC_PROTOCOL_VERSION
|
|
492
|
+
meshId: string
|
|
493
|
+
/** The mesh tool name, e.g. `mesh_status` (identifier, not free text — always a fixed tool-name string). */
|
|
494
|
+
tool: string
|
|
495
|
+
sessionId?: string
|
|
496
|
+
callerRole: ToolCallerRole
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export interface ToolCallRecordResponse {
|
|
500
|
+
rateLimitExceeded: boolean
|
|
501
|
+
callsInWindow: number
|
|
502
|
+
/** Short machine-oriented advisory string, or null. Never free text describing content. */
|
|
503
|
+
advisory: string | null
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isToolCallRecordRequest(value: unknown): value is ToolCallRecordRequest {
|
|
507
|
+
return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'tool', 'sessionId', 'callerRole'])
|
|
508
|
+
&& value.v === TURN_IPC_PROTOCOL_VERSION
|
|
509
|
+
&& isEvidenceIdentifier(value.meshId)
|
|
510
|
+
&& isEvidenceIdentifier(value.tool)
|
|
511
|
+
&& isOptionalId(value.sessionId)
|
|
512
|
+
&& isToolCallerRole(value.callerRole)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function decodeToolCallRecordRequest(value: unknown): ToolCallRecordRequest | null {
|
|
516
|
+
return isToolCallRecordRequest(value) ? value : null
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function isToolCallRecordResponse(value: unknown): value is ToolCallRecordResponse {
|
|
520
|
+
return isRecord(value) && hasOnlyKeys(value, ['rateLimitExceeded', 'callsInWindow', 'advisory'])
|
|
521
|
+
&& typeof value.rateLimitExceeded === 'boolean'
|
|
522
|
+
&& isNonNegativeInt(value.callsInWindow)
|
|
523
|
+
&& (value.advisory === null || typeof value.advisory === 'string')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function decodeToolCallRecordResponse(value: unknown): ToolCallRecordResponse | null {
|
|
527
|
+
return isToolCallRecordResponse(value) ? value : null
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ─── ledger_query ───────────────────────────────────────────────────────────
|
|
531
|
+
//
|
|
532
|
+
// Wiring-unification Phase C, workstream C-W9b.
|
|
533
|
+
//
|
|
534
|
+
// Replaces the remaining bare `readLedgerEntries(meshId, opts)` /
|
|
535
|
+
// `getLedgerSummary(meshId)` in-process reads that `turn_query` cannot serve
|
|
536
|
+
// (§5 C2's `mesh-tools-mission.ts` file-header note, 2026-09-24: "general
|
|
537
|
+
// ledger browsing by arbitrary `kind`/`since`/`node` filters —
|
|
538
|
+
// `turn_query`'s shape... has no free-form kind-list or node filter, so this
|
|
539
|
+
// is a real API mismatch, not a mechanical rename"). `turn_query` stays
|
|
540
|
+
// scoped to the turn-ledger's own attempt/event rows; `ledger_query` is the
|
|
541
|
+
// general record browse surface (C-W9a: the daemon's `mesh_local_records` plus
|
|
542
|
+
// the turn ledger's task outcomes, `readLocalRecords`) `meshTaskHistory` /
|
|
543
|
+
// `meshLedgerQuery` need.
|
|
544
|
+
//
|
|
545
|
+
// CONTENT BOUNDARY: `MeshLedgerEntry.payload` (daemon-core `mesh-ledger.ts`)
|
|
546
|
+
// is `Record<string, unknown>` — genuinely unbounded JSON (a graph plan, a free-text checkpoint message), NOT constrained to
|
|
547
|
+
// `mesh_record`'s scalar allow-list. This is fine for the SAME reason
|
|
548
|
+
// `mission_upsert`'s `goal` and `note_upsert`'s `text` are fine (see that
|
|
549
|
+
// section's note above): this is local IPC between mcp-server and the
|
|
550
|
+
// daemon that owns the ledger, both on the operator's own machine — never a
|
|
551
|
+
// cross-machine or server hop. `entryPayload` below is therefore an
|
|
552
|
+
// intentionally-unvalidated `Record<string, unknown>` passthrough, exactly
|
|
553
|
+
// like `MissionUpsertRequest.goal` is unvalidated free text.
|
|
554
|
+
|
|
555
|
+
export interface LedgerQueryRequest {
|
|
556
|
+
v: typeof TURN_IPC_PROTOCOL_VERSION
|
|
557
|
+
meshId: string
|
|
558
|
+
/** One or more ledger `kind` strings (daemon-core `MeshLedgerKind`; mesh-shared cannot import that union, so these are identifiers here). Omitted = every kind. */
|
|
559
|
+
kind?: readonly string[]
|
|
560
|
+
/** ISO-8601 or epoch-ms-as-string — passed through to `readLedgerEntries`'s `since` (parsed via `new Date()`). */
|
|
561
|
+
since?: string
|
|
562
|
+
/** Filter to entries whose nodeId is equivalent to this daemon id (matched via `daemonIdsEquivalent` daemon-side, not raw `===`). */
|
|
563
|
+
node?: string
|
|
564
|
+
tail?: number
|
|
565
|
+
/** 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). */
|
|
566
|
+
includeSummary?: boolean
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export interface LedgerQueryEntryWire {
|
|
570
|
+
id: string
|
|
571
|
+
meshId: string
|
|
572
|
+
timestamp: string
|
|
573
|
+
kind: string
|
|
574
|
+
nodeId?: string
|
|
575
|
+
sessionId?: string
|
|
576
|
+
providerType?: string
|
|
577
|
+
taskId?: string
|
|
578
|
+
/** Unbounded JSON passthrough — see file section's CONTENT BOUNDARY note. */
|
|
579
|
+
payload: Record<string, unknown>
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export interface LedgerQuerySummaryWire {
|
|
583
|
+
meshId: string
|
|
584
|
+
totalEntries: number
|
|
585
|
+
taskDispatched: number
|
|
586
|
+
taskCompleted: number
|
|
587
|
+
taskFailed: number
|
|
588
|
+
taskStalled: number
|
|
589
|
+
sessionLaunched: number
|
|
590
|
+
checkpointCreated: number
|
|
591
|
+
lastActivityAt: string | null
|
|
592
|
+
recentFailures: number
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export interface LedgerQueryResponse {
|
|
596
|
+
entries: readonly LedgerQueryEntryWire[]
|
|
597
|
+
summary?: LedgerQuerySummaryWire
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function isLedgerQueryRequest(value: unknown): value is LedgerQueryRequest {
|
|
601
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'kind', 'since', 'node', 'tail', 'includeSummary'])) return false
|
|
602
|
+
if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
|
|
603
|
+
if (!isEvidenceIdentifier(value.meshId)) return false
|
|
604
|
+
if (value.kind !== undefined && (!Array.isArray(value.kind) || value.kind.length === 0 || !value.kind.every(isEvidenceIdentifier))) return false
|
|
605
|
+
if (value.since !== undefined && typeof value.since !== 'string') return false
|
|
606
|
+
if (!isOptionalId(value.node)) return false
|
|
607
|
+
if (value.tail !== undefined && !isNonNegativeInt(value.tail)) return false
|
|
608
|
+
if (value.includeSummary !== undefined && typeof value.includeSummary !== 'boolean') return false
|
|
609
|
+
return true
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function decodeLedgerQueryRequest(value: unknown): LedgerQueryRequest | null {
|
|
613
|
+
return isLedgerQueryRequest(value) ? value : null
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const LEDGER_QUERY_ENTRY_KEYS = ['id', 'meshId', 'timestamp', 'kind', 'nodeId', 'sessionId', 'providerType', 'taskId', 'payload'] as const
|
|
617
|
+
|
|
618
|
+
function isLedgerQueryEntryWire(value: unknown): value is LedgerQueryEntryWire {
|
|
619
|
+
if (!isRecord(value) || !hasOnlyKeys(value, LEDGER_QUERY_ENTRY_KEYS)) return false
|
|
620
|
+
if (!isEvidenceIdentifier(value.id) || !isEvidenceIdentifier(value.meshId)) return false
|
|
621
|
+
if (typeof value.timestamp !== 'string' || typeof value.kind !== 'string') return false
|
|
622
|
+
if (!isOptionalId(value.nodeId) || !isOptionalId(value.sessionId) || !isOptionalId(value.providerType) || !isOptionalId(value.taskId)) return false
|
|
623
|
+
if (!isRecord(value.payload)) return false
|
|
624
|
+
return true
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function isLedgerQuerySummaryWire(value: unknown): value is LedgerQuerySummaryWire {
|
|
628
|
+
const keys = ['meshId', 'totalEntries', 'taskDispatched', 'taskCompleted', 'taskFailed', 'taskStalled', 'sessionLaunched', 'checkpointCreated', 'lastActivityAt', 'recentFailures'] as const
|
|
629
|
+
if (!isRecord(value) || !hasOnlyKeys(value, keys)) return false
|
|
630
|
+
if (!isEvidenceIdentifier(value.meshId)) return false
|
|
631
|
+
for (const k of ['totalEntries', 'taskDispatched', 'taskCompleted', 'taskFailed', 'taskStalled', 'sessionLaunched', 'checkpointCreated', 'recentFailures'] as const) {
|
|
632
|
+
if (!isNonNegativeInt(value[k])) return false
|
|
633
|
+
}
|
|
634
|
+
if (value.lastActivityAt !== null && typeof value.lastActivityAt !== 'string') return false
|
|
635
|
+
return true
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function isLedgerQueryResponse(value: unknown): value is LedgerQueryResponse {
|
|
639
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ['entries', 'summary'])) return false
|
|
640
|
+
if (!Array.isArray(value.entries) || !value.entries.every(isLedgerQueryEntryWire)) return false
|
|
641
|
+
if (value.summary !== undefined && !isLedgerQuerySummaryWire(value.summary)) return false
|
|
642
|
+
return true
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function decodeLedgerQueryResponse(value: unknown): LedgerQueryResponse | null {
|
|
646
|
+
return isLedgerQueryResponse(value) ? value : null
|
|
647
|
+
}
|