@adhdev/mesh-shared 1.0.60-rc.8 → 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.
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,137 @@
1
+ /**
2
+ * turn-ipc — typed IPC command contracts for the six commands that replace
3
+ * mcp-server's direct `mesh-runtime.db` access (design §5 C2 "MCP server").
4
+ *
5
+ * Wiring-unification Phase C, workstream C-W6
6
+ * (docs/design/2026-09-23-wiring-unification.md §5 C2/C3, C9 row C-W6).
7
+ *
8
+ * WHY THIS FILE EXISTS
9
+ * ---------------------
10
+ * mcp-server is a second, in-process writer of `mesh-runtime.db` (~45-69
11
+ * direct-mutation call sites, ~54 reads, per the C-W6 brief). C2 moves every
12
+ * one of those call sites behind IPC, executed in the daemon that owns the
13
+ * turn ledger and the seqscribe node, so mcp-server never opens the SQLite
14
+ * file itself. This file is the WIRE CONTRACT for that IPC surface — request/
15
+ * response shapes, closed enums, runtime decoders — shared by the daemon-core
16
+ * command handlers (added with C-W6 proper; NOT this file) and the mcp-server
17
+ * client (`mcp-server/src/ipc/turn-commands.ts`, this workstream).
18
+ *
19
+ * The six commands (§5 C2):
20
+ * - turn_observe — submit one TurnEvidence; returns the reducer verdict.
21
+ * - mesh_record — append a content-free scalar record (`mesh.record`
22
+ * topic entry), replacing `appendLedgerEntry`.
23
+ * - turn_cancel — cancel an attempt (`cancel` evidence, source `mcp_probe`
24
+ * or `operator`).
25
+ * - operator_status — record a fire-and-forget operator/tool-call status
26
+ * (`operator_status` evidence + the former
27
+ * `recordMeshCoordinatorToolCall` log write).
28
+ * - turn_query — read attempt/event rows (own or fleet scope).
29
+ * - mesh_index_query — read `mesh_topic_index` rows (fleet-wide, replacing
30
+ * the in-memory read model / parity reads).
31
+ *
32
+ * CONTENT BOUNDARY
33
+ * -----------------
34
+ * Every request/response field is an identifier, a closed enum, a boolean, a
35
+ * counter or a timestamp. `turn_observe` carries a `TurnEvidence`, which is
36
+ * content-free by construction (`turn-evidence.ts`). `mesh_record` carries a
37
+ * `ProjectedScalars` payload restricted to the SAME allow-list keys
38
+ * `seqscribe/mesh-event-projection.ts` already reviews and ships
39
+ * (`PROJECTED_PAYLOAD_KEYS`, 35 keys) — this file pins a STRUCTURAL COPY of
40
+ * that key list (`MESH_RECORD_PAYLOAD_KEYS`) so mesh-shared does not import
41
+ * daemon-core (leaf constraint), with a test asserting the two lists agree.
42
+ * Summaries, report bodies, progress notes and modal text never travel in any
43
+ * of these payloads — they go to the content-class `mesh.<id>.handoff` topic
44
+ * and are referenced by `SummaryRef {topic, writer, seq}` (already declared
45
+ * in `./turn-evidence.ts`).
46
+ *
47
+ * VERSIONING
48
+ * -----------
49
+ * Every request carries `v: 1` so a future incompatible change can be
50
+ * detected by the responder before it tries to interpret the args.
51
+ *
52
+ * LAYOUT
53
+ * -------
54
+ * This module holds the error codes and the command registry; the per-command
55
+ * contracts live in ./turn-ipc-{turn,mission,queue,stats}.ts (re-exported here)
56
+ * over the shared guard primitives in ./turn-ipc-guards.ts.
57
+ */
58
+
59
+ import { isRecord } from './protocol/envelope'
60
+ import { hasOnlyKeys, makeGuard } from './turn-ipc-guards';
61
+
62
+ export { TURN_IPC_PROTOCOL_VERSION } from './turn-ipc-guards';
63
+ export * from './turn-ipc-turn';
64
+ export * from './turn-ipc-mission';
65
+ export * from './turn-ipc-queue';
66
+ export * from './turn-ipc-stats';
67
+
68
+ // ─── error codes ────────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Closed error-code union every command response may carry instead of (or in
72
+ * addition to, for the client-side wrapper) a result.
73
+ *
74
+ * daemon_required — no daemon reachable (no IPC/local transport
75
+ * connection could be established). Distinct from
76
+ * every other failure: it means "there is nobody to
77
+ * ask", not "the ask failed."
78
+ * turn_ledger_unavailable — a daemon answered but its turn ledger is not
79
+ * armed yet (mid-boot, mid-migration).
80
+ * ledger_not_owner — the daemon that answered does not own the
81
+ * attempt/mesh addressed by the request (routing
82
+ * mistake, or the owner changed after a reclaim).
83
+ */
84
+ export const TURN_IPC_ERROR_CODES = ['daemon_required', 'turn_ledger_unavailable', 'ledger_not_owner'] as const
85
+ export type TurnIpcErrorCode = typeof TURN_IPC_ERROR_CODES[number]
86
+
87
+ export const isTurnIpcErrorCode = makeGuard(TURN_IPC_ERROR_CODES)
88
+
89
+ /** Structured failure shape a responder returns instead of a result. Never thrown across the wire — the client maps it to a typed error (see mcp-server/src/ipc/turn-commands.ts). */
90
+ export interface TurnIpcError {
91
+ code: TurnIpcErrorCode
92
+ /** Short machine-oriented detail, e.g. which daemon was expected. Never free text describing content. */
93
+ detail?: string
94
+ }
95
+
96
+ export function isTurnIpcError(value: unknown): value is TurnIpcError {
97
+ return isRecord(value) && hasOnlyKeys(value, ['code', 'detail']) && isTurnIpcErrorCode(value.code)
98
+ && (value.detail === undefined || typeof value.detail === 'string')
99
+ }
100
+
101
+ // ─── command registry ───────────────────────────────────────────────────────
102
+
103
+ /** The complete, closed set of IPC command names this contract defines. */
104
+ export const TURN_IPC_COMMANDS = [
105
+ 'turn_observe',
106
+ 'mesh_record',
107
+ 'turn_cancel',
108
+ 'operator_status',
109
+ 'turn_query',
110
+ 'mesh_index_query',
111
+ 'mission_upsert',
112
+ 'mission_query',
113
+ 'note_upsert',
114
+ 'note_forget',
115
+ // C-W9b additions (2026-09-24 14:00 stamp "C-W9"):
116
+ 'tool_call_record',
117
+ 'ledger_query',
118
+ 'mission_list_query',
119
+ // C-W9a additions (the event ledger retired; the queue behind IPC):
120
+ 'record_local',
121
+ 'queue_query',
122
+ 'queue_enqueue',
123
+ 'queue_enqueue_batch',
124
+ 'queue_cancel',
125
+ 'queue_requeue',
126
+ 'direct_dispatch_record',
127
+ 'active_work_query',
128
+ 'recovery_context_query',
129
+ // C-W9c additions (2026-09-24 19:00 stamp — the last mcp-server in-process
130
+ // daemon-core paths: mission reads, task/mission stats, orphaned-pin
131
+ // helpers, one prune audit):
132
+ 'task_stats_query',
133
+ 'prune_stale_direct',
134
+ 'orphaned_pin_notify',
135
+ ] as const
136
+ export type TurnIpcCommand = typeof TURN_IPC_COMMANDS[number]
137
+ export const isTurnIpcCommand = makeGuard(TURN_IPC_COMMANDS)
package/src/types.ts CHANGED
@@ -153,4 +153,12 @@ export interface RepoMeshSessionStatus {
153
153
  startedAt?: string | null
154
154
  lastActivityAt?: string | null
155
155
  isCached?: boolean
156
+ /**
157
+ * Worker MCP delivery outcome for this session's launch (present only for a
158
+ * mesh-delegated launch). `reason` is a short enum code, never free text —
159
+ * see `WORKER_MCP_DELIVERY_REASONS` (daemon-core's `mesh/worker-mcp-isolation.ts`,
160
+ * the source of the vocabulary this field's values are drawn from; not
161
+ * re-exported here so mesh-shared stays a dependency-free leaf).
162
+ */
163
+ workerMcp?: { delivered: boolean; reason?: string }
156
164
  }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * worker-protocol — what a delegated worker is told, and what it may call.
