@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,226 +1,97 @@
|
|
|
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 {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { isRecord, errorText, isMissingPathError } from "./unknown-value.js";
|
|
14
|
-
export const RUN_TERMINAL_REPORT_FILE = "report.json";
|
|
15
|
-
export const RUN_TERMINAL_ERROR_FILE = "error.json";
|
|
16
|
-
export const RUN_TERMINAL_EVIDENCE_FILE = "evidence.json";
|
|
17
|
-
export const RUN_TERMINAL_ERROR_SETTLEMENT_FILE = "error.settlement.json";
|
|
18
|
-
export const RUN_TERMINAL_ARTIFACT_FILES = [
|
|
19
|
-
RUN_TERMINAL_REPORT_FILE,
|
|
20
|
-
RUN_TERMINAL_ERROR_FILE,
|
|
21
|
-
"audit-incomplete.json",
|
|
22
|
-
];
|
|
9
|
+
import { basename } from "node:path";
|
|
10
|
+
import { parseRunLeaf } from "./role-run-placement.js";
|
|
11
|
+
import { readHistoryRowsSync, readSectionSync, writeSectionSync } from "./run-dossier.js";
|
|
12
|
+
import { isRecord } from "./unknown-value.js";
|
|
23
13
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* Relative to the run directory.
|
|
14
|
+
* Terminal body field naming the `attempt-history` row this settlement appended.
|
|
15
|
+
* Readers resolve submitted payloads by this identity (#1161 甲); they must not
|
|
16
|
+
* pick the last history row of the same role.
|
|
28
17
|
*/
|
|
29
|
-
export const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
export const ATTEMPT_HISTORY_IDENTITY_FIELD = "attemptHistoryIdentity";
|
|
19
|
+
/**
|
|
20
|
+
* Terminal body field naming the court that wrote this terminal (#1161 R2).
|
|
21
|
+
* Historical `state.jsonl` terminal rows keep it so a later re-projection of the
|
|
22
|
+
* same court can recover that court's `attemptHistoryIdentity` without reading
|
|
23
|
+
* the leg's latest terminal or guessing by payload.
|
|
24
|
+
*/
|
|
25
|
+
export const TERMINAL_COURT_ATTEMPT_FIELD = "courtAttemptId";
|
|
26
|
+
/** Replace the leg's terminal. */
|
|
27
|
+
export function writeRunTerminal(runDirectory, face, body) {
|
|
28
|
+
writeSectionSync(runDirectory, "terminal", { face, at: new Date().toISOString(), body });
|
|
37
29
|
}
|
|
38
|
-
const UNIQUE_ERROR_FALLBACK_NAME = new RegExp(`^${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");
|
|
39
30
|
/**
|
|
40
|
-
* Minimum producer-owned face
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* objects are unreadable (ADR 0043).
|
|
31
|
+
* Read the leg's terminal. Minimum producer-owned face: a typed object body
|
|
32
|
+
* with a nonblank `role` (ADR 0043); anything else is unreadable. Absence is a
|
|
33
|
+
* valid no-receipt state. A damaged current.json throws.
|
|
44
34
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
export function readRunTerminal(runDirectory) {
|
|
36
|
+
const terminal = readSectionSync(runDirectory, "terminal");
|
|
37
|
+
if (terminal === undefined)
|
|
38
|
+
return { status: "absent" };
|
|
39
|
+
const { face, body } = terminal;
|
|
40
|
+
if (face !== "report" && face !== "error" && face !== "no_receipt") {
|
|
41
|
+
return { status: "unreadable", reason: "terminal section has no known face" };
|
|
48
42
|
}
|
|
49
43
|
if (!isRecord(body)) {
|
|
50
|
-
return {
|
|
51
|
-
ok: false,
|
|
52
|
-
reason: `terminal artifact JSON value is not a typed object (${Array.isArray(body) ? "array" : typeof body})`,
|
|
53
|
-
};
|
|
44
|
+
return { status: "unreadable", reason: "terminal body is not a typed object" };
|
|
54
45
|
}
|
|
55
46
|
if (typeof body.role !== "string" || body.role.trim() === "") {
|
|
56
|
-
return {
|
|
57
|
-
ok: false,
|
|
58
|
-
reason: "terminal artifact missing nonblank producer-owned role field",
|
|
59
|
-
};
|
|
47
|
+
return { status: "unreadable", reason: "terminal body missing nonblank producer-owned role field" };
|
|
60
48
|
}
|
|
61
|
-
|
|
49
|
+
if (face !== "report")
|
|
50
|
+
return { status: "present", face, body };
|
|
51
|
+
const resolved = withSubmittedPayloads(runDirectory, body);
|
|
52
|
+
if (resolved.status === "unreadable")
|
|
53
|
+
return resolved;
|
|
54
|
+
return { status: "present", face, body: resolved.body };
|
|
62
55
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
path,
|
|
75
|
-
reason: errorText(error),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
let parsed;
|
|
79
|
-
try {
|
|
80
|
-
parsed = JSON.parse(raw);
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
return {
|
|
84
|
-
status: "unreadable",
|
|
85
|
-
file,
|
|
86
|
-
path,
|
|
87
|
-
reason: error instanceof Error
|
|
88
|
-
? error.message
|
|
89
|
-
: `terminal artifact JSON parse failed: ${String(error)}`,
|
|
90
|
-
};
|
|
56
|
+
/**
|
|
57
|
+
* The report's outcome carries the verdict facts only; the role's submitted payloads are not
|
|
58
|
+
* stored a second time. A reader that wants them takes them from the attempt-history row this
|
|
59
|
+
* settlement named on the terminal body (`attemptHistoryIdentity`). When payloads are absent
|
|
60
|
+
* and that identity is missing or cannot supply them, the gap is unreadable — never a silent
|
|
61
|
+
* present body (#1161 R2-gap).
|
|
62
|
+
*/
|
|
63
|
+
function withSubmittedPayloads(runDirectory, body) {
|
|
64
|
+
const outcome = body.outcome;
|
|
65
|
+
if (!isRecord(outcome) || outcome.payloads !== undefined) {
|
|
66
|
+
return { status: "present", body };
|
|
91
67
|
}
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
68
|
+
const identity = body[ATTEMPT_HISTORY_IDENTITY_FIELD];
|
|
69
|
+
if (typeof identity !== "string" || identity.trim() === "") {
|
|
94
70
|
return {
|
|
95
71
|
status: "unreadable",
|
|
96
|
-
|
|
97
|
-
path,
|
|
98
|
-
reason: usable.reason,
|
|
72
|
+
reason: "terminal report has no payloads and no attemptHistoryIdentity",
|
|
99
73
|
};
|
|
100
74
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
catch (error) {
|
|
111
|
-
if (isMissingPathError(error))
|
|
112
|
-
continue;
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
for (const name of names.sort((a, b) => a.localeCompare(b))) {
|
|
116
|
-
if (!UNIQUE_ERROR_FALLBACK_NAME.test(name))
|
|
117
|
-
continue;
|
|
118
|
-
found.push(join(dir, name));
|
|
75
|
+
for (const row of readHistoryRowsSync(runDirectory)) {
|
|
76
|
+
if (row.kind !== "attempt-history" || row.identity !== identity || !isRecord(row.payload))
|
|
77
|
+
continue;
|
|
78
|
+
const attempt = row.payload.outcome;
|
|
79
|
+
if (isRecord(attempt) && Array.isArray(attempt.payloads)) {
|
|
80
|
+
return {
|
|
81
|
+
status: "present",
|
|
82
|
+
body: { ...body, outcome: { ...outcome, payloads: attempt.payloads } },
|
|
83
|
+
};
|
|
119
84
|
}
|
|
120
85
|
}
|
|
121
|
-
return
|
|
86
|
+
return {
|
|
87
|
+
status: "unreadable",
|
|
88
|
+
reason: `terminal attemptHistoryIdentity ${identity} has no readable attempt-history payloads`,
|
|
89
|
+
};
|
|
122
90
|
}
|
|
123
91
|
/**
|
|
124
|
-
*
|
|
125
|
-
* fallbacks are shared across sibling runs, so binding uses this runId only.
|
|
92
|
+
* Run-directory face is `<runId>@<role>`.
|
|
126
93
|
* Sole authority for runDirectory → runId (last `@` split).
|
|
127
94
|
*/
|
|
128
95
|
export function runIdFromRunDirectory(runDirectory) {
|
|
129
96
|
return parseRunLeaf(basename(runDirectory))?.runId;
|
|
130
97
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Shared parent-directory unique fallback may be adopted only when the
|
|
133
|
-
* publisher-owned body.runId equals this run directory's runId. Same-run
|
|
134
|
-
* artifactsDir / runDirectory candidates keep path ownership and skip this.
|
|
135
|
-
* expectedRunId undefined (unparseable run dir) → never bound.
|
|
136
|
-
*/
|
|
137
|
-
function presentUniqueFallbackBoundToRun(body, expectedRunId) {
|
|
138
|
-
if (expectedRunId === undefined)
|
|
139
|
-
return false;
|
|
140
|
-
return typeof body.runId === "string" && body.runId === expectedRunId;
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Sole authority for seam-owned unique error.<uuid>.json candidates.
|
|
144
|
-
* Used by both clearOpposite (settlement) and readRunTerminalArtifact — do not
|
|
145
|
-
* re-enumerate the same-run / parent unique set elsewhere.
|
|
146
|
-
* Ownership:
|
|
147
|
-
* - same-run dirs (artifacts/, runDir): path ownership — all unique names
|
|
148
|
-
* - parent runs/: only body.runId-bound faces; unparseable runId or unreadable body → none
|
|
149
|
-
*/
|
|
150
|
-
export async function listSeamOwnedUniqueErrorFacePaths(runDirectory) {
|
|
151
|
-
const artifactsDir = roleRunArtifactsDirectory(runDirectory);
|
|
152
|
-
const owned = await listUniqueErrorFallbackPaths([
|
|
153
|
-
artifactsDir,
|
|
154
|
-
runDirectory,
|
|
155
|
-
]);
|
|
156
|
-
const expectedRunId = runIdFromRunDirectory(runDirectory);
|
|
157
|
-
for (const path of await listUniqueErrorFallbackPaths([dirname(runDirectory)])) {
|
|
158
|
-
const read = await readTerminalArtifactAtPath(path, "error.json");
|
|
159
|
-
if (read === undefined || read.status !== "present")
|
|
160
|
-
continue;
|
|
161
|
-
if (!presentUniqueFallbackBoundToRun(read.body, expectedRunId))
|
|
162
|
-
continue;
|
|
163
|
-
owned.push(path);
|
|
164
|
-
}
|
|
165
|
-
return owned;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Publish contract (#953): only failure publish continues after clearOpposite
|
|
169
|
-
* failure, so a multi-class residue (residual report/audit beside a new error
|
|
170
|
-
* face) means the current settlement is failure. Prefer failure-class faces
|
|
171
|
-
* over success/audit — never filesystem mtime (copy/restore/utimes can lie).
|
|
172
|
-
*/
|
|
173
|
-
function failureClassRank(file) {
|
|
174
|
-
return file === "error.json" ? 1 : 0;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Read the current typed terminal artifact for a run directory.
|
|
178
|
-
*
|
|
179
|
-
* Candidate set (publisher-owned faces only):
|
|
180
|
-
* 1) conventional artifacts/{report,error,audit-incomplete}.json
|
|
181
|
-
* 2) publisher fixed failure fallbacks (error.settlement.json faces)
|
|
182
|
-
* 3) seam-owned unique error.<uuid>.json via listSeamOwnedUniqueErrorFacePaths
|
|
183
|
-
* (same-run path ownership + parent body.runId binding — shared with clear)
|
|
184
|
-
*
|
|
185
|
-
* Invariant: when more than one present face remains (e.g. clearOpposite failed
|
|
186
|
-
* during failure publish and a fallback was settled beside a residual report),
|
|
187
|
-
* adopt failure-class over success/audit by the publish contract — not mtime.
|
|
188
|
-
* Same-class ties keep candidate enumeration order (conventional before
|
|
189
|
-
* fallbacks before unique).
|
|
190
|
-
*
|
|
191
|
-
* Unreadable faces never outrank a present face. Parent unique unreadable
|
|
192
|
-
* files never enter the shared enumerator (cannot prove run identity).
|
|
193
|
-
* Absence of every known durable face is a valid no-receipt state.
|
|
194
|
-
*/
|
|
195
|
-
export async function readRunTerminalArtifact(runDirectory) {
|
|
196
|
-
const artifactsDir = roleRunArtifactsDirectory(runDirectory);
|
|
197
|
-
const present = [];
|
|
198
|
-
const unreadable = [];
|
|
199
|
-
const consider = (read) => {
|
|
200
|
-
if (read === undefined || read.status === "absent")
|
|
201
|
-
return;
|
|
202
|
-
if (read.status === "present") {
|
|
203
|
-
present.push(read);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
unreadable.push(read);
|
|
207
|
-
};
|
|
208
|
-
for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
|
|
209
|
-
consider(await readTerminalArtifactAtPath(join(artifactsDir, file), file));
|
|
210
|
-
}
|
|
211
|
-
for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
|
|
212
|
-
consider(await readTerminalArtifactAtPath(join(runDirectory, relative), "error.json"));
|
|
213
|
-
}
|
|
214
|
-
// Unique same-run + parent-bound faces: one enumerator shared with clear.
|
|
215
|
-
for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
|
|
216
|
-
consider(await readTerminalArtifactAtPath(path, "error.json"));
|
|
217
|
-
}
|
|
218
|
-
if (present.length > 0) {
|
|
219
|
-
present.sort((a, b) => failureClassRank(b.file) - failureClassRank(a.file));
|
|
220
|
-
return present[0];
|
|
221
|
-
}
|
|
222
|
-
if (unreadable.length > 0) {
|
|
223
|
-
return unreadable[0];
|
|
224
|
-
}
|
|
225
|
-
return { status: "absent" };
|
|
226
|
-
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readPageSync } from "./run-dossier.js";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { isEnoent, isRecord } from "./unknown-value.js";
|
|
@@ -61,13 +62,24 @@ async function readJsonObject(path) {
|
|
|
61
62
|
throw error;
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const LEGACY_BOARD_PAGE = {
|
|
66
|
+
admitted: "admitted-request.json",
|
|
67
|
+
invocation: "invocation.json"
|
|
68
|
+
};
|
|
69
|
+
function readBoardPageTicketNumberFromRows(runDirectory, section) {
|
|
70
|
+
const fromRows = readPageSync(runDirectory, section);
|
|
71
|
+
return fromRows === void 0 ? void 0 : ticketFromRecord(fromRows);
|
|
72
|
+
}
|
|
73
|
+
async function readBoardPageTicketNumberFromLegacy(runDirectory, section) {
|
|
74
|
+
const record = await readJsonObject(join(runDirectory, LEGACY_BOARD_PAGE[section]));
|
|
66
75
|
if (record === void 0) return void 0;
|
|
67
76
|
return ticketFromRecord(record);
|
|
68
77
|
}
|
|
69
78
|
async function readBoardTicketNumber(runDirectory) {
|
|
70
|
-
return
|
|
79
|
+
return readBoardPageTicketNumberFromRows(runDirectory, "admitted") ?? readBoardPageTicketNumberFromRows(runDirectory, "invocation");
|
|
80
|
+
}
|
|
81
|
+
async function readMigrationBoardTicketNumber(runDirectory) {
|
|
82
|
+
return await readBoardTicketNumber(runDirectory) ?? await readBoardPageTicketNumberFromLegacy(runDirectory, "admitted") ?? await readBoardPageTicketNumberFromLegacy(runDirectory, "invocation");
|
|
71
83
|
}
|
|
72
84
|
async function readMigrationDerivedTicketNumber(runDirectory) {
|
|
73
85
|
const record = await readJsonObject(
|
|
@@ -88,6 +100,7 @@ export {
|
|
|
88
100
|
parseTicketNumber,
|
|
89
101
|
readBoardTicketNumber,
|
|
90
102
|
readDeclaredTicketNumber,
|
|
103
|
+
readMigrationBoardTicketNumber,
|
|
91
104
|
readMigrationDerivedTicketNumber,
|
|
92
105
|
readRunTicketNumber,
|
|
93
106
|
requireSafePositiveTicketNumber,
|
package/dist/session-identity.js
CHANGED
|
@@ -1,32 +1,63 @@
|
|
|
1
|
-
import { mkdir, readFile } from "node:fs/promises";
|
|
2
1
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { writeFileAtomically } from "./atomic-write.js";
|
|
4
2
|
import { DEFAULT_ROLE_TURN_HOST, lookupHeadlessHostDescription, lookupHostDescription, } from "./host-descriptions.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { renderCurrentSync, RUN_STATE_FILE } from "./run-dossier.js";
|
|
4
|
+
import { readSitianRecords, reportRunRecord } from "./sitian-facade.js";
|
|
5
|
+
import { errorText, isRecord } from "./unknown-value.js";
|
|
6
|
+
/** Log record kind of one host-session binding (a host's native session id for this run). */
|
|
7
|
+
export const HOST_SESSION_ID_RECORD_KIND = "host-session-id";
|
|
8
|
+
/**
|
|
9
|
+
* Durable session binding: one state row per bind, scoped by host name — a run
|
|
10
|
+
* that changes hosts on resume must never hand one host's native session id to
|
|
11
|
+
* another. The latest record for the host wins. The public call projects
|
|
12
|
+
* `current.json` at the bind so host.sessions appears before the host CLI
|
|
13
|
+
* starts; a refused projection does not undo the binding fact or become a
|
|
14
|
+
* host/session failure (facts stay in state.jsonl; settlement still renders).
|
|
15
|
+
*/
|
|
16
|
+
export function createSessionIdentityAuthority(authority, hostName) {
|
|
17
|
+
const runDirectoryOf = (principal) => dirname(authority.decode(principal).sessionDirectory);
|
|
8
18
|
return {
|
|
9
19
|
resolveSessionFile(principal) {
|
|
10
20
|
return authority.decode(principal).sessionFile;
|
|
11
21
|
},
|
|
12
22
|
async load(principal) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
const runDirectory = runDirectoryOf(principal);
|
|
24
|
+
const { records, diagnostics } = await readSitianRecords(join(runDirectory, RUN_STATE_FILE));
|
|
25
|
+
// Control path fails closed on damaged rows — same class as readPageSync.
|
|
26
|
+
// Rendering may keep reachable facts; dispatch must not reuse a prior binding
|
|
27
|
+
// after syntax damage (#1161 C3).
|
|
28
|
+
if (diagnostics.length > 0) {
|
|
29
|
+
throw new Error(`${RUN_STATE_FILE} has ${diagnostics.length} malformed row(s); refusing stale session binding: ${runDirectory}`);
|
|
30
|
+
}
|
|
31
|
+
let bound;
|
|
32
|
+
for (const record of records) {
|
|
33
|
+
if (record.kind !== HOST_SESSION_ID_RECORD_KIND)
|
|
34
|
+
continue;
|
|
35
|
+
// Non-object / cannot name a host: unattributable damage — refuse
|
|
36
|
+
// (settled C3-payload). Never treat as a skippable "other host" row.
|
|
37
|
+
if (!isRecord(record.payload) || typeof record.payload.host !== "string") {
|
|
16
38
|
throw new Error("durable session binding is invalid");
|
|
17
39
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
// Explicit other host only: that host's bad id must not block this one.
|
|
41
|
+
if (record.payload.host !== hostName)
|
|
42
|
+
continue;
|
|
43
|
+
// This host's null / non-string sessionId: refuse — do not reuse a prior
|
|
44
|
+
// binding for the same host (#1161 C3).
|
|
45
|
+
if (typeof record.payload.sessionId !== "string") {
|
|
46
|
+
throw new Error("durable session binding is invalid");
|
|
47
|
+
}
|
|
48
|
+
bound = record.payload.sessionId;
|
|
24
49
|
}
|
|
50
|
+
return bound;
|
|
25
51
|
},
|
|
26
52
|
async bind(principal, sessionId) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
53
|
+
const runDirectory = runDirectoryOf(principal);
|
|
54
|
+
reportRunRecord(runDirectory, HOST_SESSION_ID_RECORD_KIND, { host: hostName, sessionId }, "session-identity");
|
|
55
|
+
try {
|
|
56
|
+
renderCurrentSync(runDirectory);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
process.stderr.write(`[session-identity] current.json render refused after host-session-id; binding fact kept: ${runDirectory}: ${errorText(error)}\n`);
|
|
60
|
+
}
|
|
30
61
|
},
|
|
31
62
|
};
|
|
32
63
|
}
|
|
@@ -41,7 +72,7 @@ export async function readStoredHostSessionId(host, authority, principal) {
|
|
|
41
72
|
const description = lookupHostDescription(host) ?? lookupHeadlessHostDescription(host);
|
|
42
73
|
if (description === undefined)
|
|
43
74
|
return undefined;
|
|
44
|
-
const sessionId = await createSessionIdentityAuthority(authority,
|
|
75
|
+
const sessionId = await createSessionIdentityAuthority(authority, host).load(principal);
|
|
45
76
|
if (sessionId === undefined || sessionId === "")
|
|
46
77
|
return undefined;
|
|
47
78
|
return sessionId;
|
package/dist/sitian-appender.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { appendFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { sessionDirectoryOf } from "./role-run-placement.js";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { parseRunLeaf, sessionDirectoryOf } from "./role-run-placement.js";
|
|
5
|
+
import { RUN_HISTORY_FILE, RUN_HISTORY_KINDS, RUN_LOG_FILE, RUN_STATE_FILE, RUN_STATE_KINDS } from "./run-dossier-files.js";
|
|
5
6
|
import { ticketNumberFromSitianSubject } from "./run-ticket-number.js";
|
|
6
7
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
|
|
7
8
|
import {
|
|
@@ -29,17 +30,7 @@ function appendSitianRecordBlock(input, block) {
|
|
|
29
30
|
);
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
const S4_SUBMISSION_LEDGER_KINDS = /* @__PURE__ */ new Set([
|
|
33
|
-
"candidate",
|
|
34
|
-
"roundContext",
|
|
35
|
-
"outcome",
|
|
36
|
-
"sealed",
|
|
37
|
-
"post-seal-anomaly"
|
|
38
|
-
]);
|
|
39
33
|
function resolveSitianVolumeCategory(kind) {
|
|
40
|
-
if (S4_SUBMISSION_LEDGER_KINDS.has(kind)) {
|
|
41
|
-
return "submission-ledger";
|
|
42
|
-
}
|
|
43
34
|
return kind;
|
|
44
35
|
}
|
|
45
36
|
const SITIAN_RECORDS_LEAF = "records.jsonl";
|
|
@@ -71,13 +62,20 @@ function resolveSitianRecordPathInLedger(input, ledgerHome) {
|
|
|
71
62
|
recordFile = paths.recordFile;
|
|
72
63
|
} else if (category === "ticket-provenance" && input.runDirectory !== void 0) {
|
|
73
64
|
sessionDir = input.runDirectory;
|
|
74
|
-
recordFile =
|
|
65
|
+
recordFile = join(sessionDir, RUN_HISTORY_FILE);
|
|
75
66
|
} else {
|
|
76
67
|
if (input.sessionParent === void 0 || input.sessionParent.length === 0 || !physicallyContainedIn(ledgerHome, input.sessionParent)) {
|
|
77
68
|
throw new Error("Sitian record ownership requires a parent session inside the ledger home");
|
|
78
69
|
}
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
const sessionParentDir = dirname(input.sessionParent);
|
|
71
|
+
const runDirectory = dirname(sessionParentDir);
|
|
72
|
+
if (basename(sessionParentDir) === "session" && parseRunLeaf(basename(runDirectory)) !== void 0) {
|
|
73
|
+
sessionDir = runDirectory;
|
|
74
|
+
recordFile = join(runDirectory, RUN_HISTORY_KINDS.has(input.kind) ? RUN_HISTORY_FILE : RUN_STATE_KINDS.has(input.kind) ? RUN_STATE_FILE : RUN_LOG_FILE);
|
|
75
|
+
} else {
|
|
76
|
+
sessionDir = sitianVolumeDirectory(sessionParentDir, category);
|
|
77
|
+
recordFile = sitianVolumeRecordsFile(sessionDir);
|
|
78
|
+
}
|
|
81
79
|
}
|
|
82
80
|
if (!physicallyContainedIn(ledgerHome, sessionDir)) {
|
|
83
81
|
throw new Error("Sitian record ownership requires a directory inside the ledger home");
|
|
@@ -126,7 +124,6 @@ function appendSitianRecord(input) {
|
|
|
126
124
|
}
|
|
127
125
|
}
|
|
128
126
|
export {
|
|
129
|
-
S4_SUBMISSION_LEDGER_KINDS,
|
|
130
127
|
appendSitianRecord,
|
|
131
128
|
appendSitianRecordBlock,
|
|
132
129
|
resolveSitianRecordPath,
|
package/dist/sitian-facade.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionFileOf } from "./role-run-placement.js";
|
|
1
2
|
import { appendSitianRecord } from "./sitian-appender.js";
|
|
2
3
|
export * from "./sitian-contracts.js";
|
|
3
4
|
export * from "./sitian-appender.js";
|
|
@@ -5,6 +6,16 @@ export * from "./sitian-reader.js";
|
|
|
5
6
|
function sitianReport(input) {
|
|
6
7
|
return appendSitianRecord(input);
|
|
7
8
|
}
|
|
9
|
+
function reportRunRecord(runDirectory, kind, payload, source) {
|
|
10
|
+
return appendSitianRecord({
|
|
11
|
+
level: "event",
|
|
12
|
+
kind,
|
|
13
|
+
sessionParent: sessionFileOf(runDirectory),
|
|
14
|
+
source,
|
|
15
|
+
payload
|
|
16
|
+
});
|
|
17
|
+
}
|
|
8
18
|
export {
|
|
19
|
+
reportRunRecord,
|
|
9
20
|
sitianReport
|
|
10
21
|
};
|
package/dist/sitian-reader.js
CHANGED
|
@@ -1,41 +1,56 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { isRecord, errorText } from "./unknown-value.js";
|
|
4
|
-
|
|
5
|
-
if (!
|
|
6
|
-
|
|
2
|
+
import { isRecord, errorText, isEnoent } from "./unknown-value.js";
|
|
3
|
+
function decodeSitianRecordLine(line, lineNumber) {
|
|
4
|
+
if (!line.trim()) return void 0;
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(line);
|
|
7
|
+
if (isRecord(parsed)) {
|
|
8
|
+
return { ok: true, record: parsed };
|
|
9
|
+
}
|
|
10
|
+
const typeDesc = parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed;
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
diagnostic: {
|
|
14
|
+
kind: "malformed",
|
|
15
|
+
line: lineNumber,
|
|
16
|
+
raw: line,
|
|
17
|
+
error: `expected JSON object, got ${typeDesc}`
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
diagnostic: {
|
|
24
|
+
kind: "malformed",
|
|
25
|
+
line: lineNumber,
|
|
26
|
+
raw: line,
|
|
27
|
+
error: errorText(error)
|
|
28
|
+
}
|
|
29
|
+
};
|
|
7
30
|
}
|
|
8
|
-
|
|
31
|
+
}
|
|
32
|
+
function parseSitianRecordText(text) {
|
|
9
33
|
const lines = text.split("\n");
|
|
10
34
|
const records = [];
|
|
11
35
|
const diagnostics = [];
|
|
12
36
|
for (let index = 0; index < lines.length; index += 1) {
|
|
13
|
-
const
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (isRecord(parsed)) {
|
|
18
|
-
records.push(parsed);
|
|
19
|
-
} else {
|
|
20
|
-
const typeDesc = parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed;
|
|
21
|
-
diagnostics.push({
|
|
22
|
-
kind: "malformed",
|
|
23
|
-
line: index + 1,
|
|
24
|
-
raw: line,
|
|
25
|
-
error: `expected JSON object, got ${typeDesc}`
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
} catch (error) {
|
|
29
|
-
diagnostics.push({
|
|
30
|
-
kind: "malformed",
|
|
31
|
-
line: index + 1,
|
|
32
|
-
raw: line,
|
|
33
|
-
error: errorText(error)
|
|
34
|
-
});
|
|
35
|
-
}
|
|
37
|
+
const decoded = decodeSitianRecordLine(lines[index], index + 1);
|
|
38
|
+
if (decoded === void 0) continue;
|
|
39
|
+
if (decoded.ok) records.push(decoded.record);
|
|
40
|
+
else diagnostics.push(decoded.diagnostic);
|
|
36
41
|
}
|
|
37
42
|
return { records, diagnostics };
|
|
38
43
|
}
|
|
44
|
+
async function readSitianRecords(recordFile) {
|
|
45
|
+
try {
|
|
46
|
+
return parseSitianRecordText(await readFile(recordFile, "utf8"));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (isEnoent(error)) return { records: [], diagnostics: [] };
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
39
52
|
export {
|
|
53
|
+
decodeSitianRecordLine,
|
|
54
|
+
parseSitianRecordText,
|
|
40
55
|
readSitianRecords
|
|
41
56
|
};
|
package/dist/submission-gate.js
CHANGED
|
@@ -52,7 +52,7 @@ export function createDefaultGateOfficerSummon(options) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* Book a typed pointer
|
|
55
|
+
* Book a typed pointer in the parent run's current.json officers section.
|
|
56
56
|
* Offline mocks without a real session leave no nested volume (lawful zero).
|
|
57
57
|
*/
|
|
58
58
|
function bookDirectOfficerPointer(context, officer, result, summoned, toolCallId) {
|