@akagilnc/pi-workflow-roles 0.1.5216 → 0.1.5241
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/CLAUDE.md +3 -0
- package/README.md +1 -1
- package/dist/acp-host/production-host.js +658 -829
- package/dist/engine-detour-usage.js +4 -16
- package/dist/headless-host/production-host.js +777 -862
- package/dist/host-session-record.js +303 -0
- package/dist/pi/role-turn-host.js +1 -21
- package/dist/public-cli/main.js +200 -487
- package/dist/public-cli/post-admission.js +11 -60
- package/dist/sitian-appender.js +10 -155
- package/package.json +1 -1
- package/src/acp-host/role-turn-host.ts +36 -108
- package/src/engine-detour-usage.ts +4 -16
- package/src/external-host-turn-loop.ts +2 -18
- package/src/headless-host/role-turn-host.ts +67 -31
- package/src/host-contracts.ts +0 -15
- package/src/host-session-record.ts +325 -25
- package/src/notary-role.ts +0 -1
- package/src/pi/role-turn-host.ts +1 -25
- package/src/public-cli/countersign-run.ts +4 -3
- package/src/public-cli/post-admission.ts +10 -69
- package/src/public-role-summons.ts +1 -2
- package/src/role-envelope.ts +2 -2
- package/src/role-runtime.ts +0 -14
- package/src/sitian-appender.ts +16 -205
- package/dist/host-transition-prior-native.js +0 -78
- package/dist/public-cli/case-dossier-delivery.js +0 -109
- package/src/host-transition-prior-native.ts +0 -80
- package/src/pi/pi-normalization.ts +0 -291
- package/src/public-cli/case-dossier-delivery.ts +0 -147
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Middle external-host role-turn loop (#820 / ADR 0082).
|
|
3
|
-
* One copy: serial,
|
|
3
|
+
* One copy: serial, abort merge/race, 8-round closeRound retry,
|
|
4
4
|
* host-aborted, round-limit. Last hop = ExternalHostTurnDriver (four verbs).
|
|
5
5
|
*/
|
|
6
6
|
import type {
|
|
@@ -9,7 +9,6 @@ import type {
|
|
|
9
9
|
RoleTurnRequest,
|
|
10
10
|
RoleTurnResult,
|
|
11
11
|
} from "./host-contracts.ts";
|
|
12
|
-
import { isOfficerReviewSeat } from "./packaged-role-registry.ts";
|
|
13
12
|
|
|
14
13
|
export const EXTERNAL_ROLE_TURN_ROUND_LIMIT = 8 as const;
|
|
15
14
|
|
|
@@ -55,21 +54,6 @@ export function mergeRoleTurnAbortSignals(
|
|
|
55
54
|
return AbortSignal.any([prepared, request]);
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
/**
|
|
59
|
-
* Host-transition prior-native paths are machine handoff material (#617 DK-4).
|
|
60
|
-
* #879: station-child officer dialogue must not receive those paths spliced into
|
|
61
|
-
* the review prompt — officers already hold --source-run / binding pointers.
|
|
62
|
-
*/
|
|
63
|
-
export function promptWithPriorNativePaths(basePrompt: string, request: RoleTurnRequest): string {
|
|
64
|
-
if (request.continuation.kind !== "resume") return basePrompt;
|
|
65
|
-
if (request.stationChild === true && isOfficerReviewSeat(request.activation.role)) {
|
|
66
|
-
return basePrompt;
|
|
67
|
-
}
|
|
68
|
-
const paths = request.hostTransition?.priorNativePaths;
|
|
69
|
-
if (paths === undefined || paths.length === 0) return basePrompt;
|
|
70
|
-
return `${basePrompt}\n${paths.join("\n")}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
57
|
export function isHostAbortedError(error: unknown): boolean {
|
|
74
58
|
return typeof error === "object" && error !== null && (error as { code?: unknown }).code === "host-aborted";
|
|
75
59
|
}
|
|
@@ -135,7 +119,7 @@ export async function driveExternalRoleTurnRounds(
|
|
|
135
119
|
request: RoleTurnRequest,
|
|
136
120
|
driver: ExternalHostTurnDriver,
|
|
137
121
|
): Promise<RoleTurnResult> {
|
|
138
|
-
let prompt =
|
|
122
|
+
let prompt = prepared.prompt;
|
|
139
123
|
const abortSignal = mergeRoleTurnAbortSignals(prepared.abortSignal, request.signal);
|
|
140
124
|
let stderr = "";
|
|
141
125
|
|
|
@@ -22,7 +22,10 @@ import {
|
|
|
22
22
|
type SessionIdentityAuthority,
|
|
23
23
|
} from "../prepared-role-turn.ts";
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
copyAndRecordHostDossier,
|
|
27
|
+
recordNativeSessionPointer,
|
|
28
|
+
} from "../host-session-record.ts";
|
|
26
29
|
import {
|
|
27
30
|
closeJsonSchemaForCodex,
|
|
28
31
|
codexTurnArgs,
|
|
@@ -261,6 +264,7 @@ function spawnHeadlessTurn(options: {
|
|
|
261
264
|
readonly stdin?: string;
|
|
262
265
|
/** Called for each complete stdout line as it arrives (live stream-json). */
|
|
263
266
|
readonly onStdoutLine?: (line: string) => void;
|
|
267
|
+
readonly onClose?: () => void;
|
|
264
268
|
}): Promise<{ code: number | null; stdout: string; stderr: string; timedOut: boolean }> {
|
|
265
269
|
return new Promise((resolve, reject) => {
|
|
266
270
|
if (options.signal?.aborted) {
|
|
@@ -325,8 +329,15 @@ function spawnHeadlessTurn(options: {
|
|
|
325
329
|
lineBuffer = "";
|
|
326
330
|
}
|
|
327
331
|
};
|
|
332
|
+
let aborted = false;
|
|
328
333
|
const settle = (code: number | null): void => {
|
|
329
334
|
if (settled) return;
|
|
335
|
+
if (aborted) {
|
|
336
|
+
settled = true;
|
|
337
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
338
|
+
reject(hostAbortedError("headless host aborted"));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
330
341
|
// Final flush first: onStdoutLine may failLine (reject + settled=true).
|
|
331
342
|
flushStdoutLines("", true);
|
|
332
343
|
if (settled) return;
|
|
@@ -340,11 +351,9 @@ function spawnHeadlessTurn(options: {
|
|
|
340
351
|
resolve({ code, stdout: resultStdout, stderr, timedOut });
|
|
341
352
|
};
|
|
342
353
|
const onAbort = (): void => {
|
|
343
|
-
child.kill("SIGTERM");
|
|
344
354
|
if (settled) return;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
reject(hostAbortedError("headless host aborted"));
|
|
355
|
+
aborted = true;
|
|
356
|
+
child.kill("SIGTERM");
|
|
348
357
|
};
|
|
349
358
|
child.stdout.setEncoding("utf8").on("data", (chunk: string) => { flushStdoutLines(chunk, false); });
|
|
350
359
|
child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
|
|
@@ -357,7 +366,10 @@ function spawnHeadlessTurn(options: {
|
|
|
357
366
|
options.signal?.removeEventListener("abort", onAbort);
|
|
358
367
|
reject(error);
|
|
359
368
|
});
|
|
360
|
-
child.on("close", (code) =>
|
|
369
|
+
child.on("close", (code) => {
|
|
370
|
+
options.onClose?.();
|
|
371
|
+
settle(code);
|
|
372
|
+
});
|
|
361
373
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
362
374
|
if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
|
|
363
375
|
timer = setTimeout(() => {
|
|
@@ -498,6 +510,8 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
498
510
|
}
|
|
499
511
|
|
|
500
512
|
const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
|
|
513
|
+
let exitedSessionId: string | undefined;
|
|
514
|
+
try {
|
|
501
515
|
outcome = await driveExternalRoleTurnRounds(prepared, request, {
|
|
502
516
|
roundLimitName: "HeadlessRoundLimit",
|
|
503
517
|
currentSessionId: () => sessionId,
|
|
@@ -532,9 +546,23 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
532
546
|
}
|
|
533
547
|
|
|
534
548
|
const codexObserver = codex ? createCodexExecTurnObserver() : undefined;
|
|
549
|
+
let pointerRecorded = false;
|
|
550
|
+
let codexIdObserved = false;
|
|
551
|
+
if (sessionId !== undefined && sessionId !== "") {
|
|
552
|
+
pointerRecorded = recordNativeSessionPointer({
|
|
553
|
+
host: config.hostName,
|
|
554
|
+
sessionId,
|
|
555
|
+
cwd: request.cwd,
|
|
556
|
+
sessionParent,
|
|
557
|
+
home: request.home,
|
|
558
|
+
}) !== undefined;
|
|
559
|
+
}
|
|
560
|
+
|
|
535
561
|
let spawned: { code: number | null; stdout: string; stderr: string; timedOut: boolean };
|
|
562
|
+
let childExited = false;
|
|
536
563
|
try {
|
|
537
564
|
spawned = await spawnHeadlessTurn({
|
|
565
|
+
onClose() { childExited = true; },
|
|
538
566
|
binary: config.binary,
|
|
539
567
|
args,
|
|
540
568
|
cwd: request.cwd,
|
|
@@ -554,16 +582,33 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
554
582
|
}
|
|
555
583
|
// One bounded live seam owns both recording and host-specific reduction.
|
|
556
584
|
codexObserver?.observe(event);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
sessionParent,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
585
|
+
if (!codex && typeof event === "object" && event !== null && "session_id" in event
|
|
586
|
+
&& typeof event.session_id === "string" && event.session_id !== "" && event.session_id !== sessionId) {
|
|
587
|
+
sessionId = event.session_id;
|
|
588
|
+
recordNativeSessionPointer({ host: config.hostName, sessionId, cwd: request.cwd, sessionParent, home: request.home });
|
|
589
|
+
}
|
|
590
|
+
if (codex && !codexIdObserved) {
|
|
591
|
+
const tid = codexObserver?.result().threadId;
|
|
592
|
+
if (tid !== undefined && tid !== "") {
|
|
593
|
+
codexIdObserved = true;
|
|
594
|
+
pointerRecorded = recordNativeSessionPointer({
|
|
595
|
+
host: config.hostName,
|
|
596
|
+
sessionId: tid,
|
|
597
|
+
cwd: request.cwd,
|
|
598
|
+
sessionParent,
|
|
599
|
+
home: request.home,
|
|
600
|
+
}) !== undefined;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
564
603
|
},
|
|
565
604
|
});
|
|
605
|
+
const roundSessionId = (codex ? codexObserver?.result().threadId : undefined) ?? sessionId;
|
|
606
|
+
exitedSessionId = roundSessionId;
|
|
607
|
+
if (codex && roundSessionId !== undefined && roundSessionId !== "" && !pointerRecorded) {
|
|
608
|
+
recordNativeSessionPointer({ host: config.hostName, sessionId: roundSessionId, cwd: request.cwd, sessionParent, home: request.home });
|
|
609
|
+
}
|
|
566
610
|
} catch (error) {
|
|
611
|
+
if (childExited) exitedSessionId = (codex ? codexObserver?.result().threadId : undefined) ?? sessionId;
|
|
567
612
|
if (isHostAbortedError(error)) throw error;
|
|
568
613
|
const message = error instanceof Error ? error.message : String(error);
|
|
569
614
|
const observedHostFailure = codexObserver?.result().failureDiagnostic;
|
|
@@ -577,24 +622,6 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
577
622
|
}, observedHostFailure),
|
|
578
623
|
};
|
|
579
624
|
}
|
|
580
|
-
// SitianInfrastructureError.knownCause is session; spawn errno stays activation.
|
|
581
|
-
const isRecordFailure =
|
|
582
|
-
typeof error === "object"
|
|
583
|
-
&& error !== null
|
|
584
|
-
&& ((error as { knownCause?: unknown }).knownCause === "session"
|
|
585
|
-
|| (error as { name?: unknown }).name === "SitianInfrastructureError");
|
|
586
|
-
if (isRecordFailure) {
|
|
587
|
-
return {
|
|
588
|
-
status: "terminal",
|
|
589
|
-
result: failure(
|
|
590
|
-
"session",
|
|
591
|
-
"HostSessionRecordFailure",
|
|
592
|
-
"host-session-record-failed",
|
|
593
|
-
{ diagnostic: message, sessionId },
|
|
594
|
-
message,
|
|
595
|
-
),
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
625
|
return {
|
|
599
626
|
status: "terminal",
|
|
600
627
|
result: failure("activation", "HeadlessSpawnFailure", "spawn-failed", {
|
|
@@ -773,6 +800,15 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
773
800
|
return { status: "delivered", stderr: spawned.stderr };
|
|
774
801
|
},
|
|
775
802
|
});
|
|
803
|
+
} finally {
|
|
804
|
+
if (exitedSessionId !== undefined && exitedSessionId !== "") copyAndRecordHostDossier({
|
|
805
|
+
host: config.hostName, sessionId: exitedSessionId, cwd: request.cwd,
|
|
806
|
+
sessionDirectory: join(request.runDirectory, "session"), sessionParent,
|
|
807
|
+
continuation: request.continuation,
|
|
808
|
+
...(request.model !== undefined ? { model: request.model } : {}),
|
|
809
|
+
...(request.home !== undefined ? { home: request.home } : {}),
|
|
810
|
+
});
|
|
811
|
+
}
|
|
776
812
|
} finally {
|
|
777
813
|
try {
|
|
778
814
|
await prepared.dispose?.();
|
package/src/host-contracts.ts
CHANGED
|
@@ -158,19 +158,6 @@ export type RoleTurnModelConfig = {
|
|
|
158
158
|
readonly thinking?: string;
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
/**
|
|
162
|
-
* Typed cross-host resume handoff (#617 DK-4).
|
|
163
|
-
* Present only when post-admission projects a real host switch.
|
|
164
|
-
* priorNativeKind names the record family the paths belong to — Pi's own
|
|
165
|
-
* session file, or sitian run records (ADR 0077) — so a consuming adapter
|
|
166
|
-
* reads the handoff without knowing which host wrote it.
|
|
167
|
-
* Target host reads those files itself; projector never copies bytes.
|
|
168
|
-
*/
|
|
169
|
-
export type RoleTurnHostTransition = {
|
|
170
|
-
readonly priorNativeKind: "pi-native" | "sitian";
|
|
171
|
-
readonly priorNativePaths: readonly string[];
|
|
172
|
-
};
|
|
173
|
-
|
|
174
161
|
/** One main-session turn request over the host-neutral execution seam. */
|
|
175
162
|
export type RoleTurnRequest = {
|
|
176
163
|
readonly principal: DurablePrincipal;
|
|
@@ -193,8 +180,6 @@ export type RoleTurnRequest = {
|
|
|
193
180
|
* has no parent to observe and leaves it absent.
|
|
194
181
|
*/
|
|
195
182
|
readonly signal?: AbortSignal;
|
|
196
|
-
/** Set by post-admission only on a real host switch; never on same-host resume. */
|
|
197
|
-
readonly hostTransition?: RoleTurnHostTransition;
|
|
198
183
|
/**
|
|
199
184
|
* Court-turn attempt (#637 / #833): sole-final per attempt. Open court, summons,
|
|
200
185
|
* and resume-with-message set this; bare resume without an open court omits it.
|
|
@@ -1,41 +1,341 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Host conversation dossiers are native file copies; sitian append-only (ADR 0086).
|
|
3
|
+
* Replaces live event streaming (ADR 0077) with post-exit CLI original copy.
|
|
4
|
+
* Writes native-session-pointer when session ID is obtained.
|
|
5
|
+
* Writes native-session-copy (or native-session-warning on retry failure) after CLI exit.
|
|
6
|
+
* Log line write failures declare to stderr and never abort the leg.
|
|
6
7
|
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import {
|
|
10
|
+
appendFileSync,
|
|
11
|
+
copyFileSync,
|
|
12
|
+
closeSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
openSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
statSync,
|
|
20
|
+
type Dirent,
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
|
|
25
|
+
import type { RoleTurnContinuation } from "./host-contracts.ts";
|
|
7
26
|
import { sitianReport } from "./sitian-facade.ts";
|
|
27
|
+
import type { SitianRecordInput } from "./sitian-contracts.ts";
|
|
8
28
|
|
|
9
29
|
/** Volume category under `<run>/session/<kind>/records.jsonl`. */
|
|
10
30
|
export const HOST_SESSION_RECORD_KIND = "host-session" as const;
|
|
11
31
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
32
|
+
function declareHostSessionFailure(error: unknown): void {
|
|
33
|
+
const message = error instanceof Error ? (error.stack || error.message) : String(error);
|
|
34
|
+
process.stderr.write(`[host-session] Dossier record failure: ${message}\n`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Declare sitian record failure once to stderr without aborting the leg (ADR 0086). */
|
|
38
|
+
export function sitianReportSafe(input: SitianRecordInput): void {
|
|
39
|
+
try {
|
|
40
|
+
sitianReport(input);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
declareHostSessionFailure(error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Recursively scan for Codex rollout file matching sessionId under sessionsDir. */
|
|
47
|
+
function findCodexRollout(sessionsDir: string, sessionId: string): string | undefined {
|
|
48
|
+
if (!existsSync(sessionsDir)) return undefined;
|
|
49
|
+
const matches: Array<{ path: string; mtime: number }> = [];
|
|
50
|
+
function scan(dir: string, depth = 0): void {
|
|
51
|
+
if (depth > 6) return;
|
|
52
|
+
let entries: Dirent[];
|
|
53
|
+
try {
|
|
54
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
55
|
+
} catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const fullPath = join(dir, entry.name);
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
scan(fullPath, depth + 1);
|
|
62
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) {
|
|
63
|
+
try {
|
|
64
|
+
const stat = statSync(fullPath);
|
|
65
|
+
matches.push({ path: fullPath, mtime: stat.mtimeMs });
|
|
66
|
+
} catch {
|
|
67
|
+
matches.push({ path: fullPath, mtime: 0 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
scan(sessionsDir);
|
|
73
|
+
if (matches.length === 0 || matches[0] === undefined) return undefined;
|
|
74
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
75
|
+
return matches[0]?.path;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sanitize working directory to match Claude Code's project directory name layout.
|
|
80
|
+
* Claude Code replaces all non-alphanumeric characters (slashes, dots, underscores, spaces, etc.) with '-'.
|
|
81
|
+
*/
|
|
82
|
+
export function sanitizeClaudeCwd(cwd: string): string {
|
|
83
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
16
84
|
}
|
|
17
85
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the CLI native session original path.
|
|
88
|
+
* Codex: single file rollout from ~/.codex/sessions/** /rollout-*-${sessionId}.jsonl
|
|
89
|
+
* Claude: single file transcript from ~/.claude/projects/<sanitized-cwd>/${sessionId}.jsonl
|
|
90
|
+
* Grok: directory ~/.grok/sessions/<encoded-cwd>/${sessionId}
|
|
91
|
+
* Pi / Hermes: undefined (Pi directly lands in session.jsonl; Hermes is state.db excluded from ADR 0086)
|
|
92
|
+
*/
|
|
93
|
+
export function resolveNativeSessionPath(options: {
|
|
20
94
|
readonly host: string;
|
|
95
|
+
readonly sessionId: string;
|
|
96
|
+
readonly cwd: string;
|
|
97
|
+
readonly home?: string | undefined;
|
|
98
|
+
}): string | undefined {
|
|
99
|
+
const home = options.home || homedir();
|
|
100
|
+
if (options.host === "claude") {
|
|
101
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
|
|
102
|
+
const sanitizedCwd = sanitizeClaudeCwd(options.cwd);
|
|
103
|
+
return join(configDir, "projects", sanitizedCwd, `${options.sessionId}.jsonl`);
|
|
104
|
+
}
|
|
105
|
+
if (options.host === "codex") {
|
|
106
|
+
return findCodexRollout(codexSessionsDirectory(home), options.sessionId);
|
|
107
|
+
}
|
|
108
|
+
if (options.host === "grok-build") {
|
|
109
|
+
const grokHome = process.env.GROK_HOME || join(home, ".grok");
|
|
110
|
+
return join(grokHome, "sessions", encodeURIComponent(options.cwd), options.sessionId);
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function codexSessionsDirectory(home?: string): string {
|
|
116
|
+
return join(process.env.CODEX_HOME || join(home || homedir(), ".codex"), "sessions");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Count each host activation, including Pi turns without an external copy. */
|
|
120
|
+
export function recordRunStart(runDirectory: string): void {
|
|
121
|
+
try { appendFileSync(join(runDirectory, ".run-starts"), "\n"); }
|
|
122
|
+
catch (error) { declareHostSessionFailure(error); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the host dossier landing destination under `<run>/session/`.
|
|
127
|
+
* Format: `<run>/session/<host>-<model>-<n>` (single file adds .jsonl; model slashes replaced with -).
|
|
128
|
+
* Ordinal n is the start/resume count in the run (initial 1, each resume +1).
|
|
129
|
+
*/
|
|
130
|
+
export function resolveHostDossierLandingPath(options: {
|
|
131
|
+
readonly host: string;
|
|
132
|
+
readonly model?: { readonly model?: string; readonly thinking?: string } | undefined;
|
|
133
|
+
readonly sessionDirectory: string;
|
|
134
|
+
readonly continuation: RoleTurnContinuation;
|
|
135
|
+
}): { readonly landingPath: string; readonly ordinal: number; readonly sanitizedModel: string } {
|
|
136
|
+
const sanitizedModel = (options.model?.model ?? "default").replace(/[\/\\]+/g, "-");
|
|
137
|
+
let maxOrdinal = 0;
|
|
138
|
+
|
|
139
|
+
if (existsSync(options.sessionDirectory)) {
|
|
140
|
+
try {
|
|
141
|
+
const entries = readdirSync(options.sessionDirectory);
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
const match = entry.match(/-(\d+)(?:\.jsonl|\.failed)?$/);
|
|
144
|
+
const first = match?.[1];
|
|
145
|
+
if (first !== undefined) {
|
|
146
|
+
const val = parseInt(first, 10);
|
|
147
|
+
if (!isNaN(val) && val > maxOrdinal) {
|
|
148
|
+
maxOrdinal = val;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Ignore directory read errors
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const startsPath = join(dirname(options.sessionDirectory), ".run-starts");
|
|
158
|
+
let starts = 0;
|
|
159
|
+
if (existsSync(startsPath)) starts = readFileSync(startsPath, "utf8").length;
|
|
160
|
+
const ordinal = Math.max(maxOrdinal + 1, starts, options.continuation.kind === "resume" ? 2 : 1);
|
|
161
|
+
|
|
162
|
+
const baseName = `${options.host}-${sanitizedModel}-${ordinal}`;
|
|
163
|
+
const landingPath = options.host === "grok-build"
|
|
164
|
+
? join(options.sessionDirectory, baseName)
|
|
165
|
+
: join(options.sessionDirectory, `${baseName}.jsonl`);
|
|
166
|
+
|
|
167
|
+
return { landingPath, ordinal, sanitizedModel };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Record native session pointer line to Sitian as soon as session ID is known. */
|
|
171
|
+
export function recordNativeSessionPointer(options: {
|
|
172
|
+
readonly host: string;
|
|
173
|
+
readonly sessionId: string;
|
|
21
174
|
readonly cwd: string;
|
|
22
175
|
readonly sessionParent: string;
|
|
23
|
-
readonly
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
176
|
+
readonly home?: string | undefined;
|
|
177
|
+
}): string | undefined {
|
|
178
|
+
if (options.host === "pi" || options.host === "hermes") {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const nativePath = options.host === "codex"
|
|
182
|
+
? codexSessionsDirectory(options.home)
|
|
183
|
+
: resolveNativeSessionPath(options);
|
|
184
|
+
if (nativePath === undefined) return undefined;
|
|
185
|
+
|
|
186
|
+
sitianReportSafe({
|
|
31
187
|
level: "event",
|
|
32
188
|
kind: HOST_SESSION_RECORD_KIND,
|
|
33
|
-
host:
|
|
34
|
-
cwd:
|
|
35
|
-
sessionParent:
|
|
36
|
-
source:
|
|
37
|
-
payload:
|
|
38
|
-
|
|
39
|
-
|
|
189
|
+
host: options.host,
|
|
190
|
+
cwd: options.cwd,
|
|
191
|
+
sessionParent: options.sessionParent,
|
|
192
|
+
source: `${options.host}-dossier`,
|
|
193
|
+
payload: {
|
|
194
|
+
type: "native-session-pointer",
|
|
195
|
+
nativePath,
|
|
196
|
+
sessionId: options.sessionId,
|
|
197
|
+
},
|
|
40
198
|
});
|
|
199
|
+
return nativePath;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** File clone via APFS `cp -c` with fallback to `copyFileSync`. */
|
|
203
|
+
function copyFileCloneOrFallback(src: string, dest: string): void {
|
|
204
|
+
try {
|
|
205
|
+
const res = spawnSync("cp", ["-c", src, dest]);
|
|
206
|
+
if (res.status === 0) return;
|
|
207
|
+
} catch {
|
|
208
|
+
// cp execution failed, fall back
|
|
209
|
+
}
|
|
210
|
+
copyFileSync(src, dest);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Copy both grok-build originals: chat_history.jsonl and usage.json into destDir. */
|
|
214
|
+
function copyGrokDossier(srcDir: string, destDir: string): void {
|
|
215
|
+
const chatHistorySrc = join(srcDir, "chat_history.jsonl");
|
|
216
|
+
if (!existsSync(chatHistorySrc)) {
|
|
217
|
+
throw new Error(`Grok native chat_history.jsonl missing at ${chatHistorySrc}`);
|
|
218
|
+
}
|
|
219
|
+
mkdirSync(destDir, { recursive: true });
|
|
220
|
+
const chatHistoryDest = join(destDir, "chat_history.jsonl");
|
|
221
|
+
copyFileCloneOrFallback(chatHistorySrc, chatHistoryDest);
|
|
222
|
+
|
|
223
|
+
const usageSrc = join(srcDir, "usage.json");
|
|
224
|
+
if (!existsSync(usageSrc)) {
|
|
225
|
+
throw new Error(`Grok native usage.json missing at ${usageSrc}`);
|
|
226
|
+
}
|
|
227
|
+
copyFileCloneOrFallback(usageSrc, join(destDir, "usage.json"));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Copy native host session original to run session dossier after child process exits.
|
|
232
|
+
* If copy fails, retries once.
|
|
233
|
+
* On success, appends native-session-copy line to Sitian.
|
|
234
|
+
* On repeated failure, appends native-session-warning line to Sitian.
|
|
235
|
+
* Does not throw; turn outcome is never altered by copy success or failure.
|
|
236
|
+
*/
|
|
237
|
+
export function copyAndRecordHostDossier(options: {
|
|
238
|
+
readonly host: string;
|
|
239
|
+
readonly sessionId: string;
|
|
240
|
+
readonly cwd: string;
|
|
241
|
+
readonly sessionDirectory: string;
|
|
242
|
+
readonly sessionParent: string;
|
|
243
|
+
readonly continuation: RoleTurnContinuation;
|
|
244
|
+
readonly model?: { readonly model?: string; readonly thinking?: string } | undefined;
|
|
245
|
+
readonly home?: string | undefined;
|
|
246
|
+
}): void {
|
|
247
|
+
if (options.host === "pi" || options.host === "hermes") {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const nativePath = resolveNativeSessionPath({
|
|
251
|
+
host: options.host,
|
|
252
|
+
sessionId: options.sessionId,
|
|
253
|
+
cwd: options.cwd,
|
|
254
|
+
...(options.home !== undefined ? { home: options.home } : {}),
|
|
255
|
+
});
|
|
256
|
+
if (nativePath === undefined && options.host !== "codex") return;
|
|
257
|
+
|
|
258
|
+
let destination: ReturnType<typeof resolveHostDossierLandingPath>;
|
|
259
|
+
try {
|
|
260
|
+
destination = resolveHostDossierLandingPath(options);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
sitianReportSafe({
|
|
263
|
+
level: "event",
|
|
264
|
+
kind: HOST_SESSION_RECORD_KIND,
|
|
265
|
+
host: options.host,
|
|
266
|
+
cwd: options.cwd,
|
|
267
|
+
sessionParent: options.sessionParent,
|
|
268
|
+
source: `${options.host}-dossier`,
|
|
269
|
+
payload: {
|
|
270
|
+
type: "native-session-warning",
|
|
271
|
+
sessionId: options.sessionId,
|
|
272
|
+
error: error instanceof Error ? error.message : String(error),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const { landingPath, ordinal, sanitizedModel } = destination;
|
|
278
|
+
|
|
279
|
+
let lastError: unknown;
|
|
280
|
+
let copySuccess = false;
|
|
281
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
282
|
+
try {
|
|
283
|
+
if (options.host === "grok-build") {
|
|
284
|
+
copyGrokDossier(nativePath!, landingPath);
|
|
285
|
+
} else {
|
|
286
|
+
if (nativePath === undefined || !existsSync(nativePath)) {
|
|
287
|
+
throw new Error(`Native session file missing for ${options.host} session ${options.sessionId}`);
|
|
288
|
+
}
|
|
289
|
+
mkdirSync(dirname(landingPath), { recursive: true });
|
|
290
|
+
copyFileCloneOrFallback(nativePath, landingPath);
|
|
291
|
+
}
|
|
292
|
+
copySuccess = true;
|
|
293
|
+
break;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
lastError = error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (copySuccess) {
|
|
300
|
+
sitianReportSafe({
|
|
301
|
+
level: "event",
|
|
302
|
+
kind: HOST_SESSION_RECORD_KIND,
|
|
303
|
+
host: options.host,
|
|
304
|
+
cwd: options.cwd,
|
|
305
|
+
sessionParent: options.sessionParent,
|
|
306
|
+
source: `${options.host}-dossier`,
|
|
307
|
+
payload: {
|
|
308
|
+
type: "native-session-copy",
|
|
309
|
+
nativePath,
|
|
310
|
+
landingPath,
|
|
311
|
+
ordinal,
|
|
312
|
+
sessionId: options.sessionId,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
// A partial first attempt must not masquerade as a complete native original.
|
|
317
|
+
try { rmSync(landingPath, { recursive: options.host === "grok-build", force: true }); }
|
|
318
|
+
catch { /* warning below reports the original copy failure */ }
|
|
319
|
+
// Reserve the failed ordinal outside the native dossier landing namespace.
|
|
320
|
+
try {
|
|
321
|
+
mkdirSync(options.sessionDirectory, { recursive: true });
|
|
322
|
+
closeSync(openSync(join(options.sessionDirectory, `${options.host}-${sanitizedModel}-${ordinal}.failed`), "a"));
|
|
323
|
+
} catch { /* the warning below retains the original copy failure */ }
|
|
324
|
+
sitianReportSafe({
|
|
325
|
+
level: "event",
|
|
326
|
+
kind: HOST_SESSION_RECORD_KIND,
|
|
327
|
+
host: options.host,
|
|
328
|
+
cwd: options.cwd,
|
|
329
|
+
sessionParent: options.sessionParent,
|
|
330
|
+
source: `${options.host}-dossier`,
|
|
331
|
+
payload: {
|
|
332
|
+
type: "native-session-warning",
|
|
333
|
+
nativePath,
|
|
334
|
+
landingPath,
|
|
335
|
+
ordinal,
|
|
336
|
+
sessionId: options.sessionId,
|
|
337
|
+
error: lastError instanceof Error ? lastError.message : String(lastError),
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
41
341
|
}
|
package/src/notary-role.ts
CHANGED
|
@@ -177,7 +177,6 @@ export function createNotaryRoleRuntime(
|
|
|
177
177
|
});
|
|
178
178
|
// Evidence assembly only (ADR 0018): soul + bound projection.
|
|
179
179
|
// Ticket value is envelope-admitted at activate — not re-read from flags here.
|
|
180
|
-
// #879 case dossier freeze→readingMaterial is envelope-owned (shared before_agent_start).
|
|
181
180
|
pi.on("before_agent_start", (event) => {
|
|
182
181
|
if (activation === undefined) {
|
|
183
182
|
throw new Error("符宝郎未激活");
|
package/src/pi/role-turn-host.ts
CHANGED
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
RoleTurnResult,
|
|
23
23
|
} from "../host-contracts.ts";
|
|
24
24
|
import { ExplicitInternalActivationError } from "../host-contracts.ts";
|
|
25
|
-
import { isOfficerReviewSeat } from "../packaged-role-registry.ts";
|
|
26
25
|
import { applyEngineChildEnv, ENGINE_MODEL_FLAG_NAME, normalizeEngineName } from "../engine-detour.ts";
|
|
27
26
|
import { projectActivationFlags } from "../role-activation-flags.ts";
|
|
28
27
|
import { encodeUserDialogueStdin } from "../user-dialogue-stdin.ts";
|
|
@@ -377,30 +376,7 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
377
376
|
|
|
378
377
|
return {
|
|
379
378
|
async executeTurn(request: RoleTurnRequest): Promise<RoleTurnResult> {
|
|
380
|
-
|
|
381
|
-
// Pi already owns its own session file, so only sitian prior volume rides in.
|
|
382
|
-
// #879: station-child officer dialogue keeps peer words — do not splice
|
|
383
|
-
// host-transition priorNativePaths into the review prompt body.
|
|
384
|
-
let turnRequest = request;
|
|
385
|
-
const officerStationChild =
|
|
386
|
-
request.stationChild === true && isOfficerReviewSeat(request.activation.role);
|
|
387
|
-
const paths =
|
|
388
|
-
!officerStationChild && request.hostTransition?.priorNativeKind === "sitian"
|
|
389
|
-
? request.hostTransition.priorNativePaths
|
|
390
|
-
: undefined;
|
|
391
|
-
if (
|
|
392
|
-
request.continuation.kind === "resume"
|
|
393
|
-
&& paths !== undefined
|
|
394
|
-
&& paths.length > 0
|
|
395
|
-
) {
|
|
396
|
-
turnRequest = {
|
|
397
|
-
...request,
|
|
398
|
-
continuation: {
|
|
399
|
-
...request.continuation,
|
|
400
|
-
prompt: `${request.continuation.prompt}\n${paths.join("\n")}`,
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
379
|
+
const turnRequest = request;
|
|
404
380
|
const roleEntry = await realpath(resolveInternalRoleEntrypoint(config.packageRoot));
|
|
405
381
|
const extraArgs = buildPiTurnExtraArgs(
|
|
406
382
|
turnRequest,
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* mechanical layer only verifies; countersign reuses that typed identity for bind
|
|
9
9
|
* (ADR 0075 / 0081). Code never matches instruction text against book-known
|
|
10
10
|
* numbers. Who may call 起居郎 and in what order is not written into law
|
|
11
|
-
* (ADR 0075 不规定谁调用起居郎、顺序归调用者).
|
|
12
|
-
*
|
|
13
|
-
* so freeze loads issue face (ADR 0075: 每次过庭都跑是调用者用法 /
|
|
11
|
+
* (ADR 0075 不规定谁调用起居郎、顺序归调用者). #1092: code no longer freezes or
|
|
12
|
+
* delivers 起居录 paths — roles locate records by ticket. Bound refresh hands the
|
|
13
|
+
* typed key to 起居郎 so freeze loads issue face (ADR 0075: 每次过庭都跑是调用者用法 /
|
|
14
|
+
* typed handoff).
|
|
14
15
|
*/
|
|
15
16
|
import type { DurablePrincipalAuthority } from "../host-contracts.ts";
|
|
16
17
|
import { projectCourtTicketNumbers } from "../diarist-contracts.ts";
|