@akagilnc/pi-workflow-roles 0.1.3280 → 0.1.3353

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.
Files changed (41) hide show
  1. package/README.md +5 -5
  2. package/README.zh-CN.md +5 -5
  3. package/dist/audit-escalation.js +31 -4
  4. package/dist/gatekeeper-role.js +25 -4
  5. package/dist/grok/production-host.js +1167 -862
  6. package/dist/inspector-contracts.js +5 -2
  7. package/dist/notary-contracts.js +16 -5
  8. package/dist/packaged-role-registry.js +0 -7
  9. package/dist/public-cli/main.js +867 -757
  10. package/package.json +1 -1
  11. package/src/audit-escalation.ts +52 -8
  12. package/src/collector-ledger.ts +309 -74
  13. package/src/collector-receipt.ts +0 -1
  14. package/src/collector-role.ts +21 -14
  15. package/src/diarist-mechanical.ts +16 -5
  16. package/src/gatekeeper-role.ts +33 -4
  17. package/src/grok/role-envelope.ts +68 -52
  18. package/src/inspector-contracts.ts +6 -3
  19. package/src/inspector-role.ts +5 -2
  20. package/src/notary-contracts.ts +20 -5
  21. package/src/notary-role.ts +2 -2
  22. package/src/packaged-role-registry.ts +0 -12
  23. package/src/public-cli/cli.ts +50 -125
  24. package/src/public-cli/coder-run.ts +5 -19
  25. package/src/public-cli/collector-run.ts +65 -21
  26. package/src/public-cli/countersign-run.ts +14 -40
  27. package/src/public-cli/doctor-run.ts +50 -6
  28. package/src/public-cli/fixer-run.ts +5 -19
  29. package/src/public-cli/gleaner-left-run.ts +14 -40
  30. package/src/public-cli/inspector-run.ts +51 -7
  31. package/src/public-cli/instruction-seat-run.ts +27 -0
  32. package/src/public-cli/judge-run.ts +11 -36
  33. package/src/public-cli/merger-run.ts +5 -19
  34. package/src/public-cli/notary-run.ts +54 -5
  35. package/src/public-cli/option-definitions.ts +3 -3
  36. package/src/public-cli/post-admission.ts +76 -0
  37. package/src/public-cli/reviewer-run.ts +5 -19
  38. package/src/public-cli/run-lifecycle.ts +342 -107
  39. package/src/public-cli/settlement.ts +134 -71
  40. package/src/role-runtime.ts +18 -1
  41. package/src/submission-ledger.ts +55 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3280",
3
+ "version": "0.1.3353",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -34,6 +34,20 @@ export type AuditEscalationToolResult = {
34
34
  usage?: Usage;
35
35
  };
36
36
 
37
+ export type OfficerAuditEscalationDecision = {
38
+ readonly status: "escalate";
39
+ readonly officer?: "inspector" | "notary";
40
+ readonly reason?: unknown;
41
+ readonly findings?: unknown;
42
+ readonly conflicts?: unknown;
43
+ readonly decisionGate?: unknown;
44
+ readonly usage?: Usage;
45
+ };
46
+
47
+ export type AuditEscalationDecision =
48
+ | Extract<ComplianceDecision, { status: "escalate" }>
49
+ | OfficerAuditEscalationDecision;
50
+
37
51
  /**
38
52
  * Build the escalation delivery face.
39
53
  * Role-delivered fields ride under the escalation discriminator (ADR 0055).
@@ -44,17 +58,26 @@ export type AuditEscalationToolResult = {
44
58
  * never overwritten (not folded, not dropped, not swapped into the audit home).
45
59
  */
