@akagilnc/pi-workflow-roles 0.1.1840 → 0.1.1854

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.
@@ -5,17 +5,31 @@
5
5
  * does not statically reach @earendil-works/pi-coding-agent.
6
6
  */
7
7
  import { createAgentSession, DefaultResourceLoader, SettingsManager, } from "@earendil-works/pi-coding-agent";
8
+ import { createRecordSession } from "./sitian-record-entry.js";
9
+ function resolveSessionManager(options) {
10
+ if (options.sessionManager !== undefined)
11
+ return options.sessionManager;
12
+ return createRecordSession({
13
+ cwd: options.cwd,
14
+ kind: options.kind,
15
+ ...(options.subject === undefined ? {} : { subject: options.subject }),
16
+ ...(options.parent === undefined ? {} : { parent: options.parent }),
17
+ });
18
+ }
8
19
  /**
9
20
  * Single createAgentSession + Settings construction for all in-process children.
21
+ * Child SessionManager construction for identity-declared records lives here so
22
+ * role modules do not call createRecordSession directly.
10
23
  */
11
24
  export async function openInProcessAgentSession(options) {
12
25
  const settings = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: false } });
26
+ const sessionManager = resolveSessionManager(options);
13
27
  const createArgs = {
14
28
  cwd: options.cwd,
15
29
  model: options.model,
16
30
  thinkingLevel: options.thinkingLevel ?? "off",
17
31
  modelRuntime: options.modelRuntime,
18
- sessionManager: options.sessionManager,
32
+ sessionManager,
19
33
  settingsManager: settings,
20
34
  ...(options.noTools === undefined ? {} : { noTools: options.noTools }),
21
35
  ...(options.tools === undefined ? {} : { tools: options.tools }),
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
- import { ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
5
+ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
6
6
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
7
7
  import { Type } from "typebox";
8
8
  import { Value } from "typebox/value";
@@ -11,8 +11,6 @@ import {
11
11
  mintNavigatorInvocationId
12
12
  } from "./navigator-invocation-identity.js";
13
13
  import { PACKAGED_ROLE_REGISTRY, packagedRoleMetadata } from "./packaged-role-registry.js";
14
- import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
15
- import { activationBookDirectory, resolveActivationLedgerHome } from "./activation-ledger-topology.js";
16
14
  import { openInProcessAgentSession } from "./in-process-session.js";
17
15
  import { renderPublicAkRoleCommand } from "./public-command-renderer.js";
18
16
  import { issueRoot, subjectPath } from "./work-subject-identity.js";
@@ -218,14 +216,6 @@ function navigatorSubjectKeyForInput(subjectRoot, reference, cwd = process.cwd()
218
216
  }
219
217
  return navigatorSubjectKey(subjectRoot, resolvedReference);
220
218
  }
221
- function subjectDirectory(cwd, subjectKey) {
222
- const book = activationBookDirectory(
223
- resolveActivationLedgerHome(),
224
- resolveBookKeyFromGit(cwd)
225
- );
226
- const digest = createHash("sha256").update(subjectKey).digest("hex").slice(0, 32);
227
- return join(book, "navigator", digest);
228
- }
229
219
  function navigatorModelSettingPath() {
230
220
  return join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "navigator-model.json");
231
221
  }
