@akagilnc/pi-workflow-roles 0.1.5504 → 0.1.5575
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +5965 -5994
- package/dist/analyst-gate-cycles-read.js +23 -55
- package/dist/analyst-ledger.js +21 -35
- package/dist/analyst-metric-families/acceptance-success-rework.js +25 -1
- package/dist/analyst-metric-families/round-timeline.js +2 -5
- package/dist/archivist-record-pointer.js +38 -30
- package/dist/auditor-dossier-tool.js +4 -3
- package/dist/auditor-soul.js +3 -11
- package/dist/doctor-evidence.js +3 -3
- package/dist/engine-detour-usage.js +6 -14
- package/dist/headless-host/production-host.js +5316 -5324
- package/dist/host-descriptions.js +0 -4
- package/dist/host-session-record.js +266 -267
- package/dist/migrate-book-topology.js +1314 -696
- package/dist/notary-source-run.js +1 -1
- package/dist/packaged-role-registry.js +5 -44
- package/dist/pi/role-turn-host.js +53 -3
- package/dist/public-cli/auto-resume.js +8 -47
- package/dist/public-cli/instruction-seat-run.js +5 -3
- package/dist/public-cli/invocation.js +51 -56
- package/dist/public-cli/main.js +1916 -1748
- package/dist/public-cli/post-admission.js +96 -55
- package/dist/public-cli/run-lifecycle.js +27 -53
- package/dist/public-cli/settlement.js +157 -302
- package/dist/role-run-placement.js +1 -11
- package/dist/role-run-relocation.js +72 -50
- package/dist/run-dossier-files.js +30 -0
- package/dist/run-dossier.js +322 -0
- package/dist/run-terminal-artifacts.js +68 -197
- package/dist/run-ticket-number.js +16 -3
- package/dist/session-identity.js +49 -18
- package/dist/sitian-appender.js +13 -16
- package/dist/sitian-facade.js +11 -0
- package/dist/sitian-reader.js +44 -29
- package/dist/submission-gate.js +1 -1
- package/dist/ticket-provenance.js +22 -7
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -0
- package/src/acp-host/production-host.ts +1 -1
- package/src/acp-host/role-turn-host.ts +9 -2
- package/src/analyst-gate-cycles-read.ts +26 -60
- package/src/analyst-ledger.ts +27 -42
- package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
- package/src/analyst-metric-families/gate-cycles.ts +1 -1
- package/src/analyst-metric-families/round-timeline.ts +2 -5
- package/src/analyst-page.ts +3 -3
- package/src/archivist-record-entry.ts +2 -2
- package/src/archivist-record-pointer.ts +50 -36
- package/src/auditor-dossier-tool.ts +7 -5
- package/src/auditor-soul.ts +3 -9
- package/src/book-topology-migration-placement.ts +16 -3
- package/src/book-topology-record-class-migrators.ts +4 -2
- package/src/book-topology-runs-migrator.ts +6 -3
- package/src/doctor-contracts.ts +1 -1
- package/src/doctor-evidence.ts +2 -2
- package/src/engine-detour-tool.ts +1 -1
- package/src/engine-detour-usage.ts +6 -16
- package/src/external-host-turn-loop.ts +25 -0
- package/src/headless-host/production-host.ts +1 -4
- package/src/headless-host/role-turn-host.ts +54 -20
- package/src/host-descriptions.ts +0 -6
- package/src/host-session-record.ts +224 -140
- package/src/notary-source-run.ts +1 -1
- package/src/packaged-role-registry.ts +1 -50
- package/src/pi/adapter.ts +50 -7
- package/src/pi/role-turn-host.ts +51 -3
- package/src/public-cli/auto-resume.ts +7 -43
- package/src/public-cli/instruction-seat-run.ts +9 -2
- package/src/public-cli/invocation.ts +61 -87
- package/src/public-cli/post-admission.ts +111 -72
- package/src/public-cli/run-lifecycle.ts +27 -72
- package/src/public-cli/settlement.ts +193 -418
- package/src/public-cli/terminal.ts +1 -1
- package/src/role-run-placement.ts +2 -13
- package/src/role-run-relocation.ts +71 -68
- package/src/role-runtime.ts +6 -10
- package/src/run-dossier-files.ts +33 -0
- package/src/run-dossier.ts +357 -0
- package/src/run-terminal-artifacts.ts +83 -237
- package/src/run-ticket-number.ts +41 -11
- package/src/session-identity.ts +52 -20
- package/src/sitian-appender.ts +17 -18
- package/src/sitian-facade.ts +22 -0
- package/src/sitian-reader.ts +62 -32
- package/src/submission-gate.ts +1 -1
- package/src/ticket-provenance.ts +20 -7
- package/src/ticket-trajectory.ts +10 -10
|
@@ -1,275 +1,121 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* The reader only checks presence and structural
|
|
2
|
+
* The leg's terminal: the `terminal` section of current.json (#1161), written
|
|
3
|
+
* whole by settlement, last write wins. `report` = an accepted / escalated
|
|
4
|
+
* receipt, `error` = a controlled failure, `no_receipt` = the leg ended with no
|
|
5
|
+
* accepted receipt (#836). The reader only checks presence and structural
|
|
6
6
|
* readability — it does not re-derive role outcomes or invent a second
|
|
7
7
|
* candidate algorithm.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
11
|
-
import { basename, dirname, join } from "node:path";
|
|
9
|
+
import { basename } from "node:path";
|
|
12
10
|
|
|
13
|
-
import { parseRunLeaf
|
|
11
|
+
import { parseRunLeaf } from "./role-run-placement.ts";
|
|
12
|
+
import { readHistoryRowsSync, readSectionSync, writeSectionSync } from "./run-dossier.ts";
|
|
14
13
|
|
|
15
|
-
import { isRecord
|
|
14
|
+
import { isRecord } from "./unknown-value.ts";
|
|
16
15
|
|
|
17
|
-
export
|
|
18
|
-
export const RUN_TERMINAL_ERROR_FILE = "error.json" as const;
|
|
19
|
-
export const RUN_TERMINAL_EVIDENCE_FILE = "evidence.json" as const;
|
|
20
|
-
export const RUN_TERMINAL_ERROR_SETTLEMENT_FILE = "error.settlement.json" as const;
|
|
21
|
-
export const RUN_TERMINAL_ARTIFACT_FILES = [
|
|
22
|
-
RUN_TERMINAL_REPORT_FILE,
|
|
23
|
-
RUN_TERMINAL_ERROR_FILE,
|
|
24
|
-
"audit-incomplete.json",
|
|
25
|
-
] as const;
|
|
16
|
+
export type RunTerminalFace = "report" | "error" | "no_receipt";
|
|
26
17
|
|
|
27
|
-
export type
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Fixed durable failure paths publishFailureArtifacts may settle when the
|
|
31
|
-
* conventional artifacts/error.json name cannot be written. Shared face so the
|
|
32
|
-
* reader follows the publisher — not a parallel search algorithm.
|
|
33
|
-
* Relative to the run directory.
|
|
34
|
-
*/
|
|
35
|
-
export const RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS = [
|
|
36
|
-
`artifacts/${RUN_TERMINAL_ERROR_SETTLEMENT_FILE}`,
|
|
37
|
-
RUN_TERMINAL_ERROR_SETTLEMENT_FILE,
|
|
38
|
-
] as const;
|
|
39
|
-
|
|
40
|
-
/** Publisher and reader share the open-ended error face stem. */
|
|
41
|
-
export const UNIQUE_ERROR_FALLBACK_STEM = "error";
|
|
42
|
-
export function uniqueErrorFallbackName(): string {
|
|
43
|
-
return `${UNIQUE_ERROR_FALLBACK_STEM}.${randomUUID()}.json`;
|
|
44
|
-
}
|
|
45
|
-
const UNIQUE_ERROR_FALLBACK_NAME = new RegExp(
|
|
46
|
-
`^${UNIQUE_ERROR_FALLBACK_STEM}\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.json$`, "i",
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
export type RunTerminalArtifactRead =
|
|
18
|
+
export type RunTerminalRead =
|
|
50
19
|
| { readonly status: "absent" }
|
|
51
20
|
| {
|
|
52
21
|
readonly status: "present";
|
|
53
|
-
readonly
|
|
54
|
-
readonly path: string;
|
|
22
|
+
readonly face: RunTerminalFace;
|
|
55
23
|
readonly body: Record<string, unknown>;
|
|
56
24
|
}
|
|
57
|
-
| {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
25
|
+
| { readonly status: "unreadable"; readonly reason: string };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Terminal body field naming the `attempt-history` row this settlement appended.
|
|
29
|
+
* Readers resolve submitted payloads by this identity (#1161 甲); they must not
|
|
30
|
+
* pick the last history row of the same role.
|
|
31
|
+
*/
|
|
32
|
+
export const ATTEMPT_HISTORY_IDENTITY_FIELD = "attemptHistoryIdentity" as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Terminal body field naming the court that wrote this terminal (#1161 R2).
|
|
36
|
+
* Historical `state.jsonl` terminal rows keep it so a later re-projection of the
|
|
37
|
+
* same court can recover that court's `attemptHistoryIdentity` without reading
|
|
38
|
+
* the leg's latest terminal or guessing by payload.
|
|
39
|
+
*/
|
|
40
|
+
export const TERMINAL_COURT_ATTEMPT_FIELD = "courtAttemptId" as const;
|
|
41
|
+
|
|
42
|
+
/** Replace the leg's terminal. */
|
|
43
|
+
export function writeRunTerminal(
|
|
44
|
+
runDirectory: string,
|
|
45
|
+
face: RunTerminalFace,
|
|
46
|
+
body: Record<string, unknown>,
|
|
47
|
+
): void {
|
|
48
|
+
writeSectionSync(runDirectory, "terminal", { face, at: new Date().toISOString(), body });
|
|
49
|
+
}
|
|
63
50
|
|
|
64
51
|
/**
|
|
65
|
-
* Minimum producer-owned face
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* objects are unreadable (ADR 0043).
|
|
52
|
+
* Read the leg's terminal. Minimum producer-owned face: a typed object body
|
|
53
|
+
* with a nonblank `role` (ADR 0043); anything else is unreadable. Absence is a
|
|
54
|
+
* valid no-receipt state. A damaged current.json throws.
|
|
69
55
|
*/
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
56
|
+
export function readRunTerminal(runDirectory: string): RunTerminalRead {
|
|
57
|
+
const terminal = readSectionSync(runDirectory, "terminal");
|
|
58
|
+
if (terminal === undefined) return { status: "absent" };
|
|
59
|
+
const { face, body } = terminal;
|
|
60
|
+
if (face !== "report" && face !== "error" && face !== "no_receipt") {
|
|
61
|
+
return { status: "unreadable", reason: "terminal section has no known face" };
|
|
75
62
|
}
|
|
76
63
|
if (!isRecord(body)) {
|
|
77
|
-
return {
|
|
78
|
-
ok: false,
|
|
79
|
-
reason: `terminal artifact JSON value is not a typed object (${Array.isArray(body) ? "array" : typeof body})`,
|
|
80
|
-
};
|
|
64
|
+
return { status: "unreadable", reason: "terminal body is not a typed object" };
|
|
81
65
|
}
|
|
82
66
|
if (typeof body.role !== "string" || body.role.trim() === "") {
|
|
83
|
-
return {
|
|
84
|
-
ok: false,
|
|
85
|
-
reason: "terminal artifact missing nonblank producer-owned role field",
|
|
86
|
-
};
|
|
67
|
+
return { status: "unreadable", reason: "terminal body missing nonblank producer-owned role field" };
|
|
87
68
|
}
|
|
88
|
-
return {
|
|
69
|
+
if (face !== "report") return { status: "present", face, body };
|
|
70
|
+
const resolved = withSubmittedPayloads(runDirectory, body);
|
|
71
|
+
if (resolved.status === "unreadable") return resolved;
|
|
72
|
+
return { status: "present", face, body: resolved.body };
|
|
89
73
|
}
|
|
90
74
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
let parsed: unknown;
|
|
108
|
-
try {
|
|
109
|
-
parsed = JSON.parse(raw);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
return {
|
|
112
|
-
status: "unreadable",
|
|
113
|
-
file,
|
|
114
|
-
path,
|
|
115
|
-
reason:
|
|
116
|
-
error instanceof Error
|
|
117
|
-
? error.message
|
|
118
|
-
: `terminal artifact JSON parse failed: ${String(error)}`,
|
|
119
|
-
};
|
|
75
|
+
/**
|
|
76
|
+
* The report's outcome carries the verdict facts only; the role's submitted payloads are not
|
|
77
|
+
* stored a second time. A reader that wants them takes them from the attempt-history row this
|
|
78
|
+
* settlement named on the terminal body (`attemptHistoryIdentity`). When payloads are absent
|
|
79
|
+
* and that identity is missing or cannot supply them, the gap is unreadable — never a silent
|
|
80
|
+
* present body (#1161 R2-gap).
|
|
81
|
+
*/
|
|
82
|
+
function withSubmittedPayloads(
|
|
83
|
+
runDirectory: string,
|
|
84
|
+
body: Record<string, unknown>,
|
|
85
|
+
):
|
|
86
|
+
| { readonly status: "present"; readonly body: Record<string, unknown> }
|
|
87
|
+
| { readonly status: "unreadable"; readonly reason: string } {
|
|
88
|
+
const outcome = body.outcome;
|
|
89
|
+
if (!isRecord(outcome) || outcome.payloads !== undefined) {
|
|
90
|
+
return { status: "present", body };
|
|
120
91
|
}
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
92
|
+
const identity = body[ATTEMPT_HISTORY_IDENTITY_FIELD];
|
|
93
|
+
if (typeof identity !== "string" || identity.trim() === "") {
|
|
123
94
|
return {
|
|
124
95
|
status: "unreadable",
|
|
125
|
-
|
|
126
|
-
path,
|
|
127
|
-
reason: usable.reason,
|
|
96
|
+
reason: "terminal report has no payloads and no attemptHistoryIdentity",
|
|
128
97
|
};
|
|
129
98
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
let names: string[];
|
|
139
|
-
try {
|
|
140
|
-
names = await readdir(dir);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
if (isMissingPathError(error)) continue;
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
for (const name of names.sort((a, b) => a.localeCompare(b))) {
|
|
146
|
-
if (!UNIQUE_ERROR_FALLBACK_NAME.test(name)) continue;
|
|
147
|
-
found.push(join(dir, name));
|
|
99
|
+
for (const row of readHistoryRowsSync(runDirectory)) {
|
|
100
|
+
if (row.kind !== "attempt-history" || row.identity !== identity || !isRecord(row.payload)) continue;
|
|
101
|
+
const attempt = row.payload.outcome;
|
|
102
|
+
if (isRecord(attempt) && Array.isArray(attempt.payloads)) {
|
|
103
|
+
return {
|
|
104
|
+
status: "present",
|
|
105
|
+
body: { ...body, outcome: { ...outcome, payloads: attempt.payloads } },
|
|
106
|
+
};
|
|
148
107
|
}
|
|
149
108
|
}
|
|
150
|
-
return
|
|
109
|
+
return {
|
|
110
|
+
status: "unreadable",
|
|
111
|
+
reason: `terminal attemptHistoryIdentity ${identity} has no readable attempt-history payloads`,
|
|
112
|
+
};
|
|
151
113
|
}
|
|
152
114
|
|
|
153
115
|
/**
|
|
154
|
-
*
|
|
155
|
-
* fallbacks are shared across sibling runs, so binding uses this runId only.
|
|
116
|
+
* Run-directory face is `<runId>@<role>`.
|
|
156
117
|
* Sole authority for runDirectory → runId (last `@` split).
|
|
157
118
|
*/
|
|
158
119
|
export function runIdFromRunDirectory(runDirectory: string): string | undefined {
|
|
159
120
|
return parseRunLeaf(basename(runDirectory))?.runId;
|
|
160
121
|
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Shared parent-directory unique fallback may be adopted only when the
|
|
164
|
-
* publisher-owned body.runId equals this run directory's runId. Same-run
|
|
165
|
-
* artifactsDir / runDirectory candidates keep path ownership and skip this.
|
|
166
|
-
* expectedRunId undefined (unparseable run dir) → never bound.
|
|
167
|
-
*/
|
|
168
|
-
function presentUniqueFallbackBoundToRun(
|
|
169
|
-
body: Record<string, unknown>,
|
|
170
|
-
expectedRunId: string | undefined,
|
|
171
|
-
): boolean {
|
|
172
|
-
if (expectedRunId === undefined) return false;
|
|
173
|
-
return typeof body.runId === "string" && body.runId === expectedRunId;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Sole authority for seam-owned unique error.<uuid>.json candidates.
|
|
178
|
-
* Used by both clearOpposite (settlement) and readRunTerminalArtifact — do not
|
|
179
|
-
* re-enumerate the same-run / parent unique set elsewhere.
|
|
180
|
-
* Ownership:
|
|
181
|
-
* - same-run dirs (artifacts/, runDir): path ownership — all unique names
|
|
182
|
-
* - parent runs/: only body.runId-bound faces; unparseable runId or unreadable body → none
|
|
183
|
-
*/
|
|
184
|
-
export async function listSeamOwnedUniqueErrorFacePaths(
|
|
185
|
-
runDirectory: string,
|
|
186
|
-
): Promise<readonly string[]> {
|
|
187
|
-
const artifactsDir = roleRunArtifactsDirectory(runDirectory);
|
|
188
|
-
const owned: string[] = await listUniqueErrorFallbackPaths([
|
|
189
|
-
artifactsDir,
|
|
190
|
-
runDirectory,
|
|
191
|
-
]);
|
|
192
|
-
const expectedRunId = runIdFromRunDirectory(runDirectory);
|
|
193
|
-
for (const path of await listUniqueErrorFallbackPaths([dirname(runDirectory)])) {
|
|
194
|
-
const read = await readTerminalArtifactAtPath(path, "error.json");
|
|
195
|
-
if (read === undefined || read.status !== "present") continue;
|
|
196
|
-
if (!presentUniqueFallbackBoundToRun(read.body, expectedRunId)) continue;
|
|
197
|
-
owned.push(path);
|
|
198
|
-
}
|
|
199
|
-
return owned;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
type PresentOrUnreadable = Exclude<RunTerminalArtifactRead, { status: "absent" }>;
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Publish contract (#953): only failure publish continues after clearOpposite
|
|
206
|
-
* failure, so a multi-class residue (residual report/audit beside a new error
|
|
207
|
-
* face) means the current settlement is failure. Prefer failure-class faces
|
|
208
|
-
* over success/audit — never filesystem mtime (copy/restore/utimes can lie).
|
|
209
|
-
*/
|
|
210
|
-
function failureClassRank(file: RunTerminalArtifactFile): number {
|
|
211
|
-
return file === "error.json" ? 1 : 0;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Read the current typed terminal artifact for a run directory.
|
|
216
|
-
*
|
|
217
|
-
* Candidate set (publisher-owned faces only):
|
|
218
|
-
* 1) conventional artifacts/{report,error,audit-incomplete}.json
|
|
219
|
-
* 2) publisher fixed failure fallbacks (error.settlement.json faces)
|
|
220
|
-
* 3) seam-owned unique error.<uuid>.json via listSeamOwnedUniqueErrorFacePaths
|
|
221
|
-
* (same-run path ownership + parent body.runId binding — shared with clear)
|
|
222
|
-
*
|
|
223
|
-
* Invariant: when more than one present face remains (e.g. clearOpposite failed
|
|
224
|
-
* during failure publish and a fallback was settled beside a residual report),
|
|
225
|
-
* adopt failure-class over success/audit by the publish contract — not mtime.
|
|
226
|
-
* Same-class ties keep candidate enumeration order (conventional before
|
|
227
|
-
* fallbacks before unique).
|
|
228
|
-
*
|
|
229
|
-
* Unreadable faces never outrank a present face. Parent unique unreadable
|
|
230
|
-
* files never enter the shared enumerator (cannot prove run identity).
|
|
231
|
-
* Absence of every known durable face is a valid no-receipt state.
|
|
232
|
-
*/
|
|
233
|
-
export async function readRunTerminalArtifact(
|
|
234
|
-
runDirectory: string,
|
|
235
|
-
): Promise<RunTerminalArtifactRead> {
|
|
236
|
-
const artifactsDir = roleRunArtifactsDirectory(runDirectory);
|
|
237
|
-
const present: Array<Extract<PresentOrUnreadable, { status: "present" }>> = [];
|
|
238
|
-
const unreadable: Array<Extract<PresentOrUnreadable, { status: "unreadable" }>> =
|
|
239
|
-
[];
|
|
240
|
-
|
|
241
|
-
const consider = (read: RunTerminalArtifactRead | undefined): void => {
|
|
242
|
-
if (read === undefined || read.status === "absent") return;
|
|
243
|
-
if (read.status === "present") {
|
|
244
|
-
present.push(read);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
unreadable.push(read);
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
|
|
251
|
-
consider(await readTerminalArtifactAtPath(join(artifactsDir, file), file));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
|
|
255
|
-
consider(
|
|
256
|
-
await readTerminalArtifactAtPath(join(runDirectory, relative), "error.json"),
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Unique same-run + parent-bound faces: one enumerator shared with clear.
|
|
261
|
-
for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
|
|
262
|
-
consider(await readTerminalArtifactAtPath(path, "error.json"));
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (present.length > 0) {
|
|
266
|
-
present.sort(
|
|
267
|
-
(a, b) => failureClassRank(b.file) - failureClassRank(a.file),
|
|
268
|
-
);
|
|
269
|
-
return present[0]!;
|
|
270
|
-
}
|
|
271
|
-
if (unreadable.length > 0) {
|
|
272
|
-
return unreadable[0]!;
|
|
273
|
-
}
|
|
274
|
-
return { status: "absent" };
|
|
275
|
-
}
|
package/src/run-ticket-number.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Typed ticketNumber reader for a retained run's durable pages.
|
|
3
|
-
*
|
|
4
|
-
* page
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Production board reads use state.jsonl rows only (admitted, then invocation).
|
|
4
|
+
* Pre-#1161 page files are readable only via readMigrationBoardTicketNumber.
|
|
5
|
+
* Migration derivation page last so worktree-derived placement stays readable
|
|
6
|
+
* without forging a board assertion. Missing page (ENOENT) → try next / undefined;
|
|
7
|
+
* damage and non-ENOENT IO failures propagate.
|
|
7
8
|
*/
|
|
9
|
+
import { readPageSync } from "./run-dossier.ts";
|
|
8
10
|
import { readFile } from "node:fs/promises";
|
|
9
11
|
import { join } from "node:path";
|
|
10
12
|
|
|
@@ -111,25 +113,53 @@ async function readJsonObject(
|
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
|
|
116
|
+
/** Pre-#1161 separate board pages — migration input only, not a runtime fact path. */
|
|
117
|
+
const LEGACY_BOARD_PAGE: Readonly<Record<"admitted" | "invocation", string>> = {
|
|
118
|
+
admitted: "admitted-request.json",
|
|
119
|
+
invocation: "invocation.json",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function readBoardPageTicketNumberFromRows(
|
|
123
|
+
runDirectory: string,
|
|
124
|
+
section: "admitted" | "invocation",
|
|
125
|
+
): number | undefined {
|
|
126
|
+
const fromRows = readPageSync(runDirectory, section);
|
|
127
|
+
return fromRows === undefined ? undefined : ticketFromRecord(fromRows);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function readBoardPageTicketNumberFromLegacy(
|
|
115
131
|
runDirectory: string,
|
|
116
|
-
|
|
132
|
+
section: "admitted" | "invocation",
|
|
117
133
|
): Promise<number | undefined> {
|
|
118
|
-
const record = await readJsonObject(join(runDirectory,
|
|
134
|
+
const record = await readJsonObject(join(runDirectory, LEGACY_BOARD_PAGE[section]));
|
|
119
135
|
if (record === undefined) return undefined;
|
|
120
136
|
return ticketFromRecord(record);
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
/**
|
|
124
|
-
* Board-only ticketNumber (admitted → invocation).
|
|
125
|
-
*
|
|
140
|
+
* Board-only ticketNumber from fact rows (admitted → invocation).
|
|
141
|
+
* Public / production identity paths use this sole projection — never legacy pages.
|
|
126
142
|
*/
|
|
127
143
|
export async function readBoardTicketNumber(
|
|
128
144
|
runDirectory: string,
|
|
129
145
|
): Promise<number | undefined> {
|
|
130
146
|
return (
|
|
131
|
-
(
|
|
132
|
-
(
|
|
147
|
+
readBoardPageTicketNumberFromRows(runDirectory, "admitted") ??
|
|
148
|
+
readBoardPageTicketNumberFromRows(runDirectory, "invocation")
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Migration-boundary board ticket: fact rows first, then pre-#1161 page files.
|
|
154
|
+
* Only #852 migrators and placement may call this; runtime identity must not.
|
|
155
|
+
*/
|
|
156
|
+
export async function readMigrationBoardTicketNumber(
|
|
157
|
+
runDirectory: string,
|
|
158
|
+
): Promise<number | undefined> {
|
|
159
|
+
return (
|
|
160
|
+
(await readBoardTicketNumber(runDirectory)) ??
|
|
161
|
+
(await readBoardPageTicketNumberFromLegacy(runDirectory, "admitted")) ??
|
|
162
|
+
(await readBoardPageTicketNumberFromLegacy(runDirectory, "invocation"))
|
|
133
163
|
);
|
|
134
164
|
}
|
|
135
165
|
|
package/src/session-identity.ts
CHANGED
|
@@ -1,42 +1,77 @@
|
|
|
1
|
-
import { mkdir, readFile } from "node:fs/promises";
|
|
2
1
|
import { dirname, join } from "node:path";
|
|
3
2
|
|
|
4
|
-
import { writeFileAtomically } from "./atomic-write.ts";
|
|
5
3
|
import type { DurablePrincipal, DurablePrincipalAuthority } from "./host-contracts.ts";
|
|
6
4
|
import {
|
|
7
5
|
DEFAULT_ROLE_TURN_HOST,
|
|
8
6
|
lookupHeadlessHostDescription,
|
|
9
7
|
lookupHostDescription,
|
|
10
8
|
} from "./host-descriptions.ts";
|
|
9
|
+
import { renderCurrentSync, RUN_STATE_FILE } from "./run-dossier.ts";
|
|
10
|
+
import { readSitianRecords, reportRunRecord } from "./sitian-facade.ts";
|
|
11
|
+
import { errorText, isRecord } from "./unknown-value.ts";
|
|
11
12
|
import type { SessionIdentityAuthority } from "./prepared-role-turn.ts";
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
+
/** Log record kind of one host-session binding (a host's native session id for this run). */
|
|
15
|
+
export const HOST_SESSION_ID_RECORD_KIND = "host-session-id" as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Durable session binding: one state row per bind, scoped by host name — a run
|
|
19
|
+
* that changes hosts on resume must never hand one host's native session id to
|
|
20
|
+
* another. The latest record for the host wins. The public call projects
|
|
21
|
+
* `current.json` at the bind so host.sessions appears before the host CLI
|
|
22
|
+
* starts; a refused projection does not undo the binding fact or become a
|
|
23
|
+
* host/session failure (facts stay in state.jsonl; settlement still renders).
|
|
24
|
+
*/
|
|
14
25
|
export function createSessionIdentityAuthority(
|
|
15
26
|
authority: DurablePrincipalAuthority,
|
|
16
|
-
|
|
27
|
+
hostName: string,
|
|
17
28
|
): SessionIdentityAuthority {
|
|
18
|
-
const
|
|
19
|
-
|
|
29
|
+
const runDirectoryOf = (principal: DurablePrincipal): string =>
|
|
30
|
+
dirname(authority.decode(principal).sessionDirectory);
|
|
20
31
|
return {
|
|
21
32
|
resolveSessionFile(principal) {
|
|
22
33
|
return authority.decode(principal).sessionFile;
|
|
23
34
|
},
|
|
24
35
|
async load(principal) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
const runDirectory = runDirectoryOf(principal);
|
|
37
|
+
const { records, diagnostics } = await readSitianRecords(join(runDirectory, RUN_STATE_FILE));
|
|
38
|
+
// Control path fails closed on damaged rows — same class as readPageSync.
|
|
39
|
+
// Rendering may keep reachable facts; dispatch must not reuse a prior binding
|
|
40
|
+
// after syntax damage (#1161 C3).
|
|
41
|
+
if (diagnostics.length > 0) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`${RUN_STATE_FILE} has ${diagnostics.length} malformed row(s); refusing stale session binding: ${runDirectory}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
let bound: string | undefined;
|
|
47
|
+
for (const record of records) {
|
|
48
|
+
if (record.kind !== HOST_SESSION_ID_RECORD_KIND) continue;
|
|
49
|
+
// Non-object / cannot name a host: unattributable damage — refuse
|
|
50
|
+
// (settled C3-payload). Never treat as a skippable "other host" row.
|
|
51
|
+
if (!isRecord(record.payload) || typeof record.payload.host !== "string") {
|
|
28
52
|
throw new Error("durable session binding is invalid");
|
|
29
53
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
// Explicit other host only: that host's bad id must not block this one.
|
|
55
|
+
if (record.payload.host !== hostName) continue;
|
|
56
|
+
// This host's null / non-string sessionId: refuse — do not reuse a prior
|
|
57
|
+
// binding for the same host (#1161 C3).
|
|
58
|
+
if (typeof record.payload.sessionId !== "string") {
|
|
59
|
+
throw new Error("durable session binding is invalid");
|
|
60
|
+
}
|
|
61
|
+
bound = record.payload.sessionId;
|
|
34
62
|
}
|
|
63
|
+
return bound;
|
|
35
64
|
},
|
|
36
65
|
async bind(principal, sessionId) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
const runDirectory = runDirectoryOf(principal);
|
|
67
|
+
reportRunRecord(runDirectory, HOST_SESSION_ID_RECORD_KIND, { host: hostName, sessionId }, "session-identity");
|
|
68
|
+
try {
|
|
69
|
+
renderCurrentSync(runDirectory);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
process.stderr.write(
|
|
72
|
+
`[session-identity] current.json render refused after host-session-id; binding fact kept: ${runDirectory}: ${errorText(error)}\n`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
40
75
|
},
|
|
41
76
|
};
|
|
42
77
|
}
|
|
@@ -54,10 +89,7 @@ export async function readStoredHostSessionId(
|
|
|
54
89
|
if (host === undefined || host === DEFAULT_ROLE_TURN_HOST) return undefined;
|
|
55
90
|
const description = lookupHostDescription(host) ?? lookupHeadlessHostDescription(host);
|
|
56
91
|
if (description === undefined) return undefined;
|
|
57
|
-
const sessionId = await createSessionIdentityAuthority(
|
|
58
|
-
authority,
|
|
59
|
-
description.sessionBindingFile,
|
|
60
|
-
).load(principal);
|
|
92
|
+
const sessionId = await createSessionIdentityAuthority(authority, host).load(principal);
|
|
61
93
|
if (sessionId === undefined || sessionId === "") return undefined;
|
|
62
94
|
return sessionId;
|
|
63
95
|
}
|
package/src/sitian-appender.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { appendFileSync } from "node:fs";
|
|
8
|
-
import { dirname, join } from "node:path";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
9
9
|
|
|
10
|
-
import { sessionDirectoryOf } from "./role-run-placement.ts";
|
|
10
|
+
import { parseRunLeaf, sessionDirectoryOf } from "./role-run-placement.ts";
|
|
11
|
+
import { RUN_HISTORY_FILE, RUN_HISTORY_KINDS, RUN_LOG_FILE, RUN_STATE_FILE, RUN_STATE_KINDS } from "./run-dossier-files.ts";
|
|
11
12
|
import { ticketNumberFromSitianSubject } from "./run-ticket-number.ts";
|
|
12
13
|
|
|
13
14
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
|
|
@@ -42,20 +43,8 @@ export function appendSitianRecordBlock(input: SitianRecordInput, block: string)
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
/**
|
|
46
|
-
export const S4_SUBMISSION_LEDGER_KINDS = new Set([
|
|
47
|
-
"candidate",
|
|
48
|
-
"roundContext",
|
|
49
|
-
"outcome",
|
|
50
|
-
"sealed",
|
|
51
|
-
"post-seal-anomaly"
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
/** Compute the volume partition key for directory placement. */
|
|
46
|
+
/** Volume directory name for a record kind: the kind itself. */
|
|
55
47
|
export function resolveSitianVolumeCategory(kind: string): string {
|
|
56
|
-
if (S4_SUBMISSION_LEDGER_KINDS.has(kind)) {
|
|
57
|
-
return "submission-ledger";
|
|
58
|
-
}
|
|
59
48
|
return kind;
|
|
60
49
|
}
|
|
61
50
|
|
|
@@ -115,8 +104,10 @@ export function resolveSitianRecordPathInLedger(
|
|
|
115
104
|
sessionDir = paths.sessionDir;
|
|
116
105
|
recordFile = paths.recordFile;
|
|
117
106
|
} else if (category === "ticket-provenance" && input.runDirectory !== undefined) {
|
|
107
|
+
// True-unbound diary staging: the run's history volume (#1161). Never a
|
|
108
|
+
// top-level run/records.jsonl — that leaf is only the ticket-root diary.
|
|
118
109
|
sessionDir = input.runDirectory;
|
|
119
|
-
recordFile =
|
|
110
|
+
recordFile = join(sessionDir, RUN_HISTORY_FILE);
|
|
120
111
|
} else {
|
|
121
112
|
if (
|
|
122
113
|
input.sessionParent === undefined
|
|
@@ -125,8 +116,16 @@ export function resolveSitianRecordPathInLedger(
|
|
|
125
116
|
) {
|
|
126
117
|
throw new Error("Sitian record ownership requires a parent session inside the ledger home");
|
|
127
118
|
}
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
const sessionParentDir = dirname(input.sessionParent);
|
|
120
|
+
const runDirectory = dirname(sessionParentDir);
|
|
121
|
+
if (basename(sessionParentDir) === "session" && parseRunLeaf(basename(runDirectory)) !== undefined) {
|
|
122
|
+
// A role run's own session: every record kind shares the run's one log (#1161).
|
|
123
|
+
sessionDir = runDirectory;
|
|
124
|
+
recordFile = join(runDirectory, RUN_HISTORY_KINDS.has(input.kind) ? RUN_HISTORY_FILE : RUN_STATE_KINDS.has(input.kind) ? RUN_STATE_FILE : RUN_LOG_FILE);
|
|
125
|
+
} else {
|
|
126
|
+
sessionDir = sitianVolumeDirectory(sessionParentDir, category);
|
|
127
|
+
recordFile = sitianVolumeRecordsFile(sessionDir);
|
|
128
|
+
}
|
|
130
129
|
}
|
|
131
130
|
if (!physicallyContainedIn(ledgerHome, sessionDir)) {
|
|
132
131
|
throw new Error("Sitian record ownership requires a directory inside the ledger home");
|
package/src/sitian-facade.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - sitianReport → appender (SitianRecord rows, append + identity claim)
|
|
7
7
|
* Read paths: readSitianRecords (canonical rows).
|
|
8
8
|
*/
|
|
9
|
+
import { sessionFileOf } from "./role-run-placement.ts";
|
|
9
10
|
import { appendSitianRecord } from "./sitian-appender.ts";
|
|
10
11
|
import type { RecordPointer, SitianRecordInput } from "./sitian-contracts.ts";
|
|
11
12
|
|
|
@@ -20,3 +21,24 @@ export * from "./sitian-reader.ts";
|
|
|
20
21
|
export function sitianReport(input: SitianRecordInput): RecordPointer {
|
|
21
22
|
return appendSitianRecord(input);
|
|
22
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One record of a role run: history.jsonl for the history kinds and state.jsonl for the
|
|
27
|
+
* state kinds (see the appender), log.jsonl for the run's own diagnostics (host dossier pointers,
|
|
28
|
+
* dispatch / resume / post-admission faults, host stderr). Same single entry
|
|
29
|
+
* as every other record; the run is named, never a file path.
|
|
30
|
+
*/
|
|
31
|
+
export function reportRunRecord(
|
|
32
|
+
runDirectory: string,
|
|
33
|
+
kind: string,
|
|
34
|
+
payload: unknown,
|
|
35
|
+
source: string,
|
|
36
|
+
): RecordPointer {
|
|
37
|
+
return appendSitianRecord({
|
|
38
|
+
level: "event",
|
|
39
|
+
kind,
|
|
40
|
+
sessionParent: sessionFileOf(runDirectory),
|
|
41
|
+
source,
|
|
42
|
+
payload,
|
|
43
|
+
});
|
|
44
|
+
}
|