@akagilnc/pi-workflow-roles 0.1.1876 → 0.1.1883

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.
@@ -1,113 +1,105 @@
1
- import { lstatSync, realpathSync, statSync, writeFileSync, } from "node:fs";
2
- import { dirname, isAbsolute, resolve } from "node:path";
3
- import { activationBookDirectory, ensureRealDirectoryTree, errnoCode, errorText, pathContainedIn, } from "./activation-ledger-topology.js";
4
- /**
5
- * Typed missing durable session principal. Callers discriminate with instanceof/code;
6
- * never by parsing message prose. Original filesystem causes are retained.
7
- */
8
- export class ActivationSessionFileMissingError extends Error {
9
- code = "AK_ACTIVATION_SESSION_FILE_MISSING";
10
- path;
11
- constructor(path, options) {
12
- super(`Workflow role activation durable session file does not exist: ${path}`, options?.cause === undefined ? undefined : { cause: options.cause });
13
- this.name = "ActivationSessionFileMissingError";
14
- this.path = path;
15
- }
1
+ import {
2
+ lstatSync,
3
+ realpathSync,
4
+ statSync,
5
+ writeFileSync
6
+ } from "node:fs";
7
+ import { isAbsolute, resolve } from "node:path";
8
+ import {
9
+ errnoCode,
10
+ errorText
11
+ } from "./activation-ledger-topology.js";
12
+ class ActivationSessionFileMissingError extends Error {
13
+ code = "AK_ACTIVATION_SESSION_FILE_MISSING";
14
+ path;
15
+ constructor(path, options) {
16
+ super(
17
+ `Workflow role activation durable session file does not exist: ${path}`,
18
+ options?.cause === void 0 ? void 0 : { cause: options.cause }
19
+ );
20
+ this.name = "ActivationSessionFileMissingError";
21
+ this.path = path;
22
+ }
16
23
  }
17
- function materializeDeferredSessionFile(sessionManager, resolvedFile, ledgerHome) {
18
- const header = sessionManager.getHeader?.();
19
- if (header === null || header === undefined || header.type !== "session") {
20
- throw new ActivationSessionFileMissingError(resolvedFile);
21
- }
22
- ensureRealDirectoryTree(ledgerHome, dirname(resolvedFile));
23
- try {
24
- writeFileSync(resolvedFile, `${JSON.stringify(header)}\n`, { flag: "wx" });
25
- }
26
- catch (error) {
27
- if (errnoCode(error) !== "EEXIST") {
28
- throw new Error(`Workflow role activation failed to materialize durable session file (${resolvedFile}): ${errorText(error)}`, { cause: error });
29
- }
30
- // Lost a create race — validate the winner below.
31
- }
32
- // Rebind full SessionManager so subsequent Pi appends use O_APPEND (flushed=true)
33
- // instead of exclusive wx create against the file we just wrote.
34
- if (typeof sessionManager.setSessionFile === "function") {
35
- sessionManager.setSessionFile(resolvedFile);
36
- }
24
+ function materializeDeferredSessionFile(sessionManager, resolvedFile) {
25
+ const header = sessionManager.getHeader?.();
26
+ if (header === null || header === void 0 || header.type !== "session") {
27
+ throw new ActivationSessionFileMissingError(resolvedFile);
28
+ }
29
+ try {
30
+ writeFileSync(resolvedFile, `${JSON.stringify(header)}
31
+ `, { flag: "wx" });
32
+ } catch (error) {
33
+ if (errnoCode(error) !== "EEXIST") {
34
+ throw new Error(
35
+ `Workflow role activation failed to materialize durable session file (${resolvedFile}): ${errorText(error)}`,
36
+ { cause: error }
37
+ );
38
+ }
39
+ }
40
+ if (typeof sessionManager.setSessionFile === "function") {
41
+ sessionManager.setSessionFile(resolvedFile);
42
+ }
37
43
  }
38
- /**
39
- * Admit only a durable Pi session file principal under the resolved machine ledger
40
- * book (ADR 0048). Requires an existing regular file at admission: resolve real paths
41
- * and prove containment under the real book. Reject relative paths, outside-book paths,
42
- * directories, symlink escapes, and nonexistent paths that cannot be materialized from
43
- * the live SessionManager header. Original filesystem causes are retained.
44
- *
45
- * Upstream Pi defers exclusive create until the first assistant message. When the path
46
- * is the live SessionManager principal under the book and only the header is in memory,
47
- * admission materializes that header onto the same path before the fact is written so
48
- * the role fact never points at a session that may be created later.
49
- */
50
- export function durableSessionPointer(sessionManager, options) {
51
- const file = sessionManager.getSessionFile?.();
52
- if (typeof file !== "string" || file.length === 0) {
53
- throw new Error("Workflow role activation requires a durable Pi session file principal (getSessionFile); directory-only or --no-session invocations are rejected");
54
- }
55
- if (!isAbsolute(file)) {
56
- throw new Error(`Workflow role activation requires an absolute durable session file path under the machine ledger book; got relative path: ${file}`);
57
- }
58
- const resolvedFile = resolve(file);
59
- const bookRoot = resolve(activationBookDirectory(options.ledgerHome, options.bookKey));
60
- if (!pathContainedIn(bookRoot, resolvedFile)) {
61
- throw new Error(`Workflow role activation requires the durable session file principal under the machine ledger book (${bookRoot}); got: ${resolvedFile}`);
62
- }
63
- try {
64
- lstatSync(resolvedFile);
65
- }
66
- catch (error) {
67
- if (errnoCode(error) !== "ENOENT") {
68
- throw new Error(`Workflow role activation failed to stat durable session file (${resolvedFile}): ${errorText(error)}`, { cause: error });
69
- }
70
- try {
71
- materializeDeferredSessionFile(sessionManager, resolvedFile, options.ledgerHome);
72
- }
73
- catch (materializeError) {
74
- // Missing principal: rethrow typed missing-file identity with the original ENOENT cause.
75
- // Other materialize failures keep their own typed/native identity.
76
- if (materializeError instanceof ActivationSessionFileMissingError) {
77
- throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
78
- }
79
- throw materializeError;
80
- }
44
+ function durableSessionPointer(sessionManager) {
45
+ const file = sessionManager.getSessionFile?.();
46
+ if (typeof file !== "string" || file.length === 0) {
47
+ throw new Error(
48
+ "Workflow role activation requires a durable Pi session file principal (getSessionFile); directory-only or --no-session invocations are rejected"
49
+ );
50
+ }
51
+ if (!isAbsolute(file)) {
52
+ throw new Error(
53
+ `Workflow role activation requires an absolute durable session file path; got relative path: ${file}`
54
+ );
55
+ }
56
+ const resolvedFile = resolve(file);
57
+ try {
58
+ lstatSync(resolvedFile);
59
+ } catch (error) {
60
+ if (errnoCode(error) !== "ENOENT") {
61
+ throw new Error(
62
+ `Workflow role activation failed to stat durable session file (${resolvedFile}): ${errorText(error)}`,
63
+ { cause: error }
64
+ );
81
65
  }
82
- let realBook;
83
66
  try {
84
- realBook = realpathSync(bookRoot);
85
- }
86
- catch (error) {
87
- throw new Error(`Workflow role activation machine ledger book is not resolvable (${bookRoot}): ${errorText(error)}`, { cause: error });
88
- }
89
- let realFile;
90
- try {
91
- realFile = realpathSync(resolvedFile);
92
- }
93
- catch (error) {
67
+ materializeDeferredSessionFile(sessionManager, resolvedFile);
68
+ } catch (materializeError) {
69
+ if (materializeError instanceof ActivationSessionFileMissingError) {
94
70
  throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
95
- }
96
- if (!pathContainedIn(realBook, realFile)) {
97
- throw new Error(`Workflow role activation requires the durable session file principal under the machine ledger book (${realBook}); got: ${realFile}`);
98
- }
99
- let info;
100
- try {
101
- info = statSync(realFile);
102
- }
103
- catch (error) {
104
- throw new Error(`Workflow role activation failed to stat durable session file (${realFile}): ${errorText(error)}`, { cause: error });
105
- }
106
- if (info.isDirectory()) {
107
- throw new Error(`Workflow role activation durable session principal must be a file, not a directory: ${realFile}`);
108
- }
109
- if (!info.isFile()) {
110
- throw new Error(`Workflow role activation durable session principal is not a regular file: ${realFile}`);
111
- }
112
- return { kind: "session-file", path: realFile };
71
+ }
72
+ throw materializeError;
73
+ }
74
+ }
75
+ let realFile;
76
+ try {
77
+ realFile = realpathSync(resolvedFile);
78
+ } catch (error) {
79
+ throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
80
+ }
81
+ let info;
82
+ try {
83
+ info = statSync(realFile);
84
+ } catch (error) {
85
+ throw new Error(
86
+ `Workflow role activation failed to stat durable session file (${realFile}): ${errorText(error)}`,
87
+ { cause: error }
88
+ );
89
+ }
90
+ if (info.isDirectory()) {
91
+ throw new Error(
92
+ `Workflow role activation durable session principal must be a file, not a directory: ${realFile}`
93
+ );
94
+ }
95
+ if (!info.isFile()) {
96
+ throw new Error(
97
+ `Workflow role activation durable session principal is not a regular file: ${realFile}`
98
+ );
99
+ }
100
+ return { kind: "session-file", path: realFile };
113
101
  }
102
+ export {
103
+ ActivationSessionFileMissingError,
104
+ durableSessionPointer
105
+ };
@@ -1,19 +1,58 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, writeFileSync } from "node:fs";
3
- import { dirname, resolve, join } from "node:path";
2
+ import { existsSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, resolve, join, relative, sep } from "node:path";
4
4
  import { SessionManager } from "@earendil-works/pi-coding-agent";
5
5
  import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
6
6
  import {
7
7
  roleRunSessionCoordinates
8
8
  } from "./sitian-role-run-coordinates.js";
9
9
  import {
10
+ ActivationLedgerError,
10
11
  activationBookDirectory,
11
12
  ensureRealDirectoryTree,
13
+ errorText,
14
+ pathContainedIn,
12
15
  physicallyContainedIn,
13
16
  resolveActivationLedgerHome
14
17
  } from "./activation-ledger-topology.js";
15
18
  const { findMostRecentSession } = await import(new URL("./core/session-manager.js", import.meta.resolve("@earendil-works/pi-coding-agent")).href);
16
19
  const WORKER_SUBMISSION_GATE_KIND = "worker-submission-gate";
20
+ function assertRecentFinalFileUnderLedgerHome(ledgerHome, sessionDir, recentFile) {
21
+ const absoluteHome = resolve(ledgerHome);
22
+ const absoluteSessionDir = resolve(sessionDir);
23
+ const absoluteFile = resolve(recentFile);
24
+ const relToHome = relative(absoluteHome, absoluteSessionDir);
25
+ const segments = relToHome.split(sep);
26
+ if (relToHome === "" || isAbsolute(relToHome) || relToHome === ".." || relToHome.startsWith(`..${sep}`) || segments[0] !== "books" || segments[1] === void 0 || segments[1] === "" || segments[1] === "." || segments[1] === "..") {
27
+ throw new ActivationLedgerError(
28
+ `sitian record sessionDir must be under a ledger book (${ledgerHome}): ${sessionDir}`
29
+ );
30
+ }
31
+ const bookRoot = join(absoluteHome, "books", segments[1]);
32
+ let realBookRoot;
33
+ try {
34
+ realBookRoot = realpathSync(bookRoot);
35
+ } catch (error) {
36
+ throw new ActivationLedgerError(
37
+ `activation ledger book is not resolvable (${bookRoot}): ${errorText(error)}`,
38
+ { cause: error }
39
+ );
40
+ }
41
+ let realFile;
42
+ try {
43
+ realFile = realpathSync(absoluteFile);
44
+ } catch (error) {
45
+ throw new ActivationLedgerError(
46
+ `sitian record session file is not resolvable (${absoluteFile}): ${errorText(error)}`,
47
+ { cause: error }
48
+ );
49
+ }
50
+ if (realFile !== realBookRoot && !pathContainedIn(realBookRoot, realFile)) {
51
+ throw new ActivationLedgerError(
52
+ `sitian record session must be under the ledger book (${bookRoot}): ${recentFile}`
53
+ );
54
+ }
55
+ }
17
56
  function createRecordSession(options) {
18
57
  const cwd = options.cwd;
19
58
  const parentFile = options.parent?.getSessionFile();
@@ -39,7 +78,10 @@ function createRecordSession(options) {
39
78
  const mayResumeSameNest = options.subject !== void 0 || options.kind === WORKER_SUBMISSION_GATE_KIND;
40
79
  if (mayResumeSameNest) {
41
80
  const recentFile = findMostRecentSession(sessionDir, cwd);
42
- if (recentFile !== null) return SessionManager.open(recentFile, sessionDir, cwd);
81
+ if (recentFile !== null) {
82
+ assertRecentFinalFileUnderLedgerHome(ledgerHome, sessionDir, recentFile);
83
+ return SessionManager.open(recentFile, sessionDir, cwd);
84
+ }
43
85
  }
44
86
  const session = SessionManager.create(
45
87
  cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.1876",
3
+ "version": "0.1.1883",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -4,13 +4,10 @@ import {
4
4
  statSync,
5
5
  writeFileSync,
6
6
  } from "node:fs";
7
- import { dirname, isAbsolute, resolve } from "node:path";
7
+ import { isAbsolute, resolve } from "node:path";
8
8
  import {
9
- activationBookDirectory,
10
- ensureRealDirectoryTree,
11
9
  errnoCode,
12
10
  errorText,
13
- pathContainedIn,
14
11
  } from "./activation-ledger-topology.ts";
15
12
 
16
13
  /** Durable pointer to the authoritative Pi session file principal (ADR 0048/0049). */
@@ -47,13 +44,14 @@ export class ActivationSessionFileMissingError extends Error {
47
44
  function materializeDeferredSessionFile(
48
45
  sessionManager: ActivationSessionManager,
49
46
  resolvedFile: string,
50
- ledgerHome: string,
51
47
  ): void {
52
48
  const header = sessionManager.getHeader?.();
53
49
  if (header === null || header === undefined || header.type !== "session") {
54
50
  throw new ActivationSessionFileMissingError(resolvedFile);
55
51
  }
56
- ensureRealDirectoryTree(ledgerHome, dirname(resolvedFile));
52
+ // Activation does not own record placement (ADR 0065 / #221). Parent directory is the
53
+ // SessionManager principal's own dir — write the header in place; do not pull ledgerHome
54
+ // containment or ensureRealDirectoryTree back onto this admission path.
57
55
  try {
58
56
  writeFileSync(resolvedFile, `${JSON.stringify(header)}\n`, { flag: "wx" });
59
57
  } catch (error) {
@@ -73,23 +71,23 @@ function materializeDeferredSessionFile(
73
71
  }
74
72
 
75
73
  /**
76
- * Admit only a durable Pi session file principal under the resolved machine ledger
77
- * book (ADR 0048). Requires an existing regular file at admission: resolve real paths
78
- * and prove containment under the real book. Reject relative paths, outside-book paths,
79
- * directories, symlink escapes, and nonexistent paths that cannot be materialized from
80
- * the live SessionManager header. Original filesystem causes are retained.
74
+ * Admit a durable Pi session file principal (ADR 0048/0065).
75
+ * Requires an existing regular file at admission (or a live SessionManager header that
76
+ * can be materialized onto the same path). Rejects relative paths, directories,
77
+ * non-files, and nonexistent paths that cannot be materialized. Original filesystem
78
+ * causes are retained.
79
+ *
80
+ * Record-placement (session must live under the ledger book) is NOT enforced here —
81
+ * that check lives on the sitian record entry (createRecordSession). Activation only
82
+ * binds the durable principal identity for the role run.
81
83
  *
82
84
  * Upstream Pi defers exclusive create until the first assistant message. When the path
83
- * is the live SessionManager principal under the book and only the header is in memory,
84
- * admission materializes that header onto the same path before the fact is written so
85
- * the role fact never points at a session that may be created later.
85
+ * is the live SessionManager principal and only the header is in memory, admission
86
+ * materializes that header onto the same path before the fact is written so the role
87
+ * fact never points at a session that may be created later.
86
88
  */
87
89
  export function durableSessionPointer(
88
90
  sessionManager: ActivationSessionManager,
89
- options: {
90
- ledgerHome: string;
91
- bookKey: string;
92
- },
93
91
  ): ActivationSessionPointer {
94
92
  const file = sessionManager.getSessionFile?.();
95
93
  if (typeof file !== "string" || file.length === 0) {
@@ -99,17 +97,11 @@ export function durableSessionPointer(
99
97
  }
100
98
  if (!isAbsolute(file)) {
101
99
  throw new Error(
102
- `Workflow role activation requires an absolute durable session file path under the machine ledger book; got relative path: ${file}`,
100
+ `Workflow role activation requires an absolute durable session file path; got relative path: ${file}`,
103
101
  );
104
102
  }
105
103
 
106
104
  const resolvedFile = resolve(file);
107
- const bookRoot = resolve(activationBookDirectory(options.ledgerHome, options.bookKey));
108
- if (!pathContainedIn(bookRoot, resolvedFile)) {
109
- throw new Error(
110
- `Workflow role activation requires the durable session file principal under the machine ledger book (${bookRoot}); got: ${resolvedFile}`,
111
- );
112
- }
113
105
 
114
106
  try {
115
107
  lstatSync(resolvedFile);
@@ -121,7 +113,7 @@ export function durableSessionPointer(
121
113
  );
122
114
  }
123
115
  try {
124
- materializeDeferredSessionFile(sessionManager, resolvedFile, options.ledgerHome);
116
+ materializeDeferredSessionFile(sessionManager, resolvedFile);
125
117
  } catch (materializeError) {
126
118
  // Missing principal: rethrow typed missing-file identity with the original ENOENT cause.
127
119
  // Other materialize failures keep their own typed/native identity.
@@ -132,16 +124,6 @@ export function durableSessionPointer(
132
124
  }
133
125
  }
134
126
 
135
- let realBook: string;
136
- try {
137
- realBook = realpathSync(bookRoot);
138
- } catch (error) {
139
- throw new Error(
140
- `Workflow role activation machine ledger book is not resolvable (${bookRoot}): ${errorText(error)}`,
141
- { cause: error },
142
- );
143
- }
144
-
145
127
  let realFile: string;
146
128
  try {
147
129
  realFile = realpathSync(resolvedFile);
@@ -149,12 +131,6 @@ export function durableSessionPointer(
149
131
  throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
150
132
  }
151
133
 
152
- if (!pathContainedIn(realBook, realFile)) {
153
- throw new Error(
154
- `Workflow role activation requires the durable session file principal under the machine ledger book (${realBook}); got: ${realFile}`,
155
- );
156
- }
157
-
158
134
  let info: ReturnType<typeof statSync>;
159
135
  try {
160
136
  info = statSync(realFile);
@@ -869,7 +869,7 @@ export function createRoleRuntimeExtension(
869
869
  const bookKey = resolveBookKeyFromGit(ctx.cwd);
870
870
  const correlation = correlationIdentityFromEnv();
871
871
  const ledgerHome = resolveActivationLedgerHome();
872
- const session = durableSessionPointer(ctx.sessionManager, { ledgerHome, bookKey });
872
+ const session = durableSessionPointer(ctx.sessionManager);
873
873
 
874
874
  if (dependencies.createNavigatorAttendance !== undefined) {
875
875
  let work: NavigatorWorkContext;
@@ -4,8 +4,8 @@
4
4
  * 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
5
5
  */
6
6
  import { createHash } from "node:crypto";
7
- import { existsSync, writeFileSync } from "node:fs";
8
- import { dirname, resolve, join } from "node:path";
7
+ import { existsSync, realpathSync, writeFileSync } from "node:fs";
8
+ import { dirname, isAbsolute, resolve, join, relative, sep } from "node:path";
9
9
  import { SessionManager } from "@earendil-works/pi-coding-agent";
10
10
 
11
11
  import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
@@ -14,8 +14,11 @@ export {
14
14
  type RoleRunSessionCoordinates,
15
15
  } from "./sitian-role-run-coordinates.ts";
16
16
  import {
17
+ ActivationLedgerError,
17
18
  activationBookDirectory,
18
19
  ensureRealDirectoryTree,
20
+ errorText,
21
+ pathContainedIn,
19
22
  physicallyContainedIn,
20
23
  resolveActivationLedgerHome,
21
24
  } from "./activation-ledger-topology.ts";
@@ -46,9 +49,74 @@ export type CreateRecordSessionOptions = {
46
49
  /** Authorized no-subject kind that may resume the most recent same-nest peer (ADR 0066). Sole string true source for gate resume identity. */
47
50
  export const WORKER_SUBMISSION_GATE_KIND = "worker-submission-gate";
48
51
 
52
+ /**
53
+ * Sole file-level placement lock for a resumed same-nest principal (ADR 0065 / #221).
54
+ * ensureRealDirectoryTree already owns the sessionDir chain; a final .jsonl symlink is
55
+ * invisible to that directory walk, so this runs once before SessionManager.open.
56
+ * Circle is the book physical root that owns sessionDir — not merely ledger home — so a
57
+ * books/A nest cannot resume a final file whose realpath lands in books/B.
58
+ * realpath/stat failures stay typed ActivationLedgerError with original cause — never
59
+ * wash through physicalPathIdentity's non-ENOENT lexical fallback.
60
+ */
61
+ function assertRecentFinalFileUnderLedgerHome(
62
+ ledgerHome: string,
63
+ sessionDir: string,
64
+ recentFile: string,
65
+ ): void {
66
+ const absoluteHome = resolve(ledgerHome);
67
+ const absoluteSessionDir = resolve(sessionDir);
68
+ const absoluteFile = resolve(recentFile);
69
+ // sessionDir just passed ensureRealDirectoryTree: derive the owning book lexically.
70
+ const relToHome = relative(absoluteHome, absoluteSessionDir);
71
+ const segments = relToHome.split(sep);
72
+ if (
73
+ relToHome === ""
74
+ || isAbsolute(relToHome)
75
+ || relToHome === ".."
76
+ || relToHome.startsWith(`..${sep}`)
77
+ || segments[0] !== "books"
78
+ || segments[1] === undefined
79
+ || segments[1] === ""
80
+ || segments[1] === "."
81
+ || segments[1] === ".."
82
+ ) {
83
+ throw new ActivationLedgerError(
84
+ `sitian record sessionDir must be under a ledger book (${ledgerHome}): ${sessionDir}`,
85
+ );
86
+ }
87
+ const bookRoot = join(absoluteHome, "books", segments[1]);
88
+ let realBookRoot: string;
89
+ try {
90
+ realBookRoot = realpathSync(bookRoot);
91
+ } catch (error) {
92
+ throw new ActivationLedgerError(
93
+ `activation ledger book is not resolvable (${bookRoot}): ${errorText(error)}`,
94
+ { cause: error },
95
+ );
96
+ }
97
+ let realFile: string;
98
+ try {
99
+ realFile = realpathSync(absoluteFile);
100
+ } catch (error) {
101
+ throw new ActivationLedgerError(
102
+ `sitian record session file is not resolvable (${absoluteFile}): ${errorText(error)}`,
103
+ { cause: error },
104
+ );
105
+ }
106
+ if (realFile !== realBookRoot && !pathContainedIn(realBookRoot, realFile)) {
107
+ throw new ActivationLedgerError(
108
+ `sitian record session must be under the ledger book (${bookRoot}): ${recentFile}`,
109
+ );
110
+ }
111
+ }
112
+
49
113
  /**
50
114
  * Sole package entry that constructs a durable Pi session record (ADR 0065).
51
115
  * No destination/path parameters — location is computed from ledger topology only.
116
+ * SessionDir placement is owned by ensureRealDirectoryTree; resumed recent final-file
117
+ * identity is checked once before SessionManager.open (directory walk cannot see a
118
+ * trailing .jsonl symlink). New principals mint under the already-validated sessionDir
119
+ * via destination-free SessionManager.create — no derived postcondition.
52
120
  * Resume via Pi findMostRecentSession is limited to subject-keyed identity and the
53
121
  * authorized worker-submission-gate durable path. Ordinary no-subject children
54
122
  * (evidence-children, auditor-roles, …) always mint a fresh session — never reopen
@@ -88,6 +156,7 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
88
156
  parentSession = parentFile;
89
157
  }
90
158
 
159
+ // Directory-chain ownership: containment + physical components (no parallel assert).
91
160
  ensureRealDirectoryTree(ledgerHome, sessionDir);
92
161
  // Subject-keyed nests continue by subject digest; gate durable resume is the only
93
162
  // authorized no-subject same-nest continuation. All other kinds mint fresh.
@@ -95,7 +164,10 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
95
164
  options.subject !== undefined || options.kind === WORKER_SUBMISSION_GATE_KIND;
96
165
  if (mayResumeSameNest) {
97
166
  const recentFile = findMostRecentSession(sessionDir, cwd);
98
- if (recentFile !== null) return SessionManager.open(recentFile, sessionDir, cwd);
167
+ if (recentFile !== null) {
168
+ assertRecentFinalFileUnderLedgerHome(ledgerHome, sessionDir, recentFile);
169
+ return SessionManager.open(recentFile, sessionDir, cwd);
170
+ }
99
171
  }
100
172
 
101
173
  const session = SessionManager.create(