@octane-xplat/mcp 0.16.1 → 0.17.1

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/README.md CHANGED
@@ -61,12 +61,12 @@ Options: `--workspace` / `XPLAT_MCP_WORKSPACE` (default cwd), `--app` /
61
61
  | `xplat_select_app` | Explicitly bind an app by id/path/name. Ambiguous or unknown selections fail with the candidate list — the server never picks silently. |
62
62
  | `xplat_capability_matrix` | Per-platform capability entries: build, launch, logs, inspection, screenshot, interaction, and the rest are separate rows with status, detail, and alternatives. |
63
63
  | `xplat_permissions` | Grant states per scope plus the action-time check result and recovery path. |
64
- | `xplat_sessions` | Bound sessions: owned vs attached user-owned, state, build identity. |
64
+ | `xplat_sessions` | Retained sessions and launch progress; optional `sessionId` selects exact status, including failures and ended sessions. |
65
65
  | `xplat_targets` | Launchable targets per platform (simulators, devices, host targets) with availability, missing-toolchain reasons, and busy/ownership state. |
66
66
  | `xplat_build` | Build the bound app for one platform as a job — same commands `xplat build` runs. `requestKey` dedupes retries. |
67
- | `xplat_launch` | Owned dev session on one target as a job; completes at confirmed readiness, session owns the processes until `xplat_session_end`. |
67
+ | `xplat_launch` | Start an owned session and return its durable ID immediately; poll `xplat_sessions` for readiness, end with `xplat_session_end`. |
68
68
  | `xplat_attach` | Bind a compatible already-running dev session (user-owned, preserved). |
69
- | `xplat_logs` | Bounded ordered logs for a session or build job: cursor, limit, stream filter, truncation/reset indicators, `historical` flag for dead sessions. |
69
+ | `xplat_diagnostics` | Retained session discovery; log pages under `logs`, native summaries under `reports`, explicit source `coverage`, and original report reads. Build/action jobs retain their own logs. |
70
70
  | `xplat_dev_actions` | The bound app's live development-action registry: instance identity, revision, declarations (contracts, effects, availability). Empty vs disconnected vs no-bridge are distinct states. |
71
71
  | `xplat_dev_action_invoke` | Invoke an action against an exact app instance + registry revision. Pre-validates input, availability, grants and queue bounds; returns a job (`requestKey` dedupes retries). |
72
72
  | `xplat_dev_action_status` | Invocation status/result — same job data restricted to dev-action jobs, with instance/request identity. |
@@ -102,7 +102,7 @@ snapshot availability, so readiness alone is not a successful retrieval.
102
102
  "name": "xplat_docs_read",
103
103
  "arguments": {
104
104
  "package": "@octane-xplat/ui",
105
- "path": "docs/verify/test-identifiers.md",
105
+ "path": "docs/testing/test-identifiers.md",
106
106
  "limit": 2000
107
107
  }
108
108
  }
@@ -197,8 +197,8 @@ cancel, ownership-safe cleanup, bounded logs, and capability-matrix entries.
197
197
 
198
198
  Readiness is probe/marker-confirmed (`ready` session state) — HTTP 200 for
199
199
  web, the host-start marker for macOS, `Successfully synced application` /
200
- first `CONSOLE` line for `ns run`. A timeout or pre-ready exit fails the job
201
- and ends the owned session honestly. Chromium is located via
200
+ first `CONSOLE` line for `ns run`. A timeout or pre-ready exit marks the session `failed`
201
+ and releases its owned resources. Chromium is located via
202
202
  `XPLAT_MCP_CHROMIUM`, the Playwright browser cache, or system installs;
203
203
  without it the web session still reports the server URL with an explicit
204
204
  `browser.launched: false`.
@@ -207,6 +207,21 @@ Physical iOS devices, Windows attach detection, and non-simulator iOS log
207
207
  taps are reported limitations, not silent gaps — the capability matrix
208
208
  carries the detail and an alternative.
209
209
 