3
+ *
4
+ * Wiring-unification Phase F1 (docs/design/2026-09-23-wiring-unification.md §7b).
5
+ *
6
+ * Measured 2026-09-23 on the live preview ledger: 635 worker attempts in 14
7
+ * days, 3 `report_completion` calls. The worker MCP was delivered to every
8
+ * provider and never used, because nothing ever told the worker it existed —
9
+ * the dispatched task body carried no instruction and the coordinator prompt
10
+ * did not mention the worker tools at all. The only text a worker saw about
11
+ * its tools was the tool description.
12
+ *
13
+ * This module is the single source for BOTH sides of that contract:
14
+ * - `WORKER_TOOLS` — exactly what `adhdev mcp --mode worker` advertises
15
+ * (a parity test in mcp-server pins ListTools to this tuple);
16
+ * - `renderWorkerProtocolFooter()` — the block appended to EVERY dispatched
17
+ * task body, so a worker learns the protocol from the task itself;
18
+ * - `renderCoordinatorWorkerSection()` — the matching paragraph the
19
+ * coordinator prompt carries, so the coordinator expects structured reports
20
+ * and stops polling for them.
21
+ *
22
+ * Wiring-unification Phase H2 adds an optional mission-brief block: when a
23
+ * dispatched task belongs to a mission that carries a `MissionBrief`
24
+ * (`./mission-brief.ts`), `renderWorkerProtocolFooter` renders it ABOVE the
25
+ * `WORKER_PROTOCOL_FOOTER_MARKER` line. This is deliberate placement, not an
26
+ * accident of insertion order: `hasWorkerProtocolFooter` / `stripWorkerProtocolFooter`
27
+ * both key off the marker line alone, so a body with a brief block strips
28
+ * identically to one without — the brief is authored content from the
29
+ * coordinator's point of view (like the task message itself), never part of
30
+ * the protocol contract the marker demarcates.
31
+ */
32
+
33
+ import { renderMissionBriefBlock, type MissionBrief } from './mission-brief'
34
+ import { isMeshTaskDifficulty } from './brain-routing'
35
+
36
+ export const WORKER_TOOLS = [
37
+ 'report_completion',
38
+ 'progress_update',
39
+ 'peer_context_pull',
40
+ 'git_status',
41
+ 'git_log',
42
+ 'git_diff',
43
+ ] as const
44
+
45
+ export type WorkerTool = typeof WORKER_TOOLS[number]
46
+
47
+ const WORKER_TOOL_SET: ReadonlySet<string> = new Set(WORKER_TOOLS)
48
+
49
+ export function isWorkerTool(value: unknown): value is WorkerTool {
50
+ return typeof value === 'string' && WORKER_TOOL_SET.has(value)
51
+ }
52
+
53
+ /** Stable marker line so a body is never footered twice and tests can find it. */
54
+ export const WORKER_PROTOCOL_FOOTER_MARKER = '--- adhdev worker protocol ---'
55
+
56
+ export interface WorkerProtocolFooterInput {
57
+ taskId?: string
58
+ taskMode?: string
59
+ difficulty?: string
60
+ readonly?: boolean
61
+ /** Number of handoff notes enclosed above the footer, when any. */
62
+ enclosedHandoffNotes?: number
63
+ /** H2: the owning mission's brief, when this task belongs to one. Rendered above the marker line — see module doc. */
64
+ missionBrief?: MissionBrief
65
+ }
66
+
67
+ export function hasWorkerProtocolFooter(body: string): boolean {
68
+ return typeof body === 'string' && body.includes(WORKER_PROTOCOL_FOOTER_MARKER)
69
+ }
70
+
71
+ /**
72
+ * The protocol block appended to a dispatched task. Written for the model that
73
+ * receives it: imperative, short, and explicit about what is NOT available so
74
+ * the worker does not go looking for coordinator tools.
75
+ */
76
+ export function renderWorkerProtocolFooter(input: WorkerProtocolFooterInput = {}): string {
77
+ const briefBlock = input.missionBrief ? renderMissionBriefBlock(input.missionBrief) : null
78
+ const lines: string[] = briefBlock ? [briefBlock, '', WORKER_PROTOCOL_FOOTER_MARKER] : [WORKER_PROTOCOL_FOOTER_MARKER]
79
+ const scope: string[] = []
80
+ if (input.taskId) scope.push(`task ${input.taskId}`)
81
+ if (input.taskMode) scope.push(`mode ${input.taskMode}`)
82
+ // MCP-usage-audit item 2: validate against the canonical difficulty set
83
+ // (this package's own brain-routing vocabulary — never duplicated here)
84
+ // rather than rendering an internal caller's typo or stale value verbatim.
85
+ // The MCP tool-schema enum already blocks a bad value AT THE TOOLS; this
86
+ // covers internal callers that can still construct a task with one.
87
+ if (input.difficulty) {
88
+ if (isMeshTaskDifficulty(input.difficulty)) {
89
+ scope.push(`difficulty ${input.difficulty}`)
90
+ } else {
91
+ // eslint-disable-next-line no-console -- mesh-shared is a dependency-free
92
+ // leaf with no logger abstraction; this is the one place that matters.
93
+ console.warn(
94
+ `[worker-protocol] task ${input.taskId ?? '(unknown)'}: dropping invalid difficulty '${input.difficulty}' `
95
+ + `from the worker footer (expected one of easy, medium, difficult, freeform)`,
96
+ )
97
+ }
98
+ }
99
+ if (input.readonly) scope.push('read-only')
100
+ if (scope.length) lines.push(`You are a delegated worker (${scope.join(', ')}).`)
101
+ else lines.push('You are a delegated worker.')
102
+ lines.push(
103
+ 'When your work is finished, blocked, or has failed, call `report_completion` exactly once. '
104
+ + 'Its `summary` is recorded verbatim as the authoritative record of this task — your terminal is not '
105
+ + 'scraped for it — so state what you did, what you found, and where you left the branch.',
106
+ 'For work that runs longer than a few minutes, call `progress_update` at natural checkpoints so the '
107
+ + 'coordinator can see you are alive without polling.',
108
+ '`peer_context_pull` shows what sibling tasks in this mission have reported; `git_status` / `git_diff` / '
109
+ + '`git_log` inspect your own workspace — all three REQUIRE the absolute path to it as `workspace`.',
110
+ 'You have no coordinator tools (no `mesh_*`) by design: do not enqueue, dispatch, or restart anything. '
111
+ + 'If you need a decision from the coordinator, finish with `report_completion` and outcome `blocked`, '
112
+ + 'listing what you need in `blockers`.',
113
+ 'On a code-changing task, `report_completion` with outcome `completed` requires `touched_files` — send '
114
+ + '`[]` if you changed nothing, omitting the field is what gets refused. `blocked`/`failed` never need it, '
115
+ + 'and a read-only task should omit it or send `[]`.',
116
+ 'If `report_completion` is refused, the response carries `validationErrors` (or a `hint`) naming exactly '
117
+ + 'what to fix — correct that field and call it again; a refusal records nothing.',
118
+ )
119
+ if (input.enclosedHandoffNotes && input.enclosedHandoffNotes > 0) {
120
+ lines.push(
121
+ `The ${input.enclosedHandoffNotes} handoff note(s) above were written by agents who touched this code before you; `
122
+ + 'honour their conflict guidance and add your own in `handoff_notes` when you report.',
123
+ )
124
+ }
125
+ return lines.join('\n')
126
+ }
127
+
128
+ /**
129
+ * The authored text of a dispatched body: everything before the footer marker.
130
+ * Ledger descriptors, replay fixtures and tests compare authored text, never
131
+ * the protocol block.
132
+ */
133
+ export function stripWorkerProtocolFooter(body: string): string {
134
+ if (typeof body !== 'string') return body
135
+ const at = body.indexOf(WORKER_PROTOCOL_FOOTER_MARKER)
136
+ if (at < 0) return body
137
+ return body.slice(0, at).replace(/\s+$/, '')
138
+ }
139
+
140
+ /**
141
+ * Append the footer to a body that does not carry one yet. Idempotent.
142
+ */
143
+ export function appendWorkerProtocolFooter(body: string, input: WorkerProtocolFooterInput = {}): string {
144
+ if (hasWorkerProtocolFooter(body)) return body
145
+ const trimmed = body.replace(/\s+$/, '')
146
+ return `${trimmed}\n\n${renderWorkerProtocolFooter(input)}`
147
+ }
148
+
149
+ /**
150
+ * The paragraph the COORDINATOR prompt carries about its workers. Kept next to
151
+ * the footer so the two halves of the protocol cannot drift apart.
152
+ */
153
+ export function renderCoordinatorWorkerSection(): string {
154
+ return [
155
+ '## Workers',
156
+ '',
157
+ `Every task you dispatch is delivered with a worker protocol footer. Workers hold exactly these tools: ${WORKER_TOOLS.map((tool) => `\`${tool}\``).join(', ')} — and no \`mesh_*\` tools.`,
158
+ '- A worker finishes by calling `report_completion`; its structured report (outcome, summary, touched files, branch state, handoff notes) is what reaches you as the completion event. Read that report — do not re-derive the outcome from `mesh_read_chat` or the terminal unless the report is missing.',
159
+ '- `progress_update` from a worker arrives as a lightweight progress note. Do NOT poll `mesh_status`, `mesh_view_queue` or `mesh_read_chat` to check on a running worker; completion, progress, blocked and failure all arrive as events.',
160
+ '- To reach a busy worker mid-task use `mesh_notify_worker`: the memo is delivered on the worker\'s next tool call (or when it becomes idle). A worker that reports `blocked` is asking you for a decision — answer it with `mesh_send_task` to the same session.',
161
+ '- `mesh_status` is for node health and capacity before delegating, never for progress.',
162
+ '- A `code_change` task\'s declared `owned_paths` (H1) is enforced at claim time: a second `code_change` task whose `owned_paths` overlaps an already-claimed task\'s is refused rather than silently racing it. A worker\'s `report_completion.touched_files` is compared against its own task\'s declaration afterward and any mismatch is surfaced back to you as evidence — it is never a validation failure, since a worker often cannot know its exact final file list at enqueue time.',
163
+ ].join('\n')
164
+ }
@@ -1,107 +1,112 @@
1
1
  /**
2
- * ws-protocol — shared string-literal unions for the daemon↔server WS surface
3
- * and the daemon↔dashboard P2P DataChannel surface.
2
+ * ws-protocol — name-only aliases for the daemon↔server WS surface and the
3
+ * daemon↔dashboard P2P DataChannel surface, plus the `auth_ok.payload.limits`
4
+ * contract.
4
5
  *
5
- * Fragmentation audit: these message types existed as a TypeScript union in
6
- * exactly ONE package (the proprietary daemon-cloud's server-connection.ts),
7
- * which is a leaf CONSUMER — the other two participants (the Workers server
8
- * and OSS daemon-core, which is the primary `status_report` producer) matched
9
- * bare string literals by hand. Renaming `auth_ok` server-side would compile
10
- * everywhere and leave the daemon reconnecting forever. Pure literals, zero
11
- * runtime deps — the textbook mesh-shared leaf.
12
- *
13
- * SCOPE HONESTY: this file declares the OSS-visible protocol surface. The
14
- * proprietary repo's server-connection.ts remains the authority for the full
15
- * ServerToDaemon command set; it should adopt these unions and extend them
16
- * (`ServerToDaemonMsg | <proprietary extras>`) rather than re-declaring the
17
- * shared members. Members here are the ones OSS daemon-core itself produces
18
- * or matches.
6
+ * Wiring-unification Phase A2: the typed frame unions now live in ./protocol
7
+ * (one file per link, payloads included). This file keeps the symbols that
8
+ * pre-dated them — `DaemonToServerWsMsg`, `ServerToDaemonWsMsg`,
9
+ * `P2PSignalingWsMsg`, `DashboardP2PMessageKind` and the two name arrays —
10
+ * DERIVED from the protocol unions rather than re-declared, so they cannot
11
+ * drift again. Before this derivation the list here said `offer|answer|ice`
12
+ * while the wire said `p2p_offer|p2p_answer|p2p_ice`, declared a `log` frame
13
+ * nothing sent or handled, omitted `daemon_mesh_command` (sent by a raw
14
+ * `ws.send`), and listed 12 of the ~27 P2P kinds — and nothing imported the
15
+ * drifted members, so nothing noticed.
19
16
  */
