@akagilnc/pi-workflow-roles 0.1.5483 → 0.1.5571
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 +6312 -6376
- package/dist/analyst-gate-cycles-read.js +28 -78
- package/dist/analyst-ledger.js +26 -42
- 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-entry.js +4 -3
- package/dist/archivist-record-pointer.js +71 -36
- package/dist/atomic-write.js +2 -2
- package/dist/auditor-dossier-tool.js +11 -6
- package/dist/auditor-soul.js +5 -12
- package/dist/collector-config.js +6 -5
- package/dist/doctor-evidence.js +8 -4
- package/dist/engine-detour-usage.js +8 -22
- package/dist/headless-host/production-host.js +5969 -6011
- package/dist/host-descriptions.js +0 -4
- package/dist/host-session-record.js +266 -267
- package/dist/ledger-session-read.js +5 -0
- package/dist/migrate-book-topology.js +1369 -722
- package/dist/navigator-public-session.js +13 -17
- package/dist/notary-source-run.js +8 -16
- package/dist/packaged-role-registry.js +5 -56
- package/dist/pi/durable-principal.js +2 -3
- package/dist/pi/role-turn-host.js +79 -15
- package/dist/public-cli/auto-resume.js +8 -47
- package/dist/public-cli/instruction-seat-run.js +9 -7
- package/dist/public-cli/invocation.js +67 -100
- package/dist/public-cli/main.js +2689 -2532
- package/dist/public-cli/post-admission.js +96 -55
- package/dist/public-cli/run-lifecycle.js +44 -100
- package/dist/public-cli/settlement.js +184 -429
- package/dist/public-cli/terminal.js +0 -12
- package/dist/role-run-placement.js +53 -16
- package/dist/role-run-relocation.js +111 -52
- package/dist/run-dossier-files.js +30 -0
- package/dist/run-dossier.js +322 -0
- package/dist/run-terminal-artifacts.js +69 -202
- package/dist/run-ticket-number.js +49 -7
- package/dist/session-assistant-usage.js +9 -18
- package/dist/session-identity.js +49 -19
- package/dist/sitian-appender.js +29 -18
- package/dist/sitian-facade.js +11 -0
- package/dist/sitian-reader.js +44 -29
- package/dist/submission-gate.js +1 -1
- package/dist/submission-ledger.js +4 -4
- package/dist/ticket-provenance-contracts.js +2 -8
- package/dist/ticket-provenance.js +25 -9
- 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 +11 -4
- package/src/analyst-gate-cycles-read.ts +31 -88
- package/src/analyst-ledger.ts +36 -51
- 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 +6 -5
- package/src/archivist-record-pointer.ts +92 -42
- package/src/atomic-write.ts +3 -1
- package/src/auditor-dossier-tool.ts +14 -8
- package/src/auditor-soul.ts +5 -10
- 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/collector-config.ts +5 -3
- package/src/doctor-contracts.ts +1 -1
- package/src/doctor-evidence.ts +7 -3
- package/src/engine-detour-tool.ts +1 -1
- package/src/engine-detour-usage.ts +12 -25
- package/src/external-host-turn-loop.ts +25 -0
- package/src/factory-board.ts +16 -81
- package/src/headless-host/production-host.ts +1 -4
- package/src/headless-host/role-turn-host.ts +56 -21
- package/src/host-descriptions.ts +0 -6
- package/src/host-session-record.ts +224 -140
- package/src/ledger-session-read.ts +6 -0
- package/src/navigator-public-session.ts +13 -18
- package/src/navigator-work-context.ts +3 -1
- package/src/notary-role.ts +2 -1
- package/src/notary-source-run.ts +8 -18
- package/src/packaged-role-registry.ts +1 -65
- package/src/pi/adapter.ts +50 -7
- package/src/pi/durable-principal.ts +2 -4
- package/src/pi/role-turn-host.ts +85 -14
- package/src/public-cli/auto-resume.ts +7 -43
- package/src/public-cli/instruction-seat-run.ts +13 -6
- package/src/public-cli/invocation.ts +90 -144
- package/src/public-cli/post-admission.ts +111 -72
- package/src/public-cli/run-lifecycle.ts +47 -115
- package/src/public-cli/settlement.ts +221 -589
- package/src/public-cli/terminal.ts +1 -61
- package/src/role-envelope.ts +4 -2
- package/src/role-run-placement.ts +65 -17
- package/src/role-run-relocation.ts +110 -70
- package/src/role-runtime.ts +46 -69
- package/src/run-dossier-files.ts +33 -0
- package/src/run-dossier.ts +357 -0
- package/src/run-terminal-artifacts.ts +84 -240
- package/src/run-ticket-number.ts +82 -14
- package/src/session-assistant-usage.ts +8 -20
- package/src/session-identity.ts +52 -21
- package/src/sitian-appender.ts +33 -26
- package/src/sitian-facade.ts +22 -0
- package/src/sitian-reader.ts +62 -32
- package/src/submission-gate.ts +1 -1
- package/src/submission-ledger.ts +4 -4
- package/src/ticket-provenance-contracts.ts +3 -6
- package/src/ticket-provenance.ts +23 -11
- package/src/ticket-snapshot.ts +3 -2
- package/src/ticket-trajectory.ts +95 -109
|
@@ -5,16 +5,14 @@
|
|
|
5
5
|
* Writes native-session-copy (or native-session-warning on retry failure) after CLI exit.
|
|
6
6
|
* Log line write failures declare to stderr and never abort the leg.
|
|
7
7
|
*/
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
8
9
|
import { spawnSync } from "node:child_process";
|
|
9
10
|
import {
|
|
10
|
-
appendFileSync,
|
|
11
11
|
copyFileSync,
|
|
12
|
-
closeSync,
|
|
13
12
|
existsSync,
|
|
14
|
-
openSync,
|
|
15
|
-
readFileSync,
|
|
16
13
|
mkdirSync,
|
|
17
14
|
readdirSync,
|
|
15
|
+
renameSync,
|
|
18
16
|
rmSync,
|
|
19
17
|
statSync,
|
|
20
18
|
type Dirent,
|
|
@@ -22,11 +20,10 @@ import {
|
|
|
22
20
|
import { homedir } from "node:os";
|
|
23
21
|
import { dirname, join } from "node:path";
|
|
24
22
|
|
|
25
|
-
import type { RoleTurnContinuation } from "./host-contracts.ts";
|
|
26
23
|
import { sitianReport } from "./sitian-facade.ts";
|
|
27
24
|
import type { SitianRecordInput } from "./sitian-contracts.ts";
|
|
28
25
|
|
|
29
|
-
import { errorText } from "./unknown-value.ts";
|
|
26
|
+
import { errorText, isEnoent } from "./unknown-value.ts";
|
|
30
27
|
|
|
31
28
|
/** Volume category under `<run>/session/<kind>/records.jsonl`. */
|
|
32
29
|
export const HOST_SESSION_RECORD_KIND = "host-session" as const;
|
|
@@ -47,31 +44,41 @@ export function sitianReportSafe(input: SitianRecordInput): void {
|
|
|
47
44
|
|
|
48
45
|
/** Recursively scan for Codex rollout file matching sessionId under sessionsDir. */
|
|
49
46
|
function findCodexRollout(sessionsDir: string, sessionId: string): string | undefined {
|
|
50
|
-
|
|
47
|
+
// Direct readdir: only ENOENT is absence. EACCES/EPERM keep their cause (#1161 C3-io).
|
|
51
48
|
const matches: Array<{ path: string; mtime: number }> = [];
|
|
52
49
|
function scan(dir: string, depth = 0): void {
|
|
53
50
|
if (depth > 6) return;
|
|
54
51
|
let entries: Dirent[];
|
|
55
52
|
try {
|
|
56
53
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
57
|
-
} catch {
|
|
58
|
-
return;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (isEnoent(error)) return;
|
|
56
|
+
throw error;
|
|
59
57
|
}
|
|
60
58
|
for (const entry of entries) {
|
|
61
59
|
const fullPath = join(dir, entry.name);
|
|
62
60
|
if (entry.isDirectory()) {
|
|
63
61
|
scan(fullPath, depth + 1);
|
|
64
62
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) {
|
|
63
|
+
// Ranking needs a real mtime. Silent mtime:0 would sort unreadable
|
|
64
|
+
// matches last and let an older readable sibling win (#1161 C3-io).
|
|
65
|
+
// ENOENT = vanished between readdir and stat (skip); other IO keeps cause.
|
|
65
66
|
try {
|
|
66
67
|
const stat = statSync(fullPath);
|
|
67
68
|
matches.push({ path: fullPath, mtime: stat.mtimeMs });
|
|
68
|
-
} catch {
|
|
69
|
-
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (isEnoent(error)) continue;
|
|
71
|
+
throw error;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
try {
|
|
77
|
+
scan(sessionsDir);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (isEnoent(error)) return undefined;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
75
82
|
if (matches.length === 0 || matches[0] === undefined) return undefined;
|
|
76
83
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
77
84
|
return matches[0]?.path;
|
|
@@ -118,55 +125,18 @@ function codexSessionsDirectory(home?: string): string {
|
|
|
118
125
|
return join(process.env.CODEX_HOME || join(home || homedir(), ".codex"), "sessions");
|
|
119
126
|
}
|
|
120
127
|
|
|
121
|
-
/** Count each host activation, including Pi turns without an external copy. */
|
|
122
|
-
export function recordRunStart(runDirectory: string): void {
|
|
123
|
-
try { appendFileSync(join(runDirectory, ".run-starts"), "\n"); }
|
|
124
|
-
catch (error) { declareHostSessionFailure(error); }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
128
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
129
|
+
* Host original landing under `<run>/session/`: one file per host, never
|
|
130
|
+
* numbered — every exit overwrites the same name (#1161).
|
|
131
|
+
* codex / claude: `<host>.jsonl`; grok-build: directory `<host>`.
|
|
131
132
|
*/
|
|
132
133
|
export function resolveHostDossierLandingPath(options: {
|
|
133
134
|
readonly host: string;
|
|
134
|
-
readonly model?: { readonly model?: string; readonly thinking?: string } | undefined;
|
|
135
135
|
readonly sessionDirectory: string;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (existsSync(options.sessionDirectory)) {
|
|
142
|
-
try {
|
|
143
|
-
const entries = readdirSync(options.sessionDirectory);
|
|
144
|
-
for (const entry of entries) {
|
|
145
|
-
const match = entry.match(/-(\d+)(?:\.jsonl|\.failed)?$/);
|
|
146
|
-
const first = match?.[1];
|
|
147
|
-
if (first !== undefined) {
|
|
148
|
-
const val = parseInt(first, 10);
|
|
149
|
-
if (!isNaN(val) && val > maxOrdinal) {
|
|
150
|
-
maxOrdinal = val;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
} catch {
|
|
155
|
-
// Ignore directory read errors
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const startsPath = join(dirname(options.sessionDirectory), ".run-starts");
|
|
160
|
-
let starts = 0;
|
|
161
|
-
if (existsSync(startsPath)) starts = readFileSync(startsPath, "utf8").length;
|
|
162
|
-
const ordinal = Math.max(maxOrdinal + 1, starts, options.continuation.kind === "resume" ? 2 : 1);
|
|
163
|
-
|
|
164
|
-
const baseName = `${options.host}-${sanitizedModel}-${ordinal}`;
|
|
165
|
-
const landingPath = options.host === "grok-build"
|
|
166
|
-
? join(options.sessionDirectory, baseName)
|
|
167
|
-
: join(options.sessionDirectory, `${baseName}.jsonl`);
|
|
168
|
-
|
|
169
|
-
return { landingPath, ordinal, sanitizedModel };
|
|
136
|
+
}): string {
|
|
137
|
+
return options.host === "grok-build"
|
|
138
|
+
? join(options.sessionDirectory, options.host)
|
|
139
|
+
: join(options.sessionDirectory, `${options.host}.jsonl`);
|
|
170
140
|
}
|
|
171
141
|
|
|
172
142
|
/** Record native session pointer line to Sitian as soon as session ID is known. */
|
|
@@ -212,29 +182,24 @@ function copyFileCloneOrFallback(src: string, dest: string): void {
|
|
|
212
182
|
copyFileSync(src, dest);
|
|
213
183
|
}
|
|
214
184
|
|
|
215
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Copy both grok-build originals: chat_history.jsonl and usage.json into destDir.
|
|
187
|
+
* Direct copy — no existsSync/access preflight. Real ENOENT/EACCES surface from
|
|
188
|
+
* the copy itself (Node fs: do not exists/access before open — #1161 C3-io).
|
|
189
|
+
*/
|
|
216
190
|
function copyGrokDossier(srcDir: string, destDir: string): void {
|
|
217
|
-
const chatHistorySrc = join(srcDir, "chat_history.jsonl");
|
|
218
|
-
if (!existsSync(chatHistorySrc)) {
|
|
219
|
-
throw new Error(`Grok native chat_history.jsonl missing at ${chatHistorySrc}`);
|
|
220
|
-
}
|
|
221
191
|
mkdirSync(destDir, { recursive: true });
|
|
222
|
-
|
|
223
|
-
copyFileCloneOrFallback(
|
|
224
|
-
|
|
225
|
-
const usageSrc = join(srcDir, "usage.json");
|
|
226
|
-
if (!existsSync(usageSrc)) {
|
|
227
|
-
throw new Error(`Grok native usage.json missing at ${usageSrc}`);
|
|
228
|
-
}
|
|
229
|
-
copyFileCloneOrFallback(usageSrc, join(destDir, "usage.json"));
|
|
192
|
+
copyFileCloneOrFallback(join(srcDir, "chat_history.jsonl"), join(destDir, "chat_history.jsonl"));
|
|
193
|
+
copyFileCloneOrFallback(join(srcDir, "usage.json"), join(destDir, "usage.json"));
|
|
230
194
|
}
|
|
231
195
|
|
|
232
196
|
/**
|
|
233
|
-
* Copy native host session original
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* Does not throw; turn outcome is never
|
|
197
|
+
* Copy the native host session original over the run's single landing path
|
|
198
|
+
* after the child process exits. Copies beside the landing path and swaps in
|
|
199
|
+
* on success, so a failed copy never destroys the previous good original.
|
|
200
|
+
* Retries once. Success appends a native-session-copy line, repeated failure a
|
|
201
|
+
* native-session-warning line. Does not throw; the turn outcome is never
|
|
202
|
+
* altered by copy success or failure.
|
|
238
203
|
*/
|
|
239
204
|
export function copyAndRecordHostDossier(options: {
|
|
240
205
|
readonly host: string;
|
|
@@ -242,102 +207,221 @@ export function copyAndRecordHostDossier(options: {
|
|
|
242
207
|
readonly cwd: string;
|
|
243
208
|
readonly sessionDirectory: string;
|
|
244
209
|
readonly sessionParent: string;
|
|
245
|
-
readonly continuation: RoleTurnContinuation;
|
|
246
|
-
readonly model?: { readonly model?: string; readonly thinking?: string } | undefined;
|
|
247
210
|
readonly home?: string | undefined;
|
|
248
211
|
}): void {
|
|
249
|
-
if (options.host === "
|
|
212
|
+
if (options.host === "hermes") {
|
|
250
213
|
return;
|
|
251
214
|
}
|
|
252
|
-
const
|
|
215
|
+
const report = (payload: Record<string, unknown>): void => sitianReportSafe({
|
|
216
|
+
level: "event",
|
|
217
|
+
kind: HOST_SESSION_RECORD_KIND,
|
|
253
218
|
host: options.host,
|
|
254
|
-
sessionId: options.sessionId,
|
|
255
219
|
cwd: options.cwd,
|
|
256
|
-
|
|
220
|
+
sessionParent: options.sessionParent,
|
|
221
|
+
source: `${options.host}-dossier`,
|
|
222
|
+
payload,
|
|
257
223
|
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
host: options.host,
|
|
268
|
-
cwd: options.cwd,
|
|
269
|
-
sessionParent: options.sessionParent,
|
|
270
|
-
source: `${options.host}-dossier`,
|
|
271
|
-
payload: {
|
|
272
|
-
type: "native-session-warning",
|
|
273
|
-
sessionId: options.sessionId,
|
|
274
|
-
error: errorText(error),
|
|
275
|
-
},
|
|
224
|
+
// Pi already lands at session/session.jsonl; record that landing through the
|
|
225
|
+
// same native-session-copy row current.json projects — no copy, no second
|
|
226
|
+
// original parser.
|
|
227
|
+
if (options.host === "pi") {
|
|
228
|
+
report({
|
|
229
|
+
type: "native-session-copy",
|
|
230
|
+
nativePath: options.sessionParent,
|
|
231
|
+
landingPath: options.sessionParent,
|
|
232
|
+
sessionId: options.sessionId,
|
|
276
233
|
});
|
|
277
234
|
return;
|
|
278
235
|
}
|
|
279
|
-
|
|
236
|
+
|
|
237
|
+
const landingPath = resolveHostDossierLandingPath(options);
|
|
238
|
+
// Per-attempt staging/previous names: concurrent copies of the same landing
|
|
239
|
+
// must not share one `.copying` / `.previous` path (#1161 C2). Node's fs
|
|
240
|
+
// copy is not atomic; unique sibling + rename is the smallest ownership fix.
|
|
241
|
+
// Swap-failure restores that attempt's own `.previous.<token>` inline — no
|
|
242
|
+
// next-call orphan scan (J6: do not invent a second recovery mechanism).
|
|
243
|
+
//
|
|
244
|
+
// Native-path discovery (incl. Codex rollout scan) stays inside this attempt
|
|
245
|
+
// try: ADR 0086 one-retry + native-session-warning covers discover/copy
|
|
246
|
+
// failure alike. A throw outside would abort the host leg (#1161 C3-io).
|
|
280
247
|
|
|
281
248
|
let lastError: unknown;
|
|
282
249
|
let copySuccess = false;
|
|
250
|
+
let stagingForCleanup: string | undefined;
|
|
251
|
+
let nativePath: string | undefined;
|
|
283
252
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
253
|
+
const attemptToken = randomBytes(6).toString("hex");
|
|
254
|
+
const staging = `${landingPath}.copying.${attemptToken}`;
|
|
255
|
+
stagingForCleanup = staging;
|
|
284
256
|
try {
|
|
257
|
+
nativePath = resolveNativeSessionPath({
|
|
258
|
+
host: options.host,
|
|
259
|
+
sessionId: options.sessionId,
|
|
260
|
+
cwd: options.cwd,
|
|
261
|
+
...(options.home !== undefined ? { home: options.home } : {}),
|
|
262
|
+
});
|
|
263
|
+
if (nativePath === undefined && options.host !== "codex") {
|
|
264
|
+
stagingForCleanup = undefined;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
285
267
|
if (options.host === "grok-build") {
|
|
286
|
-
copyGrokDossier(nativePath!,
|
|
268
|
+
copyGrokDossier(nativePath!, staging);
|
|
287
269
|
} else {
|
|
288
|
-
|
|
270
|
+
// Undefined path = discovery found nothing (true absence). Present path:
|
|
271
|
+
// copy directly so EACCES/EPERM keep their cause — no existsSync wash.
|
|
272
|
+
if (nativePath === undefined) {
|
|
289
273
|
throw new Error(`Native session file missing for ${options.host} session ${options.sessionId}`);
|
|
290
274
|
}
|
|
291
275
|
mkdirSync(dirname(landingPath), { recursive: true });
|
|
292
|
-
copyFileCloneOrFallback(nativePath,
|
|
276
|
+
copyFileCloneOrFallback(nativePath, staging);
|
|
277
|
+
}
|
|
278
|
+
if (options.host === "grok-build") {
|
|
279
|
+
// A directory cannot be renamed over: park the previous original aside,
|
|
280
|
+
// move the new one in, and put the old one back only if the landing is
|
|
281
|
+
// still absent. This attempt owns its `.previous.<token>` through
|
|
282
|
+
// success or failure — restore-if-absent, then clear the token when it
|
|
283
|
+
// is no longer the unique original (peer may have published landing).
|
|
284
|
+
// Never delete previous when restore failed and landing is still absent.
|
|
285
|
+
// Cleanup failure must not wash into native-session-copy success
|
|
286
|
+
// (#1161 C2 / 失败诚实). No orphan scan, no cross-call recovery, no queue.
|
|
287
|
+
const previous = `${landingPath}.previous.${attemptToken}`;
|
|
288
|
+
const hadPrevious = existsSync(landingPath);
|
|
289
|
+
let parked = false;
|
|
290
|
+
let keepUniquePrevious = false;
|
|
291
|
+
let cleanupError: unknown;
|
|
292
|
+
let swapFailure: unknown;
|
|
293
|
+
let restoreError: unknown;
|
|
294
|
+
try {
|
|
295
|
+
if (hadPrevious) {
|
|
296
|
+
renameSync(landingPath, previous);
|
|
297
|
+
parked = true;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
renameSync(staging, landingPath);
|
|
301
|
+
} catch (swapError) {
|
|
302
|
+
if (parked && existsSync(previous) && !existsSync(landingPath)) {
|
|
303
|
+
try {
|
|
304
|
+
renameSync(previous, landingPath);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
restoreError = error;
|
|
307
|
+
// Restore failed; previous may still be the only old original.
|
|
308
|
+
if (existsSync(previous) && !existsSync(landingPath)) {
|
|
309
|
+
keepUniquePrevious = true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
swapFailure = swapError;
|
|
314
|
+
}
|
|
315
|
+
} finally {
|
|
316
|
+
if (parked && existsSync(previous) && !keepUniquePrevious) {
|
|
317
|
+
try {
|
|
318
|
+
rmSync(previous, { recursive: true, force: true });
|
|
319
|
+
} catch (error) {
|
|
320
|
+
cleanupError = error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (swapFailure !== undefined) {
|
|
325
|
+
// Keep every residual cause; previous cleanup names its path explicitly
|
|
326
|
+
// (Node errno usually embeds path — still pass residual so AggregateError
|
|
327
|
+
// never silently omits which leftover remains; #1161 C2).
|
|
328
|
+
lastError = combineCopyFailureCauses(
|
|
329
|
+
swapFailure,
|
|
330
|
+
restoreError,
|
|
331
|
+
cleanupError,
|
|
332
|
+
cleanupError === undefined ? undefined : previous,
|
|
333
|
+
);
|
|
334
|
+
// Ordinary swap fail with restore+cleanup success → ADR 0086 retry once.
|
|
335
|
+
// Restore or cleanup incomplete → keep residual paths; do not retry
|
|
336
|
+
// (retry would re-park / wash residual-previous causes).
|
|
337
|
+
if (restoreError !== undefined || cleanupError !== undefined) {
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
rmSync(staging, { recursive: true, force: true });
|
|
342
|
+
stagingForCleanup = undefined;
|
|
343
|
+
} catch (stagingCleanupError) {
|
|
344
|
+
lastError = combineCopyFailureCauses(
|
|
345
|
+
swapFailure,
|
|
346
|
+
undefined,
|
|
347
|
+
stagingCleanupError,
|
|
348
|
+
staging,
|
|
349
|
+
);
|
|
350
|
+
// Residual already recorded; clear so the final pass does not re-rm / duplicate.
|
|
351
|
+
stagingForCleanup = undefined;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (cleanupError !== undefined) {
|
|
357
|
+
// Swap landed but cleanup ownership is incomplete — leave the true
|
|
358
|
+
// cause with residual previous path; do not retry or claim copy.
|
|
359
|
+
lastError = combineCopyFailureCauses(cleanupError, undefined, cleanupError, previous);
|
|
360
|
+
stagingForCleanup = undefined;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
renameSync(staging, landingPath); // atomically replaces the previous file
|
|
293
365
|
}
|
|
294
366
|
copySuccess = true;
|
|
367
|
+
stagingForCleanup = undefined;
|
|
295
368
|
break;
|
|
296
369
|
} catch (error) {
|
|
297
370
|
lastError = error;
|
|
371
|
+
try {
|
|
372
|
+
rmSync(staging, { recursive: true, force: true });
|
|
373
|
+
stagingForCleanup = undefined;
|
|
374
|
+
} catch (cleanupError) {
|
|
375
|
+
lastError = combineCopyFailureCauses(error, undefined, cleanupError, staging);
|
|
376
|
+
// Residual already recorded; clear so the final pass does not re-rm / duplicate.
|
|
377
|
+
stagingForCleanup = undefined;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
298
380
|
}
|
|
299
381
|
}
|
|
300
382
|
|
|
301
383
|
if (copySuccess) {
|
|
302
|
-
|
|
303
|
-
level: "event",
|
|
304
|
-
kind: HOST_SESSION_RECORD_KIND,
|
|
305
|
-
host: options.host,
|
|
306
|
-
cwd: options.cwd,
|
|
307
|
-
sessionParent: options.sessionParent,
|
|
308
|
-
source: `${options.host}-dossier`,
|
|
309
|
-
payload: {
|
|
310
|
-
type: "native-session-copy",
|
|
311
|
-
nativePath,
|
|
312
|
-
landingPath,
|
|
313
|
-
ordinal,
|
|
314
|
-
sessionId: options.sessionId,
|
|
315
|
-
},
|
|
316
|
-
});
|
|
384
|
+
report({ type: "native-session-copy", nativePath, landingPath, sessionId: options.sessionId });
|
|
317
385
|
} else {
|
|
318
|
-
// A partial
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
source: `${options.host}-dossier`,
|
|
333
|
-
payload: {
|
|
334
|
-
type: "native-session-warning",
|
|
335
|
-
nativePath,
|
|
336
|
-
landingPath,
|
|
337
|
-
ordinal,
|
|
338
|
-
sessionId: options.sessionId,
|
|
339
|
-
error: errorText(lastError),
|
|
340
|
-
},
|
|
386
|
+
// A partial attempt must not masquerade as a complete native original.
|
|
387
|
+
if (stagingForCleanup !== undefined) {
|
|
388
|
+
try {
|
|
389
|
+
rmSync(stagingForCleanup, { recursive: true, force: true });
|
|
390
|
+
} catch (cleanupError) {
|
|
391
|
+
lastError = combineCopyFailureCauses(lastError, undefined, cleanupError, stagingForCleanup);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
report({
|
|
395
|
+
type: "native-session-warning",
|
|
396
|
+
nativePath,
|
|
397
|
+
landingPath,
|
|
398
|
+
sessionId: options.sessionId,
|
|
399
|
+
error: errorText(lastError),
|
|
341
400
|
});
|
|
342
401
|
}
|
|
343
402
|
}
|
|
403
|
+
|
|
404
|
+
/** Fold swap / restore / staging-or-previous cleanup causes without washing any. */
|
|
405
|
+
function combineCopyFailureCauses(
|
|
406
|
+
primary: unknown,
|
|
407
|
+
restoreError: unknown,
|
|
408
|
+
cleanupError: unknown,
|
|
409
|
+
residualPath?: string,
|
|
410
|
+
): unknown {
|
|
411
|
+
const errors: unknown[] = [primary];
|
|
412
|
+
const parts = [errorText(primary)];
|
|
413
|
+
if (restoreError !== undefined) {
|
|
414
|
+
errors.push(restoreError);
|
|
415
|
+
parts.push(`restore failed: ${errorText(restoreError)}`);
|
|
416
|
+
}
|
|
417
|
+
if (cleanupError !== undefined) {
|
|
418
|
+
if (cleanupError !== primary) errors.push(cleanupError);
|
|
419
|
+
parts.push(
|
|
420
|
+
residualPath === undefined
|
|
421
|
+
? `cleanup failed: ${errorText(cleanupError)}`
|
|
422
|
+
: `cleanup failed: ${errorText(cleanupError)} (residual: ${residualPath})`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (errors.length === 1 && parts.length === 1) return primary;
|
|
426
|
+
return new AggregateError(errors, parts.join("; "), { cause: primary });
|
|
427
|
+
}
|
|
@@ -8,6 +8,12 @@ import { isRecord } from "./unknown-value.ts";
|
|
|
8
8
|
|
|
9
9
|
export type LedgerSessionRow = Record<string, unknown>;
|
|
10
10
|
|
|
11
|
+
/** Complete Pi volume decoding: malformed EOF is an error, with no row-shape filtering. */
|
|
12
|
+
export async function readStrictPiSessionJsonl(path: string): Promise<unknown[]> {
|
|
13
|
+
const text = await readFile(path, "utf8");
|
|
14
|
+
return text.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line) as unknown);
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
/**
|
|
12
18
|
* Loud JSONL failure that still retains rows parsed before the bad line.
|
|
13
19
|
* Callers that only need the throw keep catching Error; owners that must
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { basename } from "node:path";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { sitianReportSafe } from "./host-session-record.ts";
|
|
13
13
|
import {
|
|
14
14
|
NavigatorUnavailableError,
|
|
15
15
|
navigatorProviderFailureFromPublicTerminal,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "./navigator-session-contracts.ts";
|
|
22
22
|
import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
23
23
|
import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
|
|
24
|
+
import { parseRunLeaf } from "./role-run-placement.ts";
|
|
24
25
|
import type { PublicSummonResult } from "./public-role-summons.ts";
|
|
25
26
|
import { CliUsageError } from "./public-cli/cli-errors.ts";
|
|
26
27
|
import { isNavigatorSeat } from "./packaged-role-registry.ts";
|
|
@@ -57,11 +58,9 @@ async function resolveNavigatorLedgerHome(context: HostContext): Promise<string
|
|
|
57
58
|
export const NAVIGATOR_HOST_RUN_POINTER_ENTRY = "ak-navigator-host-run";
|
|
58
59
|
|
|
59
60
|
export function runIdFromNavigatorDirectory(runDirectory: string): string | undefined {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const runId = entry.slice(0, entry.length - suffix.length);
|
|
64
|
-
return runId.length > 0 ? runId : undefined;
|
|
61
|
+
const parsed = parseRunLeaf(basename(runDirectory));
|
|
62
|
+
if (parsed === undefined || parsed.role !== "navigator") return undefined;
|
|
63
|
+
return parsed.runId;
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
export function readNavigatorHostRunPointer(entries: readonly unknown[]): string | undefined {
|
|
@@ -285,18 +284,14 @@ export function createNativeNavigatorSessionFactory(deps?: {
|
|
|
285
284
|
routePlaybookReadFailure: () => routePlaybookReadFailure,
|
|
286
285
|
appendEntry: (customType, data) => {
|
|
287
286
|
sessionManager.appendCustomEntry(customType, data);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
});
|
|
297
|
-
} catch {
|
|
298
|
-
// best-effort
|
|
299
|
-
}
|
|
287
|
+
sitianReportSafe({
|
|
288
|
+
level: "event",
|
|
289
|
+
kind: "attendance",
|
|
290
|
+
cwd: context.cwd,
|
|
291
|
+
sessionParent: sessionManager.getSessionFile(),
|
|
292
|
+
payload: { customType, data },
|
|
293
|
+
source: "navigator-public-session",
|
|
294
|
+
});
|
|
300
295
|
},
|
|
301
296
|
entries: () => sessionManager.getEntries(),
|
|
302
297
|
setModel: async (next, nextThinking) => {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
|
|
8
|
+
import { sessionDirectoryOf } from "./role-run-placement.ts";
|
|
9
|
+
|
|
8
10
|
import { loadDoctorCase } from "./doctor-evidence.ts";
|
|
9
11
|
import { isEnoent } from "./unknown-value.ts";
|
|
10
12
|
import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
|
|
@@ -69,7 +71,7 @@ export async function loadNavigatorWorkContext(
|
|
|
69
71
|
const publicRunDir = runDirectoryFromHostContext(options.context);
|
|
70
72
|
const currentSessionDir = options.context.sessionManager.getSessionDir();
|
|
71
73
|
const isBoundPublicRun = publicRunDir !== undefined
|
|
72
|
-
&& resolve(currentSessionDir) === resolve(publicRunDir
|
|
74
|
+
&& resolve(currentSessionDir) === resolve(sessionDirectoryOf(publicRunDir));
|
|
73
75
|
if (subjectMode === "public-instruction" && isBoundPublicRun) {
|
|
74
76
|
let admitted;
|
|
75
77
|
try {
|
package/src/notary-role.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RoleHost, HostContext } from "./host-contracts.ts";
|
|
2
2
|
import { registerFiledSubmissionTool } from "./filed-submission.ts";
|
|
3
3
|
import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
|
|
4
|
+
import { isSafePositiveTicketNumber } from "./run-ticket-number.ts";
|
|
4
5
|
/**
|
|
5
6
|
* Public Notary role runtime — direct officer seat (not through Gatekeeper province).
|
|
6
7
|
* Caller supplies only a source-run locator; Notary self-fetches authoritative materials.
|
|
@@ -44,7 +45,7 @@ export function readNotaryTicketFlag(flag: unknown): number | undefined {
|
|
|
44
45
|
}
|
|
45
46
|
if (flag.trim() === "") return undefined;
|
|
46
47
|
const n = Number(flag);
|
|
47
|
-
if (!
|
|
48
|
+
if (!isSafePositiveTicketNumber(n)) {
|
|
48
49
|
throw new Error(
|
|
49
50
|
"Notary ak-notary-ticket-number is present but not a safe positive integer string",
|
|
50
51
|
);
|
package/src/notary-source-run.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./activation-ledger-topology.ts";
|
|
15
15
|
import type { NotarySourceRunLocator } from "./notary-contracts.ts";
|
|
16
16
|
import { findRunDirectoryById, readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
|
|
17
|
+
import { formatRunLeaf, parseRunLeaf } from "./role-run-placement.ts";
|
|
17
18
|
|
|
18
19
|
async function readRetainedIdentity(runDirectory: string) {
|
|
19
20
|
try {
|
|
@@ -26,11 +27,6 @@ async function readRetainedIdentity(runDirectory: string) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
// Run directory leaf: <runId>@<role>. Production uses uuidv7 runIds; offline tracers
|
|
30
|
-
// may use deterministic non-uuid leaves. Role token stays identifier-shaped.
|
|
31
|
-
const RUN_DIR_NAME =
|
|
32
|
-
/^([A-Za-z0-9][A-Za-z0-9._-]*)@([A-Za-z][A-Za-z0-9_-]*)$/;
|
|
33
|
-
|
|
34
30
|
export class NotarySourceRunError extends Error {
|
|
35
31
|
constructor(message: string, options?: ErrorOptions) {
|
|
36
32
|
super(message, options);
|
|
@@ -38,12 +34,6 @@ export class NotarySourceRunError extends Error {
|
|
|
38
34
|
}
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
function parseRunDirectoryName(name: string): { runId: string; role: string } | undefined {
|
|
42
|
-
const match = RUN_DIR_NAME.exec(name);
|
|
43
|
-
if (match === null) return undefined;
|
|
44
|
-
return { runId: match[1]!, role: match[2]! };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
async function requireRunDirectory(candidate: string, display: string): Promise<string> {
|
|
48
38
|
let real: string;
|
|
49
39
|
try {
|
|
@@ -68,7 +58,7 @@ async function requireRunDirectory(candidate: string, display: string): Promise<
|
|
|
68
58
|
`notary --source-run must be a run directory: ${display}`,
|
|
69
59
|
);
|
|
70
60
|
}
|
|
71
|
-
const identity =
|
|
61
|
+
const identity = parseRunLeaf(basename(real));
|
|
72
62
|
if (identity === undefined) {
|
|
73
63
|
throw new NotarySourceRunError(
|
|
74
64
|
`notary --source-run must be named <runId>@<role>: ${basename(real)}`,
|
|
@@ -84,7 +74,7 @@ async function requireRunDirectory(candidate: string, display: string): Promise<
|
|
|
84
74
|
* - absolute/relative path that realpath's to that same book runs slot
|
|
85
75
|
*
|
|
86
76
|
* Rejects project-tree projections and any path outside the project's ledger book runs.
|
|
87
|
-
* Retained
|
|
77
|
+
* Retained state.jsonl run-state must match basename identity and book binding.
|
|
88
78
|
*/
|
|
89
79
|
export async function resolveNotarySourceRunLocator(options: {
|
|
90
80
|
readonly projectRoot: string;
|
|
@@ -102,13 +92,13 @@ export async function resolveNotarySourceRunLocator(options: {
|
|
|
102
92
|
const bookRunsRoot = join(bookDirectory, "runs");
|
|
103
93
|
|
|
104
94
|
let candidate: string;
|
|
105
|
-
const bare =
|
|
95
|
+
const bare = parseRunLeaf(raw);
|
|
106
96
|
if (bare !== undefined && !raw.includes("/") && !raw.includes("\\")) {
|
|
107
97
|
candidate = (await findRunDirectoryById(options.home, bare.runId, bookKey, bare.role))
|
|
108
|
-
?? join(bookRunsRoot,
|
|
98
|
+
?? join(bookRunsRoot, formatRunLeaf(bare.runId, bare.role));
|
|
109
99
|
} else {
|
|
110
100
|
candidate = isAbsolute(raw) ? raw : resolve(options.projectRoot, raw);
|
|
111
|
-
const identity =
|
|
101
|
+
const identity = parseRunLeaf(basename(candidate));
|
|
112
102
|
const subjectDirectory = dirname(dirname(candidate));
|
|
113
103
|
const isLegacyUnboundLocator =
|
|
114
104
|
identity !== undefined &&
|
|
@@ -126,7 +116,7 @@ export async function resolveNotarySourceRunLocator(options: {
|
|
|
126
116
|
}
|
|
127
117
|
|
|
128
118
|
const real = await requireRunDirectory(candidate, raw);
|
|
129
|
-
const identity =
|
|
119
|
+
const identity = parseRunLeaf(basename(real))!;
|
|
130
120
|
|
|
131
121
|
const bookIdentity = physicalPathIdentity(bookDirectory);
|
|
132
122
|
const parentIdentity = physicalPathIdentity(dirname(real));
|
|
@@ -176,7 +166,7 @@ export async function loadNotarySourceRunLocator(
|
|
|
176
166
|
path: string,
|
|
177
167
|
): Promise<NotarySourceRunLocator> {
|
|
178
168
|
const real = await requireRunDirectory(path, path);
|
|
179
|
-
const identity =
|
|
169
|
+
const identity = parseRunLeaf(basename(real))!;
|
|
180
170
|
const runState = await readRetainedIdentity(real);
|
|
181
171
|
if (runState === undefined) {
|
|
182
172
|
throw new NotarySourceRunError(
|