@@ -341,7 +331,6 @@ function createNavigatorAttendance(options) {
341
331
  let subject = options.subject;
342
332
  let authority = options.authority;
343
333
  let contextError = options.contextError;
344
- let sessionDir = options.sessionDir;
345
334
  let candidates;
346
335
  const invocationPrincipal = options.invocationId ?? mintNavigatorInvocationId();
347
336
  let activeInvocationId = invocationPrincipal;
@@ -454,7 +443,7 @@ ${text}
454
443
  sessionReady = (async () => {
455
444
  let created;
456
445
  try {
457
- created = await options.createSession({ context: options.context, sessionDir, ...options.modelSettingPath === void 0 ? {} : { modelSettingPath: options.modelSettingPath }, tool });
446
+ created = await options.createSession({ context: options.context, subject: subjectKey, ...options.modelSettingPath === void 0 ? {} : { modelSettingPath: options.modelSettingPath }, tool });
458
447
  } catch (error) {
459
448
  throw navigatorUnavailableError("session", error);
460
449
  }
@@ -584,7 +573,7 @@ ${helpContext}
584
573
  await promptAllowingRejectedPrepare(RECEIPT_DELIVERY_PROMPT, true);
585
574
  }
586
575
  if (output === void 0 && delivery.nextAction() === "no-receipt" && activeSession.providerFailure?.() === void 0) {
587
- const facts = delivery.facts({ runPointer: sessionDir, attemptPointer: invocationId });
576
+ const facts = delivery.facts({ runPointer: activeSession.recordPointer(), attemptPointer: invocationId });
588
577
  activeSession.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
589
578
  preparationNoReceipt = true;
590
579
  candidates = [];
@@ -622,7 +611,6 @@ ${helpContext}
622
611
  subject = next.subject;
623
612
  authority = next.authority;
624
613
  contextError = next.contextError;
625
- sessionDir = options.sessionDirectory?.(next.subjectKey) ?? options.sessionDir;
626
614
  },
627
615
  /**
628
616
  * Start live-help subprocesses during activation without beginning full
@@ -777,7 +765,7 @@ ${helpContext}
777
765
  }
778
766
  function createNativeNavigatorSessionFactory(defaultModelSettingPath = navigatorModelSettingPath()) {
779
767
  const sharedModelRuntime = ModelRuntime.create({ allowModelNetwork: false });
780
- return async ({ context, sessionDir, modelSettingPath, tool }) => {
768
+ return async ({ context, subject, modelSettingPath, tool }) => {
781
769
  let configured;
782
770
  try {
783
771
  configured = await readNavigatorModelSetting(modelSettingPath ?? defaultModelSettingPath);
@@ -800,15 +788,6 @@ function createNativeNavigatorSessionFactory(defaultModelSettingPath = navigator
800
788
  throw navigatorUnavailableError("auth", error);
801
789
  }
802
790
  if (!auth.ok) throw new NavigatorUnavailableError("auth", auth.error);
803
- try {
804
- const sessionInfo = await stat(sessionDir);
805
- if (!sessionInfo.isDirectory()) throw new NavigatorUnavailableError("session", `Navigator session path is not a directory: ${sessionDir}`);
806
- } catch (error) {
807
- if (error instanceof NavigatorUnavailableError) throw error;
808
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
809
- throw navigatorUnavailableError("session", error);
810
- }
811
- }
812
791
  let providerFailure;
813
792
  const assignProviderFailure = (fact) => {
814
793
  if (fact !== void 0) providerFailure = fact;
@@ -959,10 +938,12 @@ function createNativeNavigatorSessionFactory(defaultModelSettingPath = navigator
959
938
  try {
960
939
  opened = await openInProcessAgentSession({
961
940
  cwd: context.cwd,
941
+ kind: "navigator",
942
+ subject,
943
+ parent: context.sessionManager,
962
944
  model,
963
945
  modelRuntime,
964
946
  thinkingLevel: parsed.thinkingLevel,
965
- sessionManager: SessionManager.continueRecent(context.cwd, sessionDir),
966
947
  noTools: "all",
967
948
  tools: [NAVIGATOR_PREPARE_TOOL_NAME],
968
949
  customTools: [tool]
@@ -1016,6 +997,7 @@ function createNativeNavigatorSessionFactory(defaultModelSettingPath = navigator
1016
997
  }
1017
998
  },
1018
999
  getThinkingLevel: () => opened.session.thinkingLevel,
1000
+ recordPointer: () => opened.session.sessionManager.getSessionDir(),
1019
1001
  dispose: () => opened.dispose()
1020
1002
  };
1021
1003
  };
@@ -1028,11 +1010,6 @@ function registerNavigatorModelCommand(pi, path = navigatorModelSettingPath()) {
1028
1010
  }
1029
1011
  });
1030
1012
  }
1031
- function navigatorSessionDirectory(context, subjectKey) {
1032
- const current = context.sessionManager.getSessionDir();
1033
- const key = subjectKey ?? subjectPath(current, context.cwd);
1034
- return subjectDirectory(context.cwd, key);
1035
- }
1036
1013
  export {
1037
1014
  NAVIGATOR_DEFAULT_MODEL,
1038
1015
  NAVIGATOR_EVENT_TYPE,
@@ -1048,7 +1025,6 @@ export {
1048
1025
  navigatorModelSettingPath,
1049
1026
  navigatorProviderFailure,
1050
1027
  navigatorProviderFailureFromError,
1051
- navigatorSessionDirectory,
1052
1028
  navigatorSubjectKey,
1053
1029
  navigatorSubjectKeyForInput,
1054
1030
  navigatorUnavailableError,
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { dirname, resolve, join } from "node:path";
2
3
  import { SessionManager } from "@earendil-works/pi-coding-agent";
3
4
  import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
@@ -10,13 +11,26 @@ import {
10
11
  physicallyContainedIn,
11
12
  resolveActivationLedgerHome
12
13
  } from "./activation-ledger-topology.js";
14
+ const { findMostRecentSession } = await import(new URL("./core/session-manager.js", import.meta.resolve("@earendil-works/pi-coding-agent")).href);
13
15
  function createRecordSession(options) {
14
16
  const cwd = options.cwd;
15
17
  const parentFile = options.parent?.getSessionFile();
18
+ const ledgerHome = resolveActivationLedgerHome();
19
+ if (options.subject !== void 0) {
20
+ const digest = createHash("sha256").update(options.subject).digest("hex").slice(0, 32);
21
+ const sessionDir2 = join(
22
+ activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)),
23
+ options.kind,
24
+ digest
25
+ );
26
+ ensureRealDirectoryTree(ledgerHome, sessionDir2);
27
+ const recentFile = findMostRecentSession(sessionDir2, cwd);
28
+ if (recentFile !== null) return SessionManager.open(recentFile, sessionDir2, cwd);
29
+ return SessionManager.create(cwd, sessionDir2, parentFile ? { parentSession: parentFile } : void 0);
30
+ }
16
31
  if (parentFile === void 0 || parentFile.length === 0) {
17
32
  return SessionManager.inMemory(cwd);
18
33
  }
19
- const ledgerHome = resolveActivationLedgerHome();
20
34
  const parentResolved = resolve(parentFile);
21
35
  const sessionDir = physicallyContainedIn(ledgerHome, parentResolved) ? join(dirname(parentResolved), options.kind) : join(activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)), options.kind);
22
36
  ensureRealDirectoryTree(ledgerHome, sessionDir);
@@ -23,7 +23,6 @@ import {
23
23
  createNativeNavigatorSessionFactory,
24
24
  createNavigatorAttendance,
25
25
  navigatorUnavailableError,
26
- navigatorSessionDirectory,
27
26
  navigatorSubjectKey,
28
27
  navigatorSubjectKeyForInput,
29
28
  registerNavigatorModelCommand,
@@ -261,14 +260,11 @@ export default function roleRuntime(pi: ExtensionAPI): void {
261
260
  auditDoctorCompliance: createPiDoctorAuditor(),
262
261
  loadNavigatorWorkContext: (options) => loadNavigatorWorkContext(pi, options),
263
262
  createNavigatorAttendance: (options) => {
264
- const sessionDir = navigatorSessionDirectory(options.context, options.subjectKey);
265
263
  return createNavigatorAttendance({
266
264
  context: options.context,
267
265
  role: options.role,
268
266
  phase: options.phase,
269
267
  subjectKey: options.subjectKey,
270
- sessionDir,
271
- sessionDirectory: (subjectKey) => navigatorSessionDirectory(options.context, subjectKey),
272
268
  subject: options.subject,
273
269
  authority: options.authority,
274
270
  invocationId: options.invocationId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.1840",
3
+ "version": "0.1.1854",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -14,14 +14,15 @@ import {
14
14
  } from "@earendil-works/pi-coding-agent";
15
15
  import type { Api, Model } from "@earendil-works/pi-ai";
16
16
 
17
- export type OpenInProcessAgentSessionOptions = {
17
+ import { createRecordSession } from "./sitian-record-entry.ts";
18
+
19
+ type OpenInProcessAgentSessionBase = {
18
20
  readonly cwd: string;
19
21
  /** Required when loading a resource loader / systemPrompt. Callers own scratch lifecycle. */
20
22
  readonly agentDir?: string;
21
23
  readonly model: Model<Api>;
22
24
  readonly modelRuntime: ModelRuntime;
23
25
  readonly thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
24
- readonly sessionManager: SessionManager;
25
26
  readonly systemPrompt?: string;
26
27
  readonly customTools?: ToolDefinition[];
27
28
  readonly noTools?: "all" | "builtin";
@@ -29,20 +30,56 @@ export type OpenInProcessAgentSessionOptions = {
29
30
  readonly tools?: string[];
30
31
  };
31
32
 
33
+ /**
34
+ * Shared open admits exactly one session source:
35
+ * - pre-built sessionManager, or
36
+ * - kind/subject/parent identity relations (Sitian obtains the manager).
37
+ * Encoded as a type relation — no runtime missing-arg guard.
38
+ */
39
+ export type OpenInProcessAgentSessionOptions =
40
+ | (OpenInProcessAgentSessionBase & {
41
+ readonly sessionManager: SessionManager;
42
+ readonly kind?: never;
43
+ readonly subject?: never;
44
+ readonly parent?: never;
45
+ })
46
+ | (OpenInProcessAgentSessionBase & {
47
+ readonly sessionManager?: undefined;
48
+ /** Record kind for the Sitian entry. */
49
+ readonly kind: string;
50
+ /** Stable work identity for book-level records which continue across role runs. */
51
+ readonly subject?: string;
52
+ /** Optional parent session — supplies parentSession link / nest principal. */
53
+ readonly parent?: { getSessionFile(): string | undefined };
54
+ });
55
+
56
+ function resolveSessionManager(options: OpenInProcessAgentSessionOptions): SessionManager {
57
+ if (options.sessionManager !== undefined) return options.sessionManager;
58
+ return createRecordSession({
59
+ cwd: options.cwd,
60
+ kind: options.kind,
61
+ ...(options.subject === undefined ? {} : { subject: options.subject }),
62
+ ...(options.parent === undefined ? {} : { parent: options.parent }),
63
+ });
64
+ }
65
+
32
66
  /**
33
67
  * Single createAgentSession + Settings construction for all in-process children.
68
+ * Child SessionManager construction for identity-declared records lives here so
69
+ * role modules do not call createRecordSession directly.
34
70
  */
35
71
  export async function openInProcessAgentSession(
36
72
  options: OpenInProcessAgentSessionOptions,
37
73
  ): Promise<{ session: Awaited<ReturnType<typeof createAgentSession>>["session"]; dispose(): void }> {
38
74
  const settings = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: false } });
