@adhdev/mesh-shared 1.0.60-rc.6 → 1.0.60-rc.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 (47) hide show
  1. package/dist/brain-routing.d.ts +2 -2
  2. package/dist/index.d.ts +10 -0
  3. package/dist/index.js +2831 -35
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2505 -35
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/magi.d.ts +1 -1
  8. package/dist/mesh-tool-names.d.ts +27 -2
  9. package/dist/mesh-vocabulary.d.ts +70 -0
  10. package/dist/mission-brief.d.ts +62 -0
  11. package/dist/outbound-message.d.ts +202 -0
  12. package/dist/path-ownership.d.ts +101 -0
  13. package/dist/protocol/daemon-server.d.ts +388 -0
  14. package/dist/protocol/dashboard-daemon-p2p.d.ts +212 -0
  15. package/dist/protocol/dashboard-server.d.ts +240 -0
  16. package/dist/protocol/envelope.d.ts +55 -0
  17. package/dist/protocol/index.d.ts +15 -0
  18. package/dist/session-launch.d.ts +113 -0
  19. package/dist/session-status.d.ts +61 -0
  20. package/dist/slot-proposal.d.ts +1 -1
  21. package/dist/turn-evidence.d.ts +438 -0
  22. package/dist/turn-ipc.d.ts +974 -0
  23. package/dist/types.d.ts +11 -0
  24. package/dist/worker-protocol.d.ts +69 -0
  25. package/dist/ws-protocol.d.ts +71 -41
  26. package/package.json +1 -1
  27. package/src/brain-routing.ts +3 -3
  28. package/src/index.ts +10 -0
  29. package/src/magi.ts +1 -1
  30. package/src/mesh-tool-names.ts +65 -19
  31. package/src/mesh-vocabulary.ts +98 -0
  32. package/src/mission-brief.ts +153 -0
  33. package/src/outbound-message.ts +320 -0
  34. package/src/path-ownership.ts +271 -0
  35. package/src/protocol/daemon-server.ts +552 -0
  36. package/src/protocol/dashboard-daemon-p2p.ts +272 -0
  37. package/src/protocol/dashboard-server.ts +340 -0
  38. package/src/protocol/envelope.ts +75 -0
  39. package/src/protocol/index.ts +16 -0
  40. package/src/session-launch.ts +247 -0
  41. package/src/session-status.ts +140 -0
  42. package/src/slot-proposal.ts +1 -1
  43. package/src/turn-evidence.ts +602 -0
  44. package/src/turn-ipc.ts +2231 -0
  45. package/src/types.ts +8 -0
  46. package/src/worker-protocol.ts +164 -0
  47. package/src/ws-protocol.ts +102 -93