20
17
 
21
- /** Messages the daemon sends UP to the Workers server over the WS bridge. */
22
- export type DaemonToServerWsMsg =
23
- | 'auth'
24
- | 'status_report'
25
- | 'status_heartbeat'
26
- | 'status_event'
27
- | 'command_result'
28
- | 'error'
29
- | 'agent_event'
30
- | 'log'
31
- /**
32
- * seqscribe Beacon vectors (design §7.1). ONE daemon-initiated frame carries
33
- * both directions — `op: 'put'` stores this node's content-free vector
34
- * report, `op: 'get'` asks for the board — because the server has no way to
35
- * wake itself: `DaemonConnectionDO` has neither an alarm nor a timer, and
36
- * adding one would hit the most request-quota-pressured axis in the system.
37
- */
38
- | 'beacon_vectors';
39
-
40
- /** Server→daemon control messages the OSS engine reacts to. */
41
- export type ServerToDaemonWsMsg =
42
- | 'auth_ok'
43
- | 'auth_error'
44
- | 'machine_evicted'
45
- | 'force_disconnect'
46
- | 'token_revoked'
47
- | 'version_mismatch'
48
- | 'force_update_required'
49
- | 'command'
50
- | 'agent_command'
51
- | 'resolve_action'
52
- /**
53
- * Reply to a `beacon_vectors` GET, correlated by `requestId`. A PUT is
54
- * fire-and-forget and gets no reply at all — the beacon is advisory, so a
55
- * lost report costs one debounce cycle of prediction accuracy and nothing
56
- * else, which is not worth an ack round trip.
57
- */
58
- | 'beacon_vectors_result';
18
+ import {
19
+ DAEMON_TO_SERVER_TYPES,
20
+ SERVER_TO_DAEMON_CONTROL_TYPES,
21
+ type DaemonToServerType,
22
+ type ServerToDaemonControlType,
23
+ type P2PSignalType,
24
+ type MeshP2PSignalType,
25
+ type DashboardToDaemonP2PType,
26
+ type DaemonToDashboardP2PType,
27
+ } from './protocol';
59
28
 
