@akagilnc/pi-workflow-roles 0.1.4745 → 0.1.4772

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.
@@ -10,11 +10,12 @@ import { writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
11
  import { buildAutoResumeContinuationPrompt, buildResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
12
12
  import { CliUsageError } from "./cli-errors.js";
13
- import { bindAdmittedTicketNumber, buildInstructionTransportPrompt, freezeAttachmentsIntoRun, } from "./invocation.js";
13
+ import { bindAdmittedTicketNumber, buildInstructionTransportPrompt, freezeAttachmentsIntoRun, relocateAdmittedRunToTicket, } from "./invocation.js";
14
14
  import { readRecordedSubmissionRows } from "../submission-ledger.js";
15
15
  import { pathContainedIn } from "../activation-ledger-topology.js";
16
16
  import { pickEngineAxis } from "../package-resources/engine-material.js";
17
17
  import { resolveHostAwareSessionAvailability } from "../session-identity.js";
18
+ import { rewriteRunDirectoryPathValue } from "../role-run-relocation.js";
18
19
  import { isOfficerReviewSeat } from "../host-contracts.js";
19
20
  import { deliverCaseDossierAsAttachment } from "./case-dossier-delivery.js";
20
21
  /** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
@@ -35,13 +36,31 @@ function isStationChildOfficerDialogue(role, env) {
35
36
  }
36
37
  import { mintEngineDetourInvocationScope, readInvocationSelectedHost, withEngineDetourInvocationScope, } from "../engine-detour-usage.js";
37
38
  import { projectHostTransitionPriorNative } from "../host-transition-prior-native.js";
38
- import { missingCredentialPreDispatchFailure, postRunMissingCredentialFailure, } from "./public-run-credentials.js";
39
- import { acquireRunWriterLease, clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
39
+ import { clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, } from "./run-lifecycle.js";
40
40
  import { processCancelDiagnostic, processCancelSignalName, } from "./process-cancel.js";
41
41
  import { homeFromRunDirectory } from "../activation-ledger-topology.js";
42
42
  import { attemptProducedFreshSubmission, classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, settleHostEndedNoReceipt, attachRecordedSubmissions, } from "./settlement.js";
43
43
  import {} from "./terminal.js";
44
44
  import { ensureRealArtifactsDirectory, persistReturnedRunState, runWithAutoResumeLoop, TurnDispatchedFailure, } from "./auto-resume.js";
45
+ function projectRelocatedTurnIdentity(request, result, admitted, relocation) {
46
+ const rewrite = (value) => rewriteRunDirectoryPathValue(value, relocation.oldRunDirectory, relocation.newRunDirectory);
47
+ const mutableRequest = request;
48
+ mutableRequest.runDirectory = admitted.runDirectory;
49
+ mutableRequest.principal = admitted.principal;
50
+ const activation = mutableRequest.activation;
51
+ if (activation !== null && typeof activation === "object" && !Array.isArray(activation)) {
52
+ for (const field of ["taskPath", "packetPath", "prerequisitesPath", "inputPath", "requestManifestPath"]) {
53
+ const record = activation;
54
+ if (field in record)
55
+ record[field] = rewrite(record[field]);
56
+ }
57
+ }
58
+ if (result.terminal !== undefined) {
59
+ for (const artifact of result.terminal.artifacts) {
60
+ artifact.path = rewrite(artifact.path);
61
+ }
62
+ }
63
+ }
45
64
  /** #855: process-cancel settlement never re-enters auto-resume. */
46
65
  function withProcessCancelSkipAutoResume(result, signal) {
47
66
  if (processCancelSignalName(signal) === undefined) {
@@ -110,11 +129,10 @@ export const POST_ADMISSION_CLEANUP_DIAGNOSTIC_ENTRY_TYPE = "ak_post_admission_c
110
129
  * redundant second copy (#840 bounce class 1: 同一业务规则只保留一个权威实现).
111
130
  */
112
131
  async function recordBestEffortPostDispatchDiagnostic(admitted, env, diagnostic, io) {
113
- io.stderr(formatCliDiagnostic(diagnostic));
114
132
  const payload = { diagnostic, recordedAt: new Date().toISOString() };
133
+ let retentionFailure;
115
134
  try {
116
135
  await env.sessionAppender(env.principalAuthority, admitted.principal, POST_ADMISSION_CLEANUP_DIAGNOSTIC_ENTRY_TYPE, payload);
117
- return;
118
136
  }
119
137
  catch (appendError) {
120
138
  try {
@@ -122,9 +140,19 @@ async function recordBestEffortPostDispatchDiagnostic(admitted, env, diagnostic,
122
140
  await writeFile(join(artifactsDir, `post-admission-diagnostic-${randomUUID()}.json`), `${JSON.stringify({ version: 1, ...payload }, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
123
141
  }
124
142
  catch (artifactError) {
125
- io.stderr(formatCliDiagnostic(`post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; artifact=${describeErrorIdentity(artifactError)}`));
143
+ retentionFailure =
144
+ `post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; artifact=${describeErrorIdentity(artifactError)}`;
126
145
  }
127
146
  }
147
+ try {
148
+ io.stderr(formatCliDiagnostic(diagnostic));
149
+ if (retentionFailure !== undefined) {
150
+ io.stderr(formatCliDiagnostic(retentionFailure));
151
+ }
152
+ }
153
+ catch {
154
+ // Presentation is best-effort beside an already-formed host terminal.
155
+ }
128
156
  }
129
157
  /**
130
158
  * Factory-seat resume method-material face (#833): one authority for
@@ -325,15 +353,19 @@ export async function dispatchPostAdmissionTurn(input) {
325
353
  if (ticketNumber !== undefined)
326
354
  await bindAdmittedTicketNumber(admitted, ticketNumber);
327
355
  }
356
+ // #863: shared post-admission bind must relocate unbound→ticket in-home
357
+ // before lease release (work-seat self-report and any prior board bind).
358
+ const relocation = await relocateAdmittedRunToTicket(admitted, env.principalAuthority, lease);
359
+ if (relocation !== undefined) {
360
+ projectRelocatedTurnIdentity(request, result, admitted, relocation);
361
+ }
328
362
  if (adapters.afterDispatch !== undefined)
329
363
  await adapters.afterDispatch(admitted, lease);
330
364
  return result;
331
365
  }
332
366
  catch (error) {
333
- const primaryFailure = result.terminal !== undefined
334
- && !isLawfulTypedTerminalOutcome(result.terminal.roleOutcome);
335
- if (primaryFailure) {
336
- await recordBestEffortPostDispatchDiagnostic(admitted, env, `afterDispatch failed beside primary terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
367
+ if (result.terminal !== undefined) {
368
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `afterDispatch failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
337
369
  return result;
338
370
  }
339
371
  return {
@@ -350,13 +382,8 @@ export async function dispatchPostAdmissionTurn(input) {
350
382
  }
351
383
  };
352
384
  try {
353
- const missingCredential = missingCredentialPreDispatchFailure(env.model, env.credentials);
354
- if (missingCredential !== undefined) {
355
- return {
356
- ...(await presentControlledFailure(admitted, withEngineDetourInvocationScope(missingCredential, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState)),
357
- ...deferredPersist,
358
- };
359
- }
385
+ // #987: do not pre-block or classify host dispatch from AK credential
386
+ // catalog facts. Per-invocation runner/host evidence owns provider identity.
360
387
  // #617 DK-4: capture previous invocation host before markRunRunning overwrites it.
361
388
  // Single authority projectHostTransitionPriorNative classifies the prior native volume.
362
389
  // Same-run resume (#637) keeps host identity on the run's invocation page.
@@ -549,11 +576,10 @@ export async function dispatchPostAdmissionTurn(input) {
549
576
  const runnerKnownFailure = adapters.resolveRunnerKnownFailure !== undefined && sessionFile !== ""
550
577
  ? await adapters.resolveRunnerKnownFailure({ result, sessionFile })
551
578
  : result.knownFailure;
552
- const credentialFailure = postRunMissingCredentialFailure(result, env.model, env.credentials);
553
579
  resolution = await resolveAuditedRunnerFailureResolution({
554
580
  runner: runnerKnownFailure,
555
581
  sessionFile,
556
- credential: credentialFailure,
582
+ credential: undefined,
557
583
  runDirectory: admitted.runDirectory,
558
584
  });
559
585
  // A direct, current signal from the host/runner itself (timeout / host
@@ -573,7 +599,6 @@ export async function dispatchPostAdmissionTurn(input) {
573
599
  const directHostFailureSignal = result.timedOut
574
600
  || result.knownFailure !== undefined
575
601
  || runnerKnownFailure !== undefined
576
- || credentialFailure !== undefined
577
602
  || processCancelName !== undefined;
578
603
  hostSignalFailed =
579
604
  directHostFailureSignal
@@ -679,24 +704,7 @@ export async function dispatchPostAdmissionTurn(input) {
679
704
  await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
680
705
  }
681
706
  catch (error) {
682
- // #836: `settledOutcome.terminal` already carries recorded
683
- // submissions (attachRecordedSubmissions above). A real run-state
684
- // persistence failure here must surface loudly through the same
685
- // controlled-failure seam every other dispatch-time failure in
686
- // this function uses (ADR 0080: one settlement disposition owner)
687
- // — not escape uncaught to auto-resume's dispatch-retry path,
688
- // whose exhausted-budget terminal carries no recorded submissions
689
- // at all. skipRunStateWrite: the write that just threw is the same
690
- // write presentControlledFailure would otherwise retry — don't
691
- // call a known-failing operation twice.
692
- const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
693
- timedOut: false,
694
- code: null,
695
- stderr: "",
696
- thrown: error,
697
- skipRunStateWrite: true,
698
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
699
- return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...failed, turnDispatched: true, ...deferredPersist }, env.signal));
707
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
700
708
  }
701
709
  }
702
710
  // Lawful persist + present is the caller's stop seam (auto-resume loop /
@@ -768,19 +776,7 @@ export async function dispatchPostAdmissionTurn(input) {
768
776
  await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
769
777
  }
770
778
  catch (error) {
771
- // #836: same run-state persistence hazard as the settled/accepted
772
- // branch above — route through the shared controlled-failure seam so
773
- // the real failure surfaces loudly instead of escaping uncaught to
774
- // auto-resume's dispatch-retry path with no recorded submissions.
775
- // skipRunStateWrite: don't retry the write that just threw.
776
- const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
777
- timedOut: false,
778
- code: null,
779
- stderr: "",
780
- thrown: error,
781
- skipRunStateWrite: true,
782
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
783
- return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...failed, turnDispatched: true, ...deferredPersist }, env.signal));
779
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
784
780
  }
785
781
  }
786
782
  // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
@@ -793,17 +789,20 @@ export async function dispatchPostAdmissionTurn(input) {
793
789
  });
794
790
  }
795
791
  finally {
796
- try {
797
- await lease.release();
798
- }
799
- catch (error) {
800
- // lease.release() is documented best-effort and never rejects in the
801
- // production acquireRunWriterLease implementation (createWriterLease
802
- // reports cleanup failures via callback, never throws) — this guard is
803
- // structural only: an uncaught throw from a finally block silently
804
- // replaces whatever the try already returned, including a properly
805
- // tagged turnDispatched:true result (#840 r9 判词 class 1 boundary).
806
- io.stderr(formatCliDiagnostic(`writer lease release failed unexpectedly (best-effort continue): ${describeErrorIdentity(error)}`));
792
+ // Public manual resume (#987) holds no package writer lease.
793
+ if (lease !== undefined) {
794
+ try {
795
+ await lease.release();
796
+ }
797
+ catch (error) {
798
+ // lease.release() is documented best-effort and never rejects in the
799
+ // production acquireRunWriterLease implementation (createWriterLease
800
+ // reports cleanup failures via callback, never throws) — this guard is
801
+ // structural only: an uncaught throw from a finally block silently
802
+ // replaces whatever the try already returned, including a properly
803
+ // tagged turnDispatched:true result (#840 r9 判词 class 1 boundary).
804
+ io.stderr(formatCliDiagnostic(`writer lease release failed unexpectedly (best-effort continue): ${describeErrorIdentity(error)}`));
805
+ }
807
806
  }
808
807
  }
809
808
  }
@@ -951,8 +950,10 @@ export async function prepareSummonsResumeMaterials(runDirectory, summons) {
951
950
  */
952
951
  export async function runPostAdmissionSeatResume(input) {
953
952
  let request = input.request;
954
- // Load once for runDirectory / structural rejection / afterAdmittedLoad;
955
- // court identity is judged only after the writer lease is held (below).
953
+ // Load once for runDirectory / structural rejection / afterAdmittedLoad.
954
+ // Court identity for public manual resume is judged in the turn builder
955
+ // (#987: no package writer-lease gate before host CLI resume). Station-child
956
+ // auto-resume still acquires the shared lease in runWithAutoResumeLoop.
956
957
  let loaded;
957
958
  try {
958
959
  loaded = await input.load(request);
@@ -981,8 +982,9 @@ export async function runPostAdmissionSeatResume(input) {
981
982
  let preparedCleanup;
982
983
  const buildRequestAfterLease = async () => {
983
984
  let openCourtAttemptId;
984
- // Build uses the admitted judged under this lease (rehydrated when open
985
- // court materials ride). Settlement identity stays on the outer admitted.
985
+ // Build uses the admitted for this resume (rehydrated when open court
986
+ // materials ride). Settlement identity stays on the outer admitted.
987
+ // Name kept for station-child callers that still build under lease.
986
988
  let admittedForBuild = loaded.admitted;
987
989
  // Bare resume: open-court pointer is the continue signal (not ledger seal).
988
990
  if (request.summons === undefined) {
@@ -1043,9 +1045,10 @@ export async function runPostAdmissionSeatResume(input) {
1043
1045
  }
1044
1046
  return turnRequest;
1045
1047
  };
1046
- // Court recovery / open under lease, then dispatch.
1048
+ // Court recovery / open, then dispatch. Public manual resume does not take a
1049
+ // package writer lease before the host CLI (#987 / ADR 0080 one-shot).
1047
1050
  // Station-child same-ticket/same-parent resume is call-local auto-resume
1048
- // (#840 / #416). Public `ak-role resume` stays one-shot (ADR 0080).
1051
+ // (#840 / #416) and still acquires the shared lease in its loop.
1049
1052
  // afterAdmittedPrepare runs inside this try so mint failure and cleanup share one finally.
1050
1053
  try {
1051
1054
  if (input.afterAdmittedPrepare !== undefined) {
@@ -1217,74 +1220,41 @@ export async function runPostAdmissionResumable(input) {
1217
1220
  });
1218
1221
  }
1219
1222
  /**
1220
- * Manual resume: lease + dispatch. Pass-through to the host — no sealed-accepted
1221
- * short-circuit (#833 / #416). Court open (summons / message / open court) is
1222
- * built under lease when using buildRequestAfterLease; sole-final stays per-attempt.
1223
- * After-lease build shares dispatchAfterWriterLease with station-child auto-resume.
1223
+ * Manual resume: pass-through to the host CLI resume — no package writer-lease
1224
+ * pre-gate (#987), no sealed-accepted short-circuit (#833 / #416). Court open
1225
+ * (summons / message / open court) is built when using buildRequestAfterLease;
1226
+ * sole-final stays per-attempt. Station-child auto-resume keeps shared lease
1227
+ * acquire in runWithAutoResumeLoop + dispatchAfterWriterLease.
1224
1228
  */
1225
1229
  export async function runPostAdmissionManualResume(input) {
1226
1230
  const { admitted, env, io, adapters, effectiveEngine, buildRequestAfterLease, } = input;
1227
1231
  let request = input.request;
1228
1232
  // #617 DK-3: manual resume writes the live seat/env model (same as new legs).
1229
1233
  const effectiveModel = env.model;
1230
- let lease;
1231
- let staleWriterLeaseReclaimed;
1232
- try {
1233
- lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic, kind) => {
1234
- // Record the typed fact before the fallible sink: if io.stderr throws
1235
- // (acquire deliberately swallows diagnostic-sink failures), the reclaim
1236
- // still happened and must stay observable.
1237
- if (kind === "stale-reclaimed")
1238
- staleWriterLeaseReclaimed = true;
1239
- io.stderr(diagnostic);
1240
- });
1241
- }
1242
- catch (error) {
1243
- if (error instanceof RunWriterLeaseHeldError) {
1244
- io.stderr(formatCliDiagnostic(error.message));
1245
- // A held rejection after our own reclaim must still carry the fact that
1246
- // this caller reclaimed the stale lock — e.g. another resumer re-locked
1247
- // before our retry create (#629).
1248
- return {
1249
- exitCode: 1,
1250
- ...(staleWriterLeaseReclaimed === true
1251
- ? { staleWriterLeaseReclaimed: true }
1252
- : {}),
1253
- };
1254
- }
1255
- throw error;
1256
- }
1257
1234
  // Explicit public resume is a new counting unit — mint once for this call.
1258
1235
  const invocationScopeId = mintEngineDetourInvocationScope({
1259
1236
  ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
1260
1237
  });
1261
- const result = await dispatchAfterWriterLease({
1262
- lease,
1263
- build: async () => {
1264
- if (request === undefined) {
1265
- if (buildRequestAfterLease === undefined) {
1266
- throw new Error("runPostAdmissionManualResume requires request or buildRequestAfterLease");
1267
- }
1268
- request = await buildRequestAfterLease();
1269
- }
1270
- request = withEngineDetourInvocationScope(request, invocationScopeId);
1271
- return request;
1238
+ if (request === undefined) {
1239
+ if (buildRequestAfterLease === undefined) {
1240
+ throw new Error("runPostAdmissionManualResume requires request or buildRequestAfterLease");
1241
+ }
1242
+ request = await buildRequestAfterLease();
1243
+ }
1244
+ request = withEngineDetourInvocationScope(request, invocationScopeId);
1245
+ const result = await dispatchPostAdmissionTurn({
1246
+ admitted,
1247
+ env: {
1248
+ ...env,
1249
+ ...(effectiveModel === undefined ? {} : { model: effectiveModel }),
1250
+ ...(admitted.correlationId === undefined
1251
+ ? {}
1252
+ : { correlationId: admitted.correlationId }),
1272
1253
  },
1273
- dispatch: (turnRequest) => dispatchPostAdmissionTurn({
1274
- admitted,
1275
- env: {
1276
- ...env,
1277
- ...(effectiveModel === undefined ? {} : { model: effectiveModel }),
1278
- ...(admitted.correlationId === undefined
1279
- ? {}
1280
- : { correlationId: admitted.correlationId }),
1281
- },
1282
- io,
1283
- request: turnRequest,
1284
- lease,
1285
- adapters,
1286
- ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
1287
- }),
1254
+ io,
1255
+ request,
1256
+ adapters,
1257
+ ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
1288
1258
  });
1289
1259
  if (result.terminal !== undefined &&
1290
1260
  isLawfulTypedTerminalOutcome(result.terminal.roleOutcome)) {
@@ -1298,10 +1268,5 @@ export async function runPostAdmissionManualResume(input) {
1298
1268
  if (result.terminal !== undefined) {
1299
1269
  result.terminal.autoResumeCount = 0;
1300
1270
  }
1301
- return {
1302
- ...result,
1303
- ...(staleWriterLeaseReclaimed === true
1304
- ? { staleWriterLeaseReclaimed: true }
1305
- : {}),
1306
- };
1271
+ return result;
1307
1272
  }
@@ -15,6 +15,7 @@ import { readLatestTypedProviderHttpObservation, } from "../typed-provider-http.
15
15
  export { clearTypedProviderHttpObservation, recordTypedProviderHttpStatus, readLatestTypedProviderHttpObservation, } from "../typed-provider-http.js";
16
16
  import { parseCollectorRepository } from "../collector-config.js";
17
17
  import { interpretDurableCourtTicketNumbers, sameCourtTicketNumbers, } from "../diarist-contracts.js";
18
+ import { rewriteAdmittedRoleRunPage, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
18
19
  import { appendEngineSessionMaterial, engineSessionMaterialFromOptions, pickEngineAxis, } from "../package-resources/engine-material.js";
19
20
  import { recordEffectiveInvocationModel, requireAuthorityRef, isReviewerLens, } from "./invocation.js";
20
21
  /** Providers eligible for v1 typed-429 resume (Codex / xAI only).
@@ -209,14 +210,15 @@ async function readRoleRunStateDisk(runDirectory) {
209
210
  return undefined;
210
211
  if (typeof record.admittedRequestPath !== "string")
211
212
  return undefined;
212
- const runDir = typeof record.runDirectory === "string" && record.runDirectory.trim() !== ""
213
+ const storedRunDirectory = typeof record.runDirectory === "string" && record.runDirectory.trim() !== ""
213
214
  ? record.runDirectory
214
215
  : runDirectory;
216
+ const runDir = runDirectory;
215
217
  // Principal wire stays uninterpreted — authority owns legacy sessionFile fallback.
216
218
  const principalWire = {
217
- sessionDirectory: record.sessionDirectory,
219
+ sessionDirectory: rewriteRunDirectoryPathValue(record.sessionDirectory, storedRunDirectory, runDirectory),
218
220
  ...(typeof record.sessionFile === "string"
219
- ? { sessionFile: record.sessionFile }
221
+ ? { sessionFile: rewriteRunDirectoryPathValue(record.sessionFile, storedRunDirectory, runDirectory) }
220
222
  : {}),
221
223
  };
222
224
  let resumable;
@@ -242,7 +244,7 @@ async function readRoleRunStateDisk(runDirectory) {
242
244
  bookKey: record.bookKey,
243
245
  projectRoot: record.projectRoot,
244
246
  runDirectory: runDir,
245
- admittedRequestPath: record.admittedRequestPath,
247
+ admittedRequestPath: rewriteRunDirectoryPathValue(record.admittedRequestPath, storedRunDirectory, runDirectory),
246
248
  principalWire,
247
249
  ...(phase === undefined ? {} : { phase }),
248
250
  ...(resumable === undefined ? {} : { resumable }),
@@ -1017,6 +1019,13 @@ async function loadResumableRunRecord(home, runId, authority) {
1017
1019
  const raw = JSON.parse(await readFile(run.admittedRequestPath, "utf8"));
1018
1020
  if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
1019
1021
  const record = raw;
1022
+ const storedRunDirectory = typeof record.runDirectory === "string" && record.runDirectory.trim() !== ""
1023
+ ? record.runDirectory
1024
+ : run.runDirectory;
1025
+ rewriteAdmittedRoleRunPage(record, [{
1026
+ oldRunDirectory: storedRunDirectory,
1027
+ newRunDirectory: run.runDirectory,
1028
+ }]);
1020
1029
  if (typeof record.instruction === "string") {
1021
1030
  instruction = record.instruction;
1022
1031
  }
@@ -1201,6 +1210,18 @@ async function loadResumableRunRecord(home, runId, authority) {
1201
1210
  else if (mergedSet.tickets !== undefined) {
1202
1211
  courtTicketNumbers = mergedSet.tickets;
1203
1212
  }
1213
+ const sourceRunLeaf = basename(sourceRunPath ?? "").split("@");
1214
+ const referencedRunId = sourceRun?.runId ?? sourceRunLeaf[0];
1215
+ const referencedRole = sourceRun?.role ?? sourceRunLeaf[1];
1216
+ if (referencedRunId !== undefined && referencedRunId !== "") {
1217
+ const currentSourceRunDirectory = await findRunDirectoryById(home, referencedRunId, run.bookKey, referencedRole);
1218
+ if (currentSourceRunDirectory !== undefined) {
1219
+ sourceRunPath = currentSourceRunDirectory;
1220
+ if (sourceRun !== undefined) {
1221
+ sourceRun = { ...sourceRun, runDirectory: currentSourceRunDirectory };
1222
+ }
1223
+ }
1224
+ }
1204
1225
  return {
1205
1226
  run,
1206
1227
  principal,
@@ -132,6 +132,21 @@ function rewriteSummonsMaterials(value, rewrites) {
132
132
  value.attachmentPaths = value.attachmentPaths.map((path) => rewriteRunDirectoryPathValueAgainstRewrites(path, rewrites));
133
133
  }
134
134
  }
135
+ /** Project one admitted page in memory after its containing run moved. */
136
+ export function rewriteAdmittedRoleRunPage(page, rewrites) {
137
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, ADMITTED_PAGE_FIELDS, rewrites);
138
+ rewriteSourceRunLocator(page.sourceRun, rewrites);
139
+ if (Array.isArray(page.attachments)) {
140
+ for (const attachment of page.attachments) {
141
+ if (isPlainObject(attachment)) {
142
+ rewriteRunDirectoryPathFieldsAgainstRewrites(attachment, ["frozenPath"], rewrites);
143
+ }
144
+ }
145
+ }
146
+ if (isPlainObject(page.principal)) {
147
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, ["sessionDirectory", "sessionFile"], rewrites);
148
+ }
149
+ }
135
150
  async function rewriteJsonObjectFile(path, fields, rewrites) {
136
151
  if (!existsSync(path))
137
152
  return;
@@ -307,9 +322,9 @@ async function rewriteNestedMachinePathPages(pagesDirectory, rewrites) {
307
322
  /**
308
323
  * Rewrite admitted-request / invocation / run-state path fields (attachment
309
324
  * frozenPath / summons.attachmentPaths pointers only), then nested package-owned
310
- * session seams, after the run directory has already been moved or copied.
311
- * `pagesDirectory` is where the pages now live; path strings still naming an
312
- * old run directory become the matching new directory. `crossRunRewrites`
325
+ * session seams. Callers may rewrite before or after the filesystem move/copy:
326
+ * `pagesDirectory` is where the pages currently live on disk; path strings that
327
+ * still name `oldRunDirectory` become `newRunDirectory`. `crossRunRewrites`
313
328
  * covers officer/parent pointers that landed under a different final placement.
314
329
  */
315
330
  export async function rewriteRoleRunDurablePages(input) {
@@ -318,21 +333,7 @@ export async function rewriteRoleRunDurablePages(input) {
318
333
  const admittedPath = join(pagesDirectory, "admitted-request.json");
319
334
  if (existsSync(admittedPath)) {
320
335
  const page = JSON.parse(await readFile(admittedPath, "utf8"));
321
- rewriteRunDirectoryPathFieldsAgainstRewrites(page, ADMITTED_PAGE_FIELDS, rewrites);
322
- // Nested notary typed locator — same value family as top-level sourceRunPath.
323
- rewriteSourceRunLocator(page.sourceRun, rewrites);
324
- if (Array.isArray(page.attachments)) {
325
- for (const attachment of page.attachments) {
326
- if (isPlainObject(attachment)) {
327
- // Pointer only — never open or mutate frozen attachment bytes.
328
- rewriteRunDirectoryPathFieldsAgainstRewrites(attachment, ["frozenPath"], rewrites);
329
- }
330
- }
331
- }
332
- // Nested principal wire (sessionDirectory/sessionFile) when present.
333
- if (isPlainObject(page.principal)) {
334
- rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, ["sessionDirectory", "sessionFile"], rewrites);
335
- }
336
+ rewriteAdmittedRoleRunPage(page, rewrites);
336
337
  await writeFile(admittedPath, `${JSON.stringify(page, null, 2)}\n`, "utf8");
337
338
  }
338
339
  const invocationPath = join(pagesDirectory, "invocation.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4745",
3
+ "version": "0.1.4772",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -397,7 +397,15 @@ export async function findPlacedMigratingRun(
397
397
  const preferred = destPathFromSourceRelative(booksDirectory, bookKey, sourceRelative);
398
398
  if (preferred !== undefined) {
399
399
  const hit = matches.find((path) => path === preferred);
400
- return hit === undefined ? undefined : placedRunFromPath(hit);
400
+ if (hit !== undefined) return placedRunFromPath(hit);
401
+ // T9 may have used the board ticket to move an historical unbound leaf
402
+ // under `<ticket>/runs`. The complete leaf is the placement identity;
403
+ // when it remains unique, downstream record/mixed/T11 consumers must
404
+ // follow that authoritative T9 result rather than re-derive unbound.
405
+ if (sourceRelative.replaceAll("\\", "/").startsWith("unbound/runs/")) {
406
+ return uniquePlacedRun(leafName, matches);
407
+ }
408
+ return undefined;
401
409
  }
402
410
  // Historical flat `runs/<leaf>` alias after T9 nested the unique complete leaf.
403
411
  if (isFlatRunsRelative(sourceRelative, leafName)) {