@adhdev/mesh-shared 1.0.60-rc.9 → 1.0.60
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,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
|
+
}
|