@akagilnc/pi-workflow-roles 0.1.5504 → 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 +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
|
@@ -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
|
+
}
|
package/src/notary-source-run.ts
CHANGED
|
@@ -74,7 +74,7 @@ async function requireRunDirectory(candidate: string, display: string): Promise<
|
|
|
74
74
|
* - absolute/relative path that realpath's to that same book runs slot
|
|
75
75
|
*
|
|
76
76
|
* Rejects project-tree projections and any path outside the project's ledger book runs.
|
|
77
|
-
* Retained
|
|
77
|
+
* Retained state.jsonl run-state must match basename identity and book binding.
|
|
78
78
|
*/
|
|
79
79
|
export async function resolveNotarySourceRunLocator(options: {
|
|
80
80
|
readonly projectRoot: string;
|
|
@@ -25,22 +25,9 @@ export const LEGACY_REVIEW_OUTPUT_ROLES = new Map<string, string>([
|
|
|
25
25
|
["ak_auditor_output", "auditor"],
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* Success-face fields that still differ by seat. Presence and omission match
|
|
30
|
-
* the former per-seat publishers. Key order is not part of the contract.
|
|
31
|
-
*/
|
|
32
|
-
export type PackagedArtifactLeaf = {
|
|
33
|
-
readonly key: string;
|
|
34
|
-
readonly from?: string;
|
|
35
|
-
readonly omitUndefined?: true;
|
|
36
|
-
readonly copyArray?: true;
|
|
37
|
-
readonly callerProvenance?: true;
|
|
38
|
-
};
|
|
39
|
-
|
|
28
|
+
/** Success-terminal facts that still differ by seat. */
|
|
40
29
|
export type PackagedArtifactFace = {
|
|
41
30
|
readonly reportPhase?: true;
|
|
42
|
-
readonly evidenceRole?: true;
|
|
43
|
-
readonly leaves: readonly PackagedArtifactLeaf[];
|
|
44
31
|
readonly doctorReportFacts?: true;
|
|
45
32
|
};
|
|
46
33
|
|
|
@@ -126,13 +113,6 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
126
113
|
settlement: "sealed",
|
|
127
114
|
artifactFace: {
|
|
128
115
|
reportPhase: true,
|
|
129
|
-
evidenceRole: true,
|
|
130
|
-
leaves: [
|
|
131
|
-
{ key: "phase" },
|
|
132
|
-
{ key: "packetPath" },
|
|
133
|
-
{ key: "prerequisitesPath", omitUndefined: true },
|
|
134
|
-
{ key: "prerequisites" },
|
|
135
|
-
],
|
|
136
116
|
},
|
|
137
117
|
boardPlacement: "marshal",
|
|
138
118
|
activationFlags: [
|
|
@@ -164,11 +144,6 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
164
144
|
settlement: "sealed",
|
|
165
145
|
artifactFace: {
|
|
166
146
|
reportPhase: true,
|
|
167
|
-
evidenceRole: true,
|
|
168
|
-
leaves: [
|
|
169
|
-
{ key: "phase" },
|
|
170
|
-
{ key: "taskPath" },
|
|
171
|
-
],
|
|
172
147
|
},
|
|
173
148
|
activationFlags: [
|
|
174
149
|
{ field: "taskPath", flag: "ak-coder-task", binds: "input" },
|
|
@@ -203,13 +178,6 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
203
178
|
/** Publish only an accepted ledger outcome; audit escalation stays unsettled here. */
|
|
204
179
|
sealedAcceptedOnly: true,
|
|
205
180
|
artifactFace: {
|
|
206
|
-
evidenceRole: true,
|
|
207
|
-
leaves: [
|
|
208
|
-
{ key: "baseRevision" },
|
|
209
|
-
{ key: "lens" },
|
|
210
|
-
{ key: "authorityRefs", copyArray: true },
|
|
211
|
-
{ key: "callerProvenance", callerProvenance: true },
|
|
212
|
-
],
|
|
213
181
|
},
|
|
214
182
|
/** Frozen base/lens/authority become the initial prompt; instruction follows. */
|
|
215
183
|
transportPrompt: "skill-args",
|
|
@@ -242,12 +210,6 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
242
210
|
outputTool: COLLECTOR_OUTPUT_TOOL,
|
|
243
211
|
settlement: "sealed",
|
|
244
212
|
artifactFace: {
|
|
245
|
-
evidenceRole: true,
|
|
246
|
-
leaves: [
|
|
247
|
-
{ key: "prNumber", omitUndefined: true },
|
|
248
|
-
{ key: "repository", from: "repository.canonical" },
|
|
249
|
-
{ key: "manifestDigest" },
|
|
250
|
-
],
|
|
251
213
|
},
|
|
252
214
|
boardPlacement: "collector",
|
|
253
215
|
/** Taishi acceptance is a typed groups array, not a status leaf. */
|
|
@@ -277,12 +239,6 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
277
239
|
auditTool: DOCTOR_AUDIT_TOOL_NAME,
|
|
278
240
|
settlement: "sealed",
|
|
279
241
|
artifactFace: {
|
|
280
|
-
evidenceRole: true,
|
|
281
|
-
leaves: [
|
|
282
|
-
{ key: "issueNumber" },
|
|
283
|
-
{ key: "caseRunsPath" },
|
|
284
|
-
{ key: "caseIdentity" },
|
|
285
|
-
],
|
|
286
242
|
doctorReportFacts: true,
|
|
287
243
|
},
|
|
288
244
|
/** Navigator subject is the doctor case, not the case-path file bytes. */
|
|
@@ -309,11 +265,6 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
309
265
|
outputTool: MERGER_OUTPUT_TOOL_NAME,
|
|
310
266
|
settlement: "sealed",
|
|
311
267
|
artifactFace: {
|
|
312
|
-
evidenceRole: true,
|
|
313
|
-
leaves: [
|
|
314
|
-
{ key: "mergerInputPath" },
|
|
315
|
-
{ key: "derived" },
|
|
316
|
-
],
|
|
317
268
|
},
|
|
318
269
|
activationFlags: [
|
|
319
270
|
{ field: "inputPath", from: "mergerInputPath", flag: "ak-merger-input", binds: "input" },
|
package/src/pi/adapter.ts
CHANGED
|
@@ -18,6 +18,10 @@ import type {
|
|
|
18
18
|
import { createOAuthKeepalive, type OAuthKeepaliveOptions } from "../oauth-keepalive.ts";
|
|
19
19
|
import { createRoleRuntimeExtension, type RoleRuntimeDependencies } from "../role-runtime.ts";
|
|
20
20
|
import { renderAgentStartMaterials } from "../agent-start-materials.ts";
|
|
21
|
+
import { runDirectoryFromHostContext } from "../host-contracts.ts";
|
|
22
|
+
import { sitianReportSafe } from "../host-session-record.ts";
|
|
23
|
+
import { isTerminatingToolName } from "../package-contracts/terminating-tools.ts";
|
|
24
|
+
import { sessionFileOf } from "../role-run-placement.ts";
|
|
21
25
|
import { projectCorrectableExecuteRejection } from "../submission-correctable-error.ts";
|
|
22
26
|
import {
|
|
23
27
|
WorkerCommitReminderError,
|
|
@@ -148,6 +152,12 @@ export function createPiRoleHostAdapter(
|
|
|
148
152
|
options: { transcriptFromContext?: (context: ExtensionContext) => string; oauthKeepalive?: OAuthKeepaliveOptions } = {},
|
|
149
153
|
): PiRoleHostAdapter {
|
|
150
154
|
const keepalive = createOAuthKeepalive(options.oauthKeepalive);
|
|
155
|
+
// Pi chains the before_agent_start handlers, each seeing the prompt the one before left; the
|
|
156
|
+
// prompt Pi sends is the one the last registered handler leaves. Only that handler's result is
|
|
157
|
+
// what a start was given, so only it records.
|
|
158
|
+
let beforeAgentStartHandlers = 0;
|
|
159
|
+
// Parameters of registered terminating tools — the schema Pi was handed.
|
|
160
|
+
const registeredToolParameters = new Map<string, unknown>();
|
|
151
161
|
// Decode transport exactly once before Pi's native handler chain. Pi retains
|
|
152
162
|
// its own transform/image propagation and per-handler error isolation.
|
|
153
163
|
pi.on("input", (value) => {
|
|
@@ -160,10 +170,13 @@ export function createPiRoleHostAdapter(
|
|
|
160
170
|
},
|
|
161
171
|
registerFlag: (name, definition) => pi.registerFlag(name, definition),
|
|
162
172
|
getFlag: (name) => pi.getFlag(name),
|
|
163
|
-
registerTool: (tool) =>
|
|
164
|
-
tool,
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
registerTool: (tool) => {
|
|
174
|
+
registeredToolParameters.set(tool.name, tool.parameters);
|
|
175
|
+
return pi.registerTool(toPiToolDefinition(
|
|
176
|
+
tool,
|
|
177
|
+
(context) => projectPiContext(context, options.transcriptFromContext),
|
|
178
|
+
));
|
|
179
|
+
},
|
|
167
180
|
getAllTools: () => pi.getAllTools().map(({ name, sourceInfo }) => ({
|
|
168
181
|
name,
|
|
169
182
|
...(sourceInfo?.path === undefined ? {} : { sourceInfo: { path: sourceInfo.path } }),
|
|
@@ -174,10 +187,40 @@ export function createPiRoleHostAdapter(
|
|
|
174
187
|
const context = (value: ExtensionContext) => projectPiContext(value, options.transcriptFromContext);
|
|
175
188
|
if (registration[0] === "before_agent_start") {
|
|
176
189
|
const [, handler] = registration;
|
|
190
|
+
const position = beforeAgentStartHandlers;
|
|
191
|
+
beforeAgentStartHandlers += 1;
|
|
177
192
|
pi.on("before_agent_start", (value, ctx) => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
const projected = context(ctx);
|
|
194
|
+
const fold = (settled: unknown) => {
|
|
195
|
+
const folded = foldBeforeAgentStartReturn(settled, value.systemPrompt);
|
|
196
|
+
const runDirectory = runDirectoryFromHostContext(projected);
|
|
197
|
+
if (runDirectory !== undefined && position === beforeAgentStartHandlers - 1) {
|
|
198
|
+
const delivered = (folded as { systemPrompt?: string } | undefined)?.systemPrompt ?? value.systemPrompt;
|
|
199
|
+
// Record the same parameters object Pi was handed — not a
|
|
200
|
+
// headless draft-07 projection of another host's transport.
|
|
201
|
+
let outputSchema: unknown;
|
|
202
|
+
for (const [name, parameters] of registeredToolParameters) {
|
|
203
|
+
if (!isTerminatingToolName(name)) continue;
|
|
204
|
+
outputSchema = parameters;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
// History write failures do not stop a run (#833): declared, and Pi starts.
|
|
208
|
+
sitianReportSafe({
|
|
209
|
+
level: "event",
|
|
210
|
+
kind: "turn-delivery",
|
|
211
|
+
sessionParent: sessionFileOf(runDirectory),
|
|
212
|
+
source: "pi-adapter",
|
|
213
|
+
payload: {
|
|
214
|
+
systemPrompt: delivered,
|
|
215
|
+
...(outputSchema === undefined ? {} : { outputSchema }),
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return folded;
|
|
220
|
+
};
|
|
221
|
+
const result = handler({ prompt: value.prompt, systemPrompt: value.systemPrompt, systemPromptOptions: value.systemPromptOptions }, projected);
|
|
222
|
+
if (result instanceof Promise) return result.then(fold);
|
|
223
|
+
return fold(result);
|
|
181
224
|
});
|
|
182
225
|
} else if (registration[0] === "input") {
|
|
183
226
|
const [, handler] = registration;
|