@vostride/agent-qa-dashboard 0.1.21 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,43 +1,27 @@
1
- import { D as DashboardDatabase } from './index-ygQLgXzT.js';
2
- export { A as AppiumManager, C as ConfigManager, L as LiveExecutionEvent, R as RunRow, S as StepRow, T as TestFileInfo, a as TestFileManager, b as TestRunHandle, c as TestRunner, d as TestValidationError, e as TestValidationResult, f as createRouter, r as readJsonBody, s as startServer } from './index-ygQLgXzT.js';
3
- import { Reporter, SecretRedactor, SuiteDefinition, RunArtifactReporterContext, SuiteSummary, TestDefinition, StepResult, TestResult, RunSummary, HookResultEvent } from '@vostride/agent-qa-core';
4
- import 'node:http';
5
- import '@vostride/agent-qa-mcp';
1
+ import { D as DashboardDatabase, E as ExecutionBackend, a as ExecuteOptions, P as PreflightOptions, A as AgentHost, b as PostProcessingRelay, L as LiveEditorAttachment } from './index-Ds4Lv4mT.js';
2
+ export { c as AgentRegistryHandle, d as AppiumManager, C as ConfigFileInvalidError, e as ConfigManager, f as LIVE_EDITOR_SUPERSEDED_CLOSE_CODE, g as LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE, h as LiveEditorSubjects, M as MintLiveUrlInput, i as PostProcessingListener, R as RUN_SERVICE_ANALYSIS_TEMPLATE, j as RUN_SERVICE_ARTIFACT_TEMPLATE, k as RUN_SERVICE_DETAIL_TEMPLATE, l as RUN_SERVICE_EVENTS_TEMPLATE, m as RUN_SERVICE_EVIDENCE_TEMPLATE, n as RUN_SERVICE_LIVE_URL_TEMPLATE, o as RUN_SERVICE_MARKS_TEMPLATE, p as RUN_SERVICE_MARK_PREVIEW_TEMPLATE, q as RUN_SERVICE_OK_ROUTE, r as RUN_SERVICE_ROUTE_TABLE, s as RUN_SERVICE_START_ROUTE, t as ReplaceDocumentOutcome, u as ResumePoint, v as RunOwner, w as RunOwnerProbe, x as RunOwnerState, y as RunOwnership, z as RunOwnershipInput, B as RunRow, F as RunServicePorts, G as RunServiceRouteHandlerName, H as RunServiceRouteMatch, I as RunServiceRouteRow, J as RunServiceRouter, K as RunServiceStartInput, N as RunSettledDetails, O as RunSettledStatus, Q as RunsDatabaseVersionRefusedError, T as TestFileInfo, S as TestFileManager, U as TestValidationError, V as TestValidationResult, W as backstopServerError, X as classifyRunOwner, Y as completeRunOwnership, Z as createLiveEditorSubjects, _ as createPostProcessingRelay, $ as createRouter, a0 as createRunServiceRoutes, a1 as defaultRunOwnership, a2 as matchRunServiceRoute, a3 as orphanSummary, a4 as ownedElsewhere, a5 as ownsRunServicePath, a6 as readConfigValues, a7 as readJsonBody, a8 as refuseUnparseableRequest, a9 as runDenialOf, aa as serializeConfigWrite, ab as startServer, ac as tryParseRequestUrl } from './index-Ds4Lv4mT.js';
3
+ import { RunAttributes } from '@vostride/agent-qa-core';
4
+ import { RunEventEnvelope, RunArtifactConfigSnapshot, ArtifactRecord, RunArtifactTestSourceSnapshot, RunArtifactSuiteSourceSnapshot, RunArtifactTestsSourceSnapshot, RunSession, PostProcessingStatus, PostProcessingDisposition, RunId, RunDetailRows, RunsArtifactResult, RunDetailProjection, RunsArtifactInput, LiveViewSubjectRef, LiveUrlVerdict, LiveUrlRefusalReason, LiveStreamDeclaration, LiveStreamKind, RunsMarkOutcome } from '@vostride/agent-qa-schema';
5
+ import { EventEmitter } from 'node:events';
6
+ import { IncomingMessage, ServerResponse } from 'node:http';
7
+ import WebSocket from 'ws';
8
+ import '@vostride/agent-qa-runner';
6
9
  import 'yaml';
7
- import 'node:events';
8
- import 'node:child_process';
9
10
 
10
- declare const SCHEMA_VERSION = 1;
11
+ declare const SCHEMA_VERSION = 2;
11
12
  interface DashboardMigration {
12
13
  version: number;
13
14
  sql: string[];
14
15
  }
15
16
  declare const MIGRATIONS: DashboardMigration[];
16
17
 
