@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.
Files changed (67) hide show
  1. package/dist/brain-routing.d.ts +6 -8
  2. package/dist/git-diff.d.ts +21 -0
  3. package/dist/index.d.ts +12 -1
  4. package/dist/index.js +2876 -77
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +2625 -72
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/interpolation.d.ts +0 -9
  9. package/dist/json.d.ts +4 -0
  10. package/dist/keyed-doc-delta.d.ts +105 -0
  11. package/dist/mesh-tool-names.d.ts +31 -2
  12. package/dist/mesh-vocabulary.d.ts +66 -0
  13. package/dist/mission-brief.d.ts +62 -0
  14. package/dist/outbound-message.d.ts +201 -0
  15. package/dist/path-ownership.d.ts +101 -0
  16. package/dist/protocol/daemon-server.d.ts +356 -0
  17. package/dist/protocol/dashboard-daemon-p2p.d.ts +213 -0
  18. package/dist/protocol/dashboard-server.d.ts +240 -0
  19. package/dist/protocol/dashboard-wire-version.d.ts +21 -0
  20. package/dist/protocol/envelope.d.ts +55 -0
  21. package/dist/protocol/index.d.ts +16 -0
  22. package/dist/session-launch.d.ts +105 -0
  23. package/dist/session-status.d.ts +75 -0
  24. package/dist/slot-proposal.d.ts +2 -28
  25. package/dist/turn-evidence.d.ts +434 -0
  26. package/dist/turn-ipc-guards.d.ts +18 -0
  27. package/dist/turn-ipc-mission.d.ts +250 -0
  28. package/dist/turn-ipc-queue.d.ts +196 -0
  29. package/dist/turn-ipc-stats.d.ts +72 -0
  30. package/dist/turn-ipc-turn.d.ts +176 -0
  31. package/dist/turn-ipc.d.ts +90 -0
  32. package/dist/types.d.ts +11 -0
  33. package/dist/worker-protocol.d.ts +69 -0
  34. package/dist/ws-protocol.d.ts +69 -41
  35. package/package.json +1 -1
  36. package/src/brain-routing.ts +9 -11
  37. package/src/git-diff.ts +48 -0
  38. package/src/index.ts +12 -1
  39. package/src/interpolation.ts +0 -18
  40. package/src/json.ts +10 -0
  41. package/src/keyed-doc-delta.ts +413 -0
  42. package/src/mesh-tool-names.ts +66 -25
  43. package/src/mesh-vocabulary.ts +94 -0
  44. package/src/mission-brief.ts +153 -0
  45. package/src/outbound-message.ts +319 -0
  46. package/src/path-ownership.ts +271 -0
  47. package/src/protocol/daemon-server.ts +510 -0
  48. package/src/protocol/dashboard-daemon-p2p.ts +273 -0
  49. package/src/protocol/dashboard-server.ts +340 -0
  50. package/src/protocol/dashboard-wire-version.ts +27 -0
  51. package/src/protocol/envelope.ts +75 -0
  52. package/src/protocol/index.ts +17 -0
  53. package/src/session-launch.ts +245 -0
  54. package/src/session-status.ts +164 -0
  55. package/src/slot-proposal.ts +2 -48
  56. package/src/turn-evidence.ts +596 -0
  57. package/src/turn-ipc-guards.ts +61 -0
  58. package/src/turn-ipc-mission.ts +647 -0
  59. package/src/turn-ipc-queue.ts +493 -0
  60. package/src/turn-ipc-stats.ts +193 -0
  61. package/src/turn-ipc-turn.ts +453 -0
  62. package/src/turn-ipc.ts +137 -0
  63. package/src/types.ts +8 -0
  64. package/src/worker-protocol.ts +164 -0
  65. package/src/ws-protocol.ts +98 -93
  66. package/dist/magi.d.ts +0 -281
  67. package/src/magi.ts +0 -310
