@akagilnc/pi-workflow-roles 0.1.3414 → 0.1.3445

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 (72) hide show
  1. package/dist/archivist-record-entry.js +21 -12
  2. package/dist/archivist-record-topology.js +20 -0
  3. package/dist/auditor-dossier-tool.js +67 -76
  4. package/dist/canonical-json.js +66 -69
  5. package/dist/compliance-transport.js +99 -86
  6. package/dist/countersign-contracts.js +23 -25
  7. package/dist/doctor-contracts.js +169 -145
  8. package/dist/engine-detour-tool.js +83 -87
  9. package/dist/engine-detour.js +148 -183
  10. package/dist/evidence-child-executor.js +12 -9
  11. package/dist/exact-utf8.js +11 -10
  12. package/dist/git-object-id.js +10 -5
  13. package/dist/gleaner-left-contracts.js +33 -35
  14. package/dist/grok/production-host.js +239 -154
  15. package/dist/inspector-contracts.js +28 -31
  16. package/dist/institutional-resolution.js +101 -104
  17. package/dist/ledger-session-read.js +241 -0
  18. package/dist/merger-contracts.js +66 -56
  19. package/dist/navigator-attendance.js +0 -6
  20. package/dist/navigator-session-contracts.js +6 -10
  21. package/dist/notary-contracts.js +91 -100
  22. package/dist/open-tool-schema.js +32 -31
  23. package/dist/package-contracts/collector-output.js +56 -43
  24. package/dist/package-contracts/fixer-output.js +52 -50
  25. package/dist/package-contracts/fixer-packet.js +67 -64
  26. package/dist/package-contracts/gatekeeper-output.js +48 -56
  27. package/dist/package-contracts/judge-output.js +23 -20
  28. package/dist/package-contracts/navigator-output.js +35 -40
  29. package/dist/package-contracts/reviewer-output.js +94 -110
  30. package/dist/package-contracts/terminating-infrastructure.js +62 -90
  31. package/dist/package-contracts/worker-output.js +30 -11
  32. package/dist/package-resources/engine-material.js +58 -94
  33. package/dist/pi/in-process-session.js +2 -12
  34. package/dist/public-cli/config.js +403 -485
  35. package/dist/public-cli/main.js +1226 -561
  36. package/dist/public-cli/registry.js +107 -114
  37. package/dist/receipt-delivery-policy.js +74 -76
  38. package/dist/session-opening-materials.js +44 -47
  39. package/dist/sha256.js +5 -3
  40. package/dist/sitian-appender.js +204 -210
  41. package/dist/sitian-contracts.js +15 -19
  42. package/dist/sitian-facade.js +5 -10
  43. package/dist/sitian-reader.js +37 -46
  44. package/dist/stream-idle-guard.js +63 -71
  45. package/dist/ticket-seat-memory.js +244 -0
  46. package/package.json +1 -1
  47. package/scripts/build-package.mjs +41 -0
  48. package/src/analyst-gate-cycles-read.ts +152 -83
  49. package/src/analyst-ledger.ts +170 -43
  50. package/src/archivist-record-entry.ts +32 -11
  51. package/src/archivist-record-topology.ts +39 -0
  52. package/src/evidence-child-executor.ts +18 -10
  53. package/src/grok/production-host.ts +44 -9
  54. package/src/host-contracts.ts +18 -0
  55. package/src/host-transition-prior-native.ts +8 -1
  56. package/src/ledger-session-read.ts +56 -0
  57. package/src/navigator-attendance.ts +0 -6
  58. package/src/navigator-session-contracts.ts +15 -14
  59. package/src/pi/durable-principal.ts +6 -0
  60. package/src/pi/in-process-session.ts +10 -17
  61. package/src/public-cli/cli.ts +2 -14
  62. package/src/public-cli/config.ts +6 -28
  63. package/src/public-cli/inspector-run.ts +21 -10
  64. package/src/public-cli/invocation.ts +8 -25
  65. package/src/public-cli/notary-run.ts +21 -4
  66. package/src/public-cli/option-definitions.ts +2 -2
  67. package/src/public-cli/post-admission.ts +333 -42
  68. package/src/public-cli/registry.ts +2 -8
  69. package/src/public-cli/run-lifecycle.ts +1 -3
  70. package/src/public-cli/settlement.ts +266 -81
  71. package/src/session-opening-materials.ts +4 -2
  72. package/src/ticket-seat-memory.ts +454 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pure archivist record placement topology (ADR 0048 / 0065).
