@akagilnc/pi-workflow-roles 0.1.3412 → 0.1.3439
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/archivist-record-entry.js +21 -12
- package/dist/archivist-record-topology.js +20 -0
- package/dist/auditor-dossier-tool.js +67 -76
- package/dist/canonical-json.js +66 -69
- package/dist/compliance-transport.js +99 -86
- package/dist/countersign-contracts.js +23 -25
- package/dist/doctor-contracts.js +169 -145
- package/dist/engine-detour-tool.js +83 -87
- package/dist/engine-detour.js +148 -183
- package/dist/evidence-child-executor.js +10 -1
- package/dist/exact-utf8.js +11 -10
- package/dist/git-object-id.js +10 -5
- package/dist/gleaner-left-contracts.js +33 -35
- package/dist/grok/production-host.js +229 -118
- package/dist/inspector-contracts.js +28 -31
- package/dist/institutional-resolution.js +101 -104
- package/dist/ledger-session-read.js +241 -0
- package/dist/merger-contracts.js +66 -56
- package/dist/notary-contracts.js +91 -100
- package/dist/open-tool-schema.js +32 -31
- package/dist/package-contracts/collector-output.js +56 -43
- package/dist/package-contracts/fixer-output.js +52 -50
- package/dist/package-contracts/fixer-packet.js +67 -64
- package/dist/package-contracts/gatekeeper-output.js +48 -56
- package/dist/package-contracts/judge-output.js +23 -20
- package/dist/package-contracts/navigator-output.js +35 -40
- package/dist/package-contracts/reviewer-output.js +94 -110
- package/dist/package-contracts/terminating-infrastructure.js +62 -90
- package/dist/package-contracts/worker-output.js +30 -11
- package/dist/package-resources/engine-material.js +58 -94
- package/dist/public-cli/config.js +420 -484
- package/dist/public-cli/main.js +1219 -524
- package/dist/public-cli/registry.js +107 -114
- package/dist/receipt-delivery-policy.js +74 -76
- package/dist/session-opening-materials.js +44 -47
- package/dist/sha256.js +5 -3
- package/dist/sitian-appender.js +204 -210
- package/dist/sitian-contracts.js +15 -19
- package/dist/sitian-facade.js +5 -10
- package/dist/sitian-reader.js +37 -46
- package/dist/stream-idle-guard.js +63 -71
- package/dist/ticket-seat-memory.js +244 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +41 -0
- package/souls/audit-law.md +4 -0
- package/souls/countersign.md +1 -1
- package/souls/gleaner-left.md +1 -1
- package/souls/inspector.md +1 -1
- package/souls/notary.md +1 -1
- package/souls/reviewer.md +1 -1
- package/src/analyst-gate-cycles-read.ts +152 -83
- package/src/analyst-ledger.ts +170 -43
- package/src/archivist-record-entry.ts +32 -11
- package/src/archivist-record-topology.ts +39 -0
- package/src/evidence-child-executor.ts +14 -1
- package/src/grok/production-host.ts +44 -9
- package/src/host-contracts.ts +18 -0
- package/src/host-transition-prior-native.ts +8 -1
- package/src/ledger-session-read.ts +56 -0
- package/src/pi/durable-principal.ts +6 -0
- package/src/public-cli/inspector-run.ts +21 -10
- package/src/public-cli/invocation.ts +4 -20
- package/src/public-cli/notary-run.ts +21 -4
- package/src/public-cli/post-admission.ts +333 -42
- package/src/public-cli/settlement.ts +266 -81
- package/src/session-opening-materials.ts +4 -2
- package/src/ticket-seat-memory.ts +454 -0
|
@@ -33,9 +33,12 @@ import {
|
|
|
33
33
|
} from "./compliance-transport.ts";
|
|
34
34
|
import {
|
|
35
35
|
extractSessionTimestampSpan,
|
|
36
|
+
intervalRowsAroundAnchor,
|
|
37
|
+
LedgerSessionJsonlError,
|
|
36
38
|
readLedgerSessionJsonl,
|
|
37
39
|
type LedgerSessionRow,
|
|
38
40
|
} from "./ledger-session-read.ts";
|
|
41
|
+
import { isTicketSeatRunBindingRow } from "./ticket-seat-memory.ts";
|
|
39
42
|
|
|
40
43
|
/** One completed gate round: direct officer receipt or historical province/officer pair. */
|
|
41
44
|
/** Honest origin discriminant: direct summons vs historical province dispatch. */
|
|
@@ -91,17 +94,17 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
91
94
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
for (const row of rows) {
|
|
97
|
-
if (row.type !== "custom" || row.customType !== AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE) continue;
|
|
98
|
-
if (!isRecord(row.data) || !isRecord(row.data.parent)) continue;
|
|
99
|
-
const id = row.data.parent.attemptEntryId;
|
|
100
|
-
if (typeof id === "string" && id.length > 0) return id;
|
|
101
|
-
}
|
|
102
|
-
return undefined;
|
|
97
|
+
function isParentAttemptBindingRow(row: LedgerSessionRow): boolean {
|
|
98
|
+
return row.type === "custom" && row.customType === AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE;
|
|
103
99
|
}
|
|
104
100
|
|
|
101
|
+
/** One summons' interval via shared #636 binding-slice (parent-attempt markers). */
|
|
102
|
+
function intervalRowsForGateCall(
|
|
103
|
+
rows: readonly LedgerSessionRow[],
|
|
104
|
+
callRowIndex: number,
|
|
105
|
+
): readonly LedgerSessionRow[] {
|
|
106
|
+
return intervalRowsAroundAnchor(rows, callRowIndex, isParentAttemptBindingRow).rows;
|
|
107
|
+
}
|
|
105
108
|
|
|
106
109
|
/** Only true absence (ENOENT). ENOTDIR is damaged topology — must stay loud. */
|
|
107
110
|
function isMissingDirectoryError(error: unknown): boolean {
|
|
@@ -160,44 +163,6 @@ function acceptedGateReceiptIds(
|
|
|
160
163
|
return accepted;
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
/**
|
|
164
|
-
* Last accepted gate terminating toolCall on a nested volume (dispatch or officer).
|
|
165
|
-
* Preference: keep the last accepted receipt; fall back to a rejected call only
|
|
166
|
-
* when no accepted receipt exists (so classify can lawfully omit). Identity only
|
|
167
|
-
* — typed args are validated after recognition so unusable facts fail loud
|
|
168
|
-
* instead of being skipped as "not a gate volume". Soul-audit and other non-gate
|
|
169
|
-
* tools never qualify.
|
|
170
|
-
*/
|
|
171
|
-
function extractLastGateToolCall(
|
|
172
|
-
rows: readonly LedgerSessionRow[],
|
|
173
|
-
): GateToolCall | undefined {
|
|
174
|
-
const acceptedIds = acceptedGateReceiptIds(rows);
|
|
175
|
-
let lastAccepted: GateToolCall | undefined;
|
|
176
|
-
let lastRejected: GateToolCall | undefined;
|
|
177
|
-
for (const row of rows) {
|
|
178
|
-
const message = isRecord(row.message) ? row.message : undefined;
|
|
179
|
-
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
180
|
-
for (const part of message.content) {
|
|
181
|
-
if (!isRecord(part) || part.type !== "toolCall") continue;
|
|
182
|
-
if (typeof part.id !== "string" || part.id.length === 0) continue;
|
|
183
|
-
if (typeof part.name !== "string" || part.name.length === 0) continue;
|
|
184
|
-
if (!isGateTerminatingToolName(part.name)) continue;
|
|
185
|
-
const call: GateToolCall = {
|
|
186
|
-
toolName: part.name,
|
|
187
|
-
args: isRecord(part.arguments) ? part.arguments : undefined,
|
|
188
|
-
accepted: acceptedIds.has(part.id),
|
|
189
|
-
};
|
|
190
|
-
if (call.accepted) {
|
|
191
|
-
lastAccepted = call;
|
|
192
|
-
} else if (lastAccepted === undefined) {
|
|
193
|
-
// Only retain rejected while no accepted receipt has appeared yet.
|
|
194
|
-
lastRejected = call;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return lastAccepted ?? lastRejected;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
166
|
function requireAcceptedGateStatus(
|
|
202
167
|
args: Record<string, unknown> | undefined,
|
|
203
168
|
filePath: string,
|
|
@@ -243,6 +208,8 @@ type ClassifiedVolume =
|
|
|
243
208
|
readonly reason?: string;
|
|
244
209
|
/** Present only when the volume carries ak_auditor_parent_attempt_binding. */
|
|
245
210
|
readonly attemptEntryId?: string;
|
|
211
|
+
/** Binding parent session file when present (#636 multi-parent memory filter). */
|
|
212
|
+
readonly parentSessionFile?: string;
|
|
246
213
|
}
|
|
247
214
|
| {
|
|
248
215
|
readonly kind: "officer";
|
|
@@ -254,30 +221,81 @@ type ClassifiedVolume =
|
|
|
254
221
|
readonly findingsCount: number;
|
|
255
222
|
readonly officerWallMs: number;
|
|
256
223
|
readonly attemptEntryId?: string;
|
|
224
|
+
readonly parentSessionFile?: string;
|
|
257
225
|
};
|
|
258
226
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
227
|
+
/** Nearest preceding attempt binding (id + parent session file) before row index. */
|
|
228
|
+
function nearestAttemptBindingBefore(
|
|
229
|
+
rows: readonly LedgerSessionRow[],
|
|
230
|
+
beforeIndex: number,
|
|
231
|
+
): { readonly attemptEntryId?: string; readonly parentSessionFile?: string } {
|
|
232
|
+
for (let i = beforeIndex - 1; i >= 0; i -= 1) {
|
|
233
|
+
const row = rows[i]!;
|
|
234
|
+
if (!isParentAttemptBindingRow(row)) continue;
|
|
235
|
+
if (!isRecord(row.data) || !isRecord(row.data.parent)) continue;
|
|
236
|
+
const id = row.data.parent.attemptEntryId;
|
|
237
|
+
const sessionFile = row.data.parent.sessionFile;
|
|
238
|
+
return {
|
|
239
|
+
...(typeof id === "string" && id.length > 0 ? { attemptEntryId: id } : {}),
|
|
240
|
+
...(typeof sessionFile === "string" && sessionFile.length > 0
|
|
241
|
+
? { parentSessionFile: sessionFile }
|
|
242
|
+
: {}),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {};
|
|
246
|
+
}
|
|
269
247
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
248
|
+
/**
|
|
249
|
+
* All accepted gate terminating toolCalls on a volume (dispatch or officer),
|
|
250
|
+
* each with nearest preceding attempt binding. Ticket-seat continuous memory
|
|
251
|
+
* (#636) may carry multiple parent summons in one file — last-only would drop rounds.
|
|
252
|
+
*/
|
|
253
|
+
function extractAllAcceptedGateToolCalls(
|
|
254
|
+
rows: readonly LedgerSessionRow[],
|
|
255
|
+
): ReadonlyArray<GateToolCall & { readonly rowIndex: number; readonly attemptEntryId?: string; readonly parentSessionFile?: string }> {
|
|
256
|
+
const acceptedIds = acceptedGateReceiptIds(rows);
|
|
257
|
+
const out: Array<GateToolCall & { readonly rowIndex: number; readonly attemptEntryId?: string; readonly parentSessionFile?: string }> = [];
|
|
258
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
259
|
+
const row = rows[rowIndex]!;
|
|
260
|
+
const message = isRecord(row.message) ? row.message : undefined;
|
|
261
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
262
|
+
for (const part of message.content) {
|
|
263
|
+
if (!isRecord(part) || part.type !== "toolCall") continue;
|
|
264
|
+
if (typeof part.id !== "string" || part.id.length === 0) continue;
|
|
265
|
+
if (typeof part.name !== "string" || part.name.length === 0) continue;
|
|
266
|
+
if (!isGateTerminatingToolName(part.name)) continue;
|
|
267
|
+
if (!acceptedIds.has(part.id)) continue;
|
|
268
|
+
const binding = nearestAttemptBindingBefore(rows, rowIndex);
|
|
269
|
+
out.push({
|
|
270
|
+
toolName: part.name,
|
|
271
|
+
args: isRecord(part.arguments) ? part.arguments : undefined,
|
|
272
|
+
accepted: true,
|
|
273
|
+
rowIndex,
|
|
274
|
+
...binding,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
276
277
|
}
|
|
277
|
-
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function projectAcceptedGateCall(
|
|
282
|
+
filePath: string,
|
|
283
|
+
rows: readonly LedgerSessionRow[],
|
|
284
|
+
call: GateToolCall & {
|
|
285
|
+
readonly rowIndex: number;
|
|
286
|
+
readonly attemptEntryId?: string;
|
|
287
|
+
readonly parentSessionFile?: string;
|
|
288
|
+
},
|
|
289
|
+
): ClassifiedVolume | undefined {
|
|
290
|
+
// Continuous memory volumes carry many summons; span only this binding's interval.
|
|
291
|
+
const span = requireAcceptedGateSpan(intervalRowsForGateCall(rows, call.rowIndex), filePath);
|
|
278
292
|
const status = requireAcceptedGateStatus(call.args, filePath);
|
|
279
293
|
const findings = asStringFindings(call.args?.findings);
|
|
280
294
|
const findingsCount = findings.length;
|
|
295
|
+
const bindingFields = {
|
|
296
|
+
...(call.attemptEntryId === undefined ? {} : { attemptEntryId: call.attemptEntryId }),
|
|
297
|
+
...(call.parentSessionFile === undefined ? {} : { parentSessionFile: call.parentSessionFile }),
|
|
298
|
+
};
|
|
281
299
|
|
|
282
300
|
if (DISPATCH_TOOLS.has(call.toolName)) {
|
|
283
301
|
// Lawful province non-dispatch release — opens no round, never unreadable (#597).
|
|
@@ -303,7 +321,7 @@ async function classifyAuditorVolume(
|
|
|
303
321
|
officer,
|
|
304
322
|
status,
|
|
305
323
|
...(reason === undefined ? {} : { reason }),
|
|
306
|
-
...
|
|
324
|
+
...bindingFields,
|
|
307
325
|
};
|
|
308
326
|
}
|
|
309
327
|
|
|
@@ -323,10 +341,41 @@ async function classifyAuditorVolume(
|
|
|
323
341
|
findings,
|
|
324
342
|
findingsCount,
|
|
325
343
|
officerWallMs: span.wallMs,
|
|
326
|
-
...
|
|
344
|
+
...bindingFields,
|
|
327
345
|
};
|
|
328
346
|
}
|
|
329
347
|
|
|
348
|
+
async function classifyAuditorVolume(
|
|
349
|
+
filePath: string,
|
|
350
|
+
): Promise<readonly ClassifiedVolume[]> {
|
|
351
|
+
// Canonical JSONL errors propagate — failure honesty (never wash to fewer rounds).
|
|
352
|
+
// Shared ticket-seat main volumes are the exception: later-run damage after a
|
|
353
|
+
// closed binding must not erase earlier gate rounds (same ownership rule as
|
|
354
|
+
// timeline attribution). Pure nested gate volumes without run bindings stay loud.
|
|
355
|
+
let rows: readonly LedgerSessionRow[];
|
|
356
|
+
try {
|
|
357
|
+
rows = await readLedgerSessionJsonl(filePath);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (
|
|
360
|
+
error instanceof LedgerSessionJsonlError
|
|
361
|
+
&& error.prefixRows.some((row) => isTicketSeatRunBindingRow(row))
|
|
362
|
+
) {
|
|
363
|
+
rows = error.prefixRows;
|
|
364
|
+
} else {
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const accepted = extractAllAcceptedGateToolCalls(rows);
|
|
369
|
+
// Rejected-only / non-gate volumes omit (empty accepted set).
|
|
370
|
+
if (accepted.length === 0) return [];
|
|
371
|
+
const volumes: ClassifiedVolume[] = [];
|
|
372
|
+
for (const call of accepted) {
|
|
373
|
+
const projected = projectAcceptedGateCall(filePath, rows, call);
|
|
374
|
+
if (projected !== undefined) volumes.push(projected);
|
|
375
|
+
}
|
|
376
|
+
return volumes;
|
|
377
|
+
}
|
|
378
|
+
|
|
330
379
|
function pairGateRounds(
|
|
331
380
|
volumes: readonly ClassifiedVolume[],
|
|
332
381
|
): readonly AnalystGateCycleRound[] {
|
|
@@ -402,29 +451,49 @@ function pairGateRounds(
|
|
|
402
451
|
}
|
|
403
452
|
|
|
404
453
|
/**
|
|
405
|
-
* Read and pair gate-cycle rounds from
|
|
454
|
+
* Read and pair gate-cycle rounds from one or more auditor-roles directories.
|
|
406
455
|
* ENOENT (directory truly absent) → []. ENOTDIR and other errors propagate
|
|
407
456
|
* (failure honesty — damaged topology must not wash to zero rounds).
|
|
457
|
+
*
|
|
458
|
+
* #636 ticket-seat memory may place officer volumes under book-level subject
|
|
459
|
+
* digests shared across parent runs; pass `parentSessionFile` to keep only
|
|
460
|
+
* rounds whose attempt binding names that parent.
|
|
408
461
|
*/
|
|
409
462
|
export async function readAnalystGateCyclesFromAuditorRoles(
|
|
410
|
-
auditorRolesDirectory: string,
|
|
463
|
+
auditorRolesDirectory: string | readonly string[],
|
|
464
|
+
options: {
|
|
465
|
+
readonly parentSessionFile?: string;
|
|
466
|
+
} = {},
|
|
411
467
|
): Promise<readonly AnalystGateCycleRound[]> {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
names = entries
|
|
416
|
-
.filter((e) => e.isFile() && e.name.endsWith(".jsonl"))
|
|
417
|
-
.map((e) => e.name)
|
|
418
|
-
.sort();
|
|
419
|
-
} catch (error) {
|
|
420
|
-
if (isMissingDirectoryError(error)) return [];
|
|
421
|
-
throw error;
|
|
422
|
-
}
|
|
423
|
-
|
|
468
|
+
const directories = typeof auditorRolesDirectory === "string"
|
|
469
|
+
? [auditorRolesDirectory]
|
|
470
|
+
: auditorRolesDirectory;
|
|
424
471
|
const volumes: ClassifiedVolume[] = [];
|
|
425
|
-
for (const
|
|
426
|
-
|
|
427
|
-
|
|
472
|
+
for (const directory of directories) {
|
|
473
|
+
let names: string[];
|
|
474
|
+
try {
|
|
475
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
476
|
+
names = entries
|
|
477
|
+
.filter((e) => e.isFile() && e.name.endsWith(".jsonl"))
|
|
478
|
+
.map((e) => e.name)
|
|
479
|
+
.sort();
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (isMissingDirectoryError(error)) continue;
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
for (const name of names) {
|
|
485
|
+
const classified = await classifyAuditorVolume(join(directory, name));
|
|
486
|
+
for (const volume of classified) {
|
|
487
|
+
if (
|
|
488
|
+
options.parentSessionFile !== undefined &&
|
|
489
|
+
volume.parentSessionFile !== undefined &&
|
|
490
|
+
volume.parentSessionFile !== options.parentSessionFile
|
|
491
|
+
) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
volumes.push(volume);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
428
497
|
}
|
|
429
498
|
return pairGateRounds(volumes);
|
|
430
499
|
}
|
package/src/analyst-ledger.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { join } from "node:path";
|
|
|
17
17
|
|
|
18
18
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
|
|
19
19
|
import {
|
|
20
|
+
pathContainedIn,
|
|
20
21
|
physicalPathIdentity,
|
|
21
22
|
resolveActivationLedgerHome,
|
|
22
23
|
} from "./activation-ledger-topology.ts";
|
|
@@ -24,8 +25,10 @@ import {
|
|
|
24
25
|
extractSessionModelSequence,
|
|
25
26
|
extractSessionTimestampSpan,
|
|
26
27
|
extractSessionToolIntervals,
|
|
28
|
+
intervalRowsForMatchingBinding,
|
|
27
29
|
LedgerSessionJsonlError,
|
|
28
30
|
readLedgerSessionJsonl,
|
|
31
|
+
type LedgerSessionRow,
|
|
29
32
|
type SessionToolInterval,
|
|
30
33
|
} from "./ledger-session-read.ts";
|
|
31
34
|
import {
|
|
@@ -43,6 +46,11 @@ import {
|
|
|
43
46
|
readAnalystGateCyclesFromAuditorRoles,
|
|
44
47
|
type AnalystGateCycleRound,
|
|
45
48
|
} from "./analyst-gate-cycles-read.ts";
|
|
49
|
+
import {
|
|
50
|
+
isTicketSeatRunBindingRow,
|
|
51
|
+
resolveTicketSeatMemoryNestDirectories,
|
|
52
|
+
ticketSeatRunBindingRunId,
|
|
53
|
+
} from "./ticket-seat-memory.ts";
|
|
46
54
|
|
|
47
55
|
export type { AnalystGateCycleRound } from "./analyst-gate-cycles-read.ts";
|
|
48
56
|
|
|
@@ -201,6 +209,103 @@ async function resolveSessionFile(
|
|
|
201
209
|
return join(runDirectory, "session", "session.jsonl");
|
|
202
210
|
}
|
|
203
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Attribute session rows to one run (#636 D).
|
|
214
|
+
* Private run volumes (session under runDirectory) keep the whole file.
|
|
215
|
+
* Shared ticket-seat main volumes must carry this run's binding interval;
|
|
216
|
+
* whole-volume stats would let later continuations pollute earlier runs.
|
|
217
|
+
* Unbounded external volumes stay honest missing — never silent full ownership.
|
|
218
|
+
*
|
|
219
|
+
* `closed` on shared intervals: a later binding ended ownership before EOF of
|
|
220
|
+
* the provided rows. On damaged-prefix recovery, only closed intervals keep
|
|
221
|
+
* full frame/model/tool facts — open intervals may have lost rows to the bad line.
|
|
222
|
+
* Private volumes are never closed against a prefix (the whole file is this run).
|
|
223
|
+
*/
|
|
224
|
+
function attributeSessionRowsForRun(input: {
|
|
225
|
+
readonly rows: readonly LedgerSessionRow[];
|
|
226
|
+
readonly sessionFile: string;
|
|
227
|
+
readonly runDirectory: string;
|
|
228
|
+
readonly runId: string;
|
|
229
|
+
}):
|
|
230
|
+
| {
|
|
231
|
+
readonly kind: "attributed";
|
|
232
|
+
readonly rows: readonly LedgerSessionRow[];
|
|
233
|
+
readonly closed: boolean;
|
|
234
|
+
}
|
|
235
|
+
| { readonly kind: "missing-boundary"; readonly reason: string } {
|
|
236
|
+
if (pathContainedIn(input.runDirectory, input.sessionFile)) {
|
|
237
|
+
return { kind: "attributed", rows: input.rows, closed: false };
|
|
238
|
+
}
|
|
239
|
+
const interval = intervalRowsForMatchingBinding(
|
|
240
|
+
input.rows,
|
|
241
|
+
isTicketSeatRunBindingRow,
|
|
242
|
+
(row) => ticketSeatRunBindingRunId(row) === input.runId,
|
|
243
|
+
);
|
|
244
|
+
if (interval === undefined) {
|
|
245
|
+
return {
|
|
246
|
+
kind: "missing-boundary",
|
|
247
|
+
reason:
|
|
248
|
+
"shared session volume has no ticket-seat run binding for this run",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return { kind: "attributed", rows: interval.rows, closed: interval.closed };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Admit frame span (and partial edges) from already-attributed session rows.
|
|
256
|
+
* Shared by the clean parse path and closed-interval damage recovery.
|
|
257
|
+
*/
|
|
258
|
+
function admitFrameSpanFromRows(rows: readonly LedgerSessionRow[]): {
|
|
259
|
+
readonly frameSpan?: AnalystRunFrameSpan;
|
|
260
|
+
readonly missingReason?: string;
|
|
261
|
+
readonly partialFirstFrameAt: AnalystFirstFrameAt;
|
|
262
|
+
readonly partialLastFrameAt: AnalystOptionalTimestamp;
|
|
263
|
+
readonly models: readonly string[];
|
|
264
|
+
} {
|
|
265
|
+
const models = extractSessionModelSequence(rows);
|
|
266
|
+
const span = extractSessionTimestampSpan(rows);
|
|
267
|
+
let partialFirstFrameAt: AnalystFirstFrameAt = { status: "absent" };
|
|
268
|
+
let partialLastFrameAt: AnalystOptionalTimestamp = { status: "absent" };
|
|
269
|
+
if (span.startedAt === undefined || span.endedAt === undefined) {
|
|
270
|
+
if (span.startedAt !== undefined) {
|
|
271
|
+
partialFirstFrameAt = { status: "present", at: span.startedAt };
|
|
272
|
+
}
|
|
273
|
+
if (span.endedAt !== undefined) {
|
|
274
|
+
partialLastFrameAt = { status: "present", at: span.endedAt };
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
models,
|
|
278
|
+
partialFirstFrameAt,
|
|
279
|
+
partialLastFrameAt,
|
|
280
|
+
missingReason: "session timeline has no usable timestamps",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const startedMs = Date.parse(span.startedAt);
|
|
284
|
+
const endedMs = Date.parse(span.endedAt);
|
|
285
|
+
if (!Number.isFinite(startedMs) || !Number.isFinite(endedMs)) {
|
|
286
|
+
return {
|
|
287
|
+
models,
|
|
288
|
+
partialFirstFrameAt: { status: "present", at: span.startedAt },
|
|
289
|
+
partialLastFrameAt: { status: "present", at: span.endedAt },
|
|
290
|
+
missingReason: "session timeline timestamps are not parseable instants",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (endedMs < startedMs) {
|
|
294
|
+
return {
|
|
295
|
+
models,
|
|
296
|
+
partialFirstFrameAt: { status: "present", at: span.startedAt },
|
|
297
|
+
partialLastFrameAt: { status: "present", at: span.endedAt },
|
|
298
|
+
missingReason: "session timeline end is earlier than start",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
models,
|
|
303
|
+
frameSpan: { startedAt: span.startedAt, endedAt: span.endedAt },
|
|
304
|
+
partialFirstFrameAt: { status: "present", at: span.startedAt },
|
|
305
|
+
partialLastFrameAt: { status: "present", at: span.endedAt },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
204
309
|
/** Session first/last usable timestamps retained for B-wave wall-clock kernels. */
|
|
205
310
|
export type AnalystRunFrameSpan = {
|
|
206
311
|
readonly startedAt: string;
|
|
@@ -277,55 +382,67 @@ async function classifyScopedRun(input: {
|
|
|
277
382
|
let partialFirstFrameAt: AnalystFirstFrameAt = { status: "absent" };
|
|
278
383
|
let partialLastFrameAt: AnalystOptionalTimestamp = { status: "absent" };
|
|
279
384
|
|
|
280
|
-
// 1) session timeline
|
|
385
|
+
// 1) session timeline — shared ticket-seat volumes slice to this run's binding.
|
|
386
|
+
// Damage after a closed binding interval must not erase that run's full facts.
|
|
281
387
|
const sessionFile = await resolveSessionFile(input.runDirectory);
|
|
282
|
-
let rows:
|
|
388
|
+
let rows: readonly LedgerSessionRow[] | undefined;
|
|
389
|
+
const admitAttributed = (
|
|
390
|
+
attributedRows: readonly LedgerSessionRow[],
|
|
391
|
+
): void => {
|
|
392
|
+
rows = attributedRows;
|
|
393
|
+
const admitted = admitFrameSpanFromRows(attributedRows);
|
|
394
|
+
models = admitted.models;
|
|
395
|
+
partialFirstFrameAt = admitted.partialFirstFrameAt;
|
|
396
|
+
partialLastFrameAt = admitted.partialLastFrameAt;
|
|
397
|
+
if (admitted.frameSpan !== undefined) {
|
|
398
|
+
frameSpan = admitted.frameSpan;
|
|
399
|
+
} else if (admitted.missingReason !== undefined) {
|
|
400
|
+
missingSources.push("session-timeline");
|
|
401
|
+
reasons.push(admitted.missingReason);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
283
404
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
405
|
+
const rawRows = await readLedgerSessionJsonl(sessionFile);
|
|
406
|
+
const attributed = attributeSessionRowsForRun({
|
|
407
|
+
rows: rawRows,
|
|
408
|
+
sessionFile,
|
|
409
|
+
runDirectory: input.runDirectory,
|
|
410
|
+
runId: input.runId,
|
|
411
|
+
});
|
|
412
|
+
if (attributed.kind === "missing-boundary") {
|
|
288
413
|
missingSources.push("session-timeline");
|
|
289
|
-
reasons.push(
|
|
290
|
-
// Span incomplete, but any usable timestamp remains a typed partial fact.
|
|
291
|
-
if (span.startedAt !== undefined) {
|
|
292
|
-
partialFirstFrameAt = { status: "present", at: span.startedAt };
|
|
293
|
-
}
|
|
294
|
-
if (span.endedAt !== undefined) {
|
|
295
|
-
partialLastFrameAt = { status: "present", at: span.endedAt };
|
|
296
|
-
}
|
|
414
|
+
reasons.push(attributed.reason);
|
|
297
415
|
} else {
|
|
298
|
-
|
|
299
|
-
// Damaged spans stay page-local unreadable — never throw or emit negative/NaN wallMs.
|
|
300
|
-
const startedMs = Date.parse(span.startedAt);
|
|
301
|
-
const endedMs = Date.parse(span.endedAt);
|
|
302
|
-
if (!Number.isFinite(startedMs) || !Number.isFinite(endedMs)) {
|
|
303
|
-
missingSources.push("session-timeline");
|
|
304
|
-
reasons.push("session timeline timestamps are not parseable instants");
|
|
305
|
-
partialFirstFrameAt = { status: "present", at: span.startedAt };
|
|
306
|
-
partialLastFrameAt = { status: "present", at: span.endedAt };
|
|
307
|
-
} else if (endedMs < startedMs) {
|
|
308
|
-
missingSources.push("session-timeline");
|
|
309
|
-
reasons.push("session timeline end is earlier than start");
|
|
310
|
-
partialFirstFrameAt = { status: "present", at: span.startedAt };
|
|
311
|
-
partialLastFrameAt = { status: "present", at: span.endedAt };
|
|
312
|
-
} else {
|
|
313
|
-
frameSpan = { startedAt: span.startedAt, endedAt: span.endedAt };
|
|
314
|
-
}
|
|
416
|
+
admitAttributed(attributed.rows);
|
|
315
417
|
}
|
|
316
418
|
} catch (error) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
//
|
|
419
|
+
// Single parse kernel: attribute the prefix, then decide by binding closure.
|
|
420
|
+
// Closed shared interval → full frame/model/tool (damage belongs to a later run).
|
|
421
|
+
// Open interval / private volume / missing boundary → honest missing + partial edges.
|
|
320
422
|
if (error instanceof LedgerSessionJsonlError) {
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
423
|
+
const attributed = attributeSessionRowsForRun({
|
|
424
|
+
rows: error.prefixRows,
|
|
425
|
+
sessionFile,
|
|
426
|
+
runDirectory: input.runDirectory,
|
|
427
|
+
runId: input.runId,
|
|
428
|
+
});
|
|
429
|
+
if (attributed.kind === "attributed" && attributed.closed) {
|
|
430
|
+
admitAttributed(attributed.rows);
|
|
431
|
+
} else if (attributed.kind === "attributed") {
|
|
432
|
+
missingSources.push("session-timeline");
|
|
433
|
+
reasons.push(errorText(error));
|
|
434
|
+
const admitted = admitFrameSpanFromRows(attributed.rows);
|
|
435
|
+
models = admitted.models;
|
|
436
|
+
partialFirstFrameAt = admitted.partialFirstFrameAt;
|
|
437
|
+
partialLastFrameAt = admitted.partialLastFrameAt;
|
|
438
|
+
} else {
|
|
439
|
+
missingSources.push("session-timeline");
|
|
440
|
+
reasons.push(errorText(error));
|
|
441
|
+
// missing-boundary on damaged prefix: no partial ownership of sibling runs.
|
|
327
442
|
}
|
|
328
|
-
|
|
443
|
+
} else {
|
|
444
|
+
missingSources.push("session-timeline");
|
|
445
|
+
reasons.push(errorText(error));
|
|
329
446
|
}
|
|
330
447
|
}
|
|
331
448
|
|
|
@@ -399,11 +516,21 @@ async function classifyScopedRun(input: {
|
|
|
399
516
|
// Nested auditor-roles gate pairs stay inside the sole scan (families must
|
|
400
517
|
// not readdir this tree again). Missing directory → []. Damaged discovered
|
|
401
518
|
// nested JSONL is page-local unreadable — never silently under-count rounds.
|
|
519
|
+
// #636: ticket-seat memory nests via sole discovery helper.
|
|
402
520
|
let gateCycles: readonly AnalystGateCycleRound[];
|
|
403
521
|
try {
|
|
404
|
-
|
|
522
|
+
const memoryNests = await resolveTicketSeatMemoryNestDirectories({
|
|
523
|
+
runDirectory: input.runDirectory,
|
|
524
|
+
seats: ["inspector", "notary"],
|
|
525
|
+
});
|
|
526
|
+
const directories = [
|
|
405
527
|
join(input.runDirectory, "session", "auditor-roles"),
|
|
406
|
-
|
|
528
|
+
...memoryNests,
|
|
529
|
+
];
|
|
530
|
+
const parentSessionFile = join(input.runDirectory, "session", "session.jsonl");
|
|
531
|
+
gateCycles = await readAnalystGateCyclesFromAuditorRoles(directories, {
|
|
532
|
+
parentSessionFile,
|
|
533
|
+
});
|
|
407
534
|
} catch (error) {
|
|
408
535
|
return {
|
|
409
536
|
kind: "unreadable",
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* 调用方只声明自己是谁的什么;落点由候簿拓扑算出,签名不含任何落点/路径参数。
|
|
4
4
|
* 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
|
|
5
5
|
*/
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
6
|
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
7
|
import { dirname, resolve, join } from "node:path";
|
|
9
8
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
@@ -18,6 +17,9 @@ import {
|
|
|
18
17
|
physicallyContainedIn,
|
|
19
18
|
resolveActivationLedgerHomeForPath,
|
|
20
19
|
} from "./activation-ledger-topology.ts";
|
|
20
|
+
import { subjectKeyedRecordDirectory } from "./archivist-record-topology.ts";
|
|
21
|
+
|
|
22
|
+
export { subjectKeyedRecordDirectory } from "./archivist-record-topology.ts";
|
|
21
23
|
|
|
22
24
|
const CURRENT_SESSION_LEDGER = "current-session.json";
|
|
23
25
|
|
|
@@ -120,6 +122,13 @@ function assertRecentFinalFileUnderSessionDir(
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/** Open result including the sole resumed fact (nest existed before this open). */
|
|
126
|
+
export type RecordSessionOpen = {
|
|
127
|
+
readonly session: SessionManager;
|
|
128
|
+
/** True only when an existing same-nest volume was reopened (subject/gate path). */
|
|
129
|
+
readonly resumed: boolean;
|
|
130
|
+
};
|
|
131
|
+
|
|
123
132
|
/**
|
|
124
133
|
* Sole package entry that constructs a durable Pi session record (ADR 0065).
|
|
125
134
|
* No destination/path parameters — location is computed from ledger topology only.
|
|
@@ -133,8 +142,10 @@ function assertRecentFinalFileUnderSessionDir(
|
|
|
133
142
|
* a sibling volume selected only by kind/cwd/mtime.
|
|
134
143
|
* New persisted principals materialize their deferred session header before return so
|
|
135
144
|
* custom-entry-only writers do not need a parallel delayed-header helper.
|
|
145
|
+
*
|
|
146
|
+
* `resumed` is the sole open-or-continue fact — callers must not re-probe nest existence.
|
|
136
147
|
*/
|
|
137
|
-
export function
|
|
148
|
+
export function createRecordSessionOpen(options: CreateRecordSessionOptions): RecordSessionOpen {
|
|
138
149
|
const cwd = options.cwd;
|
|
139
150
|
const parentFile = options.parent?.getSessionFile();
|
|
140
151
|
// Path → ledger home is owned by topology (explicit env.home nests via parent path).
|
|
@@ -144,16 +155,18 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
|
|
|
144
155
|
let parentSession: string | undefined;
|
|
145
156
|
|
|
146
157
|
if (options.subject !== undefined) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
options.
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
sessionDir = subjectKeyedRecordDirectory({
|
|
159
|
+
cwd,
|
|
160
|
+
kind: options.kind,
|
|
161
|
+
subject: options.subject,
|
|
162
|
+
...(parentFile === undefined || parentFile.length === 0
|
|
163
|
+
? {}
|
|
164
|
+
: { parentSessionFile: parentFile }),
|
|
165
|
+
});
|
|
153
166
|
parentSession = parentFile && parentFile.length > 0 ? parentFile : undefined;
|
|
154
167
|
} else if (parentFile === undefined || parentFile.length === 0) {
|
|
155
168
|
// No durable parent principal — preserve prior in-memory child behavior.
|
|
156
|
-
return SessionManager.inMemory(cwd);
|
|
169
|
+
return { session: SessionManager.inMemory(cwd), resumed: false };
|
|
157
170
|
} else {
|
|
158
171
|
const parentResolved = resolve(parentFile);
|
|
159
172
|
// Nest under parent only when the parent record already lives under the package home.
|
|
@@ -177,7 +190,10 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
|
|
|
177
190
|
if (mayResumeSameNest && nestAlreadyExists) {
|
|
178
191
|
const recentFile = readCurrentSession(sessionDir);
|
|
179
192
|
assertRecentFinalFileUnderSessionDir(sessionDir, recentFile);
|
|
180
|
-
return
|
|
193
|
+
return {
|
|
194
|
+
session: SessionManager.open(recentFile, sessionDir, cwd),
|
|
195
|
+
resumed: true,
|
|
196
|
+
};
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
const session = SessionManager.create(
|
|
@@ -202,5 +218,10 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
|
|
|
202
218
|
writeCurrentSession(sessionDir, file);
|
|
203
219
|
}
|
|
204
220
|
}
|
|
205
|
-
return session;
|
|
221
|
+
return { session, resumed: false };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Session-only facade — most callers only need the manager. */
|
|
225
|
+
export function createRecordSession(options: CreateRecordSessionOptions): SessionManager {
|
|
226
|
+
return createRecordSessionOpen(options).session;
|
|
206
227
|
}
|