@akagilnc/pi-workflow-roles 0.1.5216 → 0.1.5235
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/dist/acp-host/production-host.js +556 -617
- package/dist/engine-detour-usage.js +4 -16
- package/dist/headless-host/production-host.js +673 -648
- package/dist/host-session-record.js +303 -0
- package/dist/pi/role-turn-host.js +1 -21
- package/dist/public-cli/main.js +153 -361
- package/dist/public-cli/post-admission.js +7 -43
- package/dist/sitian-appender.js +10 -146
- 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/pi/role-turn-host.ts +1 -25
- package/src/public-cli/post-admission.ts +6 -51
- package/src/public-role-summons.ts +1 -2
- package/src/sitian-appender.ts +16 -191
- package/dist/host-transition-prior-native.js +0 -78
- package/src/host-transition-prior-native.ts +0 -80
- package/src/pi/pi-normalization.ts +0 -291
|
@@ -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/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,
|