210
+ Launch accepts an optional exact target ID or platform name. Platform or omitted
211
+ selection requires exactly one eligible target; ambiguity returns candidates.
212
+ Use the same session ID for status, diagnostics, and cleanup:
213
+
214
+ ```js
215
+ const launch = await client.callTool({name: "xplat_launch", arguments: {target: "ios"}});
216
+ const {sessionId} = launch.structuredContent.data.session;
217
+ await client.callTool({name: "xplat_sessions", arguments: {sessionId}});
218
+ await client.callTool({name: "xplat_diagnostics", arguments: {sessionId}});
219
+ await client.callTool({name: "xplat_session_end", arguments: {sessionId}});
220
+ ```
221
+
222
+ Ending a session
223
+ also cancels startup when it has not reached readiness.
224
+
210
225
  ## Inspect and interact with Chromium
211
226
 
212
227
  This development package adds `xplat_inspect`, `xplat_interact`, and
@@ -17,7 +17,7 @@ function registerRuntimeTools(server, context, respond) {
17
17
  });
18
18
  });
19
19
  server.registerTool("xplat_build", {
20
- description: "Build the bound app for one platform as a background job (the same commands `xplat build` runs). Returns a job id — poll xplat_job_status and read output with xplat_logs. Repeating with the same requestKey returns the live job instead of duplicating it.",
20
+ description: "Build the bound app for one platform as a background job (the same commands `xplat build` runs). Returns a job id — poll xplat_job_status and read output with xplat_diagnostics. Repeating with the same requestKey returns the live job instead of duplicating it.",
21
21
  inputSchema: {
22
22
  platform: z.enum(identity_exports.PLATFORMS),
23
23
  requestKey: z.string().optional().describe("Idempotence key for retries")
@@ -36,17 +36,15 @@ function registerRuntimeTools(server, context, respond) {
36
36
  });
37
37
  });
38
38
  server.registerTool("xplat_launch", {
39
- description: "Start an owned development session: bind one app to one target and run the platform dev command as a job. The job completes when readiness is confirmed — a failed launch after a successful build reports accurately, and the session keeps the app alive until xplat_session_end.",
39
+ description: "Start an owned development session: return one durable session id immediately. Read launch progress and readiness through xplat_sessions; diagnostics use the same session id, including after shutdown.",
40
40
  inputSchema: {
41
- platform: z.enum(identity_exports.PLATFORMS),
42
- targetId: z.string().optional().describe("Target from xplat_targets; required when several are available"),
41
+ target: z.string().optional().describe("Exact discovered target id or platform name; omit only when one eligible target exists"),
43
42
  requestKey: z.string().optional().describe("Idempotence key for retries"),
44
43
  browserMode: z.enum(["headless", "headed"]).optional().describe("Web only. Headed mode enables scoped OS keyboard input on qualified hosts; reserve desktop input before using it.")
45
44
  }
46
- }, async ({ platform, targetId, requestKey, browserMode }) => {
45
+ }, async ({ target, requestKey, browserMode }) => {
47
46
  const result = await runtime.launch({
48
- platform,
49
- targetId,
47
+ target,
50
48
  requestKey,
51
49
  browserMode
52
50
  });
@@ -77,12 +75,14 @@ function registerRuntimeTools(server, context, respond) {
77
75
  data: result
78
76
  });
79
77
  });