17
- interface RunIdMigrationOptions {
18
- dbPath: string;
19
- screenshotsDir?: string;
20
- videosDir?: string;
21
- logger?: (message: string) => void;
22
- }
23
- interface RunIdMigrationResult {
24
- migratedRunCount: number;
25
- totalRunCount: number;
26
- runIdMap: Record<string, string>;
27
- preservedRunIds: string[];
28
- updatedScreenshotRows: number;
29
- updatedVideoRows: number;
30
- renamedScreenshotDirs: Array<{
31
- from: string;
32
- to: string;
33
- }>;
34
- renamedVideoDirs: Array<{
35
- from: string;
36
- to: string;
37
- }>;
38
- missingArtifacts: string[];
39
- }
40
- declare function migrateWorkspaceRunIds({ dbPath, screenshotsDir, videosDir, logger, }: RunIdMigrationOptions): Promise<RunIdMigrationResult>;
18
+ declare function createRunOwnerHostResolver({ platform, hostname, readPlatformUuid, readPidNamespace, }?: {
19
+ platform?: NodeJS.Platform;
20
+ hostname?: () => string;
21
+ readPlatformUuid?: () => string | undefined;
22
+ readPidNamespace?: () => string | undefined;
23
+ }): () => string;
24
+ declare const resolveRunOwnerHost: () => string;
41
25
 