60
- /** P2P signaling relayed through the server WS. */
61
- export type P2PSignalingWsMsg =
62
- | 'p2p_ready'
63
- | 'offer'
64
- | 'answer'
65
- | 'ice'
66
- | 'mesh_p2p_ready'
67
- | 'mesh_p2p_offer'
68
- | 'mesh_p2p_answer'
69
- | 'mesh_p2p_ice';
29
+ /** Messages the daemon sends UP to the Workers server over the WS bridge (see protocol/daemon-server.ts). */
30
+ export type DaemonToServerWsMsg = DaemonToServerType;
70
31
 
71
32
  /**
72
- * Dashboard↔daemon P2P DataChannel JSON message kinds. Previously matched as
73
- * hand-synced literals on both ends with NO shared symbol anywhere —
74
- * `p2p_evicted` had exactly two occurrences repo-wide (emit + handle).
33
+ * Server→daemon CONTROL messages (see protocol/daemon-server.ts). The server
34
+ * additionally relays an open command namespace as the frame `type`
35
+ * (`ServerDirectCommandMsg`), which is deliberately not a member here.
75
36
  */
76
- export type DashboardP2PMessageKind =
77
- | 'ping'
78
- | 'pong'
79
- | 'status_report'
80
- | 'status_event'
81
- | 'p2p_evicted'
82
- | 'command'
83
- | 'command_result'
84
- | 'command_result_chunk'
85
- | 'screenshot_start'
86
- | 'screenshot_stop'
87
- | 'pty_input'
88
- | 'pty_resize';
37
+ export type ServerToDaemonWsMsg = ServerToDaemonControlType;
38
+
39
+ /** P2P signaling relayed through the server WS — the `p2p_`/`mesh_p2p_` spellings the wire actually uses. */
40
+ export type P2PSignalingWsMsg = P2PSignalType | MeshP2PSignalType;
41
+
42
+ /** Every dashboard↔daemon P2P DataChannel JSON kind, both directions (see protocol/dashboard-daemon-p2p.ts). */
43
+ export type DashboardP2PMessageKind = DashboardToDaemonP2PType | DaemonToDashboardP2PType;
89
44
 