75
+ const sessionManager = resolveSessionManager(options);
39
76
 
40
77
  const createArgs: Parameters<typeof createAgentSession>[0] = {
41
78
  cwd: options.cwd,
42
79
  model: options.model,
43
80
  thinkingLevel: options.thinkingLevel ?? "off",
44
81
  modelRuntime: options.modelRuntime,
45
- sessionManager: options.sessionManager,
82
+ sessionManager,
46
83
  settingsManager: settings,
47
84
  ...(options.noTools === undefined ? {} : { noTools: options.noTools }),
48
85
  ...(options.tools === undefined ? {} : { tools: options.tools }),
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
 
6
- import { ModelRuntime, SessionManager, type ExtensionContext, type ExtensionAPI, type ToolDefinition } from "@earendil-works/pi-coding-agent";
6
+ import { ModelRuntime, type ExtensionContext, type ExtensionAPI, type ToolDefinition } from "@earendil-works/pi-coding-agent";
7
7
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
8
8
  import { Type, type Static } from "typebox";
9
9
  import { Value } from "typebox/value";
@@ -13,8 +13,6 @@ import {
13
13
  mintNavigatorInvocationId,
14
14
  } from "./navigator-invocation-identity.ts";
15
15
  import { PACKAGED_ROLE_REGISTRY, type PackagedRole, packagedRoleMetadata } from "./packaged-role-registry.ts";
16
- import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
17
- import { activationBookDirectory, resolveActivationLedgerHome } from "./activation-ledger-topology.ts";
18
16
  import { openInProcessAgentSession } from "./in-process-session.ts";
19
17
  import { renderPublicAkRoleCommand } from "./public-command-renderer.ts";
20
18
  import { issueRoot, subjectPath } from "./work-subject-identity.ts";
@@ -233,12 +231,14 @@ export type NavigatorPreparationSession = {
233
231
  providerFailure?(): NavigatorProviderFailureFact | undefined;
234
232
  setModel?(model: string, thinkingLevel: "off" | "max"): Promise<void>;
235
233
  getThinkingLevel?(): string;
234
+ /** Durable record pointer for unchanged no-receipt lifecycle facts. Required — missing pointer fails honestly. */
235
+ recordPointer(): string;
236
236
  dispose(): void;
237
237
  };
238
238
 
239
239
  export type NavigatorSessionFactory = (options: {
240
240
  context: ExtensionContext;
241
- sessionDir: string;
241
+ subject: string;
242
242
  modelSettingPath?: string;
243
243
  tool: ToolDefinition;
244
244
  }) => Promise<NavigatorPreparationSession>;
@@ -248,7 +248,6 @@ export type NavigatorAttendanceOptions = {
248
248
  role: string;
249
249
  phase: NavigatorPhase;
250
250
  subjectKey: string;
251
- sessionDir: string;
252
251
  loadSoul: () => Promise<string>;
253
252
  loadRoutePlaybook?: () => Promise<string>;
254
253
  loadRoleHelp: (role: NavigatorTargetRole) => Promise<string>;
@@ -257,7 +256,6 @@ export type NavigatorAttendanceOptions = {
257
256
  subject: string;
258
257
  authority: string;
259
258
  contextError?: unknown;
260
- sessionDirectory?: (subjectKey: string) => string;
261
259
  /** Exact principal owned by shared role lifecycle; attendance never overrides it. */
262
260
  invocationId?: string;
263
261
  onEvent: (event: NavigatorEvent, report: NavigatorReport) => void | Promise<void>;
@@ -426,15 +424,6 @@ export function navigatorSubjectKeyForInput(subjectRoot: string, reference: stri
426
424
  return navigatorSubjectKey(subjectRoot, resolvedReference);
427
425
  }
428
426
 
429
- function subjectDirectory(cwd: string, subjectKey: string): string {
430
- const book = activationBookDirectory(
431
- resolveActivationLedgerHome(),
432
- resolveBookKeyFromGit(cwd),
433
- );
434
- const digest = createHash("sha256").update(subjectKey).digest("hex").slice(0, 32);
435
- return join(book, "navigator", digest);
436
- }
437
-
438
427
  export function navigatorModelSettingPath(): string {
439
428
  return join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "navigator-model.json");
440
429
  }
@@ -588,7 +577,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
588
577
  let subject = options.subject;
589
578
  let authority = options.authority;
590
579
  let contextError = options.contextError;
591
- let sessionDir = options.sessionDir;
592
580
  let candidates: NavigatorCandidate[] | undefined;
593
581
  // Shared lifecycle owns the principal when supplied; otherwise mint once per attendance.
594
582
  const invocationPrincipal = options.invocationId ?? mintNavigatorInvocationId();
@@ -721,7 +709,7 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
721
709
  sessionReady = (async () => {
722
710
  let created: NavigatorPreparationSession;
723
711
  try {
724
- created = await options.createSession({ context: options.context, sessionDir, ...(options.modelSettingPath === undefined ? {} : { modelSettingPath: options.modelSettingPath }), tool });
712
+ created = await options.createSession({ context: options.context, subject: subjectKey, ...(options.modelSettingPath === undefined ? {} : { modelSettingPath: options.modelSettingPath }), tool });
725
713
  } catch (error) {
726
714
  throw navigatorUnavailableError("session", error);
727
715
  }
@@ -841,7 +829,7 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
841
829
  await promptAllowingRejectedPrepare(RECEIPT_DELIVERY_PROMPT, true);
842
830
  }
843
831
  if (output === undefined && delivery.nextAction() === "no-receipt" && activeSession.providerFailure?.() === undefined) {
844
- const facts = delivery.facts({ runPointer: sessionDir, attemptPointer: invocationId });
832
+ const facts = delivery.facts({ runPointer: activeSession.recordPointer(), attemptPointer: invocationId });
845
833
  activeSession.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
846
834
  preparationNoReceipt = true;
847
835
  candidates = [];
@@ -884,7 +872,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
884
872
  subject = next.subject;
885
873
  authority = next.authority;
886
874
  contextError = next.contextError;
887
- sessionDir = options.sessionDirectory?.(next.subjectKey) ?? options.sessionDir;
888
875
  },
889
876
  /**
890
877
  * Start live-help subprocesses during activation without beginning full
@@ -1056,7 +1043,7 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1056
1043
  // Pre-warm once per factory (per process). Concurrent prepares share the same
1057
1044
  // runtime construction rather than each paying ModelRuntime.create under load.
1058
1045
  const sharedModelRuntime = ModelRuntime.create({ allowModelNetwork: false });
1059
- return async ({ context, sessionDir, modelSettingPath, tool }) => {
1046
+ return async ({ context, subject, modelSettingPath, tool }) => {
1060
1047
  let configured: string;
1061
1048
  try {
1062
1049
  configured = await readNavigatorModelSetting(modelSettingPath ?? defaultModelSettingPath);
@@ -1079,16 +1066,6 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1079
1066
  throw navigatorUnavailableError("auth", error);
1080
1067
  }
1081
1068
  if (!auth.ok) throw new NavigatorUnavailableError("auth", auth.error);
1082
- try {
1083
- const sessionInfo = await stat(sessionDir);
1084
- if (!sessionInfo.isDirectory()) throw new NavigatorUnavailableError("session", `Navigator session path is not a directory: ${sessionDir}`);
1085
- } catch (error) {
1086
- if (error instanceof NavigatorUnavailableError) throw error;
1087
- // Contract: README.md#Navigator-attendance — the resumable session directory may be created by session setup; retain and classify every other filesystem cause.
1088
- if (!(error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT")) {
1089
- throw navigatorUnavailableError("session", error);
1090
- }
1091
- }
1092
1069
  let providerFailure: NavigatorProviderFailureFact | undefined;
1093
1070
  const assignProviderFailure = (fact: NavigatorProviderFailureFact | undefined): void => {
1094
1071
  if (fact !== undefined) providerFailure = fact;
@@ -1253,13 +1230,15 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1253
1230
  }
1254
1231
  let opened: Awaited<ReturnType<typeof openInProcessAgentSession>>;
1255
1232
  try {
1256
- // Shared in-process session open (#233) — same ModelRuntime module instance.
1233
+ // Shared in-process session open (#233) — Sitian SessionManager from identity relations.
1257
1234
  opened = await openInProcessAgentSession({
1258
1235
  cwd: context.cwd,
1236
+ kind: "navigator",
1237
+ subject,
1238
+ parent: context.sessionManager,
1259
1239
  model,
1260
1240
  modelRuntime,
1261
1241
  thinkingLevel: parsed.thinkingLevel,
1262
- sessionManager: SessionManager.continueRecent(context.cwd, sessionDir),
1263
1242
  noTools: "all",
1264
1243
  tools: [NAVIGATOR_PREPARE_TOOL_NAME],
1265
1244
  customTools: [tool],
@@ -1312,6 +1291,7 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
1312
1291
  }
1313
1292
  },
1314
1293
  getThinkingLevel: () => opened.session.thinkingLevel,
1294
+ recordPointer: () => opened.session.sessionManager.getSessionDir(),
1315
1295
  dispose: () => opened.dispose(),
1316
1296
  };
1317
1297
  };
@@ -1326,10 +1306,4 @@ export function registerNavigatorModelCommand(pi: ExtensionAPI, path = navigator
1326
1306
  });
1327
1307
  }
1328
1308
 
1329
- export function navigatorSessionDirectory(context: ExtensionContext, subjectKey?: string): string {
1330
- const current = context.sessionManager.getSessionDir();
1331
- const key = subjectKey ?? subjectPath(current, context.cwd);
1332
- return subjectDirectory(context.cwd, key);
1333
- }
1334
-
1335
1309
  export { subjectPath };
@@ -120,7 +120,7 @@ export function createReviewerRoleRuntime(pi: ExtensionAPI, dependencies: Review
120
120
  if (!registered) {
121
121
  registered = true;
122
122
  pi.registerTool({ name: REVIEWER_OUTPUT_TOOL_NAME, label: "Reviewer Output", description: "Submit the thin Reviewer receipt after semantic compliance audit.", promptSnippet: "Submit the final Reviewer receipt", promptGuidelines: [`Use ${REVIEWER_OUTPUT_TOOL_NAME} as the sole final action.`,
123
- "This runtime provides no sub-agent tool; produce the Standards and Spec review legs sequentially in this session yourself — that is the canonical execution of the review Skill here."], parameters: reviewerOutputSchema,
123
+ "This runtime executes the Standards and Spec review legs for you as package-managed evidence-child sessions — that IS this runtime's implementation of the review Skill's parallel sub-agents. Do not refuse because no Agent tool appears in your tool list, and do not substitute your own sub-processes; work with the legs the runtime provides."], parameters: reviewerOutputSchema,
124
124
  async execute(id, parameters, signal, _update, toolCtx): Promise<AgentToolResult<unknown>> {
125
125
  if (!soul || !binding) throw new Error("Reviewer inputs were not loaded");
126
126
  requireSoleReviewerOutputCall(id, toolCtx);
@@ -337,7 +337,7 @@ export type RoleRuntimeDependencies = {
337
337
  auditDoctorCompliance?(options: { context: ExtensionContext; signal?: AbortSignal }): Promise<ComplianceDecision>;
338
338
  createCollectorClock?(): CollectorClock;
339
339
  collectorPackageExtensionPath?: string;
340
- createNavigatorAttendance?(options: { context: ExtensionContext; role: string; phase: NavigatorPhase; subjectKey: string; subject: string; authority: string; contextError?: unknown; sessionDirectory?: (subjectKey: string) => string; invocationId: string; onEvent: (event: import("./navigator-attendance.ts").NavigatorEvent, report: import("./navigator-attendance.ts").NavigatorReport) => void | Promise<void> }): NavigatorAttendanceDependency | Promise<NavigatorAttendanceDependency>;
340
+ createNavigatorAttendance?(options: { context: ExtensionContext; role: string; phase: NavigatorPhase; subjectKey: string; subject: string; authority: string; contextError?: unknown; invocationId: string; onEvent: (event: import("./navigator-attendance.ts").NavigatorEvent, report: import("./navigator-attendance.ts").NavigatorReport) => void | Promise<void> }): NavigatorAttendanceDependency | Promise<NavigatorAttendanceDependency>;
341
341
  loadNavigatorWorkContext?(options: { context: ExtensionContext; role: string; phase: NavigatorPhase }): Promise<NavigatorWorkContext>;
342
342
  loadCanonicalSkillBinding?(
343
343
  name: "tdd" | "code-review",
@@ -3,6 +3,7 @@
3
3
  * 调用方只声明自己是谁的什么;落点由候簿拓扑算出,签名不含任何落点/路径参数。
4
4
  * 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
5
5
  */
6
+ import { createHash } from "node:crypto";
6
7
  import { dirname, resolve, join } from "node:path";
7
8
  import { SessionManager } from "@earendil-works/pi-coding-agent";
8
9
 
@@ -18,6 +19,13 @@ import {
18
19
  resolveActivationLedgerHome,
19
20
  } from "./activation-ledger-topology.ts";
20
21
 
22
+ // continueRecent always allocates a deferred sessionFile; this is the discovery result.
23
+ const { findMostRecentSession } = await import(
24
+ new URL("./core/session-manager.js", import.meta.resolve("@earendil-works/pi-coding-agent")).href,
25
+ ) as {
26
+ findMostRecentSession: (sessionDir: string, cwd?: string) => string | null;
27
+ };
28
+
21
29
  /** Parent session surface needed to link and (when already under home) nest the record. */
22
30
  export type RecordSessionParent = {
23
31
  getSessionFile(): string | undefined;
@@ -30,6 +38,8 @@ export type CreateRecordSessionOptions = {
30
38
  readonly kind: string;
31
39
  /** Optional parent session — supplies parentSession link; nest under parent only when that parent already lives under the ledger home. */
32
40
  readonly parent?: RecordSessionParent;
41
+ /** Stable work identity for book-level records which continue across role runs. */
42
+ readonly subject?: string;
33
43
  };
34
44
 
35
45
  /**
@@ -39,12 +49,30 @@ export type CreateRecordSessionOptions = {
39
49
  export function createRecordSession(options: CreateRecordSessionOptions): SessionManager {
40
50
  const cwd = options.cwd;
41
51
  const parentFile = options.parent?.getSessionFile();
52
+ const ledgerHome = resolveActivationLedgerHome();
53
+
54
+ if (options.subject !== undefined) {
55
+ const digest = createHash("sha256").update(options.subject).digest("hex").slice(0, 32);
56
+ const sessionDir = join(
57
+ activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)),
58
+ options.kind,
59
+ digest,
60
+ );
61
+ ensureRealDirectoryTree(ledgerHome, sessionDir);
62
+ // Delegate discovery to Pi so custom-directory continuation retains its
63
+ // mtime ordering, valid-header scan, and cwd filtering semantics.
64
+ const recentFile = findMostRecentSession(sessionDir, cwd);
65
+ if (recentFile !== null) return SessionManager.open(recentFile, sessionDir, cwd);
66
+ return SessionManager.create(cwd, sessionDir, parentFile
67
+ ? { parentSession: parentFile }
68
+ : undefined);
69
+ }
70
+
42
71
  if (parentFile === undefined || parentFile.length === 0) {
43
72
  // No durable parent principal — preserve prior in-memory child behavior.
44
73
  return SessionManager.inMemory(cwd);
45
74
  }
46
75
 
47
- const ledgerHome = resolveActivationLedgerHome();
48
76
  const parentResolved = resolve(parentFile);
49
77
 
50
78
  // Nest under parent only when the parent record already lives under the package home.