@akagilnc/pi-workflow-roles 0.1.3452 → 0.1.3471
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/CLAUDE.md +2 -0
- package/dist/evidence-child-executor.js +2 -11
- package/dist/grok/production-host.js +196 -205
- package/dist/pi/in-process-session.js +4 -0
- package/dist/public-cli/main.js +998 -920
- package/package.json +1 -1
- package/scripts/build-package.mjs +1 -3
- package/src/analyst-gate-cycles-read.ts +3 -21
- package/src/analyst-ledger.ts +11 -42
- package/src/archivist-record-entry.ts +5 -6
- package/src/evidence-child-executor.ts +5 -15
- package/src/grok/production-host.ts +3 -32
- package/src/grok/role-envelope.ts +10 -4
- package/src/host-contracts.ts +4 -11
- package/src/host-transition-prior-native.ts +3 -7
- package/src/ledger-session-read.ts +1 -17
- package/src/pi/in-process-session.ts +6 -2
- package/src/pi/role-turn-host.ts +9 -0
- package/src/public-cli/collector-run.ts +4 -4
- package/src/public-cli/countersign-run.ts +90 -20
- package/src/public-cli/doctor-run.ts +4 -4
- package/src/public-cli/gleaner-left-run.ts +9 -6
- package/src/public-cli/inspector-run.ts +95 -32
- package/src/public-cli/instruction-seat-run.ts +7 -6
- package/src/public-cli/invocation.ts +14 -8
- package/src/public-cli/judge-run.ts +4 -4
- package/src/public-cli/notary-run.ts +91 -30
- package/src/public-cli/post-admission.ts +424 -360
- package/src/public-cli/run-lifecycle.ts +205 -0
- package/src/public-cli/seat-ticket-binding.ts +136 -16
- package/src/public-cli/settlement.ts +74 -52
- package/src/run-ticket-number.ts +53 -0
- package/src/submission-ledger.ts +86 -18
- package/dist/ledger-session-read.js +0 -241
- package/dist/ticket-seat-memory.js +0 -244
- package/src/ticket-seat-memory.ts +0 -454
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #636 ticket+seat logical memory principal for officer seats.
|
|
3
|
-
* Reuses archivist subject-keyed createRecordSession resume (navigator path);
|
|
4
|
-
* no parallel continuation machine, no length/round thresholds.
|
|
5
|
-
*
|
|
6
|
-
* Pure read/topology stays free of SessionManager so public-bin cold paths
|
|
7
|
-
* (analyst/settlement/invocation) cannot fold pi-coding-agent into main.js.
|
|
8
|
-
* Open-session loads createRecordSessionOpen via runtime-constructed import.
|
|
9
|
-
*/
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
11
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
import { pathToFileURL } from "node:url";
|
|
14
|
-
|
|
15
|
-
import { homeFromRunDirectory } from "./activation-ledger-topology.ts";
|
|
16
|
-
import { subjectKeyedRecordDirectory } from "./archivist-record-topology.ts";
|
|
17
|
-
import type {
|
|
18
|
-
DurablePrincipal,
|
|
19
|
-
DurablePrincipalAuthority,
|
|
20
|
-
DurablePrincipalCoordinates,
|
|
21
|
-
} from "./host-contracts.ts";
|
|
22
|
-
import type { LedgerSessionRow } from "./ledger-session-read.ts";
|
|
23
|
-
import { resolvePackageRootDir } from "./session-opening-materials.ts";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Per-run binding marker on continuous ticket-seat main volumes (#636 D).
|
|
27
|
-
* Analyst slices frame/model/tool facts to the interval owned by this marker
|
|
28
|
-
* via the shared binding-interval helper — same algorithm as gate/settlement.
|
|
29
|
-
*/
|
|
30
|
-
export const TICKET_SEAT_RUN_BINDING_ENTRY_TYPE = "ak_ticket_seat_run_binding" as const;
|
|
31
|
-
|
|
32
|
-
export type TicketSeatRunBinding = {
|
|
33
|
-
readonly version: 1;
|
|
34
|
-
readonly runId: string;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export function isTicketSeatRunBindingRow(row: LedgerSessionRow): boolean {
|
|
38
|
-
return row.type === "custom" && row.customType === TICKET_SEAT_RUN_BINDING_ENTRY_TYPE;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Non-empty runId from a ticket-seat run binding row; otherwise undefined. */
|
|
42
|
-
export function ticketSeatRunBindingRunId(row: LedgerSessionRow): string | undefined {
|
|
43
|
-
if (!isTicketSeatRunBindingRow(row)) return undefined;
|
|
44
|
-
if (row.data === null || typeof row.data !== "object" || Array.isArray(row.data)) {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
const runId = (row.data as { runId?: unknown }).runId;
|
|
48
|
-
return typeof runId === "string" && runId.length > 0 ? runId : undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
type CreateRecordSessionOpen = typeof import("./archivist-record-entry.ts").createRecordSessionOpen;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Resolve createRecordSessionOpen without a static graph edge into the public
|
|
55
|
-
* CLI bundle. Reuses sole package-root walk + built/source two-path probe
|
|
56
|
-
* (load-production-grok-host / #580).
|
|
57
|
-
*/
|
|
58
|
-
async function loadCreateRecordSessionOpen(): Promise<CreateRecordSessionOpen> {
|
|
59
|
-
const packageRoot = resolvePackageRootDir(import.meta.url);
|
|
60
|
-
const built = join(packageRoot, "dist/archivist-record-entry.js");
|
|
61
|
-
const source = join(packageRoot, "src/archivist-record-entry.ts");
|
|
62
|
-
const target = existsSync(built) ? built : source;
|
|
63
|
-
const mod = (await import(pathToFileURL(target).href)) as {
|
|
64
|
-
createRecordSessionOpen: CreateRecordSessionOpen;
|
|
65
|
-
};
|
|
66
|
-
return mod.createRecordSessionOpen;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Record kind for officer ticket-seat memory nests (subject-keyed resume). */
|
|
70
|
-
export const TICKET_SEAT_MEMORY_KIND = "auditor-roles" as const;
|
|
71
|
-
|
|
72
|
-
/** Seats that continue a ticket-keyed memory principal (#636). */
|
|
73
|
-
export const TICKET_SEAT_MEMORY_SEATS = ["inspector", "notary", "auditor"] as const;
|
|
74
|
-
|
|
75
|
-
export type TicketSeatMemorySeat = (typeof TICKET_SEAT_MEMORY_SEATS)[number];
|
|
76
|
-
|
|
77
|
-
export function isTicketSeatMemorySeat(value: string): value is TicketSeatMemorySeat {
|
|
78
|
-
return (TICKET_SEAT_MEMORY_SEATS as readonly string[]).includes(value);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Explicit ticket+seat binding fact for memory side effects (#636).
|
|
83
|
-
* Directory-outside-run guessing is not a business identity.
|
|
84
|
-
*/
|
|
85
|
-
export function isTicketSeatMemoryBound(input: {
|
|
86
|
-
readonly role: string;
|
|
87
|
-
readonly ticketNumber?: number;
|
|
88
|
-
}): input is { readonly role: TicketSeatMemorySeat; readonly ticketNumber: number } {
|
|
89
|
-
return (
|
|
90
|
-
isTicketSeatMemorySeat(input.role) &&
|
|
91
|
-
typeof input.ticketNumber === "number" &&
|
|
92
|
-
Number.isSafeInteger(input.ticketNumber) &&
|
|
93
|
-
input.ticketNumber >= 1
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function isEnoent(error: unknown): boolean {
|
|
98
|
-
return (
|
|
99
|
-
error instanceof Error &&
|
|
100
|
-
"code" in error &&
|
|
101
|
-
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Logical memory subject = ticket number + seat.
|
|
107
|
-
* Digest placement is owned by createRecordSession; this string is the sole subject key.
|
|
108
|
-
* Seat is the closed TicketSeatMemorySeat type — callers narrow before entry.
|
|
109
|
-
*/
|
|
110
|
-
export function ticketSeatMemorySubject(
|
|
111
|
-
ticketNumber: number,
|
|
112
|
-
seat: TicketSeatMemorySeat,
|
|
113
|
-
): string {
|
|
114
|
-
if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`ticket-seat memory subject requires a positive ticket number, got ${String(ticketNumber)}`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
return `${ticketNumber}:${seat}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Book-relative session directory via sole archivist subject topology. */
|
|
123
|
-
export function ticketSeatMemorySessionDirectory(input: {
|
|
124
|
-
readonly ticketNumber: number;
|
|
125
|
-
readonly seat: TicketSeatMemorySeat;
|
|
126
|
-
readonly cwd: string;
|
|
127
|
-
readonly home?: string;
|
|
128
|
-
readonly parentSessionFile?: string;
|
|
129
|
-
}): string {
|
|
130
|
-
return subjectKeyedRecordDirectory({
|
|
131
|
-
cwd: input.cwd,
|
|
132
|
-
kind: TICKET_SEAT_MEMORY_KIND,
|
|
133
|
-
subject: ticketSeatMemorySubject(input.ticketNumber, input.seat),
|
|
134
|
-
...(input.parentSessionFile === undefined
|
|
135
|
-
? {}
|
|
136
|
-
: { parentSessionFile: input.parentSessionFile }),
|
|
137
|
-
...(input.home === undefined ? {} : { home: input.home }),
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
type RunAdmittedPageFacts = {
|
|
142
|
-
readonly ticketNumber?: number;
|
|
143
|
-
readonly projectRoot?: string;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* One read of admitted-request.json facts used by ticket-seat discovery.
|
|
148
|
-
* Missing page (ENOENT) → empty facts; damage / non-ENOENT IO propagate.
|
|
149
|
-
*/
|
|
150
|
-
async function readRunAdmittedPageFacts(
|
|
151
|
-
runDirectory: string,
|
|
152
|
-
): Promise<RunAdmittedPageFacts> {
|
|
153
|
-
try {
|
|
154
|
-
const raw = JSON.parse(
|
|
155
|
-
await readFile(join(runDirectory, "admitted-request.json"), "utf8"),
|
|
156
|
-
) as unknown;
|
|
157
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
158
|
-
return {};
|
|
159
|
-
}
|
|
160
|
-
const record = raw as Record<string, unknown>;
|
|
161
|
-
const ticketNumber = record.ticketNumber;
|
|
162
|
-
const projectRoot = record.projectRoot;
|
|
163
|
-
return {
|
|
164
|
-
...(typeof ticketNumber === "number" &&
|
|
165
|
-
Number.isSafeInteger(ticketNumber) &&
|
|
166
|
-
ticketNumber >= 1
|
|
167
|
-
? { ticketNumber }
|
|
168
|
-
: {}),
|
|
169
|
-
...(typeof projectRoot === "string" ? { projectRoot } : {}),
|
|
170
|
-
};
|
|
171
|
-
} catch (error) {
|
|
172
|
-
if (isEnoent(error)) return {};
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Ticket number from invocation.json only (admitted page already consulted). */
|
|
178
|
-
async function readInvocationTicketNumber(
|
|
179
|
-
runDirectory: string,
|
|
180
|
-
): Promise<number | undefined> {
|
|
181
|
-
try {
|
|
182
|
-
const raw = JSON.parse(
|
|
183
|
-
await readFile(join(runDirectory, "invocation.json"), "utf8"),
|
|
184
|
-
) as unknown;
|
|
185
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
const ticketNumber = (raw as Record<string, unknown>).ticketNumber;
|
|
189
|
-
if (
|
|
190
|
-
typeof ticketNumber === "number" &&
|
|
191
|
-
Number.isSafeInteger(ticketNumber) &&
|
|
192
|
-
ticketNumber >= 1
|
|
193
|
-
) {
|
|
194
|
-
return ticketNumber;
|
|
195
|
-
}
|
|
196
|
-
} catch (error) {
|
|
197
|
-
if (isEnoent(error)) return undefined;
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
200
|
-
return undefined;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Sole reader of ticketNumber from a retained run's admitted-request.json,
|
|
205
|
-
* falling back to invocation.json. Same integer rules as public-cli source-run
|
|
206
|
-
* inheritance (#635). Missing page (ENOENT) → try next / undefined; damage and
|
|
207
|
-
* non-ENOENT IO failures propagate (failure honesty).
|
|
208
|
-
*/
|
|
209
|
-
export async function readRunTicketNumber(
|
|
210
|
-
runDirectory: string,
|
|
211
|
-
): Promise<number | undefined> {
|
|
212
|
-
const admitted = await readRunAdmittedPageFacts(runDirectory);
|
|
213
|
-
if (admitted.ticketNumber !== undefined) return admitted.ticketNumber;
|
|
214
|
-
return readInvocationTicketNumber(runDirectory);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Sole discovery of ticket-seat memory nest directories for a retained run.
|
|
219
|
-
* Reads ticket + projectRoot once from the admitted page (ticket falls back to
|
|
220
|
-
* invocation only); home path-derives from the run.
|
|
221
|
-
* Missing ticket / projectRoot / home topology → [] (callers keep legacy parent-nest lookup).
|
|
222
|
-
* Damaged pages and non-ENOENT IO failures propagate (failure honesty).
|
|
223
|
-
*/
|
|
224
|
-
export async function resolveTicketSeatMemoryNestDirectories(input: {
|
|
225
|
-
readonly runDirectory: string;
|
|
226
|
-
readonly seats: readonly TicketSeatMemorySeat[];
|
|
227
|
-
}): Promise<readonly string[]> {
|
|
228
|
-
if (input.seats.length === 0) return [];
|
|
229
|
-
|
|
230
|
-
// One admitted-request read supplies both ticket and projectRoot; ticket may
|
|
231
|
-
// still fall back to invocation.json without re-opening the admitted page.
|
|
232
|
-
const admitted = await readRunAdmittedPageFacts(input.runDirectory);
|
|
233
|
-
const ticketNumber =
|
|
234
|
-
admitted.ticketNumber ?? (await readInvocationTicketNumber(input.runDirectory));
|
|
235
|
-
if (ticketNumber === undefined) return [];
|
|
236
|
-
|
|
237
|
-
let home: string;
|
|
238
|
-
try {
|
|
239
|
-
home = homeFromRunDirectory(input.runDirectory);
|
|
240
|
-
} catch {
|
|
241
|
-
// Path not under package .ak-roles topology — no ticket-seat nest to discover.
|
|
242
|
-
return [];
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const projectRoot = admitted.projectRoot;
|
|
246
|
-
if (projectRoot === undefined) return [];
|
|
247
|
-
|
|
248
|
-
return input.seats.map((seat) =>
|
|
249
|
-
ticketSeatMemorySessionDirectory({
|
|
250
|
-
ticketNumber,
|
|
251
|
-
seat,
|
|
252
|
-
cwd: projectRoot,
|
|
253
|
-
home,
|
|
254
|
-
}),
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Open-or-create the ticket+seat memory principal via createRecordSession subject
|
|
260
|
-
* resume (navigator path). Returns coordinates for the durable principal codec.
|
|
261
|
-
*
|
|
262
|
-
* `ledgerAnchorSessionFile` must live under the intended package home `.ak-roles`
|
|
263
|
-
* tree so createRecordSession path-derives the correct ledger home (ADR 0048).
|
|
264
|
-
* Subject placement still lands at book/kind/digest — anchor is not the nest.
|
|
265
|
-
*
|
|
266
|
-
* `resumed` comes only from the archivist open fact — no parallel existsSync.
|
|
267
|
-
*/
|
|
268
|
-
export async function openTicketSeatMemoryCoordinates(input: {
|
|
269
|
-
readonly ticketNumber: number;
|
|
270
|
-
readonly seat: TicketSeatMemorySeat;
|
|
271
|
-
readonly cwd: string;
|
|
272
|
-
readonly ledgerAnchorSessionFile: string;
|
|
273
|
-
/**
|
|
274
|
-
* When set, append the per-run binding via the already-opened manager
|
|
275
|
-
* (no parallel path-only custom-entry writer).
|
|
276
|
-
*/
|
|
277
|
-
readonly runId?: string;
|
|
278
|
-
}): Promise<DurablePrincipalCoordinates & { readonly resumed: boolean }> {
|
|
279
|
-
const createRecordSessionOpen = await loadCreateRecordSessionOpen();
|
|
280
|
-
const opened = createRecordSessionOpen({
|
|
281
|
-
cwd: input.cwd,
|
|
282
|
-
kind: TICKET_SEAT_MEMORY_KIND,
|
|
283
|
-
subject: ticketSeatMemorySubject(input.ticketNumber, input.seat),
|
|
284
|
-
parent: {
|
|
285
|
-
getSessionFile: () => input.ledgerAnchorSessionFile,
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
// Durable per-run boundary on the shared main volume before any turn writes.
|
|
289
|
-
// Reuse SessionManager.appendCustomEntry — id/parent/timestamp/flush owned there.
|
|
290
|
-
if (input.runId !== undefined) {
|
|
291
|
-
const data: TicketSeatRunBinding = { version: 1, runId: input.runId };
|
|
292
|
-
opened.session.appendCustomEntry(TICKET_SEAT_RUN_BINDING_ENTRY_TYPE, data);
|
|
293
|
-
}
|
|
294
|
-
const sessionFile = opened.session.getSessionFile();
|
|
295
|
-
const sessionDirectory = opened.session.getSessionDir();
|
|
296
|
-
if (typeof sessionFile !== "string" || sessionFile.length === 0) {
|
|
297
|
-
throw new Error(
|
|
298
|
-
`ticket-seat memory principal missing session file for ticket #${input.ticketNumber} seat ${input.seat}`,
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
if (typeof sessionDirectory !== "string" || sessionDirectory.length === 0) {
|
|
302
|
-
throw new Error(
|
|
303
|
-
`ticket-seat memory principal missing session directory for ticket #${input.ticketNumber} seat ${input.seat}`,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
return { sessionDirectory, sessionFile, resumed: opened.resumed };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export type TicketSeatMemoryRebindResult = {
|
|
310
|
-
readonly coordinates: DurablePrincipalCoordinates;
|
|
311
|
-
/** True when the nest already existed — callers must send continuation.kind=resume. */
|
|
312
|
-
readonly resumed: boolean;
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Rebind an admitted public officer run onto the ticket+seat memory principal.
|
|
317
|
-
* Run directory (admitted-request / attachments / artifacts) stays independent;
|
|
318
|
-
* only the logical memory principal continues across runs (#636 / ADR 0079).
|
|
319
|
-
*
|
|
320
|
-
* Host authority seals coordinates (no public-layer principal forgery).
|
|
321
|
-
* Page rewrite: missing page (ENOENT) is lawful skip; other failures propagate.
|
|
322
|
-
*/
|
|
323
|
-
export async function rebindAdmittedToTicketSeatMemory(input: {
|
|
324
|
-
readonly admitted: {
|
|
325
|
-
readonly runId: string;
|
|
326
|
-
readonly runDirectory: string;
|
|
327
|
-
readonly projectRoot: string;
|
|
328
|
-
readonly ticketNumber?: number;
|
|
329
|
-
principal: DurablePrincipal;
|
|
330
|
-
};
|
|
331
|
-
readonly seat: TicketSeatMemorySeat;
|
|
332
|
-
readonly principalAuthority: DurablePrincipalAuthority;
|
|
333
|
-
}): Promise<TicketSeatMemoryRebindResult | undefined> {
|
|
334
|
-
const ticketNumber = input.admitted.ticketNumber;
|
|
335
|
-
if (ticketNumber === undefined) return undefined;
|
|
336
|
-
|
|
337
|
-
// Anchor under the already-issued run principal so ledger home path-derives
|
|
338
|
-
// from the package home that issued admission (never ambient passwd home).
|
|
339
|
-
const anchor = input.principalAuthority.decode(input.admitted.principal);
|
|
340
|
-
const opened = await openTicketSeatMemoryCoordinates({
|
|
341
|
-
ticketNumber,
|
|
342
|
-
seat: input.seat,
|
|
343
|
-
cwd: input.admitted.projectRoot,
|
|
344
|
-
ledgerAnchorSessionFile: anchor.sessionFile,
|
|
345
|
-
// Analyst attributes frame/model/tool to this binding interval only (#636 D).
|
|
346
|
-
runId: input.admitted.runId,
|
|
347
|
-
});
|
|
348
|
-
const coordinates: DurablePrincipalCoordinates = {
|
|
349
|
-
sessionDirectory: opened.sessionDirectory,
|
|
350
|
-
sessionFile: opened.sessionFile,
|
|
351
|
-
};
|
|
352
|
-
const principal = input.principalAuthority.seal(coordinates);
|
|
353
|
-
// Validate round-trip through the host authority.
|
|
354
|
-
input.principalAuthority.decode(principal);
|
|
355
|
-
(input.admitted as { principal: DurablePrincipal }).principal = principal;
|
|
356
|
-
|
|
357
|
-
for (const page of ["admitted-request.json", "invocation.json"] as const) {
|
|
358
|
-
const path = join(input.admitted.runDirectory, page);
|
|
359
|
-
try {
|
|
360
|
-
const raw = JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
361
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
362
|
-
const next = {
|
|
363
|
-
...(raw as Record<string, unknown>),
|
|
364
|
-
sessionDirectory: coordinates.sessionDirectory,
|
|
365
|
-
sessionFile: coordinates.sessionFile,
|
|
366
|
-
};
|
|
367
|
-
await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
368
|
-
} catch (error) {
|
|
369
|
-
if (
|
|
370
|
-
error instanceof Error &&
|
|
371
|
-
"code" in error &&
|
|
372
|
-
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
373
|
-
) {
|
|
374
|
-
// Missing page is lawful for some seats; skip.
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
throw error;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return { coordinates, resumed: opened.resumed };
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const TICKET_SEAT_LAST_HOST_FILE = "last-host.json";
|
|
384
|
-
|
|
385
|
-
export type TicketSeatMemoryLastHost = {
|
|
386
|
-
readonly host: string;
|
|
387
|
-
/**
|
|
388
|
-
* Established Grok native isolation run (GROK_HOME parent) when Grok has run on this nest.
|
|
389
|
-
* Preserved across non-Grok hosts so same-host retry and return-to-Grok reopen the same home.
|
|
390
|
-
* Absent until a Grok turn records ownership.
|
|
391
|
-
*/
|
|
392
|
-
readonly runDirectory?: string;
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
/** Read the last host that wrote the ticket-seat memory nest (#617 DK-4 / #636). */
|
|
396
|
-
export async function readTicketSeatMemoryLastHost(
|
|
397
|
-
sessionDirectory: string,
|
|
398
|
-
): Promise<TicketSeatMemoryLastHost | undefined> {
|
|
399
|
-
try {
|
|
400
|
-
const raw = JSON.parse(
|
|
401
|
-
await readFile(join(sessionDirectory, TICKET_SEAT_LAST_HOST_FILE), "utf8"),
|
|
402
|
-
) as unknown;
|
|
403
|
-
if (
|
|
404
|
-
raw === null ||
|
|
405
|
-
typeof raw !== "object" ||
|
|
406
|
-
Array.isArray(raw) ||
|
|
407
|
-
typeof (raw as { host?: unknown }).host !== "string" ||
|
|
408
|
-
(raw as { host: string }).host.length === 0
|
|
409
|
-
) {
|
|
410
|
-
throw new Error(
|
|
411
|
-
`ticket-seat memory last-host page is damaged under ${sessionDirectory}`,
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
const host = (raw as { host: string }).host;
|
|
415
|
-
const runDirectory = (raw as { runDirectory?: unknown }).runDirectory;
|
|
416
|
-
return {
|
|
417
|
-
host,
|
|
418
|
-
...(typeof runDirectory === "string" && runDirectory.length > 0
|
|
419
|
-
? { runDirectory }
|
|
420
|
-
: {}),
|
|
421
|
-
};
|
|
422
|
-
} catch (error) {
|
|
423
|
-
if (
|
|
424
|
-
error instanceof Error &&
|
|
425
|
-
"code" in error &&
|
|
426
|
-
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
427
|
-
) {
|
|
428
|
-
return undefined;
|
|
429
|
-
}
|
|
430
|
-
throw error;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/** Persist the host that just wrote the ticket-seat memory nest. */
|
|
435
|
-
export async function writeTicketSeatMemoryLastHost(
|
|
436
|
-
sessionDirectory: string,
|
|
437
|
-
host: string,
|
|
438
|
-
runDirectory?: string,
|
|
439
|
-
): Promise<void> {
|
|
440
|
-
if (host.trim() === "") {
|
|
441
|
-
throw new Error("ticket-seat memory last-host requires a non-empty host");
|
|
442
|
-
}
|
|
443
|
-
const body: TicketSeatMemoryLastHost = {
|
|
444
|
-
host,
|
|
445
|
-
...(runDirectory !== undefined && runDirectory.length > 0
|
|
446
|
-
? { runDirectory }
|
|
447
|
-
: {}),
|
|
448
|
-
};
|
|
449
|
-
await writeFile(
|
|
450
|
-
join(sessionDirectory, TICKET_SEAT_LAST_HOST_FILE),
|
|
451
|
-
`${JSON.stringify(body, null, 2)}\n`,
|
|
452
|
-
"utf8",
|
|
453
|
-
);
|
|
454
|
-
}
|