@akagilnc/pi-workflow-roles 0.1.1883 → 0.1.1918

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.
@@ -6,7 +6,7 @@ import { executeEvidenceChild } from "./evidence-child-executor.js";
6
6
  export function projectSharedChildFailure(error) {
7
7
  if (typeof error === "object" && error !== null && "evidenceChildFailure" in error) {
8
8
  const classification = error.evidenceChildFailure;
9
- if (classification === "provider" || classification === "child") {
9
+ if (classification === "provider" || classification === "child" || classification === "unknown") {
10
10
  Object.assign(error, { reviewerFailure: classification });
11
11
  }
12
12
  }
@@ -0,0 +1,60 @@
1
+ import { readFile, unlink, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ const TYPED_HTTP_FILE = "typed-provider-http.json";
4
+ function typedProviderHttpPath(runDirectory) {
5
+ return join(runDirectory, TYPED_HTTP_FILE);
6
+ }
7
+ async function clearTypedProviderHttpObservation(runDirectory) {
8
+ try {
9
+ await unlink(typedProviderHttpPath(runDirectory));
10
+ } catch (error) {
11
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
12
+ return;
13
+ }
14
+ throw error;
15
+ }
16
+ }
17
+ async function recordTypedProviderHttpStatus(runDirectory, observation) {
18
+ if (observation.httpStatus >= 200 && observation.httpStatus < 300) {
19
+ await clearTypedProviderHttpObservation(runDirectory);
20
+ return;
21
+ }
22
+ const body = {
23
+ httpStatus: observation.httpStatus,
24
+ provider: observation.provider
25
+ };
26
+ await writeFile(
27
+ typedProviderHttpPath(runDirectory),
28
+ `${JSON.stringify(body)}
29
+ `,
30
+ "utf8"
31
+ );
32
+ }
33
+ async function readLatestTypedProviderHttpObservation(runDirectory) {
34
+ let text;
35
+ try {
36
+ text = await readFile(typedProviderHttpPath(runDirectory), "utf8");
37
+ } catch (error) {
38
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
39
+ return void 0;
40
+ }
41
+ throw error;
42
+ }
43
+ const raw = JSON.parse(text);
44
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
45
+ throw new Error("typed provider HTTP observation must be a JSON object");
46
+ }
47
+ const record = raw;
48
+ if (typeof record.httpStatus !== "number") {
49
+ throw new Error("typed provider HTTP observation missing numeric httpStatus");
50
+ }
51
+ if (typeof record.provider !== "string" || record.provider.trim() === "") {
52
+ throw new Error("typed provider HTTP observation missing non-empty provider");
53
+ }
54
+ return { httpStatus: record.httpStatus, provider: record.provider };
55
+ }
56
+ export {
57
+ clearTypedProviderHttpObservation,
58
+ readLatestTypedProviderHttpObservation,
59
+ recordTypedProviderHttpStatus
60
+ };
@@ -0,0 +1,19 @@
1
+ function isNonSuccessHttpStatus(status) {
2
+ return typeof status === "number" && (status < 200 || status >= 300);
3
+ }
4
+ function hasUpstreamErrorTestimony(input) {
5
+ if (isNonSuccessHttpStatus(input.httpStatus)) return true;
6
+ return Array.isArray(input.diagnostics) && input.diagnostics.length > 0;
7
+ }
8
+ function projectConfirmedRemotePayload(input) {
9
+ return {
10
+ ...input.body === void 0 ? {} : { body: input.body },
11
+ ...input.code === void 0 ? {} : { code: input.code },
12
+ ...input.errno === void 0 ? {} : { errno: input.errno }
13
+ };
14
+ }
15
+ export {
16
+ hasUpstreamErrorTestimony,
17
+ isNonSuccessHttpStatus,
18
+ projectConfirmedRemotePayload
19
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.1883",
3
+ "version": "0.1.1918",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -14,6 +14,8 @@ const entries = [
14
14
  "activation-reconciliation",
15
15
  "sitian-role-run-coordinates",
16
16
  "sitian-record-entry",
17
+ "typed-provider-http",
18
+ "upstream-error-testimony",
17
19
  ];
18
20
 
19
21
  /**
@@ -33,6 +33,11 @@ import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
33
33
  import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
34
34
  import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.ts";
35
35
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
36
+ import {
37
+ hasUpstreamErrorTestimony,
38
+ isNonSuccessHttpStatus,
39
+ projectConfirmedRemotePayload,
40
+ } from "./upstream-error-testimony.ts";
36
41
 
37
42
  // ── shared constants / types ──────────────────────────────────────────────
38
43
 
@@ -186,15 +191,25 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
186
191
  const idle = createStreamIdleGuard(
187
192
  options.signal === undefined ? {} : { parentSignal: options.signal },
188
193
  );
194
+ // Typed HTTP status observed via onResponse — never inferred from prose.
195
+ let observedHttpStatus: number | undefined;
189
196
  try {
190
197
  const requestSignal = request?.signal;
191
198
  const streamSignal = requestSignal === undefined
192
199
  ? idle.signal
193
200
  : AbortSignal.any([idle.signal, requestSignal]);
201
+ const priorOnResponse = request?.onResponse;
194
202
  const inheritedRequest = {
195
203
  ...(request ?? {}),
196
204
  ...(dispatch.auth.env === undefined ? {} : { env: dispatch.auth.env }),
197
205
  signal: streamSignal,
206
+ onResponse: async (
207
+ response: { status: number; headers: Record<string, string> },
208
+ model: Model<Api>,
209
+ ) => {
210
+ if (typeof response?.status === "number") observedHttpStatus = response.status;
211
+ await priorOnResponse?.(response, model);
212
+ },
198
213
  } as ProviderStreamOptions;
199
214
  if (options.runCompletion !== undefined) {
200
215
  await new Promise<void>((resolve) => setImmediate(resolve));
@@ -209,12 +224,12 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
209
224
  ),
210
225
  streamSignal,
211
226
  );
212
- const response: AssistantMessage = {
227
+ const response: AssistantMessage = attachObservedHttpStatus({
213
228
  ...completed,
214
229
  api: model.api,
215
230
  provider: model.provider,
216
231
  model: model.id,
217
- };
232
+ }, observedHttpStatus);
218
233
  if (response.stopReason === "error" || response.stopReason === "aborted") {
219
234
  wrapped.push({ type: "error", reason: response.stopReason, error: response });
220
235
  } else {
@@ -232,9 +247,12 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
232
247
  if (next.done) break;
233
248
  sawEvent = true;
234
249
  idle.poke();
235
- wrapped.push(next.value as any);
250
+ wrapped.push(enrichStreamEvent(next.value, observedHttpStatus) as any);
236
251
  }
237
- const response = await waitForStream(source.result(), idle.signal);
252
+ const response = attachObservedHttpStatus(
253
+ await waitForStream(source.result(), idle.signal),
254
+ observedHttpStatus,
255
+ );
238
256
  if (!sawEvent) wrapped.end(response);
239
257
  return;
240
258
  } catch (error) {
@@ -264,17 +282,26 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
264
282
  continue;
265
283
  }
266
284
  state.streamFailure = failure;
267
- const response: AssistantMessage = {
268
- role: "assistant",
269
- content: [],
285
+ const projected = projectStructuredRemote(failure);
286
+ // Prefer structured fields on the thrown failure; fall back to onResponse observation.
287
+ const httpStatus = projected.httpStatus ?? numericHttpStatus(observedHttpStatus);
288
+ const response = {
289
+ role: "assistant" as const,
290
+ content: [] as [],
270
291
  api: model.api,
271
292
  provider: model.provider,
272
293
  model: model.id,
273
294
  usage: emptyUsage(),
274
- stopReason: "error",
295
+ stopReason: "error" as const,
296
+ // Preserve the held failure message bytes — do not rewrite.
275
297
  errorMessage: failure instanceof Error ? failure.message : String(failure),
276
298
  timestamp: Date.now(),
277
- };
299
+ ...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
300
+ ...(httpStatus === undefined ? {} : { status: httpStatus, statusCode: httpStatus }),
301
+ ...(projected.body === undefined ? {} : { body: projected.body }),
302
+ ...(projected.code === undefined ? {} : { code: projected.code }),
303
+ ...(projected.errno === undefined ? {} : { errno: projected.errno }),
304
+ } as unknown as AssistantMessage;
278
305
  wrapped.push({ type: "error", reason: "error", error: response });
279
306
  wrapped.end(response);
280
307
  return;
@@ -346,11 +373,114 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
346
373
  return state;
347
374
  }
348
375
 
376
+ type EvidenceChildFailureClassification = "provider" | "child" | "unknown";
349
377
  type ClassifiedReviewerError = Error & Readonly<{
350
- evidenceChildFailure: "provider" | "child";
378
+ evidenceChildFailure: EvidenceChildFailureClassification;
351
379
  evidenceChildOriginal?: unknown;
352
380
  }>;
353
- function classifiedError(error: unknown, evidenceChildFailure: "provider" | "child"): ClassifiedReviewerError {
381
+ function numericHttpStatus(value: unknown): number | undefined {
382
+ return isNonSuccessHttpStatus(value) ? value : undefined;
383
+ }
384
+
385
+ type StructuredRemoteProjection = {
386
+ readonly hasTestimony: boolean;
387
+ readonly httpStatus?: number;
388
+ readonly diagnostics?: unknown;
389
+ readonly body?: unknown;
390
+ readonly code?: unknown;
391
+ readonly errno?: unknown;
392
+ };
393
+
394
+ /**
395
+ * Cause-chain reader over the shared upstream-testimony authority.
396
+ * Shape walking stays here; testimony + confirmed-remote payload rules are shared.
397
+ */
398
+ function projectStructuredRemote(error: unknown): StructuredRemoteProjection {
399
+ let httpStatus: number | undefined;
400
+ let diagnostics: unknown;
401
+ let body: unknown;
402
+ let code: unknown;
403
+ let errno: unknown;
404
+ let cursor: unknown = error;
405
+ const seen = new Set<unknown>();
406
+ while (typeof cursor === "object" && cursor !== null && !seen.has(cursor)) {
407
+ seen.add(cursor);
408
+ const record = cursor as Record<string, unknown>;
409
+ const nodeStatus = numericHttpStatus(record.statusCode)
410
+ ?? numericHttpStatus(record.status)
411
+ ?? numericHttpStatus(record.httpStatus);
412
+ const nodeDiagnostics = Array.isArray(record.diagnostics) && record.diagnostics.length > 0
413
+ ? record.diagnostics
414
+ : undefined;
415
+ const nodeHasTestimony = hasUpstreamErrorTestimony({
416
+ ...(nodeStatus === undefined ? {} : { httpStatus: nodeStatus }),
417
+ ...(nodeDiagnostics === undefined ? {} : { diagnostics: nodeDiagnostics }),
418
+ });
419
+ if (httpStatus === undefined && nodeStatus !== undefined) httpStatus = nodeStatus;
420
+ if (diagnostics === undefined && nodeDiagnostics !== undefined) diagnostics = nodeDiagnostics;
421
+ // Payload only from confirmed-remote nodes — never arbitrary local Error.code.
422
+ if (nodeHasTestimony) {
423
+ const payload = projectConfirmedRemotePayload(record);
424
+ if (body === undefined && payload.body !== undefined) body = payload.body;
425
+ if (code === undefined && payload.code !== undefined) code = payload.code;
426
+ if (errno === undefined && payload.errno !== undefined) errno = payload.errno;
427
+ }
428
+ cursor = record.cause;
429
+ }
430
+ return {
431
+ hasTestimony: hasUpstreamErrorTestimony({
432
+ ...(httpStatus === undefined ? {} : { httpStatus }),
433
+ ...(diagnostics === undefined ? {} : { diagnostics }),
434
+ }),
435
+ ...(httpStatus === undefined ? {} : { httpStatus }),
436
+ ...(diagnostics === undefined ? {} : { diagnostics }),
437
+ ...(body === undefined ? {} : { body }),
438
+ ...(code === undefined ? {} : { code }),
439
+ ...(errno === undefined ? {} : { errno }),
440
+ };
441
+ }
442
+
443
+ /**
444
+ * Attach a directly observed HTTP status onto an error/aborted assistant message.
445
+ * Does not invent status from errorMessage prose; skips when already held.
446
+ */
447
+ function attachObservedHttpStatus<T extends AssistantMessage>(
448
+ message: T,
449
+ observedHttpStatus: number | undefined,
450
+ ): T {
451
+ if (observedHttpStatus === undefined) return message;
452
+ if (message.stopReason !== "error" && message.stopReason !== "aborted") return message;
453
+ if (numericHttpStatus(observedHttpStatus) === undefined) return message;
454
+ if (projectStructuredRemote(message).httpStatus !== undefined) return message;
455
+ return Object.assign(message, {
456
+ status: observedHttpStatus,
457
+ statusCode: observedHttpStatus,
458
+ });
459
+ }
460
+ function enrichStreamEvent(event: unknown, observedHttpStatus: number | undefined): unknown {
461
+ if (observedHttpStatus === undefined || event === null || typeof event !== "object") return event;
462
+ const record = event as Record<string, unknown>;
463
+ if (record.type === "error" && record.error !== null && typeof record.error === "object") {
464
+ return {
465
+ ...record,
466
+ error: attachObservedHttpStatus(record.error as AssistantMessage, observedHttpStatus),
467
+ };
468
+ }
469
+ if (record.type === "done" && record.message !== null && typeof record.message === "object") {
470
+ return {
471
+ ...record,
472
+ message: attachObservedHttpStatus(record.message as AssistantMessage, observedHttpStatus),
473
+ };
474
+ }
475
+ if (record.partial !== null && typeof record.partial === "object") {
476
+ return {
477
+ ...record,
478
+ partial: attachObservedHttpStatus(record.partial as AssistantMessage, observedHttpStatus),
479
+ };
480
+ }
481
+ return event;
482
+ }
483
+ function classifiedError(error: unknown, evidenceChildFailure: EvidenceChildFailureClassification): ClassifiedReviewerError {
354
484
  const diagnostic = typeof error === "object" && error !== null && typeof (error as { errorMessage?: unknown }).errorMessage === "string"
355
485
  ? (error as { errorMessage: string }).errorMessage
356
486
  : error === undefined ? "" : String(error);
@@ -359,7 +489,9 @@ function classifiedError(error: unknown, evidenceChildFailure: "provider" | "chi
359
489
  : Object.assign(new Error(diagnostic, { cause: error }), { evidenceChildOriginal: error });
360
490
  const classification = "evidenceChildFailure" in wrapped
361
491
  ? (wrapped as ClassifiedReviewerError).evidenceChildFailure
362
- : evidenceChildFailure;
492
+ : evidenceChildFailure === "provider" && !projectStructuredRemote(error).hasTestimony
493
+ ? "unknown"
494
+ : evidenceChildFailure;
363
495
  return Object.assign(wrapped, { evidenceChildFailure: classification });
364
496
  }
365
497
 
@@ -461,13 +593,19 @@ export async function executeEvidenceChild(
461
593
  const lastAssistant = [...session.messages]
462
594
  .reverse()
463
595
  .find((message) => message.role === "assistant");
464
- if (lastAssistant?.role === "assistant" && lastAssistant.stopReason === "error") {
596
+ // error|aborted assistant stops share the upstream-testimony rule: provider only
597
+ // with direct HTTP/SDK testimony, otherwise existing unknown. child is reserved
598
+ // for real local child/report failures (no assistant / blank report / cleanup).
599
+ if (
600
+ lastAssistant?.role === "assistant"
601
+ && (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")
602
+ ) {
465
603
  throw classifiedError(
466
604
  new Error(lastAssistant.errorMessage ?? "", { cause: lastAssistant }),
467
- "provider",
605
+ projectStructuredRemote(lastAssistant).hasTestimony ? "provider" : "unknown",
468
606
  );
469
607
  }
470
- if (lastAssistant?.role !== "assistant" || lastAssistant.stopReason === "aborted") {
608
+ if (lastAssistant?.role !== "assistant") {
471
609
  throw classifiedError(
472
610
  new Error("Evidence child child terminated without a report", {
473
611
  cause: lastAssistant ?? session.messages,
@@ -820,22 +958,37 @@ export async function executeAuditorChild(
820
958
  else if (retentionFailure !== undefined) throw retentionFailure;
821
959
  } catch (retentionFailure) {
822
960
  if (response.stopReason !== "error") throw retentionFailure;
961
+ // Do not trim/rewrite the held errorMessage bytes.
962
+ const diagnostic = typeof response.errorMessage === "string" && response.errorMessage.trim() !== ""
963
+ ? response.errorMessage
964
+ : undefined;
965
+ const projected = projectStructuredRemote(response);
823
966
  const failure = new Error(
824
- response.errorMessage?.trim() || "provider failure",
967
+ diagnostic ?? "",
825
968
  { cause: retentionFailure },
826
969
  ) as Error & {
827
- knownCause: "provider";
970
+ knownCause: "provider" | "unrecognized";
828
971
  failureCode?: string;
829
972
  details: Record<string, unknown>;
830
973
  };
831
- failure.name = response.model || response.provider || "Error";
832
- failure.knownCause = "provider";
833
- failure.failureCode = response.provider || response.model;
974
+ if (projected.hasTestimony && (response.model || response.provider)) {
975
+ failure.name = response.model || response.provider || "Error";
976
+ failure.failureCode = response.provider || response.model;
977
+ }
978
+ failure.knownCause = projected.hasTestimony ? "provider" : "unrecognized";
834
979
  const retentionError = retentionFailure instanceof Error ? retentionFailure : undefined;
835
980
  const retentionCause = retentionError?.cause;
836
981
  failure.details = {
837
- ...(response.provider ? { provider: response.provider } : {}),
838
- ...(response.model ? { model: response.model } : {}),
982
+ ...(diagnostic === undefined ? {} : { errorMessage: diagnostic }),
983
+ ...(projected.hasTestimony && response.provider ? { provider: response.provider } : {}),
984
+ ...(projected.hasTestimony && response.model ? { model: response.model } : {}),
985
+ ...(response.api ? { api: response.api } : {}),
986
+ ...(response.rawStopReason ? { rawStopReason: response.rawStopReason } : {}),
987
+ ...(projected.httpStatus === undefined ? {} : { httpStatus: projected.httpStatus }),
988
+ ...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
989
+ ...(projected.body === undefined ? {} : { body: projected.body }),
990
+ ...(projected.code === undefined ? {} : { code: projected.code }),
991
+ ...(projected.errno === undefined ? {} : { errno: projected.errno }),
839
992
  retentionFailure: {
840
993
  name: retentionError?.name ?? typeof retentionFailure,
841
994
  message: retentionError?.message ?? String(retentionFailure),
@@ -862,8 +1015,8 @@ export async function executeAuditorChild(
862
1015
  parent: binding.parent,
863
1016
  failure: {
864
1017
  cause: failure.knownCause,
865
- identity: { name: failure.name, code: failure.failureCode },
866
- diagnostic: failure.message,
1018
+ ...(failure.failureCode === undefined ? {} : { identity: { name: failure.name, code: failure.failureCode } }),
1019
+ ...(failure.message === "" ? {} : { diagnostic: failure.message }),
867
1020
  details: failure.details,
868
1021
  },
869
1022
  });
@@ -28,14 +28,19 @@ import {
28
28
  } from "./human-format.ts";
29
29
  import {
30
30
  DEFAULT_REFRESH_BOUNDARY_SECONDS,
31
+ buildTicketTrajectoryBookIndex,
31
32
  loadTicketTrajectoryRuns,
33
+ loadUnboundTrajectoryRuns,
32
34
  renderTicketTrajectoryStationHtml,
35
+ type TicketTrajectoryBookIndex,
33
36
  type TicketTrajectoryRun,
34
37
  type TrajectoryClock,
35
38
  type TrajectoryScheduler,
39
+ type UnboundTrajectoryRun,
36
40
  } from "./ticket-trajectory.ts";
37
41
  import type { BoardSnapshot, SnapshotTicket, TicketIssueState } from "./ticket-snapshot.ts";
38
42
 
43
+
39
44
  /** Unaccepted latest-run mtime bands (page-visible thresholds). */
40
45
  export const UNACCEPTED_FLYING_MS = 2 * 60 * 1000;
41
46
  export const UNACCEPTED_WATCH_MS = 15 * 60 * 1000;
@@ -403,6 +408,40 @@ function latestKnownStation(runs: readonly TicketTrajectoryRun[]): string | unde
403
408
  return latest.station;
404
409
  }
405
410
 
411
+ /**
412
+ * Unbound activation card for the existing unknown-set presentation seam.
413
+ * Not a ticket: no issue number, never placed in a yamen column.
414
+ */
415
+ function renderUnboundRunCard(input: {
416
+ bookKey: string;
417
+ unbound: UnboundTrajectoryRun;
418
+ }): string {
419
+ const { run, role, observedAt } = input.unbound;
420
+ const correlationId = input.unbound.correlationId;
421
+ const stationLabel =
422
+ run.station === "unknown" ? "未知站" : (YAMEN_LABELS[run.station] ?? run.station);
423
+ const corrAttr = correlationId === undefined ? "" : ` data-correlation="${attr(correlationId)}"`;
424
+ const corrMeta =
425
+ correlationId === undefined ? "" : ` · corr ${escapeHtml(correlationId)}`;
426
+ return [
427
+ `<article class="ticket unbound-run"`,
428
+ ` data-unbound-run="true"`,
429
+ ` data-book="${attr(input.bookKey)}"`,
430
+ ` data-run-id="${attr(run.runId)}"`,
431
+ corrAttr,
432
+ ` data-placement="unknown"`,
433
+ `>`,
434
+ `<header class="ticket-header">`,
435
+ `<span class="ticket-title">未绑定 · ${escapeHtml(run.runId)}</span>`,
436
+ `<span class="yamen-tag yamen-unknown" data-yamen="unknown">${escapeHtml(stationLabel)}</span>`,
437
+ `</header>`,
438
+ `<p class="unbound-meta" data-unbound-meta="true">`,
439
+ `role ${escapeHtml(role)}${corrMeta} · ${escapeHtml(observedAt)}`,
440
+ `</p>`,
441
+ `</article>`,
442
+ ].join("");
443
+ }
444
+
406
445
  function renderTicketArticle(input: {
407
446
  bookKey: string;
408
447
  prepared: PreparedTicket;
@@ -681,8 +720,9 @@ async function prepareTicket(
681
720
  ledgerDir: string,
682
721
  ticket: SnapshotTicket,
683
722
  now: Date,
723
+ bookIndex?: TicketTrajectoryBookIndex,
684
724
  ): Promise<PreparedTicket> {
685
- const runs = await loadTicketTrajectoryRuns(ledgerDir, ticket.issueNumber);
725
+ const runs = await loadTicketTrajectoryRuns(ledgerDir, ticket.issueNumber, bookIndex);
686
726
  const pending = ticket.state !== "closed" && runs.length === 0;
687
727
  const currentState = decideTicketCurrentState({
688
728
  ticketState: ticket.state,
@@ -862,9 +902,11 @@ async function renderLaneHtml(
862
902
  now: Date,
863
903
  options?: { hidden?: boolean },
864
904
  ): Promise<RenderedLane> {
905
+ // One book-level flat-run index shared by every ticket in this lane.
906
+ const bookIndex = await buildTicketTrajectoryBookIndex(ledgerDir);
865
907
  const prepared = new Map<number, PreparedTicket>();
866
908
  for (const ticket of tickets) {
867
- prepared.set(ticket.issueNumber, await prepareTicket(ledgerDir, ticket, now));
909
+ prepared.set(ticket.issueNumber, await prepareTicket(ledgerDir, ticket, now, bookIndex));
868
910
  }
869
911
 
870
912
  const childrenByParent = buildDirectChildrenByParent(prepared);
@@ -980,6 +1022,20 @@ async function renderLaneHtml(
980
1022
  list.push(entry);
981
1023
  byPlacement.set(entry.placement, list);
982
1024
  }
1025
+
1026
+ // Unbound flat runs: isolated from every ticket, honest on the unknown seam.
1027
+ const unboundRuns = await loadUnboundTrajectoryRuns(ledgerDir, bookIndex);
1028
+ for (const unbound of unboundRuns) {
1029
+ unknownEntries.push({
1030
+ placement: "unknown",
1031
+ band: 0,
1032
+ activityMs: unbound.run.mtimeMs,
1033
+ // Sort after real tickets; no issue identity (unbound is not a ticket).
1034
+ issueNumber: Number.MAX_SAFE_INTEGER,
1035
+ html: renderUnboundRunCard({ bookKey, unbound }),
1036
+ });
1037
+ }
1038
+
983
1039
  unknownEntries.sort(entryCompare);
984
1040
  unknownTickets.sort((a, b) => a.issueNumber - b.issueNumber);
985
1041
 
@@ -1469,7 +1525,8 @@ export async function renderFactoryBoardHtml(
1469
1525
  generatedAt,
1470
1526
  );
1471
1527
  }
1472
- const lane = await renderLaneHtml(
1528
+ let lane: RenderedLane;
1529
+ lane = await renderLaneHtml(
1473
1530
  book.bookKey,
1474
1531
  resolve(book.ledgerDir),
1475
1532
  bookSnap.tickets,