80
- server.registerTool("xplat_logs", {
81
- description: "Bounded, ordered log read for a session or build job. Cursor-paginated: pass the previous nextCursor to continue; truncated/dropped flags reveal gaps and reset marks a restarted capture. A stopped or disconnected session reports its capture as historical.",
78
+ server.registerTool("xplat_diagnostics", {
79
+ description: "List retained sessions or read logs and existing native report summaries. Log cursors do not page reports; reportId with sessionId reads a retained original. Session selectors include retained logs and native reports; standalone build jobs have log captures only.",
82
80
  inputSchema: {
81
+ reportId: z.string().optional(),
82
+ platform: z.enum(identity_exports.PLATFORMS).optional(),
83
83
  sessionId: z.string().optional(),
84
84
  jobId: z.string().optional(),
85
- cursor: z.number().optional().describe("Continue after this record seq"),
85
+ cursor: z.number().int().nonnegative().optional(),
86
86
  limit: z.number().int().positive().max(5e3).optional(),
87
87
  stream: z.enum([
88
88
  "stdout",
@@ -90,22 +90,8 @@ function registerRuntimeTools(server, context, respond) {
90
90
  "lifecycle"
91
91
  ]).optional()
92
92
  }
93
- }, async ({ sessionId, jobId, cursor, limit, stream }) => {
94
- if (!sessionId && !jobId) return respond({
95
- ok: false,
96
- error: {
97
- code: "invalid_selection",
98
- message: "Pass a sessionId or a jobId.",
99
- recovery: { action: "Use xplat_sessions or a build/launch job id." }
100
- }
101
- });
102
- const result = runtime.readLogs({
103
- sessionId,
104
- jobId,
105
- cursor,
106
- limit,
107
- stream
108
- });
93
+ }, async (args) => {
94
+ const result = await runtime.readDiagnostics(args);
109
95
  return result.error ? respond({
110
96
  ok: false,
111
97
  error: result.error
package/dist/server.js CHANGED
@@ -15,7 +15,7 @@ import { dirname, join } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  //#region src/server.ts
17
17
  var SERVER_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8")).version;
18
- var INSTRUCTIONS = "MCP server for Octane Xplat apps. Call xplat_project_status first — it reports detected apps, framework versions, guidance, prerequisites, and capabilities. If several apps are detected, ask the developer which to use and bind it with xplat_select_app; never guess. With a dev-mode app running, xplat_dev_actions lists the app-declared action registry and xplat_dev_action_invoke runs one against an exact instance and registry revision. For runtime work, check xplat_capability_matrix then xplat_targets; xplat_launch starts an owned session as a job and xplat_attach binds a session you already run. Results always say what was actually checked; a successful build is not proof of behavior.";
18
+ var INSTRUCTIONS = "MCP server for Octane Xplat apps. Call xplat_project_status first — it reports detected apps, framework versions, guidance, prerequisites, and capabilities. If several apps are detected, ask the developer which to use and bind it with xplat_select_app; never guess. With a dev-mode app running, xplat_dev_actions lists the app-declared action registry and xplat_dev_action_invoke runs one against an exact instance and registry revision. For runtime work, check xplat_capability_matrix then xplat_targets; xplat_launch returns a durable session identity and launch progress and xplat_attach binds a session you already run. Results always say what was actually checked; a successful build is not proof of behavior.";
19
19
  var respond = (outcome) => ({
20
20
  structuredContent: outcome,
21
21
  isError: outcome.ok === false,
@@ -129,9 +129,12 @@ function createXplatMcpServer(context) {
129
129
  });
130
130
  });
131
131
  server.registerTool("xplat_sessions", {
132
- description: "List bound sessions: owned sessions the server started and attached user-owned sessions. Runtime tools bind one app to one target; attached sessions are preserved on end.",
133
- inputSchema: {}
134
- }, async () => ok({ sessions: context.sessions.list() }));
132
+ description: "List retained sessions or read exact sessionId status, including launch progress, failures and ended sessions. Attached user-owned apps are preserved on end.",
133
+ inputSchema: { sessionId: z.string().optional() }
134
+ }, async ({ sessionId }) => {
135
+ const result = sessionId ? context.runtime.sessionStatus(sessionId) : context.runtime.listSessions();
136
+ return result.error ? fail(result.error) : ok(result);
137
+ });
135
138
  server.registerTool("xplat_job_status", {
136
139
  description: "Bounded status query for a job started by a runtime tool.",
137
140
  inputSchema: { jobId: z.string() }
@@ -150,9 +153,12 @@ function createXplatMcpServer(context) {
150
153
  description: "End a session with ownership-safe cleanup: owned sessions release their resources; attached user-owned sessions are detached and preserved.",
151
154
  inputSchema: { sessionId: z.string() }
152
155
  }, async ({ sessionId }) => {
153
- const usable = context.sessions.requireUsable(sessionId);
154
- if (!usable.usable && usable.error.code === "not_found") return fail(usable.error);
155
- return ok({ result: await context.runtime.endSession(sessionId) });
156
+ try {
157
+ const result = await context.runtime.endSession(sessionId);
158
+ return result.error ? fail(result.error) : ok({ result });
159
+ } catch (error) {
160
+ return fail((0, errors_exports.toContractError)(error));
161
+ }
156
162
  });
157
163
  server.registerTool("xplat_dev_actions", {
158
164
  description: "List the bound app's declared development actions — the live registry announced by a running dev app: stable ids, titles, input/result contracts, effects, availability with reasons, and the registry revision. Never executes an action. An empty registry is reported distinctly from a disconnected app.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/mcp",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
4
4
  "description": "MCP server for Octane Xplat consumer apps — project identity, capability discovery, permission and evidence contracts.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "1.31.0",
37
- "@octane-xplat/cli": "0.16.1",
37
+ "@octane-xplat/cli": "0.17.1",
38
38
  "yaml": "2.9.1",
39
39
  "zod": "4.6.5"
40
40
  },
package/src/context.ts CHANGED
@@ -30,13 +30,16 @@ export interface RuntimeService {
30
30
  listTargets(args: { platform?: Platform }): Promise<any>
31
31
  build(args: { platform: Platform; requestKey?: string }): Promise<any>
32
32
  launch(args: {
33
- platform: Platform
34
- targetId?: string
33
+ target?: string
35
34
  requestKey?: string
36
35
  browserMode?: 'headless' | 'headed'
37
36
  }): Promise<any>
37
+ listSessions(): any
38
+ sessionStatus(sessionId: string): any
38
39
  attach(args: { platform: Platform; targetId?: string }): Promise<any>
39
- readLogs(args: {
40
+ readDiagnostics(args: {
41
+ reportId?: string
42
+ platform?: Platform
40
43
  sessionId?: string
41
44
  jobId?: string
42
45
  cursor?: number
@@ -39,7 +39,7 @@ export function registerRuntimeTools(
39
39
  'xplat_build',
40
40
  {
41
41
  description:
42
- 'Build the bound app for one platform as a background job (the same commands `xplat build` runs). Returns a job id — poll xplat_job_status and read output with xplat_logs. Repeating with the same requestKey returns the live job instead of duplicating it.',
42
+ 'Build the bound app for one platform as a background job (the same commands `xplat build` runs). Returns a job id — poll xplat_job_status and read output with xplat_diagnostics. Repeating with the same requestKey returns the live job instead of duplicating it.',
43
43
  inputSchema: {
44
44
  platform: z.enum(PLATFORMS as Platform[]),
45
45
  requestKey: z.string().optional().describe('Idempotence key for retries'),
@@ -57,13 +57,14 @@ export function registerRuntimeTools(
57
57
  'xplat_launch',
58
58
  {
59
59
  description:
60
- 'Start an owned development session: bind one app to one target and run the platform dev command as a job. The job completes when readiness is confirmed — a failed launch after a successful build reports accurately, and the session keeps the app alive until xplat_session_end.',
60
+ 'Start an owned development session: return one durable session id immediately. Read launch progress and readiness through xplat_sessions; diagnostics use the same session id, including after shutdown.',
61
61
  inputSchema: {
62
- platform: z.enum(PLATFORMS as Platform[]),
63
- targetId: z
62
+ target: z
64
63
  .string()
65
64
  .optional()
66
- .describe('Target from xplat_targets; required when several are available'),
65
+ .describe(
66
+ 'Exact discovered target id or platform name; omit only when one eligible target exists',
67
+ ),
67
68
  requestKey: z.string().optional().describe('Idempotence key for retries'),
68
69
  browserMode: z
69
70
  .enum(['headless', 'headed'])
@@ -73,8 +74,8 @@ export function registerRuntimeTools(
73
74
  ),
74
75
  },
75
76
  },
76
- async ({ platform, targetId, requestKey, browserMode }) => {
77
- const result = await runtime.launch({ platform, targetId, requestKey, browserMode })
77
+ async ({ target, requestKey, browserMode }) => {
78
+ const result = await runtime.launch({ target, requestKey, browserMode })
78
79
  return result.error
79
80
  ? respond({ ok: false, error: result.error })
80
81
  : respond({ ok: true, data: result })
@@ -103,31 +104,22 @@ export function registerRuntimeTools(
103
104
  )
104
105
 
105
106
  server.registerTool(
106
- 'xplat_logs',
107
+ 'xplat_diagnostics',
107
108
  {
108
109
  description:
109
- 'Bounded, ordered log read for a session or build job. Cursor-paginated: pass the previous nextCursor to continue; truncated/dropped flags reveal gaps and reset marks a restarted capture. A stopped or disconnected session reports its capture as historical.',
110
+ 'List retained sessions or read logs and existing native report summaries. Log cursors do not page reports; reportId with sessionId reads a retained original. Session selectors include retained logs and native reports; standalone build jobs have log captures only.',
110
111
  inputSchema: {
112
+ reportId: z.string().optional(),
113
+ platform: z.enum(PLATFORMS as Platform[]).optional(),
111
114
  sessionId: z.string().optional(),
112
115
  jobId: z.string().optional(),
113
- cursor: z.number().optional().describe('Continue after this record seq'),
116
+ cursor: z.number().int().nonnegative().optional(),
114
117
  limit: z.number().int().positive().max(5000).optional(),
115
118
  stream: z.enum(['stdout', 'stderr', 'lifecycle']).optional(),
116
119
  },
117
120
  },
118
- async ({ sessionId, jobId, cursor, limit, stream }) => {
119
- if (!sessionId && !jobId) {
120
- return respond({
121
- ok: false,
122
- error: {
123
- code: 'invalid_selection',
124
- message: 'Pass a sessionId or a jobId.',
125
- recovery: { action: 'Use xplat_sessions or a build/launch job id.' },
126
- },
127
- })
128
- }
129
-
130
- const result = runtime.readLogs({ sessionId, jobId, cursor, limit, stream })
121
+ async (args) => {
122
+ const result = await runtime.readDiagnostics(args)
131
123
  return result.error
132
124
  ? respond({ ok: false, error: result.error })
133
125
  : respond({ ok: true, data: result })
package/src/server.ts CHANGED
@@ -31,7 +31,7 @@ const INSTRUCTIONS =
31
31
  'MCP server for Octane Xplat apps. Call xplat_project_status first — it reports detected apps, framework versions, guidance, prerequisites, and capabilities. ' +
32
32
  'If several apps are detected, ask the developer which to use and bind it with xplat_select_app; never guess. ' +
33
33
  'With a dev-mode app running, xplat_dev_actions lists the app-declared action registry and xplat_dev_action_invoke runs one against an exact instance and registry revision. ' +
34
- 'For runtime work, check xplat_capability_matrix then xplat_targets; xplat_launch starts an owned session as a job and xplat_attach binds a session you already run. ' +
34
+ 'For runtime work, check xplat_capability_matrix then xplat_targets; xplat_launch returns a durable session identity and launch progress and xplat_attach binds a session you already run. ' +
35
35
  'Results always say what was actually checked; a successful build is not proof of behavior.'
36
36
 
37
37
  const respond: Respond = (outcome) => ({
@@ -216,10 +216,15 @@ export function createXplatMcpServer(context: WorkspaceContext) {
216
216
  'xplat_sessions',
217
217
  {
218
218
  description:
219
- 'List bound sessions: owned sessions the server started and attached user-owned sessions. Runtime tools bind one app to one target; attached sessions are preserved on end.',
220
- inputSchema: {},
219
+ 'List retained sessions or read exact sessionId status, including launch progress, failures and ended sessions. Attached user-owned apps are preserved on end.',
220
+ inputSchema: { sessionId: z.string().optional() },
221
+ },
222
+ async ({ sessionId }) => {
223
+ const result = sessionId
224
+ ? context.runtime.sessionStatus(sessionId)
225
+ : context.runtime.listSessions()
226
+ return result.error ? fail(result.error) : ok(result)
221
227
  },
222
- async () => ok({ sessions: context.sessions.list() }),
223
228
  )
224
229
 
225
230
  server.registerTool(
@@ -261,12 +266,12 @@ export function createXplatMcpServer(context: WorkspaceContext) {
261
266
  inputSchema: { sessionId: z.string() },
262
267
  },
263
268
  async ({ sessionId }) => {
264
- const usable = context.sessions.requireUsable(sessionId)
265
- if (!usable.usable && usable.error.code === 'not_found') {
266
- return fail(usable.error)
269
+ try {
270
+ const result = await context.runtime.endSession(sessionId)
271
+ return result.error ? fail(result.error) : ok({ result })
272
+ } catch (error) {
273
+ return fail(toContractError(error))
267
274
  }
268
-
269
- return ok({ result: await context.runtime.endSession(sessionId) })
270
275
  },
271
276
  )
272
277
 
@@ -163,13 +163,26 @@ export type DevActionInvocationStatus =
163
163
  /** The app may or may not have run the action; reported, never silently retried. */
164
164
  | 'unknown'
165
165
 
166
+ /** One handler's failure as structured data — which owner threw, and the error it threw. */
167
+ export interface DevActionFailure {
168
+ /** 0-based position, in attach order, of the failing handler among the action's handlers at dispatch. */
169
+ handler: number
170
+ /** Optional owner label (e.g. component name) supplied at attach time. */
171
+ label?: string
172
+ name: string
173
+ message: string
174
+ stack?: string
175
+ /** Stack with frames resolved to original source positions by the dev server; present only when at least one frame resolved (web today). */
176
+ sourceStack?: string
177
+ }
178
+
166
179
  export interface DevActionOutcome {
167
180
  requestId: string
168
181
  status: DevActionInvocationStatus
169
182
  /** Structured result validated against the declared result contract. */
170
183
  result?: Record<string, unknown>
171
184
  /** Machine-readable failure detail; mapped to ContractError codes server-side. */
172
- error?: { code: string; message: string }
185
+ error?: { code: string; message: string; failures?: DevActionFailure[] }
173
186
  /** What the app knows changed before a failure/cancellation; absent = unknown. */
174
187
  partialEffects?: string[]
175
188
  completedAt: string
@@ -7,7 +7,7 @@
7
7
  * preserves it). Restarting always establishes a new session identity; tools
8
8
  * refuse operations against stopped or disconnected sessions.
9
9
  *
10
- * Jobs are bounded units of work (build, launch, dev-action, …) whose status
10
+ * Jobs are bounded units of work (build, dev-action, …) whose status
11
11
  * is queryable without holding a tool call open. Repeating a request with the
12
12
  * same `requestKey` returns the live job instead of duplicating it.
13
13
  * Cancellation is cooperative: `cancelling` and `cancelled` are distinct
@@ -25,6 +25,8 @@ export const JOB_TERMINAL_STATES: readonly JobState[]
25
25
  export type SessionState =
26
26
  /** Bound but not yet ready for observation/interaction. */
27
27
  | 'starting'
28
+ /** Launch failed; retained diagnostics remain readable. */
29
+ | 'failed'
28
30
  /** Ready — inspection, logs, and authorized interactions may proceed. */
29
31
  | 'ready'
30
32
  /** Lost contact; last-known data is historical evidence, never current state. */
@@ -57,6 +59,7 @@ export interface SessionBinding {
57
59
  processKind: 'fresh' | 'warm' | null
58
60
  buildIdentity?: BuildIdentity
59
61
  detail?: string
62
+ launch?: { phase?: string; error?: ContractError; [key: string]: unknown }
60
63
  }
61
64
 
62
65
  export interface EndSessionResult {
@@ -78,7 +81,7 @@ export interface JobProgress {
78
81
 
79
82
  export interface Job {
80
83
  jobId: string
81
- /** e.g. `build`, `launch`, `attach`, `dev-action`. */
84
+ /** e.g. `build`, `dev-action`. */
82
85
  kind: string
83
86
  sessionId?: string
84
87
  /** Idempotence key supplied by the caller or adapter. */
@@ -113,6 +116,7 @@ export interface StartJobResult {
113
116
  export interface SessionRegistry {
114
117
  /** Start a server-owned session in `starting` state. `cleanup` runs on end/shutdown and must only release resources this session created. */
115
118
  bindOwned(input: {
119
+ sessionId?: string
116
120
  appId: string
117
121
  platform: Platform
118
122
  targetId: string
@@ -133,6 +137,9 @@ export interface SessionRegistry {
133
137
  requireUsable(
134
138
  sessionId: string,
135
139
  ): { usable: true; session: SessionBinding } | { usable: false; error: ContractError }
140
+ watch(fn: (session: SessionBinding) => void): () => void
141
+ setLaunch(sessionId: string, progress: Record<string, unknown>): void
142
+ markFailed(sessionId: string, error: ContractError): void
136
143
  markReady(
137
144
  sessionId: string,
138
145
  meta?: { processKind?: 'fresh' | 'warm'; buildIdentity?: BuildIdentity },