@akagilnc/pi-workflow-roles 0.1.3902 → 0.1.3936

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.
Files changed (34) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +4 -4
  3. package/dist/acp-host/production-host.js +2540 -1994
  4. package/dist/compliance-transport.js +4 -1
  5. package/dist/headless-host/production-host.js +2274 -1728
  6. package/dist/prepared-role-turn.js +5 -0
  7. package/dist/public-cli/auto-resume.js +115 -13
  8. package/dist/public-cli/diarist-run.js +154 -0
  9. package/dist/public-cli/main.js +7082 -5854
  10. package/dist/public-cli/option-definitions.js +1 -1
  11. package/dist/public-cli/post-admission.js +455 -188
  12. package/dist/public-cli/role-turn-host-resolution.js +59 -0
  13. package/dist/public-cli/run-lifecycle.js +11 -6
  14. package/dist/public-cli/turn-request.js +1 -0
  15. package/dist/public-role-summons.js +48 -42
  16. package/dist/role-activation-flags.js +3 -0
  17. package/dist/role-runtime.js +14 -3
  18. package/dist/session-identity.js +58 -0
  19. package/package.json +1 -1
  20. package/src/compliance-transport.ts +6 -1
  21. package/src/host-contracts.ts +2 -0
  22. package/src/public-cli/auto-resume.ts +157 -11
  23. package/src/public-cli/cli.ts +25 -85
  24. package/src/public-cli/countersign-run.ts +26 -33
  25. package/src/public-cli/option-definitions.ts +1 -1
  26. package/src/public-cli/post-admission.ts +514 -159
  27. package/src/public-cli/reviewer-run.ts +0 -2
  28. package/src/public-cli/role-turn-host-resolution.ts +122 -0
  29. package/src/public-cli/run-lifecycle.ts +15 -10
  30. package/src/public-cli/turn-request.ts +3 -0
  31. package/src/public-role-summons.ts +71 -54
  32. package/src/role-activation-flags.ts +3 -0
  33. package/src/role-runtime.ts +14 -2
  34. package/src/session-identity.ts +34 -0
@@ -29,7 +29,6 @@ import {
29
29
  buildResumeContinuationPrompt,
30
30
  type PublicResumeRequest,
31
31
  } from "./run-lifecycle.ts";
