@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.
- package/dist/evidence-child-executor.js +153 -16
- package/dist/navigator-attendance.js +64 -29
- package/dist/public-cli/main.js +786 -373
- package/dist/reviewer-child-executor.js +1 -1
- package/dist/typed-provider-http.js +60 -0
- package/dist/upstream-error-testimony.js +19 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -0
- package/src/evidence-child-executor.ts +177 -24
- package/src/factory-board.ts +60 -3
- package/src/navigator-attendance.ts +86 -33
- package/src/public-cli/cli.ts +96 -39
- package/src/public-cli/coder-run.ts +36 -11
- package/src/public-cli/collector-run.ts +8 -3
- package/src/public-cli/doctor-run.ts +1 -0
- package/src/public-cli/explicit-internal.ts +61 -19
- package/src/public-cli/fixer-run.ts +36 -11
- package/src/public-cli/invocation.ts +137 -101
- package/src/public-cli/judge-run.ts +39 -11
- package/src/public-cli/merger-run.ts +36 -11
- package/src/public-cli/reviewer-run.ts +35 -11
- package/src/public-cli/run-lifecycle.ts +89 -76
- package/src/public-cli/settlement.ts +256 -66
- package/src/reviewer-child-executor.ts +1 -1
- package/src/role-runtime.ts +18 -8
- package/src/ticket-frontmatter.ts +94 -0
- package/src/ticket-trajectory.ts +145 -5
- package/src/typed-provider-http.ts +99 -0
- package/src/upstream-error-testimony.ts +45 -0
|
@@ -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
|
@@ -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 =
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
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:
|
|
378
|
+
evidenceChildFailure: EvidenceChildFailureClassification;
|
|
351
379
|
evidenceChildOriginal?: unknown;
|
|
352
380
|
}>;
|
|
353
|
-
function
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
...(
|
|
838
|
-
...(response.
|
|
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
|
});
|
package/src/factory-board.ts
CHANGED
|
@@ -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
|
-
|
|
1528
|
+
let lane: RenderedLane;
|
|
1529
|
+
lane = await renderLaneHtml(
|
|
1473
1530
|
book.bookKey,
|
|
1474
1531
|
resolve(book.ledgerDir),
|
|
1475
1532
|
bookSnap.tickets,
|