@adhdev/mesh-shared 1.0.82-rc.21 → 1.0.82-rc.23

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.
@@ -51,7 +51,7 @@ export interface LaunchDefaultAxis {
51
51
  defaultRef?: LaunchDefaultRef;
52
52
  }
53
53
  export type LaunchDefaultWarning = 'catalog_unverified' | 'manual_mcp_setup_required';
54
- export type LaunchDefaultValidationReason = 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled' | 'provider_not_detected' | 'provider_not_eligible' | 'model_unsupported' | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported' | 'thinking_not_offered' | 'catalog_unavailable';
54
+ export type LaunchDefaultValidationReason = 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled' | 'provider_not_detected' | 'provider_not_eligible' | 'provider_quota_gated' | 'model_unsupported' | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported' | 'thinking_not_offered' | 'catalog_unavailable';
55
55
  export type LaunchDefaultResolution = {
56
56
  ok: true;
57
57
  selection: LaunchDefaultSelection;
@@ -18,10 +18,10 @@
18
18
  * Order mirrors mcp-server `ALL_MESH_TOOLS` for easy visual diffing, but the consistency
19
19
  * checks are set-based (order-insensitive).
20
20
  */
21
- export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots"];
21
+ export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots", "driver_list", "driver_describe"];
22
22
  export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
23
23
  /** The count the `NN tools` barrel doc comments and consistency test assert against. */
24
- export declare const CANONICAL_MESH_TOOL_COUNT: 41;
24
+ export declare const CANONICAL_MESH_TOOL_COUNT: 43;
25
25
  /**
26
26
  * Tool names retired by the 2026-09-26 tool-surface consolidation (60 → 48).
27
27
  * Every capability behind them is still reachable: each retired name maps to the
@@ -203,3 +203,10 @@ export declare function isSubmitOutcome(value: unknown): value is SubmitOutcome;
203
203
  export declare function isInputEnvelopeWire(value: unknown): value is InputEnvelopeWire;
204
204
  /** Minimal structural check for the wire boundary — not a full decoder (mesh-shared stays a pure leaf; semantic validation stays with the handler that owns it). */
205
205
  export declare function isOutboundMessage(value: unknown): value is OutboundMessage;
206
+ /**
207
+ * Drop the `<pasted_content>` envelope lines Claude Code wraps around a pasted
208
+ * input. Only a text that OPENS with the envelope is touched; anything else is
209
+ * returned unchanged. Shared across daemon transcript merge and web echo/display
210
+ * normalization so wrapped turns correlate identically.
211
+ */
212
+ export declare function unwrapPastedContent(text: string): string;
@@ -213,11 +213,15 @@ export type P2POfferWirePayload = {
213
213
  sdp: string;
214
214
  type: string;
215
215
  peerId: string;
216
+ attemptId?: number;
217
+ attemptEpoch?: string;
216
218
  };
217
219
  export type P2PIceWirePayload = {
218
220
  candidate: string;
219
221
  mid: string;
220
222
  peerId: string;
223
+ attemptId?: number;
224
+ attemptEpoch?: string;
221
225
  };
222
226
  /**
223
227
  * A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
@@ -229,6 +233,8 @@ export type P2PIceWirePayload = {
229
233
  export type RelayedP2PSignalWirePayload = {
230
234
  peerId?: string;
231
235
  daemonId?: string;
236
+ attemptId?: number;
237
+ attemptEpoch?: string;
232
238
  sharePermission?: string;
233
239
  shareTargetSessionId?: string;
234
240
  sdp?: string;
@@ -109,12 +109,16 @@ export type SystemAnnouncementWirePayload = {
109
109
  export type DashboardP2POfferWirePayload = {
110
110
  daemonId: string;
111
111
  peerId?: string;
112
+ attemptId?: number;
113
+ attemptEpoch?: string;
112
114
  sdp: string;
113
115
  type: string;
114
116
  };
115
117
  export type DashboardP2PIceWirePayload = {
116
118
  daemonId: string;
117
119
  peerId?: string;
120
+ attemptId?: number;
121
+ attemptEpoch?: string;
118
122
  candidate: string;
119
123
  mid?: string;
120
124
  sdpMid?: string | null;
@@ -154,16 +158,22 @@ export declare const isDashboardP2PSignalType: (value: unknown) => value is "p2p
154
158
  export type DashboardP2PReadyWirePayload = {
155
159
  daemonId: string;
156
160
  peerId: string;
161
+ attemptId?: number;
162
+ attemptEpoch?: string;
157
163
  };
158
164
  export type DashboardP2PAnswerWirePayload = {
159
165
  daemonId: string;
160
166
  peerId: string;
161
167
  sdp: string;
162
168
  type: string;
169
+ attemptId?: number;
170
+ attemptEpoch?: string;
163
171
  };
164
172
  export type DashboardP2PLocalIceWirePayload = {
165
173
  daemonId: string;
166
174
  peerId: string;
175
+ attemptId?: number;
176
+ attemptEpoch?: string;
167
177
  candidate: string;
168
178
  sdpMid: string | null;
169
179
  sdpMLineIndex: number | null;
@@ -0,0 +1,11 @@
1
+ /** Persisted role-launch intent. Restart replays this selection rather than
2
+ * consulting newer machine defaults; restore preserves its original provenance. */
3
+ import { type LaunchDefaultRole, type LaunchDefaultSelection, type LaunchDefaultAxis, type LaunchDefaultRef } from './launch-defaults';
4
+ export interface RoleLaunchSelection {
5
+ role: LaunchDefaultRole;
6
+ selection: LaunchDefaultSelection;
7
+ applied: Record<keyof LaunchDefaultSelection, LaunchDefaultAxis>;
8
+ defaultRef?: LaunchDefaultRef;
9
+ documentRevision: number;
10
+ }
11
+ export declare function parseRoleLaunchSelection(value: unknown): RoleLaunchSelection | undefined;
@@ -1,25 +1,5 @@
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
- export declare const MODEL_AXIS_SOURCES: readonly ["user", "remembered", "mesh_slot", "task_override", "provider_default", "unspecified"];
1
+ import { type RoleLaunchSelection } from './role-launch';
2
+ export declare const MODEL_AXIS_SOURCES: readonly ["user", "remembered", "stored_default", "project_default", "mesh_slot", "task_override", "provider_default", "unspecified"];
23
3
  export type ModelAxisSource = typeof MODEL_AXIS_SOURCES[number];
24
4
  export declare function isModelAxisSource(value: unknown): value is ModelAxisSource;
25
5
  /**
@@ -65,6 +45,7 @@ export interface ModelSelection {
65
45
  history: ModelSelectionHistoryEntry[];
66
46
  }
67
47
  export interface SessionLaunchRecord {
48
+ roleLaunch?: RoleLaunchSelection;
68
49
  sessionId: string;
69
50
  providerType: string;
70
51
  providerVersion?: string;
@@ -35,6 +35,13 @@ import { type MeshTaskDifficulty, type NodeCapabilitySlot } from './brain-routin
35
35
  export interface CliSlotRecipe {
36
36
  /** Optional model to pin on the slot. Best-effort at launch. */
37
37
  model?: string;
38
+ /**
39
+ * Optional ordered candidate models to prefer when the provider's live
40
+ * `modelOptions` (from model discovery) is present on `DetectedCliProvider`.
41
+ * The first candidate present in `modelOptions` wins; falls back to `model`
42
+ * when `modelOptions` is absent or contains none of the candidates.
43
+ */
44
+ modelCandidates?: readonly string[];
38
45
  /** Optional thinking level, in the provider's own vocabulary. */
39
46
  thinkingLevel?: string;
40
47
  /** Difficulty range this slot handles. Empty = general-purpose. */
@@ -76,6 +83,13 @@ export interface DetectedCliProvider {
76
83
  displayName?: string;
77
84
  /** Detected version, when known. Display only — never affects the mapping. */
78
85
  version?: string;
86
+ /**
87
+ * Live discovered / resolved `modelOptions` for this provider on the node,
88
+ * when available on the status catalog. When present and non-empty, recipes
89
+ * prefer the first matching model from `[recipe.model, ...recipe.modelCandidates]`
90
+ * that exists in `modelOptions`.
91
+ */
92
+ modelOptions?: readonly string[];
79
93
  }
80
94
  /** One proposed slot plus why it was proposed. */
81
95
  export interface ProposedSlotEntry {
@@ -30,7 +30,7 @@
30
30
  * the protocol contract the marker demarcates.
31
31
  */
32
32
  import { type MissionBrief } from './mission-brief';
33
- export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff"];
33
+ export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff", "driver_list", "driver_describe", "driver_invoke"];
34
34
  export type WorkerTool = typeof WORKER_TOOLS[number];
35
35
  export declare function isWorkerTool(value: unknown): value is WorkerTool;
36
36
  /** Stable marker line so a body is never footered twice and tests can find it. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/mesh-shared",
3
- "version": "1.0.82-rc.21",
3
+ "version": "1.0.82-rc.23",
4
4
  "description": "ADHDev mesh-shared — pure mesh/git status normalizers shared by daemon-core and web-core",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,9 +11,8 @@
11
11
  * - the daemon-only verbs (launch / MCP-only pull / owner actions) that have
12
12
  * no tool.
13
13
  *
14
- * Assistant tools are a separate set from the mesh tools: none of these names
15
- * belongs in `CANONICAL_MESH_TOOL_NAMES`, and the coordinator/worker MCP modes
16
- * never publish them.
14
+ * Assistant-specific tools remain separate from mesh tools. Driver discovery
15
+ * (list/describe) is shared across all three modes; invoke remains Worker-only.
17
16
  */
18
17
 
19
18
  export const ASSISTANT_TOOLS = [
@@ -33,6 +32,8 @@ export const ASSISTANT_TOOLS = [
33
32
  'project_note',
34
33
  'launch_defaults',
35
34
  'launch_defaults_set',
35
+ 'driver_list',
36
+ 'driver_describe',
36
37
  ] as const
37
38
 
38
39
  export type AssistantTool = typeof ASSISTANT_TOOLS[number]
@@ -46,6 +47,8 @@ export function isAssistantTool(value: unknown): value is AssistantTool {
46
47
  /** Daemon verb names (§4.4 table + the five verbs below it). */
47
48
  export const ASSISTANT_VERB = {
48
49
  // tool verbs
50
+ driverList: 'driver_list',
51
+ driverDescribe: 'driver_describe',
49
52
  projects: 'assistant_projects',
50
53
  environment: 'assistant_environment',
51
54
  action: 'assistant_action',
@@ -80,6 +83,8 @@ export type AssistantVerb = typeof ASSISTANT_VERB[keyof typeof ASSISTANT_VERB]
80
83
 
81
84
  /** The daemon verb each assistant tool calls. */
82
85
  export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>> = {
86
+ driver_list: ASSISTANT_VERB.driverList,
87
+ driver_describe: ASSISTANT_VERB.driverDescribe,
83
88
  projects: ASSISTANT_VERB.projects,
84
89
  environment: ASSISTANT_VERB.environment,
85
90
  assistant_action: ASSISTANT_VERB.action,
package/src/index.ts CHANGED
@@ -45,3 +45,5 @@ export * from './keyed-doc-delta'
45
45
  export * from './provider-approval'
46
46
  export * from './provider-question'
47
47
  export * from './launch-defaults'
48
+
49
+ export * from './role-launch';
@@ -57,7 +57,7 @@ export interface LaunchDefaultAxis {
57
57
  export type LaunchDefaultWarning = 'catalog_unverified' | 'manual_mcp_setup_required';
58
58
  export type LaunchDefaultValidationReason =
59
59
  | 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled'
60
- | 'provider_not_detected' | 'provider_not_eligible' | 'model_unsupported'
60
+ | 'provider_not_detected' | 'provider_not_eligible' | 'provider_quota_gated' | 'model_unsupported'
61
61
  | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported'
62
62
  | 'thinking_not_offered' | 'catalog_unavailable';
63
63
  export type LaunchDefaultResolution =
@@ -65,6 +65,8 @@ export const CANONICAL_MESH_TOOL_NAMES = [
65
65
  'mesh_mission_list',
66
66
  'mesh_review_inbox',
67
67
  'mesh_node_slots',
68
+ 'driver_list',
69
+ 'driver_describe',
68
70
  ] as const;
69
71
 
70
72
  export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
@@ -321,3 +321,20 @@ export function isOutboundMessage(value: unknown): value is OutboundMessage {
321
321
  && isSendPolicy(candidate.policy)
322
322
  && typeof candidate.createdAt === 'number'
323
323
  }
324
+
325
+ const PASTED_OPEN_RE = /^\s*<pasted_content(?:\s+id="[^"]*")?\s*>\s*$/
326
+ const PASTED_CLOSE_RE = /^\s*<\/pasted_content(?:\s+id="[^"]*")?\s*>\s*$/
327
+
328
+ /**
329
+ * Drop the `<pasted_content>` envelope lines Claude Code wraps around a pasted
330
+ * input. Only a text that OPENS with the envelope is touched; anything else is
331
+ * returned unchanged. Shared across daemon transcript merge and web echo/display
332
+ * normalization so wrapped turns correlate identically.
333
+ */
334
+ export function unwrapPastedContent(text: string): string {
335
+ const lines = text.replace(/\r\n?/g, '\n').split('\n')
336
+ const first = lines.findIndex((line) => line.trim())
337
+ if (first < 0 || !PASTED_OPEN_RE.test(lines[first])) return text
338
+ return lines.filter((line) => !PASTED_OPEN_RE.test(line) && !PASTED_CLOSE_RE.test(line)).join('\n')
339
+ }
340
+
@@ -260,8 +260,8 @@ export const DAEMON_P2P_SIGNAL_TYPES = ['p2p_offer', 'p2p_ice'] as const
260
260
  export type DaemonP2PSignalType = typeof DAEMON_P2P_SIGNAL_TYPES[number]
261
261
  export const isDaemonP2PSignalType = makeTypeGuard(DAEMON_P2P_SIGNAL_TYPES)
262
262
 
263
- export type P2POfferWirePayload = { sdp: string; type: string; peerId: string }
264
- export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string }
263
+ export type P2POfferWirePayload = { sdp: string; type: string; peerId: string; attemptId?: number; attemptEpoch?: string }
264
+ export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string; attemptId?: number; attemptEpoch?: string }
265
265
 
266
266
  /**
267
267
  * A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
@@ -273,6 +273,8 @@ export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string
273
273
  export type RelayedP2PSignalWirePayload = {
274
274
  peerId?: string
275
275
  daemonId?: string
276
+ attemptId?: number
277
+ attemptEpoch?: string
276
278
  sharePermission?: string
277
279
  shareTargetSessionId?: string
278
280
  sdp?: string
@@ -152,6 +152,8 @@ export type SystemAnnouncementWirePayload = {
152
152
  export type DashboardP2POfferWirePayload = {
153
153
  daemonId: string
154
154
  peerId?: string
155
+ attemptId?: number
156
+ attemptEpoch?: string
155
157
  sdp: string
156
158
  type: string
157
159
  }
@@ -159,6 +161,8 @@ export type DashboardP2POfferWirePayload = {
159
161
  export type DashboardP2PIceWirePayload = {
160
162
  daemonId: string
161
163
  peerId?: string
164
+ attemptId?: number
165
+ attemptEpoch?: string
162
166
  candidate: string
163
167
  mid?: string
164
168
  sdpMid?: string | null
@@ -234,11 +238,13 @@ export const DASHBOARD_P2P_SIGNAL_TYPES = ['p2p_ready', 'p2p_answer', 'p2p_ice']
234
238
  export type DashboardP2PSignalType = typeof DASHBOARD_P2P_SIGNAL_TYPES[number]
235
239
  export const isDashboardP2PSignalType = makeTypeGuard(DASHBOARD_P2P_SIGNAL_TYPES)
236
240
 
237
- export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string }
238
- export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string }
241
+ export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string; attemptId?: number; attemptEpoch?: string }
242
+ export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string; attemptId?: number; attemptEpoch?: string }
239
243
  export type DashboardP2PLocalIceWirePayload = {
240
244
  daemonId: string
241
245
  peerId: string
246
+ attemptId?: number
247
+ attemptEpoch?: string
242
248
  candidate: string
243
249
  sdpMid: string | null
244
250
  sdpMLineIndex: number | null
@@ -0,0 +1,51 @@
1
+ /** Persisted role-launch intent. Restart replays this selection rather than
2
+ * consulting newer machine defaults; restore preserves its original provenance. */
3
+ import { isLaunchDefaultRole, isLaunchDefaultSource, type LaunchDefaultRole,
4
+ type LaunchDefaultSelection, type LaunchDefaultAxis, type LaunchDefaultRef } from './launch-defaults';
5
+ export interface RoleLaunchSelection {
6
+ role: LaunchDefaultRole;
7
+ selection: LaunchDefaultSelection;
8
+ applied: Record<keyof LaunchDefaultSelection, LaunchDefaultAxis>;
9
+ defaultRef?: LaunchDefaultRef;
10
+ documentRevision: number;
11
+ }
12
+ function record(value: unknown): Record<string, unknown> | undefined {
13
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
14
+ ? value as Record<string, unknown> : undefined;
15
+ }
16
+ function ref(value: unknown): LaunchDefaultRef | undefined {
17
+ const r = record(value);
18
+ if (!r || !isLaunchDefaultRole(r.role) || (r.scope !== 'global' && r.scope !== 'project')
19
+ || !Number.isSafeInteger(r.revision) || (r.revision as number) < 1
20
+ || (r.scope === 'project' && (typeof r.projectId !== 'string' || !r.projectId))) return undefined;
21
+ return { role: r.role, scope: r.scope, revision: r.revision as number,
22
+ ...(r.scope === 'project' ? { projectId: r.projectId as string } : {}) };
23
+ }
24
+ function axis(value: unknown): LaunchDefaultAxis | undefined {
25
+ const a = record(value);
26
+ if (!a || !isLaunchDefaultSource(a.source) || (a.value !== null && typeof a.value !== 'string')) return undefined;
27
+ const defaultRef = ref(a.defaultRef);
28
+ if (a.source === 'stored_default' && defaultRef?.scope !== 'global'
29
+ || a.source === 'project_default' && defaultRef?.scope !== 'project'
30
+ || (a.source === 'user' || a.source === 'provider_default') && a.defaultRef !== undefined) return undefined;
31
+ return { value: a.value as string | null, source: a.source, ...(defaultRef ? { defaultRef } : {}) };
32
+ }
33
+ export function parseRoleLaunchSelection(value: unknown): RoleLaunchSelection | undefined {
34
+ const r = record(value), s = record(r?.selection), a = record(r?.applied);
35
+ if (!r || !isLaunchDefaultRole(r.role) || !s || typeof s.provider !== 'string' || !s.provider
36
+ || !Number.isSafeInteger(r.documentRevision) || (r.documentRevision as number) < 1
37
+ || (s.model !== undefined && typeof s.model !== 'string')
38
+ || (s.thinkingLevel !== undefined && typeof s.thinkingLevel !== 'string')) return undefined;
39
+ const provider = axis(a?.provider), model = axis(a?.model), thinkingLevel = axis(a?.thinkingLevel);
40
+ if (!provider || !model || !thinkingLevel
41
+ || [provider, model, thinkingLevel].some(axis => axis.defaultRef && (axis.defaultRef.role !== r.role
42
+ || axis.defaultRef.scope === 'global' && axis.defaultRef.revision > (r.documentRevision as number)))
43
+ || provider.value !== s.provider
44
+ || model.value !== (s.model ?? null) || thinkingLevel.value !== (s.thinkingLevel ?? null)) return undefined;
45
+ const defaultRef = ref(r.defaultRef);
46
+ return { role: r.role, selection: { provider: s.provider,
47
+ ...(typeof s.model === 'string' ? { model: s.model } : {}),
48
+ ...(typeof s.thinkingLevel === 'string' ? { thinkingLevel: s.thinkingLevel } : {}) },
49
+ applied: { provider, model, thinkingLevel }, ...(defaultRef ? { defaultRef } : {}),
50
+ documentRevision: r.documentRevision as number };
51
+ }
@@ -1,3 +1,4 @@
1
+ import { parseRoleLaunchSelection, type RoleLaunchSelection } from './role-launch'
1
2
  /**
2
3
  * session-launch — how a session was launched: provider, model and thinking
3
4
  * level, and WHERE each value came from.
@@ -35,11 +36,14 @@ export const MODEL_AXIS_SOURCES = [
35
36
  'user',
36
37
  /** The dashboard dialog restored it from the last launch (browser-local memory). */
37
38
  'remembered',
39
+ /** The authoritative daemon role default or host project override. */
40
+ 'stored_default',
41
+ 'project_default',
38
42
  /** A mesh capability slot supplied it (or a difficulty preset the slot outranked). */
39
43
  'mesh_slot',
40
44
  /** An explicit per-task model / thinking level on a mesh queue task. */
41
45
  'task_override',
42
- /** Nothing was requested; the provider's default is known (discovery / manifest). */
46
+ /** Nothing requested: the provider chooses; resolvedDefault records it when known. */
43
47
  'provider_default',
44
48
  /** A value was requested without saying where it came from, or nothing is known. */
45
49
  'unspecified',
@@ -102,6 +106,7 @@ export interface ModelSelection {
102
106
  }
103
107
 
104
108
  export interface SessionLaunchRecord {
109
+ roleLaunch?: RoleLaunchSelection
105
110
  sessionId: string
106
111
  providerType: string
107
112
  providerVersion?: string
@@ -230,7 +235,9 @@ export function parseSessionLaunchRecord(raw: unknown): SessionLaunchRecord | un
230
235
  const providerVersion = readString(record.providerVersion)
231
236
  const workspace = readString(record.workspace)
232
237
  const autoApproveModeId = readString(record.autoApproveModeId)
238
+ const roleLaunch = parseRoleLaunchSelection(record.roleLaunch)
233
239
  return {
240
+ ...(roleLaunch ? { roleLaunch } : {}),
234
241
  sessionId,
235
242
  providerType,
236
243
  ...(providerVersion ? { providerVersion } : {}),
@@ -40,6 +40,13 @@ import {
40
40
  export interface CliSlotRecipe {
41
41
  /** Optional model to pin on the slot. Best-effort at launch. */
42
42
  model?: string
43
+ /**
44
+ * Optional ordered candidate models to prefer when the provider's live
45
+ * `modelOptions` (from model discovery) is present on `DetectedCliProvider`.
46
+ * The first candidate present in `modelOptions` wins; falls back to `model`
47
+ * when `modelOptions` is absent or contains none of the candidates.
48
+ */
49
+ modelCandidates?: readonly string[]
43
50
  /** Optional thinking level, in the provider's own vocabulary. */
44
51
  thinkingLevel?: string
45
52
  /** Difficulty range this slot handles. Empty = general-purpose. */
@@ -101,11 +108,26 @@ export const CLI_SLOT_RECIPES: Readonly<Record<string, readonly CliSlotRecipe[]>
101
108
  ],
102
109
  'antigravity-cli': [
103
110
  {
104
- model: 'Gemini 3.1 Pro (High)',
111
+ model: 'Gemini 3.8 Flash (High)',
112
+ modelCandidates: ['Gemini 3.8 Flash (High)', 'Gemini 4 Argon (Low)', 'Gemini 3.7 Flash (High)', 'Gemini 3.1 Pro (High)'],
105
113
  difficulty: ['easy'],
106
114
  maxParallel: 2,
107
115
  rationale: 'Cheap capacity for easy tasks.',
108
116
  },
117
+ {
118
+ model: 'Gemini 4 Argon (Medium)',
119
+ modelCandidates: ['Gemini 4 Argon (Medium)', 'Gemini 3.8 Flash (High)', 'Gemini 3.7 Flash (High)', 'Gemini 3.1 Pro (High)'],
120
+ difficulty: ['medium'],
121
+ maxParallel: 2,
122
+ rationale: 'Balanced Argon tier for medium tasks.',
123
+ },
124
+ {
125
+ model: 'Gemini 4 Argon (High)',
126
+ modelCandidates: ['Gemini 4 Argon (High)', 'Gemini 3.8 Flash (High)', 'Gemini 3.1 Pro (High)'],
127
+ difficulty: ['difficult'],
128
+ maxParallel: 1,
129
+ rationale: 'Reserved for hard tasks; capped at 1 to bound cost.',
130
+ },
109
131
  ],
110
132
  'cursor-cli': [
111
133
  {
@@ -137,6 +159,13 @@ export interface DetectedCliProvider {
137
159
  displayName?: string
138
160
  /** Detected version, when known. Display only — never affects the mapping. */
139
161
  version?: string
162
+ /**
163
+ * Live discovered / resolved `modelOptions` for this provider on the node,
164
+ * when available on the status catalog. When present and non-empty, recipes
165
+ * prefer the first matching model from `[recipe.model, ...recipe.modelCandidates]`
166
+ * that exists in `modelOptions`.
167
+ */
168
+ modelOptions?: readonly string[]
140
169
  }
141
170
 
142
171
  /** One proposed slot plus why it was proposed. */
@@ -197,6 +226,36 @@ function dedupeDetected(detected: readonly DetectedCliProvider[]): DetectedCliPr
197
226
  return out
198
227
  }
199
228
 
229
+ /**
230
+ * Resolve a recipe's pinned model against the provider's live `modelOptions`
231
+ * (populated from the daemon's model-discovery overlay when available). If
232
+ * `modelOptions` is absent or none of the recipe's candidates appear in it,
233
+ * falls back to `recipe.model`.
234
+ */
235
+ function resolveRecipeModel(
236
+ recipe: CliSlotRecipe,
237
+ modelOptions: readonly string[] | undefined,
238
+ ): string | undefined {
239
+ if (!Array.isArray(modelOptions) || modelOptions.length === 0) {
240
+ return recipe.model
241
+ }
242
+ const available = new Set(
243
+ modelOptions
244
+ .filter((m): m is string => typeof m === 'string' && m.trim().length > 0)
245
+ .map(m => m.trim()),
246
+ )
247
+ if (available.size === 0) return recipe.model
248
+ const candidates = [
249
+ ...(recipe.model ? [recipe.model] : []),
250
+ ...(recipe.modelCandidates ?? []),
251
+ ]
252
+ for (const candidate of candidates) {
253
+ const trimmed = typeof candidate === 'string' ? candidate.trim() : ''
254
+ if (trimmed && available.has(trimmed)) return trimmed
255
+ }
256
+ return recipe.model
257
+ }
258
+
200
259
  /**
201
260
  * Build a capability-slot proposal from a node's detected CLI providers.
202
261
  *
@@ -229,7 +288,7 @@ export function buildSlotProposal(
229
288
  // a proposal can never preview a shape the write would reshape.
230
289
  const slot = normalizeNodeCapabilitySlot({
231
290
  provider: provider.type,
232
- model: recipe.model,
291
+ model: resolveRecipeModel(recipe, provider.modelOptions),
233
292
  thinkingLevel: recipe.thinkingLevel,
234
293
  difficulty: recipe.difficulty,
235
294
  maxParallel: recipe.maxParallel,
@@ -40,6 +40,9 @@ export const WORKER_TOOLS = [
40
40
  'git_status',
41
41
  'git_log',
42
42
  'git_diff',
43
+ 'driver_list',
44
+ 'driver_describe',
45
+ 'driver_invoke',
43
46
  ] as const
44
47
 
45
48
  export type WorkerTool = typeof WORKER_TOOLS[number]