@akagilnc/pi-workflow-roles 0.1.5683 → 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.
@@ -9,6 +9,7 @@ import { runDirectoryFromHostContext } from "./host-contracts.js";
9
9
  import { piDurablePrincipalAuthority } from "./pi/durable-principal.js";
10
10
  import { bindAdmittedTicketNumber, relocateAdmittedRunToTicket, } from "./public-cli/invocation.js";
11
11
  import { isUnboundRunDirectory, sessionDirectoryOf, sessionFileOf } from "./role-run-placement.js";
12
+ import { rewriteRunDirectoryPathValue } from "./role-run-path-rewrite.js";
12
13
  import { readPageSync } from "./run-dossier.js";
13
14
  import { readDeclaredTicketNumber } from "./run-ticket-number.js";
14
15
  import { isRecord } from "./unknown-value.js";
@@ -89,6 +90,11 @@ export async function reportTicketFromHostContext(context, rawTicketNumber, auth
89
90
  // Live handles belong with rename ownership — before derived renderCurrentSync
90
91
  // (#1171 F4-R2). Reuse relocate's heldLease.relocate seam; keep raw I/O throw.
91
92
  const applyLiveHandles = (nextDirectory) => {
93
+ // Capture live session handle before ownership transfer. After relocate the
94
+ // write handle must share host-authority coords with the principal (#1183):
95
+ // project the prior path onto the new leaf — never rebuild default
96
+ // session.jsonl over a non-default seal (and do not trust a setter rebuild).
97
+ const priorSessionFile = context.sessionManager.getSessionFile?.();
92
98
  context.runDirectory = nextDirectory;
93
99
  // Same-leg spawn env only (#1171 F1): never overwrite another leg's ambient
94
100
  // AK_ROLE_RUN_DIR in a shared process. HostContext is the per-turn authority
@@ -96,8 +102,12 @@ export async function reportTicketFromHostContext(context, rawTicketNumber, auth
96
102
  if (process.env.AK_ROLE_RUN_DIR === runDirectory) {
97
103
  process.env.AK_ROLE_RUN_DIR = nextDirectory;
98
104
  }
99
- // Native host rebind only — SessionManager has no setSessionDir; appends follow sessionFile.
100
- context.sessionManager.setSessionFile?.(sessionFileOf(nextDirectory));
105
+ const projectedSessionFile = typeof priorSessionFile === "string" && priorSessionFile.trim() !== ""
106
+ ? rewriteRunDirectoryPathValue(priorSessionFile, runDirectory, nextDirectory)
107
+ : sessionFileOf(nextDirectory);
108
+ if (typeof projectedSessionFile === "string") {
109
+ context.sessionManager.setSessionFile?.(projectedSessionFile);
110
+ }
101
111
  };
102
112
  const relocation = alreadyPlaced
103
113
  ? undefined
@@ -0,0 +1,149 @@
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
+ import { isRecord } from "./unknown-value.js";
11
+ export const ADMITTED_PAGE_PATH_FIELDS = [
12
+ "runDirectory",
13
+ "admittedRequestPath", // pre-#1161 pages only
14
+ "sessionDirectory",
15
+ "sessionFile",
16
+ // #1165/#1168: caller file-flag paths stay as given.
17
+ "mergerInputPath",
18
+ "sourceRunPath",
19
+ ];
20
+ export const INVOCATION_PAGE_PATH_FIELDS = [
21
+ "runDirectory",
22
+ "sessionDirectory",
23
+ "sessionFile",
24
+ ];
25
+ export const RUN_STATE_PAGE_PATH_FIELDS = [
26
+ "runDirectory",
27
+ "admittedRequestPath", // pre-#1161 pages only
28
+ "sessionDirectory",
29
+ "sessionFile",
30
+ ];
31
+ /** Typed principal session coordinates rewritten when the owning run leaf moves. */
32
+ export const PRINCIPAL_SESSION_PATH_FIELDS = [
33
+ "sessionDirectory",
34
+ "sessionFile",
35
+ ];
36
+ const SOURCE_RUN_LOCATOR_FIELDS = ["runDirectory"];
37
+ const SUMMONS_PATH_FIELDS = ["sourceRunPath"];
38
+ /** Rewrite one path value that points at or under oldRunDirectory. */
39
+ export function rewriteRunDirectoryPathValue(value, oldRunDirectory, newRunDirectory) {
40
+ if (typeof value !== "string")
41
+ return value;
42
+ if (value === oldRunDirectory)
43
+ return newRunDirectory;
44
+ const prefix = `${oldRunDirectory}${sep}`;
45
+ if (value.startsWith(prefix)) {
46
+ return `${newRunDirectory}${value.slice(oldRunDirectory.length)}`;
47
+ }
48
+ return value;
49
+ }
50
+ /**
51
+ * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
52
+ * nested run path is not partially claimed by a shorter sibling prefix.
53
+ */
54
+ export function rewriteRunDirectoryPathValueAgainstRewrites(value, rewrites) {
55
+ if (typeof value !== "string" || rewrites.length === 0)
56
+ return value;
57
+ let best;
58
+ for (const rewrite of rewrites) {
59
+ if (value === rewrite.oldRunDirectory ||
60
+ value.startsWith(`${rewrite.oldRunDirectory}${sep}`)) {
61
+ if (best === undefined ||
62
+ rewrite.oldRunDirectory.length > best.oldRunDirectory.length) {
63
+ best = rewrite;
64
+ }
65
+ }
66
+ }
67
+ if (best === undefined)
68
+ return value;
69
+ return rewriteRunDirectoryPathValue(value, best.oldRunDirectory, best.newRunDirectory);
70
+ }
71
+ /** In-place rewrite of selected fields on one record (single pair). */
72
+ export function rewriteRunDirectoryPathFields(record, fields, oldRunDirectory, newRunDirectory) {
73
+ rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, [
74
+ { oldRunDirectory, newRunDirectory },
75
+ ]);
76
+ }
77
+ /** In-place rewrite of selected fields against a rewrite set. */
78
+ export function rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, rewrites) {
79
+ for (const field of fields) {
80
+ if (field in record) {
81
+ record[field] = rewriteRunDirectoryPathValueAgainstRewrites(record[field], rewrites);
82
+ }
83
+ }
84
+ }
85
+ /** Principal session paths only — shared live + durable projection after a run move. */
86
+ export function rewritePrincipalSessionPaths(principal, oldRunDirectory, newRunDirectory) {
87
+ rewriteRunDirectoryPathFields(principal, PRINCIPAL_SESSION_PATH_FIELDS, oldRunDirectory, newRunDirectory);
88
+ }
89
+ /**
90
+ * Live turn request: move runDirectory and keep principal session paths on the
91
+ * new leaf. Sole path = host authority decode → rewrite coords copy → seal
92
+ * (#1183 / #636). No shape-based in-place rewrite and no silent skip.
93
+ */
94
+ export function projectTurnRequestLiveRunDirectory(request, newRunDirectory, authority) {
95
+ const old = request.runDirectory;
96
+ if (old === newRunDirectory)
97
+ return;
98
+ const coords = { ...authority.decode(request.principal) };
99
+ rewritePrincipalSessionPaths(coords, old, newRunDirectory);
100
+ const sealed = authority.seal(coords);
101
+ request.runDirectory = newRunDirectory;
102
+ request.principal = sealed;
103
+ }
104
+ /** Typed notary/source-run locator: only runDirectory is a path. */
105
+ export function rewriteSourceRunLocator(value, rewrites) {
106
+ if (!isRecord(value))
107
+ return;
108
+ rewriteRunDirectoryPathFieldsAgainstRewrites(value, SOURCE_RUN_LOCATOR_FIELDS, rewrites);
109
+ }
110
+ /** Same-ticket summons materials: typed source-run; caller attach paths stay as-is (#1165). */
111
+ export function rewriteSummonsMaterials(value, rewrites) {
112
+ if (!isRecord(value))
113
+ return;
114
+ rewriteRunDirectoryPathFieldsAgainstRewrites(value, SUMMONS_PATH_FIELDS, rewrites);
115
+ rewriteSourceRunLocator(value.sourceRun, rewrites);
116
+ // #1165: summons.attachmentPaths are caller paths — do not rewrite.
117
+ }
118
+ /** Project one admitted page in memory after its containing run moved. */
119
+ export function rewriteAdmittedRoleRunPage(page, rewrites) {
120
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, ADMITTED_PAGE_PATH_FIELDS, rewrites);
121
+ rewriteSourceRunLocator(page.sourceRun, rewrites);
122
+ // #1165: admitted attachments are caller paths outside the run directory — do not rewrite.
123
+ if (isRecord(page.principal)) {
124
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, PRINCIPAL_SESSION_PATH_FIELDS, rewrites);
125
+ }
126
+ }
127
+ /**
128
+ * #1183: current.json location fields follow the directory being rendered.
129
+ * Fact rows stay as written (append-only); only the rendering projects stored→actual.
130
+ * Returns the same object when already aligned. Caller file-flag paths stay opaque.
131
+ */
132
+ export function projectRenderedPagePaths(page, actualRunDirectory) {
133
+ const stored = typeof page.runDirectory === "string" && page.runDirectory.trim() !== ""
134
+ ? page.runDirectory
135
+ : undefined;
136
+ if (stored === undefined || stored === actualRunDirectory)
137
+ return page;
138
+ const projected = structuredClone(page);
139
+ const rewrites = [
140
+ { oldRunDirectory: stored, newRunDirectory: actualRunDirectory },
141
+ ];
142
+ // Admitted field set is the location-field superset of invocation; runState adds
143
+ // principal (covered) and optional currentCourt summons under this run.
144
+ rewriteAdmittedRoleRunPage(projected, rewrites);
145
+ if (isRecord(projected.currentCourt)) {
146
+ rewriteSummonsMaterials(projected.currentCourt.summons, rewrites);
147
+ }
148
+ return projected;
149
+ }
@@ -1,64 +1,27 @@
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
  import { activationBookDirectory, physicalPathIdentity } from "./activation-ledger-topology.js";