@@ -0,0 +1,16 @@
1
+ /**
2
+ * protocol — typed wire contracts for the three transport links.
3
+ *
4
+ * ./daemon-server.ts daemon ↔ Workers server WS
5
+ * ./dashboard-server.ts dashboard ↔ UserSessionDO WS (+ share viewer ↔ SharedSessionDO)
6
+ * ./dashboard-daemon-p2p.ts dashboard ↔ daemon DataChannel
7
+ *
8
+ * Each exports name tuples, discriminated unions, type guards and a
9
+ * `decode<Link><Direction>(raw)` parse-boundary function. ../ws-protocol.ts
10
+ * keeps the pre-existing name-only aliases derived from these.
11
+ */
12
+
13
+ export * from './envelope'
14
+ export * from './daemon-server'
15
+ export * from './dashboard-server'
16
+ export * from './dashboard-daemon-p2p'
@@ -0,0 +1,247 @@
1
+ /**
2
+ * session-launch — how a session was launched: provider, model and thinking
3
+ * level, and WHERE each value came from.
4
+ *
5
+ * Wiring-unification Phase E (docs/design/2026-09-23-wiring-unification.md §7,
6
+ * RC5). Before this record the only trace of a session's model was a display
7
+ * string in recent-activity (`buildLegacyModelModeSummaryMetadata`), so nobody
8
+ * could answer "did I pick this model, did the dashboard restore it from last
9
+ * time, did a mesh slot choose it, or is it just the provider default?" — and
10
+ * after a daemon restart not even the value survived.
11
+ *
12
+ * The daemon owns the record (daemon-core `SessionRegistry`); this file holds the
13
+ * shape and the pure readers so daemon-core, web-core, the server and the MCP
14
+ * server all read one definition. It is a dependency-free leaf, like the rest of
15
+ * mesh-shared.
16
+ *
17
+ * ★Server content boundary (CLAUDE.md): the full record rides P2P only. The
18
+ * server path carries exactly two derived fields — `model` (an identifier,
19
+ * passed through {@link sanitizeModelIdentifier}) and `modelSource` (an enum,
20
+ * passed through {@link isModelAxisSource}). Never widen that to the record.
21
+ */
22
+
23
+ /**
24
+ * Where a model / thinking-level value came from.
25
+ *
26
+ * `restore` is deliberately NOT a member: a hosted session re-attached after a
27
+ * daemon restart keeps the ORIGINAL source (the user still picked that model);
28
+ * only `SessionLaunchRecord.launchedBy` records that the session came back via
29
+ * restore.
30
+ */
31
+ export const MODEL_AXIS_SOURCES = [
32
+ /** Picked explicitly for this launch (dashboard dropdown, coordinator dialog). */
33
+ 'user',
34
+ /** The dashboard dialog restored it from the last launch (browser-local memory). */
35
+ 'remembered',
36
+ /** A mesh capability slot supplied it (or a difficulty preset the slot outranked). */
37
+ 'mesh_slot',
38
+ /** An explicit per-task model / thinking level on a mesh queue task. */
39
+ 'task_override',
40
+ /** Nothing was requested; the provider's default is known (discovery / manifest). */
41
+ 'provider_default',
42
+ /** A value was requested without saying where it came from, or nothing is known. */
43
+ 'unspecified',
44
+ ] as const
45
+
46
+ export type ModelAxisSource = typeof MODEL_AXIS_SOURCES[number]
47
+
48
+ export function isModelAxisSource(value: unknown): value is ModelAxisSource {
49
+ return typeof value === 'string' && (MODEL_AXIS_SOURCES as readonly string[]).includes(value)
50
+ }
51
+
52
+ /**
53
+ * Which surface started the session. `restore` means "re-attached to a hosted
54
+ * runtime that outlived a daemon restart" — the axis sources are preserved.
55
+ */
56
+ export const SESSION_LAUNCHED_BY = ['dashboard', 'mesh', 'cli', 'api', 'restore'] as const
57
+
58
+ export type SessionLaunchedBy = typeof SESSION_LAUNCHED_BY[number]
59
+
60
+ export function isSessionLaunchedBy(value: unknown): value is SessionLaunchedBy {
61
+ return typeof value === 'string' && (SESSION_LAUNCHED_BY as readonly string[]).includes(value)
62
+ }
63
+
64
+ /** How a value entered the axis history. */
65
+ export type ModelSelectionVia = 'launch' | 'change_model' | 'observed'
66
+
67
+ export interface ModelSelectionHistoryEntry {
68
+ at: number
69
+ value: string
70
+ via: ModelSelectionVia
71
+ }
72
+
73
+ /** One axis (model, or thinking level) of a session launch. */
74
+ export interface ModelSelection {
75
+ /** What the caller asked for. Absent = nothing requested. */
76
+ requested?: string
77
+ source: ModelAxisSource
78
+ /**
79
+ * The value actually handed to the provider: after `modelLaunchValueMap`
80
+ * (CLI `--model` template) or the ACP `setConfigOption` call. Absent when the
81
+ * request was not applied (no template, the config call failed) or nothing
82
+ * was requested.
83
+ */
84
+ launchValue?: string
85
+ /**
86
+ * The provider default this session is expected to run with when nothing was
87
+ * requested (`source === 'provider_default'` only): discovery `models[0]`,
88
+ * else the manifest's first option. Nothing was passed to the provider.
89
+ */
90
+ resolvedDefault?: string
91
+ /** Last explicit runtime change (`change_model` / `set_thought_level`). */
92
+ current?: string
93
+ /** Last value the provider itself reported (statusline, native history). */
94
+ observed?: string
95
+ observedAt?: number
96
+ /** Every value change in order. The last entry is the value in force. */
97
+ history: ModelSelectionHistoryEntry[]
98
+ }
99
+
100
+ export interface SessionLaunchRecord {
101
+ sessionId: string
102
+ providerType: string
103
+ providerVersion?: string
104
+ providerChannel?: 'stable' | 'preview'
105
+ /** `restore` = re-attached after a daemon restart; the axis sources stay the original ones. */
106
+ launchedBy: SessionLaunchedBy
107
+ launchedAt: number
108
+ workspace?: string
109
+ model: ModelSelection
110
+ thinkingLevel: ModelSelection
111
+ autoApproveModeId?: string
112
+ /**
113
+ * Reserved by the design (§7 E1) for sanitized launch argv. Not populated:
114
+ * argv carries coordinator system-prompt text and config paths, and the
115
+ * session-host record already keeps the raw argv for restore.
116
+ */
117
+ cliArgs?: string[]
118
+ }
119
+
120
+ /** History is bounded; the first `launch` entry is always kept. */
121
+ export const MODEL_SELECTION_HISTORY_LIMIT = 20
122
+
123
+ /** The value in force now: the latest change, else the launch-time value. */
124
+ export function effectiveModelSelectionValue(selection: ModelSelection | null | undefined): string | undefined {
125
+ if (!selection) return undefined
126
+ const last = Array.isArray(selection.history) ? selection.history[selection.history.length - 1] : undefined
127
+ return last?.value
128
+ ?? selection.current
129
+ ?? selection.observed
130
+ ?? launchModelSelectionValue(selection)
131
+ }
132
+
133
+ /** The value the session started with (what was passed, else asked for, else the default). */
134
+ export function launchModelSelectionValue(selection: ModelSelection | null | undefined): string | undefined {
135
+ if (!selection) return undefined
136
+ const launch = Array.isArray(selection.history)
137
+ ? selection.history.find((entry) => entry.via === 'launch')
138
+ : undefined
139
+ return launch?.value ?? selection.launchValue ?? selection.requested ?? selection.resolvedDefault
140
+ }
141
+
142
+ export interface ModelSelectionDisplay {
143
+ /** The value in force now. */
144
+ value: string
145
+ /** The value the session launched with, when it differs from `value`. */
146
+ changedFrom?: string
147
+ source: ModelAxisSource
148
+ }
149
+
150
+ /** Pure projection for UI chips: "sonnet · chosen", "sonnet → opus (changed)". */
151
+ export function describeModelSelection(selection: ModelSelection | null | undefined): ModelSelectionDisplay | undefined {
152
+ const value = effectiveModelSelectionValue(selection)
153
+ if (!selection || !value) return undefined
154
+ const launched = launchModelSelectionValue(selection)
155
+ return {
156
+ value,
157
+ ...(launched && launched !== value ? { changedFrom: launched } : {}),
158
+ source: isModelAxisSource(selection.source) ? selection.source : 'unspecified',
159
+ }
160
+ }
161
+
162
+ /**
163
+ * The only form a model / thinking value may take on the server path: an
164
+ * identifier (`claude-opus-4-1`, `gpt-5.6-sol`, `openai/gpt-4o`, `opus[1m]`,
165
+ * `high`). No whitespace, bounded length — a free-text sentence cannot pass.
166
+ * Human labels (`Gemini 3.7 Flash (High)`) are P2P-only and are dropped here.
167
+ */
168
+ const MODEL_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]+-]{0,95}$/
169
+
170
+ export function sanitizeModelIdentifier(value: unknown): string | undefined {
171
+ if (typeof value !== 'string') return undefined
172
+ const trimmed = value.trim()
173
+ return MODEL_IDENTIFIER_PATTERN.test(trimmed) ? trimmed : undefined
174
+ }
175
+
176
+ function readString(value: unknown): string | undefined {
177
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined
178
+ }
179
+
180
+ function readFiniteNumber(value: unknown): number | undefined {
181
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined
182
+ }
183
+
184
+ function parseModelSelection(raw: unknown): ModelSelection | undefined {
185
+ if (!raw || typeof raw !== 'object') return undefined
186
+ const record = raw as Record<string, unknown>
187
+ const history: ModelSelectionHistoryEntry[] = []
188
+ if (Array.isArray(record.history)) {
189
+ for (const entry of record.history) {
190
+ if (!entry || typeof entry !== 'object') continue
191
+ const e = entry as Record<string, unknown>
192
+ const value = readString(e.value)
193
+ const at = readFiniteNumber(e.at)
194
+ const via = e.via === 'launch' || e.via === 'change_model' || e.via === 'observed' ? e.via : undefined
195
+ if (value && at !== undefined && via) history.push({ at, value, via })
196
+ }
197
+ }
198
+ const requested = readString(record.requested)
199
+ const launchValue = readString(record.launchValue)
200
+ const resolvedDefault = readString(record.resolvedDefault)
201
+ const current = readString(record.current)
202
+ const observed = readString(record.observed)
203
+ const observedAt = readFiniteNumber(record.observedAt)
204
+ return {
205
+ source: isModelAxisSource(record.source) ? record.source : 'unspecified',
206
+ ...(requested ? { requested } : {}),
207
+ ...(launchValue ? { launchValue } : {}),
208
+ ...(resolvedDefault ? { resolvedDefault } : {}),
209
+ ...(current ? { current } : {}),
210
+ ...(observed ? { observed } : {}),
211
+ ...(observedAt !== undefined ? { observedAt } : {}),
212
+ history: history.slice(-MODEL_SELECTION_HISTORY_LIMIT),
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Validate a record that crossed a process or storage boundary (session-host
218
+ * record meta on restore, a P2P snapshot). Returns undefined for anything that
219
+ * is not recognisably a launch record; unknown keys are dropped.
220
+ */
221
+ export function parseSessionLaunchRecord(raw: unknown): SessionLaunchRecord | undefined {
222
+ if (!raw || typeof raw !== 'object') return undefined
223
+ const record = raw as Record<string, unknown>
224
+ const sessionId = readString(record.sessionId)
225
+ const providerType = readString(record.providerType)
226
+ const launchedAt = readFiniteNumber(record.launchedAt)
227
+ const model = parseModelSelection(record.model)
228
+ const thinkingLevel = parseModelSelection(record.thinkingLevel)
229
+ if (!sessionId || !providerType || launchedAt === undefined || !model || !thinkingLevel) return undefined
230
+ const providerVersion = readString(record.providerVersion)
231
+ const workspace = readString(record.workspace)
232
+ const autoApproveModeId = readString(record.autoApproveModeId)
233
+ return {
234
+ sessionId,
235
+ providerType,
236
+ ...(providerVersion ? { providerVersion } : {}),
237
+ ...(record.providerChannel === 'stable' || record.providerChannel === 'preview'
238
+ ? { providerChannel: record.providerChannel }
239
+ : {}),
240
+ launchedBy: isSessionLaunchedBy(record.launchedBy) ? record.launchedBy : 'api',
241
+ launchedAt,
242
+ ...(workspace ? { workspace } : {}),
243
+ model,
244
+ thinkingLevel,
245
+ ...(autoApproveModeId ? { autoApproveModeId } : {}),
246
+ }
247
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * session-status — the ONE session status vocabulary.
3
+ *
4
+ * Wiring-unification Phase A1 (docs/design/2026-09-23-wiring-unification.md).
5
+ *
6
+ * Before this file, the same eleven-member union was declared three times in
7
+ * daemon-core (`SessionStatus`, `ManagedStatus`, and a stale `AgentStatus`) and
8
+ * five independent "is it busy?" sets disagreed with each other — for example
9
+ * `waiting_approval` counted as busy in three of them and idle in the other
10
+ * two. Every consumer now derives from here:
11
+ *
12
+ * - the canonical union `SessionStatus` and its member list `SESSION_STATUSES`;
13
+ * - a behavioural CLASS per status (`SESSION_STATUS_CLASS`) — the only thing
14
+ * send / dispatch / UI logic may branch on;
15
+ * - an alias table for the raw spellings providers and status lanes still
16
+ * emit (`running`, `streaming`, `no_progress`, …) so classification is total.
17
+ *
18
+ * This lives in mesh-shared (a dependency-free leaf) because web-core must not
19
+ * value-import the daemon-core barrel, and the server has no daemon-core at all.
20
+ */
21
+
22
+ export const SESSION_STATUSES = [
23
+ 'idle',
24
+ 'generating',
25
+ 'waiting_approval',
26
+ 'waiting_choice',
27
+ 'finalizing',
28
+ 'error',
29
+ 'stopped',
30
+ 'starting',
31
+ 'panel_hidden',
32
+ 'not_monitored',
33
+ 'disconnected',
34
+ ] as const
35
+
36
+ export type SessionStatus = typeof SESSION_STATUSES[number]
37
+
38
+ export const RECENT_SESSION_BUCKETS = ['needs_attention', 'working', 'task_complete', 'idle'] as const
39
+
40
+ export type RecentSessionBucket = typeof RECENT_SESSION_BUCKETS[number]
41
+
42
+ /**
43
+ * Behavioural class of a status.
44
+ *
45
+ * working — the agent owns the turn; input is queued, not delivered.
46
+ * blocked — the agent is waiting on a human decision (approval / choice);
47
+ * alive, and the turn is not over.
48
+ * ready — the agent will accept input now.
49
+ * dead — the session will never accept input again.
50
+ * unknown — the spelling is not one we classify (callers must fail safe).
51
+ */
52
+ export type SessionStatusClass = 'working' | 'blocked' | 'ready' | 'dead' | 'unknown'
53
+
54
+ export const SESSION_STATUS_CLASS: Record<SessionStatus, Exclude<SessionStatusClass, 'unknown'>> = {
55
+ generating: 'working',
56
+ finalizing: 'working',
57
+ starting: 'working',
58
+ waiting_approval: 'blocked',
59
+ waiting_choice: 'blocked',
60
+ idle: 'ready',
61
+ panel_hidden: 'ready',
62
+ not_monitored: 'ready',
63
+ error: 'dead',
64
+ stopped: 'dead',
65
+ disconnected: 'dead',
66
+ }
67
+
68
+ /**
69
+ * Raw spellings that still reach status consumers from IDE/CDP providers, the
70
+ * chat-tail status lane and legacy adapters, mapped onto the canonical member
71
+ * that carries the same behavioural meaning. Keep this table exhaustive over
72
+ * what the five pre-unification busy sets accepted; a spelling missing here
73
+ * classifies as `unknown`, which every consumer must treat as "do not assume
74
+ * idle".
75
+ */
76
+ export const SESSION_STATUS_ALIASES: Readonly<Record<string, SessionStatus>> = {
77
+ running: 'generating',
78
+ streaming: 'generating',
79
+ busy: 'generating',
80
+ working: 'generating',
81
+ loading: 'generating',
82
+ loading_reference: 'generating',
83
+ thinking: 'generating',
84
+ active: 'generating',
85
+ // Status-lane refinements of a turn that is still owned by the agent.
86
+ no_progress: 'generating',
87
+ long_generating: 'generating',
88
+ initializing: 'starting',
89
+ waiting: 'waiting_approval',
90
+ }
91
+
92
+ const SESSION_STATUS_SET: ReadonlySet<string> = new Set(SESSION_STATUSES)
93
+
94
+ export function isSessionStatus(value: unknown): value is SessionStatus {
95
+ return typeof value === 'string' && SESSION_STATUS_SET.has(value)
96
+ }
97
+
98
+ /** Canonical member for a raw status spelling, or null when unrecognised. */
99
+ export function normalizeSessionStatus(raw: unknown): SessionStatus | null {
100
+ if (typeof raw !== 'string') return null
101
+ const value = raw.trim().toLowerCase()
102
+ if (SESSION_STATUS_SET.has(value)) return value as SessionStatus
103
+ return SESSION_STATUS_ALIASES[value] ?? null
104
+ }
105
+
106
+ export function classifySessionStatus(raw: unknown): SessionStatusClass {
107
+ const status = normalizeSessionStatus(raw)
108
+ return status ? SESSION_STATUS_CLASS[status] : 'unknown'
109
+ }
110
+
111
+ export function isWorkingStatus(raw: unknown): boolean {
112
+ return classifySessionStatus(raw) === 'working'
113
+ }
114
+
115
+ export function isBlockedStatus(raw: unknown): boolean {
116
+ return classifySessionStatus(raw) === 'blocked'
117
+ }
118
+
119
+ export function isReadyStatus(raw: unknown): boolean {
120
+ return classifySessionStatus(raw) === 'ready'
121
+ }
122
+
123
+ export function isDeadStatus(raw: unknown): boolean {
124
+ return classifySessionStatus(raw) === 'dead'
125
+ }
126
+
127
+ /**
128
+ * "Busy" in the sense every pre-unification set meant: the agent cannot take a
129
+ * new instruction right now, whether because it is working or because it is
130
+ * blocked on a human. Dead sessions are NOT busy — they are gone, and callers
131
+ * that need to distinguish must check `isDeadStatus` first.
132
+ */
133
+ export function isBusyStatus(raw: unknown): boolean {
134
+ const cls = classifySessionStatus(raw)
135
+ return cls === 'working' || cls === 'blocked'
136
+ }
137
+
138
+ export function statusesOfClass(cls: Exclude<SessionStatusClass, 'unknown'>): SessionStatus[] {
139
+ return SESSION_STATUSES.filter((status) => SESSION_STATUS_CLASS[status] === cls)
140
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Detection of installed CLI providers already exists per node (the status
5
5
  * snapshot's `availableProviders`), and applying a slot profile already exists
6
- * (`mesh_node_slots_set`, dry-run by default). What was missing is the bit in
6
+ * (`mesh_node_slots` action "set", dry-run by default). What was missing is the bit in
7
7
  * between: turning "these CLIs are installed on this node" into a concrete
8
8
  * `NodeCapabilitySlot[]` draft the operator can review. This module is that
9
9
  * bridge, and nothing more — it is a PURE proposal generator. It never writes;