@akagilnc/pi-workflow-roles 0.1.4062 → 0.1.4196
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 +2075 -1088
- package/dist/archivist-record-entry.js +204 -20
- package/dist/archivist-record-topology.js +36 -16
- package/dist/auditor-dossier-tool.js +2 -1
- package/dist/diarist-contracts.js +96 -0
- package/dist/doctor-evidence.js +3 -2
- package/dist/headless-host/production-host.js +2215 -1228
- package/dist/migrate-book-topology.js +278917 -0
- package/dist/navigator-public-session.js +22 -25
- package/dist/notary-source-run.js +7 -4
- package/dist/pi/durable-principal.js +9 -6
- package/dist/public-cli/auto-resume.js +2 -5
- package/dist/public-cli/diarist-run.js +27 -25
- package/dist/public-cli/instruction-seat-run.js +8 -11
- package/dist/public-cli/invocation.js +127 -63
- package/dist/public-cli/main.js +1507 -645
- package/dist/public-cli/option-definitions.js +2 -2
- package/dist/public-cli/post-admission.js +47 -12
- package/dist/public-cli/run-lifecycle.js +206 -58
- package/dist/public-cli/seat-ticket-binding.js +3 -6
- package/dist/public-cli/settlement.js +5 -3
- package/dist/role-run-placement.js +84 -0
- package/dist/role-run-relocation.js +358 -0
- package/dist/run-terminal-artifacts.js +2 -1
- package/dist/run-ticket-number.js +51 -32
- package/dist/sitian-appender.js +11 -26
- package/dist/submission-ledger.js +78 -19
- package/dist/ticket-provenance.js +3 -1
- package/package.json +3 -2
- package/scripts/build-package.mjs +34 -2
- package/souls/countersign.md +2 -2
- package/souls/fixer.md +3 -2
- package/src/analyst-ledger.ts +14 -15
- package/src/archivist-record-entry.ts +285 -38
- package/src/archivist-record-topology.ts +43 -18
- package/src/auditor-dossier-tool.ts +2 -1
- package/src/book-topology-migration-placement.ts +493 -0
- package/src/book-topology-migration.ts +220 -0
- package/src/book-topology-migrators-t11.ts +856 -0
- package/src/book-topology-mixed-volume-migrators.ts +470 -0
- package/src/book-topology-partition-migrators.ts +25 -0
- package/src/book-topology-record-class-migrators.ts +779 -0
- package/src/book-topology-runs-migrator.ts +244 -0
- package/src/diarist-contracts.ts +116 -0
- package/src/diarist-role.ts +6 -0
- package/src/diarist.ts +2 -0
- package/src/doctor-evidence.ts +4 -1
- package/src/doctor-role.ts +1 -1
- package/src/navigator-public-session.ts +33 -30
- package/src/notary-source-run.ts +9 -4
- package/src/pi/durable-principal.ts +9 -13
- package/src/public-cli/auto-resume.ts +2 -6
- package/src/public-cli/countersign-run.ts +182 -32
- package/src/public-cli/diarist-run.ts +34 -26
- package/src/public-cli/instruction-seat-run.ts +7 -11
- package/src/public-cli/invocation.ts +192 -58
- package/src/public-cli/option-definitions.ts +2 -2
- package/src/public-cli/post-admission.ts +73 -13
- package/src/public-cli/run-lifecycle.ts +255 -65
- package/src/public-cli/seat-ticket-binding.ts +3 -6
- package/src/public-cli/settlement.ts +6 -4
- package/src/role-run-placement.ts +128 -0
- package/src/role-run-relocation.ts +491 -0
- package/src/role-runtime.ts +11 -5
- package/src/run-terminal-artifacts.ts +3 -1
- package/src/run-ticket-number.ts +80 -20
- package/src/sitian-appender.ts +25 -28
- package/src/submission-ledger.ts +102 -19
- package/src/ticket-provenance.ts +4 -1
- package/src/ticket-trajectory.ts +22 -35
- package/src/worker-role.ts +4 -4
- package/src/worker-submission-gates.ts +6 -6
|
@@ -3,23 +3,40 @@
|
|
|
3
3
|
* 调用方只声明自己是谁的什么;落点由候簿拓扑算出,签名不含任何落点/路径参数。
|
|
4
4
|
* 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
closeSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
statSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
7
18
|
import { dirname, resolve, join } from "node:path";
|
|
19
|
+
import { StringDecoder } from "node:string_decoder";
|
|
8
20
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
9
21
|
|
|
10
|
-
import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
|
|
11
22
|
import {
|
|
12
23
|
ActivationLedgerError,
|
|
13
|
-
activationBookDirectory,
|
|
14
24
|
ensureRealDirectoryTree,
|
|
25
|
+
errnoCode,
|
|
15
26
|
errorText,
|
|
16
27
|
pathContainedIn,
|
|
17
28
|
physicallyContainedIn,
|
|
18
29
|
resolveActivationLedgerHomeForPath,
|
|
19
30
|
} from "./activation-ledger-topology.ts";
|
|
20
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
NAVIGATOR_RECORD_KIND,
|
|
33
|
+
resolveNavigatorWorkSubjectPlacement,
|
|
34
|
+
} from "./archivist-record-topology.ts";
|
|
21
35
|
|
|
22
|
-
export {
|
|
36
|
+
export {
|
|
37
|
+
NAVIGATOR_RECORD_KIND,
|
|
38
|
+
resolveNavigatorWorkSubjectPlacement,
|
|
39
|
+
} from "./archivist-record-topology.ts";
|
|
23
40
|
|
|
24
41
|
const CURRENT_SESSION_LEDGER = "current-session.json";
|
|
25
42
|
|
|
@@ -58,20 +75,234 @@ function writeCurrentSession(sessionDir: string, sessionFile: string): void {
|
|
|
58
75
|
}
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
/**
|
|
78
|
+
/** True when wx lost to a peer creator — native EEXIST on the write itself. */
|
|
79
|
+
function isCurrentSessionClaimRace(error: unknown): boolean {
|
|
80
|
+
if (!(error instanceof ActivationLedgerError)) return errnoCode(error) === "EEXIST";
|
|
81
|
+
return errnoCode(error.cause) === "EEXIST";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function currentSessionLedgerPath(sessionDir: string): string {
|
|
85
|
+
return join(sessionDir, CURRENT_SESSION_LEDGER);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Same bounds as Pi readSessionHeader — do not invent a second scan ceiling. */
|
|
89
|
+
const SESSION_HEADER_CHUNK_BYTES = 4096;
|
|
90
|
+
const MAX_SESSION_HEADER_SCAN_BYTES = 1024 * 1024;
|
|
91
|
+
|
|
92
|
+
type NavigatorSessionHeader = {
|
|
93
|
+
readonly type: "session";
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly cwd?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Bounded session-header discovery matching Pi readSessionHeader:
|
|
100
|
+
* 4KiB chunks, ≤1MiB total, StringDecoder across reads + EOF flush, skip leading
|
|
101
|
+
* blank lines, first non-blank JSON line only. Accepted session header → value;
|
|
102
|
+
* complete non-session / malformed first line → rejected (non-candidate, stop);
|
|
103
|
+
* no complete line yet → incomplete (keep reading). Oversize-without-header →
|
|
104
|
+
* undefined. Real open/read I/O failures propagate — never washed into non-candidate.
|
|
105
|
+
*/
|
|
106
|
+
type SessionHeaderRead =
|
|
107
|
+
| { readonly kind: "accepted"; readonly header: NavigatorSessionHeader }
|
|
108
|
+
| { readonly kind: "rejected" }
|
|
109
|
+
| { readonly kind: "incomplete" };
|
|
110
|
+
|
|
111
|
+
function readBoundedSessionHeader(filePath: string): NavigatorSessionHeader | undefined {
|
|
112
|
+
const fd = openSync(filePath, "r");
|
|
113
|
+
try {
|
|
114
|
+
const buffer = Buffer.allocUnsafe(SESSION_HEADER_CHUNK_BYTES);
|
|
115
|
+
const decoder = new StringDecoder("utf8");
|
|
116
|
+
let pending = "";
|
|
117
|
+
let scanned = 0;
|
|
118
|
+
const consume = (chunk: string): SessionHeaderRead => {
|
|
119
|
+
pending += chunk;
|
|
120
|
+
let lineStart = 0;
|
|
121
|
+
let newlineIndex = pending.indexOf("\n", lineStart);
|
|
122
|
+
while (newlineIndex !== -1) {
|
|
123
|
+
const line = pending.slice(lineStart, newlineIndex);
|
|
124
|
+
lineStart = newlineIndex + 1;
|
|
125
|
+
newlineIndex = pending.indexOf("\n", lineStart);
|
|
126
|
+
if (line.trim() === "") continue;
|
|
127
|
+
// First non-blank complete line decides — never scan past a rejection.
|
|
128
|
+
pending = pending.slice(lineStart);
|
|
129
|
+
const header = parseSessionHeaderLine(line);
|
|
130
|
+
return header === undefined
|
|
131
|
+
? { kind: "rejected" }
|
|
132
|
+
: { kind: "accepted", header };
|
|
133
|
+
}
|
|
134
|
+
pending = pending.slice(lineStart);
|
|
135
|
+
return { kind: "incomplete" };
|
|
136
|
+
};
|
|
137
|
+
const finishPending = (tail: string): NavigatorSessionHeader | undefined => {
|
|
138
|
+
const result = consume(tail);
|
|
139
|
+
if (result.kind === "accepted") return result.header;
|
|
140
|
+
if (result.kind === "rejected") return undefined;
|
|
141
|
+
// EOF with residual non-blank bytes and no newline: treat as one final line.
|
|
142
|
+
if (pending.trim() === "") return undefined;
|
|
143
|
+
const header = parseSessionHeaderLine(pending);
|
|
144
|
+
pending = "";
|
|
145
|
+
return header;
|
|
146
|
+
};
|
|
147
|
+
while (scanned < MAX_SESSION_HEADER_SCAN_BYTES) {
|
|
148
|
+
const toRead = Math.min(buffer.length, MAX_SESSION_HEADER_SCAN_BYTES - scanned);
|
|
149
|
+
const bytesRead = readSync(fd, buffer, 0, toRead, null);
|
|
150
|
+
if (bytesRead === 0) {
|
|
151
|
+
return finishPending(decoder.end());
|
|
152
|
+
}
|
|
153
|
+
scanned += bytesRead;
|
|
154
|
+
const found = consume(decoder.write(buffer.subarray(0, bytesRead)));
|
|
155
|
+
if (found.kind === "accepted") return found.header;
|
|
156
|
+
if (found.kind === "rejected") return undefined;
|
|
157
|
+
}
|
|
158
|
+
// Exactly at the scan limit with a complete final line is still accepted;
|
|
159
|
+
// any further unread byte means the header never arrived in bounds.
|
|
160
|
+
const probe = Buffer.allocUnsafe(1);
|
|
161
|
+
if (readSync(fd, probe, 0, 1, null) === 0) {
|
|
162
|
+
return finishPending(decoder.end());
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
} finally {
|
|
166
|
+
closeSync(fd);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseSessionHeaderLine(line: string): NavigatorSessionHeader | undefined {
|
|
171
|
+
if (line.trim() === "") return undefined;
|
|
172
|
+
let parsed: unknown;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(line);
|
|
175
|
+
} catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
typeof parsed !== "object"
|
|
180
|
+
|| parsed === null
|
|
181
|
+
|| (parsed as { type?: unknown }).type !== "session"
|
|
182
|
+
|| typeof (parsed as { id?: unknown }).id !== "string"
|
|
183
|
+
|| (parsed as { id: string }).id.length === 0
|
|
184
|
+
) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const cwd = (parsed as { cwd?: unknown }).cwd;
|
|
188
|
+
return {
|
|
189
|
+
type: "session",
|
|
190
|
+
id: (parsed as { id: string }).id,
|
|
191
|
+
...(typeof cwd === "string" ? { cwd } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Pre-sidecar navigator continuation (historical findMostRecentSession selection):
|
|
197
|
+
* bounded valid session header, header.cwd resolves equal to the calling cwd,
|
|
198
|
+
* newest file mtime wins. Nest-local only — not a generic kind fallback.
|
|
199
|
+
* Returns undefined when no candidate matches (caller mints fresh, old null path).
|
|
200
|
+
* Directory/stat/read I/O failures propagate as ActivationLedgerError (failure honesty);
|
|
201
|
+
* format non-candidates stay skippable.
|
|
202
|
+
*/
|
|
203
|
+
function mostRecentNavigatorSessionFile(sessionDir: string, cwd: string): string | undefined {
|
|
204
|
+
const absoluteSessionDir = resolve(sessionDir);
|
|
205
|
+
const absoluteCwd = resolve(cwd);
|
|
206
|
+
let names: string[];
|
|
207
|
+
try {
|
|
208
|
+
names = readdirSync(absoluteSessionDir);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw new ActivationLedgerError(
|
|
211
|
+
`archivist navigator nest is not readable (${sessionDir}): ${errorText(error)}`,
|
|
212
|
+
{ cause: error },
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const matched: { path: string; mtimeMs: number }[] = [];
|
|
216
|
+
for (const name of names) {
|
|
217
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
218
|
+
const filePath = join(absoluteSessionDir, name);
|
|
219
|
+
let st;
|
|
220
|
+
try {
|
|
221
|
+
st = statSync(filePath);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new ActivationLedgerError(
|
|
224
|
+
`archivist navigator session file is not stat-able (${filePath}): ${errorText(error)}`,
|
|
225
|
+
{ cause: error },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (!st.isFile()) continue;
|
|
229
|
+
let header: NavigatorSessionHeader | undefined;
|
|
230
|
+
try {
|
|
231
|
+
header = readBoundedSessionHeader(filePath);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw new ActivationLedgerError(
|
|
234
|
+
`archivist navigator session header is not readable (${filePath}): ${errorText(error)}`,
|
|
235
|
+
{ cause: error },
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (header === undefined) continue;
|
|
239
|
+
if (typeof header.cwd !== "string" || header.cwd.length === 0) continue;
|
|
240
|
+
if (resolve(header.cwd) !== absoluteCwd) continue;
|
|
241
|
+
matched.push({ path: filePath, mtimeMs: st.mtimeMs });
|
|
242
|
+
}
|
|
243
|
+
if (matched.length === 0) return undefined;
|
|
244
|
+
matched.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
245
|
+
return matched[0]!.path;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Resume target for an existing navigator work-subject nest.
|
|
250
|
+
* Prefer the AK current-session sidecar. When absent (legal pre-sidecar / migrated
|
|
251
|
+
* shape), adopt by historical cwd + mtime recency and write the sidecar once.
|
|
252
|
+
* No match → undefined so the sole entry mints fresh (old null path).
|
|
253
|
+
* Ordinary kinds and worker-submission-gate do not use this path.
|
|
254
|
+
*/
|
|
255
|
+
function resolveNavigatorNestContinuation(
|
|
256
|
+
sessionDir: string,
|
|
257
|
+
cwd: string,
|
|
258
|
+
): string | undefined {
|
|
259
|
+
const ledgerPath = currentSessionLedgerPath(sessionDir);
|
|
260
|
+
if (existsSync(ledgerPath)) {
|
|
261
|
+
const recentFile = readCurrentSession(sessionDir);
|
|
262
|
+
assertRecentFinalFileUnderSessionDir(sessionDir, recentFile);
|
|
263
|
+
return recentFile;
|
|
264
|
+
}
|
|
265
|
+
const adopted = mostRecentNavigatorSessionFile(sessionDir, cwd);
|
|
266
|
+
if (adopted === undefined) return undefined;
|
|
267
|
+
assertRecentFinalFileUnderSessionDir(sessionDir, adopted);
|
|
268
|
+
try {
|
|
269
|
+
writeCurrentSession(sessionDir, adopted);
|
|
270
|
+
return adopted;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// Concurrent first adopter lost wx — read the winner through the same
|
|
273
|
+
// containment gate. Other write failures keep their typed cause.
|
|
274
|
+
if (isCurrentSessionClaimRace(error)) {
|
|
275
|
+
const recentFile = readCurrentSession(sessionDir);
|
|
276
|
+
assertRecentFinalFileUnderSessionDir(sessionDir, recentFile);
|
|
277
|
+
return recentFile;
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Durable parent session surface that links and nests the record. */
|
|
62
284
|
export type RecordSessionParent = {
|
|
63
285
|
getSessionFile(): string | undefined;
|
|
64
286
|
};
|
|
65
287
|
|
|
66
288
|
export type CreateRecordSessionOptions = {
|
|
67
|
-
/** Role working directory
|
|
289
|
+
/** Role working directory passed to the record session; not a placement input. */
|
|
68
290
|
readonly cwd: string;
|
|
69
291
|
/** What kind of record this is (e.g. "auditor-roles"). Single path segment; not a destination path. */
|
|
70
292
|
readonly kind: string;
|
|
71
|
-
/**
|
|
293
|
+
/** Parent session — nesting authority for ordinary kinds; optional parentSession link for navigator. */
|
|
72
294
|
readonly parent?: RecordSessionParent;
|
|
73
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* Durable work-subject intent. For navigator only, selects the book-top
|
|
297
|
+
* `navigator/<work-subject>` nest (dossier-topology / #852); does not restore
|
|
298
|
+
* generic kind partitions.
|
|
299
|
+
*/
|
|
74
300
|
readonly subject?: string;
|
|
301
|
+
/**
|
|
302
|
+
* Process home when no parent path can derive the ledger home. Not a record
|
|
303
|
+
* destination — same identity surface as resolveActivationLedgerHome(home).
|
|
304
|
+
*/
|
|
305
|
+
readonly home?: string;
|
|
75
306
|
};
|
|
76
307
|
|
|
77
308
|
/** Authorized no-subject kind that may resume the most recent same-nest peer (ADR 0066). Sole string true source for gate resume identity. */
|
|
@@ -122,10 +353,10 @@ function assertRecentFinalFileUnderSessionDir(
|
|
|
122
353
|
}
|
|
123
354
|
}
|
|
124
355
|
|
|
125
|
-
/** Open result including the sole
|
|
356
|
+
/** Open result including the sole continuation fact. */
|
|
126
357
|
export type RecordSessionOpen = {
|
|
127
358
|
readonly session: SessionManager;
|
|
128
|
-
/** True
|
|
359
|
+
/** True when an existing navigator work-subject or worker-submission-gate volume was reopened. */
|
|
129
360
|
readonly resumed: boolean;
|
|
130
361
|
};
|
|
131
362
|
|
|
@@ -136,8 +367,8 @@ export type RecordSessionOpen = {
|
|
|
136
367
|
* identity is checked once before SessionManager.open (directory walk cannot see a
|
|
137
368
|
* trailing .jsonl symlink). New principals mint under the already-validated sessionDir
|
|
138
369
|
* via destination-free SessionManager.create — no derived postcondition.
|
|
139
|
-
* Resume via the AK-owned current-session ledger is limited to subject
|
|
140
|
-
* and the authorized worker-submission-gate durable path (ADR 0066).
|
|
370
|
+
* Resume via the AK-owned current-session ledger is limited to navigator work-subject
|
|
371
|
+
* nests and the authorized worker-submission-gate durable path (ADR 0066 / #852).
|
|
141
372
|
* Other ordinary no-subject children (auditor-roles, evidence-children, …) always mint fresh.
|
|
142
373
|
* New persisted principals materialize their deferred session header before return so
|
|
143
374
|
* custom-entry-only writers do not need a parallel delayed-header helper.
|
|
@@ -147,52 +378,68 @@ export type RecordSessionOpen = {
|
|
|
147
378
|
export function createRecordSessionOpen(options: CreateRecordSessionOptions): RecordSessionOpen {
|
|
148
379
|
const cwd = options.cwd;
|
|
149
380
|
const parentFile = options.parent?.getSessionFile();
|
|
150
|
-
// Path → ledger home is owned by topology (explicit env.home nests via parent path).
|
|
151
|
-
const ledgerHome = resolveActivationLedgerHomeForPath(parentFile);
|
|
152
381
|
|
|
153
382
|
let sessionDir: string;
|
|
154
383
|
let parentSession: string | undefined;
|
|
384
|
+
let mayResumeSameNest: boolean;
|
|
385
|
+
let ledgerHome: string;
|
|
155
386
|
|
|
156
|
-
|
|
157
|
-
|
|
387
|
+
// #852 sole book-top exception: navigator/<work-subject> via one topology authority.
|
|
388
|
+
// Holds for no parent / unmaterialized parent / materialized parent — never silent in-memory.
|
|
389
|
+
if (options.kind === NAVIGATOR_RECORD_KIND && options.subject !== undefined) {
|
|
390
|
+
const placement = resolveNavigatorWorkSubjectPlacement({
|
|
158
391
|
cwd,
|
|
159
|
-
kind: options.kind,
|
|
160
392
|
subject: options.subject,
|
|
161
393
|
...(parentFile === undefined || parentFile.length === 0
|
|
162
394
|
? {}
|
|
163
395
|
: { parentSessionFile: parentFile }),
|
|
396
|
+
...(options.home === undefined ? {} : { home: options.home }),
|
|
164
397
|
});
|
|
398
|
+
sessionDir = placement.sessionDir;
|
|
399
|
+
ledgerHome = placement.ledgerHome;
|
|
165
400
|
parentSession = parentFile && parentFile.length > 0 ? parentFile : undefined;
|
|
401
|
+
mayResumeSameNest = true;
|
|
166
402
|
} else if (parentFile === undefined || parentFile.length === 0) {
|
|
167
|
-
|
|
168
|
-
|
|
403
|
+
if (options.subject === undefined) {
|
|
404
|
+
// No durable principal requested: preserve prior in-memory child behavior.
|
|
405
|
+
return { session: SessionManager.inMemory(cwd), resumed: false };
|
|
406
|
+
}
|
|
407
|
+
throw new Error("Durable record ownership requires a parent session inside the ledger home");
|
|
169
408
|
} else {
|
|
409
|
+
ledgerHome = resolveActivationLedgerHomeForPath(parentFile);
|
|
170
410
|
const parentResolved = resolve(parentFile);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
: join(activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)), options.kind);
|
|
411
|
+
if (!physicallyContainedIn(ledgerHome, parentResolved)) {
|
|
412
|
+
throw new Error("Durable record ownership requires a parent session inside the ledger home");
|
|
413
|
+
}
|
|
414
|
+
// Ordinary kinds: durable parent's file is the sole nesting authority. A divergent
|
|
415
|
+
// SessionManager directory must not create a second placement route.
|
|
416
|
+
// Do not restore generic bookDir/<kind> fallback for foreign/unrooted parents.
|
|
417
|
+
sessionDir = join(dirname(parentResolved), options.kind);
|
|
179
418
|
parentSession = parentFile;
|
|
419
|
+
mayResumeSameNest = options.kind === WORKER_SUBMISSION_GATE_KIND;
|
|
180
420
|
}
|
|
181
421
|
|
|
182
422
|
const nestAlreadyExists = existsSync(sessionDir);
|
|
183
423
|
// Directory-chain ownership: containment + physical components (no parallel assert).
|
|
184
424
|
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
185
|
-
// Subject-keyed nests continue by subject digest; worker-submission-gate is the
|
|
186
|
-
// sole authorized no-subject same-nest continuation. All other kinds mint fresh.
|
|
187
|
-
const mayResumeSameNest =
|
|
188
|
-
options.subject !== undefined || options.kind === WORKER_SUBMISSION_GATE_KIND;
|
|
189
425
|
if (mayResumeSameNest && nestAlreadyExists) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
426
|
+
if (options.kind === NAVIGATOR_RECORD_KIND) {
|
|
427
|
+
const recentFile = resolveNavigatorNestContinuation(sessionDir, cwd);
|
|
428
|
+
if (recentFile !== undefined) {
|
|
429
|
+
return {
|
|
430
|
+
session: SessionManager.open(recentFile, sessionDir, cwd),
|
|
431
|
+
resumed: true,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
// No sidecar and no cwd-matching session — mint fresh in the existing nest.
|
|
435
|
+
} else {
|
|
436
|
+
const recentFile = readCurrentSession(sessionDir);
|
|
437
|
+
assertRecentFinalFileUnderSessionDir(sessionDir, recentFile);
|
|
438
|
+
return {
|
|
439
|
+
session: SessionManager.open(recentFile, sessionDir, cwd),
|
|
440
|
+
resumed: true,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
196
443
|
}
|
|
197
444
|
|
|
198
445
|
const session = SessionManager.create(
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure archivist record placement topology (ADR 0048 / 0065).
|
|
2
|
+
* Pure archivist record placement topology (ADR 0048 / 0065 / dossier-topology).
|
|
3
3
|
* No SessionManager / pi-coding-agent — cold public-bin and ledger discovery
|
|
4
4
|
* may import this without pulling open-session runtime into the graph.
|
|
5
|
+
*
|
|
6
|
+
* Navigator work-subject is the sole book-top subject partition (#852):
|
|
7
|
+
* books/<book>/navigator/<work-subject-digest>/. Callers must not re-hash
|
|
8
|
+
* subject or re-join book/navigator/digest themselves.
|
|
5
9
|
*/
|
|
6
10
|
import { createHash } from "node:crypto";
|
|
7
11
|
import { join } from "node:path";
|
|
@@ -10,30 +14,51 @@ import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
|
|
|
10
14
|
import {
|
|
11
15
|
activationBookDirectory,
|
|
12
16
|
resolveActivationLedgerHome,
|
|
13
|
-
|
|
17
|
+
tryHomeFromAkRolesPath,
|
|
14
18
|
} from "./activation-ledger-topology.ts";
|
|
15
19
|
|
|
20
|
+
/** Book-top partition for cross-ticket navigator route-memory (dossier-topology). */
|
|
21
|
+
export const NAVIGATOR_RECORD_KIND = "navigator" as const;
|
|
22
|
+
|
|
23
|
+
/** One resolved navigator work-subject placement — ledger root and nest together. */
|
|
24
|
+
export type NavigatorWorkSubjectPlacement = {
|
|
25
|
+
readonly ledgerHome: string;
|
|
26
|
+
readonly sessionDir: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
16
29
|
/**
|
|
17
|
-
* Sole subject
|
|
18
|
-
*
|
|
30
|
+
* Sole navigator work-subject placement under the ledger book.
|
|
31
|
+
* Returns ledgerHome and nest once so createRecordSessionOpen does not re-branch
|
|
32
|
+
* parent/home. Work-subject key shape is sha256(subject) hex truncated to 32
|
|
33
|
+
* (#852 out of scope — shape unchanged).
|
|
34
|
+
*
|
|
35
|
+
* parentSessionFile contributes home only when tryHomeFromAkRolesPath succeeds;
|
|
36
|
+
* otherwise the explicit home (from HostContext.runDirectory) is kept. Never
|
|
37
|
+
* invents a second home via passwd/env fallback from a foreign parent path.
|
|
19
38
|
*/
|
|
20
|
-
export function
|
|
39
|
+
export function resolveNavigatorWorkSubjectPlacement(input: {
|
|
21
40
|
readonly cwd: string;
|
|
22
|
-
readonly kind: string;
|
|
23
41
|
readonly subject: string;
|
|
24
|
-
/** Parent session file — home path-derives from it
|
|
42
|
+
/** Parent session file — home path-derives from it only under .ak-roles. */
|
|
25
43
|
readonly parentSessionFile?: string;
|
|
26
|
-
/** Explicit process home when no parent
|
|
44
|
+
/** Explicit process home when no ledger parent path is available. */
|
|
27
45
|
readonly home?: string;
|
|
28
|
-
}):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
}): NavigatorWorkSubjectPlacement {
|
|
47
|
+
let ledgerHome: string | undefined;
|
|
48
|
+
if (input.parentSessionFile !== undefined && input.parentSessionFile.length > 0) {
|
|
49
|
+
const fromParent = tryHomeFromAkRolesPath(input.parentSessionFile);
|
|
50
|
+
if (fromParent !== undefined && fromParent.length > 0) {
|
|
51
|
+
ledgerHome = resolveActivationLedgerHome(fromParent);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
ledgerHome ??= resolveActivationLedgerHome(input.home);
|
|
33
55
|
const digest = createHash("sha256").update(input.subject).digest("hex").slice(0, 32);
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
return {
|
|
57
|
+
ledgerHome,
|
|
58
|
+
sessionDir: join(
|
|
59
|
+
activationBookDirectory(ledgerHome, resolveBookKeyFromGit(input.cwd)),
|
|
60
|
+
NAVIGATOR_RECORD_KIND,
|
|
61
|
+
digest,
|
|
62
|
+
),
|
|
63
|
+
};
|
|
39
64
|
}
|
|
@@ -2,6 +2,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { HostContext } from "./host-contracts.ts";
|
|
5
|
+
import { roleRunArtifactsDirectory } from "./role-run-placement.ts";
|
|
5
6
|
import { Type } from "typebox";
|
|
6
7
|
|
|
7
8
|
export const AUDITOR_DOSSIER_TOOL_NAME = "ak_get_run_dossier" as const;
|
|
@@ -45,7 +46,7 @@ export function createAuditorDossierTool(
|
|
|
45
46
|
admittedRequest: join(runDirectory, "admitted-request.json"),
|
|
46
47
|
parentSessionCandidate: join(runDirectory, "session", "session.jsonl"),
|
|
47
48
|
attachments: join(runDirectory, "attachments"),
|
|
48
|
-
artifacts:
|
|
49
|
+
artifacts: roleRunArtifactsDirectory(runDirectory),
|
|
49
50
|
};
|
|
50
51
|
return {
|
|
51
52
|
content: [{ type: "text", text: JSON.stringify(details) }],
|