46
60
  export function buildAuditEscalationResult(
47
- decision: Extract<ComplianceDecision, { status: "escalate" }>,
61
+ decision: AuditEscalationDecision,
48
62
  deliveredOutput?: unknown,
49
63
  ): AuditEscalationResult {
50
64
  const auditOwned: Record<string, unknown> = {
51
65
  kind: AUDIT_ESCALATION_KIND,
52
66
  };
53
67
  if (Object.hasOwn(decision, "conflicts")) {
54
- auditOwned.conflicts = decision.conflicts;
68
+ auditOwned.conflicts = (decision as { conflicts?: unknown }).conflicts;
55
69
  }
56
70
  if (Object.hasOwn(decision, "decisionGate")) {
57
- auditOwned.auditDecisionGate = decision.decisionGate;
71
+ auditOwned.auditDecisionGate = (decision as { decisionGate?: unknown }).decisionGate;
72
+ }
73
+ if (Object.hasOwn(decision, "reason") && (decision as { reason?: unknown }).reason !== undefined) {
74
+ auditOwned.reason = (decision as { reason?: unknown }).reason;
75
+ }
76
+ if (Object.hasOwn(decision, "officer") && (decision as { officer?: unknown }).officer !== undefined) {
77
+ auditOwned.officer = (decision as { officer?: unknown }).officer;
78
+ }
79
+ if (Object.hasOwn(decision, "findings") && (decision as { findings?: unknown }).findings !== undefined) {
80
+ auditOwned.findings = (decision as { findings?: unknown }).findings;
58
81
  }
59
82
  const deliveredFields =
60
83
  deliveredOutput !== undefined &&
@@ -63,9 +86,14 @@ export function buildAuditEscalationResult(
63
86
  !Array.isArray(deliveredOutput)
64
87
  ? { ...(deliveredOutput as Record<string, unknown>) }
65
88
  : {};
66
- // Role output cannot fill an absent audit-owned field.
89
+ // Role output cannot fill an absent audit/officer-owned field.
67
90
  delete deliveredFields.conflicts;
68
91
  delete deliveredFields.auditDecisionGate;
92
+ if (Object.hasOwn(decision, "officer")) {
93
+ delete deliveredFields.reason;
94
+ delete deliveredFields.officer;
95
+ delete deliveredFields.findings;
96
+ }
69
97
  const result = {
70
98
  ...deliveredFields,
71
99
  ...auditOwned,
@@ -82,11 +110,27 @@ export function isAuditEscalationProjection(
82
110
  return AUDIT_ESCALATION_LIVE_REGISTRY.has(value);
83
111
  }
84
112
 
85
- function humanDecisionText(result: AuditEscalationResult): string {
86
- const lines = ["Human decision required: compliance audit escalation."];
113
+ function humanDecisionText(
114
+ decision: AuditEscalationDecision,
115
+ result: AuditEscalationResult,
116
+ ): string {
117
+ const officer = Object.hasOwn(decision, "officer")
118
+ ? (decision as OfficerAuditEscalationDecision).officer
119
+ : undefined;
120
+ const lines = [
121
+ officer === undefined
122
+ ? "Human decision required: compliance audit escalation."
123
+ : `Human decision required: ${officer} escalation.`,
124
+ ];
125
+ if (officer !== undefined && typeof result.reason === "string") {
126
+ lines.push(`Reason: ${result.reason}`);
127
+ }
87
128
  if (Array.isArray(result.conflicts)) {
88
129
  lines.push("Conflicts:", ...result.conflicts.map((conflict) => `- ${conflict}`));
89
130
  }
131
+ if (officer !== undefined && Array.isArray(result.findings) && result.findings.length > 0) {
132
+ lines.push("Findings:", ...result.findings.map((finding) => `- ${finding}`));
133
+ }
90
134
  const gate = result.auditDecisionGate;
91
135
  if (gate !== null && typeof gate === "object" && !Array.isArray(gate)) {
92
136
  const record = gate as Record<string, unknown>;
@@ -99,12 +143,12 @@ function humanDecisionText(result: AuditEscalationResult): string {
99
143
  }
100
144
 
101
145
  export function projectAuditEscalation(
102
- decision: Extract<ComplianceDecision, { status: "escalate" }>,
146
+ decision: AuditEscalationDecision,
103
147
  deliveredOutput?: unknown,
104
148
  ): AuditEscalationToolResult {
105
149
  const details = buildAuditEscalationResult(decision, deliveredOutput);
106
150
  return {
107
- content: [{ type: "text", text: humanDecisionText(details) }],
151
+ content: [{ type: "text", text: humanDecisionText(decision, details) }],
108
152
  details,
109
153
  terminate: true,
110
154
  ...(decision.usage === undefined ? {} : { usage: decision.usage }),
@@ -116,6 +116,11 @@ export function projectObserveContextView(modelView: ObserveModelView): ObserveM
116
116
 
117
117
  export type CollectorOperationalTool = (typeof COLLECTOR_OPERATIONAL_TOOLS)[number];
118
118
 
119
+ export const COLLECTOR_ACTIVATION_ENTRY_TYPE = "ak-collector-activation" as const;
120
+ export const COLLECTOR_SNAPSHOT_ENTRY_TYPE = "ak-collector-snapshot" as const;
121
+ export const COLLECTOR_REQUEST_ENTRY_TYPE = "ak-collector-request" as const;
122
+ export const COLLECTOR_WAIT_ENTRY_TYPE = "ak-collector-wait" as const;
123
+
119
124
  export type CollectorRequestAttempt = {
120
125
  attemptId: string;
121
126
  requestId: string;
@@ -156,11 +161,23 @@ export type CollectorConfigState = {
156
161
  manifest: CollectorManifest;
157
162
  };
158
163
 
164
+ /** Durable session journal sink owned by the shared execution seam. */
165
+ export type CollectorDurableJournal = {
166
+ append(customType: string, data: unknown): void;
167
+ };
168
+
169
+ export type CollectorLedgerOptions = {
170
+ readonly clock?: CollectorClock | undefined;
171
+ readonly journal?: CollectorDurableJournal | undefined;
172
+ /** Prior dossier custom entries; replayed in order through live business transitions. */
173
+ readonly dossierEntries?: Iterable<unknown> | undefined;
174
+ };
175
+
159
176
  export type CollectorLedger = {
160
177
  readonly config: CollectorConfigState;
161
178
  readonly fatal: boolean;
162
179
  readonly fatalReason: string | undefined;
163
- readonly outputAccepted: boolean;
180
+ readonly outputCandidate: boolean;
164
181
  readonly activationRecorded: boolean;
165
182
  readonly activationTime: Date | undefined;
166
183
  readonly deadlineTime: Date | undefined;
@@ -177,9 +194,9 @@ export type CollectorLedger = {
177
194
  latchFatal(reason: string, cause?: unknown): Error;
178
195
  assertNotFatal(): void;
179
196
  recordActivation(clock: CollectorClock): void;
197
+ recordOutputCandidate(): void;
180
198
  beginOperational(toolName: string, toolCallId: string): void;
181
199
  completeOperational(toolCallId: string): void;
182
- markOutputAccepted(): void;
183
200
  noteCutoffObserved(): void;
184
201
  assertOutputObservationLaw(clock: CollectorClock): void;
185
202
 
@@ -242,10 +259,16 @@ export function collectorToolArgumentsValid(
242
259
  }
243
260
  }
244
261
 
245
- export function createCollectorLedger(config: CollectorConfigState): CollectorLedger {
262
+ export function createCollectorLedger(
263
+ config: CollectorConfigState,
264
+ options?: CollectorLedgerOptions,
265
+ ): CollectorLedger {
266
+ const clock = options?.clock;
267
+ const journal = options?.journal;
246
268
  let fatal = false;
247
269
  let fatalReason: string | undefined;
248
- let outputAccepted = false;
270
+ let outputCandidate = false;
271
+ let pendingOutputCallId: string | undefined;
249
272
  let activationTime: Date | undefined;
250
273
  let deadlineTime: Date | undefined;
251
274
  let activationMono: number | undefined;
@@ -349,6 +372,216 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
349
372
  return { user, prInitial, reviews, reactions, issueComments, reviewComments, prTerminal };
350
373
  };
351
374
 
375
+
376
+ const appendJournal = (customType: string, data: unknown): void => {
377
+ journal?.append(customType, data);
378
+ };
379
+
380
+ const bindDeadlineMonoFromWall = (): void => {
381
+ if (clock === undefined || activationTime === undefined || deadlineTime === undefined) return;
382
+ const wallNow = clock.wallNow().getTime();
383
+ const monoNow = clock.monoNow();
384
+ activationMono = monoNow - (wallNow - activationTime.getTime());
385
+ deadlineMono = monoNow + (deadlineTime.getTime() - wallNow);
386
+ };
387
+
388
+ const rebindSnapshotCompletedMono = (snapshot: CollectorSnapshot): CollectorSnapshot => {
389
+ const copy: CollectorSnapshot = { ...snapshot, evidenceIds: [...snapshot.evidenceIds] };
390
+ if (deadlineTime !== undefined && deadlineMono !== undefined && copy.completedAt !== undefined) {
391
+ const completedWall = new Date(copy.completedAt).getTime();
392
+ copy.completedMono = deadlineMono + (completedWall - deadlineTime.getTime());
393
+ }
394
+ return copy;
395
+ };
396
+
397
+ const commitActivationWindow = (nextActivation: Date, nextDeadline: Date): void => {
398
+ if (activationTime !== undefined) return;
399
+ activationTime = nextActivation;
400
+ deadlineTime = nextDeadline;
401
+ bindDeadlineMonoFromWall();
402
+ };
403
+
404
+ const recoverAmbiguousRequestLosses = (
405
+ snapshotId: string,
406
+ observedRecords: readonly CollectorEvidenceRecord[],
407
+ ): void => {
408
+ for (const failure of transportFailures) {
409
+ if (failure.recovered || failure.kind !== "ambiguous_request_loss") continue;
410
+ if (failure.marker === undefined || failure.requestId === undefined) continue;
411
+ const found = observedRecords.find((record) =>
412
+ record.kind === "issue_comment" &&
413
+ record.authorLogin === requesterLogin &&
414
+ typeof record.body === "string" &&
415
+ record.body.includes(failure.marker!)
416
+ );
417
+ if (found === undefined) continue;
418
+ failure.recovered = true;
419
+ const attempt = attempts.find((item) =>
420
+ item.status === "ambiguous_loss" &&
421
+ item.requestId === failure.requestId &&
422
+ item.marker === failure.marker
423
+ );
424
+ if (attempt !== undefined) {
425
+ attempt.status = "recovered";
426
+ attempt.commentEvidenceId = found.evidenceId;
427
+ attempt.recoverySnapshotId = snapshotId;
428
+ }
429
+ }
430
+ };
431
+
432
+ const finalizeObservedSnapshot = (snapshot: CollectorSnapshot): CollectorSnapshot => {
433
+ if (requesterLogin === undefined) {
434
+ for (const id of snapshot.evidenceIds) {
435
+ const record = evidenceById.get(id);
436
+ if (record?.kind === "authenticated_user" && record.authorLogin !== undefined) {
437
+ requesterLogin = record.authorLogin.toLowerCase();
438
+ break;
439
+ }
440
+ }
441
+ }
442
+ const stored = rebindSnapshotCompletedMono(snapshot);
443
+ snapshots.push(stored);
444
+ latestCompleteSnapshotId = stored.snapshotId;
445
+ const observedRecords = stored.evidenceIds.flatMap((id) => {
446
+ const record = evidenceById.get(id);
447
+ return record === undefined ? [] : [record];
448
+ });
449
+ recoverAmbiguousRequestLosses(stored.snapshotId, observedRecords);
450
+ observedGeneration = mutationGeneration;
451
+ if (
452
+ finalObservationRequired &&
453
+ stored.completedMono !== undefined &&
454
+ deadlineMono !== undefined &&
455
+ stored.completedMono >= deadlineMono
456
+ ) {
457
+ finalObservationCompleted = true;
458
+ } else if (
459
+ clock !== undefined &&
460
+ deadlineMono !== undefined &&
461
+ clock.monoNow() >= deadlineMono &&
462
+ stored.completedMono !== undefined &&
463
+ stored.completedMono >= deadlineMono
464
+ ) {
465
+ finalObservationRequired = true;
466
+ finalObservationCompleted = true;
467
+ }
468
+ return stored;
469
+ };
470
+
471
+ const commitObservedSnapshot = (
472
+ snapshot: CollectorSnapshot,
473
+ evidenceRecords: readonly CollectorEvidenceRecord[],
474
+ ): CollectorSnapshot => {
475
+ for (const record of evidenceRecords) {
476
+ storeEvidence(record);
477
+ }
478
+ return finalizeObservedSnapshot(snapshot);
479
+ };
480
+
481
+ const commitRequestAttempt = (
482
+ attempt: CollectorRequestAttempt,
483
+ attemptKey: string,
484
+ commentEvidence: CollectorEvidenceRecord | undefined,
485
+ ): void => {
486
+ if (commentEvidence !== undefined) {
487
+ storeEvidence(commentEvidence);
488
+ }
489
+ const existing = attempts.find((item) => item.attemptId === attempt.attemptId);
490
+ if (existing === undefined) {
491
+ attempts.push({ ...attempt });
492
+ attemptKeys.add(attemptKey);
493
+ } else {
494
+ Object.assign(existing, attempt);
495
+ }
496
+ if (attempt.status === "ambiguous_loss" && !transportFailures.some((failure) => failure.requestId === attempt.requestId && failure.observedHead === attempt.observedHead)) {
497
+ transportFailures.push({
498
+ failureId: sha256Text(`loss:${attempt.attemptId}`).slice(0, 16),
499
+ kind: "ambiguous_request_loss",
500
+ message: attempt.responseDiagnostics ?? "ambiguous request loss",
501
+ requestId: attempt.requestId,
502
+ observedHead: attempt.observedHead,
503
+ marker: attempt.marker,
504
+ recovered: false,
505
+ });
506
+ }
507
+ if (attempt.status === "succeeded" || attempt.status === "ambiguous_loss") {
508
+ mutationGeneration += 1;
509
+ finalObservationCompleted = false;
510
+ }
511
+ };
512
+
513
+ const commitWaitRecord = (wait: CollectorWaitRecord): void => {
514
+ waits.push({ ...wait });
515
+ if (wait.cutoffReached) finalObservationRequired = true;
516
+ mutationGeneration += 1;
517
+ finalObservationCompleted = false;
518
+ };
519
+
520
+ const replayDossierEntries = (entries: Iterable<unknown>): void => {
521
+ const orderedEntries = Array.from(entries);
522
+ const finalRequestStatus = new Map<string, CollectorRequestAttempt["status"]>();
523
+ for (const raw of orderedEntries) {
524
+ if (typeof raw !== "object" || raw === null) continue;
525
+ const entry = raw as { type?: unknown; customType?: unknown; data?: unknown };
526
+ if (entry.type !== "custom" || entry.customType !== COLLECTOR_REQUEST_ENTRY_TYPE || typeof entry.data !== "object" || entry.data === null) continue;
527
+ const attempt = (entry.data as Record<string, unknown>).attempt as CollectorRequestAttempt | undefined;
528
+ if (attempt?.attemptId !== undefined) finalRequestStatus.set(attempt.attemptId, attempt.status);
529
+ }
530
+ for (const raw of orderedEntries) {
531
+ if (typeof raw !== "object" || raw === null) continue;
532
+ const entry = raw as { type?: unknown; customType?: unknown; data?: unknown };
533
+ if (entry.type !== "custom" || typeof entry.data !== "object" || entry.data === null) continue;
534
+ const data = entry.data as Record<string, unknown>;
535
+ if (entry.customType === COLLECTOR_ACTIVATION_ENTRY_TYPE) {
536
+ if (typeof data.activationTime === "string" && typeof data.deadlineTime === "string") {
537
+ commitActivationWindow(new Date(data.activationTime), new Date(data.deadlineTime));
538
+ }
539
+ continue;
540
+ }
541
+ if (entry.customType === COLLECTOR_SNAPSHOT_ENTRY_TYPE) {
542
+ const snapshot = data.snapshot as CollectorSnapshot | undefined;
543
+ if (snapshot?.snapshotId === undefined || !Array.isArray(data.evidence)) continue;
544
+ // Generations advance only via request/wait commits and observation sync — never from collection sizes or payload counters.
545
+ commitObservedSnapshot(snapshot, data.evidence as CollectorEvidenceRecord[]);
546
+ continue;
547
+ }
548
+ if (entry.customType === COLLECTOR_REQUEST_ENTRY_TYPE) {
549
+ const attempt = data.attempt as CollectorRequestAttempt | undefined;
550
+ if (attempt?.attemptId === undefined) continue;
551
+ const attemptKey = typeof data.attemptKey === "string"
552
+ ? data.attemptKey
553
+ : [config.repository.canonical, String(config.prNumber), attempt.observedHead, attempt.requestId].join("|");
554
+ const commentEvidence = data.commentEvidence as CollectorEvidenceRecord | undefined;
555
+ const replayAttempt = attempt.status === "started" && finalRequestStatus.get(attempt.attemptId) === "started"
556
+ ? {
557
+ ...attempt,
558
+ status: "ambiguous_loss" as const,
559
+ responseDiagnostics: "request interrupted after dispatch before completion was recorded",
560
+ }
561
+ : attempt;
562
+ commitRequestAttempt(replayAttempt, attemptKey, commentEvidence);
563
+ continue;
564
+ }
565
+ if (entry.customType === COLLECTOR_WAIT_ENTRY_TYPE) {
566
+ const wait = data.waitRecord as CollectorWaitRecord | undefined;
567
+ if (wait?.waitId === undefined) continue;
568
+ commitWaitRecord(wait);
569
+ }
570
+ }
571
+ };
572
+
573
+ if (options?.dossierEntries !== undefined) {
574
+ replayDossierEntries(options.dossierEntries);
575
+ }
576
+ if (clock !== undefined && deadlineMono !== undefined && clock.monoNow() >= deadlineMono) {
577
+ finalObservationRequired = true;
578
+ const lastSnap = snapshots.find((item) => item.snapshotId === latestCompleteSnapshotId);
579
+ if (lastSnap?.completedMono !== undefined && lastSnap.completedMono >= deadlineMono) {
580
+ finalObservationCompleted = true;
581
+ }
582
+ }
583
+
584
+
352
585
  const ledger: CollectorLedger = {
353
586
  get config() {
354
587
  return config;
@@ -359,8 +592,8 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
359
592
  get fatalReason() {
360
593
  return fatalReason;
361
594
  },
362
- get outputAccepted() {
363
- return outputAccepted;
595
+ get outputCandidate() {
596
+ return outputCandidate || pendingOutputCallId !== undefined;
364
597
  },
365
598
  get activationRecorded() {
366
599
  return activationTime !== undefined;
@@ -409,12 +642,28 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
409
642
  activationMono = clock.monoNow();
410
643
  deadlineTime = new Date(activationTime.getTime() + COLLECTOR_ELIGIBILITY_MS);
411
644
  deadlineMono = activationMono + COLLECTOR_ELIGIBILITY_MS;
645
+ appendJournal(COLLECTOR_ACTIVATION_ENTRY_TYPE, {
646
+ activationTime: activationTime.toISOString(),
647
+ deadlineTime: deadlineTime.toISOString(),
648
+ });
649
+ },
650
+
651
+ recordOutputCandidate() {
652
+ assertNotFatal();
653
+ outputCandidate = true;
412
654
  },
413
655
 
414
656
  beginOperational(toolName, toolCallId) {
415
657
  assertNotFatal();
416
- if (outputAccepted && toolName !== COLLECTOR_OUTPUT_TOOL) {
417
- throw latchFatal("回执已受理,本局不再受理操作");
658
+ if (
659
+ toolName !== COLLECTOR_OUTPUT_TOOL &&
660
+ (outputCandidate || pendingOutputCallId !== undefined)
661
+ ) {
662
+ throw new Error("通进司已产出输出候选,本局不再受理操作");
663
+ }
664
+ if (toolName === COLLECTOR_OUTPUT_TOOL) {
665
+ pendingOutputCallId = toolCallId;
666
+ return;
418
667
  }
419
668
  // Idempotent for the same call across tool_call preflight and execute.
420
669
  if (activeOperationalCallId === toolCallId) {
@@ -424,10 +673,6 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
424
673
  throw latchFatal("通进司操作调用已在进行");
425
674
  }
426
675
 
427
- if (toolName === COLLECTOR_OUTPUT_TOOL) {
428
- return;
429
- }
430
-
431
676
  if (!isOperationalTool(toolName)) {
432
677
  throw latchFatal(`未知通进司工具 ${toolName}`);
433
678
  }
@@ -435,17 +680,14 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
435
680
  },
436
681
 
437
682
  completeOperational(toolCallId) {
683
+ if (pendingOutputCallId === toolCallId) {
684
+ pendingOutputCallId = undefined;
685
+ }
438
686
  if (activeOperationalCallId === toolCallId) {
439
687
  activeOperationalCallId = undefined;
440
688
  }
441
689
  },
442
690
 
443
- markOutputAccepted() {
444
- assertNotFatal();
445
- if (outputAccepted) throw latchFatal("通进司回执为唯一终局");
446
- outputAccepted = true;
447
- },
448
-
449
691
  noteCutoffObserved() {
450
692
  finalObservationRequired = true;
451
693
  },
@@ -584,31 +826,6 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
584
826
  `${completedAt}:${pr.headOid}:${storedIds.join(",")}`,
585
827
  ).slice(0, 16);
586
828
 
587
- // Recover ambiguous request markers if present.
588
- for (const failure of transportFailures) {
589
- if (failure.recovered || failure.kind !== "ambiguous_request_loss") continue;
590
- if (failure.marker === undefined || failure.requestId === undefined) continue;
591
- const found = storedRecords.find((record) =>
592
- record.kind === "issue_comment" &&
593
- record.authorLogin === requesterLogin &&
594
- typeof record.body === "string" &&
595
- record.body.includes(failure.marker!)
596
- );
597
- if (found) {
598
- failure.recovered = true;
599
- const attempt = attempts.find((item) =>
600
- item.status === "ambiguous_loss" &&
601
- item.requestId === failure.requestId &&
602
- item.marker === failure.marker
603
- );
604
- if (attempt) {
605
- attempt.status = "recovered";
606
- attempt.commentEvidenceId = found.evidenceId;
607
- attempt.recoverySnapshotId = snapshotId;
608
- }
609
- }
610
- }
611
-
612
829
  const snapshot: CollectorSnapshot = {
613
830
  snapshotId,
614
831
  observedAt,
@@ -624,23 +841,33 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
624
841
  pageDiagnostics,
625
842
  normalizedByteLength,
626
843
  };
627
- snapshots.push(snapshot);
628
- latestCompleteSnapshotId = snapshotId;
629
- observedGeneration = mutationGeneration;
630
- if (finalObservationRequired && completedMono >= (deadlineMono ?? 0)) {
631
- finalObservationCompleted = true;
632
- } else if (cutoff) {
633
- finalObservationCompleted = true;
844
+ if (cutoff) {
845
+ finalObservationRequired = true;
634
846
  }
847
+ const committed = finalizeObservedSnapshot(snapshot);
848
+ appendJournal(COLLECTOR_SNAPSHOT_ENTRY_TYPE, {
849
+ snapshot: committed,
850
+ evidence: committed.evidenceIds.flatMap((id) => {
851
+ const record = evidenceById.get(id);
852
+ return record === undefined ? [] : [record];
853
+ }),
854
+ });
635
855
 
636
856
  const modelView = buildObserveModelView({
637
- snapshot,
638
- records: storedRecords,
857
+ snapshot: committed,
858
+ records: committed.evidenceIds.flatMap((id) => {
859
+ const record = evidenceById.get(id);
860
+ return record === undefined ? [] : [record];
861
+ }),
639
862
  requesterLogin,
640
863
  attempts,
641
864
  });
642
865
 
643
- return { snapshot, contextView: projectObserveContextView(modelView) };
866
+ return {
867
+ snapshot: committed,
868
+ modelView,
869
+ contextView: projectObserveContextView(modelView),
870
+ };
644
871
  },
645
872
 
646
873
  async request(input, transport, clock, signal) {
@@ -715,8 +942,11 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
715
942
  startedAt,
716
943
  status: "started",
717
944
  };
718
- attempts.push(attempt);
719
- attemptKeys.add(attemptKey);
945
+ commitRequestAttempt(attempt, attemptKey, undefined);
946
+ appendJournal(COLLECTOR_REQUEST_ENTRY_TYPE, {
947
+ attempt: { ...attempt },
948
+ attemptKey,
949
+ });
720
950
 
721
951
  const result = await transport.createIssueComment({
722
952
  owner: config.repository.owner,
@@ -726,16 +956,26 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
726
956
  ...(signal === undefined ? {} : { signal }),
727
957
  });
728
958
 
729
- // Successful or ambiguous request completion dirties observation generation.
730
- mutationGeneration += 1;
731
- finalObservationCompleted = false;
959
+ const journalRequest = (commentEvidence: CollectorEvidenceRecord | undefined): void => {
960
+ const attemptKey = [
961
+ config.repository.canonical,
962
+ String(config.prNumber),
963
+ attempt.observedHead,
964
+ attempt.requestId,
965
+ ].join("|");
966
+ appendJournal(COLLECTOR_REQUEST_ENTRY_TYPE, {
967
+ attempt: { ...attempt },
968
+ attemptKey,
969
+ commentEvidence,
970
+ });
971
+ };
732
972
 
733
973
  if (result.kind === "success") {
734
- const record = storeEvidence(
735
- normalizeIssueCommentEvidence(result.comment, startedAt),
736
- );
974
+ const record = normalizeIssueCommentEvidence(result.comment, startedAt);
737
975
  attempt.status = "succeeded";
738
976
  attempt.commentEvidenceId = record.evidenceId;
977
+ commitRequestAttempt(attempt, attemptKey, record);
978
+ journalRequest(record);
739
979
  return {
740
980
  status: "succeeded",
741
981
  attemptId,
@@ -749,15 +989,8 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
749
989
  if (result.kind === "ambiguous_loss") {
750
990
  attempt.status = "ambiguous_loss";
751
991
  attempt.responseDiagnostics = result.diagnostics;
752
- transportFailures.push({
753
- failureId: sha256Text(`loss:${attemptId}`).slice(0, 16),
754
- kind: "ambiguous_request_loss",
755
- message: result.diagnostics,
756
- requestId: request.id,
757
- observedHead: snapshot.headOid,
758
- marker,
759
- recovered: false,
760
- });
992
+ commitRequestAttempt(attempt, attemptKey, undefined);
993
+ journalRequest(undefined);
761
994
  return {
762
995
  status: "ambiguous_loss",
763
996
  attemptId,
@@ -770,6 +1003,8 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
770
1003
 
771
1004
  attempt.status = "rejected";
772
1005
  attempt.responseDiagnostics = result.diagnostics;
1006
+ commitRequestAttempt(attempt, attemptKey, undefined);
1007
+ journalRequest(undefined);
773
1008
  throw latchFatal(`通进司请求被拒:${result.diagnostics}`);
774
1009
  },
775
1010
 
@@ -804,9 +1039,6 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
804
1039
  await clock.sleep(effectiveMs, signal);
805
1040
  const endedAt = clock.wallNow().toISOString();
806
1041
  const cutoffReached = pastCutoff(clock);
807
- if (cutoffReached) finalObservationRequired = true;
808
- mutationGeneration += 1;
809
- finalObservationCompleted = false;
810
1042
  const record: CollectorWaitRecord = {
811
1043
  waitId,
812
1044
  requestedMs: input.durationMs,
@@ -815,7 +1047,10 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
815
1047
  endedAt,
816
1048
  cutoffReached,
817
1049
  };
818
- waits.push(record);
1050
+ commitWaitRecord(record);
1051
+ appendJournal(COLLECTOR_WAIT_ENTRY_TYPE, {
1052
+ waitRecord: record,
1053
+ });
819
1054
  return {
820
1055
  waitId,
821
1056
  requestedMs: input.durationMs,
@@ -42,7 +42,6 @@ export function buildCollectorReceipt(
42
42
  clock?: CollectorClock,
43
43
  ): CollectorReceipt {
44
44
  ledger.assertNotFatal();
45
- if (ledger.outputAccepted) fail("Collector output is singleton");
46
45
  if (ledger.unresolvedTransportFailure) fail("Collector cannot output while a transport failure is unrecovered");
47
46
  if (ledger.latestCompleteSnapshotId === undefined) fail("Collector output requires a complete final snapshot");
48
47
  if (ledger.activationTime === undefined || ledger.deadlineTime === undefined) fail("Collector output requires activation timeline");