@akagilnc/pi-workflow-roles 0.1.5845 → 0.1.5855

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 (66) hide show
  1. package/README.md +4 -4
  2. package/README.zh-CN.md +4 -4
  3. package/dist/acp-host/production-host.js +348 -1254
  4. package/dist/acp-host/role-turn-host.js +4 -98
  5. package/dist/headless-host/production-host.js +15731 -16582
  6. package/dist/headless-host/role-turn-host.js +6 -31
  7. package/dist/migrate-book-topology.js +31 -16
  8. package/dist/navigator-attendance.js +54 -404
  9. package/dist/navigator-invocation-identity.js +0 -2
  10. package/dist/navigator-lookup-host.js +41 -0
  11. package/dist/navigator-lookup.js +68 -0
  12. package/dist/navigator-route-table.js +42 -0
  13. package/dist/package-contracts/navigator-output.js +3 -11
  14. package/dist/packaged-role-registry.js +3 -27
  15. package/dist/public-cli/instruction-seat-run.js +4 -1
  16. package/dist/public-cli/invocation.js +13 -26
  17. package/dist/public-cli/main.js +286 -114
  18. package/dist/public-cli/option-definitions.js +24 -4
  19. package/dist/public-cli/post-admission.js +0 -9
  20. package/dist/public-cli/role-seat-runtime.js +14 -0
  21. package/dist/public-cli/settlement.js +14 -40
  22. package/dist/public-cli/terminal.js +1 -8
  23. package/dist/public-cli/turn-request.js +0 -3
  24. package/dist/public-role-summons.js +0 -1
  25. package/dist/receipt-delivery-policy.js +0 -23
  26. package/dist/role-runtime-dependencies.js +4 -11
  27. package/dist/role-runtime.js +18 -194
  28. package/dist/submission-ledger.js +19 -15
  29. package/dist/ticket-progress.js +23 -5
  30. package/extensions/role-runtime.ts +0 -16
  31. package/package.json +1 -1
  32. package/scripts/build-package.mjs +3 -7
  33. package/src/acp-host/role-turn-host.ts +8 -97
  34. package/src/headless-host/description.ts +0 -2
  35. package/src/headless-host/role-turn-host.ts +7 -32
  36. package/src/host-contracts.ts +5 -7
  37. package/src/navigator-attendance.ts +86 -549
  38. package/src/navigator-invocation-identity.ts +0 -2
  39. package/src/navigator-lookup-host.ts +45 -0
  40. package/src/navigator-lookup.ts +98 -0
  41. package/src/navigator-route-table.ts +92 -0
  42. package/src/package-contracts/navigator-output.ts +4 -15
  43. package/src/packaged-role-registry.ts +3 -32
  44. package/src/public-cli/instruction-seat-run.ts +3 -0
  45. package/src/public-cli/invocation.ts +18 -30
  46. package/src/public-cli/option-definitions.ts +25 -4
  47. package/src/public-cli/post-admission.ts +0 -11
  48. package/src/public-cli/role-seat-runtime.ts +14 -0
  49. package/src/public-cli/settlement.ts +23 -51
  50. package/src/public-cli/terminal.ts +3 -18
  51. package/src/public-cli/turn-request.ts +0 -5
  52. package/src/public-role-summons.ts +0 -8
  53. package/src/receipt-delivery-policy.ts +0 -21
  54. package/src/role-runtime-dependencies.ts +4 -11
  55. package/src/role-runtime.ts +19 -210
  56. package/src/submission-ledger.ts +23 -16
  57. package/src/ticket-progress.ts +32 -5
  58. package/dist/navigator-public-session.js +0 -205
  59. package/dist/navigator-role.js +0 -6
  60. package/dist/navigator-session-contracts.js +0 -227
  61. package/dist/navigator-work-context.js +0 -72
  62. package/resources/navigator-route-playbook.md +0 -33
  63. package/src/navigator-public-session.ts +0 -305
  64. package/src/navigator-role.ts +0 -15
  65. package/src/navigator-session-contracts.ts +0 -320
  66. package/src/navigator-work-context.ts +0 -97
@@ -1,82 +1,18 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
4
-
5
- import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
6
- import { type Static } from "typebox";
7
-
8
- import {
9
- NAVIGATOR_INVOCATION_ENTRY,
10
- NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY,
11
- mintNavigatorInvocationId,
12
- } from "./navigator-invocation-identity.ts";
13
- import { PACKAGED_ROLE_REGISTRY, type PackagedRole } from "./packaged-role-registry.ts";
14
- import {
15
- activationBookDirectory,
16
- resolveActivationLedgerHome,
17
- } from "./activation-ledger-topology.ts";
18
1
  import type { HostContext } from "./host-contracts.ts";