13
+ import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.js";
12
14
  import { readRoleRunIdentity } from "./public-cli/run-lifecycle.js";
13
15
  import { findRoleRunDirectory, roleRunPlacement } from "./role-run-placement.js";
14
- import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.js";
16
+ import { INVOCATION_PAGE_PATH_FIELDS, PRINCIPAL_SESSION_PATH_FIELDS, RUN_STATE_PAGE_PATH_FIELDS, projectTurnRequestLiveRunDirectory, rewriteAdmittedRoleRunPage, rewritePrincipalSessionPaths, rewriteRunDirectoryPathFields, rewriteRunDirectoryPathFieldsAgainstRewrites, rewriteRunDirectoryPathValue, rewriteRunDirectoryPathValueAgainstRewrites, rewriteSummonsMaterials, } from "./role-run-path-rewrite.js";
15
17
  import { readPageSync, updateSectionSync } from "./run-dossier.js";
16
- import { isRecord, isEnoent } from "./unknown-value.js";
17
- const ADMITTED_PAGE_FIELDS = [
18
- "runDirectory",
19
- "admittedRequestPath", // pre-#1161 pages only
20
- "sessionDirectory",
21
- "sessionFile",
22
- // #1165/#1168: caller file-flag paths (requestManifestPath, prerequisitesPath)
23
- // stay as given. Obsolete copy fields (taskPath/packetPath) are not rewritten —
24
- // stock volumes keep their bytes; live relocate no longer maintains them.
25
- "mergerInputPath",
26
- "sourceRunPath",
27
- ];
28
- const INVOCATION_PAGE_FIELDS = [
29
- "runDirectory",
30
- "sessionDirectory",
31
- "sessionFile",
32
- ];
33
- const RUN_STATE_PAGE_FIELDS = [
34
- "runDirectory",
35
- "admittedRequestPath", // pre-#1161 pages only
36
- "sessionDirectory",
37
- "sessionFile",
38
- ];
39
- /** Typed principal session coordinates rewritten when the owning run leaf moves. */
40
- export const PRINCIPAL_SESSION_PATH_FIELDS = [
41
- "sessionDirectory",
42
- "sessionFile",
43
- ];
44
- const SOURCE_RUN_LOCATOR_FIELDS = ["runDirectory"];
45
- const SUMMONS_PATH_FIELDS = ["sourceRunPath"];
18
+ import { isEnoent, isRecord } from "./unknown-value.js";
19
+ // Re-export the pure path API so existing import sites stay on this module path.
20
+ export { PRINCIPAL_SESSION_PATH_FIELDS, projectTurnRequestLiveRunDirectory, rewriteAdmittedRoleRunPage, rewritePrincipalSessionPaths, rewriteRunDirectoryPathFields, rewriteRunDirectoryPathFieldsAgainstRewrites, rewriteRunDirectoryPathValue, rewriteRunDirectoryPathValueAgainstRewrites, } from "./role-run-path-rewrite.js";
46
21
  const OFFICER_POINTER_FIELDS = ["sessionFile", "runDirectory"];
