@akagilnc/pi-workflow-roles 0.1.5212 → 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 +580 -1031
- package/dist/engine-detour-usage.js +4 -16
- package/dist/headless-host/production-host.js +701 -1066
- package/dist/host-session-record.js +303 -0
- package/dist/migrate-book-topology.js +0 -1
- package/dist/pi/role-turn-host.js +1 -21
- package/dist/public-cli/main.js +153 -362
- package/dist/public-cli/post-admission.js +7 -43
- package/dist/sitian-appender.js +10 -146
- package/dist/ticket-provenance-contracts.js +4 -42
- package/dist/ticket-provenance.js +27 -456
- package/package.json +1 -1
- package/src/acp-host/role-turn-host.ts +36 -108
- package/src/diarist.ts +5 -5
- 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/src/ticket-provenance-contracts.ts +10 -48
- package/src/ticket-provenance.ts +33 -499
- 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
|
@@ -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,
|
|
@@ -69,10 +69,8 @@ function isStationChildOfficerDialogue(
|
|
|
69
69
|
}
|
|
70
70
|
import {
|
|
71
71
|
mintEngineDetourInvocationScope,
|
|
72
|
-
readInvocationSelectedHost,
|
|
73
72
|
withEngineDetourInvocationScope,
|
|
74
73
|
} from "../engine-detour-usage.ts";
|
|
75
|
-
import { projectHostTransitionPriorNative } from "../host-transition-prior-native.ts";
|
|
76
74
|
import type { CredentialProviders, SeatModelConfig } from "./config.ts";
|
|
77
75
|
import {
|
|
78
76
|
clearCurrentCourt,
|
|
@@ -92,6 +90,7 @@ import {
|
|
|
92
90
|
type CatchableProcessSignal,
|
|
93
91
|
} from "./process-cancel.ts";
|
|
94
92
|
import { homeFromRunDirectory } from "../activation-ledger-topology.ts";
|
|
93
|
+
import { recordRunStart } from "../host-session-record.ts";
|
|
95
94
|
import {
|
|
96
95
|
attemptProducedFreshSubmission,
|
|
97
96
|
classifyPostAdmissionFailure,
|
|
@@ -808,53 +807,11 @@ export async function dispatchPostAdmissionTurn<
|
|
|
808
807
|
try {
|
|
809
808
|
// #987: do not pre-block or classify host dispatch from AK credential
|
|
810
809
|
// catalog facts. Per-invocation runner/host evidence owns provider identity.
|
|
811
|
-
// #617 DK-4: capture previous invocation host before markRunRunning overwrites it.
|
|
812
|
-
// Single authority projectHostTransitionPriorNative classifies the prior native volume.
|
|
813
|
-
// Same-run resume (#637) keeps host identity on the run's invocation page.
|
|
814
|
-
let previousHost: string | undefined;
|
|
815
|
-
const liveHost = env.host;
|
|
816
|
-
const principalCoordinates =
|
|
817
|
-
admitted.principal === undefined
|
|
818
|
-
? undefined
|
|
819
|
-
: env.principalAuthority.decode(admitted.principal);
|
|
820
|
-
let hostTransition: RoleTurnRequest["hostTransition"];
|
|
821
|
-
try {
|
|
822
|
-
previousHost = readInvocationSelectedHost(admitted.runDirectory);
|
|
823
|
-
hostTransition =
|
|
824
|
-
previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
|
|
825
|
-
? await projectHostTransitionPriorNative({
|
|
826
|
-
previousHost,
|
|
827
|
-
liveHost,
|
|
828
|
-
piSessionFile: principalCoordinates.sessionFile,
|
|
829
|
-
})
|
|
830
|
-
: undefined;
|
|
831
|
-
} catch (error) {
|
|
832
|
-
// prior-native IO is on the public one-shot path — controlled failure, not bare throw.
|
|
833
|
-
return {
|
|
834
|
-
...(await presentControlledFailure(
|
|
835
|
-
admitted,
|
|
836
|
-
withEngineDetourInvocationScope({
|
|
837
|
-
timedOut: false,
|
|
838
|
-
code: null,
|
|
839
|
-
stderr: "",
|
|
840
|
-
thrown: error,
|
|
841
|
-
}, request.invocationScopeId),
|
|
842
|
-
adapters,
|
|
843
|
-
env.principalAuthority,
|
|
844
|
-
io,
|
|
845
|
-
persistRunState,
|
|
846
|
-
)) as { exitCode: number; admitted: A; terminal: T },
|
|
847
|
-
...deferredPersist,
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
|
|
851
810
|
await clearTypedProviderHttpObservation(admitted.runDirectory);
|
|
852
811
|
// The authoritative host write (markRunRunning) is delayed to just before
|
|
853
812
|
// executeTurn — not merely past beforeDispatch (#840 r9 判词 class 2). Any
|
|
854
|
-
// pre-turn retry (beforeDispatch,
|
|
855
|
-
//
|
|
856
|
-
// committing env.host earlier would make readInvocationSelectedHost read back the
|
|
857
|
-
// new host on the retry and silently drop hostTransition.
|
|
813
|
+
// pre-turn retry (beforeDispatch, continuation assembly) must not commit a host
|
|
814
|
+
// identity until the turn is ready.
|
|
858
815
|
// Failures settle the run (presentControlledFailure); they must not leave it
|
|
859
816
|
// permanently running. Call-local auto-resume retries this hook until it
|
|
860
817
|
// succeeds; only an exhausted nested station child skips the parent loop
|
|
@@ -903,13 +860,10 @@ export async function dispatchPostAdmissionTurn<
|
|
|
903
860
|
if (env.stationChild !== undefined) {
|
|
904
861
|
turnRequest = { ...turnRequest, stationChild: env.stationChild };
|
|
905
862
|
}
|
|
906
|
-
if (hostTransition !== undefined) {
|
|
907
|
-
turnRequest = { ...turnRequest, hostTransition };
|
|
908
|
-
}
|
|
909
863
|
// Selected host axis rides the shared Host envelope for in-turn tools
|
|
910
864
|
// (detour usage ledger) — never a pre-spawn invocation.json reread.
|
|
911
|
-
if (typeof
|
|
912
|
-
turnRequest = { ...turnRequest, host:
|
|
865
|
+
if (typeof env.host === "string" && env.host.trim() !== "") {
|
|
866
|
+
turnRequest = { ...turnRequest, host: env.host.trim() };
|
|
913
867
|
}
|
|
914
868
|
// 0081 non-dialogue face: freeze pointer section under run/attachments/.
|
|
915
869
|
// Seat consumes via loadCaseDossierReadingMaterial → existing agent-start
|
|
@@ -939,6 +893,7 @@ export async function dispatchPostAdmissionTurn<
|
|
|
939
893
|
|
|
940
894
|
let result: RoleTurnResult;
|
|
941
895
|
try {
|
|
896
|
+
recordRunStart(admitted.runDirectory);
|
|
942
897
|
result = await env.roleTurnHost.executeTurn(turnRequest);
|
|
943
898
|
} catch (error) {
|
|
944
899
|
const processCancelName = processCancelSignalName(env.signal);
|
|
@@ -796,8 +796,7 @@ export async function summonGateOfficer(options: {
|
|
|
796
796
|
// Hard path resolve: fail loud — never fall through to packageMachineHome (#604 / #675).
|
|
797
797
|
home = homeFromRunDirectory(options.sourceRunDirectory);
|
|
798
798
|
}
|
|
799
|
-
// Officer host is seat-owned only (#821)
|
|
800
|
-
// hostTransition consumer in post-admission — not an officer override channel.
|
|
799
|
+
// Officer host is seat-owned only (#821), not a parent override channel.
|
|
801
800
|
// #879: binding pointer = parent run directory; dialogue content = submission body.
|
|
802
801
|
// Conclusion re-ask keeps sole ownership of reviewReask when present.
|
|
803
802
|
let gateReviewInstruction: string | undefined;
|