@akagilnc/pi-workflow-roles 0.1.4759 → 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.
- package/dist/acp-host/production-host.js +227 -428
- package/dist/headless-host/production-host.js +233 -434
- package/dist/migrate-book-topology.js +1255 -1087
- package/dist/notary-source-run.js +12 -2
- package/dist/public-cli/invocation.js +11 -8
- package/dist/public-cli/main.js +198 -399
- package/dist/public-cli/post-admission.js +27 -1
- package/dist/public-cli/run-lifecycle.js +25 -4
- package/dist/role-run-relocation.js +19 -18
- package/package.json +1 -1
- package/src/book-topology-migration-placement.ts +9 -1
- package/src/book-topology-runs-migrator.ts +156 -4
- package/src/notary-source-run.ts +18 -2
- package/src/public-cli/invocation.ts +13 -9
- package/src/public-cli/post-admission.ts +36 -0
- package/src/public-cli/run-lifecycle.ts +42 -4
- package/src/role-run-relocation.ts +27 -30
|
@@ -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. */
|
|
@@ -41,6 +42,25 @@ import { homeFromRunDirectory } from "../activation-ledger-topology.js";
|
|
|
41
42
|
import { attemptProducedFreshSubmission, classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, settleHostEndedNoReceipt, attachRecordedSubmissions, } from "./settlement.js";
|
|
42
43
|
import {} from "./terminal.js";
|
|
43
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
|
+
}
|
|
44
64
|
/** #855: process-cancel settlement never re-enters auto-resume. */
|
|
45
65
|
function withProcessCancelSkipAutoResume(result, signal) {
|
|
46
66
|
if (processCancelSignalName(signal) === undefined) {
|
|
@@ -333,6 +353,12 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
333
353
|
if (ticketNumber !== undefined)
|
|
334
354
|
await bindAdmittedTicketNumber(admitted, ticketNumber);
|
|
335
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
|
+
}
|
|
336
362
|
if (adapters.afterDispatch !== undefined)
|
|
337
363
|
await adapters.afterDispatch(admitted, lease);
|
|
338
364
|
return result;
|
|
@@ -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
|
|
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
|
|
311
|
-
* `pagesDirectory` is where the pages
|
|
312
|
-
*
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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)) {
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* #865 T9: migrate retained runs into ticket-scoped or unbound placement.
|
|
3
3
|
* Inventory covers legacy flat `runs/`, existing `<ticket>/runs/`, and
|
|
4
4
|
* `unbound/runs/`. Attribution reuses the shared migrating-run helper;
|
|
5
|
-
* already-canonical trees copy as-is.
|
|
5
|
+
* already-canonical ticket trees copy as-is. Unbound leaves that already
|
|
6
|
+
* hold a board typed ticketNumber place under that ticket (#863 stock).
|
|
6
7
|
*/
|
|
7
|
-
import { cp, mkdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { cp, mkdir, rename, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
9
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
9
10
|
|
|
10
11
|
import {
|
|
@@ -16,16 +17,21 @@ import {
|
|
|
16
17
|
resolveMigratingRunTicket,
|
|
17
18
|
} from "./book-topology-migration-placement.ts";
|
|
18
19
|
import {
|
|
20
|
+
assertBookTopologyMigrationPrerequisites,
|
|
19
21
|
reconcileMigrationPartition,
|
|
20
22
|
type BookTopologyMigrationContext,
|
|
21
23
|
type BookTopologyPartitionMigrator,
|
|
22
24
|
type MigrationItemOutcome,
|
|
23
25
|
} from "./book-topology-migration.ts";
|
|
26
|
+
import { listBookRunDirectories } from "./role-run-placement.ts";
|
|
24
27
|
import {
|
|
25
28
|
rewriteRoleRunDurablePages,
|
|
26
29
|
type RunDirectoryPathRewrite,
|
|
27
30
|
} from "./role-run-relocation.ts";
|
|
28
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
MIGRATION_TICKET_DERIVATION_PAGE,
|
|
33
|
+
readBoardTicketNumber,
|
|
34
|
+
} from "./run-ticket-number.ts";
|
|
29
35
|
|
|
30
36
|
const RUNS_PARTITION = "runs";
|
|
31
37
|
|
|
@@ -141,9 +147,38 @@ async function planBookMoves(
|
|
|
141
147
|
historicalRunDirectory = join(booksDirectory, bookKey, leaf.relativePath);
|
|
142
148
|
}
|
|
143
149
|
derivation = undefined;
|
|
150
|
+
} else if (leaf.layout === "unbound") {
|
|
151
|
+
// #863: board typed ticket on an unbound leaf → place under that ticket.
|
|
152
|
+
// No board ticket → stay unbound. Never invent from prose or derivation.
|
|
153
|
+
const boardTicket = leaf.isDirectory
|
|
154
|
+
? await readBoardTicketNumber(leaf.sourcePath)
|
|
155
|
+
: undefined;
|
|
156
|
+
if (parsed !== undefined && boardTicket !== undefined) {
|
|
157
|
+
targetPath = destinationRunDirectory(
|
|
158
|
+
booksDirectory,
|
|
159
|
+
bookKey,
|
|
160
|
+
boardTicket,
|
|
161
|
+
parsed.runId,
|
|
162
|
+
parsed.role,
|
|
163
|
+
);
|
|
164
|
+
disposition = "placed";
|
|
165
|
+
historicalRunDirectory = join(
|
|
166
|
+
booksDirectory,
|
|
167
|
+
bookKey,
|
|
168
|
+
"unbound",
|
|
169
|
+
"runs",
|
|
170
|
+
leaf.leafName,
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
targetPath = join(booksDirectory, bookKey, leaf.relativePath);
|
|
174
|
+
disposition = "unbound";
|
|
175
|
+
historicalRunDirectory = undefined;
|
|
176
|
+
}
|
|
177
|
+
derivation = undefined;
|
|
144
178
|
} else if (leaf.layout !== "flat") {
|
|
179
|
+
// Already-canonical `<ticket>/runs/` — copy as-is.
|
|
145
180
|
targetPath = join(booksDirectory, bookKey, leaf.relativePath);
|
|
146
|
-
disposition =
|
|
181
|
+
disposition = "placed";
|
|
147
182
|
historicalRunDirectory = undefined;
|
|
148
183
|
derivation = undefined;
|
|
149
184
|
} else if (parsed === undefined) {
|
|
@@ -242,3 +277,120 @@ export const bookTopologyRunsMigrator: BookTopologyPartitionMigrator = {
|
|
|
242
277
|
return reconcileMigrationPartition(RUNS_PARTITION, "entries", outcomes);
|
|
243
278
|
},
|
|
244
279
|
};
|
|
280
|
+
|
|
281
|
+
export type BoardBoundUnboundRelocation = {
|
|
282
|
+
readonly from: string;
|
|
283
|
+
readonly to: string;
|
|
284
|
+
readonly ticketNumber: number;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
type PlannedBoardBoundMove = {
|
|
288
|
+
readonly sourcePath: string;
|
|
289
|
+
readonly targetPath: string;
|
|
290
|
+
readonly ticketNumber: number;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Live in-place repair for already-migrated trees (#863 stock): plan the full
|
|
295
|
+
* board-bound unbound→ticket batch first, refuse any overwrite before mutation,
|
|
296
|
+
* rewrite durable pages (including batch `crossRunRewrites`) while sources still
|
|
297
|
+
* sit at unbound, then rename. Parse/write failures therefore leave sources in
|
|
298
|
+
* place so a retry can finish the same closure; rename-only retries still find
|
|
299
|
+
* remaining unbound sources. Leaves without a board ticket stay unbound.
|
|
300
|
+
*/
|
|
301
|
+
export async function relocateBoardBoundUnboundRunsInBook(
|
|
302
|
+
bookDirectory: string,
|
|
303
|
+
): Promise<readonly BoardBoundUnboundRelocation[]> {
|
|
304
|
+
const bookKey = basename(bookDirectory);
|
|
305
|
+
const booksDirectory = dirname(bookDirectory);
|
|
306
|
+
const unboundRuns = join(bookDirectory, "unbound", "runs");
|
|
307
|
+
let entries;
|
|
308
|
+
try {
|
|
309
|
+
entries = await readdir(unboundRuns, { withFileTypes: true });
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (isMigrationEnoent(error)) return [];
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const planned: PlannedBoardBoundMove[] = [];
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
if (!entry.isDirectory()) continue;
|
|
318
|
+
const parsed = parseRunLeaf(entry.name);
|
|
319
|
+
if (parsed === undefined) continue;
|
|
320
|
+
const sourcePath = join(unboundRuns, entry.name);
|
|
321
|
+
const boardTicket = await readBoardTicketNumber(sourcePath);
|
|
322
|
+
if (boardTicket === undefined) continue;
|
|
323
|
+
const targetPath = destinationRunDirectory(
|
|
324
|
+
booksDirectory,
|
|
325
|
+
bookKey,
|
|
326
|
+
boardTicket,
|
|
327
|
+
parsed.runId,
|
|
328
|
+
parsed.role,
|
|
329
|
+
);
|
|
330
|
+
planned.push({
|
|
331
|
+
sourcePath,
|
|
332
|
+
targetPath,
|
|
333
|
+
ticketNumber: boardTicket,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (planned.length === 0) return [];
|
|
338
|
+
|
|
339
|
+
for (const move of planned) {
|
|
340
|
+
if (await pathExists(move.targetPath)) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`board-bound unbound relocate refuses to overwrite ${move.targetPath} with ${move.sourcePath}`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const crossRunRewrites: RunDirectoryPathRewrite[] = planned.map((move) => ({
|
|
348
|
+
oldRunDirectory: move.sourcePath,
|
|
349
|
+
newRunDirectory: move.targetPath,
|
|
350
|
+
}));
|
|
351
|
+
const relocatedBySource = new Map(
|
|
352
|
+
planned.map((move) => [move.sourcePath, move] as const),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Rewrite before any rename so durable-page failures remain retryable from
|
|
356
|
+
// the same unbound sources (failure-honesty: no relocatedCount=0 whitewash).
|
|
357
|
+
for (const runDir of await listBookRunDirectories(bookDirectory)) {
|
|
358
|
+
const move = relocatedBySource.get(runDir);
|
|
359
|
+
await rewriteRoleRunDurablePages({
|
|
360
|
+
pagesDirectory: runDir,
|
|
361
|
+
oldRunDirectory: move?.sourcePath ?? runDir,
|
|
362
|
+
newRunDirectory: move?.targetPath ?? runDir,
|
|
363
|
+
crossRunRewrites,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const move of planned) {
|
|
368
|
+
await mkdir(dirname(move.targetPath), { recursive: true });
|
|
369
|
+
await rename(move.sourcePath, move.targetPath);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return planned.map((move) => ({
|
|
373
|
+
from: move.sourcePath,
|
|
374
|
+
to: move.targetPath,
|
|
375
|
+
ticketNumber: move.ticketNumber,
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Walk every book under `booksDirectory` and relocate board-bound unbound
|
|
381
|
+
* runs in place. Requires zero live writer locks (same gate as topology migrate).
|
|
382
|
+
*/
|
|
383
|
+
export async function relocateBoardBoundUnboundRunsInBooks(
|
|
384
|
+
booksDirectory: string,
|
|
385
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
386
|
+
): Promise<readonly BoardBoundUnboundRelocation[]> {
|
|
387
|
+
await assertBookTopologyMigrationPrerequisites(booksDirectory, env);
|
|
388
|
+
const relocated: BoardBoundUnboundRelocation[] = [];
|
|
389
|
+
for (const bookKey of await listMigrationBookKeys(booksDirectory)) {
|
|
390
|
+
const batch = await relocateBoardBoundUnboundRunsInBook(
|
|
391
|
+
join(booksDirectory, bookKey),
|
|
392
|
+
);
|
|
393
|
+
relocated.push(...batch);
|
|
394
|
+
}
|
|
395
|
+
return relocated;
|
|
396
|
+
}
|
package/src/notary-source-run.ts
CHANGED
|
@@ -87,7 +87,8 @@ export async function resolveNotarySourceRunLocator(options: {
|
|
|
87
87
|
|
|
88
88
|
const ledgerHome = resolveActivationLedgerHome(options.home);
|
|
89
89
|
const bookKey = resolveBookKeyFromGit(options.projectRoot);
|
|
90
|
-
const
|
|
90
|
+
const bookDirectory = activationBookDirectory(ledgerHome, bookKey);
|
|
91
|
+
const bookRunsRoot = join(bookDirectory, "runs");
|
|
91
92
|
|
|
92
93
|
let candidate: string;
|
|
93
94
|
const bare = parseRunDirectoryName(raw);
|
|
@@ -96,12 +97,27 @@ export async function resolveNotarySourceRunLocator(options: {
|
|
|
96
97
|
?? join(bookRunsRoot, `${bare.runId}@${bare.role}`);
|
|
97
98
|
} else {
|
|
98
99
|
candidate = isAbsolute(raw) ? raw : resolve(options.projectRoot, raw);
|
|
100
|
+
const identity = parseRunDirectoryName(basename(candidate));
|
|
101
|
+
const subjectDirectory = dirname(dirname(candidate));
|
|
102
|
+
const isLegacyUnboundLocator =
|
|
103
|
+
identity !== undefined &&
|
|
104
|
+
basename(dirname(candidate)) === "runs" &&
|
|
105
|
+
basename(subjectDirectory) === "unbound" &&
|
|
106
|
+
resolve(dirname(subjectDirectory)) === resolve(bookDirectory);
|
|
107
|
+
if (isLegacyUnboundLocator) {
|
|
108
|
+
candidate = (await findRunDirectoryById(
|
|
109
|
+
options.home,
|
|
110
|
+
identity.runId,
|
|
111
|
+
bookKey,
|
|
112
|
+
identity.role,
|
|
113
|
+
)) ?? candidate;
|
|
114
|
+
}
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
const real = await requireRunDirectory(candidate, raw);
|
|
102
118
|
const identity = parseRunDirectoryName(basename(real))!;
|
|
103
119
|
|
|
104
|
-
const bookIdentity = physicalPathIdentity(
|
|
120
|
+
const bookIdentity = physicalPathIdentity(bookDirectory);
|
|
105
121
|
const parentIdentity = physicalPathIdentity(dirname(real));
|
|
106
122
|
const subjectBookIdentity = physicalPathIdentity(dirname(dirname(dirname(real))));
|
|
107
123
|
if (
|
|
@@ -41,7 +41,6 @@ import {
|
|
|
41
41
|
requireSafePositiveTicketNumber,
|
|
42
42
|
} from "../run-ticket-number.ts";
|
|
43
43
|
import {
|
|
44
|
-
rewriteRoleRunDurablePages,
|
|
45
44
|
rewriteRunDirectoryPathFields,
|
|
46
45
|
rewriteRunDirectoryPathValue,
|
|
47
46
|
} from "../role-run-relocation.ts";
|
|
@@ -628,8 +627,8 @@ export async function relocateAdmittedRunToTicket(
|
|
|
628
627
|
admitted: AdmittedRoleInvocation,
|
|
629
628
|
authority: DurablePrincipalAuthority,
|
|
630
629
|
heldLease?: { relocate(runDirectory: string): void },
|
|
631
|
-
): Promise<
|
|
632
|
-
if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) return;
|
|
630
|
+
): Promise<{ oldRunDirectory: string; newRunDirectory: string } | undefined> {
|
|
631
|
+
if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) return undefined;
|
|
633
632
|
const oldRunDirectory = admitted.runDirectory;
|
|
634
633
|
const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(oldRunDirectory));
|
|
635
634
|
const target = roleRunPlacement(ledgerHome, {
|
|
@@ -639,7 +638,17 @@ export async function relocateAdmittedRunToTicket(
|
|
|
639
638
|
role: admitted.role,
|
|
640
639
|
});
|
|
641
640
|
ensureRoleRunDirectory(ledgerHome, dirname(target.runDirectory));
|
|
641
|
+
// Host sealing is pure identity projection, but may reject the coordinates.
|
|
642
|
+
// Keep that failure before the filesystem commit point.
|
|
643
|
+
const principal = authority.seal(target);
|
|
644
|
+
|
|
645
|
+
// Rename is the only durable commit. Persisted paths are resolved from typed
|
|
646
|
+
// run identity on read, so online relocation never writes unleased peers or
|
|
647
|
+
// pretends a directory rename plus page rewrites form one transaction.
|
|
642
648
|
await rename(oldRunDirectory, target.runDirectory);
|
|
649
|
+
|
|
650
|
+
// rename moved the open lock inode with the directory. Transfer cleanup
|
|
651
|
+
// ownership immediately after the commit.
|
|
643
652
|
heldLease?.relocate(target.runDirectory);
|
|
644
653
|
|
|
645
654
|
const admittedRecord = admitted as unknown as Record<string, unknown>;
|
|
@@ -664,14 +673,9 @@ export async function relocateAdmittedRunToTicket(
|
|
|
664
673
|
target.runDirectory,
|
|
665
674
|
) as string;
|
|
666
675
|
}
|
|
667
|
-
const principal = authority.seal(target);
|
|
668
676
|
(admitted as { principal: DurablePrincipal }).principal = principal;
|
|
669
677
|
|
|
670
|
-
|
|
671
|
-
pagesDirectory: target.runDirectory,
|
|
672
|
-
oldRunDirectory,
|
|
673
|
-
newRunDirectory: target.runDirectory,
|
|
674
|
-
});
|
|
678
|
+
return { oldRunDirectory, newRunDirectory: target.runDirectory };
|
|
675
679
|
}
|
|
676
680
|
|
|
677
681
|
/**
|
|
@@ -22,11 +22,13 @@ import {
|
|
|
22
22
|
bindAdmittedTicketNumber,
|
|
23
23
|
buildInstructionTransportPrompt,
|
|
24
24
|
freezeAttachmentsIntoRun,
|
|
25
|
+
relocateAdmittedRunToTicket,
|
|
25
26
|
} from "./invocation.ts";
|
|
26
27
|
import { readRecordedSubmissionRows } from "../submission-ledger.ts";
|
|
27
28
|
import { pathContainedIn } from "../activation-ledger-topology.ts";
|
|
28
29
|
import { pickEngineAxis } from "../package-resources/engine-material.ts";
|
|
29
30
|
import { resolveHostAwareSessionAvailability } from "../session-identity.ts";
|
|
31
|
+
import { rewriteRunDirectoryPathValue } from "../role-run-relocation.ts";
|
|
30
32
|
|
|
31
33
|
import type {
|
|
32
34
|
ControlledFailureCause,
|
|
@@ -117,6 +119,34 @@ import {
|
|
|
117
119
|
TurnDispatchedFailure,
|
|
118
120
|
} from "./auto-resume.ts";
|
|
119
121
|
|
|
122
|
+
function projectRelocatedTurnIdentity(
|
|
123
|
+
request: RoleTurnRequest,
|
|
124
|
+
result: { readonly terminal?: TerminalResult },
|
|
125
|
+
admitted: AdmittedRoleInvocation,
|
|
126
|
+
relocation: { readonly oldRunDirectory: string; readonly newRunDirectory: string },
|
|
127
|
+
): void {
|
|
128
|
+
const rewrite = (value: unknown) => rewriteRunDirectoryPathValue(
|
|
129
|
+
value,
|
|
130
|
+
relocation.oldRunDirectory,
|
|
131
|
+
relocation.newRunDirectory,
|
|
132
|
+
);
|
|
133
|
+
const mutableRequest = request as unknown as Record<string, unknown>;
|
|
134
|
+
mutableRequest.runDirectory = admitted.runDirectory;
|
|
135
|
+
mutableRequest.principal = admitted.principal;
|
|
136
|
+
const activation = mutableRequest.activation;
|
|
137
|
+
if (activation !== null && typeof activation === "object" && !Array.isArray(activation)) {
|
|
138
|
+
for (const field of ["taskPath", "packetPath", "prerequisitesPath", "inputPath", "requestManifestPath"] as const) {
|
|
139
|
+
const record = activation as Record<string, unknown>;
|
|
140
|
+
if (field in record) record[field] = rewrite(record[field]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (result.terminal !== undefined) {
|
|
144
|
+
for (const artifact of result.terminal.artifacts) {
|
|
145
|
+
artifact.path = rewrite(artifact.path) as string;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
120
150
|
/** #855: process-cancel settlement never re-enters auto-resume. */
|
|
121
151
|
function withProcessCancelSkipAutoResume<T extends object>(
|
|
122
152
|
result: T,
|
|
@@ -703,6 +733,12 @@ export async function dispatchPostAdmissionTurn<
|
|
|
703
733
|
: (asserted as { ticketNumber: number }).ticketNumber;
|
|
704
734
|
if (ticketNumber !== undefined) await bindAdmittedTicketNumber(admitted, ticketNumber);
|
|
705
735
|
}
|
|
736
|
+
// #863: shared post-admission bind must relocate unbound→ticket in-home
|
|
737
|
+
// before lease release (work-seat self-report and any prior board bind).
|
|
738
|
+
const relocation = await relocateAdmittedRunToTicket(admitted, env.principalAuthority, lease);
|
|
739
|
+
if (relocation !== undefined) {
|
|
740
|
+
projectRelocatedTurnIdentity(request, result, admitted, relocation);
|
|
741
|
+
}
|
|
706
742
|
if (adapters.afterDispatch !== undefined) await adapters.afterDispatch(admitted, lease);
|
|
707
743
|
return result;
|
|
708
744
|
} catch (error) {
|
|
@@ -40,6 +40,10 @@ import {
|
|
|
40
40
|
} from "../diarist-contracts.ts";
|
|
41
41
|
import type { DoctorCaseIdentity } from "../doctor-contracts.ts";
|
|
42
42
|
import type { NotarySourceRunLocator } from "../notary-contracts.ts";
|
|
43
|
+
import {
|
|
44
|
+
rewriteAdmittedRoleRunPage,
|
|
45
|
+
rewriteRunDirectoryPathValue,
|
|
46
|
+
} from "../role-run-relocation.ts";
|
|
43
47
|
import {
|
|
44
48
|
appendEngineSessionMaterial,
|
|
45
49
|
engineSessionMaterialFromOptions,
|
|
@@ -419,15 +423,20 @@ async function readRoleRunStateDisk(
|
|
|
419
423
|
if (typeof record.projectRoot !== "string") return undefined;
|
|
420
424
|
if (typeof record.sessionDirectory !== "string") return undefined;
|
|
421
425
|
if (typeof record.admittedRequestPath !== "string") return undefined;
|
|
422
|
-
const
|
|
426
|
+
const storedRunDirectory =
|
|
423
427
|
typeof record.runDirectory === "string" && record.runDirectory.trim() !== ""
|
|
424
428
|
? record.runDirectory
|
|
425
429
|
: runDirectory;
|
|
430
|
+
const runDir = runDirectory;
|
|
426
431
|
// Principal wire stays uninterpreted — authority owns legacy sessionFile fallback.
|
|
427
432
|
const principalWire: RoleRunPrincipalWire = {
|
|
428
|
-
sessionDirectory:
|
|
433
|
+
sessionDirectory: rewriteRunDirectoryPathValue(
|
|
434
|
+
record.sessionDirectory,
|
|
435
|
+
storedRunDirectory,
|
|
436
|
+
runDirectory,
|
|
437
|
+
) as string,
|
|
429
438
|
...(typeof record.sessionFile === "string"
|
|
430
|
-
? { sessionFile: record.sessionFile }
|
|
439
|
+
? { sessionFile: rewriteRunDirectoryPathValue(record.sessionFile, storedRunDirectory, runDirectory) as string }
|
|
431
440
|
: {}),
|
|
432
441
|
};
|
|
433
442
|
let resumable: TypedHttp429Observation | undefined;
|
|
@@ -458,7 +467,11 @@ async function readRoleRunStateDisk(
|
|
|
458
467
|
bookKey: record.bookKey,
|
|
459
468
|
projectRoot: record.projectRoot,
|
|
460
469
|
runDirectory: runDir,
|
|
461
|
-
admittedRequestPath:
|
|
470
|
+
admittedRequestPath: rewriteRunDirectoryPathValue(
|
|
471
|
+
record.admittedRequestPath,
|
|
472
|
+
storedRunDirectory,
|
|
473
|
+
runDirectory,
|
|
474
|
+
) as string,
|
|
462
475
|
principalWire,
|
|
463
476
|
...(phase === undefined ? {} : { phase }),
|
|
464
477
|
...(resumable === undefined ? {} : { resumable }),
|
|
@@ -1433,6 +1446,14 @@ async function loadResumableRunRecord(
|
|
|
1433
1446
|
);
|
|
1434
1447
|
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
|
1435
1448
|
const record = raw as Record<string, unknown>;
|
|
1449
|
+
const storedRunDirectory =
|
|
1450
|
+
typeof record.runDirectory === "string" && record.runDirectory.trim() !== ""
|
|
1451
|
+
? record.runDirectory
|
|
1452
|
+
: run.runDirectory;
|
|
1453
|
+
rewriteAdmittedRoleRunPage(record, [{
|
|
1454
|
+
oldRunDirectory: storedRunDirectory,
|
|
1455
|
+
newRunDirectory: run.runDirectory,
|
|
1456
|
+
}]);
|
|
1436
1457
|
if (typeof record.instruction === "string") {
|
|
1437
1458
|
instruction = record.instruction;
|
|
1438
1459
|
}
|
|
@@ -1649,6 +1670,23 @@ async function loadResumableRunRecord(
|
|
|
1649
1670
|
} else if (mergedSet.tickets !== undefined) {
|
|
1650
1671
|
courtTicketNumbers = mergedSet.tickets;
|
|
1651
1672
|
}
|
|
1673
|
+
const sourceRunLeaf = basename(sourceRunPath ?? "").split("@");
|
|
1674
|
+
const referencedRunId = sourceRun?.runId ?? sourceRunLeaf[0];
|
|
1675
|
+
const referencedRole = sourceRun?.role ?? sourceRunLeaf[1];
|
|
1676
|
+
if (referencedRunId !== undefined && referencedRunId !== "") {
|
|
1677
|
+
const currentSourceRunDirectory = await findRunDirectoryById(
|
|
1678
|
+
home,
|
|
1679
|
+
referencedRunId,
|
|
1680
|
+
run.bookKey,
|
|
1681
|
+
referencedRole,
|
|
1682
|
+
);
|
|
1683
|
+
if (currentSourceRunDirectory !== undefined) {
|
|
1684
|
+
sourceRunPath = currentSourceRunDirectory;
|
|
1685
|
+
if (sourceRun !== undefined) {
|
|
1686
|
+
sourceRun = { ...sourceRun, runDirectory: currentSourceRunDirectory };
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1652
1690
|
return {
|
|
1653
1691
|
run,
|
|
1654
1692
|
principal,
|