@akagilnc/pi-workflow-roles 0.1.4620 → 0.1.4631
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.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +231 -218
- package/dist/activation-ledger-topology.js +0 -4
- package/dist/headless-host/production-host.js +215 -202
- package/dist/pi/role-turn-host.js +0 -3
- package/dist/public-cli/auto-resume.js +26 -0
- package/dist/public-cli/main.js +1054 -780
- package/dist/public-cli/post-admission.js +101 -7
- package/dist/public-cli/process-cancel.js +57 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +0 -1
- package/src/activation-ledger-topology.ts +1 -6
- package/src/activation-ledger.ts +5 -236
- package/src/analyst-entry.ts +19 -2
- package/src/analyst-ledger.ts +111 -6
- package/src/archivist-record-entry.ts +1 -1
- package/src/pi/role-turn-host.ts +0 -3
- package/src/public-cli/auto-resume.ts +34 -0
- package/src/public-cli/cli.ts +30 -0
- package/src/public-cli/main.ts +25 -3
- package/src/public-cli/post-admission.ts +173 -6
- package/src/public-cli/process-cancel.ts +75 -0
- package/src/role-runtime.ts +4 -51
- package/dist/activation-reconciliation.js +0 -61
- package/src/activation-reconciliation.ts +0 -163
|
@@ -37,10 +37,36 @@ import { mintEngineDetourInvocationScope, readInvocationSelectedHost, withEngine
|
|
|
37
37
|
import { projectHostTransitionPriorNative } from "../host-transition-prior-native.js";
|
|
38
38
|
import { missingCredentialPreDispatchFailure, postRunMissingCredentialFailure, } from "./public-run-credentials.js";
|
|
39
39
|
import { acquireRunWriterLease, clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
|
|
40
|
+
import { processCancelDiagnostic, processCancelSignalName, } from "./process-cancel.js";
|
|
40
41
|
import { homeFromRunDirectory } from "../activation-ledger-topology.js";
|
|
41
42
|
import { attemptProducedFreshSubmission, classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, settleHostEndedNoReceipt, attachRecordedSubmissions, } from "./settlement.js";
|
|
42
43
|
import {} from "./terminal.js";
|
|
43
44
|
import { ensureRealArtifactsDirectory, persistReturnedRunState, runWithAutoResumeLoop, TurnDispatchedFailure, } from "./auto-resume.js";
|
|
45
|
+
/** #855: process-cancel settlement never re-enters auto-resume. */
|
|
46
|
+
function withProcessCancelSkipAutoResume(result, signal) {
|
|
47
|
+
if (processCancelSignalName(signal) === undefined) {
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
return { ...result, skipAutoResume: true };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* #855: turn already started, then a catchable process signal arrived before
|
|
54
|
+
* lawful seal — settle as named cancel failure and skip auto-resume.
|
|
55
|
+
*/
|
|
56
|
+
async function settleProcessCancelAfterTurn(input) {
|
|
57
|
+
const failed = await settleAfterTurnStarted(input.admitted, withEngineDetourInvocationScope({
|
|
58
|
+
timedOut: input.result.timedOut,
|
|
59
|
+
code: input.result.code,
|
|
60
|
+
stderr: input.result.stderr,
|
|
61
|
+
knownDiagnostic: processCancelDiagnostic(input.cancelName),
|
|
62
|
+
}, input.invocationScopeId), input.adapters, input.env.principalAuthority, input.io, input.persistRunState);
|
|
63
|
+
return {
|
|
64
|
+
...failed,
|
|
65
|
+
turnDispatched: true,
|
|
66
|
+
skipAutoResume: true,
|
|
67
|
+
...input.deferredPersist,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
44
70
|
/**
|
|
45
71
|
* Nested station-child role already finished its own call-local loop.
|
|
46
72
|
* Parent records that failure once and must not auto-resume into a re-summon
|
|
@@ -258,6 +284,26 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
258
284
|
if (afterDispatchApplied)
|
|
259
285
|
return result;
|
|
260
286
|
afterDispatchApplied = true;
|
|
287
|
+
// #855: re-read cancel before afterDispatch finalization — never present
|
|
288
|
+
// lawful success after a catchable process signal. skipRunStateWrite: run-state
|
|
289
|
+
// may already be terminal from a prior lawful persist; do not reopen as resumable.
|
|
290
|
+
const cancelBeforeFinish = processCancelSignalName(env.signal);
|
|
291
|
+
if (cancelBeforeFinish !== undefined &&
|
|
292
|
+
result.terminal !== undefined &&
|
|
293
|
+
isLawfulTypedTerminalOutcome(result.terminal.roleOutcome)) {
|
|
294
|
+
result = {
|
|
295
|
+
...(await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
|
|
296
|
+
timedOut: false,
|
|
297
|
+
code: null,
|
|
298
|
+
stderr: "",
|
|
299
|
+
knownDiagnostic: processCancelDiagnostic(cancelBeforeFinish),
|
|
300
|
+
skipRunStateWrite: true,
|
|
301
|
+
}, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState)),
|
|
302
|
+
turnDispatched: true,
|
|
303
|
+
skipAutoResume: true,
|
|
304
|
+
...deferredPersist,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
261
307
|
try {
|
|
262
308
|
// #858: an unbound seat may assert its ticket on the existing receipt.
|
|
263
309
|
// Read the original accepted payload; do not rewrite it, infer from prose,
|
|
@@ -412,13 +458,16 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
412
458
|
result = await env.roleTurnHost.executeTurn(turnRequest);
|
|
413
459
|
}
|
|
414
460
|
catch (error) {
|
|
461
|
+
const processCancelName = processCancelSignalName(env.signal);
|
|
415
462
|
const settled = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
|
|
416
463
|
timedOut: false,
|
|
417
464
|
code: null,
|
|
418
465
|
stderr: "",
|
|
419
|
-
thrown:
|
|
466
|
+
thrown: processCancelName === undefined
|
|
467
|
+
? error
|
|
468
|
+
: new Error(processCancelDiagnostic(processCancelName), { cause: error }),
|
|
420
469
|
}, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
|
|
421
|
-
return await finishAfterTurn({ ...settled, turnDispatched: true, ...deferredPersist });
|
|
470
|
+
return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...settled, turnDispatched: true, ...deferredPersist }, env.signal));
|
|
422
471
|
}
|
|
423
472
|
// `result.stderr` stays live in memory for the real classification below
|
|
424
473
|
// regardless of whether this durable mirror write succeeds. A write
|
|
@@ -514,10 +563,12 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
514
563
|
// retroactively invalidate an acceptance that already sealed this turn
|
|
515
564
|
// — a resolved 429 observed earlier in the same session is exactly
|
|
516
565
|
// that.
|
|
566
|
+
const processCancelName = processCancelSignalName(env.signal);
|
|
517
567
|
const directHostFailureSignal = result.timedOut
|
|
518
568
|
|| result.knownFailure !== undefined
|
|
519
569
|
|| runnerKnownFailure !== undefined
|
|
520
|
-
|| credentialFailure !== undefined
|
|
570
|
+
|| credentialFailure !== undefined
|
|
571
|
+
|| processCancelName !== undefined;
|
|
521
572
|
hostSignalFailed =
|
|
522
573
|
directHostFailureSignal
|
|
523
574
|
|| (result.code !== null && result.code !== 0)
|
|
@@ -553,11 +604,15 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
553
604
|
// where a bare nonzero exit / resolution failure must not be outranked
|
|
554
605
|
// by someone else's earlier success (#836 r12 class 2).
|
|
555
606
|
const staleAcceptanceOutranksRealFailure = !settledIsFreshThisAttempt && hostSignalFailed;
|
|
607
|
+
// #855: re-read cancel after trySettle/attach awaits — a signal in this
|
|
608
|
+
// window must not land as lawful accepted.
|
|
609
|
+
const cancelAfterSettle = processCancelSignalName(env.signal);
|
|
556
610
|
if (settled !== undefined
|
|
557
611
|
&& shouldPresent(settled)
|
|
558
612
|
&& !directHostFailureSignal
|
|
559
613
|
&& !staleAcceptanceOutranksRealFailure
|
|
560
|
-
&& stderrLogWriteFailure === undefined
|
|
614
|
+
&& stderrLogWriteFailure === undefined
|
|
615
|
+
&& cancelAfterSettle === undefined) {
|
|
561
616
|
// This court sealed — drop open-court pointer (bare resume no longer continues it).
|
|
562
617
|
if (settled.roleOutcome.kind === "accepted" &&
|
|
563
618
|
request.courtAttemptId !== undefined &&
|
|
@@ -597,6 +652,22 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
597
652
|
return await finishAfterTurn({ ...settledFailure, turnDispatched: true, ...deferredPersist });
|
|
598
653
|
}
|
|
599
654
|
if (settledOutcome !== undefined) {
|
|
655
|
+
// #855: final cancel re-read before lawful seal — settlement window may
|
|
656
|
+
// have received SIGTERM/SIGINT/SIGHUP after the earlier snapshot.
|
|
657
|
+
const lateCancel = processCancelSignalName(env.signal);
|
|
658
|
+
if (lateCancel !== undefined) {
|
|
659
|
+
return await finishAfterTurn(await settleProcessCancelAfterTurn({
|
|
660
|
+
admitted,
|
|
661
|
+
cancelName: lateCancel,
|
|
662
|
+
result,
|
|
663
|
+
adapters,
|
|
664
|
+
env,
|
|
665
|
+
io,
|
|
666
|
+
persistRunState,
|
|
667
|
+
deferredPersist,
|
|
668
|
+
invocationScopeId: request.invocationScopeId,
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
600
671
|
if (persistRunState) {
|
|
601
672
|
try {
|
|
602
673
|
await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
|
|
@@ -619,16 +690,18 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
619
690
|
thrown: error,
|
|
620
691
|
skipRunStateWrite: true,
|
|
621
692
|
}, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
|
|
622
|
-
return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
|
|
693
|
+
return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...failed, turnDispatched: true, ...deferredPersist }, env.signal));
|
|
623
694
|
}
|
|
624
695
|
}
|
|
625
696
|
// Lawful persist + present is the caller's stop seam (auto-resume loop /
|
|
626
697
|
// manual resume), not this retried host-turn function.
|
|
698
|
+
// #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
|
|
627
699
|
return await finishAfterTurn({ ...settledOutcome, ...deferredPersist });
|
|
628
700
|
}
|
|
629
701
|
// Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
|
|
630
702
|
if (hostSignalFailed) {
|
|
631
703
|
const resolutionInput = controlledFailureInputFromResolution(resolution);
|
|
704
|
+
const processCancelName = processCancelSignalName(env.signal);
|
|
632
705
|
const stderrLogWriteDetails = stderrLogWriteFailure === undefined
|
|
633
706
|
? undefined
|
|
634
707
|
: { stderrLogWriteFailure: describeCaughtError(stderrLogWriteFailure) };
|
|
@@ -637,6 +710,9 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
637
710
|
code: result.code,
|
|
638
711
|
stderr: result.stderr,
|
|
639
712
|
...resolutionInput,
|
|
713
|
+
...(processCancelName === undefined
|
|
714
|
+
? {}
|
|
715
|
+
: { knownDiagnostic: processCancelDiagnostic(processCancelName) }),
|
|
640
716
|
// Secondary fact only — never the cause. Rides on whichever channel
|
|
641
717
|
// classification actually reads (knownFailure.details owns it when
|
|
642
718
|
// a knownFailure exists; the top-level knownDetails otherwise).
|
|
@@ -651,7 +727,7 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
651
727
|
}
|
|
652
728
|
: { knownDetails: stderrLogWriteDetails }),
|
|
653
729
|
}, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
|
|
654
|
-
return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
|
|
730
|
+
return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...failed, turnDispatched: true, ...deferredPersist }, env.signal));
|
|
655
731
|
}
|
|
656
732
|
// Nothing else was wrong, but the durable stderr mirror itself failed to
|
|
657
733
|
// write — that is the real (infrastructure) problem in this case, not a
|
|
@@ -666,6 +742,21 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
666
742
|
return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
|
|
667
743
|
}
|
|
668
744
|
const noReceipt = await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority, courtScope), courtScope);
|
|
745
|
+
// #855: cancel during no_receipt settlement window is not lawful success.
|
|
746
|
+
const noReceiptCancel = processCancelSignalName(env.signal);
|
|
747
|
+
if (noReceiptCancel !== undefined) {
|
|
748
|
+
return await finishAfterTurn(await settleProcessCancelAfterTurn({
|
|
749
|
+
admitted,
|
|
750
|
+
cancelName: noReceiptCancel,
|
|
751
|
+
result,
|
|
752
|
+
adapters,
|
|
753
|
+
env,
|
|
754
|
+
io,
|
|
755
|
+
persistRunState,
|
|
756
|
+
deferredPersist,
|
|
757
|
+
invocationScopeId: request.invocationScopeId,
|
|
758
|
+
}));
|
|
759
|
+
}
|
|
669
760
|
if (persistRunState) {
|
|
670
761
|
try {
|
|
671
762
|
await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
|
|
@@ -683,9 +774,10 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
683
774
|
thrown: error,
|
|
684
775
|
skipRunStateWrite: true,
|
|
685
776
|
}, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
|
|
686
|
-
return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
|
|
777
|
+
return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...failed, turnDispatched: true, ...deferredPersist }, env.signal));
|
|
687
778
|
}
|
|
688
779
|
}
|
|
780
|
+
// #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
|
|
689
781
|
return await finishAfterTurn({
|
|
690
782
|
exitCode: exitCodeForTerminalOutcome(noReceipt.roleOutcome),
|
|
691
783
|
admitted,
|
|
@@ -972,6 +1064,7 @@ export async function runPostAdmissionSeatResume(input) {
|
|
|
972
1064
|
io: input.io,
|
|
973
1065
|
sessionAppender: env.sessionAppender,
|
|
974
1066
|
autoResumeLimit: env.autoResumeLimit,
|
|
1067
|
+
...(env.signal === undefined ? {} : { signal: env.signal }),
|
|
975
1068
|
buildInitialPayload: () => ({ resumeTurn: false }),
|
|
976
1069
|
buildResumePayload: () => ({ resumeTurn: true }),
|
|
977
1070
|
// Same as public manual resume: prior-court sealed acceptance is not a
|
|
@@ -1095,6 +1188,7 @@ export async function runPostAdmissionResumable(input) {
|
|
|
1095
1188
|
io,
|
|
1096
1189
|
sessionAppender: env.sessionAppender,
|
|
1097
1190
|
autoResumeLimit: env.autoResumeLimit,
|
|
1191
|
+
...(env.signal === undefined ? {} : { signal: env.signal }),
|
|
1098
1192
|
buildInitialPayload: buildScopedInitial,
|
|
1099
1193
|
buildResumePayload: buildScopedResume,
|
|
1100
1194
|
dispatch: async (request, lease, _isFirst, attemptIo) => {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #855: catchable process signals for ak-role public entry.
|
|
3
|
+
* SIGTERM / SIGINT / SIGHUP abort the shared controller so hosts can gracefully
|
|
4
|
+
* terminate children (SIGTERM only, never SIGKILL). Uncatchable death (SIGKILL,
|
|
5
|
+
* crash, power loss) is reported via run-state + writer lease on analyst.
|
|
6
|
+
*/
|
|
7
|
+
const CATCHABLE_PROCESS_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
8
|
+
/**
|
|
9
|
+
* Install once per process entry. First catchable signal wins; later signals
|
|
10
|
+
* are ignored so settlement can finish without re-entry storms.
|
|
11
|
+
*/
|
|
12
|
+
export function installProcessCancelHandlers(processRef = process) {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
let received;
|
|
15
|
+
const listeners = new Map();
|
|
16
|
+
for (const sig of CATCHABLE_PROCESS_SIGNALS) {
|
|
17
|
+
const onSignal = () => {
|
|
18
|
+
if (received !== undefined)
|
|
19
|
+
return;
|
|
20
|
+
received = sig;
|
|
21
|
+
try {
|
|
22
|
+
controller.abort(sig);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// already aborted
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
listeners.set(sig, onSignal);
|
|
29
|
+
processRef.on(sig, onSignal);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
receivedSignal: () => received,
|
|
34
|
+
dispose() {
|
|
35
|
+
for (const [sig, onSignal] of listeners) {
|
|
36
|
+
processRef.off(sig, onSignal);
|
|
37
|
+
}
|
|
38
|
+
listeners.clear();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Signal name when this AbortSignal was aborted by installProcessCancelHandlers. */
|
|
43
|
+
export function processCancelSignalName(signal) {
|
|
44
|
+
if (signal === undefined || signal.aborted !== true)
|
|
45
|
+
return undefined;
|
|
46
|
+
const reason = signal.reason;
|
|
47
|
+
if (reason === "SIGTERM" ||
|
|
48
|
+
reason === "SIGINT" ||
|
|
49
|
+
reason === "SIGHUP") {
|
|
50
|
+
return reason;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/** Diagnostic line for a catchable process-signal termination (failure honesty). */
|
|
55
|
+
export function processCancelDiagnostic(signalName) {
|
|
56
|
+
return `ak-role terminated by ${signalName}`;
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -19,7 +19,6 @@ const entries = [
|
|
|
19
19
|
"pi/in-process-session",
|
|
20
20
|
"activation-ledger-git",
|
|
21
21
|
"activation-ledger-topology",
|
|
22
|
-
"activation-reconciliation",
|
|
23
22
|
"archivist-record-entry",
|
|
24
23
|
// Pure subject nest topology — static import of archivist-record-entry;
|
|
25
24
|
// cold discovery without SessionManager (#636). Keep listed while that
|
|
@@ -91,11 +91,6 @@ export function activationBookDirectory(ledgerHome: string, bookKey: string): st
|
|
|
91
91
|
return join(ledgerHome, "books", bookKey);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/** Append-only waiting ledger path for one book. */
|
|
95
|
-
export function activationWaitingLedgerPath(ledgerHome: string, bookKey: string): string {
|
|
96
|
-
return join(activationBookDirectory(ledgerHome, bookKey), "waiting.jsonl");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
94
|
/** True when candidate resolves strictly inside root (boundary-safe; not a string-prefix check). */
|
|
100
95
|
export function pathContainedIn(root: string, candidate: string): boolean {
|
|
101
96
|
const rel = relative(root, candidate);
|
|
@@ -323,7 +318,7 @@ export function ensureRealDirectoryTree(root: string, targetDir: string): string
|
|
|
323
318
|
|
|
324
319
|
/**
|
|
325
320
|
* Reject a pre-existing ledger-file symlink before open/write follows it.
|
|
326
|
-
* A computed book may only
|
|
321
|
+
* A computed book path may only write its own regular file — any symlink,
|
|
327
322
|
* including a target still inside the machine ledger home (cross-book redirect),
|
|
328
323
|
* violates ADR 0048 partition identity. Missing paths are admitted (O_CREAT).
|
|
329
324
|
*/
|
package/src/activation-ledger.ts
CHANGED
|
@@ -1,25 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
writeSync,
|
|
7
|
-
} from "node:fs";
|
|
8
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
9
|
-
|
|
10
|
-
import type { ActivationSessionPointer } from "./activation-ledger-session.ts";
|
|
11
|
-
import {
|
|
12
|
-
ActivationLedgerError,
|
|
13
|
-
activationWaitingLedgerPath,
|
|
14
|
-
assertLedgerFileInsideHome,
|
|
15
|
-
ensureRealDirectoryTree,
|
|
16
|
-
errorText,
|
|
17
|
-
} from "./activation-ledger-topology.ts";
|
|
18
|
-
|
|
19
|
-
/** Private open flags: O_NOFOLLOW closes the lstat→open TOCTOU window. */
|
|
20
|
-
const ACTIVATION_LEDGER_APPEND_OPEN_FLAGS =
|
|
21
|
-
constants.O_APPEND | constants.O_CREAT | constants.O_WRONLY | constants.O_NOFOLLOW;
|
|
22
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Activation ledger surface: durable session principal + book/home topology.
|
|
3
|
+
* #855: two-face waiting.jsonl reconciliation deleted — this module no longer
|
|
4
|
+
* appends accepted-activation facts or own a waiting path.
|
|
5
|
+
*/
|
|
23
6
|
export {
|
|
24
7
|
ActivationGitRepositoryRequiredError,
|
|
25
8
|
resolveBookKeyFromGit,
|
|
@@ -33,219 +16,5 @@ export {
|
|
|
33
16
|
export {
|
|
34
17
|
ActivationLedgerError,
|
|
35
18
|
activationBookDirectory,
|
|
36
|
-
activationWaitingLedgerPath,
|
|
37
19
|
resolveActivationLedgerHome,
|
|
38
20
|
} from "./activation-ledger-topology.ts";
|
|
39
|
-
|
|
40
|
-
/** Caller-preassigned correlation id, or an explicit absent identity (never empty string). */
|
|
41
|
-
export type ActivationCorrelationIdentity =
|
|
42
|
-
| { readonly kind: "caller"; readonly id: string }
|
|
43
|
-
| { readonly kind: "absent" };
|
|
44
|
-
|
|
45
|
-
export const ACCEPTED_ACTIVATION_EVENT = "accepted-activation" as const;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Package-owned index-only top-level keys for accepted-activation facts (ADR 0049).
|
|
49
|
-
* Sole machine key contract: projection and tests consume this descriptor — not prose markers.
|
|
50
|
-
*/
|
|
51
|
-
export const ACCEPTED_ACTIVATION_FACT_KEYS = Object.freeze([
|
|
52
|
-
"event",
|
|
53
|
-
"role",
|
|
54
|
-
"observedAt",
|
|
55
|
-
"bookKey",
|
|
56
|
-
"session",
|
|
57
|
-
"correlation",
|
|
58
|
-
] as const);
|
|
59
|
-
|
|
60
|
-
export type AcceptedActivationFactKey = (typeof ACCEPTED_ACTIVATION_FACT_KEYS)[number];
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Closed activation fact: index fields only (ADR 0049).
|
|
64
|
-
* No prompt, transcript, argv, excerpt, or other content.
|
|
65
|
-
*/
|
|
66
|
-
export type AcceptedActivationFact = {
|
|
67
|
-
readonly event: typeof ACCEPTED_ACTIVATION_EVENT;
|
|
68
|
-
readonly role: string;
|
|
69
|
-
readonly observedAt: string;
|
|
70
|
-
readonly bookKey: string;
|
|
71
|
-
readonly session: ActivationSessionPointer;
|
|
72
|
-
readonly correlation: ActivationCorrelationIdentity;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// Keys tuple ↔ fact type must stay exact (compile fail on drift; no second field list).
|
|
76
|
-
type ExactKeyMatch<T, K extends PropertyKey> =
|
|
77
|
-
Exclude<keyof T, K> | Exclude<K, keyof T> extends never ? true : never;
|
|
78
|
-
const _acceptedActivationFactKeysMatch: ExactKeyMatch<
|
|
79
|
-
AcceptedActivationFact,
|
|
80
|
-
AcceptedActivationFactKey
|
|
81
|
-
> = true;
|
|
82
|
-
void _acceptedActivationFactKeysMatch;
|
|
83
|
-
|
|
84
|
-
/** Trusted typed inputs for building the closed fact (canonical fact minus event discriminant). */
|
|
85
|
-
export type AcceptedActivationFactInput = Omit<AcceptedActivationFact, "event">;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Host correlation channel (not a CLI flag): a non-blank AK_CORRELATION_ID carries
|
|
89
|
-
* the caller id verbatim; missing/blank/whitespace-only yields the typed absent identity.
|
|
90
|
-
*/
|
|
91
|
-
export function correlationIdentityFromEnv(
|
|
92
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
93
|
-
): ActivationCorrelationIdentity {
|
|
94
|
-
const raw = env.AK_CORRELATION_ID;
|
|
95
|
-
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
96
|
-
return { kind: "caller", id: raw };
|
|
97
|
-
}
|
|
98
|
-
return { kind: "absent" };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Descriptor-driven top-level pick: only ACCEPTED_ACTIVATION_FACT_KEYS leave this boundary.
|
|
103
|
-
* Nested session/correlation are rebuilt closed before the pick (ADR 0049: first-phase ledger holds zero content bytes by construction).
|
|
104
|
-
*/
|
|
105
|
-
function projectAcceptedActivationFact(
|
|
106
|
-
input: AcceptedActivationFactInput,
|
|
107
|
-
): AcceptedActivationFact {
|
|
108
|
-
const closed: AcceptedActivationFact = {
|
|
109
|
-
event: ACCEPTED_ACTIVATION_EVENT,
|
|
110
|
-
role: input.role,
|
|
111
|
-
observedAt: input.observedAt,
|
|
112
|
-
bookKey: input.bookKey,
|
|
113
|
-
session: { kind: "session-file", path: input.session.path },
|
|
114
|
-
correlation: input.correlation.kind === "caller"
|
|
115
|
-
? { kind: "caller", id: input.correlation.id }
|
|
116
|
-
: { kind: "absent" },
|
|
117
|
-
};
|
|
118
|
-
// Descriptor is the sole top-level emission contract (typed pick; no content keys).
|
|
119
|
-
return Object.fromEntries(
|
|
120
|
-
ACCEPTED_ACTIVATION_FACT_KEYS.map((key) => [key, closed[key]]),
|
|
121
|
-
) as AcceptedActivationFact;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Construct the closed fact from trusted typed inputs only (descriptor projection — no content keys). */
|
|
125
|
-
export function buildAcceptedActivationFact(input: AcceptedActivationFactInput): AcceptedActivationFact {
|
|
126
|
-
return projectAcceptedActivationFact(input);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Serialize only the closed index fields (descriptor projection — no content keys). */
|
|
130
|
-
export function serializeAcceptedActivationFact(fact: AcceptedActivationFact): string {
|
|
131
|
-
return `${JSON.stringify(projectAcceptedActivationFact(fact))}\n`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Bare O_APPEND write of one complete line under an absolute ledger home.
|
|
136
|
-
* Private helper: production-bound to writeSync; owns open/write/close honesty only.
|
|
137
|
-
* Close failure cannot mask open/write; simultaneous close is nested evidence.
|
|
138
|
-
*/
|
|
139
|
-
function appendActivationLedgerLine(
|
|
140
|
-
ledgerPath: string,
|
|
141
|
-
line: Uint8Array,
|
|
142
|
-
options: { ledgerHome: string },
|
|
143
|
-
): void {
|
|
144
|
-
if (!isAbsolute(options.ledgerHome)) {
|
|
145
|
-
throw new ActivationLedgerError(
|
|
146
|
-
`activation ledger home must be absolute: ${options.ledgerHome}`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
const resolvedLedger = resolve(ledgerPath);
|
|
150
|
-
const resolvedHome = resolve(options.ledgerHome);
|
|
151
|
-
const parent = dirname(resolvedLedger);
|
|
152
|
-
ensureRealDirectoryTree(resolvedHome, parent);
|
|
153
|
-
assertLedgerFileInsideHome(resolvedLedger, resolvedHome);
|
|
154
|
-
|
|
155
|
-
// Fail-closed guard (same shape as PR #418's publication seam): on platforms
|
|
156
|
-
// without O_NOFOLLOW (e.g. Windows, nodejs/node#41590) the JS bitwise-or in
|
|
157
|
-
// ACTIVATION_LEDGER_APPEND_OPEN_FLAGS would silently drop the flag and the
|
|
158
|
-
// lstat→open TOCTOU anti-symlink protection would vanish while the comment
|
|
159
|
-
// above still claims it. Refuse loudly via the existing typed ledger failure
|
|
160
|
-
// channel instead of appending unprotected; never silently degrade.
|
|
161
|
-
if (typeof constants.O_NOFOLLOW !== "number") {
|
|
162
|
-
throw new ActivationLedgerError(
|
|
163
|
-
"activation ledger append requires O_NOFOLLOW open-flag support (anti-symlink TOCTOU protection must not be silently dropped); refusing to append",
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
const bytes = Buffer.isBuffer(line) ? line : Buffer.from(line);
|
|
167
|
-
let ledgerFd: number | undefined;
|
|
168
|
-
let primaryFailure: unknown;
|
|
169
|
-
try {
|
|
170
|
-
try {
|
|
171
|
-
ledgerFd = openSync(resolvedLedger, ACTIVATION_LEDGER_APPEND_OPEN_FLAGS, 0o644);
|
|
172
|
-
} catch (error) {
|
|
173
|
-
// Native ELOOP/filesystem cause retained; no errno dispatch.
|
|
174
|
-
throw new ActivationLedgerError(
|
|
175
|
-
`activation ledger failed to open ledger file (${resolvedLedger}): ${errorText(error)}`,
|
|
176
|
-
{ cause: error },
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
let opened: ReturnType<typeof fstatSync>;
|
|
181
|
-
try {
|
|
182
|
-
opened = fstatSync(ledgerFd);
|
|
183
|
-
} catch (error) {
|
|
184
|
-
throw new ActivationLedgerError(
|
|
185
|
-
`activation ledger failed to fstat ledger file (${resolvedLedger}): ${errorText(error)}`,
|
|
186
|
-
{ cause: error },
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
if (!opened.isFile()) {
|
|
190
|
-
throw new ActivationLedgerError(
|
|
191
|
-
`activation ledger is not a regular file: ${resolvedLedger}`,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const written = writeSync(ledgerFd, bytes, 0, bytes.length, null);
|
|
196
|
-
if (written !== bytes.length) {
|
|
197
|
-
throw new ActivationLedgerError(
|
|
198
|
-
`activation ledger short write: wrote ${written} of ${bytes.length} bytes to ${resolvedLedger}`,
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
} catch (error) {
|
|
202
|
-
primaryFailure = error;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (ledgerFd !== undefined) {
|
|
206
|
-
try {
|
|
207
|
-
closeSync(ledgerFd);
|
|
208
|
-
} catch (closeFailure) {
|
|
209
|
-
if (primaryFailure !== undefined) {
|
|
210
|
-
throw new AggregateError(
|
|
211
|
-
[primaryFailure, closeFailure],
|
|
212
|
-
"activation ledger operation and close failed",
|
|
213
|
-
{ cause: primaryFailure },
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
throw closeFailure;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (primaryFailure !== undefined) throw primaryFailure;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Append one complete JSONL record with one O_APPEND write of the full record.
|
|
225
|
-
* Shared-ledger contract: concurrent successful append-only producers cannot
|
|
226
|
-
* overwrite one another. A short write is an honest infrastructure failure
|
|
227
|
-
* (ADR 0049) — no non-append rollback/truncate. Close failure cannot mask the
|
|
228
|
-
* primary write cause; simultaneous close evidence is retained.
|
|
229
|
-
*/
|
|
230
|
-
export function appendAcceptedActivationFact(
|
|
231
|
-
ledgerPath: string,
|
|
232
|
-
fact: AcceptedActivationFact,
|
|
233
|
-
options: { ledgerHome: string },
|
|
234
|
-
): void {
|
|
235
|
-
appendActivationLedgerLine(
|
|
236
|
-
ledgerPath,
|
|
237
|
-
Buffer.from(serializeAcceptedActivationFact(fact), "utf8"),
|
|
238
|
-
{ ledgerHome: options.ledgerHome },
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function appendAcceptedActivationToBook(options: {
|
|
243
|
-
ledgerHome: string;
|
|
244
|
-
fact: AcceptedActivationFact;
|
|
245
|
-
}): void {
|
|
246
|
-
appendAcceptedActivationFact(
|
|
247
|
-
activationWaitingLedgerPath(options.ledgerHome, options.fact.bookKey),
|
|
248
|
-
options.fact,
|
|
249
|
-
{ ledgerHome: options.ledgerHome },
|
|
250
|
-
);
|
|
251
|
-
}
|
package/src/analyst-entry.ts
CHANGED
|
@@ -22,9 +22,12 @@ import {
|
|
|
22
22
|
} from "./analyst-cohort.ts";
|
|
23
23
|
import {
|
|
24
24
|
scanAnalystIssueRuns,
|
|
25
|
+
type AnalystGhostLeg,
|
|
25
26
|
type AnalystReadableRunFacts,
|
|
26
27
|
type AnalystScopedRunScan,
|
|
27
28
|
} from "./analyst-ledger.ts";
|
|
29
|
+
|
|
30
|
+
export type { AnalystGhostLeg, AnalystGhostLeaseCheck } from "./analyst-ledger.ts";
|
|
28
31
|
import {
|
|
29
32
|
mergeAnalystLibraryIndexRows,
|
|
30
33
|
rowFromIssueMetricsPage,
|
|
@@ -173,6 +176,11 @@ export type AnalystIssueModeResult = {
|
|
|
173
176
|
readonly mode: "issue";
|
|
174
177
|
readonly page: AnalystIssueMetricsPage;
|
|
175
178
|
readonly pagePath: string;
|
|
179
|
+
/**
|
|
180
|
+
* #855 live observation: admitted|running runs whose writer lease cannot prove
|
|
181
|
+
* a live holder. Fresh each query — never cached on the metrics page.
|
|
182
|
+
*/
|
|
183
|
+
readonly ghostLegs: readonly AnalystGhostLeg[];
|
|
176
184
|
};
|
|
177
185
|
|
|
178
186
|
export type AnalystSweepModeResult = {
|
|
@@ -264,7 +272,16 @@ export async function readOrComputeAnalystIssuePage(
|
|
|
264
272
|
const raw = await readFile(pagePath, "utf8");
|
|
265
273
|
const page = JSON.parse(raw) as AnalystIssueMetricsPage;
|
|
266
274
|
if (cachedPageMatchesRequestedScope(page, { bookKey, ...input })) {
|
|
267
|
-
|
|
275
|
+
// #855 ghost legs are live observations — always fresh, never from the page cache.
|
|
276
|
+
const ghostScan = await scanAnalystIssueRuns({
|
|
277
|
+
bookKey,
|
|
278
|
+
...(issueNumber === undefined ? {} : { ticketNumber: issueNumber }),
|
|
279
|
+
...(options?.scanProjectRoot === undefined
|
|
280
|
+
? {}
|
|
281
|
+
: { projectRoot: options.scanProjectRoot }),
|
|
282
|
+
...(options?.home === undefined ? {} : { home: options.home }),
|
|
283
|
+
});
|
|
284
|
+
return { mode: "issue", page, pagePath, ghostLegs: ghostScan.ghostLegs };
|
|
268
285
|
}
|
|
269
286
|
// Existing page is for a different / absent ticket scope — same kernel recompute.
|
|
270
287
|
} catch (error) {
|
|
@@ -364,7 +381,7 @@ async function runAnalystIssueMode(
|
|
|
364
381
|
]);
|
|
365
382
|
}
|
|
366
383
|
|
|
367
|
-
return { mode: "issue", page, pagePath };
|
|
384
|
+
return { mode: "issue", page, pagePath, ghostLegs: scan.ghostLegs };
|
|
368
385
|
}
|
|
369
386
|
|
|
370
387
|
async function runAnalystSweepMode(
|