@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.
@@ -35,11 +35,11 @@ function describeCaughtError(error) {
35
35
  function isStationChildOfficerDialogue(role, env) {
36
36
  return env.stationChild === true && isOfficerReviewSeat(role);
37
37
  }
38
- import { mintEngineDetourInvocationScope, readInvocationSelectedHost, withEngineDetourInvocationScope, } from "../engine-detour-usage.js";
39
- import { projectHostTransitionPriorNative } from "../host-transition-prior-native.js";
38
+ import { mintEngineDetourInvocationScope, withEngineDetourInvocationScope, } from "../engine-detour-usage.js";
40
39
  import { clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, } from "./run-lifecycle.js";
41
40
  import { processCancelDiagnostic, processCancelSignalName, } from "./process-cancel.js";
42
41
  import { homeFromRunDirectory } from "../activation-ledger-topology.js";
42
+ import { recordRunStart } from "../host-session-record.js";
43
43
  import { attemptProducedFreshSubmission, classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatErrorCauseDetail, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, publishedFailureErrorPath, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, settleHostEndedNoReceipt, attachRecordedSubmissions, rewritePublishedFailureErrorPath, } from "./settlement.js";
44
44
  import {} from "./terminal.js";
45
45
  import { ensureRealArtifactsDirectory, persistReturnedRunState, runWithAutoResumeLoop, TurnDispatchedFailure, } from "./auto-resume.js";
@@ -407,45 +407,11 @@ export async function dispatchPostAdmissionTurn(input) {
407
407
  try {
408
408
  // #987: do not pre-block or classify host dispatch from AK credential
409
409
  // catalog facts. Per-invocation runner/host evidence owns provider identity.
410
- // #617 DK-4: capture previous invocation host before markRunRunning overwrites it.
411
- // Single authority projectHostTransitionPriorNative classifies the prior native volume.
412
- // Same-run resume (#637) keeps host identity on the run's invocation page.
413
- let previousHost;
414
- const liveHost = env.host;
415
- const principalCoordinates = admitted.principal === undefined
416
- ? undefined
417
- : env.principalAuthority.decode(admitted.principal);
418
- let hostTransition;
419
- try {
420
- previousHost = readInvocationSelectedHost(admitted.runDirectory);
421
- hostTransition =
422
- previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
423
- ? await projectHostTransitionPriorNative({
424
- previousHost,
425
- liveHost,
426
- piSessionFile: principalCoordinates.sessionFile,
427
- })
428
- : undefined;
429
- }
430
- catch (error) {
431
- // prior-native IO is on the public one-shot path — controlled failure, not bare throw.
432
- return {
433
- ...(await presentControlledFailure(admitted, withEngineDetourInvocationScope({
434
- timedOut: false,
435
- code: null,
436
- stderr: "",
437
- thrown: error,
438
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState)),
439
- ...deferredPersist,
440
- };
441
- }
442
410
  await clearTypedProviderHttpObservation(admitted.runDirectory);
443
411
  // The authoritative host write (markRunRunning) is delayed to just before
444
412
  // executeTurn — not merely past beforeDispatch (#840 r9 判词 class 2). Any
445
- // pre-turn retry (beforeDispatch, dossier projection, continuation
446
- // assembly) must still see the prior invocation host on its next attempt;
447
- // committing env.host earlier would make readInvocationSelectedHost read back the
448
- // new host on the retry and silently drop hostTransition.
413
+ // pre-turn retry (beforeDispatch, continuation assembly) must not commit a host
414
+ // identity until the turn is ready.
449
415
  // Failures settle the run (presentControlledFailure); they must not leave it
450
416
  // permanently running. Call-local auto-resume retries this hook until it
451
417
  // succeeds; only an exhausted nested station child skips the parent loop
@@ -487,13 +453,10 @@ export async function dispatchPostAdmissionTurn(input) {
487
453
  if (env.stationChild !== undefined) {
488
454
  turnRequest = { ...turnRequest, stationChild: env.stationChild };
489
455
  }
490
- if (hostTransition !== undefined) {
491
- turnRequest = { ...turnRequest, hostTransition };
492
- }
493
456
  // Selected host axis rides the shared Host envelope for in-turn tools
494
457
  // (detour usage ledger) — never a pre-spawn invocation.json reread.
495
- if (typeof liveHost === "string" && liveHost.trim() !== "") {
496
- turnRequest = { ...turnRequest, host: liveHost.trim() };
458
+ if (typeof env.host === "string" && env.host.trim() !== "") {
459
+ turnRequest = { ...turnRequest, host: env.host.trim() };
497
460
  }
498
461
  // 0081 non-dialogue face: freeze pointer section under run/attachments/.
499
462
  // Seat consumes via loadCaseDossierReadingMaterial → existing agent-start
@@ -514,6 +477,7 @@ export async function dispatchPostAdmissionTurn(input) {
514
477
  // auto-resume re-enters this dispatch and must reuse the same scope.
515
478
  let result;
516
479
  try {
480
+ recordRunStart(admitted.runDirectory);
517
481
  result = await env.roleTurnHost.executeTurn(turnRequest);
518
482
  }
519
483
  catch (error) {
@@ -1,11 +1,5 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import {
3
- appendFileSync,
4
- existsSync,
5
- readFileSync,
6
- unlinkSync,
7
- writeFileSync
8
- } from "node:fs";
1
+ import { randomUUID } from "node:crypto";
2
+ import { appendFileSync } from "node:fs";
9
3
  import { dirname, join } from "node:path";
10
4
  import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
11
5
  import {
@@ -19,48 +13,12 @@ import {
19
13
  import {
20
14
  SitianInfrastructureError
21
15
  } from "./sitian-contracts.js";
22
- function isRecord(value) {
23
- return typeof value === "object" && value !== null && !Array.isArray(value);
24
- }
25
- function errorCodeOf(error) {
26
- return error.code;
27
- }
28
- function identityClaimPath(recordFile, identity) {
29
- const digest = createHash("sha256").update(identity, "utf8").digest("hex");
30
- return `${recordFile}.id-${digest}`;
31
- }
32
- function createExclusiveFile(path, contents) {
33
- writeFileSync(path, contents, { encoding: "utf8", flag: "wx" });
34
- }
35
- function sealTornTail(recordFile) {
36
- if (!existsSync(recordFile)) return;
37
- const buffer = readFileSync(recordFile);
38
- if (buffer.length > 0 && buffer[buffer.length - 1] !== 10) {
39
- appendFileSync(recordFile, "\n", "utf8");
40
- }
41
- }
42
16
  function appendSitianRecordBlock(input, block) {
43
17
  if (block === "") return;
44
18
  try {
45
19
  const { sessionDir, recordFile, ledgerHome } = resolveSitianRecordPath(input);
46
20
  ensureRealDirectoryTree(ledgerHome, sessionDir);
47
- appendFileSync(recordFile, "", "utf8");
48
- sealTornTail(recordFile);
49
- const identityOf = (line) => {
50
- try {
51
- const row = JSON.parse(line);
52
- return isRecord(row) && typeof row.identity === "string" ? row.identity : void 0;
53
- } catch {
54
- return void 0;
55
- }
56
- };
57
- const identities = new Set(readFileSync(recordFile, "utf8").split("\n").map(identityOf).filter((id) => id !== void 0));
58
- for (const line of block.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
59
- const identity = identityOf(line);
60
- if (identity !== void 0 && identities.has(identity)) continue;
61
- appendFileSync(recordFile, line, "utf8");
62
- if (identity !== void 0) identities.add(identity);
63
- }
21
+ appendFileSync(recordFile, block, "utf8");
64
22
  } catch (error) {
65
23
  if (error instanceof SitianInfrastructureError) throw error;
66
24
  throw new SitianInfrastructureError(
@@ -69,106 +27,6 @@ function appendSitianRecordBlock(input, block) {
69
27
  );
70
28
  }
71
29
  }
72
- function findIdentityPointer(recordFile, identity, kind, level) {
73
- if (!existsSync(recordFile)) return void 0;
74
- const text = readFileSync(recordFile, "utf8");
75
- for (const line of text.split("\n")) {
76
- const trimmed = line.trim();
77
- if (!trimmed) continue;
78
- try {
79
- const parsed = JSON.parse(trimmed);
80
- if (isRecord(parsed) && parsed.identity === identity) {
81
- return { identity, recordFile, kind, level };
82
- }
83
- } catch {
84
- }
85
- }
86
- return void 0;
87
- }
88
- function unlinkAbsentOk(path) {
89
- try {
90
- unlinkSync(path);
91
- return void 0;
92
- } catch (error) {
93
- if (errorCodeOf(error) === "ENOENT") return void 0;
94
- return error instanceof Error ? error : new Error(errorText(error));
95
- }
96
- }
97
- function appendWithIdentityClaim(recordFile, record, row) {
98
- sealTornTail(recordFile);
99
- const claimPath = identityClaimPath(recordFile, record.identity);
100
- const existing = findIdentityPointer(
101
- recordFile,
102
- record.identity,
103
- record.kind,
104
- record.level
105
- );
106
- if (existing !== void 0) {
107
- unlinkAbsentOk(claimPath);
108
- return existing;
109
- }
110
- try {
111
- createExclusiveFile(claimPath, "");
112
- } catch (error) {
113
- if (errorCodeOf(error) !== "EEXIST") throw error;
114
- sealTornTail(recordFile);
115
- const published = findIdentityPointer(
116
- recordFile,
117
- record.identity,
118
- record.kind,
119
- record.level
120
- );
121
- if (published !== void 0) {
122
- unlinkAbsentOk(claimPath);
123
- return published;
124
- }
125
- throw new SitianInfrastructureError(
126
- `Sitian identity claim at ${claimPath} already exists for identity ${record.identity}`,
127
- { cause: error }
128
- );
129
- }
130
- let primaryFailure;
131
- let result;
132
- let cleanupFailure;
133
- try {
134
- sealTornTail(recordFile);
135
- const raced = findIdentityPointer(
136
- recordFile,
137
- record.identity,
138
- record.kind,
139
- record.level
140
- );
141
- if (raced !== void 0) {
142
- result = raced;
143
- } else {
144
- appendFileSync(recordFile, row, "utf8");
145
- result = {
146
- identity: record.identity,
147
- recordFile,
148
- kind: record.kind,
149
- level: record.level
150
- };
151
- }
152
- } catch (error) {
153
- primaryFailure = error;
154
- } finally {
155
- cleanupFailure = unlinkAbsentOk(claimPath);
156
- }
157
- if (primaryFailure !== void 0) {
158
- if (primaryFailure instanceof SitianInfrastructureError) throw primaryFailure;
159
- throw new SitianInfrastructureError(
160
- `Sitian appender persistence failure: ${errorText(primaryFailure)}`,
161
- { cause: primaryFailure }
162
- );
163
- }
164
- if (cleanupFailure !== void 0) {
165
- throw new SitianInfrastructureError(
166
- `Sitian identity claim at ${claimPath} could not be released after append for identity ${record.identity}: ${errorText(cleanupFailure)}`,
167
- { cause: cleanupFailure }
168
- );
169
- }
170
- return result;
171
- }
172
30
  const S4_SUBMISSION_LEDGER_KINDS = /* @__PURE__ */ new Set([
173
31
  "candidate",
174
32
  "roundContext",
@@ -249,7 +107,13 @@ function appendSitianRecord(input) {
249
107
  };
250
108
  const row = `${JSON.stringify(record)}
251
109
  `;
252
- return appendWithIdentityClaim(recordFile, record, row);
110
+ appendFileSync(recordFile, row, "utf8");
111
+ return {
112
+ identity: record.identity,
113
+ recordFile,
114
+ kind: record.kind,
115
+ level: record.level
116
+ };
253
117
  } catch (error) {
254
118
  if (error instanceof SitianInfrastructureError) throw error;
255
119
  throw new SitianInfrastructureError(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5216",
3
+ "version": "0.1.5235",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,3 +1,4 @@
1
+ import { join } from "node:path";
1
2
  import { spawn } from "node:child_process";
2
3
  import { createInterface } from "node:readline";
3
4
 
@@ -8,7 +9,10 @@ import {
8
9
  raceAgainstHostAbort,
9
10
  } from "../external-host-turn-loop.ts";
10
11
 
11
- import { reportHostSessionEvent } from "../host-session-record.ts";
12
+ import {
13
+ copyAndRecordHostDossier,
14
+ recordNativeSessionPointer,
15
+ } from "../host-session-record.ts";
12
16
  import {
13
17
  NAVIGATOR_OUTPUT_TOOL_NAME,
14
18
  navigatorProseFromUnknown,
@@ -20,30 +24,6 @@ import {
20
24
  } from "../prepared-role-turn.ts";
21
25
  import { acpModelId, type AcpHostDescription } from "./description.ts";
22
26
 
23
- /** #971: authorized host for usage + auto_compact ledgering only. */
24
- const GROK_BUILD_HOST = "grok-build";
25
-
26
- /** #971: grok-build prompt result carries host usage under `_meta.usage`. */
27
- function acpPromptResultHasUsage(result: Readonly<Record<string, unknown>>): boolean {
28
- const meta = result._meta;
29
- if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return false;
30
- return Object.prototype.hasOwnProperty.call(meta, "usage");
31
- }
32
-
33
- /**
34
- * #971: ledger only the authorized vendor compaction notice on grok-build.
35
- * Other `_x.ai/*` notifications stay out of host-session records.
36
- */
37
- function isAcpAutoCompactCompletedNotification(
38
- method: string,
39
- params: Readonly<Record<string, unknown>>,
40
- ): boolean {
41
- if (method !== "_x.ai/session_notification") return false;
42
- const update = params.update;
43
- if (typeof update !== "object" || update === null || Array.isArray(update)) return false;
44
- return (update as { sessionUpdate?: unknown }).sessionUpdate === "auto_compact_completed";
45
- }
46
-
47
27
  /**
48
28
  * #959: collect free-form agent text from ACP session/update stream.
49
29
  * Used only when the navigator seat spoke prose without calling the output tool.
@@ -278,7 +258,9 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
278
258
  const systemPromptOverride = renderSystemPromptOverride(prepared.systemPrompt);
279
259
  let connection: AcpConnection | undefined;
280
260
  let sessionId: string | undefined;
261
+ let sessionOpened = false;
281
262
  let accepted = false;
263
+ const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
282
264
  // Mutable so dispose failure can outrank a clean turn (headless withCleanupFailure face).
283
265
  let outcome: RoleTurnResult = failure("session", "AcpNoOutcome", "no-outcome");
284
266
  try {
@@ -286,66 +268,26 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
286
268
  outcome = failure("activation", "UncontrolledAcpSession", "ak-config-missing");
287
269
  } else {
288
270
  connection = await config.connect(request);
289
- // Live host-session records: ACP session/update → sitian sole entry (#811).
290
- // One abort + one race helper + one outer projection — write failure ends
291
- // any in-flight RPC as typed session infrastructure failure.
292
- const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
293
- const recordAbort = new AbortController();
294
- let hostSessionRecordFailure: RoleTurnKnownFailure | undefined;
295
- const hostSessionRecordResult = (): RoleTurnResult => ({
296
- code: null,
297
- stderr: "",
298
- timedOut: false,
299
- knownFailure: hostSessionRecordFailure!,
300
- });
301
- const noteHostSessionRecordFailure = (error: unknown): void => {
302
- if (hostSessionRecordFailure !== undefined) return;
303
- hostSessionRecordFailure = {
304
- cause: "session",
305
- identity: { name: "HostSessionRecordFailure", code: "host-session-record-failed" },
306
- diagnostic: error instanceof Error ? error.message : String(error),
307
- };
308
- try { recordAbort.abort(); } catch { /* already aborted */ }
309
- };
310
271
  const rpc = (
311
272
  method: string,
312
273
  params: Readonly<Record<string, unknown>>,
313
- ): Promise<Readonly<Record<string, unknown>>> =>
314
- raceAgainstHostAbort(connection!.request(method, params), recordAbort.signal, "host-session-record-failed");
274
+ ): Promise<Readonly<Record<string, unknown>>> => connection!.request(method, params);
315
275
 
316
276
  // #959: navigator free-form agent text — only while session/prompt is in flight.
317
277
  // session/load replays history via session/update; those must not enter the bucket
318
278
  // (resume / set_model load would otherwise prepend prior turns as "this turn" prose).
319
279
  const agentProseChunks: string[] = [];
320
280
  let collectAgentProse = false;
321
- // #971: usage + auto_compact are grok-build-only; same ACP adapter serves Hermes too.
322
- const ledgerGrokBuildObservability = config.hostName === GROK_BUILD_HOST;
323
281
  connection.onNotification?.((method, params) => {
324
- if (hostSessionRecordFailure !== undefined) return;
325
282
  const isSessionUpdate = method === "session/update";
326
- const isAutoCompact = ledgerGrokBuildObservability
327
- && isAcpAutoCompactCompletedNotification(method, params);
328
- // #971: session/update stays; auto_compact_completed is the only vendor notice.
329
- if (!isSessionUpdate && !isAutoCompact) return;
330
- if (isSessionUpdate && collectAgentProse) {
283
+ if (!isSessionUpdate) return;
284
+ if (collectAgentProse) {
331
285
  const chunk = acpAgentTextChunk(params);
332
286
  if (chunk !== undefined) agentProseChunks.push(chunk);
333
287
  }
334
- try {
335
- reportHostSessionEvent({
336
- host: config.hostName,
337
- cwd: request.cwd,
338
- sessionParent,
339
- source: "acp-host",
340
- event: { method, params },
341
- });
342
- } catch (error) {
343
- noteHostSessionRecordFailure(error);
344
- }
345
288
  });
346
289
 
347
- try {
348
- const initialized = await rpc("initialize", {
290
+ const initialized = await rpc("initialize", {
349
291
  protocolVersion: 1,
350
292
  clientCapabilities: {},
351
293
  });
@@ -374,9 +316,13 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
374
316
  sessionId: bindSessionId,
375
317
  ...sessionBindParams,
376
318
  });
377
- return typeof loaded.sessionId === "string" && loaded.sessionId !== ""
319
+ const loadedId = typeof loaded.sessionId === "string" && loaded.sessionId !== ""
378
320
  ? loaded.sessionId
379
321
  : bindSessionId;
322
+ if (config.hostName !== "hermes") recordNativeSessionPointer({
323
+ host: config.hostName, sessionId: loadedId, cwd: request.cwd, sessionParent, home: request.home,
324
+ });
325
+ return loadedId;
380
326
  };
381
327
  if (request.continuation.kind === "resume" && config.boundResume === "session/load") {
382
328
  const explicitHostSessionId = request.continuation.hostSessionId;
@@ -384,7 +330,9 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
384
330
  ? explicitHostSessionId
385
331
  : await config.sessionIdentity.load(request.principal);
386
332
  if (boundSessionId !== undefined && boundSessionId !== "") {
333
+ sessionId = boundSessionId;
387
334
  sessionId = await loadSession(boundSessionId);
335
+ sessionOpened = true;
388
336
  }
389
337
  }
390
338
  let sessionReady = true;
@@ -395,6 +343,10 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
395
343
  outcome = failure("session", "AcpSessionFailure", "session-id-missing");
396
344
  sessionReady = false;
397
345
  } else {
346
+ if (config.hostName !== "hermes") recordNativeSessionPointer({
347
+ host: config.hostName, sessionId, cwd: request.cwd, sessionParent, home: request.home,
348
+ });
349
+ sessionOpened = true;
398
350
  await config.sessionIdentity.bind(request.principal, sessionId);
399
351
  }
400
352
  }
@@ -415,12 +367,6 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
415
367
  roundLimitName: "AcpRoundLimit",
416
368
  currentSessionId: () => sessionId,
417
369
  async runRound({ prompt, abortSignal }) {
418
- if (hostSessionRecordFailure !== undefined) return { status: "terminal", result: hostSessionRecordResult() };
419
- // Envelope infra abort, parent cancellation (#675), and host-session
420
- // record abort (#811) share one race face for in-flight prompt.
421
- const abortParts: AbortSignal[] = [recordAbort.signal];
422
- if (abortSignal !== undefined) abortParts.push(abortSignal);
423
- const combinedAbort = AbortSignal.any(abortParts);
424
370
  let result: Readonly<Record<string, unknown>>;
425
371
  // Open the prose gate only for this prompt round; clear any stale chunks first.
426
372
  agentProseChunks.length = 0;
@@ -431,36 +377,15 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
431
377
  sessionId: activeSessionId,
432
378
  prompt: [{ type: "text", text: prompt }],
433
379
  }),
434
- combinedAbort,
380
+ abortSignal,
435
381
  "ACP host aborted",
436
382
  );
437
383
  } catch (error) {
438
384
  collectAgentProse = false;
439
385
  agentProseChunks.length = 0;
440
- if (hostSessionRecordFailure !== undefined) {
441
- return { status: "terminal", result: hostSessionRecordResult() };
442
- }
443
386
  throw error;
444
387
  }
445
388
  collectAgentProse = false;
446
- // #971: per-round grok-build usage is on the prompt JSON-RPC result, not a notice.
447
- if (ledgerGrokBuildObservability && acpPromptResultHasUsage(result)) {
448
- try {
449
- reportHostSessionEvent({
450
- host: config.hostName,
451
- cwd: request.cwd,
452
- sessionParent,
453
- source: "acp-host",
454
- event: { method: "session/prompt", result },
455
- });
456
- } catch (error) {
457
- noteHostSessionRecordFailure(error);
458
- return { status: "terminal", result: hostSessionRecordResult() };
459
- }
460
- }
461
- if (hostSessionRecordFailure !== undefined) {
462
- return { status: "terminal", result: hostSessionRecordResult() };
463
- }
464
389
  if (result.stopReason === "refusal") {
465
390
  agentProseChunks.length = 0;
466
391
  return {
@@ -487,18 +412,9 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
487
412
  accepted = true;
488
413
  },
489
414
  });
490
- outcome = hostSessionRecordFailure !== undefined ? hostSessionRecordResult() : turnResult;
415
+ outcome = turnResult;
491
416
  } // sessionReady
492
417
  } // model match else
493
- } catch (error) {
494
- // recordAbort races setup RPCs via raceAgainstHostAbort (host-aborted code);
495
- // noteHostSessionRecordFailure always sets the typed failure before aborting.
496
- if (hostSessionRecordFailure !== undefined) {
497
- outcome = hostSessionRecordResult();
498
- } else {
499
- throw error;
500
- }
501
- }
502
418
  } // mcpServers else
503
419
  } finally {
504
420
  if (connection !== undefined) {
@@ -509,6 +425,18 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
509
425
  try { await connection.close(); }
510
426
  catch { /* keep turn result */ }
511
427
  }
428
+ if (config.hostName !== "hermes" && sessionOpened && sessionId !== undefined) {
429
+ copyAndRecordHostDossier({
430
+ host: config.hostName,
431
+ sessionId,
432
+ cwd: request.cwd,
433
+ sessionDirectory: join(request.runDirectory, "session"),
434
+ sessionParent,
435
+ continuation: request.continuation,
436
+ ...(request.model !== undefined ? { model: request.model } : {}),
437
+ ...(request.home !== undefined ? { home: request.home } : {}),
438
+ });
439
+ }
512
440
  try {
513
441
  await prepared.dispose?.();
514
442
  } catch (cleanupError) {
@@ -222,7 +222,7 @@ export async function readEngineDetourToolUsage(input: {
222
222
  });
223
223
 
224
224
  const { records } = await readSitianRecords(recordFile);
225
- const calls: EngineDetourCallFact[] = [];
225
+ const callsByToolCallId = new Map<string, EngineDetourCallFact>();
226
226
  for (const record of records) {
227
227
  if (record.kind !== ENGINE_DETOUR_CALL_KIND) continue;
228
228
  const boundScope = invocationScopeIdOfRecord(record);
@@ -240,9 +240,11 @@ export async function readEngineDetourToolUsage(input: {
240
240
  };
241
241
  const fact = callFactFromSitianPayload(record.payload, pointer);
242
242
  if (fact === undefined) continue;
243
- calls.push(fact);
243
+ // ADR 0086: Reader deduplicates by toolCallId within the invocation scope
244
+ callsByToolCallId.set(fact.toolCallId, fact);
244
245
  }
245
246
 
247
+ const calls = Array.from(callsByToolCallId.values());
246
248
  return { callCount: calls.length, calls };
247
249
  }
248
250
 
@@ -300,20 +302,6 @@ export async function readInvocationEngineMounted(
300
302
  return typeof raw.engine === "string" && raw.engine.trim() !== "";
301
303
  }
302
304
 
303
- /**
304
- * Selected host from the admission invocation page — for post-admission
305
- * host-transition classification only. In-turn tools take host from the shared
306
- * Host envelope (RoleTurnRequest / HostContext), never this reader.
307
- * Undefined when absent — callers must not invent "pi".
308
- */
309
- export function readInvocationSelectedHost(runDirectory: string): string | undefined {
310
- const raw = readInvocationRecord(runDirectory);
311
- if (raw === undefined) return undefined;
312
- return typeof raw.host === "string" && raw.host.trim() !== ""
313
- ? raw.host.trim()
314
- : undefined;
315
- }
316
-
317
305
  /** Merge usage into decisiveFacts without touching role payloads. */
318
306
  export function withEngineDetourToolUsageFact<
319
307
  T extends { readonly decisiveFacts?: Readonly<Record<string, unknown>> },
@@ -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