3
+ * No SessionManager / pi-coding-agent — cold public-bin and ledger discovery
4
+ * may import this without pulling open-session runtime into the graph.
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ import { join } from "node:path";
8
+
9
+ import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
10
+ import {
11
+ activationBookDirectory,
12
+ resolveActivationLedgerHome,
13
+ resolveActivationLedgerHomeForPath,
14
+ } from "./activation-ledger-topology.ts";
15
+
16
+ /**
17
+ * Sole subject-keyed nest path under the ledger book (ADR 0048 / 0065).
18
+ * Callers must not re-hash subject or re-join book/kind/digest themselves.
19
+ */
20
+ export function subjectKeyedRecordDirectory(input: {
21
+ readonly cwd: string;
22
+ readonly kind: string;
23
+ readonly subject: string;
24
+ /** Parent session file — home path-derives from it when present. */
25
+ readonly parentSessionFile?: string;
26
+ /** Explicit process home when no parent file is available (discovery-only). */
27
+ readonly home?: string;
28
+ }): string {
29
+ const ledgerHome =
30
+ input.parentSessionFile !== undefined && input.parentSessionFile.length > 0
31
+ ? resolveActivationLedgerHomeForPath(input.parentSessionFile)
32
+ : resolveActivationLedgerHome(input.home);
33
+ const digest = createHash("sha256").update(input.subject).digest("hex").slice(0, 32);
34
+ return join(
35
+ activationBookDirectory(ledgerHome, resolveBookKeyFromGit(input.cwd)),
36
+ input.kind,
37
+ digest,
38
+ );
39
+ }
@@ -26,6 +26,12 @@ import {
26
26
  } from "./compliance-transport.ts";
27
27
  import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
28
28
  import { sitianReport } from "./sitian-facade.ts";
29
+ import {
30
+ isTicketSeatMemorySeat,
31
+ readRunTicketNumber,
32
+ ticketSeatMemorySubject,
33
+ TICKET_SEAT_MEMORY_KIND,
34
+ } from "./ticket-seat-memory.ts";
29
35
  import { createEngineDetourToolDefinition } from "./engine-detour-tool.ts";
30
36
  import { engineNameFromEnv } from "./engine-detour.ts";