19
- import { createNativeNavigatorSessionFactory } from "./navigator-public-session.ts";
2
+ import { mintNavigatorInvocationId, NAVIGATOR_INVOCATION_ENTRY } from "./navigator-invocation-identity.ts";
20
3
  import {
21
- NAVIGATOR_PREPARE_TOOL_NAME,
22
4
  NavigatorUnavailableError,
23
- navigatorModelSettingPath,
24
- navigatorProviderFailure,
25
- navigatorProviderFailureFromDiagnostics,
26
- navigatorProviderFailureFromError,
27
- navigatorProviderFailureFromPublicTerminal,
28
- navigatorProviderFailureFromStatus,
29
5
  navigatorUnavailableError,
30
- parseNavigatorModelSetting,
31
- readNavigatorModelSetting,
32
- resolveNavigatorSeatSelection,
33
- writeNavigatorModelSetting,
34
- type NavigatorPreparationSession,
35
- type NavigatorProviderFailureFact,
36
- type NavigatorSessionFactory,
6
+ type NavigatorLookup,
37
7
  type NavigatorUnavailableKey,
38
- } from "./navigator-session-contracts.ts";
39
- import { sitianReport } from "./sitian-facade.ts";
40
-
41
- export {
42
- NAVIGATOR_PREPARE_TOOL_NAME,
43
- NavigatorUnavailableError,
44
- navigatorModelSettingPath,
45
- navigatorProviderFailure,
46
- navigatorProviderFailureFromDiagnostics,
47
- navigatorProviderFailureFromError,
48
- navigatorProviderFailureFromPublicTerminal,
49
- navigatorProviderFailureFromStatus,
50
- navigatorUnavailableError,
51
- parseNavigatorModelSetting,
52
- readNavigatorModelSetting,
53
- writeNavigatorModelSetting,
54
- type NavigatorPreparationSession,
55
- type NavigatorProviderFailureFact,
56
- type NavigatorSessionFactory,
57
- type NavigatorUnavailableKey,
58
- };
59
- export { createNativeNavigatorSessionFactory };
60
- export { resolveNavigatorSeatSelection };
61
- import { issueRoot, subjectPath } from "./work-subject-identity.ts";
62
- import { createReceiptDeliveryPolicy } from "./receipt-delivery-policy.ts";
63
- import {
64
- byStatusToPlainObject,
65
- mergePreparedAdvice,
66
- navigatorAdviceBodySchema,
67
- pickPreparedProse,
68
- preparedAdviceFromUnknown,
69
- type PreparedNavigatorAdvice,
70
- } from "./package-contracts/navigator-output.ts";
71
- import { sha256Hex } from "./sha256.ts";
72
- import { isRecord } from "./unknown-value.ts";
8
+ } from "./navigator-lookup.ts";
9
+ import type { PackagedRole } from "./packaged-role-registry.ts";
10
+ import { pickPreparedProse, type PreparedNavigatorAdvice } from "./package-contracts/navigator-output.ts";
11
+ import { subjectPath } from "./work-subject-identity.ts";
73
12
 
74
13
  export const NAVIGATOR_EVENT_TYPE = "ak-navigator-attendance" as const;
75
- export { NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY };
76
-
77
- /** Every public role is a lawful navigator route target (#675 — no nested-only seats). */
78
- export const NAVIGATOR_TARGETS = PACKAGED_ROLE_REGISTRY
79
- .map(({ role, phases }) => ({ role, phases }));
14
+ export { NAVIGATOR_INVOCATION_ENTRY, NavigatorUnavailableError, navigatorUnavailableError };
15
+ export type { NavigatorUnavailableKey };
80
16
 
81
17
  export type NavigatorTargetRole = PackagedRole;
82
18
  export type NavigatorPhase = "plan" | "apply" | null;
@@ -86,28 +22,17 @@ export type NavigatorSettlement =
86
22
  | { kind: "role_infrastructure_failure"; role: string; phase: NavigatorPhase }
87
23
  | { kind: "arrival"; role: "lander"; phase: null; message?: string };
88
24
 
89
- export type NavigatorSubjectProvenance = "placeholder" | "role_input" | "user_prompt";
90
-
91
- /** #1187: auto prepare identity only — no subject/authority material fields. */
92
- export type NavigatorWorkContext = {
93
- subjectKey: string;
94
- subjectProvenance: NavigatorSubjectProvenance;
95
- contextError?: unknown;
96
- };
97
-
98
25
  /**
99
- * #959: Navigator speaks free-form prose. Code does not parse, rank, or judge
100
- * the advice — only whether attendance itself failed (host/process).
26
+ * #959 / #1192: the navigator speaks the table's own words. Code does not parse,
27
+ * rank, or judge them — only whether the lookup itself failed.
101
28
  */
102
29
  export type NavigatorReport = {
103
30
  /** Affirmative attendance only. Lawful no-advice is typed, never inferred from absence. */
104
31
  disposition: "advice" | "no-advice" | "unavailable" | "arrival";
105
- /** Present when disposition is advice — navigator words, presented as-is. */
32
+ /** Present when disposition is advice — presented as-is. */
106
33
  prose?: string;
107
34
  unavailableReason?: string;
108
35
  unavailableSource?: NavigatorUnavailableKey;
109
- unavailableCause?: NavigatorUnavailableKey;
110
- routePlaybookReadFailure?: string;
111
36
  arrivalMessage?: string;
112
37
  };