47
22
  const SITIAN_RECORD_FIELDS = ["sessionParent"];
48
23
  const SESSION_HEADER_FIELDS = ["parentSession"];
49
24
  const BINDING_PARENT_FIELDS = ["sessionFile"];
50
- /** Rewrite one path value that points at or under oldRunDirectory. */
51
- export function rewriteRunDirectoryPathValue(value, oldRunDirectory, newRunDirectory) {
52
- if (typeof value !== "string")
53
- return value;
54
- if (value === oldRunDirectory)
55
- return newRunDirectory;
56
- const prefix = `${oldRunDirectory}${sep}`;
57
- if (value.startsWith(prefix)) {
58
- return `${newRunDirectory}${value.slice(oldRunDirectory.length)}`;
59
- }
60
- return value;
61
- }
62
25
  /** Match physical aliases or a normal unbound→ticket bind, never historical migration layouts. */
63
26
  export async function retainedRunPathsMatch(recordedPath, currentPath, currentRunDirectory) {
64
27
  if (recordedPath === currentPath)
@@ -93,56 +56,6 @@ export async function retainedRunPathsMatch(recordedPath, currentPath, currentRu
93
56
  }
94
57
  return false;
95
58
  }
96
- /**
97
- * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
98
- * nested run path is not partially claimed by a shorter sibling prefix.
99
- */
100
- export function rewriteRunDirectoryPathValueAgainstRewrites(value, rewrites) {
101
- if (typeof value !== "string" || rewrites.length === 0)
102
- return value;
103
- let best;
104
- for (const rewrite of rewrites) {
105
- if (value === rewrite.oldRunDirectory ||
106
- value.startsWith(`${rewrite.oldRunDirectory}${sep}`)) {
107
- if (best === undefined ||
108
- rewrite.oldRunDirectory.length > best.oldRunDirectory.length) {
109
- best = rewrite;
110
- }
111
- }
112
- }
113
- if (best === undefined)
114
- return value;
115
- return rewriteRunDirectoryPathValue(value, best.oldRunDirectory, best.newRunDirectory);
116
- }
117
- /** In-place rewrite of selected fields on one record (single pair). */
118
- export function rewriteRunDirectoryPathFields(record, fields, oldRunDirectory, newRunDirectory) {
119
- rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, [
120
- { oldRunDirectory, newRunDirectory },
121
- ]);
122
- }
123
- /** Principal session paths only — shared live + durable projection after a run move. */
124
- export function rewritePrincipalSessionPaths(principal, oldRunDirectory, newRunDirectory) {
125
- rewriteRunDirectoryPathFields(principal, PRINCIPAL_SESSION_PATH_FIELDS, oldRunDirectory, newRunDirectory);
126
- }
127
- /**
128
- * Live turn request: move runDirectory and rewrite principal session paths in place.
129
- * Callers that lack authority.seal use this; admitted refresh seals after the same rewrite.
130
- */
131
- export function projectTurnRequestLiveRunDirectory(request, newRunDirectory) {
132
- const old = request.runDirectory;
133
- if (old === newRunDirectory)
134
- return;
135
- request.runDirectory = newRunDirectory;
136
- rewritePrincipalSessionPaths(request.principal, old, newRunDirectory);
137
- }
138
- /** In-place rewrite of selected fields against a rewrite set. */
139
- export function rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, rewrites) {
140
- for (const field of fields) {
141
- if (field in record) {
142
- record[field] = rewriteRunDirectoryPathValueAgainstRewrites(record[field], rewrites);
143
- }
144
- }
145
- }
146
59
  function collectRewrites(input) {
147
60
  const own = {
148
61
  oldRunDirectory: input.oldRunDirectory,
@@ -163,29 +76,6 @@ function collectRewrites(input) {
163
76
  }
164
77
  return out;
165
78
  }
166
- /** Typed notary/source-run locator: only runDirectory is a path. */
167
- function rewriteSourceRunLocator(value, rewrites) {
168
- if (!isRecord(value))
169
- return;
170
- rewriteRunDirectoryPathFieldsAgainstRewrites(value, SOURCE_RUN_LOCATOR_FIELDS, rewrites);
171
- }
172
- /** Same-ticket summons materials: typed source-run; caller attach paths stay as-is (#1165). */
173
- function rewriteSummonsMaterials(value, rewrites) {
174
- if (!isRecord(value))
175
- return;
176
- rewriteRunDirectoryPathFieldsAgainstRewrites(value, SUMMONS_PATH_FIELDS, rewrites);
177
- rewriteSourceRunLocator(value.sourceRun, rewrites);
178
- // #1165: summons.attachmentPaths are caller paths — do not rewrite.
179
- }
180
- /** Project one admitted page in memory after its containing run moved. */
181
- export function rewriteAdmittedRoleRunPage(page, rewrites) {
182
- rewriteRunDirectoryPathFieldsAgainstRewrites(page, ADMITTED_PAGE_FIELDS, rewrites);
183
- rewriteSourceRunLocator(page.sourceRun, rewrites);
184
- // #1165: admitted attachments are caller paths outside the run directory — do not rewrite.
185
- if (isRecord(page.principal)) {
186
- rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, PRINCIPAL_SESSION_PATH_FIELDS, rewrites);
187
- }
188
- }
189
79
  /** Pre-#1161 `session/auditor-roles/<officer>.pointer.json` (migrator input only). */