31
37
  import {
@@ -564,9 +570,16 @@ export async function executeAuditorChild(
564
570
  const parentHeader = parentSessionManager?.getHeader?.();
565
571
  const parentSessionFile = parentSessionManager?.getSessionFile?.();
566
572
  const parentAttemptEntryId = parentSessionManager?.getLeafId?.();
573
+ // #636: ticket+seat subject resumes the same nest (navigator subject path); no ticket → fresh nest.
574
+ const ticketNumber = await readRunTicketNumber(runDirectory);
575
+ const memorySubject =
576
+ ticketNumber !== undefined && isTicketSeatMemorySeat(seat)
577
+ ? ticketSeatMemorySubject(ticketNumber, seat)
578
+ : undefined;
567
579
  const auditorSessionManager: SessionManager = createRecordSession({
568
580
  cwd,
569
- kind: "auditor-roles",
581
+ kind: TICKET_SEAT_MEMORY_KIND,
582
+ ...(memorySubject === undefined ? {} : { subject: memorySubject }),
570
583
  ...(parentSessionManager === undefined ? {} : { parent: parentSessionManager }),
571
584
  });
572
585
 
@@ -962,7 +975,7 @@ export function createNativeNavigatorSessionFactory(
962
975
  return async ({ context, subject, modelSettingPath, tool }) => {
963
976
  const resolved = await resolveNavigatorSeatSelection(context, modelSettingPath, defaultModelSettingPath);
964
977
  let selection = resolved.selection;
965
- let thinkingLevel = resolved.thinkingLevel;
978
+ let thinkingLevel: string | undefined = resolved.thinkingLevel;
966
979
  let configuredLabel = resolved.configuredLabel;
967
980
 
968
981
  const { createRecordSession } = await import("./archivist-record-entry.ts");
@@ -1104,7 +1117,7 @@ export function createNativeNavigatorSessionFactory(
1104
1117
  }
1105
1118
  },
1106
1119
  entries: () => sessionManager.getEntries(),
1107
- setModel: async (next, nextThinking) => {
1120
+ setModel: async (next) => {
1108
1121
  let nextParsed: ReturnType<typeof parseNavigatorModelSetting>;
1109
1122
  try {
1110
1123
  nextParsed = parseNavigatorModelSetting(next);
@@ -1120,16 +1133,11 @@ export function createNativeNavigatorSessionFactory(
1120
1133
  `Navigator model switch requires a new session: ${configuredLabel} → ${next}`,
1121
1134
  );
1122
1135
  }
1123
- if (nextParsed.thinkingLevel !== nextThinking || thinkingLevel !== nextThinking) {
1124
- throw new NavigatorUnavailableError(
1125
- "thinking",
1126
- `Navigator thinking level ${nextThinking} is unavailable for ${next}`,
1127
- );
1128
- }
1136
+ // Thinking is opaque pass-through; no stick/availability re-check.
1129
1137
  selection = {
1130
1138
  provider: nextParsed.provider,
1131
1139
  model: nextParsed.model,
1132
- thinking: nextParsed.thinkingLevel,
1140
+ ...(nextParsed.thinkingLevel === undefined ? {} : { thinking: nextParsed.thinkingLevel }),
1133
1141
  };
1134
1142
  thinkingLevel = nextParsed.thinkingLevel;
1135
1143
  configuredLabel = next;
@@ -167,18 +167,25 @@ export async function bindProductionGrokIsolation(
167
167
  * while preserving the session dossier under runDirectory/grok-home. Success,
168
168
  * typed-result, and throw paths all clean up; cleanup failure and primary+cleanup
169
169
  * both surface.
170
+ *
171
+ * `noteOpened` fires only after bind succeeds — the actual open event, distinct
172
+ * from turn return/throw — so last-host ownership can retain the opened run
173
+ * even when the body or cleanup later throws (#636).
170
174
  */
171
175
  export async function withProductionGrokIsolation<T>(
172
176
  runDirectory: string,
173
177
  operatorHome: string,
174
178
  packageRoot: string,
175
179
  run: (binding: ProductionGrokIsolationBinding) => Promise<T>,
180
+ noteOpened?: (runDirectory: string) => void,
176
181
  ): Promise<T> {
177
182
  let binding: ProductionGrokIsolationBinding | undefined;
178
183
  let primaryFailure: ProductionGrokPrimaryFailure = NO_PRODUCTION_GROK_PRIMARY_FAILURE;
179
184
  let value!: T;
180
185
  try {
181
186
  binding = await bindProductionGrokIsolation(runDirectory, operatorHome, packageRoot);
187
+ // Open fact: bind succeeded under runDirectory. Not deferred to turn outcome.
188
+ noteOpened?.(runDirectory);
182
189
  value = await run(binding);
183
190
  } catch (error) {
184
191
  primaryFailure = { present: true, value: error };
@@ -278,6 +285,24 @@ async function recordGrokCapabilities(
278
285
  );
279
286
  }
280
287
 
288
+ /**
289
+ * Which run directory owns GROK_HOME for this turn (#636 / ADR 0077).
290
+ * Same-host ticket-seat resume reopens the prior native home; otherwise the live run.
291
+ * Private to the production host — not a test seam.
292
+ */
293
+ function resolveProductionGrokIsolationRunDirectory(
294
+ request: Pick<RoleTurnRequest, "runDirectory" | "continuation" | "nativeHomeRunDirectory">,
295
+ ): string {
296
+ if (
297
+ request.continuation.kind === "resume" &&
298
+ typeof request.nativeHomeRunDirectory === "string" &&
299
+ request.nativeHomeRunDirectory.length > 0
300
+ ) {
301
+ return request.nativeHomeRunDirectory;
302
+ }
303
+ return request.runDirectory;
304
+ }
305
+
281
306
  /**
282
307
  * Assemble the production grok-build RoleTurnHost from the S6 true adapter.
283
308
  */
@@ -317,16 +342,26 @@ export function createProductionGrokRoleTurnHost(options: ProductionGrokHostOpti
317
342
 
318
343
  return {
319
344
  executeTurn(request) {
345
+ // #636 same-host ticket-seat resume: reopen the prior run's grok-home so
346
+ // session/load hits the ACP storage that bound the shared principal.
347
+ // Fresh runs and cross-host arrivals keep isolating under request.runDirectory.
348
+ const isolationRunDirectory = resolveProductionGrokIsolationRunDirectory(request);
320
349
  const execution = serial.then(() =>
321
- withProductionGrokIsolation(request.runDirectory, request.home, packageRoot, async (binding) => {
322
- turn = binding;
323
- try {
324
- // S6 seatbelt hangs on request.home — same isolated root as GROK_HOME.
325
- return await inner.executeTurn({ ...request, home: binding.controlledHome });
326
- } finally {
327
- turn = undefined;
328
- }
329
- }),
350
+ withProductionGrokIsolation(
351
+ isolationRunDirectory,
352
+ request.home,
353
+ packageRoot,
354
+ async (binding) => {
355
+ turn = binding;
356
+ try {
357
+ // S6 seatbelt hangs on request.home — same isolated root as GROK_HOME.
358
+ return await inner.executeTurn({ ...request, home: binding.controlledHome });
359
+ } finally {
360
+ turn = undefined;
361
+ }
362
+ },
363
+ request.noteNativeHomeOpened,
364
+ ),
330
365
  );
331
366
  serial = execution.then(
332
367
  () => undefined,
@@ -165,6 +165,19 @@ export type RoleTurnRequest = {
165
165
  readonly timeoutMs?: number;
166
166
  /** Set by post-admission only on a real host switch; never on same-host resume. */
167
167
  readonly hostTransition?: RoleTurnHostTransition;
168
+ /**
169
+ * Grok native isolation run for ticket-seat resume (#636 / ADR 0079):
170
+ * reopen grok-home under this prior run (ACP storage). Covers same-host,
171
+ * same-run retry after a new run, and return-to-Grok after another host.
172
+ * Absent → isolate under runDirectory. Never a cross-host byte handoff.
173
+ */
174
+ readonly nativeHomeRunDirectory?: string;
175
+ /**
176
+ * Production Grok isolation notes the actual open event here after bind succeeds
177
+ * (#636). Open is not the same event as turn return/throw — post-admission uses
178
+ * this fact for last-host ownership instead of inferring open from the turn outcome.
179
+ */
180
+ readonly noteNativeHomeOpened?: (runDirectory: string) => void;
168
181
  };
169
182
 
170
183
  /** Turn result — only fields upper layers currently consume. */
@@ -203,6 +216,11 @@ export type NewDurablePrincipalRequest = {
203
216
  /** Host authority for issuing, checking, and temporarily decoding durable principals. */
204
217
  export interface DurablePrincipalAuthority {
205
218
  issue(request: NewDurablePrincipalRequest): DurablePrincipal;
219
+ /**
220
+ * Host-owned seal of already-placed coordinates into a durable principal wire
221
+ * object. Public layers must not forge opaque principal shapes (#636).
222
+ */
223
+ seal(coordinates: DurablePrincipalCoordinates): DurablePrincipal;
206
224
  isAvailable(principal: DurablePrincipal): Promise<boolean>;
207
225
  decode(principal: unknown): DurablePrincipalCoordinates;
208
226
  }
@@ -71,12 +71,18 @@ export async function listGrokNativeRecordPaths(runDirectory: string): Promise<s
71
71
  * Project one hostTransition only for a real switch between known hosts.
72
72
  * Unknown host names → undefined (no inject). Empty native volume still
73
73
  * yields a typed switch (empty path list).
74
+ *
75
+ * Grok native home lives under the run that wrote it. Ticket-seat memory
76
+ * cross-run switches pass `previousRunDirectory` from last-host metadata so
77
+ * the scan does not look at an empty fresh run (#636 / #617 DK-4).
74
78
  */
75
79
  export async function projectHostTransitionPriorNative(input: {
76
80
  readonly previousHost: string;
77
81
  readonly liveHost: string;
78
82
  readonly runDirectory: string;
79
83
  readonly piSessionFile: string;
84
+ /** Prior run that held Grok native records when principal continued across runs. */
85
+ readonly previousRunDirectory?: string;
80
86
  }): Promise<RoleTurnHostTransition | undefined> {
81
87
  if (input.previousHost === input.liveHost) return undefined;
82
88
  if (!isKnownRoleTurnHost(input.previousHost) || !isKnownRoleTurnHost(input.liveHost)) {
@@ -90,7 +96,8 @@ export async function projectHostTransitionPriorNative(input: {
90
96
  };
91
97
  }
92
98
  // previousHost === "grok-build": DK-7 path handoff only — do not read bytes.
93
- const paths = await listGrokNativeRecordPaths(input.runDirectory);
99
+ const grokRoot = input.previousRunDirectory ?? input.runDirectory;
100
+ const paths = await listGrokNativeRecordPaths(grokRoot);
94
101
  return {
95
102
  previousHost: "grok-build",
96
103
  priorNativePaths: paths,
@@ -258,3 +258,59 @@ export function extractSessionToolIntervals(
258
258
  : { ...base, endedAt: interval.endedAt };
259
259
  });
260
260
  }
261
+
262
+ /**
263
+ * One binding-owned interval inside a continuous volume (#636):
264
+ * from the nearest preceding binding at-or-before `anchorIndex` through the
265
+ * row before the next binding (or EOF). Gate/settlement/analyst share this
266
+ * slice — do not fork a second interval scan.
267
+ *
268
+ * `closed` is true iff a later binding ended the interval before EOF of the
269
+ * provided rows. On a damaged prefix parse, closed intervals are fully owned
270
+ * by their run; open intervals (through prefix EOF) may have lost rows to the
271
+ * bad line and stay honest-missing for full span/tools.
272
+ */
273
+ export type BindingInterval = {
274
+ readonly rows: readonly LedgerSessionRow[];
275
+ readonly closed: boolean;
276
+ };
277
+
278
+ export function intervalRowsAroundAnchor(
279
+ rows: readonly LedgerSessionRow[],
280
+ anchorIndex: number,
281
+ isBindingRow: (row: LedgerSessionRow) => boolean,
282
+ ): BindingInterval {
283
+ let start = 0;
284
+ for (let i = anchorIndex; i >= 0; i -= 1) {
285
+ if (isBindingRow(rows[i]!)) {
286
+ start = i;
287
+ break;
288
+ }
289
+ }
290
+ let end = rows.length;
291
+ for (let i = Math.max(anchorIndex, start) + 1; i < rows.length; i += 1) {
292
+ if (isBindingRow(rows[i]!)) {
293
+ end = i;
294
+ break;
295
+ }
296
+ }
297
+ return { rows: rows.slice(start, end), closed: end < rows.length };
298
+ }
299
+
300
+ /**
301
+ * Interval owned by the first binding row that satisfies `matches`.
302
+ * `undefined` when the volume carries no matching binding — callers must not
303
+ * silently treat the whole continuous volume as that owner.
304
+ */
305
+ export function intervalRowsForMatchingBinding(
306
+ rows: readonly LedgerSessionRow[],
307
+ isBindingRow: (row: LedgerSessionRow) => boolean,
308
+ matches: (row: LedgerSessionRow) => boolean,
309
+ ): BindingInterval | undefined {
310
+ for (let i = 0; i < rows.length; i += 1) {
311
+ const row = rows[i]!;
312
+ if (!isBindingRow(row) || !matches(row)) continue;
313
+ return intervalRowsAroundAnchor(rows, i, isBindingRow);
314
+ }
315
+ return undefined;
316
+ }
@@ -648,9 +648,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
648
648
  try {
649
649
  await created.setModel?.(modelSetting, model.thinkingLevel);
650
650
  if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
651
- if (created.getThinkingLevel?.() !== undefined && created.getThinkingLevel() !== model.thinkingLevel) {
652
- throw new NavigatorUnavailableError("thinking", `Navigator thinking level ${model.thinkingLevel} is unavailable for ${modelSetting}`);
653
- }
654
651
  created.appendEntry(INVOCATION_ENTRY, { invocationId, role: options.role, phase: options.phase, subjectKey });
655
652
  if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
656
653
  session = created;
@@ -665,9 +662,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
665
662
  } else {
666
663
  try {
667
664
  await session.setModel?.(modelSetting, model.thinkingLevel);
668
- if (session.getThinkingLevel?.() !== undefined && session.getThinkingLevel() !== model.thinkingLevel) {
669
- throw new NavigatorUnavailableError("thinking", `Navigator thinking level ${model.thinkingLevel} is unavailable for ${modelSetting}`);
670
- }
671
665
  } catch (error) {
672
666
  // Contract: README.md#Navigator-attendance — resumed-session configuration failures remain typed unavailable and retain the original cause.
673
667
  throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("session", error);
@@ -162,8 +162,8 @@ export type NavigatorPreparationSession = {
162
162
  appendEntry(customType: string, data: unknown): void;
163
163
  entries(): readonly unknown[];
164
164
  providerFailure?(): NavigatorProviderFailureFact | undefined;
165
- setModel?(model: string, thinkingLevel: "off" | "max"): Promise<void>;
166
- getThinkingLevel?(): string;
165
+ setModel?(model: string, thinkingLevel?: string): Promise<void>;
166
+ getThinkingLevel?(): string | undefined;
167
167
  recordPointer(): string;
168
168
  dispose(): void | Promise<void>;
169
169
  };
@@ -204,22 +204,22 @@ export async function writeNavigatorModelSetting(model: string, path = navigator
204
204
  export function parseNavigatorModelSetting(value: string): {
205
205
  provider: string;
206
206
  model: string;
207
- thinkingLevel: "off" | "max";
207
+ thinkingLevel?: string;
208
208
  } {
209
209
  const slash = value.indexOf("/");
210
210
  if (slash <= 0 || slash === value.length - 1) {
211
- throw new Error("Navigator model setting must be provider/model[:max]");
211
+ throw new Error("Navigator model setting must be provider/model[:thinking]");
212
212
  }
213
213
  const provider = value.slice(0, slash);
214
214
  const modelWithThinking = value.slice(slash + 1);
215
215
  const colon = modelWithThinking.lastIndexOf(":");
216
216
  const suffix = colon < 0 ? undefined : modelWithThinking.slice(colon + 1);
217
- if (suffix !== undefined && suffix !== "max") {
218
- throw new Error("Navigator model setting must use :max or omit the thinking suffix");
219
- }
220
217
  const model = colon < 0 ? modelWithThinking : modelWithThinking.slice(0, colon);
221
218
  if (model === "") throw new Error("Navigator model setting must include a model");
222
- return { provider, model, thinkingLevel: suffix === "max" ? "max" : "off" };
219
+ // Suffix is opaque pass-through; bare provider/model omits thinkingLevel (Pi default).
220
+ return suffix === undefined
221
+ ? { provider, model }
222
+ : { provider, model, thinkingLevel: suffix };
223
223
  }
224
224
 
225
225
  /**
@@ -230,20 +230,21 @@ export async function resolveNavigatorSeatSelection(
230
230
  context: HostContext,
231
231
  modelSettingPath: string | undefined,
232
232
  defaultModelSettingPath: string,
233
- ): Promise<{ selection: HostInstitutionalModelSelection; configuredLabel: string; thinkingLevel: "off" | "max" }> {
233
+ ): Promise<{ selection: HostInstitutionalModelSelection; configuredLabel: string; thinkingLevel?: string }> {
234
234
  const runDirectory = auditorRunDirectory(context);
235
235
  if (runDirectory !== undefined) {
236
236
  try {
237
237
  const selection = await readInstitutionalSeatSelection(runDirectory, "navigator");
238
- const thinkingLevel = selection.thinking === "max" ? "max" : "off";
239
238
  return {
240
239
  selection: {
241
240
  provider: selection.provider,
242
241
  model: selection.model,
243
242
  ...(selection.thinking === undefined ? {} : { thinking: selection.thinking }),
244
243
  },
245
- configuredLabel: `${selection.provider}/${selection.model}${thinkingLevel === "max" ? ":max" : ""}`,
246
- thinkingLevel,
244
+ configuredLabel: selection.thinking === undefined
245
+ ? `${selection.provider}/${selection.model}`
246
+ : `${selection.provider}/${selection.model}:${selection.thinking}`,
247
+ ...(selection.thinking === undefined ? {} : { thinkingLevel: selection.thinking }),
247
248
  };
248
249
  } catch (error) {
249
250
  if (
@@ -274,9 +275,9 @@ export async function resolveNavigatorSeatSelection(
274
275
  selection: {
275
276
  provider: parsed.provider,
276
277
  model: parsed.model,
277
- thinking: parsed.thinkingLevel,
278
+ ...(parsed.thinkingLevel === undefined ? {} : { thinking: parsed.thinkingLevel }),
278
279
  },
279
280
  configuredLabel: configured,
280
- thinkingLevel: parsed.thinkingLevel,
281
+ ...(parsed.thinkingLevel === undefined ? {} : { thinkingLevel: parsed.thinkingLevel }),
281
282
  };
282
283
  }
@@ -55,6 +55,12 @@ export const piDurablePrincipalAuthority: DurablePrincipalAuthority = {
55
55
  sessionFile: coordinates.sessionFile,
56
56
  });
57
57
  },
58
+ seal(coordinates: DurablePrincipalCoordinates): DurablePrincipal {
59
+ return encode({
60
+ sessionDirectory: coordinates.sessionDirectory,
61
+ sessionFile: coordinates.sessionFile,
62
+ });
63
+ },
58
64
  decode(value: unknown): DurablePrincipalCoordinates {
59
65
  const record = value as Partial<DurablePrincipalCoordinates> | null;
60
66
  if (record === null || typeof record !== "object") {
@@ -237,7 +237,7 @@ export async function openPiInstitutionalSession(
237
237
  ? childRegistry.find(selection.provider, selection.model)
238
238
  : undefined;
239
239
 
240
- const withTypedReason = (error: Error, reason: "auth" | "model" | "thinking"): Error =>
240
+ const withTypedReason = (error: Error, reason: "auth" | "model"): Error =>
241
241
  Object.assign(error, { reason });
242
242
 
243
243
  // Existing Navigator/model contract (pre-#590): unknown provider or model is
@@ -254,13 +254,15 @@ export async function openPiInstitutionalSession(
254
254
  const fallbackApi = providerDefaultModel?.api
255
255
  ?? (childProvider as any)?.api
256
256
  ?? (selection.provider === "openai-codex" ? "openai-codex-responses" : "openai-completions");
257
+ // Fallback model facts come from the provider surface only — never derive
258
+ // reasoning (or any capability) from the thinking string (#683 pass-through).
257
259
  const modelToUse = foundModel ?? {
258
260
  id: selection.model,
259
261
  name: selection.model,
260
262
  api: fallbackApi as any,
261
263
  provider: selection.provider,
262
264
  baseUrl: providerDefaultModel?.baseUrl ?? "",
263
- reasoning: selection.thinking !== undefined && selection.thinking !== "off",
265
+ reasoning: providerDefaultModel?.reasoning ?? false,
264
266
  input: ["text"],
265
267
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
266
268
  contextWindow: 128000,
@@ -631,15 +633,15 @@ export async function openPiInstitutionalSession(
631
633
  }
632
634
  }
633
635
 
634
- // 7. Create AgentSession
635
- // Existing Navigator contract: requested thinking must stick after open. Pi
636
- // clamps unsupported levels; if max was requested and not applied, fail as
637
- // typed unavailable/thinking (pre-#590 attendance setModel check).
638
- const requestedThinking = options.selection.thinking === "max" ? "max" : "off";
636
+ // 7. Create AgentSession — thinking is opaque pass-through. Absent selection
637
+ // omits thinkingLevel (Pi default). Pi clamps unsupported levels itself;
638
+ // we do not re-check or invent defaults.
639
639
  const { session } = await createAgentSession({
640
640
  cwd: options.cwd,
641
641
  model: effectiveModel,
642
- thinkingLevel: requestedThinking as any,
642
+ ...(options.selection.thinking === undefined
643
+ ? {}
644
+ : { thinkingLevel: options.selection.thinking as any }),
643
645
  modelRuntime: runtime,
644
646
  sessionManager,
645
647
  settingsManager: settings,
@@ -649,15 +651,6 @@ export async function openPiInstitutionalSession(
649
651
  ...(options.toolsAllowlist === undefined ? {} : { tools: options.toolsAllowlist as string[] }),
650
652
  ...(customTools.length === 0 ? {} : { customTools }),
651
653
  });
652
- if (requestedThinking === "max" && session.thinkingLevel !== "max") {
653
- session.dispose();
654
- throw withTypedReason(
655
- new Error(
656
- `${label} thinking level max is unavailable for ${selection.provider}/${selection.model}`,
657
- ),
658
- "thinking",
659
- );
660
- }
661
654
 
662
655
  // 8. Event subscriptions
663
656
  const listeners = new Set<(event: HostInstitutionalSessionEvent) => void>();
@@ -461,16 +461,6 @@ function cliResultFromRoleRun(result: {
461
461
  };
462
462
  }
463
463
 
464
- const THINKING_LEVELS = new Set([
465
- "off",
466
- "minimal",
467
- "low",
468
- "medium",
469
- "high",
470
- "xhigh",
471
- "max",
472
- ]);
473
-
474
464
  function defaultIo(): CliIo {
475
465
  return {
476
466
  stdout: (text) => {
@@ -505,10 +495,8 @@ type ParsedGlobal = {
505
495
  };
506
496
 
507
497
  function parseThinking(value: string): PublicThinkingLevel {
508
- if (!THINKING_LEVELS.has(value)) {
509
- throw new CliUsageError(`unknown thinking level: ${value}`);
510
- }
511
- return value as PublicThinkingLevel;
498
+ // #683: opaque pass-through — no local whitelist.
499
+ return value;
512
500
  }
513
501
 
514
502
  function parseArgv(argv: readonly string[]): ParsedGlobal {
@@ -102,16 +102,6 @@ export type InvocationModelOverride = {
102
102
  host?: string;
103
103
  };
104
104
 
105
- export const THINKING_LEVELS = new Set<PublicThinkingLevel>([
106
- "off",
107
- "minimal",
108
- "low",
109
- "medium",
110
- "high",
111
- "xhigh",
112
- "max",
113
- ]);
114
-
115
105
  export function publicCliConfigPath(home: string): string {
116
106
  if (typeof home !== "string" || home.trim() === "") {
117
107
  throw new Error("home must be explicitly provided");
@@ -336,17 +326,10 @@ export function parseModelSpec(
336
326
  const thinkingSplit = trimmed.lastIndexOf(":");
337
327
  let modelPart = trimmed;
338
328
  let thinking: PublicThinkingLevel | undefined = fallbackThinking;
339
- // #346: no colon → bare provider/model is legal. Colon present (any index,
340
- // including 0) → suffix must be a typed PublicThinkingLevel (empty/unknown
341
- // stay format rejects; never swallow ":…" into the model name).
329
+ // #346/#683: no colon → bare provider/model is legal. Colon present → suffix
330
+ // is opaque thinking pass-through (no local whitelist); never swallow into model.
342
331
  if (thinkingSplit !== -1) {
343
- const maybeThinking = trimmed.slice(thinkingSplit + 1);
344
- if (!THINKING_LEVELS.has(maybeThinking as PublicThinkingLevel)) {
345
- throw new Error(
346
- `model specification must be provider/model[:thinking], got ${spec}`,
347
- );
348
- }
349
- thinking = maybeThinking as PublicThinkingLevel;
332
+ thinking = trimmed.slice(thinkingSplit + 1);
350
333
  modelPart = trimmed.slice(0, thinkingSplit);
351
334
  }
352
335
  const slash = modelPart.indexOf("/");
@@ -507,14 +490,9 @@ function parseSeatModelConfig(value: unknown, seat: string): PersistentSeatConfi
507
490
  if (typeof raw.model !== "string" || raw.model.trim() === "") {
508
491
  throw new Error(`config seat ${seat} requires model`);
509
492
  }
510
- // #384: thinking is optional on persistent seats; when present it must be typed.
511
- if (raw.thinking !== undefined) {
512
- if (
513
- typeof raw.thinking !== "string" ||
514
- !THINKING_LEVELS.has(raw.thinking as PublicThinkingLevel)
515
- ) {
516
- throw new Error(`config seat ${seat} requires a valid thinking level`);
517
- }
493
+ // #384/#683: thinking is optional opaque pass-through; when present must be a string.
494
+ if (raw.thinking !== undefined && typeof raw.thinking !== "string") {
495
+ throw new Error(`config seat ${seat} thinking must be a string`);
518
496
  }
519
497
  const parsed: PersistentSeatConfig = {
520
498
  provider: raw.provider as string,
@@ -6,7 +6,9 @@
6
6
  */
7
7
  import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
8
8
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
9
+ import { rebindAdmittedToTicketSeatMemory } from "../ticket-seat-memory.ts";
9
10
  import { CliUsageError } from "./cli-errors.ts";
11
+ import { resolveSeatTicketBinding } from "./seat-ticket-binding.ts";
10
12
  import {
11
13
  admitInspectorInvocation,
12
14
  buildInspectorTransportPrompt,
@@ -89,8 +91,26 @@ export async function runPublicInspector(
89
91
  throw error;
90
92
  }
91
93
 
94
+ // #635 seat self-ticket then #636 ticket+seat memory principal (before admitted mark).
95
+ await resolveSeatTicketBinding(admitted, env);
96
+ const memory = await rebindAdmittedToTicketSeatMemory({
97
+ admitted,
98
+ seat: "inspector",
99
+ principalAuthority: env.principalAuthority,
100
+ });
92
101
  await markRunAdmitted(admitted, env.principalAuthority);
93
102
 
103
+ // Same ticket nest already present → native host resume (not a fresh initial).
104
+ // Prompt is the same officer transport; only continuation kind flips (#636 / ADR 0079).
105
+ const engineMaterial = engineSessionMaterialFromOptions({
106
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
107
+ packageRoot: env.packageRoot,
108
+ });
109
+ const continuation = {
110
+ kind: memory?.resumed === true ? ("resume" as const) : ("initial" as const),
111
+ prompt: buildInspectorTransportPrompt(admitted, engineMaterial),
112
+ };
113
+
94
114
  const turnRequest = buildInspectorTurnRequest(admitted, {
95
115
  packageRoot: env.packageRoot,
96
116
  home: env.home,
@@ -101,16 +121,7 @@ export async function runPublicInspector(
101
121
  ...(env.correlationId === undefined || env.correlationId.trim() === ""
102
122
  ? {}
103
123
  : { correlationId: env.correlationId }),
104
- continuation: {
105
- kind: "initial",
106
- prompt: buildInspectorTransportPrompt(
107
- admitted,
108
- engineSessionMaterialFromOptions({
109
- ...(env.engine === undefined ? {} : { engine: env.engine }),
110
- packageRoot: env.packageRoot,
111
- }),
112
- ),
113
- },
124
+ continuation,
114
125
  });
115
126
 
116
127
  return await runPostAdmissionOneShot({