113
38
 
@@ -121,165 +46,40 @@ export type NavigatorEvent = {
121
46
  prose?: string;
122
47
  unavailableReason?: string;
123
48
  unavailableSource?: NavigatorUnavailableKey;
124
- unavailableCause?: NavigatorUnavailableKey;
125
- routePlaybookReadFailure?: string;
126
49
  arrivalMessage?: string;
127
50
  };
128
51
 
129
- // #959 / #1160: prepare reuses the single navigator advice field owner.
130
- // Object root only (ADR 0060); nested shape is never a gate — every object root
131
- // reaches execute exactly once (Rule 0).
132
- const prepareSchema = navigatorAdviceBodySchema;
133
- type PrepareOutput = Static<typeof prepareSchema>;
134
-
135
52
  export type NavigatorAttendanceOptions = {
136
53
  context: HostContext;
137
54
  role: string;
138
55
  phase: NavigatorPhase;
139
56
  subjectKey: string;
140
- createSession: NavigatorSessionFactory;
141
- modelSettingPath?: string;
142
- contextError?: unknown;
57
+ lookup: NavigatorLookup;
143
58
  /** Exact principal owned by shared role lifecycle; attendance never overrides it. */
144
59
  invocationId?: string;
145
- /** Effective ceiling already resolved for this turn (#1132). */
146
- deliveryRequestLimit?: number;
147
60
  onEvent: (event: NavigatorEvent, report: NavigatorReport) => void | Promise<void>;
148
61
  };
149
62
 
150
- const INVOCATION_ENTRY = NAVIGATOR_INVOCATION_ENTRY;
151
- const SETTLEMENT_ENTRY = "ak-navigator-settlement";
152
- const unavailableKeys = new Set<NavigatorUnavailableKey>(["context", "session", "model", "thinking", "auth", "quota", "transport", "unknown"]);
153
-
154
- function unavailableKey(value: unknown): NavigatorUnavailableKey | undefined {
155
- return typeof value === "string" && unavailableKeys.has(value as NavigatorUnavailableKey)
156
- ? value as NavigatorUnavailableKey
157
- : undefined;
158
- }
159
-
160
- /** Correlate one rejected prepare call/result inside the just-finished prompt. */
161
- function rejectedPrepareReason(entries: readonly unknown[], start: number): string | undefined {
162
- const recent = entries.slice(start);
163
- const prepareCalls = new Set<string>();
164
- for (const entry of recent) {
165
- if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)
166
- || entry.message.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
167
- for (const part of entry.message.content) {
168
- if (isRecord(part) && part.type === "toolCall" && part.name === NAVIGATOR_PREPARE_TOOL_NAME
169
- && typeof part.id === "string") prepareCalls.add(part.id);
170
- }
171
- }
172
- let reason: string | undefined;
173
- for (const entry of recent) {
174
- if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)
175
- || entry.message.role !== "toolResult" || entry.message.isError !== true) continue;
176
- const callId = entry.message.toolCallId;
177
- if (entry.message.toolName !== NAVIGATOR_PREPARE_TOOL_NAME
178
- || typeof callId !== "string" || !prepareCalls.has(callId)) {
179
- return undefined;
180
- }
181
- const content = entry.message.content;
182
- const text = Array.isArray(content)
183
- ? content.flatMap((part) => isRecord(part) && typeof part.text === "string" ? [part.text] : []).join("")
184
- : typeof content === "string" ? content : "";
185
- if (text.trim() !== "") reason = text.trim();
186
- }
187
- return reason;
188
- }
189
-
190
- /**
191
- * #959 / #1160: extract prepared advice from any prepare submission shape.
192
- * Missing/empty → undefined. Never a rejection — shape is not an admission gate.
193
- * Production payload arrives via nested public summon → prepare tool.execute only;
194
- * archivist entries() never carries assistant message text, so no parallel harvest.
195
- */
196
- function normalizePreparedAdvice(value: unknown): PreparedNavigatorAdvice | undefined {
197
- return preparedAdviceFromUnknown(value);
198
- }
199
-
200
- export function navigatorSubjectKey(
201
- subjectRoot: string,
202
- subject: string,
203
- provenance: NavigatorSubjectProvenance = "role_input",
204
- ): string {
205
- if (issueRoot(subjectRoot) !== undefined || !subjectRoot.includes("/.ak/work/")) return subjectRoot;
206
- if (provenance === "placeholder") return subjectRoot;
207
- const normalized = subject.trim().replace(/\s+/g, " ");
208
- if (normalized === "") return subjectRoot;
209
- return `${subjectRoot}#${sha256Hex(normalized).slice(0, 32)}`;
210
- }
211
-
212
- /**
213
- * Role inputs for one ad-hoc work item live below role-specific run folders.
214
- * The folder and filename are transport, not identity: the shared work root
215
- * keeps natural inputs on one subject.
216
- */
217
- export function navigatorSubjectKeyForInput(subjectRoot: string, reference: string, cwd = process.cwd()): string {
218
- if (issueRoot(subjectRoot) !== undefined || !subjectRoot.includes("/.ak/work/")) return subjectRoot;
219
- const resolvedReference = resolve(cwd, reference);
220
- const marker = "/runs/";
221
- if (resolvedReference.includes(marker)) {
222
- // The work root, not a role-input filename, is the stable subject.
223
- // Different roots remain isolated without inventing a filename convention.
224
- return subjectRoot;
225
- }
226
- return navigatorSubjectKey(subjectRoot, resolvedReference);
227
- }
228
-
229
- export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => void): ToolDefinition {
230
- return {
231
- name: NAVIGATOR_PREPARE_TOOL_NAME,
232
- label: "游奕使准备",
233
- description: "提交游奕使散文建议。",
234
- parameters: prepareSchema,
235
- async execute(_id, value) {
236
- // Rule 0: the unique prepare submission is accepted once. Shape is never a gate.
237
- onOutput(value as PrepareOutput);
238
- return { content: [], details: value, terminate: true as const };
239
- },
240
- };
241
- }
242
-
243
63
  export function formatNavigatorReport(report: NavigatorReport): string {
244
- const playbookFailure = report.routePlaybookReadFailure === undefined
245
- ? []
246
- : [report.routePlaybookReadFailure];
247
- if (report.disposition === "no-advice") return playbookFailure.join("\n");
248
- if (report.disposition === "unavailable") {
249
- return [...playbookFailure, ...(report.unavailableReason ? [report.unavailableReason] : [])].join("\n");
250
- }
251
- if (report.disposition === "arrival") {
252
- return [...playbookFailure, ...(report.arrivalMessage ? [report.arrivalMessage] : [])].join("\n");
253
- }
254
- // advice — prose as-is
255
- return [...playbookFailure, ...(report.prose ? [report.prose] : [])].join("\n");
64
+ if (report.disposition === "no-advice") return "";
65
+ if (report.disposition === "unavailable") return report.unavailableReason ?? "";
66
+ if (report.disposition === "arrival") return report.arrivalMessage ?? "";
67
+ return report.prose ?? "";
256
68
  }
