@akagilnc/pi-workflow-roles 0.1.5679 → 0.1.5698

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.
@@ -20,6 +20,7 @@ import {
20
20
  type AdmittedRoleInvocation,
21
21
  } from "./public-cli/invocation.ts";
22
22
  import { isUnboundRunDirectory, sessionDirectoryOf, sessionFileOf } from "./role-run-placement.ts";
23
+ import { rewriteRunDirectoryPathValue } from "./role-run-path-rewrite.ts";
23
24
  import { readPageSync } from "./run-dossier.ts";
24
25
  import { readDeclaredTicketNumber } from "./run-ticket-number.ts";
25
26
  import { isRecord } from "./unknown-value.ts";
@@ -128,6 +129,11 @@ export async function reportTicketFromHostContext(
128
129
  // Live handles belong with rename ownership — before derived renderCurrentSync
129
130
  // (#1171 F4-R2). Reuse relocate's heldLease.relocate seam; keep raw I/O throw.
130
131
  const applyLiveHandles = (nextDirectory: string): void => {
132
+ // Capture live session handle before ownership transfer. After relocate the
133
+ // write handle must share host-authority coords with the principal (#1183):
134
+ // project the prior path onto the new leaf — never rebuild default
135
+ // session.jsonl over a non-default seal (and do not trust a setter rebuild).
136
+ const priorSessionFile = context.sessionManager.getSessionFile?.();
131
137
  (context as { runDirectory?: string }).runDirectory = nextDirectory;
132
138
  // Same-leg spawn env only (#1171 F1): never overwrite another leg's ambient
133
139
  // AK_ROLE_RUN_DIR in a shared process. HostContext is the per-turn authority
@@ -135,8 +141,12 @@ export async function reportTicketFromHostContext(
135
141
  if (process.env.AK_ROLE_RUN_DIR === runDirectory) {
136
142
  process.env.AK_ROLE_RUN_DIR = nextDirectory;
137
143
  }
138
- // Native host rebind only — SessionManager has no setSessionDir; appends follow sessionFile.
139
- context.sessionManager.setSessionFile?.(sessionFileOf(nextDirectory));
144
+ const projectedSessionFile = typeof priorSessionFile === "string" && priorSessionFile.trim() !== ""
145
+ ? rewriteRunDirectoryPathValue(priorSessionFile, runDirectory, nextDirectory)
146
+ : sessionFileOf(nextDirectory);
147
+ if (typeof projectedSessionFile === "string") {
148
+ context.sessionManager.setSessionFile?.(projectedSessionFile);
149
+ }
140
150
  };
141
151
  const relocation = alreadyPlaced
142
152
  ? undefined
@@ -3,7 +3,7 @@ import { appendFile, mkdir, writeFile } from "node:fs/promises";
3
3
  import { createServer, type Server, type Socket } from "node:net";
4
4
  import { dirname } from "node:path";
5
5
 
6
- import { sessionDirectoryOf, sessionFileOf } from "./role-run-placement.ts";
6
+ import { sessionFileOf } from "./role-run-placement.ts";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
9
  import {
@@ -94,6 +94,12 @@ export async function prepareRoleEnvelope(options: {
94
94
  * Tests may omit → runDirectory default.
95
95
  */
96
96
  readonly sessionFile?: string;
97
+ /**
98
+ * Host authority that sealed request.principal. Required for mid-turn run
99
+ * relocate to reseal opaque principal wire (#1183); absent throws on relocate
100
+ * rather than mutating opaque principal or inventing a host-specific bypass.
101
+ */
102
+ readonly principalAuthority?: import("./host-contracts.ts").DurablePrincipalAuthority;
97
103
  }): Promise<PreparedRoleTurn> {
98
104
  const { request } = options;
99
105
  if (options.socketPath === "") {
@@ -170,8 +176,9 @@ export async function prepareRoleEnvelope(options: {
170
176
  };
171
177
  const mutableRequest = request as {
172
178
  runDirectory: string;
173
- principal: { sessionDirectory?: string; sessionFile?: string };
179
+ principal: import("./host-contracts.ts").DurablePrincipal;
174
180
  };
181
+ const principalAuthority = options.principalAuthority;
175
182
  const context: HostContext = {
176
183
  cwd: request.cwd,
177
184
  mode: "print",
@@ -199,8 +206,9 @@ export async function prepareRoleEnvelope(options: {
199
206
  // and non-correctable MCP catch). Submission does not abort the turn.
200
207
  },
201
208
  };
202
- // #1171: report-ticket updates HostContext.runDirectory; keep the shared turn
203
- // request (and principal session paths) on the same leaf for adapter exit-copy.
209
+ // #1171 / #1183: report-ticket updates HostContext.runDirectory; reseal principal
210
+ // through host authority when provided. Never assign Pi session fields onto
211
+ // opaque/frozen principal wire (DurablePrincipal / #636).
204
212
  Object.defineProperty(context, "runDirectory", {
205
213
  configurable: true,
206
214
  enumerable: true,
@@ -210,14 +218,15 @@ export async function prepareRoleEnvelope(options: {
210
218
  set(next: string) {
211
219
  if (typeof next !== "string" || next.trim() === "") return;
212
220
  if (mutableRequest.runDirectory === next) return;
213
- projectTurnRequestLiveRunDirectory(mutableRequest, next);
214
- sessionFile = sessionFileOf(next);
215
- if (typeof mutableRequest.principal.sessionDirectory !== "string") {
216
- mutableRequest.principal.sessionDirectory = sessionDirectoryOf(next);
217
- }
218
- if (typeof mutableRequest.principal.sessionFile !== "string") {
219
- mutableRequest.principal.sessionFile = sessionFile;
221
+ if (principalAuthority === undefined) {
222
+ throw new Error(
223
+ "mid-turn run relocate requires principalAuthority on prepareRoleEnvelope",
224
+ );
220
225
  }
226
+ projectTurnRequestLiveRunDirectory(mutableRequest, next, principalAuthority);
227
+ // Live write handle = same host-authority coords as the resealed principal
228
+ // (#1183). Never rebuild default session.jsonl over a non-default seal.
229
+ sessionFile = principalAuthority.decode(mutableRequest.principal).sessionFile;
221
230
  },
222
231
  });
223
232
  const bookCustomMessage = (customType: string, message: { content?: string; details?: unknown }): void => {
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Sole pure authority for typed run-directory path rewrite / projection.
3
+ * No dossier I/O — run-dossier render and role-run-relocation durable walks
4
+ * both consume this module (avoids a cycle through page readers).
5
+ *
6
+ * Scope is typed machine-consumed path fields only — never free text, never
7
+ * caller file-flag path strings.
8
+ */
9
+ import { sep } from "node:path";
10
+
11
+ import type {
12
+ DurablePrincipal,
13
+ DurablePrincipalAuthority,
14
+ } from "./host-contracts.ts";
15
+ import { isRecord } from "./unknown-value.ts";
16
+
17
+ export const ADMITTED_PAGE_PATH_FIELDS = [
18
+ "runDirectory",
19
+ "admittedRequestPath", // pre-#1161 pages only
20
+ "sessionDirectory",
21
+ "sessionFile",
22
+ // #1165/#1168: caller file-flag paths stay as given.
23
+ "mergerInputPath",
24
+ "sourceRunPath",
25
+ ] as const;
26
+
27
+ export const INVOCATION_PAGE_PATH_FIELDS = [
28
+ "runDirectory",
29
+ "sessionDirectory",
30
+ "sessionFile",
31
+ ] as const;
32
+
33
+ export const RUN_STATE_PAGE_PATH_FIELDS = [
34
+ "runDirectory",
35
+ "admittedRequestPath", // pre-#1161 pages only
36
+ "sessionDirectory",
37
+ "sessionFile",
38
+ ] as const;
39
+
40
+ /** Typed principal session coordinates rewritten when the owning run leaf moves. */
41
+ export const PRINCIPAL_SESSION_PATH_FIELDS = [
42
+ "sessionDirectory",
43
+ "sessionFile",
44
+ ] as const;
45
+
46
+ const SOURCE_RUN_LOCATOR_FIELDS = ["runDirectory"] as const;
47
+ const SUMMONS_PATH_FIELDS = ["sourceRunPath"] as const;
48
+
49
+ export type RunDirectoryPathRewrite = {
50
+ readonly oldRunDirectory: string;
51
+ readonly newRunDirectory: string;
52
+ };
53
+
54
+ /** Rewrite one path value that points at or under oldRunDirectory. */
55
+ export function rewriteRunDirectoryPathValue(
56
+ value: unknown,
57
+ oldRunDirectory: string,
58
+ newRunDirectory: string,
59
+ ): unknown {
60
+ if (typeof value !== "string") return value;
61
+ if (value === oldRunDirectory) return newRunDirectory;
62
+ const prefix = `${oldRunDirectory}${sep}`;
63
+ if (value.startsWith(prefix)) {
64
+ return `${newRunDirectory}${value.slice(oldRunDirectory.length)}`;
65
+ }
66
+ return value;
67
+ }
68
+
69
+ /**
70
+ * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
71
+ * nested run path is not partially claimed by a shorter sibling prefix.
72
+ */
73
+ export function rewriteRunDirectoryPathValueAgainstRewrites(
74
+ value: unknown,
75
+ rewrites: readonly RunDirectoryPathRewrite[],
76
+ ): unknown {
77
+ if (typeof value !== "string" || rewrites.length === 0) return value;
78
+ let best: RunDirectoryPathRewrite | undefined;
79
+ for (const rewrite of rewrites) {
80
+ if (
81
+ value === rewrite.oldRunDirectory ||
82
+ value.startsWith(`${rewrite.oldRunDirectory}${sep}`)
83
+ ) {
84
+ if (
85
+ best === undefined ||
86
+ rewrite.oldRunDirectory.length > best.oldRunDirectory.length
87
+ ) {
88
+ best = rewrite;
89
+ }
90
+ }
91
+ }
92
+ if (best === undefined) return value;
93
+ return rewriteRunDirectoryPathValue(
94
+ value,
95
+ best.oldRunDirectory,
96
+ best.newRunDirectory,
97
+ );
98
+ }
99
+
100
+ /** In-place rewrite of selected fields on one record (single pair). */
101
+ export function rewriteRunDirectoryPathFields(
102
+ record: Record<string, unknown>,
103
+ fields: readonly string[],
104
+ oldRunDirectory: string,
105
+ newRunDirectory: string,
106
+ ): void {
107
+ rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, [
108
+ { oldRunDirectory, newRunDirectory },
109
+ ]);
110
+ }
111
+
112
+ /** In-place rewrite of selected fields against a rewrite set. */
113
+ export function rewriteRunDirectoryPathFieldsAgainstRewrites(
114
+ record: Record<string, unknown>,
115
+ fields: readonly string[],
116
+ rewrites: readonly RunDirectoryPathRewrite[],
117
+ ): void {
118
+ for (const field of fields) {
119
+ if (field in record) {
120
+ record[field] = rewriteRunDirectoryPathValueAgainstRewrites(
121
+ record[field],
122
+ rewrites,
123
+ );
124
+ }
125
+ }
126
+ }
127
+
128
+ /** Principal session paths only — shared live + durable projection after a run move. */
129
+ export function rewritePrincipalSessionPaths(
130
+ principal: Record<string, unknown>,
131
+ oldRunDirectory: string,
132
+ newRunDirectory: string,
133
+ ): void {
134
+ rewriteRunDirectoryPathFields(
135
+ principal,
136
+ PRINCIPAL_SESSION_PATH_FIELDS,
137
+ oldRunDirectory,
138
+ newRunDirectory,
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Live turn request: move runDirectory and keep principal session paths on the
144
+ * new leaf. Sole path = host authority decode → rewrite coords copy → seal
145
+ * (#1183 / #636). No shape-based in-place rewrite and no silent skip.
146
+ */
147
+ export function projectTurnRequestLiveRunDirectory(
148
+ request: {
149
+ runDirectory: string;
150
+ principal: DurablePrincipal;
151
+ },
152
+ newRunDirectory: string,
153
+ authority: DurablePrincipalAuthority,
154
+ ): void {
155
+ const old = request.runDirectory;
156
+ if (old === newRunDirectory) return;
157
+ const coords = { ...authority.decode(request.principal) };
158
+ rewritePrincipalSessionPaths(
159
+ coords as Record<string, unknown>,
160
+ old,
161
+ newRunDirectory,
162
+ );
163
+ const sealed = authority.seal(coords);
164
+ request.runDirectory = newRunDirectory;
165
+ (request as { principal: DurablePrincipal }).principal = sealed;
166
+ }
167
+
168
+ /** Typed notary/source-run locator: only runDirectory is a path. */
169
+ export function rewriteSourceRunLocator(
170
+ value: unknown,
171
+ rewrites: readonly RunDirectoryPathRewrite[],
172
+ ): void {
173
+ if (!isRecord(value)) return;
174
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
175
+ value,
176
+ SOURCE_RUN_LOCATOR_FIELDS,
177
+ rewrites,
178
+ );
179
+ }
180
+
181
+ /** Same-ticket summons materials: typed source-run; caller attach paths stay as-is (#1165). */
182
+ export function rewriteSummonsMaterials(
183
+ value: unknown,
184
+ rewrites: readonly RunDirectoryPathRewrite[],
185
+ ): void {
186
+ if (!isRecord(value)) return;
187
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
188
+ value,
189
+ SUMMONS_PATH_FIELDS,
190
+ rewrites,
191
+ );
192
+ rewriteSourceRunLocator(value.sourceRun, rewrites);
193
+ // #1165: summons.attachmentPaths are caller paths — do not rewrite.
194
+ }
195
+
196
+ /** Project one admitted page in memory after its containing run moved. */
197
+ export function rewriteAdmittedRoleRunPage(
198
+ page: Record<string, unknown>,
199
+ rewrites: readonly RunDirectoryPathRewrite[],
200
+ ): void {
201
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, ADMITTED_PAGE_PATH_FIELDS, rewrites);
202
+ rewriteSourceRunLocator(page.sourceRun, rewrites);
203
+ // #1165: admitted attachments are caller paths outside the run directory — do not rewrite.
204
+ if (isRecord(page.principal)) {
205
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
206
+ page.principal,
207
+ PRINCIPAL_SESSION_PATH_FIELDS,
208
+ rewrites,
209
+ );
210
+ }
211
+ }
212
+
213
+ /**
214
+ * #1183: current.json location fields follow the directory being rendered.
215
+ * Fact rows stay as written (append-only); only the rendering projects stored→actual.
216
+ * Returns the same object when already aligned. Caller file-flag paths stay opaque.
217
+ */
218
+ export function projectRenderedPagePaths(
219
+ page: Record<string, unknown>,
220
+ actualRunDirectory: string,
221
+ ): Record<string, unknown> {
222
+ const stored =
223
+ typeof page.runDirectory === "string" && page.runDirectory.trim() !== ""
224
+ ? page.runDirectory
225
+ : undefined;
226
+ if (stored === undefined || stored === actualRunDirectory) return page;
227
+ const projected = structuredClone(page);
228
+ const rewrites: readonly RunDirectoryPathRewrite[] = [
229
+ { oldRunDirectory: stored, newRunDirectory: actualRunDirectory },
230
+ ];
231
+ // Admitted field set is the location-field superset of invocation; runState adds
232
+ // principal (covered) and optional currentCourt summons under this run.
233
+ rewriteAdmittedRoleRunPage(projected, rewrites);
234
+ if (isRecord(projected.currentCourt)) {
235
+ rewriteSummonsMaterials(projected.currentCourt.summons, rewrites);
236
+ }
237
+ return projected;
238
+ }
@@ -1,81 +1,54 @@
1
1
  /**
2
- * Single authority for rewriting durable run pages after a run directory moves.
3
- * Live callers project typed fields; only historical migrators walk durable pages.
2
+ * Durable run-page path rewrite after a run directory moves, plus retained-path
3
+ * matching for live lookup. Pure path rewrite rules live in role-run-path-rewrite.ts
4
+ * (sole authority); this module owns I/O walks that need dossier page readers.
4
5
  *
5
6
  * Scope is typed machine-consumed path fields only — never free text, never
6
7
  * caller file-flag path strings. Nested walk is confined to package-owned
7
8
  * `session/` seams.
8
9
  */
9
10
  import { readdir, readFile, writeFile } from "node:fs/promises";
10
- import { dirname, join, sep } from "node:path";
11
+ import { dirname, join } from "node:path";
11
12
 
12
13
  import { activationBookDirectory, physicalPathIdentity } from "./activation-ledger-topology.ts";
14
+ import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.ts";
13
15
  import { readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
14
16
  import { findRoleRunDirectory, roleRunPlacement } from "./role-run-placement.ts";
15
-
16
- import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.ts";
17
-
17
+ import {
18
+ INVOCATION_PAGE_PATH_FIELDS,
19
+ PRINCIPAL_SESSION_PATH_FIELDS,
20
+ RUN_STATE_PAGE_PATH_FIELDS,
21
+ type RunDirectoryPathRewrite,
22
+ projectTurnRequestLiveRunDirectory,
23
+ rewriteAdmittedRoleRunPage,
24
+ rewritePrincipalSessionPaths,
25
+ rewriteRunDirectoryPathFields,
26
+ rewriteRunDirectoryPathFieldsAgainstRewrites,
27
+ rewriteRunDirectoryPathValue,
28
+ rewriteRunDirectoryPathValueAgainstRewrites,
29
+ rewriteSummonsMaterials,
30
+ } from "./role-run-path-rewrite.ts";
18
31
  import { readPageSync, updateSectionSync } from "./run-dossier.ts";
19
- import { isRecord, isEnoent } from "./unknown-value.ts";
20
-
21
- const ADMITTED_PAGE_FIELDS = [
22
- "runDirectory",
23
- "admittedRequestPath", // pre-#1161 pages only
24
- "sessionDirectory",
25
- "sessionFile",
26
- // #1165/#1168: caller file-flag paths (requestManifestPath, prerequisitesPath)
27
- // stay as given. Obsolete copy fields (taskPath/packetPath) are not rewritten —
28
- // stock volumes keep their bytes; live relocate no longer maintains them.
29
- "mergerInputPath",
30
- "sourceRunPath",
31
- ] as const;
32
-
33
- const INVOCATION_PAGE_FIELDS = [
34
- "runDirectory",
35
- "sessionDirectory",
36
- "sessionFile",
37
- ] as const;
38
-
39
- const RUN_STATE_PAGE_FIELDS = [
40
- "runDirectory",
41
- "admittedRequestPath", // pre-#1161 pages only
42
- "sessionDirectory",
43
- "sessionFile",
44
- ] as const;
32
+ import { isEnoent, isRecord } from "./unknown-value.ts";
45
33
 
46
- /** Typed principal session coordinates rewritten when the owning run leaf moves. */
47
- export const PRINCIPAL_SESSION_PATH_FIELDS = [
48
- "sessionDirectory",
49
- "sessionFile",
50
- ] as const;
34
+ // Re-export the pure path API so existing import sites stay on this module path.
35
+ export {
36
+ PRINCIPAL_SESSION_PATH_FIELDS,
37
+ type RunDirectoryPathRewrite,
38
+ projectTurnRequestLiveRunDirectory,
39
+ rewriteAdmittedRoleRunPage,
40
+ rewritePrincipalSessionPaths,
41
+ rewriteRunDirectoryPathFields,
42
+ rewriteRunDirectoryPathFieldsAgainstRewrites,
43
+ rewriteRunDirectoryPathValue,
44
+ rewriteRunDirectoryPathValueAgainstRewrites,
45
+ } from "./role-run-path-rewrite.ts";
51
46
 
52
- const SOURCE_RUN_LOCATOR_FIELDS = ["runDirectory"] as const;
53
- const SUMMONS_PATH_FIELDS = ["sourceRunPath"] as const;
54
47
  const OFFICER_POINTER_FIELDS = ["sessionFile", "runDirectory"] as const;
55
48
  const SITIAN_RECORD_FIELDS = ["sessionParent"] as const;
56
49
  const SESSION_HEADER_FIELDS = ["parentSession"] as const;
57
50
  const BINDING_PARENT_FIELDS = ["sessionFile"] as const;
58
51
 
59
- export type RunDirectoryPathRewrite = {
60
- readonly oldRunDirectory: string;
61
- readonly newRunDirectory: string;
62
- };
63
-
64
- /** Rewrite one path value that points at or under oldRunDirectory. */
65
- export function rewriteRunDirectoryPathValue(
66
- value: unknown,
67
- oldRunDirectory: string,
68
- newRunDirectory: string,
69
- ): unknown {
70
- if (typeof value !== "string") return value;
71
- if (value === oldRunDirectory) return newRunDirectory;
72
- const prefix = `${oldRunDirectory}${sep}`;
73
- if (value.startsWith(prefix)) {
74
- return `${newRunDirectory}${value.slice(oldRunDirectory.length)}`;
75
- }
76
- return value;
77
- }
78
-
79
52
  /** Match physical aliases or a normal unbound→ticket bind, never historical migration layouts. */
80
53
  export async function retainedRunPathsMatch(
81
54
  recordedPath: unknown,
@@ -109,100 +82,6 @@ export async function retainedRunPathsMatch(
109
82
  return false;
110
83
  }
111
84
 
112
- /**
113
- * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
114
- * nested run path is not partially claimed by a shorter sibling prefix.
115
- */
116
- export function rewriteRunDirectoryPathValueAgainstRewrites(
117
- value: unknown,
118
- rewrites: readonly RunDirectoryPathRewrite[],
119
- ): unknown {
120
- if (typeof value !== "string" || rewrites.length === 0) return value;
121
- let best: RunDirectoryPathRewrite | undefined;
122
- for (const rewrite of rewrites) {
123
- if (
124
- value === rewrite.oldRunDirectory ||
125
- value.startsWith(`${rewrite.oldRunDirectory}${sep}`)
126
- ) {
127
- if (
128
- best === undefined ||
129
- rewrite.oldRunDirectory.length > best.oldRunDirectory.length
130
- ) {
131
- best = rewrite;
132
- }
133
- }
134
- }
135
- if (best === undefined) return value;
136
- return rewriteRunDirectoryPathValue(
137
- value,
138
- best.oldRunDirectory,
139
- best.newRunDirectory,
140
- );
141
- }
142
-
143
- /** In-place rewrite of selected fields on one record (single pair). */
144
- export function rewriteRunDirectoryPathFields(
145
- record: Record<string, unknown>,
146
- fields: readonly string[],
147
- oldRunDirectory: string,
148
- newRunDirectory: string,
149
- ): void {
150
- rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, [
151
- { oldRunDirectory, newRunDirectory },
152
- ]);
153
- }
154
-
155
- /** Principal session paths only — shared live + durable projection after a run move. */
156
- export function rewritePrincipalSessionPaths(
157
- principal: Record<string, unknown>,
158
- oldRunDirectory: string,
159
- newRunDirectory: string,
160
- ): void {
161
- rewriteRunDirectoryPathFields(
162
- principal,
163
- PRINCIPAL_SESSION_PATH_FIELDS,
164
- oldRunDirectory,
165
- newRunDirectory,
166
- );
167
- }
168
-
169
- /**
170
- * Live turn request: move runDirectory and rewrite principal session paths in place.
171
- * Callers that lack authority.seal use this; admitted refresh seals after the same rewrite.
172
- */
173
- export function projectTurnRequestLiveRunDirectory(
174
- request: {
175
- runDirectory: string;
176
- principal: { sessionDirectory?: string; sessionFile?: string };
177
- },
178
- newRunDirectory: string,
179
- ): void {
180
- const old = request.runDirectory;
181
- if (old === newRunDirectory) return;
182
- request.runDirectory = newRunDirectory;
183
- rewritePrincipalSessionPaths(
184
- request.principal as Record<string, unknown>,
185
- old,
186
- newRunDirectory,
187
- );
188
- }
189
-
190
- /** In-place rewrite of selected fields against a rewrite set. */
191
- export function rewriteRunDirectoryPathFieldsAgainstRewrites(
192
- record: Record<string, unknown>,
193
- fields: readonly string[],
194
- rewrites: readonly RunDirectoryPathRewrite[],
195
- ): void {
196
- for (const field of fields) {
197
- if (field in record) {
198
- record[field] = rewriteRunDirectoryPathValueAgainstRewrites(
199
- record[field],
200
- rewrites,
201
- );
202
- }
203
- }
204
- }
205
-
206
85
  function collectRewrites(input: {
207
86
  readonly oldRunDirectory: string;
208
87
  readonly newRunDirectory: string;
@@ -229,51 +108,6 @@ function collectRewrites(input: {
229
108
  return out;
230
109
  }
231
110
 
232
- /** Typed notary/source-run locator: only runDirectory is a path. */
233
- function rewriteSourceRunLocator(
234
- value: unknown,
235
- rewrites: readonly RunDirectoryPathRewrite[],
236
- ): void {
237
- if (!isRecord(value)) return;
238
- rewriteRunDirectoryPathFieldsAgainstRewrites(
239
- value,
240
- SOURCE_RUN_LOCATOR_FIELDS,
241
- rewrites,
242
- );
243
- }
244
-
245
- /** Same-ticket summons materials: typed source-run; caller attach paths stay as-is (#1165). */
246
- function rewriteSummonsMaterials(
247
- value: unknown,
248
- rewrites: readonly RunDirectoryPathRewrite[],
249
- ): void {
250
- if (!isRecord(value)) return;
251
- rewriteRunDirectoryPathFieldsAgainstRewrites(
252
- value,
253
- SUMMONS_PATH_FIELDS,
254
- rewrites,
255
- );
256
- rewriteSourceRunLocator(value.sourceRun, rewrites);
257
- // #1165: summons.attachmentPaths are caller paths — do not rewrite.
258
- }
259
-
260
- /** Project one admitted page in memory after its containing run moved. */
261
- export function rewriteAdmittedRoleRunPage(
262
- page: Record<string, unknown>,
263
- rewrites: readonly RunDirectoryPathRewrite[],
264
- ): void {
265
- rewriteRunDirectoryPathFieldsAgainstRewrites(page, ADMITTED_PAGE_FIELDS, rewrites);
266
- rewriteSourceRunLocator(page.sourceRun, rewrites);
267
- // #1165: admitted attachments are caller paths outside the run directory — do not rewrite.
268
- if (isRecord(page.principal)) {
269
- rewriteRunDirectoryPathFieldsAgainstRewrites(
270
- page.principal,
271
- PRINCIPAL_SESSION_PATH_FIELDS,
272
- rewrites,
273
- );
274
- }
275
- }
276
-
277
111
  /** Pre-#1161 `session/auditor-roles/<officer>.pointer.json` (migrator input only). */
278
112
  async function rewriteLegacyOfficerPointerFile(
279
113
  path: string,
@@ -482,9 +316,9 @@ export async function rewriteRoleRunDurablePages(input: {
482
316
  const pageRewriters: Readonly<Record<"admitted" | "invocation" | "runState", (page: Record<string, unknown>) => void>> = {
483
317
  admitted: (page) => rewriteAdmittedRoleRunPage(page, rewrites),
484
318
  invocation: (page) =>
485
- rewriteRunDirectoryPathFieldsAgainstRewrites(page, INVOCATION_PAGE_FIELDS, rewrites),
319
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, INVOCATION_PAGE_PATH_FIELDS, rewrites),
486
320
  runState: (page) => {
487
- rewriteRunDirectoryPathFieldsAgainstRewrites(page, RUN_STATE_PAGE_FIELDS, rewrites);
321
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, RUN_STATE_PAGE_PATH_FIELDS, rewrites);
488
322
  if (isRecord(page.principal)) {
489
323
  rewriteRunDirectoryPathFieldsAgainstRewrites(
490
324
  page.principal,
@@ -16,12 +16,19 @@ import { sitianReport } from "./sitian-facade.ts";
16
16
  import { sitianReportSafe } from "./host-session-record.ts";
17
17
  import { createSubmissionLedgerHost, sealAcceptedSubmission } from "./submission-ledger.ts";
18
18
  import { registerFiledSubmissionTool, type FiledSubmissionBeforeAccept } from "./filed-submission.ts";
19
- import { registerReportTicketTool, REPORT_TICKET_TOOL_NAME } from "./report-ticket-tool.ts";
19
+ import {
20
+ registerReportTicketTool,
21
+ reportTicketFromHostContext,
22
+ REPORT_TICKET_TOOL_NAME,
23
+ } from "./report-ticket-tool.ts";
20
24
  import type { RoleSubmissionDeclaration } from "./role-submission-declarations.ts";
21
25
 
22
26
  import { activationTraceRecordSchema, namedActivationCause, type ActivationTraceRecord, type ActivationTraceWriter } from "./activation-trace.ts";
23
27
  import { homeFromRunDirectory } from "./activation-ledger-topology.ts";
24
- import { isSafePositiveTicketNumber, parseTicketNumber } from "./run-ticket-number.ts";
28
+ import {
29
+ isSafePositiveTicketNumber,
30
+ parseTicketNumber,
31
+ } from "./run-ticket-number.ts";
25
32
  import {
26
33
  durableSessionPointer,
27
34
  resolveBookKeyFromGit,
@@ -859,11 +866,17 @@ export function createDiaristRoleRuntime(
859
866
  const status = submitted?.status;
860
867
  if (status !== "completed") return parameters;
861
868
  const assertion = readDiaristTicketAssertion(submitted);
862
- const coords = readDiaristRunCoordinates(ctx);
863
869
  // #836 7.3: pre-bound ticket is material for the LLM, not an override.
864
870
  const ticketNumber = assertion.kind === "ticket" ? assertion.ticketNumber : undefined;
865
- // The ticket binds in the public call's own process after the turn, from the sealed
866
- // ticketNumber (settlement seam); this leg never writes the run's current.json.
871
+ // #1183: typed ticket assertion is identity acquisition — one mid-turn
872
+ // bind/relocate here, before seal / reask / infra outcome branches.
873
+ // Reuses ak_report_ticket seam; already-placed is a no-op. Host return
874
+ // must not be the first placement (dossier-topology).
875
+ if (ticketNumber !== undefined) {
876
+ await reportTicketFromHostContext(ctx, ticketNumber);
877
+ }
878
+ // Live leaf after possible relocate (commit reads history under runDirectory).
879
+ const coords = readDiaristRunCoordinates(ctx);
867
880
  // Strict-schema hosts emit ticketSessions: null for a single ticket.
868
881
  const multiTicket = submitted?.ticketSessions != null;
869
882
  const singleSessions = submitted && !multiTicket
@@ -1145,13 +1158,17 @@ export function createRoleRuntimeExtension(
1145
1158
  await pending;
1146
1159
  return pendingNavigatorPresentation?.event;
1147
1160
  };
1148
- projectClosedSubmission = async (closed, context) => projectClosedSubmissionLifecycle(
1149
- closed,
1150
- context,
1151
- navigatorPhase(roleHost, closed.role),
1152
- () => receiptDelivery.recordAccepted(),
1153
- settleNavigatorProjection,
1154
- );
1161
+ projectClosedSubmission = async (closed, context) => {
1162
+ // #1183: typed ticket identity is acquired on the ledger seam when params
1163
+ // are first read — not here after seal. Closure only projects lifecycle.
1164
+ await projectClosedSubmissionLifecycle(
1165
+ closed,
1166
+ context,
1167
+ navigatorPhase(roleHost, closed.role),
1168
+ () => receiptDelivery.recordAccepted(),
1169
+ settleNavigatorProjection,
1170
+ );
1171
+ };
1155
1172
  roleHost.on("input", (_event) => {
1156
1173
  const role = roleHost.getFlag(ROLE_FLAG.name);
1157
1174
  if (role !== undefined && !admitted) return { action: "handled" as const };