32
- import { clearReviewerDispatchRejection } from "./reviewer-dispatch-rejection.ts";
33
32
  import {
34
33
  presentStructuralRejection,
35
34
  readEngineDetourInfrastructureFailure,
@@ -84,7 +83,6 @@ function reviewerAdapters(
84
83
  methodMaterial?: PackagedMethodSkillMaterial,
85
84
  ): PostAdmissionAdapters<AdmittedReviewerInvocation> {
86
85
  return {
87
- beforeDispatch: (admitted) => clearReviewerDispatchRejection(admitted.runDirectory),
88
86
  trySettle: (admitted, authority, scope) =>
89
87
  methodMaterial === undefined
90
88
  ? Promise.resolve(undefined)
@@ -0,0 +1,122 @@
1
+ /**
2
+ * One host-adapter table and one seat-keyed selection (#617 / ADR 0082).
3
+ * Public CLI composition root and nested public summons share this seam —
4
+ * the selected parent adapter is never a child-host override.
5
+ */
6
+ import type { DurablePrincipalAuthority, RoleTurnHost } from "../host-contracts.ts";
7
+ import { packagedExternalHostNames } from "../host-descriptions.ts";
8
+ import { createPiRoleTurnHost } from "../pi/role-turn-host.ts";
9
+ import type { EffectiveSeat } from "./config.ts";
10
+ import {
11
+ observeLaunchedRolePackageIdentity,
12
+ recordLaunchedPiIdentity,
13
+ recordLaunchedRolePackageIdentity,
14
+ } from "./invocation.ts";
15
+ import { createLazyProductionExternalHost } from "./load-production-external-host.ts";
16
+ import type { PublicCallableRole } from "./registry.ts";
17
+
18
+ export type HostSelectionFailure = {
19
+ readonly kind: "host-unregistered" | "host-model-mismatch";
20
+ readonly host: string;
21
+ readonly seat: PublicCallableRole;
22
+ readonly model: string;
23
+ readonly registeredHosts: readonly string[];
24
+ };
25
+
26
+ export function formatHostSelectionFailure(failure: HostSelectionFailure): string {
27
+ return `${failure.kind}: ${failure.host}; seat=${failure.seat}; model=${failure.model}; registered: ${failure.registeredHosts.join(", ")}`;
28
+ }
29
+
30
+ export type NamedRoleTurnHostAdapter = {
31
+ readonly name: string;
32
+ readonly create: (input: { role: PublicCallableRole; model: EffectiveSeat["selection"] }) =>
33
+ | { readonly ok: true; readonly host: RoleTurnHost }
34
+ | { readonly ok: false };
35
+ };
36
+
37
+ export class HostSelectionError extends Error {
38
+ constructor(readonly failure: HostSelectionFailure) {
39
+ super(formatHostSelectionFailure(failure));
40
+ this.name = "HostSelectionError";
41
+ }
42
+ }
43
+
44
+ export type RoleTurnHostResolutionInput = {
45
+ /** Pi-adapter inject (tests). Not a selected-host override. */
46
+ readonly roleTurnHost?: RoleTurnHost;
47
+ /** Composition-root adapter table. When omitted, pi + packaged externals. */
48
+ readonly hostAdapters?: readonly NamedRoleTurnHostAdapter[];
49
+ readonly packageRoot: string;
50
+ readonly extraPiArgs?: readonly string[];
51
+ readonly timeoutMs?: number;
52
+ };
53
+
54
+ export function composeRoleTurnHostAdapters(
55
+ env: RoleTurnHostResolutionInput,
56
+ principalAuthority: DurablePrincipalAuthority,
57
+ ): readonly NamedRoleTurnHostAdapter[] {
58
+ const piHost = env.roleTurnHost ?? createPiRoleTurnHost({
59
+ packageRoot: env.packageRoot,
60
+ principalAuthority,
61
+ ...(env.extraPiArgs === undefined ? {} : { extraPiArgs: env.extraPiArgs }),
62
+ ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
63
+ recordLaunchedPiIdentity,
64
+ recordLaunchedRolePackageIdentity,
65
+ observeLaunchedRolePackageIdentity,
66
+ });
67
+ return env.hostAdapters ?? [
68
+ { name: "pi", create: () => ({ ok: true as const, host: piHost }) },
69
+ ...packagedExternalHostNames().map((name) => ({
70
+ name,
71
+ create: () => ({
72
+ ok: true as const,
73
+ host: createLazyProductionExternalHost({
74
+ packageRoot: env.packageRoot,
75
+ hostName: name,
76
+ principalAuthority,
77
+ }),
78
+ }),
79
+ })),
80
+ ];
81
+ }
82
+
83
+ export function selectRoleTurnHost(
84
+ adapters: readonly NamedRoleTurnHostAdapter[],
85
+ options: {
86
+ role: PublicCallableRole;
87
+ seat: EffectiveSeat;
88
+ },
89
+ ): RoleTurnHost {
90
+ const hostName = options.seat.host;
91
+ const adapter = adapters.find((candidate) => candidate.name === hostName);
92
+ const model = options.seat.selection === undefined
93
+ ? "unconfigured"
94
+ : `${options.seat.selection.provider}/${options.seat.selection.model}`;
95
+ const registeredHosts = adapters.map(({ name }) => name);
96
+ if (adapter === undefined) {
97
+ throw new HostSelectionError(
98
+ { kind: "host-unregistered", host: hostName, seat: options.role, model, registeredHosts },
99
+ );
100
+ }
101
+ const selected = adapter.create({ role: options.role, model: options.seat.selection });
102
+ if (!selected.ok) {
103
+ throw new HostSelectionError(
104
+ { kind: "host-model-mismatch", host: hostName, seat: options.role, model, registeredHosts },
105
+ );
106
+ }
107
+ return selected.host;
108
+ }
109
+
110
+ export function resolveRoleTurnHost(
111
+ env: RoleTurnHostResolutionInput,
112
+ options: {
113
+ role: PublicCallableRole;
114
+ seat: EffectiveSeat;
115
+ principalAuthority: DurablePrincipalAuthority;
116
+ },
117
+ ): RoleTurnHost {
118
+ return selectRoleTurnHost(
119
+ composeRoleTurnHostAdapters(env, options.principalAuthority),
120
+ { role: options.role, seat: options.seat },
121
+ );
122
+ }
@@ -571,14 +571,19 @@ export async function markRunAdmitted(
571
571
  }
572
572
 
573
573
  /**
574
- * Shared dispatch execution seam: record the effective launch model (initial or
575
- * resume override) and the authoritative seat engine/host onto invocation.json,
576
- * then transition to running.
574
+ * Shared dispatch execution seam: transition to running, then record the
575
+ * effective launch model (initial or resume override) and the authoritative
576
+ * seat engine/host onto invocation.json.
577
577
  * Role runners must not coordinate lifecycle ledger writes themselves.
578
578
  * Engine axis is authoritative here (#617): present string is written; omit/undefined
579
579
  * clears any prior engine key so unset-engine + resume does not keep a stale value.
580
580
  * (Non-authoritative partial updates still use recordEffectiveInvocationModel directly
581
581
  * with `engine: undefined` to preserve.)
582
+ * Host-page write is deliberately the last step (#840 r9 判词 class 2): this
583
+ * function is not atomic, and a caller retrying after it throws must not see
584
+ * the new host already committed while the run-state transition itself never
585
+ * completed — the run-state write happens first, so any failure past that
586
+ * point still leaves the prior invocation host in place for the retry.
582
587
  */
583
588
  export async function markRunRunning(
584
589
  runDirectory: string,
@@ -586,13 +591,6 @@ export async function markRunRunning(
586
591
  effectiveEngine?: string,
587
592
  effectiveHost?: string,
588
593
  ): Promise<void> {
589
- await recordEffectiveInvocationModel(
590
- runDirectory,
591
- effectiveModel,
592
- // Authoritative seat projection: absent engine ⇒ null (delete).
593
- effectiveEngine === undefined ? null : effectiveEngine,
594
- effectiveHost,
595
- );
596
594
  const current = await readRoleRunStateDisk(runDirectory);
597
595
  if (current === undefined) {
598
596
  throw new Error("cannot mark running: run state missing");
@@ -611,6 +609,13 @@ export async function markRunRunning(
611
609
  ...(current.phase === undefined ? {} : { phase: current.phase }),
612
610
  ...(current.currentCourt === undefined ? {} : { currentCourt: current.currentCourt }),
613
611
  });
612
+ await recordEffectiveInvocationModel(
613
+ runDirectory,
614
+ effectiveModel,
615
+ // Authoritative seat projection: absent engine ⇒ null (delete).
616
+ effectiveEngine === undefined ? null : effectiveEngine,
617
+ effectiveHost,
618
+ );
614
619
  }
615
620
 
616
621
  /** @deprecated #416: 429-only resumable marker; kept for historical runs. */
@@ -31,6 +31,8 @@ export type RoleTurnRequestProjectionOptions = {
31
31
  continuation: RoleTurnRequest["continuation"];
32
32
  /** #833 resume-with-message court attempt. */
33
33
  courtAttemptId?: string;
34
+ /** Station child role run (#840): omit automatic navigator attendance. */
35
+ stationChild?: boolean;
34
36
  };
35
37
 
36
38
  export type AdmittedTurnInvocation = {
@@ -74,5 +76,6 @@ export function projectRoleTurnRequest(
74
76
  ...(options.courtAttemptId === undefined || options.courtAttemptId.length === 0
75
77
  ? {}
76
78
  : { courtAttemptId: options.courtAttemptId }),
79
+ ...(options.stationChild === undefined ? {} : { stationChild: options.stationChild }),
77
80
  };
78
81
  }
@@ -16,6 +16,7 @@ import type { CliIo } from "./public-cli/cli-io.ts";
16
16
  import type { CredentialProviders, EffectiveSeat } from "./public-cli/config.ts";
17
17
  import type { PublicCallableRole } from "./public-cli/registry.ts";
18
18
  import type { RoleTurnHost } from "./host-contracts.ts";
19
+ import type { HostSelectionFailure, NamedRoleTurnHostAdapter } from "./public-cli/role-turn-host-resolution.ts";
19
20
  import type { TerminalResult } from "./public-cli/terminal.ts";
20
21
 
21
22
  /** Env published by the parent activation so nested summons never re-derive root. */
@@ -29,7 +30,8 @@ export type PublicSummonRole =
29
30
  | "navigator"
30
31
  | "gatekeeper"
31
32
  | "judge"
32
- | "doctor";
33
+ | "doctor"
34
+ | "diarist";
33
35
 
34
36
  export type PublicSummonRequest = {
35
37
  readonly role: PublicSummonRole;
@@ -66,12 +68,19 @@ export type PublicSummonRequest = {
66
68
  */
67
69
  readonly gateReviewInstruction?: string;
68
70
  /**
69
- * Offline test inject — same face as public CLI env.roleTurnHost. Production
70
- * summons leave this unset and use the seat-resolved host.
71
+ * Pi-adapter inject (tests). Used only as the `pi` row when composing the
72
+ * adapter table — never as an override of the child seat's selected host.
71
73
  */
72
74
  readonly roleTurnHost?: RoleTurnHost;
75
+ /**
76
+ * Composition-root adapter table (tests / parent CLI env). Child seat selects
77
+ * from this table. Production leaves unset and composes the default table.
78
+ */
79
+ readonly hostAdapters?: readonly NamedRoleTurnHostAdapter[];
73
80
  /** Offline test inject for deterministic run ids (same face as public CLI). */
74
81
  readonly createRunId?: () => string;
82
+ /** Typed ticket number handoff for diarist child run (#840). */
83
+ readonly boundTicketNumber?: number;
75
84
  };
76
85
 
77
86
  export type PublicSummonResult = {
@@ -81,6 +90,8 @@ export type PublicSummonResult = {
81
90
  readonly runDirectory?: string;
82
91
  /** Offline diagnostics from nested CLI (structural rejection text). */
83
92
  readonly stderr?: string;
93
+ /** Admitted role invocation from nested run. */
94
+ readonly admitted?: import("./public-cli/invocation.ts").AdmittedRoleInvocation;
84
95
  };
85
96
 
86
97
  function createCapturingIo(): { io: CliIo; stderrText(): string } {
@@ -167,6 +178,13 @@ function projectSeatHost(seat: EffectiveSeat): { host?: string } {
167
178
  return seat.host === undefined ? {} : { host: seat.host };
168
179
  }
169
180
 
181
+ function hostSelectionFailureFromUnknown(error: unknown): HostSelectionFailure | undefined {
182
+ if (typeof error !== "object" || error === null) return undefined;
183
+ if ((error as { name?: unknown }).name !== "HostSelectionError") return undefined;
184
+ const failure = (error as { failure?: HostSelectionFailure }).failure;
185
+ return failure;
186
+ }
187
+
170
188
  async function createSummonEnv(options: {
171
189
  readonly role: PublicCallableRole;
172
190
  readonly home: string;
@@ -176,59 +194,30 @@ async function createSummonEnv(options: {
176
194
  readonly credentials: CredentialProviders;
177
195
  readonly seat: EffectiveSeat;
178
196
  readonly extraPiArgs?: readonly string[];
197
+ readonly roleTurnHost?: RoleTurnHost;
198
+ readonly hostAdapters?: readonly NamedRoleTurnHostAdapter[];
179
199
  }) {
180
- const [{ piDurablePrincipalAuthority }, { appendPiSessionCustomEntry, createPiRoleTurnHost }] =
200
+ const [{ piDurablePrincipalAuthority }, { appendPiSessionCustomEntry }, { resolveRoleTurnHost }] =
181
201
  await Promise.all([
182
202
  import("./pi/durable-principal.ts"),
183
203
  import("./pi/role-turn-host.ts"),
204
+ import("./public-cli/role-turn-host-resolution.ts"),
184
205
  ]);
185
206
  const principalAuthority = piDurablePrincipalAuthority;
186
- // package root is request-scoped only — never leave process.env residue (#675).
187
- const piRecords = {
188
- async recordLaunchedPiIdentity(runDirectory: string, identity: unknown) {
189
- const { recordLaunchedPiIdentity } = await import("./public-cli/invocation.ts");
190
- return recordLaunchedPiIdentity(runDirectory, identity as never);
191
- },
192
- async recordLaunchedRolePackageIdentity(runDirectory: string, identity: unknown) {
193
- const { recordLaunchedRolePackageIdentity } = await import("./public-cli/invocation.ts");
194
- return recordLaunchedRolePackageIdentity(runDirectory, identity as never);
195
- },
196
- async observeLaunchedRolePackageIdentity(root: string, roleEntry: string) {
197
- const { observeLaunchedRolePackageIdentity } = await import("./public-cli/invocation.ts");
198
- return observeLaunchedRolePackageIdentity(root, roleEntry);
207
+ // Same host axis table as public CLI (#617 DK-3 / #675 / #840): child seat
208
+ // selects; injected roleTurnHost is the pi adapter only.
209
+ const roleTurnHost = resolveRoleTurnHost(
210
+ {
211
+ packageRoot: options.packageRoot,
212
+ ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
213
+ ...(options.hostAdapters === undefined ? {} : { hostAdapters: options.hostAdapters }),
214
+ ...(options.extraPiArgs === undefined || options.extraPiArgs.length === 0
215
+ ? {}
216
+ : { extraPiArgs: options.extraPiArgs }),
199
217
  },
200
- } as const;
201
- const piHost = createPiRoleTurnHost({
202
- packageRoot: options.packageRoot,
203
- principalAuthority,
204
- ...(options.extraPiArgs === undefined || options.extraPiArgs.length === 0
205
- ? {}
206
- : { extraPiArgs: options.extraPiArgs }),
207
- ...piRecords,
208
- });
209
- // Same host axis table as public CLI (#617 DK-3 / #675 / #729): seat.host is
210
- // a description-table key; pi stays the in-process default adapter.
218
+ { role: options.role, seat: options.seat, principalAuthority },
219
+ );
211
220
  const hostName = options.seat.host ?? "pi";
212
- let roleTurnHost = piHost;
213
- if (hostName !== "pi") {
214
- const { lookupHostFamily } = await import("./host-descriptions.ts");
215
- const { createLazyProductionExternalHost } = await import(
216
- "./public-cli/load-production-external-host.ts"
217
- );
218
- // #729 / #645: description tables are the sole host authority — family
219
- // dispatch (ACP vs headless) is data-driven. Unregistered keys fail closed.
220
- if (lookupHostFamily(hostName) === undefined) {
221
- throw new Error(
222
- `public role summons host unregistered: host=${hostName} seat=${options.role}`,
223
- );
224
- }
225
- // Same lazy external host face as public CLI (#820) — one builder, two callers.
226
- roleTurnHost = createLazyProductionExternalHost({
227
- packageRoot: options.packageRoot,
228
- hostName,
229
- principalAuthority,
230
- });
231
- }
232
221
  // #788: host is registered above; only then project host-facing provider.
233
222
  const { loadHostProvidersTable, projectHostFacingProvider } = await import(
234
223
  "./public-cli/host-providers.ts"
@@ -281,8 +270,9 @@ export async function summonPublicRole(
281
270
  // Nested summons resolve host on the officer seat only (flag>seat>default pi).
282
271
  // Parent run host is not an override channel (#821 / ADR 0082 host-flag-two-channels).
283
272
  const seat = resolveEffectiveSeat(config, options.role, credentials);
284
- const env = {
285
- ...(await createSummonEnv({
273
+ let summonEnv;
274
+ try {
275
+ summonEnv = await createSummonEnv({
286
276
  role: options.role,
287
277
  home,
288
278
  agentDir,
@@ -291,7 +281,21 @@ export async function summonPublicRole(
291
281
  credentials,
292
282
  seat,
293
283
  ...(options.extraPiArgs === undefined ? {} : { extraPiArgs: options.extraPiArgs }),
294
- })),
284
+ ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
285
+ ...(options.hostAdapters === undefined ? {} : { hostAdapters: options.hostAdapters }),
286
+ });
287
+ } catch (error) {
288
+ const failure = hostSelectionFailureFromUnknown(error);
289
+ if (failure !== undefined) {
290
+ const { formatHostSelectionFailure } = await import("./public-cli/role-turn-host-resolution.ts");
291
+ return { exitCode: 1, stderr: formatHostSelectionFailure(failure) };
292
+ }
293
+ throw error;
294
+ }
295
+ const env = {
296
+ ...summonEnv,
297
+ // Station child role run (#840): omit automatic navigator attendance.
298
+ stationChild: true,
295
299
  // Host config passthrough only — same face as public CLI (#422 / #675).
296
300
  ...(config.autoResumeLimit === undefined
297
301
  ? {}
@@ -304,8 +308,10 @@ export async function summonPublicRole(
304
308
  ...(options.gateReviewInstruction === undefined
305
309
  ? {}
306
310
  : { gateReviewInstruction: options.gateReviewInstruction }),
307
- // Offline test injects — same faces as public CLI env (production leaves unset).
308
- ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
311
+ ...(options.boundTicketNumber === undefined
312
+ ? {}
313
+ : { boundTicketNumber: options.boundTicketNumber }),
314
+ // createRunId is the only remaining env overlay — host is seat-selected above.
309
315
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
310
316
  };
311
317
  const captured = options.io === undefined ? createCapturingIo() : undefined;
@@ -391,6 +397,14 @@ export async function summonPublicRole(
391
397
  result = await runPublicDoctor(options.argv, env, io, parseDoctorArgv);
392
398
  break;
393
399
  }
400
+ case "diarist": {
401
+ const [{ runPublicDiarist }, { parseDiaristArgv }] = await Promise.all([
402
+ import("./public-cli/diarist-run.ts"),
403
+ import("./public-cli/invocation.ts"),
404
+ ]);
405
+ result = await runPublicDiarist(options.argv, env, io, parseDiaristArgv);
406
+ break;
407
+ }
394
408
  }
395
409
 
396
410
  const stderr = captured?.stderrText();
@@ -398,6 +412,7 @@ export async function summonPublicRole(
398
412
  return {
399
413
  exitCode: result.exitCode,
400
414
  ...(result.terminal === undefined ? {} : { terminal: result.terminal }),
415
+ ...(result.admitted === undefined ? {} : { admitted: result.admitted as import("./public-cli/invocation.ts").AdmittedRoleInvocation }),
401
416
  ...(typeof runDirectory === "string" && runDirectory.trim() !== ""
402
417
  ? { runDirectory }
403
418
  : {}),
@@ -426,8 +441,9 @@ export async function summonGateOfficer(options: {
426
441
  * when reask is absent (#786). Never a path pointer substitute.
427
442
  */
428
443
  readonly submission?: unknown;
429
- /** Offline test inject — forwarded to summonPublicRole. */
444
+ /** Pi-adapter inject — forwarded to summonPublicRole (not a parent-host override). */
430
445
  readonly roleTurnHost?: RoleTurnHost;
446
+ readonly hostAdapters?: readonly NamedRoleTurnHostAdapter[];
431
447
  /** Offline test inject — forwarded to summonPublicRole. */
432
448
  readonly createRunId?: () => string;
433
449
  }): Promise<PublicSummonResult> {
@@ -459,6 +475,7 @@ export async function summonGateOfficer(options: {
459
475
  ? {}
460
476
  : { gateReviewInstruction }),
461
477
  ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
478
+ ...(options.hostAdapters === undefined ? {} : { hostAdapters: options.hostAdapters }),
462
479
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
463
480
  } as const;
464
481
  if (options.officer === "notary") {
@@ -10,6 +10,9 @@ import { packagedRoleInputFlag, packagedRolePhaseFlag } from "./packaged-role-re
10
10
  export function projectActivationFlags(request: RoleTurnRequest): Map<string, boolean | string> {
11
11
  const activation = request.activation;
12
12
  const flags = new Map<string, boolean | string>([["ak-role", activation.role]]);
13
+ if (request.stationChild === true) {
14
+ flags.set("ak-station-child", true);
15
+ }
13
16
  const inputFlag = packagedRoleInputFlag(activation.role);
14
17
  const phaseFlag = packagedRolePhaseFlag(activation.role);
15
18
  if ("phase" in activation && phaseFlag !== undefined) flags.set(phaseFlag, activation.phase);
@@ -175,6 +175,14 @@ const GLEANER_LEFT_TRANSPORT_FLAGS = Object.freeze([
175
175
  }),
176
176
  ] as const);
177
177
 
178
+ export const STATION_CHILD_FLAG = Object.freeze({
179
+ name: "ak-station-child",
180
+ definition: Object.freeze({
181
+ description: "Station-child role run (omit automatic navigator attendance; #840)",
182
+ type: "boolean" as const,
183
+ }),
184
+ } as const);
185
+
178
186
  /**
179
187
  * Decode private transport flags into frozen admitted inputs.
180
188
  * Envelope-owned; necessary JSON decode only (public --authority-ref owns grammar).
@@ -1152,6 +1160,8 @@ export function createRoleRuntimeExtension(
1152
1160
  for (const flag of COLLECTOR_TRANSPORT_FLAGS) {
1153
1161
  roleHost.registerFlag(flag.name, flag.definition);
1154
1162
  }
1163
+ // Station-child identity (#840): omit navigator attendance. One flag.
1164
+ roleHost.registerFlag(STATION_CHILD_FLAG.name, STATION_CHILD_FLAG.definition);
1155
1165
 
1156
1166
  let admitted = false;
1157
1167
  let selectedRole: string | undefined;
@@ -2003,13 +2013,15 @@ export function createRoleRuntimeExtension(
2003
2013
  const ledgerHome = resolveActivationLedgerHomeForPath(sessionFile);
2004
2014
  const session = durableSessionPointer(ctx.sessionManager);
2005
2015
 
2006
- // Same public activation face for every role (#675 / owner 09-06): no nested-env
2007
- // skip, no work-role whitelist. Attendance attaches when the envelope supplies it.
2016
+ // Station children (court diarist, inner-gate summons) omit navigator sidecar (#840).
2017
+ // Top-level public entry legs still attend automatically.
2008
2018
  // Navigator seat never re-attaches itself — prepare turns already ARE the navigator
2009
2019
  // public activation (prevents summonPublicRole navigator ↔ attendance recursion).
2020
+ const isStationChild = roleHost.getFlag(STATION_CHILD_FLAG.name) === true;
2010
2021
  if (
2011
2022
  dependencies.createNavigatorAttendance !== undefined
2012
2023
  && entry.role !== "navigator"
2024
+ && !isStationChild
2013
2025
  ) {
2014
2026
  let work: NavigatorWorkContext;
2015
2027
  let contextError: unknown;
@@ -2,6 +2,11 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
4
  import type { DurablePrincipal, DurablePrincipalAuthority } from "./host-contracts.ts";
5
+ import {
6
+ DEFAULT_ROLE_TURN_HOST,
7
+ lookupHeadlessHostDescription,
8
+ lookupHostDescription,
9
+ } from "./host-descriptions.ts";
5
10
  import type { SessionIdentityAuthority } from "./prepared-role-turn.ts";
6
11
 
7
12
  /** Durable session binding stored beside the host-owned session principal. */
@@ -36,3 +41,32 @@ export function createSessionIdentityAuthority(
36
41
  },
37
42
  };
38
43
  }
44
+
45
+ /**
46
+ * Host-aware resumable-session probe (#840 P1). The shared auto-resume loop
47
+ * (public-cli/auto-resume.ts) asks one question — "can this principal's turn
48
+ * still be resumed" — without itself knowing which concrete host executed it.
49
+ * Pi's own session.jsonl transcript (DurablePrincipalAuthority#isAvailable)
50
+ * answers that only for the pi host; ACP and headless hosts persist their
51
+ * resumable native session id under their own session-identity binding file
52
+ * instead (#729 / #731 / #645) and never write session.jsonl, so the plain
53
+ * pi check always reports them unavailable after one attempt. One lookup per
54
+ * registered host family — no per-host-name branch, no per-seat whitelist.
55
+ */
56
+ export function resolveHostAwareSessionAvailability(
57
+ host: string | undefined,
58
+ principalAuthority: DurablePrincipalAuthority,
59
+ ): (principal: DurablePrincipal) => Promise<boolean> {
60
+ if (host === undefined || host === DEFAULT_ROLE_TURN_HOST) {
61
+ return (principal) => principalAuthority.isAvailable(principal);
62
+ }
63
+ const description = lookupHostDescription(host) ?? lookupHeadlessHostDescription(host);
64
+ if (description === undefined) {
65
+ // Host selection already fails closed on an unregistered name (#510) long
66
+ // before a turn dispatches; mirror the same fail-closed default here
67
+ // rather than guessing at pi's binding for a name pi does not own.
68
+ return () => Promise.resolve(false);
69
+ }
70
+ const sessionIdentity = createSessionIdentityAuthority(principalAuthority, description.sessionBindingFile);
71
+ return async (principal) => (await sessionIdentity.load(principal)) !== undefined;
72
+ }