@adhdev/mesh-shared 1.0.60-rc.4 → 1.0.60-rc.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2758 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2435 -17
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-tool-names.d.ts +2 -2
- package/dist/mesh-vocabulary.d.ts +70 -0
- package/dist/mission-brief.d.ts +62 -0
- package/dist/outbound-message.d.ts +202 -0
- package/dist/path-ownership.d.ts +101 -0
- package/dist/protocol/daemon-server.d.ts +388 -0
- package/dist/protocol/dashboard-daemon-p2p.d.ts +212 -0
- package/dist/protocol/dashboard-server.d.ts +240 -0
- package/dist/protocol/envelope.d.ts +55 -0
- package/dist/protocol/index.d.ts +15 -0
- package/dist/session-launch.d.ts +113 -0
- package/dist/session-status.d.ts +61 -0
- package/dist/turn-evidence.d.ts +400 -0
- package/dist/turn-ipc.d.ts +974 -0
- package/dist/worker-protocol.d.ts +69 -0
- package/dist/ws-protocol.d.ts +71 -41
- package/package.json +1 -1
- package/src/index.ts +10 -0
- package/src/mesh-tool-names.ts +0 -1
- package/src/mesh-vocabulary.ts +98 -0
- package/src/mission-brief.ts +153 -0
- package/src/outbound-message.ts +320 -0
- package/src/path-ownership.ts +271 -0
- package/src/protocol/daemon-server.ts +552 -0
- package/src/protocol/dashboard-daemon-p2p.ts +272 -0
- package/src/protocol/dashboard-server.ts +340 -0
- package/src/protocol/envelope.ts +75 -0
- package/src/protocol/index.ts +16 -0
- package/src/session-launch.ts +247 -0
- package/src/session-status.ts +140 -0
- package/src/turn-evidence.ts +549 -0
- package/src/turn-ipc.ts +2231 -0
- package/src/worker-protocol.ts +142 -0
- package/src/ws-protocol.ts +102 -93
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
|
|
35
|
+
export const WORKER_TOOLS = [
|
|
36
|
+
'report_completion',
|
|
37
|
+
'progress_update',
|
|
38
|
+
'peer_context_pull',
|
|
39
|
+
'git_status',
|
|
40
|
+
'git_log',
|
|
41
|
+
'git_diff',
|
|
42
|
+
] as const
|
|
43
|
+
|
|
44
|
+
export type WorkerTool = typeof WORKER_TOOLS[number]
|
|
45
|
+
|
|
46
|
+
const WORKER_TOOL_SET: ReadonlySet<string> = new Set(WORKER_TOOLS)
|
|
47
|
+
|
|
48
|
+
export function isWorkerTool(value: unknown): value is WorkerTool {
|
|
49
|
+
return typeof value === 'string' && WORKER_TOOL_SET.has(value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Stable marker line so a body is never footered twice and tests can find it. */
|
|
53
|
+
export const WORKER_PROTOCOL_FOOTER_MARKER = '--- adhdev worker protocol ---'
|
|
54
|
+
|
|
55
|
+
export interface WorkerProtocolFooterInput {
|
|
56
|
+
taskId?: string
|
|
57
|
+
taskMode?: string
|
|
58
|
+
difficulty?: string
|
|
59
|
+
readonly?: boolean
|
|
60
|
+
/** Number of handoff notes enclosed above the footer, when any. */
|
|
61
|
+
enclosedHandoffNotes?: number
|
|
62
|
+
/** H2: the owning mission's brief, when this task belongs to one. Rendered above the marker line — see module doc. */
|
|
63
|
+
missionBrief?: MissionBrief
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function hasWorkerProtocolFooter(body: string): boolean {
|
|
67
|
+
return typeof body === 'string' && body.includes(WORKER_PROTOCOL_FOOTER_MARKER)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The protocol block appended to a dispatched task. Written for the model that
|
|
72
|
+
* receives it: imperative, short, and explicit about what is NOT available so
|
|
73
|
+
* the worker does not go looking for coordinator tools.
|
|
74
|
+
*/
|
|
75
|
+
export function renderWorkerProtocolFooter(input: WorkerProtocolFooterInput = {}): string {
|
|
76
|
+
const briefBlock = input.missionBrief ? renderMissionBriefBlock(input.missionBrief) : null
|
|
77
|
+
const lines: string[] = briefBlock ? [briefBlock, '', WORKER_PROTOCOL_FOOTER_MARKER] : [WORKER_PROTOCOL_FOOTER_MARKER]
|
|
78
|
+
const scope: string[] = []
|
|
79
|
+
if (input.taskId) scope.push(`task ${input.taskId}`)
|
|
80
|
+
if (input.taskMode) scope.push(`mode ${input.taskMode}`)
|
|
81
|
+
if (input.difficulty) scope.push(`difficulty ${input.difficulty}`)
|
|
82
|
+
if (input.readonly) scope.push('read-only')
|
|
83
|
+
if (scope.length) lines.push(`You are a delegated worker (${scope.join(', ')}).`)
|
|
84
|
+
else lines.push('You are a delegated worker.')
|
|
85
|
+
lines.push(
|
|
86
|
+
'When your work is finished, blocked, or has failed, call `report_completion` exactly once. '
|
|
87
|
+
+ 'Its `summary` is recorded verbatim as the authoritative record of this task — your terminal is not '
|
|
88
|
+
+ 'scraped for it — so state what you did, what you found, and where you left the branch.',
|
|
89
|
+
'For work that runs longer than a few minutes, call `progress_update` at natural checkpoints so the '
|
|
90
|
+
+ 'coordinator can see you are alive without polling.',
|
|
91
|
+
'`peer_context_pull` shows what sibling tasks in this mission have reported; `git_status` / `git_diff` / '
|
|
92
|
+
+ '`git_log` inspect your own workspace (pass its absolute path as `workspace`).',
|
|
93
|
+
'You have no coordinator tools (no `mesh_*`) by design: do not enqueue, dispatch, or restart anything. '
|
|
94
|
+
+ 'If you need a decision from the coordinator, finish with `report_completion` and outcome `blocked`, '
|
|
95
|
+
+ 'listing what you need in `blockers`.',
|
|
96
|
+
)
|
|
97
|
+
if (input.enclosedHandoffNotes && input.enclosedHandoffNotes > 0) {
|
|
98
|
+
lines.push(
|
|
99
|
+
`The ${input.enclosedHandoffNotes} handoff note(s) above were written by agents who touched this code before you; `
|
|
100
|
+
+ 'honour their conflict guidance and add your own in `handoff_notes` when you report.',
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The authored text of a dispatched body: everything before the footer marker.
|
|
108
|
+
* Ledger descriptors, replay fixtures and tests compare authored text, never
|
|
109
|
+
* the protocol block.
|
|
110
|
+
*/
|
|
111
|
+
export function stripWorkerProtocolFooter(body: string): string {
|
|
112
|
+
if (typeof body !== 'string') return body
|
|
113
|
+
const at = body.indexOf(WORKER_PROTOCOL_FOOTER_MARKER)
|
|
114
|
+
if (at < 0) return body
|
|
115
|
+
return body.slice(0, at).replace(/\s+$/, '')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Append the footer to a body that does not carry one yet. Idempotent.
|
|
120
|
+
*/
|
|
121
|
+
export function appendWorkerProtocolFooter(body: string, input: WorkerProtocolFooterInput = {}): string {
|
|
122
|
+
if (hasWorkerProtocolFooter(body)) return body
|
|
123
|
+
const trimmed = body.replace(/\s+$/, '')
|
|
124
|
+
return `${trimmed}\n\n${renderWorkerProtocolFooter(input)}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The paragraph the COORDINATOR prompt carries about its workers. Kept next to
|
|
129
|
+
* the footer so the two halves of the protocol cannot drift apart.
|
|
130
|
+
*/
|
|
131
|
+
export function renderCoordinatorWorkerSection(): string {
|
|
132
|
+
return [
|
|
133
|
+
'## Workers',
|
|
134
|
+
'',
|
|
135
|
+
`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.`,
|
|
136
|
+
'- 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.',
|
|
137
|
+
'- `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.',
|
|
138
|
+
'- 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.',
|
|
139
|
+
'- `mesh_status` is for node health and capacity before delegating, never for progress.',
|
|
140
|
+
'- 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.',
|
|
141
|
+
].join('\n')
|
|
142
|
+
}
|
package/src/ws-protocol.ts
CHANGED
|
@@ -1,107 +1,116 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ws-protocol —
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
isDaemonToServerType,
|
|
22
|
+
isServerToDaemonControlType,
|
|
23
|
+
type DaemonToServerType,
|
|
24
|
+
type ServerToDaemonControlType,
|
|
25
|
+
type P2PSignalType,
|
|
26
|
+
type MeshP2PSignalType,
|
|
27
|
+
type DashboardToDaemonP2PType,
|
|
28
|
+
type DaemonToDashboardP2PType,
|
|
29
|
+
} from './protocol';
|
|
59
30
|
|
|
60
|
-
/**
|
|
61
|
-
export type
|
|
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';
|
|
31
|
+
/** Messages the daemon sends UP to the Workers server over the WS bridge (see protocol/daemon-server.ts). */
|
|
32
|
+
export type DaemonToServerWsMsg = DaemonToServerType;
|
|
70
33
|
|
|
71
34
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* `
|
|
35
|
+
* Server→daemon CONTROL messages (see protocol/daemon-server.ts). The server
|
|
36
|
+
* additionally relays an open command namespace as the frame `type`
|
|
37
|
+
* (`ServerDirectCommandMsg`), which is deliberately not a member here.
|
|
75
38
|
*/
|
|
76
|
-
export type
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
| 'command_result'
|
|
84
|
-
| 'command_result_chunk'
|
|
85
|
-
| 'screenshot_start'
|
|
86
|
-
| 'screenshot_stop'
|
|
87
|
-
| 'pty_input'
|
|
88
|
-
| 'pty_resize';
|
|
39
|
+
export type ServerToDaemonWsMsg = ServerToDaemonControlType;
|
|
40
|
+
|
|
41
|
+
/** P2P signaling relayed through the server WS — the `p2p_`/`mesh_p2p_` spellings the wire actually uses. */
|
|
42
|
+
export type P2PSignalingWsMsg = P2PSignalType | MeshP2PSignalType;
|
|
43
|
+
|
|
44
|
+
/** Every dashboard↔daemon P2P DataChannel JSON kind, both directions (see protocol/dashboard-daemon-p2p.ts). */
|
|
45
|
+
export type DashboardP2PMessageKind = DashboardToDaemonP2PType | DaemonToDashboardP2PType;
|
|
89
46
|
|
|
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
|
-
];
|
|
47
|
+
export const DAEMON_TO_SERVER_WS_MSGS: readonly DaemonToServerWsMsg[] = DAEMON_TO_SERVER_TYPES;
|
|
94
48
|
|
|
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
|
-
];
|
|
49
|
+
export const SERVER_TO_DAEMON_WS_MSGS: readonly ServerToDaemonWsMsg[] = SERVER_TO_DAEMON_CONTROL_TYPES;
|
|
100
50
|
|
|
101
|
-
export
|
|
102
|
-
|
|
51
|
+
export const isDaemonToServerWsMsg: (value: unknown) => value is DaemonToServerWsMsg = isDaemonToServerType;
|
|
52
|
+
|
|
53
|
+
export const isServerToDaemonWsMsg: (value: unknown) => value is ServerToDaemonWsMsg = isServerToDaemonControlType;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `auth_ok.payload.limits` — the plan-limit contract the server sends DOWN to
|
|
57
|
+
* the daemon for client-side enforcement.
|
|
58
|
+
*
|
|
59
|
+
* WHY THIS EXISTS: the wire spelling is `maxP2Pconnections` (lowercase `c`),
|
|
60
|
+
* inherited from `PlanLimits` in the proprietary server's plan-limits.ts. The
|
|
61
|
+
* daemon-cloud consumer independently declared the same field as
|
|
62
|
+
* `maxP2PConnections` (capital `C`) and assigned it from a `payload as any`
|
|
63
|
+
* cast — so the two spellings NEVER met under a type, TypeScript had nothing
|
|
64
|
+
* to compare, and the limit arrived as `undefined` at every comparison site.
|
|
65
|
+
* `undefined !== -1` passes the "is it unlimited?" guard, then every
|
|
66
|
+
* `count >= undefined` is false: enforcement silently degraded to unlimited on
|
|
67
|
+
* all plans, in production, for months.
|
|
68
|
+
*
|
|
69
|
+
* The lesson is NOT "pick a spelling" — it is that a cross-package wire
|
|
70
|
+
* contract asserted by hand on both ends with an `any` in between cannot fail
|
|
71
|
+
* loudly. Both producer (server) and consumer (daemon-cloud) must now import
|
|
72
|
+
* THIS symbol, so a rename breaks the build on both sides instead of silently
|
|
73
|
+
* disabling a paid-plan limit. Use `normalizeAuthOkLimits()` at the parse
|
|
74
|
+
* boundary; never re-spell these fields locally.
|
|
75
|
+
*/
|
|
76
|
+
export interface AuthOkPlanLimits {
|
|
77
|
+
/** P2P concurrent connection count (-1 = unlimited). WIRE SPELLING — lowercase `c`. */
|
|
78
|
+
maxP2Pconnections: number;
|
|
79
|
+
/** Screenshot send interval floor (seconds). 0 = real-time. */
|
|
80
|
+
screenshotIntervalSeconds: number;
|
|
81
|
+
/** Daily screenshot usage budget (minutes). -1 = unlimited. */
|
|
82
|
+
dailyScreenshotMinutes: number;
|
|
83
|
+
/** Max connectable machines/daemons (-1 = unlimited). */
|
|
84
|
+
maxMachines: number;
|
|
103
85
|
}
|
|
104
86
|
|
|
105
|
-
|
|
106
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Parse-boundary normalizer for `auth_ok.payload.limits`.
|
|
89
|
+
*
|
|
90
|
+
* Returns `null` when the payload carries no usable limits object, so callers
|
|
91
|
+
* keep their existing "no limits known yet" branch. Individual non-numeric or
|
|
92
|
+
* missing fields fall back to the UNLIMITED sentinel (-1) for count-style
|
|
93
|
+
* limits and 0 for the interval floor, matching how each consumer already
|
|
94
|
+
* treats "no constraint" — a malformed field must never be coerced to a
|
|
95
|
+
* MORE restrictive value than the server intended, or a server-side schema
|
|
96
|
+
* change would start throttling paying users.
|
|
97
|
+
*
|
|
98
|
+
* Accepts the capital-`C` misspelling as an input alias ONLY so a daemon
|
|
99
|
+
* talking to a mixed-version fleet cannot regress; output is always the
|
|
100
|
+
* canonical wire spelling.
|
|
101
|
+
*/
|
|
102
|
+
export function normalizeAuthOkLimits(raw: unknown): AuthOkPlanLimits | null {
|
|
103
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
104
|
+
const src = raw as Record<string, unknown>;
|
|
105
|
+
const num = (value: unknown, fallback: number): number =>
|
|
106
|
+
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
107
|
+
return {
|
|
108
|
+
maxP2Pconnections: num(
|
|
109
|
+
src.maxP2Pconnections !== undefined ? src.maxP2Pconnections : src.maxP2PConnections,
|
|
110
|
+
-1,
|
|
111
|
+
),
|
|
112
|
+
screenshotIntervalSeconds: num(src.screenshotIntervalSeconds, 0),
|
|
113
|
+
dailyScreenshotMinutes: num(src.dailyScreenshotMinutes, -1),
|
|
114
|
+
maxMachines: num(src.maxMachines, -1),
|
|
115
|
+
};
|
|
107
116
|
}
|