190
80
  async function rewriteLegacyOfficerPointerFile(path, rewrites) {
191
81
  const page = JSON.parse(await readFile(path, "utf8"));
@@ -371,9 +261,9 @@ export async function rewriteRoleRunDurablePages(input) {
371
261
  // One rewriter per page kind; each keeps its own field list.
372
262
  const pageRewriters = {
373
263
  admitted: (page) => rewriteAdmittedRoleRunPage(page, rewrites),
374
- invocation: (page) => rewriteRunDirectoryPathFieldsAgainstRewrites(page, INVOCATION_PAGE_FIELDS, rewrites),
264
+ invocation: (page) => rewriteRunDirectoryPathFieldsAgainstRewrites(page, INVOCATION_PAGE_PATH_FIELDS, rewrites),
375
265
  runState: (page) => {
376
- rewriteRunDirectoryPathFieldsAgainstRewrites(page, RUN_STATE_PAGE_FIELDS, rewrites);
266
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, RUN_STATE_PAGE_PATH_FIELDS, rewrites);
377
267
  if (isRecord(page.principal)) {
378
268
  rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, PRINCIPAL_SESSION_PATH_FIELDS, rewrites);
379
269
  }
@@ -20,6 +20,7 @@ import { randomUUID } from "node:crypto";
20
20
  import { readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
21
21
  import { basename, join } from "node:path";
22
22
  import { sessionFileOf } from "./role-run-placement.js";
23
+ import { projectRenderedPagePaths } from "./role-run-path-rewrite.js";
23
24
  import { RUN_CURRENT_FILE, RUN_HISTORY_FILE, RUN_LOG_FILE, RUN_STATE_FILE, OFFICER_POINTER_RECORD_KIND } from "./run-dossier-files.js";
24
25
  import { appendSitianRecord } from "./sitian-appender.js";
25
26
  import { parseSitianRecordText } from "./sitian-reader.js";
@@ -144,8 +145,16 @@ function render(runDirectory, history, state, log, unreadable) {
144
145
  const whole = {};
145
146
  for (const section of ["invocation", "admitted", "runState", "terminal"]) {
146
147
  const page = lastPayloadOfKind(state, PAGE_ROW_KIND[section]);
147
- if (page !== undefined)
148
+ if (page === undefined)
149
+ continue;
150
+ // #1183: location fields track the directory being rendered; fact rows stay put.
151
+ if ((section === "invocation" || section === "admitted" || section === "runState")
152
+ && isRecord(page)) {
153
+ whole[section] = projectRenderedPagePaths(page, runDirectory);
154
+ }
155
+ else {
148
156
  whole[section] = page;
157
+ }
149
158
  }
150
159
  let latest;
151
160
  for (const row of history) {
@@ -16,6 +16,7 @@ export const HOST_SESSION_ID_RECORD_KIND = "host-session-id";
16
16
  export function createSessionIdentityAuthority(authority, hostName) {
17
17
  const runDirectoryOf = (principal) => dirname(authority.decode(principal).sessionDirectory);
18
18
  return {
19
+ principalAuthority: authority,
19
20
  resolveSessionFile(principal) {
20
21
  return authority.decode(principal).sessionFile;
21
22
  },
@@ -6,7 +6,9 @@ import { readSitianRecords, resolveSitianRecordPathInLedger, sitianReport } from
6
6
  import { findRunDirectoryById } from "./public-cli/run-lifecycle.js";
7
7
  import { isCorrectableExecuteError } from "./submission-correctable-error.js";
8
8
  import { failOnInfrastructureFailureDeclaration, infrastructureFailureDiagnostic } from "./package-contracts/terminating-infrastructure.js";
9
+ import { reportTicketFromHostContext } from "./report-ticket-tool.js";
9
10
  import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.js";
11
+ import { readDeclaredTicketNumber } from "./run-ticket-number.js";
10
12
  import { isRecord, errorText } from "./unknown-value.js";
11
13
  /**
12
14
  * Admitted run identity for the ledger subject.
@@ -374,6 +376,15 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
374
376
  host.registerTool({
375
377
  ...tool,
376
378
  async execute(toolCallId, params, signal, update, context) {
379
+ // #1183: valid typed ticket on the submission is identity acquisition —
380
+ // bind/relocate once here, before reask / ledger restore / seal outcomes.
381
+ // Host return must not be the first placement (dossier-topology).
382
+ const typedTicket = isRecord(params)
383
+ ? readDeclaredTicketNumber(params.ticketNumber)
384
+ : undefined;
385
+ if (typedTicket !== undefined) {
386
+ await reportTicketFromHostContext(context, typedTicket);
387
+ }
377
388
  const runId = runIdentity(context);
378
389
  const attemptId = attemptIdentity(context, runId);
379
390
  const state = await stateFor(context, runId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5683",
3
+ "version": "0.1.5698",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -42,6 +42,7 @@ function createComposedAcpRoleTurnHost(
42
42
  request,
43
43
  dependencies: config.roleRuntimeDependencies,
44
44
  sessionFile: config.sessionIdentity.resolveSessionFile(request.principal),
45
+ principalAuthority: config.sessionIdentity.principalAuthority,
45
46
  socketPath: config.socketPath?.(request) ?? `/tmp/ak-acp-mcp-${randomUUID()}.sock`,
46
47
  }),
47
48
  });
@@ -382,7 +382,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
382
382
  // MCP advertises tool.parameters; prepared.jsonSchema is the headless
383
383
  // draft-07 stamp of that clone — record the delivered parameters face.
384
384
  const { $schema: _headlessStamp, ...deliveredSchema } = prepared.jsonSchema;
385
- await recordTurnDelivery(await syncTurnRequestLivePlacement(request), {
385
+ await recordTurnDelivery(await syncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority), {
386
386
  systemPrompt: systemPromptOverride,
387
387
  outputSchema: deliveredSchema,
388
388
  }, "acp-host");
@@ -503,7 +503,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
503
503
  outcome = { ...facts, ...(knownFailure === undefined ? {} : { knownFailure }) };
504
504
  }
505
505
  if (config.hostName !== "hermes" && sessionOpened && sessionId !== undefined) {
506
- const live = await trySyncTurnRequestLivePlacement(request);
506
+ const live = await trySyncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority);
507
507
  if ("runDirectory" in live) {
508
508
  try {
509
509
  copyAndRecordHostDossier({
@@ -7,6 +7,7 @@ import { existsSync } from "node:fs";
7
7
  import { basename } from "node:path";
8
8
 
9
9
  import type {
10
+ DurablePrincipalAuthority,
10
11
  RoleTurnHost,
11
12
  RoleTurnKnownFailure,
12
13
  RoleTurnRequest,
@@ -46,10 +47,12 @@ export async function resolveLiveRunDirectoryPath(
46
47
  /**
47
48
  * Prefer the request path when it still exists; otherwise re-locate by run id
48
49
  * (resolveLiveRunDirectoryPath). Mutates the live request so later turn-record /
49
- * exit-copy / fault notes stay on the new leaf.
50
+ * exit-copy / fault notes stay on the new leaf. Authority is required so opaque
51
+ * principal wire is resealed — never shape-guessed in place (#1183).
50
52
  */
51
53
  export async function syncTurnRequestLivePlacement(
52
54
  request: RoleTurnRequest,
55
+ authority: DurablePrincipalAuthority,
53
56
  ): Promise<string> {
54
57
  const live = await resolveLiveRunDirectoryPath(request.runDirectory, request.home);
55
58
  if (live === undefined) {
@@ -59,13 +62,7 @@ export async function syncTurnRequestLivePlacement(
59
62
  );
60
63
  }
61
64
  if (live !== request.runDirectory) {
62
- projectTurnRequestLiveRunDirectory(
63
- request as {
64
- runDirectory: string;
65
- principal: { sessionDirectory?: string; sessionFile?: string };
66
- },
67
- live,
68
- );
65
+ projectTurnRequestLiveRunDirectory(request, live, authority);
69
66
  }
70
67
  return live;
71
68
  }
@@ -76,37 +73,54 @@ export async function syncTurnRequestLivePlacement(
76
73
  */
77
74
  export async function trySyncTurnRequestLivePlacement(
78
75
  request: RoleTurnRequest,
76
+ authority: DurablePrincipalAuthority,
79
77
  ): Promise<{ readonly runDirectory: string } | { readonly resolveError: unknown }> {
80
78
  try {
81
- return { runDirectory: await syncTurnRequestLivePlacement(request) };
79
+ return { runDirectory: await syncTurnRequestLivePlacement(request, authority) };
82
80
  } catch (resolveError) {
83
81
  return { resolveError };
84
82
  }
85
83
  }
86
84
 
87
85
  /**
88
- * Retain a package fault on the live leaf when locatable. Lookup failure must not
89
- * replace the original fault or block later cleanup — reuse retainPackageFault when
90
- * the leaf exists; otherwise present both causes on stderr (Pi transport path shape).
86
+ * Retain a package fault on the live leaf when locatable. Directory lookup only —
87
+ * does not mutate live principal identity (#1183). Lookup failure must not replace
88
+ * the original fault or block later cleanup.
91
89
  */
92
90
  export async function retainPackageFaultBesideLivePlacement(
93
91
  request: RoleTurnRequest,
94
92
  input: { readonly diagnostic: string; readonly error?: unknown },
95
93
  ): Promise<void> {
96
- const live = await trySyncTurnRequestLivePlacement(request);
97
- if ("resolveError" in live) {
98
- const resolveDiagnostic =
99
- `live run path resolve failed beside fault: ${describeErrorIdentity(live.resolveError)}`;
94
+ let runDirectory = request.runDirectory;
95
+ if (!existsSync(runDirectory)) {
100
96
  try {
101
- process.stderr.write(`${input.diagnostic}\n`);
102
- process.stderr.write(`${resolveDiagnostic}\n`);
103
- } catch {
104
- // Best-effort presentation beside an already-chosen terminal.
97
+ const found = await resolveLiveRunDirectoryPath(request.runDirectory, request.home);
98
+ if (found === undefined) {
99
+ const resolveDiagnostic =
100
+ `live run path resolve failed beside fault: admitted run missing under ${request.home}`;
101
+ try {
102
+ process.stderr.write(`${input.diagnostic}\n`);
103
+ process.stderr.write(`${resolveDiagnostic}\n`);
104
+ } catch {
105
+ // Best-effort presentation beside an already-chosen terminal.
106
+ }
107
+ return;
108
+ }
109
+ runDirectory = found;
110
+ } catch (resolveError) {
111
+ const resolveDiagnostic =
112
+ `live run path resolve failed beside fault: ${describeErrorIdentity(resolveError)}`;
113
+ try {
114
+ process.stderr.write(`${input.diagnostic}\n`);
115
+ process.stderr.write(`${resolveDiagnostic}\n`);
116
+ } catch {
117
+ // Best-effort presentation beside an already-chosen terminal.
118
+ }
119
+ return;
105
120
  }
106
- return;
107
121
  }
108
122
  await retainPackageFault({
109
- runDirectory: live.runDirectory,
123
+ runDirectory,
110
124
  diagnostic: input.diagnostic,
111
125
  ...(Object.hasOwn(input, "error") ? { error: input.error } : {}),
112
126
  });
@@ -54,6 +54,7 @@ export function createProductionHeadlessRoleTurnHost(
54
54
  request,
55
55
  dependencies: roleRuntimeDependencies,
56
56
  sessionFile: sessionIdentity.resolveSessionFile(request.principal),
57
+ principalAuthority,
57
58
  // Same MCP relay as ACP so intermediate AK tools stay reachable;
58
59
  // headless adapter projects the row into --mcp-config.
59
60
  socketPath: `/tmp/ak-headless-mcp-${randomUUID()}.sock`,
@@ -564,7 +564,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
564
564
  }
565
565
 
566
566
  // What this CLI start was given: one history record per start.
567
- await recordTurnDelivery(await syncTurnRequestLivePlacement(request), {
567
+ await recordTurnDelivery(await syncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority), {
568
568
  systemPrompt,
569
569
  ...(startedWithSchema === undefined ? {} : { outputSchema: startedWithSchema }),
570
570
  }, "headless-host");
@@ -748,7 +748,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
748
748
  } finally {
749
749
  // Cleanup must not depend on live-path sync: lookup failure stays beside
750
750
  // the original cleanup/copy work and never skips rm(inputsDirectory).
751
- const live = await trySyncTurnRequestLivePlacement(request);
751
+ const live = await trySyncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority);
752
752
  const liveRunDirectory = "runDirectory" in live ? live.runDirectory : undefined;
753
753
  if (liveRunDirectory === undefined && "resolveError" in live) {
754
754
  await retainPackageFaultBesideLivePlacement(request, {
@@ -500,7 +500,7 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
500
500
  // bind projects host.sessions; copy records the landing. Host adapters do
501
501
  // not write current.json — public bind/settlement seams own that render.
502
502
  try {
503
- await syncTurnRequestLivePlacement(request);
503
+ await syncTurnRequestLivePlacement(request, config.principalAuthority);
504
504
  const { sessionFile, sessionDirectory } = config.principalAuthority.decode(request.principal);
505
505
  const sessionId = await readPiSessionHeaderId(sessionFile);
506
506
  if (sessionId !== undefined) {
@@ -75,4 +75,6 @@ export type SessionIdentityAuthority = Readonly<{
75
75
  bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
76
76
  /** Durable principal session path for layout ownership — not a rebuild source (#617 DK-4). */
77
77
  resolveSessionFile(principal: RoleTurnRequest["principal"]): string;
78
+ /** Host authority that sealed the principal — mid-turn relocate reseals through it (#1183). */
79
+ readonly principalAuthority: import("./host-contracts.ts").DurablePrincipalAuthority;
78
80
  }>;
@@ -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