257
69
 
258
70
  export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
71
+ const { subjectKey } = options;
259
72
  let preparation: Promise<PreparedNavigatorAdvice | undefined> | undefined;
260
73
  let preparationInFlight = false;
261
- let sessionReady: Promise<NavigatorPreparationSession> | undefined;
262
- let session: NavigatorPreparationSession | undefined;
263
- let subjectKey = options.subjectKey;
264
- // #1187: auto prepare is identity-only (role/phase/subjectKey).
265
- let contextError = options.contextError;
266
- /** Parallel-prepare result: status-keyed prose picked at settle (#1160). */
74
+ /** Looked-up rows: status-keyed prose picked at settle (#1160). */
267
75
  let preparedAdvice: PreparedNavigatorAdvice | undefined;
268
- // Shared lifecycle owns the principal when supplied; otherwise mint once per attendance.
269
- const invocationPrincipal = options.invocationId ?? mintNavigatorInvocationId();
270
- let activeInvocationId: string | undefined = invocationPrincipal;
271
- let outputSink: ((value: PrepareOutput) => void) | undefined;
272
- let settlementTail: Promise<void> = Promise.resolve();
273
- let settlementFailure: unknown;
274
76
  let preparationFailure: unknown;
275
- let observedRoutePlaybookFailure: string | undefined;
77
+ let settlementTail: Promise<void> = Promise.resolve();
276
78
  let disposed = false;
277
- /** In-flight session teardown; repeat dispose returns the same promise (#959 mutation proof). */
278
- let closing: Promise<void> | undefined;
79
+ const invocationId = options.invocationId ?? mintNavigatorInvocationId();
279
80
  // Shared attendance seam owns nested summon cancel (ADR 0018 / #959).
280
- // Role session factory only forwards HostContext.signal — never its own controller.
281
81
  const nestCancel = new AbortController();