42
26
  interface ResolveDashboardDbPathOptions {
43
27
  configDir: string;
@@ -45,56 +29,535 @@ interface ResolveDashboardDbPathOptions {
45
29
  }
46
30
  declare function resolveDashboardDbPath({ configDir, configuredDbPath, }: ResolveDashboardDbPathOptions): string;
47
31
 
48
- type ArtifactReporterContext = RunArtifactReporterContext;
49
- declare class DashboardReporter implements Reporter {
50
- private db;
51
- private artifactsDir;
52
- private suiteRunId;
53
- private currentSuiteId;
54
- private runIdToState;
55
- private executionKeyToRunId;
56
- private nameToCurrentRunId;
57
- private completedResultRunIds;
58
- private runStartTime;
59
- private onRunCreated?;
60
- private redactor?;
61
- constructor({ db, artifactsDir, onRunCreated, redactor, }: {
62
- db: DashboardDatabase;
63
- artifactsDir?: string;
64
- onRunCreated?: (runId: string) => void;
65
- redactor?: SecretRedactor;
66
- });
67
- private redactValue;
68
- private redactString;
69
- private sanitizeSecretsFileMetadata;
70
- private sanitizeArtifactPayload;
71
- private fallbackAttributes;
72
- private readAttributesFromEnv;
73
- private resolveAttributes;
74
- private withAttributeMetadata;
75
- onSuiteStart(suite: SuiteDefinition, context?: ArtifactReporterContext): void;
76
- onSuiteEnd(summary: SuiteSummary): void;
77
- onRunStart(_tests: TestDefinition[]): void;
78
- onTestStart(test: TestDefinition, filePath: string, context?: ArtifactReporterContext): Promise<void>;
79
- onStepEnd(result: StepResult, testName: string): Promise<void>;
80
- onTestEnd(result: TestResult): Promise<void>;
81
- onRunEnd(summary: RunSummary): void;
82
- onHookEnd(event: HookResultEvent): void;
83
- private getCurrentRunId;
84
- private createExecutionKey;
85
- private getStateByTestName;
86
- private getStateForResult;
87
- private deleteState;
88
- private resultKey;
89
- private buildResultErrorLog;
90
- private writeArtifact;
91
- private stageSuiteChildLink;
92
- private stageSuiteSummary;
93
- private buildSuiteMemoryAggregate;
94
- private persistStep;
95
- private saveScreenshot;
96
- private saveHealingScreenshot;
97
- private saveSubActionScreenshot;
32
+ interface RunIdentityMember {
33
+ name: string;
34
+ testId: string;
35
+ filePath: string;
36
+ suiteId?: string;
37
+ suiteName?: string;
98
38
  }
39
+ interface RunRowIdentity {
40
+ name: string;
41
+ testId?: string;
42
+ filePath?: string;
43
+ suiteId?: string;
44
+ }
45
+ declare function formatRunTitle(names: readonly string[]): string;
46
+ declare function runRowIdentity(members: readonly RunIdentityMember[]): RunRowIdentity;
47
+
48
+ /**
49
+ * `createDashboardRunIngest` — the dashboard's DB ingestion (RESEARCH Pitfall 1's
50
+ * "unnamed fifth consumer") rewired from the deleted `DashboardReporter` into a bus
51
+ * subscriber (Open Q-1 resolved as option (a): one envelope vocabulary, D-16 precedent).
52
+ *
53
+ * The bus deliberately does not carry the resolved config/source snapshots or the run's
54
+ * own id; those ride construction. Envelopes drive lifecycle/status.
55
+ * The `runs` + `run_artifacts` rows this writes stay shape-compatible with what
56
+ * `GET /api/runs/:id/artifact` and the queue read today — the spawn must survive
57
+ * until M5 (Pitfall 1's hard requirement). This module implements no Reporter
58
+ * interface — pure envelope consumption.
59
+ *
60
+ * NOTE: Plan 06 (363-06) relocated the `RunArtifact*` payload family from core to
61
+ * @vostride/agent-qa-schema (D-08); these imports now point at their canonical home.
62
+ *
63
+ * 2026-08-27 (Phase 405 plan 01, D-01/D-17/D-18): THE PROJECTION IS REBUILT ON THE BUS'S OWN
64
+ * SHAPE. Until this date every step landed in the v1 wide `steps` row through `insertStep`, which
65
+ * kept five of `StepFinalizedPayload`'s fields and dropped the rest — because the local `*Like`
66
+ * interfaces this module declared only named five. There was no `subActionRecorded` arm at all, so
67
+ * the sub-action timeline, the screenshots and the screen-state hashes reached no reader.
68
+ *
69
+ * What replaced them: the real schema payload types (so a field added to a payload is a compile
70
+ * error here rather than a silent drop), `run_members` per handle from `testStarted`/`testFinished`
71
+ * (D-18: a thin member whose stream begins with a terminal event still gets a row), `run_steps`
72
+ * carrying the WHOLE `stepFinalized` payload with `status` stored as the payload's own word, and
73
+ * `run_sub_actions` carrying the whole `subActionRecorded` payload with its nested objects
74
+ * stringified verbatim (no field is re-interpreted on the way in). The run row stores the verdict's
75
+ * own word too.
76
+ *
77
+ * `testRetryScheduled` now drops only THAT HANDLE's rows (D-17). It used to call the v1
78
+ * whole-run step delete, which wiped every step of the RUN — so in a suite, one member's
79
+ * abandoned attempt deleted the completed history of every member before it.
80
+ *
81
+ * THE NON-DARKENING `try {} catch {}` AROUND EVERY PROJECTION INSERT IS UNCHANGED and is
82
+ * load-bearing: a projection row that fails to write must never darken the `runs` row. A dropped
83
+ * step is visible as an absent step; a darkened run is a wrong verdict.
84
+ *
85
+ * 2026-08-28 (Phase 405 plan 13, D-21): phase 405 removed tables, columns, routes and fields from
86
+ * around this module and deliberately kept others. Check the repository history before restoring
87
+ * anything that looks missing here.
88
+ */
89
+ type SourceSnapshot = RunArtifactTestSourceSnapshot | RunArtifactSuiteSourceSnapshot | RunArtifactTestsSourceSnapshot | ArtifactRecord;
90
+ type ConfigSnapshot = RunArtifactConfigSnapshot | ArtifactRecord;
91
+ /**
92
+ * A run's loaded test/suite and config snapshot, handed over by the producer that resolved them
93
+ * (433-29, G-433-5). It arrives out-of-band, like `finalize()`'s runtime: the bus carries no snapshots.
94
+ */
95
+ interface DashboardRunIngestSnapshots {
96
+ config?: ConfigSnapshot;
97
+ source?: SourceSnapshot;
98
+ }
99
+ interface DashboardRunIngestDeps {
100
+ db: DashboardDatabase;
101
+ runId: string;
102
+ /** Resolved config snapshot (RunArtifactPayload-compatible) — the bus never carries it. */
103
+ configSnapshot?: ConfigSnapshot;
104
+ /** Resolved test/suite source snapshot — the bus never carries it. */
105
+ sourceSnapshot?: SourceSnapshot;
106
+ members?: readonly RunIdentityMember[];
107
+ /** Trusted run attributes, written to the run row + artifact metadata. */
108
+ attributes?: RunAttributes;
109
+ /** Fired once the primary DB row exists (log correlation / live-view wiring). */
110
+ onRunCreated?: (runId: string) => void;
111
+ /** Injectable clock for deterministic tests. */
112
+ now?: () => string;
113
+ }
114
+ /**
115
+ * The post-run section the CLI closure supplies to `finalize()` — data the bus never carries,
116
+ * so it arrives out-of-band at flush.
117
+ *
118
+ * The v1 `memory` member is DELETED (407-05 · D-02). It was the last writer feeding the dead
119
+ * artifact-drawer memory tab, no producer in `packages/cli` or `packages/runner-local` has passed
120
+ * it since v1, and a field only a test could supply is a contract claiming a producer that does
121
+ * not exist. `runs.memory_log` has NO remaining production writer either (407-16 corrects 407-05's
122
+ * claim of a second writer): its insights reader is historical-only, reporting zeroes on every
123
+ * install created since, and its retirement belongs to the insights v1 sweep.
124
+ */
125
+ interface DashboardRunIngestFinalizeExtra {
126
+ runtime?: ArtifactRecord;
127
+ }
128
+ interface DashboardRunIngest {
129
+ handler(envelope: RunEventEnvelope): void;
130
+ /**
131
+ * Stage a loaded snapshot onto this run's own artifact (433-29, F-433-15). A section is written
132
+ * only where it is absent or still the queue's `loadStatus: 'queued'` placeholder, so the first
133
+ * snapshot wins and a finalized artifact is never touched. A call before `runStarted` is held.
134
+ * A run of several members outside one suite instead files each snapshot under its member
135
+ * (444-03, D-07).
136
+ */
137
+ stageSnapshots(input: DashboardRunIngestSnapshots): void;
138
+ finalize(extra?: DashboardRunIngestFinalizeExtra): Promise<void>;
139
+ }
140
+ declare function createDashboardRunIngest(deps: DashboardRunIngestDeps): DashboardRunIngest;
141
+
142
+ type InProcessRunStatus = 'running' | 'completed' | 'failed' | 'cancelled' | 'timeout';
143
+ /** What the injected starter is handed: the request as data, and where to hand the session back. */
144
+ interface InProcessRunStartInput {
145
+ readonly opts: ExecuteOptions;
146
+ /**
147
+ * Called with the started session, as early as the run door can name it.
148
+ *
149
+ * A run that is DENIED never calls this, and that asymmetry is load-bearing: it is the only way
150
+ * the backend can tell a policy refusal apart from a run that started and then failed.
151
+ *
152
+ * (Phrased without a quoted clause after the word `from`: `validate:arch`'s dependency scanner
153
+ * reads that shape as an import specifier and reported this comment as an undeclared external
154
+ * dependency of `packages/dashboard-server`.)
155
+ */
156
+ readonly onSession: (session: RunSession) => void;
157
+ }
158
+ /**
159
+ * Start one run and resolve when it is over.
160
+ *
161
+ * REJECTION IS NOT "the run failed" — a failing run resolves. A rejection is a denial, a refused
162
+ * request field, an input error or a crash, and its message is what the dashboard shows as the
163
+ * failure summary. That is the whole user-visible win of the retirement: on the retiring wire a
164
+ * policy denial and a run that never started were the same nothing.
165
+ */
166
+ type InProcessRunStarter = (input: InProcessRunStartInput) => Promise<void>;
167
+ interface InProcessExecutionBackendOptions {
168
+ /**
169
+ * How this backend actually starts a run.
170
+ *
171
+ * NAMED `invokeRun` AND NOT `startRun`, which is not a stylistic choice. `startRun` is the RUN
172
+ * SHELL's entry point in `@vostride/agent-qa-runner`, and `run-door-invocation-census.test.ts`
173
+ * makes a complete-set claim over the whole CLI package that no file acquires it — the one legal
174
+ * call site is inside the invocation core, one line below the one `evaluate()`. A second thing
175
+ * called `startRun`, in the CLI file that composes this option, REDS that census; and the census
176
+ * is right to red, because a second spelling of somebody else's symbol is exactly what it exists
177
+ * to refuse. `invokeRun` also says what this one does: it invokes the `runs.start` OP (D-10).
178
+ */
179
+ readonly invokeRun: InProcessRunStarter;
180
+ /**
181
+ * The host's pre-enqueue check, forwarded as `preflight` (433-34, G-433-10). It runs the run
182
+ * core's production rule over the request; a rejection carrying `neutralReason` is a denial.
183
+ */
184
+ readonly preflightRun?: (opts: PreflightOptions) => Promise<void>;
185
+ /** Applied when an `ExecuteOptions` carries no `timeout` of its own. */
186
+ readonly defaultTimeout?: number;
187
+ /** How long a settled run's handle survives before it is dropped. */
188
+ readonly retentionMs?: number;
189
+ }
190
+ /**
191
+ * The dashboard's in-process `ExecutionBackend`.
192
+ *
193
+ * A CLASS, matching the shape the interface already declared (`extends EventEmitter`), with a
194
+ * factory beside it so a composing host names a function rather than a constructor — nothing here
195
+ * is meant to be subclassed, and nothing needs the `resolveCliBin`-style static the retiring class
196
+ * carried for its binary.
197
+ */
198
+ declare class InProcessExecutionBackend extends EventEmitter implements ExecutionBackend {
199
+ private readonly options;
200
+ private readonly handles;
201
+ private readonly retentionMs;
202
+ constructor(options: InProcessExecutionBackendOptions);
203
+ preflight(opts: PreflightOptions): Promise<void>;
204
+ execute(opts: ExecuteOptions): void;
205
+ kill(runId: string): boolean;
206
+ killAll(): void;
207
+ getActiveExecutions(): {
208
+ runId: string;
209
+ status: string;
210
+ startedAt: string;
211
+ duration: number;
212
+ testName?: string;
213
+ }[];
214
+ private settle;
215
+ }
216
+ declare function createInProcessExecutionBackend(options: InProcessExecutionBackendOptions): InProcessExecutionBackend;
217
+
218
+ /**
219
+ * The comment-frame keepalive interval, matching every other SSE this package serves.
220
+ *
221
+ * EXPORTED so a cell can name the number rather than sleep past it, and so the client (404-07)
222
+ * can size its own liveness expectation off the server's constant instead of a second guess.
223
+ */
224
+ declare const POST_PROCESSING_STREAM_HEARTBEAT_MS = 15000;
225
+ /** The last failure the record holds for one stage — the emitter's masked, bounded text, forwarded. */
226
+ interface PostProcessingStageFailure {
227
+ readonly error?: string;
228
+ readonly willRetry?: boolean;
229
+ }
230
+ /** What both doors answer with: the op result, plus what the run's own record says about it. */
231
+ interface PostProcessingReadBody {
232
+ /** The `runs.postprocessing` result, VERBATIM. A reshaping here would be a second contract. */
233
+ readonly status: PostProcessingStatus;
234
+ /** The `runFinished` posture, when the record has one. Absent while the run is still in flight. */
235
+ readonly disposition?: PostProcessingDisposition;
236
+ /**
237
+ * The last `analysisFailed` / `curationFailed` payload per stage.
238
+ *
239
+ * PRESENT AND POSSIBLY EMPTY when the record was read; ABSENT ENTIRELY when the events read
240
+ * refused. The difference is a fact a client can act on — "this run wrote no failure line" is not
241
+ * the same claim as "this door could not read the log".
242
+ */
243
+ readonly failures?: {
244
+ readonly analysis?: PostProcessingStageFailure;
245
+ readonly curation?: PostProcessingStageFailure;
246
+ };
247
+ }
248
+ /** A refusal, already carrying its status and its single-key body. */
249
+ interface PostProcessingRefusal {
250
+ readonly kind: 'refused';
251
+ readonly status: number;
252
+ readonly body: Record<string, string>;
253
+ }
254
+ type PostProcessingReadOutcome = {
255
+ readonly kind: 'ok';
256
+ readonly body: PostProcessingReadBody;
257
+ } | PostProcessingRefusal;
258
+ /**
259
+ * Has this chain finished, whatever became of it?
260
+ *
261
+ * TWO CLAUSES BECAUSE THERE ARE TWO TERMINALS AND THE ROW OUTRANKS THE CHIPS. A chain that wrote
262
+ * its one `RunNotification` is over by definition — that row IS the chain's terminal receipt — and
263
+ * a projection that disagreed with it would be a chain that ended twice. Absent the row, both chips
264
+ * having settled (`complete` or `failed`, the only two settled words in the four-word vocabulary) is
265
+ * the same fact read one layer down.
266
+ */
267
+ declare function isPostProcessingTerminal(status: PostProcessingStatus): boolean;
268
+ /**
269
+ * THE JOINED READ — the one place chip truth and run posture are put beside each other.
270
+ *
271
+ * Exported so the stream door and the read door answer with byte-identical bodies by CALLING one
272
+ * function rather than by two spellings that agree today. `routes.post-processing-stream.test.ts`
273
+ * compares the snapshot frame against the read route's body for exactly that reason.
274
+ */
275
+ declare function readPostProcessing(host: AgentHost, runId: string): Promise<PostProcessingReadOutcome>;
276
+ interface PostProcessingReadDeps {
277
+ /** The DECODED run id. Decoding can throw, and that throw is the caller's refusal to answer. */
278
+ readonly runId: string;
279
+ /** The injected host. Its absence is the ROUTER'S 503, not this function's. */
280
+ readonly host: AgentHost;
281
+ }
282
+ /** `GET /api/runs/:id/post-processing` — what the page polls (D-18). */
283
+ declare function servePostProcessingRead(_req: IncomingMessage, res: ServerResponse, deps: PostProcessingReadDeps): void;
284
+ interface PostProcessingStreamDeps extends PostProcessingReadDeps {
285
+ /** The in-process chip sink this browser's frames come from. */
286
+ readonly relay: PostProcessingRelay;
287
+ /** Live teardowns, so a host can count open streams and a close removes exactly one. */
288
+ readonly openStreams?: {
289
+ add(teardown: () => void): void;
290
+ delete(teardown: () => void): void;
291
+ };
292
+ }
293
+ /**
294
+ * `GET /api/runs/:id/post-processing/stream` — the DEDICATED live channel (D-16).
295
+ *
296
+ * THE ORDER IS THE CONTRACT, on `serveRunEventStream`'s own law: every refusal happens before
297
+ * `writeHead`, because a response that has already written `200 text/event-stream` cannot be told
298
+ * it failed. So the joined read runs FIRST — it is the snapshot's content and the open-time
299
+ * decision both — and only then does a head go out.
300
+ *
301
+ * THE 204 IS THE SSE SPEC'S NO-RECONNECT CLOSE, and it is the whole answer to Pitfall 6. An
302
+ * `EventSource` reconnects after ANY close except a 204, so a door that answered `200` with an
303
+ * immediate end to a run whose chain is over would be answering the same request every few seconds
304
+ * for as long as the tab is open. Three states get it:
305
+ *
306
+ * - the chain is already terminal — there is nothing left to relay, ever;
307
+ * - the run says `handedOff` — a lease-holder in ANOTHER process is draining it, and this
308
+ * process's emitter will therefore never publish a single envelope for it;
309
+ * - the run says `detached` — nothing is draining it in-process either.
310
+ *
311
+ * The last two are D-16's "closes immediately on hand-off / detach" and they are one decision with
312
+ * two words, because the fact that matters is identical: this relay cannot ever fire for that run,
313
+ * so the honest channel is the page's poll and holding a socket open would be a promise this
314
+ * process cannot keep. A snapshot with NO disposition at all is the opposite case — the run has not
315
+ * reached `runFinished` yet, which is exactly when a live channel is worth having — and it opens.
316
+ */
317
+ declare function servePostProcessingStream(req: IncomingMessage, res: ServerResponse, deps: PostProcessingStreamDeps): void;
318
+
319
+ /**
320
+ * What a run-detail reader is, spelled in this package's own vocabulary.
321
+ *
322
+ * Absence is a RESULT — `undefined` means no run is recorded under that id — and it
323
+ * is deliberately not a refusal: turning it into one is the op's job, so the "not found" word is
324
+ * minted once, in the layer every surface shares, rather than once per adapter.
325
+ */
326
+ interface DashboardRunDetailReader {
327
+ read(runId: RunId): Promise<RunDetailRows | undefined>;
328
+ readRecord(runId: RunId): Promise<{
329
+ status: string;
330
+ } | undefined>;
331
+ }
332
+ /**
333
+ * Bind a reader to THIS server's one database handle.
334
+ *
335
+ * The read is synchronous underneath (better-sqlite3) and the method is `async` anyway, because
336
+ * the port's contract is a promise and a composition root that had to know which implementations
337
+ * were synchronous would be a composition root that knows too much.
338
+ *
339
+ * THE ID IS NEVER JOINED ONTO A PATH. It is a bound parameter of a prepared statement, resolved
340
+ * against rows the database already holds — the T-405-01 mitigation, and the same law
341
+ * `RunsReaderPort.get` states for the handoff store.
342
+ */
343
+ declare function createDashboardRunDetailReader(db: DashboardDatabase): DashboardRunDetailReader;
344
+
345
+ /** A refusal, already carrying its status and its single-key body. */
346
+ interface RunDetailRefusal {
347
+ readonly kind: 'refused';
348
+ readonly status: number;
349
+ readonly body: Record<string, string>;
350
+ }
351
+ type RunDetailReadOutcome = {
352
+ readonly kind: 'ok';
353
+ readonly body: RunDetailProjection;
354
+ } | RunDetailRefusal;
355
+ /**
356
+ * THE READ. Exported so a cell can measure the outcome without a socket, and so any second door
357
+ * (405-05's authed remote row) answers with byte-identical bytes by CALLING this rather than by a
358
+ * second spelling that agrees today.
359
+ */
360
+ declare function readRunDetail(host: AgentHost, runId: string): Promise<RunDetailReadOutcome>;
361
+ interface RunDetailReadDeps {
362
+ /** The DECODED run id. Decoding can throw, and that throw is the caller's refusal to answer. */
363
+ readonly runId: string;
364
+ /** The injected host. Its absence is the ROUTER'S 503, not this function's. */
365
+ readonly host: AgentHost;
366
+ /**
367
+ * WHO IS ON THE OTHER END OF THIS SOCKET (405-10, D-09). Both doors serve the same body, masked
368
+ * at write, and nothing restores it (441 D-07); `'human'` only turns on the same-site guard.
369
+ *
370
+ * `'human'` IS PASSED BY THE LOCAL door's arms in `routes.ts`. The authed REMOTE door's
371
+ * `GET /run/:runId/detail` (`run-service-routes.ts`, 405-05) passes nothing, because it is
372
+ * legitimately reached at a non-loopback Host.
373
+ */
374
+ readonly boundary?: 'human' | 'machine';
375
+ }
376
+ /** `GET /api/runs/:id/detail` — what the rebuilt run-detail page reads (D-03). */
377
+ declare function serveRunDetailRead(req: IncomingMessage, res: ServerResponse, deps: RunDetailReadDeps): void;
378
+ type RunArtifactReadOutcome = {
379
+ readonly kind: 'ok';
380
+ readonly sha: string;
381
+ readonly contentType: string;
382
+ readonly bytes: Buffer;
383
+ readonly range?: RunsArtifactResult['range'];
384
+ readonly totalLength?: number;
385
+ } | {
386
+ readonly kind: 'range-not-satisfiable';
387
+ readonly totalLength: number;
388
+ } | RunDetailRefusal;
389
+ /**
390
+ * THE READ. Exported so a cell can measure the outcome without a socket, and so the authed remote
391
+ * door answers with byte-identical bytes by CALLING this rather than by a second spelling.
392
+ */
393
+ declare function readRunArtifact(host: AgentHost, runId: string, sha: string, range?: RunsArtifactInput['range']): Promise<RunArtifactReadOutcome>;
394
+ interface RunArtifactReadDeps {
395
+ /** The DECODED run id. Decoding can throw, and that throw is the caller's refusal to answer. */
396
+ readonly runId: string;
397
+ /** The RAW sha segment. Never decoded: a content address contains nothing percent-encoding
398
+ * could represent, so a segment that needed decoding is already not one. */
399
+ readonly sha: string;
400
+ /** The injected host. Its absence is the ROUTER'S 503, not this function's. */
401
+ readonly host: AgentHost;
402
+ }
403
+ /**
404
+ * `GET /api/runs/:runId/artifacts/:sha` — a ref, resolved (D-12).
405
+ *
406
+ * THE HEADERS ARE THE FEATURE AS MUCH AS THE BYTES.
407
+ * - `ETag: "<sha>"` — the content address IS the validator; there is nothing else it could be, and
408
+ * nothing that could make it stale, because bytes cannot change under a hash of themselves.
409
+ * - `Cache-Control: public, max-age=31536000, immutable` — for the same reason. A revalidation of
410
+ * an immutable resource is a round trip that can only ever answer "yes, still".
411
+ * - `X-Content-Type-Options: nosniff` — the second half of the T-405-15 mitigation. The op refuses
412
+ * to NAME a type a browser executes; this refuses to let the browser guess one anyway.
413
+ * - `Content-Length` — from the op's own declared `byteLength` by way of the decoded buffer.
414
+ *
415
+ * `If-None-Match` IS HONOURED because it is three lines against a header set that promises a year
416
+ * of caching, and a client that revalidates anyway should not be made to re-download a screenshot.
417
+ */
418
+ declare function serveRunArtifact(req: IncomingMessage, res: ServerResponse, deps: RunArtifactReadDeps): void;
419
+
420
+ /** Every value `[data-embed-state]` can take. */
421
+ type LiveEmbedState = 'connecting' | 'live' | 'no-stream' | 'expired' | 'refused' | 'stream-ended' | 'session-ended' | 'ended' | 'lost';
422
+ /** Every value `[data-embed-state]` can take. The 403-02 DOM cells range this list. */
423
+ declare const LIVE_EMBED_STATES: readonly ["connecting", "live", "no-stream", "expired", "refused", "stream-ended", "session-ended", "ended", "lost"];
424
+ /** Every fact the page reports to its host (428 D-15). */
425
+ type LiveEmbedReportEvent = 'ready' | 'frame' | 'ended' | 'refused' | 'lost';
426
+ declare const LIVE_EMBED_REPORT_EVENTS: readonly ["ready", "frame", "ended", "refused", "lost"];
427
+ /**
428
+ * The query parameter a host sets when it draws its own status chrome (`/live/<id>?host=dashboard`).
429
+ * Any non-empty value lands on `main[data-embed-host]` and hides the page's centred text.
430
+ */
431
+ declare const LIVE_EMBED_HOST_PARAM = "host";
432
+ /** At most one `frame` report per this many milliseconds; the report carries the newest frame. */
433
+ declare const LIVE_EMBED_REPORT_INTERVAL_MS = 500;
434
+ /**
435
+ * What an EXPIRED link says, exactly (D-02).
436
+ *
437
+ * Exported so the tracer cell and 403-02's DOM cells assert the SAME string the page renders — a
438
+ * literal spelled twice is a literal that can drift, and this one is the difference between a
439
+ * person seeing "your link timed out" and a person seeing nothing at all.
440
+ */
441
+ declare const LIVE_EMBED_EXPIRED_MESSAGE = "link expired";
442
+ /**
443
+ * What a target that declared NO live stream says, exactly (D-08).
444
+ *
445
+ * This document owns no step list, and its host — the dashboard iframe today, the 408 MCP App
446
+ * later — is invisible to it, so the sentence claims nothing about where or whether steps are
447
+ * shown. Exported so the served bytes are pinned against the constant (frame-proxy cell 25).
448
+ */
449
+ declare const LIVE_EMBED_NO_STREAM_MESSAGE = "no live preview for this target";
450
+ /** What an ended editor session says, exactly (428 D-07/D-09; frame-proxy cell 34). */
451
+ declare const LIVE_EMBED_SESSION_ENDED_MESSAGE = "session ended";
452
+ /**
453
+ * Render the page for one run.
454
+ *
455
+ * `nonce` is minted per response by the caller and appears in both the CSP header and the two
456
+ * inline elements; a page served twice never reuses one.
457
+ */
458
+ declare function renderLiveEmbedPage(input: {
459
+ runId: string;
460
+ nonce: string;
461
+ }): string;
462
+
463
+ interface FrameProxyDeps {
464
+ validate(token: string, expect: {
465
+ subject: LiveViewSubjectRef;
466
+ now: number;
467
+ }): LiveUrlVerdict;
468
+ attach(runId: string): RunSession | undefined;
469
+ /** 428 D-07: the editor-subject lookup. Absent means no editor subject is served on this socket. */
470
+ attachEditor?(sessionId: string): LiveEditorAttachment | undefined;
471
+ now?: () => number;
472
+ }
473
+ declare function createFrameProxy(deps: FrameProxyDeps): (ws: WebSocket, runId: string) => void;
474
+
475
+ /** The frame socket. A query carries the run id ONLY — never the token (D-04). */
476
+ declare const LIVE_VIEW_WS_PATH = "/api/live-view/ws";
477
+ /** The served embed page's prefix. Bare, so a host's `/api/` allowlist is not involved. */
478
+ declare const LIVE_EMBED_PATH_PREFIX = "/live/";
479
+ /** The largest first message this socket will read. A hello is a few hundred bytes. */
480
+ declare const LIVE_VIEW_HELLO_MAX_BYTES = 4096;
481
+ /** Above this much buffered, a frame is DROPPED and acked — a slow viewer must not stall CDP. */
482
+ declare const LIVE_VIEW_SLOW_VIEWER_BYTES: number;
483
+ /** Keepalive cadence. */
484
+ declare const LIVE_VIEW_PING_MS = 15000;
485
+ /** The close code every refusal uses. In the 4000-4999 application range. */
486
+ declare const LIVE_VIEW_REFUSAL_CLOSE_CODE = 4400;
487
+ /**
488
+ * Every reason this socket can refuse a viewer, BY NAME.
489
+ *
490
+ * The four token verdicts plus the three this layer alone can answer: a run this process never
491
+ * started, a handle that run does not have, and an editor session this process does not hold
492
+ * (428 D-07). Extending the token's list rather than restating it is what stops the two
493
+ * vocabularies from drifting.
494
+ */
495
+ type LiveViewRefusalReason = LiveUrlRefusalReason | 'unknown_run' | 'unknown_handle' | 'unknown_session';
496
+ declare const LIVE_VIEW_REFUSAL_REASONS: readonly ["malformed", "bad_signature", "expired", "scope_mismatch", "unknown_run", "unknown_handle", "unknown_session"];
497
+ /**
498
+ * Every reason an `ended` message can carry, each naming a different fact (403 CR-03, 428 D-07/D-09).
499
+ * `runFinished`: the run is over. `handleFinished`: this member's stream is over while its run
500
+ * continues. `sessionEnded`: an editor session is over.
501
+ */
502
+ type LiveViewEndReason = 'runFinished' | 'handleFinished' | 'sessionEnded';
503
+ declare const LIVE_VIEW_END_REASONS: readonly ["runFinished", "handleFinished", "sessionEnded"];
504
+ /** What the PAGE sends. `hello` must be first; `ack` and `renew` are valid only after `ready`. */
505
+ type LiveViewClientMessage = {
506
+ type: 'hello';
507
+ token: string;
508
+ } | {
509
+ type: 'ack';
510
+ id: number;
511
+ } | {
512
+ type: 'renew';
513
+ token: string;
514
+ };
515
+ /**
516
+ * What the SERVER sends.
517
+ *
518
+ * A `frame` is a TEXT header immediately followed by ONE BINARY message carrying the JPEG bytes.
519
+ * Two messages rather than a length-prefixed binary frame so a consumer needs no custom header
520
+ * parser — `JSON.parse` and a `Blob` are all an embed page has to know.
521
+ */
522
+ type LiveViewServerMessage = {
523
+ type: 'ready';
524
+ runId: string;
525
+ handle: number;
526
+ stream: LiveStreamDeclaration;
527
+ expiresAt: number;
528
+ } | {
529
+ type: 'refused';
530
+ reason: LiveViewRefusalReason;
531
+ } | {
532
+ type: 'frame';
533
+ id: number;
534
+ width: number;
535
+ height: number;
536
+ ts?: number;
537
+ kind?: LiveStreamKind;
538
+ } | {
539
+ type: 'renewed';
540
+ expiresAt: number;
541
+ } | {
542
+ type: 'ended';
543
+ reason: LiveViewEndReason;
544
+ };
545
+
546
+ type RunMarkOutcome = {
547
+ readonly kind: 'ok';
548
+ readonly body: RunsMarkOutcome;
549
+ } | RunDetailRefusal;
550
+ /**
551
+ * Address comes from the route; the body may never replace it or claim a caller identity. The body
552
+ * is the op's own strict input minus `runId`, so a field the op does not declare is refused here
553
+ * before anything is dispatched.
554
+ */
555
+ declare function markRun(host: AgentHost, runId: string, body: unknown): Promise<RunMarkOutcome>;
556
+ /** POST /api/runs/:id/mark. Both doors answer the op's outcome as it is; `'human'` adds the same-site guard. */
557
+ declare function registerRunMarkRoute(req: IncomingMessage, res: ServerResponse, deps: {
558
+ readonly host: AgentHost;
559
+ readonly runId: string;
560
+ readonly boundary?: 'human' | 'machine';
561
+ }): void;
99
562
 
100
- export { DashboardDatabase, DashboardReporter, MIGRATIONS, type ResolveDashboardDbPathOptions, type RunIdMigrationOptions, type RunIdMigrationResult, SCHEMA_VERSION, migrateWorkspaceRunIds, resolveDashboardDbPath };
563
+ export { AgentHost, DashboardDatabase, type DashboardRunDetailReader, type DashboardRunIngest, type DashboardRunIngestDeps, type DashboardRunIngestFinalizeExtra, ExecuteOptions, ExecutionBackend, InProcessExecutionBackend, type InProcessExecutionBackendOptions, type InProcessRunStartInput, type InProcessRunStarter, type InProcessRunStatus, LIVE_EMBED_EXPIRED_MESSAGE, LIVE_EMBED_HOST_PARAM, LIVE_EMBED_NO_STREAM_MESSAGE, LIVE_EMBED_PATH_PREFIX, LIVE_EMBED_REPORT_EVENTS, LIVE_EMBED_REPORT_INTERVAL_MS, LIVE_EMBED_SESSION_ENDED_MESSAGE, LIVE_EMBED_STATES, LIVE_VIEW_END_REASONS, LIVE_VIEW_HELLO_MAX_BYTES, LIVE_VIEW_PING_MS, LIVE_VIEW_REFUSAL_CLOSE_CODE, LIVE_VIEW_REFUSAL_REASONS, LIVE_VIEW_SLOW_VIEWER_BYTES, LIVE_VIEW_WS_PATH, LiveEditorAttachment, type LiveEmbedReportEvent, type LiveEmbedState, type LiveViewClientMessage, type LiveViewRefusalReason, type LiveViewServerMessage, MIGRATIONS, POST_PROCESSING_STREAM_HEARTBEAT_MS, type PostProcessingReadBody, PostProcessingRelay, type PostProcessingStageFailure, type ResolveDashboardDbPathOptions, type RunArtifactReadOutcome, type RunDetailReadOutcome, type RunIdentityMember, type RunMarkOutcome, type RunRowIdentity, SCHEMA_VERSION, createDashboardRunDetailReader, createDashboardRunIngest, createFrameProxy, createInProcessExecutionBackend, createRunOwnerHostResolver, formatRunTitle, isPostProcessingTerminal, markRun, readPostProcessing, readRunArtifact, readRunDetail, registerRunMarkRoute, renderLiveEmbedPage, resolveDashboardDbPath, resolveRunOwnerHost, runRowIdentity, servePostProcessingRead, servePostProcessingStream, serveRunArtifact, serveRunDetailRead };