@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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Middle external-host role-turn loop (#820 / ADR 0082).
3
- * One copy: serial, prior-native, abort merge/race, 8-round closeRound retry,
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 = promptWithPriorNativePaths(prepared.prompt, request);
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 { reportHostSessionEvent } from "../host-session-record.ts";
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
- settled = true;
346
- if (timer !== undefined) clearTimeout(timer);
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) => settle(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
- reportHostSessionEvent({
558
- host: config.hostName,
559
- cwd: request.cwd,
560
- sessionParent,
561
- source: "headless-host",
562
- event,
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?.();
@@ -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
- * Non-pi host turn events → sitian sole entry (ADR 0065 / 0077 / #811).
3
- * Adapters capture host-native structured events; this helper only packages
4
- * the common sitianReport call. session.jsonl stays header-only (#617 DK-4).
5
- * Write failures propagate (SitianInfrastructureError) — not best-effort.
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 eventField(event: unknown, key: string): string | undefined {
13
- if (typeof event !== "object" || event === null || Array.isArray(event)) return undefined;
14
- const value = (event as Record<string, unknown>)[key];
15
- return typeof value === "string" && value.length > 0 ? value : undefined;
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
- /** Live write of one host-emitted structured event. Throws on sitian persistence failure. */
19
- export function reportHostSessionEvent(input: {
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 source: string;
24
- readonly event: unknown;
25
- readonly identity?: string;
26
- readonly timestamp?: string;
27
- }): void {
28
- const identity = input.identity ?? eventField(input.event, "uuid");
29
- const timestamp = input.timestamp ?? eventField(input.event, "timestamp");
30
- sitianReport({
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: input.host,
34
- cwd: input.cwd,
35
- sessionParent: input.sessionParent,
36
- source: input.source,
37
- payload: input.event,
38
- ...(identity === undefined ? {} : { identity }),
39
- ...(timestamp === undefined ? {} : { timestamp }),
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
  }
@@ -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("符宝郎未激活");
@@ -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
- // #617 DK-7: Pi argv gets projected native paths once; never record bytes.
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 不规定谁调用起居郎、顺序归调用者). 起居录 path delivery is owned once by
12
- * post-admission (#709 / ADR 0081). Bound refresh hands the typed key to 起居郎
13
- * so freeze loads issue face (ADR 0075: 每次过庭都跑是调用者用法 / typed handoff).
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";