@@ -0,0 +1,193 @@
1
+ /**
2
+ * turn-ipc — task stats and maintenance commands: task_stats_query,
3
+ * prune_stale_direct, orphaned_pin_notify. Part of the turn-ipc wire contract
4
+ * (./turn-ipc.ts).
5
+ */
6
+
7
+ import {
8
+ TURN_IPC_PROTOCOL_VERSION,
9
+ hasOnlyKeys,
10
+ isOptionalBoolean,
11
+ isOptionalRecord,
12
+ isNonNegativeInt,
13
+ isRecordArray,
14
+ } from './turn-ipc-guards';
15
+ import { isRecord } from './protocol/envelope';
16
+ import { isEvidenceIdentifier } from './turn-evidence';
17
+
18
+ // ─── task_stats_query ───────────────────────────────────────────────────────
19
+ //
20
+ // C-W9c: replaces the in-process `computeMeshTaskStats` / `computeMeshMissionStats`
21
+ // (daemon-core `mesh-task-stats.ts`) — both scan the daemon's queue + local
22
+ // records, so both move behind one command. Exactly one of `taskIds` /
23
+ // `missionId` selects the per-task mode; `missionId` alone with `rollup: true`
24
+ // additionally returns the mission-level aggregate.
25
+
26
+ export interface TaskStatsQueryRequest {
27
+ v: typeof TURN_IPC_PROTOCOL_VERSION
28
+ meshId: string
29
+ taskIds?: readonly string[]
30
+ missionId?: string
31
+ tail?: number
32
+ /** Also compute `computeMeshMissionStats` — requires `missionId`. */
33
+ rollup?: boolean
34
+ /**
35
+ * Batch rollups: `computeMeshMissionStats` for every id, over ONE queue read and
36
+ * ONE record read (answers `missions`, `tasks` empty). Exclusive with
37
+ * `taskIds`/`missionId`. An older daemon rejects the key.
38
+ */
39
+ missionIds?: readonly string[]
40
+ }
41
+
42
+ export interface TaskStatsQueryResponse {
43
+ /** `MeshTaskStats[]`, JSON passthrough. */
44
+ tasks: readonly Record<string, unknown>[]
45
+ /** `MeshMissionStats`, JSON passthrough; present only when `rollup: true` was requested and satisfiable. */
46
+ mission?: Record<string, unknown>
47
+ /** Batch mode: `MeshMissionStats` per requested mission id, JSON passthrough. */
48
+ missions?: Record<string, Record<string, unknown>>
49
+ }
50
+
51
+ function isTaskStatsQueryRequest(value: unknown): value is TaskStatsQueryRequest {
52
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'taskIds', 'missionId', 'tail', 'rollup', 'missionIds'])) return false
53
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
54
+ if (value.taskIds !== undefined) {
55
+ if (!Array.isArray(value.taskIds) || value.taskIds.length === 0 || !value.taskIds.every(isEvidenceIdentifier)) return false
56
+ }
57
+ if (value.missionId !== undefined && !isEvidenceIdentifier(value.missionId)) return false
58
+ if (value.tail !== undefined && !isNonNegativeInt(value.tail)) return false
59
+ if (value.missionIds !== undefined) {
60
+ if (!Array.isArray(value.missionIds) || value.missionIds.length === 0 || !value.missionIds.every(isEvidenceIdentifier)) return false
61
+ if (value.taskIds !== undefined || value.missionId !== undefined) return false
62
+ }
63
+ if (value.rollup === true && value.missionId === undefined) return false
64
+ return isOptionalBoolean(value.rollup)
65
+ }
66
+
67
+ export function decodeTaskStatsQueryRequest(value: unknown): TaskStatsQueryRequest | null {
68
+ return isTaskStatsQueryRequest(value) ? value : null
69
+ }
70
+
71
+ function isTaskStatsQueryResponse(value: unknown): value is TaskStatsQueryResponse {
72
+ return isRecord(value) && hasOnlyKeys(value, ['tasks', 'mission', 'missions']) && isRecordArray(value.tasks) && isOptionalRecord(value.mission)
73
+ && (value.missions === undefined || (isRecord(value.missions) && Object.values(value.missions).every(isRecord)))
74
+ }
75
+
76
+ export function decodeTaskStatsQueryResponse(value: unknown): TaskStatsQueryResponse | null {
77
+ return isTaskStatsQueryResponse(value) ? value : null
78
+ }
79
+
80
+ // ─── prune_stale_direct ─────────────────────────────────────────────────────
81
+ //
82
+ // C-W9c: replaces the in-process `pruneStaleDirectDispatches` call
83
+ // (`mesh_cleanup_sessions` mode=prune_stale_direct, `mesh-tools-session.ts`). Unlike the other
84
+ // C-W9 commands this one does NOT take its inputs (queue/records/direct
85
+ // dispatches) on the wire — the daemon already reads them itself
86
+ // (`getQueue`/`readLocalRecords`/`getActiveDirectDispatches`), so the request
87
+ // carries only the decision knobs. `closeDispatches` (the C-W8 `turn_cancel`
88
+ // callback) also runs daemon-side now, since it is the same process.
89
+
90
+ export interface PruneStaleDirectRequest {
91
+ v: typeof TURN_IPC_PROTOCOL_VERSION
92
+ meshId: string
93
+ execute?: boolean
94
+ includeTerminal?: boolean
95
+ source?: string
96
+ }
97
+
98
+ export interface PruneStaleDirectResponse {
99
+ mode: 'execute' | 'dry_run'
100
+ includeTerminal: boolean
101
+ candidateCount: number
102
+ /** `MeshActiveWorkRecord[]`, JSON passthrough. */
103
+ prunable: readonly Record<string, unknown>[]
104
+ prunedCount: number
105
+ preservedUnacknowledged: readonly Record<string, unknown>[]
106
+ preservedLedgerOnly: readonly Record<string, unknown>[]
107
+ preservedNotOrphan: readonly Record<string, unknown>[]
108
+ }
109
+
110
+ function isPruneStaleDirectRequest(value: unknown): value is PruneStaleDirectRequest {
111
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'execute', 'includeTerminal', 'source'])) return false
112
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
113
+ if (value.source !== undefined && typeof value.source !== 'string') return false
114
+ return isOptionalBoolean(value.execute) && isOptionalBoolean(value.includeTerminal)
115
+ }
116
+
117
+ export function decodePruneStaleDirectRequest(value: unknown): PruneStaleDirectRequest | null {
118
+ return isPruneStaleDirectRequest(value) ? value : null
119
+ }
120
+
121
+ function isPruneStaleDirectResponse(value: unknown): value is PruneStaleDirectResponse {
122
+ if (!isRecord(value) || !hasOnlyKeys(value, ['mode', 'includeTerminal', 'candidateCount', 'prunable', 'prunedCount', 'preservedUnacknowledged', 'preservedLedgerOnly', 'preservedNotOrphan'])) return false
123
+ if (value.mode !== 'execute' && value.mode !== 'dry_run') return false
124
+ if (typeof value.includeTerminal !== 'boolean') return false
125
+ if (!isNonNegativeInt(value.candidateCount) || !isNonNegativeInt(value.prunedCount)) return false
126
+ return isRecordArray(value.prunable) && isRecordArray(value.preservedUnacknowledged)
127
+ && isRecordArray(value.preservedLedgerOnly) && isRecordArray(value.preservedNotOrphan)
128
+ }
129
+
130
+ export function decodePruneStaleDirectResponse(value: unknown): PruneStaleDirectResponse | null {
131
+ return isPruneStaleDirectResponse(value) ? value : null
132
+ }
133
+
134
+ // ─── orphaned_pin_notify ────────────────────────────────────────────────────
135
+ //
136
+ // C-W9c: replaces the in-process `notifyCoordinatorOfOrphanedPins` call
137
+ // (`mesh_queue_cancel` tool, `mesh-tools-queue.ts`, CANCEL-ORPHANS-PINNED-TASK).
138
+ // The core reads the daemon's live queue (`getQueue`) and, when it finds
139
+ // orphans, calls `notifyMeshCoordinator` to page the coordinator — both are
140
+ // daemon-only concerns, so the whole call moves, not just its queue read.
141
+ // `title` is a task-message-derived label (first line, truncated) — free text,
142
+ // hence local IPC only, same rationale as `record_local`'s payload.
143
+
144
+ export interface OrphanedPinNotifyRequest {
145
+ v: typeof TURN_IPC_PROTOCOL_VERSION
146
+ meshId: string
147
+ stoppedSessionId: string
148
+ excludeTaskId?: string
149
+ cause?: string
150
+ nodeId?: string
151
+ coordinatorSessionId?: string
152
+ }
153
+
154
+ export interface OrphanedPinnedTaskWire {
155
+ taskId: string
156
+ title: string
157
+ targetSessionId: string
158
+ targetNodeId?: string
159
+ missionId?: string
160
+ }
161
+
162
+ export interface OrphanedPinNotifyResponse {
163
+ orphans: readonly OrphanedPinnedTaskWire[]
164
+ }
165
+
166
+ function isOrphanedPinnedTaskWire(value: unknown): value is OrphanedPinnedTaskWire {
167
+ if (!isRecord(value) || !hasOnlyKeys(value, ['taskId', 'title', 'targetSessionId', 'targetNodeId', 'missionId'])) return false
168
+ if (!isEvidenceIdentifier(value.taskId) || !isEvidenceIdentifier(value.targetSessionId)) return false
169
+ if (typeof value.title !== 'string') return false
170
+ if (value.targetNodeId !== undefined && !isEvidenceIdentifier(value.targetNodeId)) return false
171
+ return value.missionId === undefined || isEvidenceIdentifier(value.missionId)
172
+ }
173
+
174
+ function isOrphanedPinNotifyRequest(value: unknown): value is OrphanedPinNotifyRequest {
175
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'stoppedSessionId', 'excludeTaskId', 'cause', 'nodeId', 'coordinatorSessionId'])) return false
176
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId) || !isEvidenceIdentifier(value.stoppedSessionId)) return false
177
+ if (value.excludeTaskId !== undefined && !isEvidenceIdentifier(value.excludeTaskId)) return false
178
+ if (value.cause !== undefined && typeof value.cause !== 'string') return false
179
+ if (value.nodeId !== undefined && !isEvidenceIdentifier(value.nodeId)) return false
180
+ return value.coordinatorSessionId === undefined || isEvidenceIdentifier(value.coordinatorSessionId)
181
+ }
182
+
183
+ export function decodeOrphanedPinNotifyRequest(value: unknown): OrphanedPinNotifyRequest | null {
184
+ return isOrphanedPinNotifyRequest(value) ? value : null
185
+ }
186
+
187
+ function isOrphanedPinNotifyResponse(value: unknown): value is OrphanedPinNotifyResponse {
188
+ return isRecord(value) && hasOnlyKeys(value, ['orphans']) && Array.isArray(value.orphans) && value.orphans.every(isOrphanedPinnedTaskWire)
189
+ }
190
+
191
+ export function decodeOrphanedPinNotifyResponse(value: unknown): OrphanedPinNotifyResponse | null {
192
+ return isOrphanedPinNotifyResponse(value) ? value : null
193
+ }
@@ -0,0 +1,453 @@
1
+ /**
2
+ * turn-ipc — the turn-ledger commands: turn_observe, mesh_record, turn_cancel,
3
+ * operator_status, turn_query, mesh_index_query. Part of the turn-ipc wire
4
+ * contract (./turn-ipc.ts).
5
+ */
6
+
7
+ import {
8
+ makeGuard,
9
+ TURN_IPC_PROTOCOL_VERSION,
10
+ hasOnlyKeys,
11
+ isNonNegativeInt,
12
+ isOptionalId,
13
+ isFiniteNumber,
14
+ } from './turn-ipc-guards';
15
+ import {
16
+ type TurnEvidence,
17
+ type TurnAttemptRef,
18
+ type TurnOutcome,
19
+ isTurnEvidence,
20
+ isEvidenceIdentifier,
21
+ type ProjectedScalars,
22
+ type TurnReason,
23
+ isTurnReason,
24
+ type TurnEvidenceKind,
25
+ } from './turn-evidence';
26
+ import { isRecord } from './protocol/envelope';
27
+
28
+ // ─── turn_observe ───────────────────────────────────────────────────────────
29
+
30
+ /** Reducer verdict, mirrored from the C1 reducer's `reduce()` return shape. */
31
+ export const TURN_OBSERVE_VERDICTS = ['applied', 'recorded', 'rejected'] as const
32
+ export type TurnObserveVerdict = typeof TURN_OBSERVE_VERDICTS[number]
33
+ const isTurnObserveVerdict = makeGuard(TURN_OBSERVE_VERDICTS)
34
+
35
+ export interface TurnObserveRequest {
36
+ v: typeof TURN_IPC_PROTOCOL_VERSION
37
+ evidence: TurnEvidence
38
+ }
39
+
40
+ export interface TurnObserveResponse {
41
+ verdict: TurnObserveVerdict
42
+ attemptRef: TurnAttemptRef
43
+ /** Present only when the evidence committed the attempt (verdict 'applied' + a terminal-class kind). */
44
+ outcome?: TurnOutcome
45
+ }
46
+
47
+ function isTurnObserveRequest(value: unknown): value is TurnObserveRequest {
48
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'evidence'])
49
+ && value.v === TURN_IPC_PROTOCOL_VERSION
50
+ && isTurnEvidence(value.evidence)
51
+ }
52
+
53
+ export function decodeTurnObserveRequest(value: unknown): TurnObserveRequest | null {
54
+ return isTurnObserveRequest(value) ? value : null
55
+ }
56
+
57
+ function isTurnAttemptRefValue(value: unknown): value is TurnAttemptRef {
58
+ return isRecord(value) && hasOnlyKeys(value, ['attemptId', 'generation'])
59
+ && isEvidenceIdentifier(value.attemptId) && isNonNegativeInt(value.generation)
60
+ }
61
+
62
+ function isTurnObserveResponse(value: unknown): value is TurnObserveResponse {
63
+ return isRecord(value) && hasOnlyKeys(value, ['verdict', 'attemptRef', 'outcome'])
64
+ && isTurnObserveVerdict(value.verdict)
65
+ && isTurnAttemptRefValue(value.attemptRef)
66
+ && (value.outcome === undefined || isTurnOutcomeValue(value.outcome))
67
+ }
68
+
69
+ const TURN_OUTCOMES_LOCAL = ['completed', 'failed', 'cancelled'] as const
70
+ function isTurnOutcomeValue(value: unknown): value is TurnOutcome {
71
+ return typeof value === 'string' && (TURN_OUTCOMES_LOCAL as readonly string[]).includes(value)
72
+ }
73
+
74
+ export function decodeTurnObserveResponse(value: unknown): TurnObserveResponse | null {
75
+ return isTurnObserveResponse(value) ? value : null
76
+ }
77
+
78
+ // ─── mesh_record ────────────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * Structural pin of `seqscribe/mesh-event-projection.ts` `PROJECTED_PAYLOAD_KEYS`
82
+ * (daemon-core; this leaf cannot import it). A test in this package's test
83
+ * suite AND a daemon-core test each assert their list equals this one — see
84
+ * `mesh-shared/test/turn-ipc.test.ts` and the C-W6 report's break-once table.
85
+ * Adding a key here is the SAME assertion as adding it there: non-content
86
+ * (identifier, enum, boolean, or counter), never free text.
87
+ */
88
+ export const MESH_RECORD_PAYLOAD_KEYS = [
89
+ // Identifiers
90
+ 'taskId', 'deliveryId', 'attemptId', 'missionId', 'checkpointId', 'promptId', 'providerSessionId', 'targetNoteId',
91
+ // Routing / addressing
92
+ 'nodeId', 'sessionId', 'providerType', 'transport', 'attemptedSessionId', 'holderSessionId',
93
+ // Outcome enums
94
+ 'reason', 'status', 'outcome', 'terminalKind', 'event',
95
+ 'source', 'intentionalStopReason',
96
+ // Booleans
97
+ 'retryable', 'rebound', 'forced', 'fallback', 'membershipRemoved', 'requestedForce', 'removedByRemoteDaemon',
98
+ 'completedViaReady', 'intentional', 'weak',
99
+ // Counters
100
+ 'attempt', 'attemptCount', 'count',
101
+ ] as const
102
+
103
+ const MESH_RECORD_PAYLOAD_KEY_SET: ReadonlySet<string> = new Set(MESH_RECORD_PAYLOAD_KEYS)
104
+
105
+ /** Maximum length of any string value in a `mesh_record` payload (mirrors `MAX_PROJECTED_STRING`). */
106
+ export const MAX_MESH_RECORD_STRING = 200
107
+
108
+ export interface MeshRecordRequest {
109
+ v: typeof TURN_IPC_PROTOCOL_VERSION
110
+ meshId: string
111
+ /** The original ledger `kind` string (daemon-core `MeshLedgerKind`; mesh-shared cannot import that union, so it is an identifier here). */
112
+ ledgerKind: string
113
+ nodeId?: string
114
+ sessionId?: string
115
+ taskId?: string
116
+ /** Restricted to `MESH_RECORD_PAYLOAD_KEYS` — an unknown key is a decode failure, not a silently dropped field. */
117
+ payload: ProjectedScalars
118
+ }
119
+
120
+ export interface MeshRecordResponse {
121
+ /** `mesh.<id>.events` append coordinates, for callers that need to reference the entry (e.g. a `ref`'d handoff). */
122
+ eventId: string
123
+ seq: number
124
+ }
125
+
126
+ function isProjectedScalarsValue(value: unknown): value is ProjectedScalars {
127
+ if (!isRecord(value)) return false
128
+ for (const v of Object.values(value)) {
129
+ if (v === null) continue
130
+ if (typeof v === 'boolean' || typeof v === 'number') continue
131
+ if (typeof v === 'string') { if (v.length > MAX_MESH_RECORD_STRING) return false; continue }
132
+ return false
133
+ }
134
+ return true
135
+ }
136
+
137
+ /** Every payload key must be a member of `MESH_RECORD_PAYLOAD_KEYS` — the allow-list boundary at the wire. */
138
+ export function isMeshRecordPayload(value: unknown): value is ProjectedScalars {
139
+ if (!isProjectedScalarsValue(value)) return false
140
+ for (const key of Object.keys(value as Record<string, unknown>)) {
141
+ if (!MESH_RECORD_PAYLOAD_KEY_SET.has(key)) return false
142
+ }
143
+ return true
144
+ }
145
+
146
+ function isMeshRecordRequest(value: unknown): value is MeshRecordRequest {
147
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'ledgerKind', 'nodeId', 'sessionId', 'taskId', 'payload'])
148
+ && value.v === TURN_IPC_PROTOCOL_VERSION
149
+ && isEvidenceIdentifier(value.meshId)
150
+ && isEvidenceIdentifier(value.ledgerKind)
151
+ && isOptionalId(value.nodeId) && isOptionalId(value.sessionId) && isOptionalId(value.taskId)
152
+ && isMeshRecordPayload(value.payload)
153
+ }
154
+
155
+ export function decodeMeshRecordRequest(value: unknown): MeshRecordRequest | null {
156
+ return isMeshRecordRequest(value) ? value : null
157
+ }
158
+
159
+ function isMeshRecordResponse(value: unknown): value is MeshRecordResponse {
160
+ return isRecord(value) && hasOnlyKeys(value, ['eventId', 'seq'])
161
+ && isEvidenceIdentifier(value.eventId) && isNonNegativeInt(value.seq)
162
+ }
163
+
164
+ export function decodeMeshRecordResponse(value: unknown): MeshRecordResponse | null {
165
+ return isMeshRecordResponse(value) ? value : null
166
+ }
167
+
168
+ // ─── turn_cancel ────────────────────────────────────────────────────────────
169
+
170
+ export interface TurnCancelRequest {
171
+ v: typeof TURN_IPC_PROTOCOL_VERSION
172
+ /** Exactly one of the two must be present — the decoder rejects both-or-neither. */
173
+ attemptId?: string
174
+ taskId?: string
175
+ reason: TurnReason
176
+ }
177
+
178
+ export interface TurnCancelResponse {
179
+ attemptRef: TurnAttemptRef
180
+ verdict: TurnObserveVerdict
181
+ }
182
+
183
+ function isTurnCancelRequest(value: unknown): value is TurnCancelRequest {
184
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'attemptId', 'taskId', 'reason'])) return false
185
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
186
+ const hasAttempt = value.attemptId !== undefined
187
+ const hasTask = value.taskId !== undefined
188
+ if (hasAttempt === hasTask) return false // exactly one of the two
189
+ if (hasAttempt && !isEvidenceIdentifier(value.attemptId)) return false
190
+ if (hasTask && !isEvidenceIdentifier(value.taskId)) return false
191
+ return isTurnReason(value.reason)
192
+ }
193
+
194
+ export function decodeTurnCancelRequest(value: unknown): TurnCancelRequest | null {
195
+ return isTurnCancelRequest(value) ? value : null
196
+ }
197
+
198
+ function isTurnCancelResponse(value: unknown): value is TurnCancelResponse {
199
+ return isRecord(value) && hasOnlyKeys(value, ['attemptRef', 'verdict'])
200
+ && isTurnAttemptRefValue(value.attemptRef) && isTurnObserveVerdict(value.verdict)
201
+ }
202
+
203
+ export function decodeTurnCancelResponse(value: unknown): TurnCancelResponse | null {
204
+ return isTurnCancelResponse(value) ? value : null
205
+ }
206
+
207
+ // ─── operator_status ────────────────────────────────────────────────────────
208
+
209
+ /**
210
+ * Fire-and-forget operator/tool-call status record — replaces
211
+ * `recordMeshCoordinatorToolCall` (10 sites, `mesh_tool_call_log`) and doubles
212
+ * as the `operator_status` evidence kind's wire shape.
213
+ *
214
+ * NAMING NOTE (flagged for the C-W6 implementer, see report §"mismatch"):
215
+ * the C-W6 brief's DELIVERABLES describes this request as
216
+ * `{taskId, status: MeshTaskStatus, reason}`, but C1's `operator_status`
217
+ * evidence kind (`turn-evidence.ts` `OPERATOR_STATUSES`) is the closed
218
+ * 2-value set `'completed' | 'failed'` — a verdict on an operator ACTION, not
219
+ * a `MeshTaskStatus` (5-value queue-row status: pending/assigned/completed/
220
+ * failed/cancelled). This contract follows `turn-evidence.ts` because that is
221
+ * what the C1 reducer actually consumes for this evidence kind; using the
222
+ * 5-value `MeshTaskStatus` here would let this request carry `pending` or
223
+ * `assigned`, which the reducer has no transition for.
224
+ */
225
+ export const OPERATOR_STATUS_VALUES = ['completed', 'failed'] as const
226
+ export type OperatorStatusValue = typeof OPERATOR_STATUS_VALUES[number]
227
+ const isOperatorStatusValue = makeGuard(OPERATOR_STATUS_VALUES)
228
+
229
+ export const OPERATOR_STATUS_REASON_VALUES = ['operator_update', 'refine_terminal', 'validation_terminal'] as const
230
+ export type OperatorStatusReasonValue = typeof OPERATOR_STATUS_REASON_VALUES[number]
231
+ const isOperatorStatusReasonValue = makeGuard(OPERATOR_STATUS_REASON_VALUES)
232
+
233
+ export interface OperatorStatusRequest {
234
+ v: typeof TURN_IPC_PROTOCOL_VERSION
235
+ taskId: string
236
+ status: OperatorStatusValue
237
+ reason: OperatorStatusReasonValue
238
+ }
239
+
240
+ /** Acknowledgement only — this command is fire-and-forget by design (the daemon owns rate-limiting the underlying log). */
241
+ export interface OperatorStatusResponse {
242
+ accepted: true
243
+ }
244
+
245
+ function isOperatorStatusRequest(value: unknown): value is OperatorStatusRequest {
246
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'taskId', 'status', 'reason'])
247
+ && value.v === TURN_IPC_PROTOCOL_VERSION
248
+ && isEvidenceIdentifier(value.taskId)
249
+ && isOperatorStatusValue(value.status)
250
+ && isOperatorStatusReasonValue(value.reason)
251
+ }
252
+
253
+ export function decodeOperatorStatusRequest(value: unknown): OperatorStatusRequest | null {
254
+ return isOperatorStatusRequest(value) ? value : null
255
+ }
256
+
257
+ function isOperatorStatusResponse(value: unknown): value is OperatorStatusResponse {
258
+ return isRecord(value) && hasOnlyKeys(value, ['accepted']) && value.accepted === true
259
+ }
260
+
261
+ export function decodeOperatorStatusResponse(value: unknown): OperatorStatusResponse | null {
262
+ return isOperatorStatusResponse(value) ? value : null
263
+ }
264
+
265
+ // ─── turn_query ─────────────────────────────────────────────────────────────
266
+
267
+ /** Row shape returned by `turn_query` — scalar projection of a `turn_attempts` row (C3 schema), never `data_json`/`payload_json` content. */
268
+ export interface TurnQueryAttemptRow {
269
+ attemptId: string
270
+ generation: number
271
+ meshId?: string
272
+ taskId?: string
273
+ sessionId: string
274
+ nodeId?: string
275
+ providerType?: string
276
+ state: string
277
+ terminalOutcome?: TurnOutcome
278
+ terminalReason?: TurnReason
279
+ acceptedAt: number
280
+ terminalAt?: number
281
+ }
282
+
283
+ /** Row shape returned by `turn_query` for the event stream — scalar projection of a `turn_events` row, `payload_json` NEVER included. */
284
+ export interface TurnQueryEventRow {
285
+ eventId: string
286
+ attemptId?: string
287
+ generation?: number
288
+ sessionId: string
289
+ kind: TurnEvidenceKind
290
+ source: string
291
+ verdict: TurnObserveVerdict | 'forwarded'
292
+ atMs: number
293
+ }
294
+
295
+ export interface TurnQueryRequest {
296
+ v: typeof TURN_IPC_PROTOCOL_VERSION
297
+ meshId: string
298
+ taskId?: string
299
+ attemptId?: string
300
+ sessionId?: string
301
+ state?: string
302
+ since?: number
303
+ tail?: number
304
+ }
305
+
306
+ export interface TurnQueryResponse {
307
+ attempts: readonly TurnQueryAttemptRow[]
308
+ events: readonly TurnQueryEventRow[]
309
+ /** C7-5: set while `mesh.<id>.events` replication is behind for this mesh — the caller must not treat a fleet-scope answer as complete. */
310
+ replicationPending?: boolean
311
+ }
312
+
313
+ function isTurnQueryRequest(value: unknown): value is TurnQueryRequest {
314
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'taskId', 'attemptId', 'sessionId', 'state', 'since', 'tail'])
315
+ && value.v === TURN_IPC_PROTOCOL_VERSION
316
+ && isEvidenceIdentifier(value.meshId)
317
+ && isOptionalId(value.taskId) && isOptionalId(value.attemptId) && isOptionalId(value.sessionId)
318
+ && (value.state === undefined || isEvidenceIdentifier(value.state))
319
+ && (value.since === undefined || isFiniteNumber(value.since))
320
+ && (value.tail === undefined || isNonNegativeInt(value.tail))
321
+ }
322
+
323
+ export function decodeTurnQueryRequest(value: unknown): TurnQueryRequest | null {
324
+ return isTurnQueryRequest(value) ? value : null
325
+ }
326
+
327
+ const TURN_QUERY_ATTEMPT_ROW_KEYS = [
328
+ 'attemptId', 'generation', 'meshId', 'taskId', 'sessionId', 'nodeId', 'providerType',
329
+ 'state', 'terminalOutcome', 'terminalReason', 'acceptedAt', 'terminalAt',
330
+ ] as const
331
+
332
+ function isTurnQueryAttemptRow(value: unknown): value is TurnQueryAttemptRow {
333
+ if (!isRecord(value) || !hasOnlyKeys(value, TURN_QUERY_ATTEMPT_ROW_KEYS)) return false
334
+ if (!isEvidenceIdentifier(value.attemptId) || !isNonNegativeInt(value.generation)) return false
335
+ if (!isOptionalId(value.meshId) || !isOptionalId(value.taskId)) return false
336
+ if (!isEvidenceIdentifier(value.sessionId)) return false
337
+ if (!isOptionalId(value.nodeId) || !isOptionalId(value.providerType)) return false
338
+ if (typeof value.state !== 'string') return false
339
+ if (value.terminalOutcome !== undefined && !isTurnOutcomeValue(value.terminalOutcome)) return false
340
+ if (value.terminalReason !== undefined && !isTurnReason(value.terminalReason)) return false
341
+ if (!isFiniteNumber(value.acceptedAt)) return false
342
+ if (value.terminalAt !== undefined && !isFiniteNumber(value.terminalAt)) return false
343
+ return true
344
+ }
345
+
346
+ const TURN_QUERY_EVENT_VERDICTS = ['applied', 'recorded', 'rejected', 'forwarded'] as const
347
+ const TURN_QUERY_EVENT_ROW_KEYS = ['eventId', 'attemptId', 'generation', 'sessionId', 'kind', 'source', 'verdict', 'atMs'] as const
348
+
349
+ function isTurnQueryEventRow(value: unknown): value is TurnQueryEventRow {
350
+ if (!isRecord(value) || !hasOnlyKeys(value, TURN_QUERY_EVENT_ROW_KEYS)) return false
351
+ if (!isEvidenceIdentifier(value.eventId)) return false
352
+ if (!isOptionalId(value.attemptId)) return false
353
+ if (value.generation !== undefined && !isNonNegativeInt(value.generation)) return false
354
+ if (!isEvidenceIdentifier(value.sessionId)) return false
355
+ if (typeof value.kind !== 'string') return false
356
+ if (typeof value.source !== 'string') return false
357
+ if (!(TURN_QUERY_EVENT_VERDICTS as readonly string[]).includes(value.verdict as string)) return false
358
+ if (!isFiniteNumber(value.atMs)) return false
359
+ return true
360
+ }
361
+
362
+ function isTurnQueryResponse(value: unknown): value is TurnQueryResponse {
363
+ if (!isRecord(value) || !hasOnlyKeys(value, ['attempts', 'events', 'replicationPending'])) return false
364
+ if (!Array.isArray(value.attempts) || !value.attempts.every(isTurnQueryAttemptRow)) return false
365
+ if (!Array.isArray(value.events) || !value.events.every(isTurnQueryEventRow)) return false
366
+ if (value.replicationPending !== undefined && typeof value.replicationPending !== 'boolean') return false
367
+ return true
368
+ }
369
+
370
+ export function decodeTurnQueryResponse(value: unknown): TurnQueryResponse | null {
371
+ return isTurnQueryResponse(value) ? value : null
372
+ }
373
+
374
+ // ─── mesh_index_query ───────────────────────────────────────────────────────
375
+
376
+ export const MESH_INDEX_QUERY_WRITER_SCOPES = ['own', 'fleet'] as const
377
+ export type MeshIndexQueryWriterScope = typeof MESH_INDEX_QUERY_WRITER_SCOPES[number]
378
+ const isMeshIndexQueryWriterScope = makeGuard(MESH_INDEX_QUERY_WRITER_SCOPES)
379
+
380
+ export interface MeshIndexQueryRequest {
381
+ v: typeof TURN_IPC_PROTOCOL_VERSION
382
+ meshId: string
383
+ ledgerKind?: string
384
+ sessionId?: string
385
+ taskId?: string
386
+ writer?: MeshIndexQueryWriterScope
387
+ since?: number
388
+ tail?: number
389
+ }
390
+
391
+ /** Row shape returned by `mesh_index_query` — a `mesh_topic_index` row (C3 schema). */
392
+ export interface MeshTopicIndexRow {
393
+ writer: string
394
+ seq: number
395
+ meshId: string
396
+ eventId: string
397
+ kind: string
398
+ ledgerKind?: string
399
+ taskId?: string
400
+ sessionId?: string
401
+ nodeId?: string
402
+ atMs: number
403
+ payload: ProjectedScalars
404
+ }
405
+
406
+ export interface MeshIndexQueryResponse {
407
+ rows: readonly MeshTopicIndexRow[]
408
+ /** C7-5: true while `mesh.<id>.events` replication is behind for this mesh. */
409
+ replicationPending?: boolean
410
+ }
411
+
412
+ function isMeshIndexQueryRequest(value: unknown): value is MeshIndexQueryRequest {
413
+ return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'ledgerKind', 'sessionId', 'taskId', 'writer', 'since', 'tail'])
414
+ && value.v === TURN_IPC_PROTOCOL_VERSION
415
+ && isEvidenceIdentifier(value.meshId)
416
+ && isOptionalId(value.ledgerKind) && isOptionalId(value.sessionId) && isOptionalId(value.taskId)
417
+ && (value.writer === undefined || isMeshIndexQueryWriterScope(value.writer))
418
+ && (value.since === undefined || isFiniteNumber(value.since))
419
+ && (value.tail === undefined || isNonNegativeInt(value.tail))
420
+ }
421
+
422
+ export function decodeMeshIndexQueryRequest(value: unknown): MeshIndexQueryRequest | null {
423
+ return isMeshIndexQueryRequest(value) ? value : null
424
+ }
425
+
426
+ const MESH_TOPIC_INDEX_ROW_KEYS = [
427
+ 'writer', 'seq', 'meshId', 'eventId', 'kind', 'ledgerKind', 'taskId', 'sessionId', 'nodeId', 'atMs', 'payload',
428
+ ] as const
429
+
430
+ function isMeshTopicIndexRow(value: unknown): value is MeshTopicIndexRow {
431
+ if (!isRecord(value) || !hasOnlyKeys(value, MESH_TOPIC_INDEX_ROW_KEYS)) return false
432
+ if (!isEvidenceIdentifier(value.writer) || !isNonNegativeInt(value.seq)) return false
433
+ if (!isEvidenceIdentifier(value.meshId) || !isEvidenceIdentifier(value.eventId)) return false
434
+ if (typeof value.kind !== 'string') return false
435
+ if (!isOptionalId(value.ledgerKind) || !isOptionalId(value.taskId) || !isOptionalId(value.sessionId) || !isOptionalId(value.nodeId)) return false
436
+ if (!isFiniteNumber(value.atMs)) return false
437
+ // Rows replay mesh.record entries, so the payload is held to the SAME
438
+ // allow-list as a mesh_record request — not just "any scalar map" — to
439
+ // stop a fleet-wide read from surfacing a key that was never reviewed.
440
+ if (!isMeshRecordPayload(value.payload)) return false
441
+ return true
442
+ }
443
+
444
+ export function isMeshIndexQueryResponse(value: unknown): value is MeshIndexQueryResponse {
445
+ if (!isRecord(value) || !hasOnlyKeys(value, ['rows', 'replicationPending'])) return false
446
+ if (!Array.isArray(value.rows) || !value.rows.every(isMeshTopicIndexRow)) return false
447
+ if (value.replicationPending !== undefined && typeof value.replicationPending !== 'boolean') return false
448
+ return true
449
+ }
450
+
451
+ export function decodeMeshIndexQueryResponse(value: unknown): MeshIndexQueryResponse | null {
452
+ return isMeshIndexQueryResponse(value) ? value : null
453
+ }