@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.
- package/README.md +5 -5
- package/README.zh-CN.md +5 -5
- package/dist/audit-escalation.js +31 -4
- package/dist/gatekeeper-role.js +25 -4
- package/dist/grok/production-host.js +1167 -862
- package/dist/inspector-contracts.js +5 -2
- package/dist/notary-contracts.js +16 -5
- package/dist/packaged-role-registry.js +0 -7
- package/dist/public-cli/main.js +867 -757
- package/package.json +1 -1
- package/src/audit-escalation.ts +52 -8
- package/src/collector-ledger.ts +309 -74
- package/src/collector-receipt.ts +0 -1
- package/src/collector-role.ts +21 -14
- package/src/diarist-mechanical.ts +16 -5
- package/src/gatekeeper-role.ts +33 -4
- package/src/grok/role-envelope.ts +68 -52
- package/src/inspector-contracts.ts +6 -3
- package/src/inspector-role.ts +5 -2
- package/src/notary-contracts.ts +20 -5
- package/src/notary-role.ts +2 -2
- package/src/packaged-role-registry.ts +0 -12
- package/src/public-cli/cli.ts +50 -125
- package/src/public-cli/coder-run.ts +5 -19
- package/src/public-cli/collector-run.ts +65 -21
- package/src/public-cli/countersign-run.ts +14 -40
- package/src/public-cli/doctor-run.ts +50 -6
- package/src/public-cli/fixer-run.ts +5 -19
- package/src/public-cli/gleaner-left-run.ts +14 -40
- package/src/public-cli/inspector-run.ts +51 -7
- package/src/public-cli/instruction-seat-run.ts +27 -0
- package/src/public-cli/judge-run.ts +11 -36
- package/src/public-cli/merger-run.ts +5 -19
- package/src/public-cli/notary-run.ts +54 -5
- package/src/public-cli/option-definitions.ts +3 -3
- package/src/public-cli/post-admission.ts +76 -0
- package/src/public-cli/reviewer-run.ts +5 -19
- package/src/public-cli/run-lifecycle.ts +342 -107
- package/src/public-cli/settlement.ts +134 -71
- package/src/role-runtime.ts +18 -1
- package/src/submission-ledger.ts +55 -5
package/package.json
CHANGED
package/src/audit-escalation.ts
CHANGED
|
@@ -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:
|
|
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(
|
|
86
|
-
|
|
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:
|
|
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 }),
|
package/src/collector-ledger.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
|
363
|
-
return
|
|
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 (
|
|
417
|
-
|
|
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
|
-
|
|
628
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
719
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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 =
|
|
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
|
-
|
|
753
|
-
|
|
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
|
-
|
|
1050
|
+
commitWaitRecord(record);
|
|
1051
|
+
appendJournal(COLLECTOR_WAIT_ENTRY_TYPE, {
|
|
1052
|
+
waitRecord: record,
|
|
1053
|
+
});
|
|
819
1054
|
return {
|
|
820
1055
|
waitId,
|
|
821
1056
|
requestedMs: input.durationMs,
|
package/src/collector-receipt.ts
CHANGED
|
@@ -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");
|