90
- export const DAEMON_TO_SERVER_WS_MSGS: readonly DaemonToServerWsMsg[] = [
91
- 'auth', 'status_report', 'status_heartbeat', 'status_event', 'command_result', 'error', 'agent_event', 'log',
92
- 'beacon_vectors',
93
- ];
45
+ export const DAEMON_TO_SERVER_WS_MSGS: readonly DaemonToServerWsMsg[] = DAEMON_TO_SERVER_TYPES;
94
46
 
95
- export const SERVER_TO_DAEMON_WS_MSGS: readonly ServerToDaemonWsMsg[] = [
96
- 'auth_ok', 'auth_error', 'machine_evicted', 'force_disconnect', 'token_revoked',
97
- 'version_mismatch', 'force_update_required', 'command', 'agent_command', 'resolve_action',
98
- 'beacon_vectors_result',
99
- ];
47
+ export const SERVER_TO_DAEMON_WS_MSGS: readonly ServerToDaemonWsMsg[] = SERVER_TO_DAEMON_CONTROL_TYPES;
100
48
 
101
- export function isDaemonToServerWsMsg(value: unknown): value is DaemonToServerWsMsg {
102
- return typeof value === 'string' && (DAEMON_TO_SERVER_WS_MSGS as readonly string[]).includes(value);
49
+
50
+
51
+ /**
52
+ * `auth_ok.payload.limits` — the plan-limit contract the server sends DOWN to
53
+ * the daemon for client-side enforcement.
54
+ *
55
+ * WHY THIS EXISTS: the wire spelling is `maxP2Pconnections` (lowercase `c`),
56
+ * inherited from `PlanLimits` in the proprietary server's plan-limits.ts. The
57
+ * daemon-cloud consumer independently declared the same field as
58
+ * `maxP2PConnections` (capital `C`) and assigned it from a `payload as any`
59
+ * cast — so the two spellings NEVER met under a type, TypeScript had nothing
60
+ * to compare, and the limit arrived as `undefined` at every comparison site.
61
+ * `undefined !== -1` passes the "is it unlimited?" guard, then every
62
+ * `count >= undefined` is false: enforcement silently degraded to unlimited on
63
+ * all plans, in production, for months.
64
+ *
65
+ * The lesson is NOT "pick a spelling" — it is that a cross-package wire
66
+ * contract asserted by hand on both ends with an `any` in between cannot fail
67
+ * loudly. Both producer (server) and consumer (daemon-cloud) must now import
68
+ * THIS symbol, so a rename breaks the build on both sides instead of silently
69
+ * disabling a paid-plan limit. Use `normalizeAuthOkLimits()` at the parse
70
+ * boundary; never re-spell these fields locally.
71
+ */
72
+ export interface AuthOkPlanLimits {
73
+ /** P2P concurrent connection count (-1 = unlimited). WIRE SPELLING — lowercase `c`. */
74
+ maxP2Pconnections: number;
75
+ /** Screenshot send interval floor (seconds). 0 = real-time. */
76
+ screenshotIntervalSeconds: number;
77
+ /** Daily screenshot usage budget (minutes). -1 = unlimited. */
78
+ dailyScreenshotMinutes: number;
79
+ /** Max connectable machines/daemons (-1 = unlimited). */
80
+ maxMachines: number;
103
81
  }
104
82
 
105
- export function isServerToDaemonWsMsg(value: unknown): value is ServerToDaemonWsMsg {
106
- return typeof value === 'string' && (SERVER_TO_DAEMON_WS_MSGS as readonly string[]).includes(value);
83
+ /**
84
+ * Parse-boundary normalizer for `auth_ok.payload.limits`.
85
+ *
86
+ * Returns `null` when the payload carries no usable limits object, so callers
87
+ * keep their existing "no limits known yet" branch. Individual non-numeric or
88
+ * missing fields fall back to the UNLIMITED sentinel (-1) for count-style
89
+ * limits and 0 for the interval floor, matching how each consumer already
90
+ * treats "no constraint" — a malformed field must never be coerced to a
91
+ * MORE restrictive value than the server intended, or a server-side schema
92
+ * change would start throttling paying users.
93
+ *
94
+ * Accepts the capital-`C` misspelling as an input alias ONLY so a daemon
95
+ * talking to a mixed-version fleet cannot regress; output is always the
96
+ * canonical wire spelling.
97
+ */
98
+ export function normalizeAuthOkLimits(raw: unknown): AuthOkPlanLimits | null {
99
+ if (!raw || typeof raw !== 'object') return null;
100
+ const src = raw as Record<string, unknown>;
101
+ const num = (value: unknown, fallback: number): number =>
102
+ typeof value === 'number' && Number.isFinite(value) ? value : fallback;
103
+ return {
104
+ maxP2Pconnections: num(
105
+ src.maxP2Pconnections !== undefined ? src.maxP2Pconnections : src.maxP2PConnections,
106
+ -1,
107
+ ),
108
+ screenshotIntervalSeconds: num(src.screenshotIntervalSeconds, 0),
109
+ dailyScreenshotMinutes: num(src.dailyScreenshotMinutes, -1),
110
+ maxMachines: num(src.maxMachines, -1),
111
+ };
107
112
  }