282
- const sessionHostContext = (): HostContext => {
82
+ const lookupContext = (): HostContext => {
283
83
  const parentSignal = options.context.signal;
284
84
  const signal = parentSignal === undefined
285
85
  ? nestCancel.signal
@@ -287,231 +87,88 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
287
87
  return { ...options.context, signal };
288
88
  };
289
89
 
290
- const unavailable = (invocationId: string, reason: unknown): NavigatorReport => {
291
- const failure = reason instanceof NavigatorUnavailableError
292
- ? reason
293
- : navigatorUnavailableError("unknown", reason);
90
+ const unavailable = (reason: unknown): NavigatorReport => {
91
+ const failure = navigatorUnavailableError("unknown", reason);
294
92
  return {
295
93
  disposition: "unavailable",
296
94
  unavailableReason: failure.message,
297
95
  unavailableSource: failure.unavailableSource,
298
- unavailableCause: failure.unavailableCause,
299
96
  };
300
97
  };
301
- /**
302
- * #1160: prepare runs the model in parallel from parent start and stores byStatus.
303
- * settle picks by settlement.status — no second model round.
304
- * Soul and route playbook stay on the navigator system prompt, not this user turn.
305
- */
306
- const loadMaterialsAndSession = async (invocationId: string): Promise<NavigatorPreparationSession> => {
307
- if (contextError !== undefined) throw navigatorUnavailableError("context", contextError);
308
- // #1187: prepare runs from identity (role/phase/subjectKey). Parent dispatch
309
- // and authority copies are not required materials for the auto prepare round.
310
- const modelPromise = (async () => {
311
- try {
312
- const resolved = await resolveNavigatorSeatSelection(options.context);
313
- return resolved.configuredLabel;
314
- } catch (error) {
315
- if (error instanceof NavigatorUnavailableError) throw error;
316
- throw navigatorUnavailableError("model", error);
317
- }
318
- })();
319
- const modelSetting = await modelPromise;
320
- let model: ReturnType<typeof parseNavigatorModelSetting>;
321
- try {
322
- model = parseNavigatorModelSetting(modelSetting);
323
- } catch (error) {
324
- throw navigatorUnavailableError("model", error);
325
- }
326
- const tool = createNavigatorPrepareTool((value) => { outputSink?.(value); });
327
- if (session === undefined) {
328
- sessionReady = (async () => {
329
- let created: NavigatorPreparationSession;
330
- try {
331
- created = await options.createSession({
332
- context: sessionHostContext(),
333
- subject: subjectKey,
334
- ...(options.modelSettingPath === undefined ? {} : { modelSettingPath: options.modelSettingPath }),
335
- tool,
336
- });
337
- } catch (error) {
338
- throw navigatorUnavailableError("session", error);
339
- }
340
- if (disposed) {
341
- await created.dispose();
342
- throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
343
- }
98
+
99
+ async function settleOnce(settlement: NavigatorSettlement): Promise<void> {
100
+ // Dispose during post-role grace must ignore late completion entirely (#675).
101
+ if (disposed) return;
102
+ let report: NavigatorReport;
103
+ if (settlement.kind === "arrival") {
104
+ // Arrival is presentation-only: never waits for the lookup.
105
+ report = preparationFailure !== undefined
106
+ ? unavailable(preparationFailure)
107
+ : {
108
+ disposition: "arrival",
109
+ ...(settlement.message === undefined ? {} : { arrivalMessage: settlement.message }),
110
+ };
111
+ } else {
112
+ if (preparation !== undefined) {
344
113
  try {
345
- await created.setModel?.(modelSetting, model.thinkingLevel);
346
- if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
347
- created.appendEntry(INVOCATION_ENTRY, { invocationId, role: options.role, phase: options.phase, subjectKey });
348
- if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
349
- session = created;
350
- return created;
114
+ await preparation;
351
115
  } catch (error) {
352
- if (session !== created) await created.dispose();
353
- throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("session", error);
116
+ preparationFailure ??= error;
354
117
  }
355
- })();
356
- await sessionReady;
357
- sessionReady = undefined;
358
- } else {
359
- try {
360
- await session.setModel?.(modelSetting, model.thinkingLevel);
361
- } catch (error) {
362
- throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("session", error);
118
+ preparation = undefined;
363
119
  }
364
- session.appendEntry(INVOCATION_ENTRY, { invocationId, role: options.role, phase: options.phase, subjectKey });
365
- }
366
- if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
367
- if (session === undefined) throw new Error("Navigator session was not created");
368
- return session;
369
- };
370
- const prepare = async (): Promise<PreparedNavigatorAdvice | undefined> => {
371
- // Exact principal is owned by shared lifecycle (or one mint per attendance).
372
- // Model/tool/advice paths cannot override it; role-session persistence is
373
- // pi.appendEntry at lifecycle start — not optional sessionManager probing.
374
- const invocationId = invocationPrincipal;
375
- activeInvocationId = invocationId;
376
- let output: PrepareOutput | undefined;
377
- let prepareBatchRejected = false;
378
- outputSink = (value) => {
379
- // #836 / #1160: extra prepare calls merge byStatus (same key concatenates) and prose.
380
- const next = normalizePreparedAdvice(value);
381
- if (output === undefined) {
382
- output = value;
383
- return;
120
+ if (preparationFailure !== undefined) {
121
+ // Contract: README.md#Navigator-attendance — failed attendance is typed
122
+ // unavailable without invalidating the role Receipt; retain the cause.
123
+ report = unavailable(preparationFailure);
124
+ } else {
125
+ const status = "status" in settlement ? settlement.status : undefined;
126
+ const picked = pickPreparedProse(preparedAdvice, status);
127
+ // Missing key / no rows → affirmative no-advice; never an invented line.
128
+ report = typeof picked === "string" && picked.trim() !== ""
129
+ ? { disposition: "advice", prose: picked }
130
+ : { disposition: "no-advice" };
384
131
  }
385
- const merged = mergePreparedAdvice(normalizePreparedAdvice(output), next);
386
- output = {
387
- ...(merged?.prose === undefined ? {} : { prose: merged.prose }),
388
- ...(merged === undefined || merged.byStatus.size === 0
389
- ? {}
390
- : { byStatus: byStatusToPlainObject(merged.byStatus) }),
391
- };
392
- };
393
- const activeSession = await loadMaterialsAndSession(invocationId);
394
- // #1160: parallel prepare from parent start — model runs now; settle only picks.
395
- // #1187: identity only. Parent dispatch / authority copies stay off this wire;
396
- // navigator reads ticket and station records itself when needed.
397
- const request = JSON.stringify({
398
- kind: "prepare",
132
+ }
133
+ const event: NavigatorEvent = {
134
+ version: 1,
135
+ disposition: report.disposition,
136
+ invocationId,
399
137
  role: options.role,
400
138
  phase: options.phase,
401
- invocationId,
402
139
  subjectKey,
403
- });
140
+ ...(report.prose === undefined ? {} : { prose: report.prose }),
141
+ ...(report.unavailableReason === undefined ? {} : { unavailableReason: report.unavailableReason }),
142
+ ...(report.unavailableSource === undefined ? {} : { unavailableSource: report.unavailableSource }),
143
+ ...(report.arrivalMessage === undefined ? {} : { arrivalMessage: report.arrivalMessage }),
144
+ };
145
+ if (disposed) return;
404
146
  try {
405
- try {
406
- if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
407
- // #1132: the ceiling this turn already resolved. Absent = package default.
408
- const delivery = createReceiptDeliveryPolicy(options.deliveryRequestLimit);
409
- // Production payload arrives only via nested summon → prepare tool.execute
410
- // (navigator-public-session). No assistant-entry harvest — entries() is
411
- // archivist custom-only on the wired factory (#959).
412
- const promptAllowingRejectedPrepare = async (text: string) => {
413
- const entryStart = activeSession.entries().length;
414
- prepareBatchRejected = false;
415
- let promptFailure: unknown;
416
- try {
417
- await activeSession.prompt(text);
418
- } catch (error) {
419
- promptFailure = error;
420
- }
421
- const providerFailure = activeSession.providerFailure?.();
422
- if (providerFailure !== undefined) {
423
- throw navigatorUnavailableError(providerFailure.source, promptFailure ?? "Navigator provider failure", providerFailure.cause);
424
- }
425
- const rejectedReason = rejectedPrepareReason(activeSession.entries(), entryStart);
426
- if (rejectedReason !== undefined) {
427
- // A rejected call makes every provisional output from this prompt
428
- // ineligible for publication before the correction turn starts.
429
- output = undefined;
430
- prepareBatchRejected = true;
431
- // The correction prompt below is the one spend for this rejection.
432
- delivery.recordRejected(rejectedReason);
433
- return;
434
- }
435
- if (promptFailure !== undefined) throw promptFailure;
436
- const sessionNoReceipt = activeSession.noReceipt?.();
437
- if (sessionNoReceipt !== undefined) {
438
- // This session already settled without an accepted receipt. Keep the
439
- // larger issued count and stop; a nested zero must not wipe prompts
440
- // this layer already sent (#675 / #1132).
441
- delivery.recordNestedNoReceipt(sessionNoReceipt);
442
- return;
443
- }
444
- };
445
- await promptAllowingRejectedPrepare(request);
446
- // Correction after rejected prepare on the sole model round (#1160).
447
- // #1208: resend this turn's prepare request; delivery-state stays on policy facts.
448
- while (output === undefined && prepareBatchRejected && delivery.nextAction() === "request-delivery") {
449
- delivery.recordDeliveryRequest();
450
- await promptAllowingRejectedPrepare(request);
451
- }
452
- if (output === undefined && delivery.nextAction() === "request-delivery") {
453
- delivery.closeBudget();
454
- }
455
- if (output === undefined && delivery.nextAction() === "no-receipt" && activeSession.providerFailure?.() === undefined) {
456
- // Nested @navigator run already holds lawful no_receipt + runPointer when
457
- // present. Do not write a parallel parent lifecycle off a dead side-branch
458
- // pointer (#1178). Honest empty prep → settle reports no-advice.
459
- preparedAdvice = undefined;
460
- return undefined;
461
- }
462
- } catch (error) {
463
- throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("transport", error);
464
- }
465
- if (output === undefined) {
466
- const nativeFailure = [...activeSession.entries()].reverse().find((entry: unknown) => {
467
- if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) return false;
468
- return entry.message.role === "assistant" && typeof entry.message.errorMessage === "string" && entry.message.errorMessage.trim() !== "";
469
- });
470
- const nativeMessage = isRecord(nativeFailure) && isRecord(nativeFailure.message) ? nativeFailure.message : undefined;
471
- const errorMessage = nativeMessage !== undefined && typeof nativeMessage.errorMessage === "string"
472
- ? nativeMessage.errorMessage
473
- : "Navigator did not submit direction advice";
474
- // Classification originates only at the native provider stream seam.
475
- // AssistantMessage metadata is a human diagnostic surface, not an acceptance oracle.
476
- const providerFailure = activeSession.providerFailure?.();
477
- const source = providerFailure?.source ?? "unknown";
478
- const cause = providerFailure?.cause ?? source;
479
- throw navigatorUnavailableError(source, errorMessage, cause);
480
- }
481
- preparedAdvice = normalizePreparedAdvice(output);
482
- return preparedAdvice;
483
- } finally {
484
- const playbookFailure = activeSession.routePlaybookReadFailure?.();
485
- observedRoutePlaybookFailure = typeof playbookFailure === "string" && playbookFailure.trim() !== ""
486
- ? playbookFailure
487
- : undefined;
488
- outputSink = undefined;
147
+ await options.onEvent(event, report);
148
+ } catch (error) {
149
+ // Race: dispose landed between the check and onEvent; drop only then.
150
+ if (disposed) return;
151
+ throw error;
489
152
  }
490
- };
153
+ preparation = undefined;
154
+ preparedAdvice = undefined;
155
+ preparationFailure = undefined;
156
+ }
491
157
 
492
158
  return {
493
- setWorkContext(next: NavigatorWorkContext): void | Promise<void> {
494
- let closing: void | Promise<void> | undefined;
495
- if (next.subjectKey !== subjectKey && session !== undefined) {
496
- const previous = session;
497
- session = undefined;
498
- closing = previous.dispose();
499
- }
500
- subjectKey = next.subjectKey;
501
- contextError = next.contextError;
502
- return closing;
503
- },
504
159
  prepare(): void {
505
- if (disposed || preparation !== undefined) return;
160
+ // Soul: 「plan 阶段停用」— a plan-phase parent gets no lookup at all.
161
+ if (disposed || preparation !== undefined || options.phase === "plan") return;
506
162
  preparationFailure = undefined;
507
163
  preparedAdvice = undefined;
508
- // Track live in-flight only — a resolved preparation still held until settle
509
- // drain must not read as preparing (test helpers release by this).
510
164
  preparationInFlight = true;
511
- preparation = prepare().finally(() => {
512
- preparationInFlight = false;
513
- });
514
- // Contract: README.md#Navigator-attendance — background preparation rejection is drained so the later typed settlement can report unavailable; retain the exact cause until settlement.
165
+ preparation = options.lookup({ context: lookupContext(), role: options.role })
166
+ .then((prepared) => {
167
+ preparedAdvice = prepared;
168
+ return prepared;
169
+ })
170
+ .finally(() => { preparationInFlight = false; });
171
+ // Contract: README.md#Navigator-attendance — background lookup rejection is drained so the later typed settlement can report unavailable; the exact cause is retained until settlement.
515
172
  void preparation.catch((error) => { preparationFailure = error; });
516
173
  },
517
174
  isPreparing(): boolean {
@@ -519,140 +176,20 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
519
176
  },
520
177
  settle(settlement: NavigatorSettlement): Promise<void> {
521
178
  const next = settlementTail.then(() => settleOnce(settlement));
522
- // Contract: README.md#Navigator-attendance — rejected attendance settlements are drained only to serialize later attendance; retain the exact rejection for the caller/audit path.
523
- settlementTail = next.catch((error) => { settlementFailure = error; });
179
+ // Contract: README.md#Navigator-attendance — rejected attendance settlements are drained only to serialize later attendance; the rejection still reaches the caller.
180
+ settlementTail = next.catch(() => undefined);
524
181
  return next;
525
182
  },
526
- dispose(): void | Promise<void> {
183
+ dispose(): void {
527
184
  disposed = true;
528
- // Abort nested public summon via shared signal; callers may still await session
529
- // close. Parent grace/shutdown must choose not to await (#959 reopen).
185
+ // Abort the nested lookup summon via the shared signal (ADR 0018 / #959).
530
186
  if (!nestCancel.signal.aborted) {
531
- nestCancel.abort(navigatorUnavailableError(
532
- "session",
533
- new Error("Navigator attendance was disposed"),
534
- ));
187
+ nestCancel.abort(navigatorUnavailableError("session", new Error("Navigator attendance was disposed")));
535
188
  }
536
- // Leave sessionReady so an in-flight createSession observes disposed and drains exactly once.
537
- // Late settleOnce completion observes disposed and skips onEvent.
538
- activeInvocationId = undefined;
539
- if (closing === undefined) {
540
- const current = session;
541
- session = undefined;
542
- // Preserve rejection for non-blocking recordDisposeFailure; resolve void on success.
543
- closing = Promise.resolve(current?.dispose()).then(() => undefined);
544
- }
545
- // Same in-flight teardown on repeat dispose — awaiting here re-blocks the parent court.
546
- return closing;
547
189
  },
548
190
  };
549
-
550
- async function settleOnce(settlement: NavigatorSettlement): Promise<void> {
551
- // Dispose during post-role grace must ignore late completion entirely (#675).
552
- if (disposed) return;
553
- // Keep playbook failure observed on the prepare round; settle does not re-prompt (#1160).
554
- const invocationId = activeInvocationId ?? invocationPrincipal;
555
- let report: NavigatorReport;
556
- const settlementFact = {
557
- invocationId,
558
- subjectKey,
559
- role: settlement.role,
560
- phase: settlement.phase,
561
- kind: settlement.kind,
562
- ...("status" in settlement && settlement.status !== undefined
563
- ? { status: settlement.status }
564
- : {}),
565
- };
566
- // Arrival is presentation-only: wait only for nest open, never for the model round.
567
- if (settlement.kind === "arrival") {
568
- if (sessionReady !== undefined) {
569
- try { await sessionReady; } catch (error) { preparationFailure ??= error; }
570
- }
571
- session?.appendEntry(SETTLEMENT_ENTRY, settlementFact);
572
- if (preparationFailure !== undefined) {
573
- report = unavailable(invocationId, preparationFailure);
574
- } else {
575
- report = {
576
- disposition: "arrival",
577
- ...(settlement.message === undefined ? {} : { arrivalMessage: settlement.message }),
578
- };
579
- }
580
- } else {
581
- // Drain in-flight parallel prepare, then pick by status — no second model round (#1160).
582
- if (sessionReady !== undefined) {
583
- try { await sessionReady; } catch (error) { preparationFailure ??= error; }
584
- }
585
- if (preparation !== undefined) {
586
- try {
587
- await preparation;
588
- } catch (error) {
589
- preparationFailure ??= error;
590
- }
591
- preparation = undefined;
592
- }
593
- session?.appendEntry(SETTLEMENT_ENTRY, settlementFact);
594
- if (preparationFailure !== undefined) {
595
- // Contract: README.md#Navigator-attendance — failed attendance is typed
596
- // unavailable without invalidating the role Receipt; retain the cause.
597
- report = unavailable(invocationId, preparationFailure);
598
- } else {
599
- const status = "status" in settlement ? settlement.status : undefined;
600
- const picked = pickPreparedProse(preparedAdvice, status);
601
- if (typeof picked === "string" && picked.trim() !== "") {
602
- // #959 / #1160: present the pre-written prose as-is on every parent outcome
603
- // including human_decision / escalate. No settlement model round.
604
- report = { disposition: "advice", prose: picked };
605
- } else {
606
- // Missing key / empty prepare / no prior prepare → affirmative no-advice.
607
- report = { disposition: "no-advice" };
608
- }
609
- }
610
- }
611
- if (observedRoutePlaybookFailure !== undefined) {
612
- report = { ...report, routePlaybookReadFailure: observedRoutePlaybookFailure };
613
- }
614
- const event: NavigatorEvent = {
615
- version: 1,
616
- disposition: report.disposition,
617
- invocationId,
618
- role: options.role,
619
- phase: options.phase,
620
- subjectKey,
621
- ...(report.prose === undefined ? {} : { prose: report.prose }),
622
- ...(report.unavailableReason === undefined ? {} : { unavailableReason: report.unavailableReason }),
623
- ...(report.unavailableSource === undefined ? {} : { unavailableSource: report.unavailableSource }),
624
- ...(report.unavailableCause === undefined ? {} : { unavailableCause: report.unavailableCause }),
625
- ...(report.routePlaybookReadFailure === undefined ? {} : { routePlaybookReadFailure: report.routePlaybookReadFailure }),
626
- ...(report.arrivalMessage === undefined ? {} : { arrivalMessage: report.arrivalMessage }),
627
- };
628
- // Dispose during post-role grace must ignore late completion (ADR 0052 / #106 / #675).
629
- // Every settled disposition (advice | no-advice | unavailable | arrival) is affirmative.
630
- // Only the disposed bit is typed authority to drop — never match free-text Error.message.
631
- if (disposed) return;
632
- try {
633
- await options.onEvent(event, report);
634
- } catch (error) {
635
- // Race: dispose landed between the check and onEvent; drop only then.
636
- if (disposed) return;
637
- throw error;
638
- }
639
- preparation = undefined;
640
- sessionReady = undefined;
641
- preparedAdvice = undefined;
642
- preparationFailure = undefined;
643
- observedRoutePlaybookFailure = undefined;
644
- }
645
191
  }
646
192
 
647
193
  export type NavigatorAttendance = ReturnType<typeof createNavigatorAttendance>;
648
194
 
649
- export function registerNavigatorModelCommand(pi: ExtensionAPI, path = navigatorModelSettingPath()): void {
650
- pi.registerCommand("navigator-model", {
651
- description: "Set the persistent Navigator model (provider/model[:max]).",
652
- handler: async (args) => {
653
- await writeNavigatorModelSetting(args.trim(), path);
654
- },
655
- });
656
- }
657
-
658
195
  export { subjectPath };