@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.
- package/dist/brain-routing.d.ts +2 -2
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2831 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2505 -35
- package/dist/index.mjs.map +1 -1
- package/dist/magi.d.ts +1 -1
- package/dist/mesh-tool-names.d.ts +27 -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/slot-proposal.d.ts +1 -1
- package/dist/turn-evidence.d.ts +438 -0
- package/dist/turn-ipc.d.ts +974 -0
- package/dist/types.d.ts +11 -0
- package/dist/worker-protocol.d.ts +69 -0
- package/dist/ws-protocol.d.ts +71 -41
- package/package.json +1 -1
- package/src/brain-routing.ts +3 -3
- package/src/index.ts +10 -0
- package/src/magi.ts +1 -1
- package/src/mesh-tool-names.ts +65 -19
- 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/slot-proposal.ts +1 -1
- package/src/turn-evidence.ts +602 -0
- package/src/turn-ipc.ts +2231 -0
- package/src/types.ts +8 -0
- package/src/worker-protocol.ts +164 -0
- 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
|
+
}
|
package/src/slot-proposal.ts
CHANGED
|
@@ -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
|
-
* (`
|
|
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;
|