@akagilnc/pi-workflow-roles 0.1.3652 → 0.1.3661
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/README.md +0 -2
- package/dist/acp-host/production-host.js +1056 -1236
- package/dist/public-cli/case-dossier-delivery.js +43 -0
- package/dist/public-cli/inspector-run.js +9 -6
- package/dist/public-cli/instruction-seat-run.js +24 -24
- package/dist/public-cli/invocation.js +2 -24
- package/dist/public-cli/main.js +1876 -2159
- package/dist/public-cli/post-admission.js +38 -12
- package/dist/public-cli/run-lifecycle.js +25 -14
- package/dist/public-cli/seat-ticket-binding.js +93 -70
- package/package.json +1 -1
- package/src/public-cli/case-dossier-delivery.ts +51 -0
- package/src/public-cli/countersign-run.ts +14 -17
- package/src/public-cli/diarist-run.ts +11 -13
- package/src/public-cli/inspector-run.ts +9 -10
- package/src/public-cli/instruction-seat-run.ts +28 -35
- package/src/public-cli/invocation.ts +4 -46
- package/src/public-cli/post-admission.ts +41 -6
- package/src/public-cli/run-lifecycle.ts +29 -21
- package/src/public-cli/seat-ticket-binding.ts +108 -104
- package/dist/diarist-ticket-resolution.js +0 -177
- package/resources/diarist-resolve-ticket.md +0 -39
- package/src/diarist-ticket-resolution.ts +0 -302
|
@@ -36,11 +36,9 @@ import {
|
|
|
36
36
|
type SameTicketSummonsMaterials,
|
|
37
37
|
} from "./run-lifecycle.ts";
|
|
38
38
|
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
ticketNumberFromProbe,
|
|
39
|
+
bindReusedTicketNumber,
|
|
40
|
+
resolveKnownTicketNumber,
|
|
42
41
|
tryResumeSameTicketSeatRun,
|
|
43
|
-
type InstructionTicketProbe,
|
|
44
42
|
} from "./seat-ticket-binding.ts";
|
|
45
43
|
import {
|
|
46
44
|
presentStructuralRejection,
|
|
@@ -248,11 +246,11 @@ export async function runPublicInstructionSeat(
|
|
|
248
246
|
let auditorSubject: "judge" | "doctor" | undefined;
|
|
249
247
|
let auditorSourceRun: string | undefined;
|
|
250
248
|
let auditorSourceTicket: number | undefined;
|
|
251
|
-
let
|
|
249
|
+
let reusedTicketNumber: number | undefined;
|
|
252
250
|
|
|
253
251
|
// #637 / #747: resume prior seat run with this summons' materials.
|
|
254
252
|
// Auditor (#747): parent --source-run path is the lookup key.
|
|
255
|
-
// Other instruction seats: ticket
|
|
253
|
+
// Other instruction seats: reuse known ticket (#709). No bare catch→fresh.
|
|
256
254
|
const projectRoot = parsed.project ?? env.cwd;
|
|
257
255
|
if (role === "auditor") {
|
|
258
256
|
if (parsed.subject !== "judge" && parsed.subject !== "doctor") {
|
|
@@ -288,12 +286,11 @@ export async function runPublicInstructionSeat(
|
|
|
288
286
|
return { exitCode: 2 };
|
|
289
287
|
}
|
|
290
288
|
} else {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
parsed.instruction,
|
|
289
|
+
reusedTicketNumber = await resolveKnownTicketNumber({
|
|
290
|
+
instruction: parsed.instruction,
|
|
294
291
|
projectRoot,
|
|
295
|
-
env,
|
|
296
|
-
);
|
|
292
|
+
home: env.home,
|
|
293
|
+
});
|
|
297
294
|
}
|
|
298
295
|
|
|
299
296
|
const summons: SameTicketSummonsMaterials = {
|
|
@@ -322,26 +319,22 @@ export async function runPublicInstructionSeat(
|
|
|
322
319
|
}),
|
|
323
320
|
});
|
|
324
321
|
if (resumed !== undefined) return resumed;
|
|
325
|
-
} else {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
),
|
|
342
|
-
});
|
|
343
|
-
if (resumed !== undefined) return resumed;
|
|
344
|
-
}
|
|
322
|
+
} else if (reusedTicketNumber !== undefined) {
|
|
323
|
+
const resumed = await tryResumeSameTicketSeatRun({
|
|
324
|
+
home: env.home,
|
|
325
|
+
projectRoot,
|
|
326
|
+
role,
|
|
327
|
+
ticketNumber: reusedTicketNumber,
|
|
328
|
+
freshSummons: env.freshSummons,
|
|
329
|
+
summons,
|
|
330
|
+
resume: (runId, materials) =>
|
|
331
|
+
runPublicInstructionSeatResume(
|
|
332
|
+
{ runId, ...(materials === undefined ? {} : { summons: materials }) },
|
|
333
|
+
env,
|
|
334
|
+
io,
|
|
335
|
+
),
|
|
336
|
+
});
|
|
337
|
+
if (resumed !== undefined) return resumed;
|
|
345
338
|
}
|
|
346
339
|
|
|
347
340
|
let admitted: AdmittedInstructionSeatInvocation;
|
|
@@ -406,13 +399,13 @@ export async function runPublicInstructionSeat(
|
|
|
406
399
|
request: turnRequest,
|
|
407
400
|
adapters: instructionSeatAdapters({
|
|
408
401
|
beforeDispatch: async (admittedSeat) => {
|
|
409
|
-
// #635/#
|
|
402
|
+
// #635/#709: ticket bind inside controlled-failure boundary.
|
|
410
403
|
// Auditor inherits source-run ticket (notary face).
|
|
411
|
-
// Other seats:
|
|
404
|
+
// Other seats: reuse known ticket identity.
|
|
412
405
|
if (auditorSourceTicket !== undefined) {
|
|
413
406
|
await bindAdmittedTicketNumber(admittedSeat, auditorSourceTicket);
|
|
414
|
-
} else
|
|
415
|
-
await
|
|
407
|
+
} else {
|
|
408
|
+
await bindReusedTicketNumber(admittedSeat, reusedTicketNumber);
|
|
416
409
|
}
|
|
417
410
|
},
|
|
418
411
|
}),
|
|
@@ -105,17 +105,10 @@ export type AdmittedRoleInvocationBase = {
|
|
|
105
105
|
*/
|
|
106
106
|
readonly correlationId?: string;
|
|
107
107
|
/**
|
|
108
|
-
* Typed ticketNumber after
|
|
109
|
-
* Admission does not bind from CLI flag or attachment frontmatter.
|
|
108
|
+
* Typed ticketNumber after known-identity reuse or notary source-run inheritance
|
|
109
|
+
* (#635 / #709). Admission does not bind from CLI flag or attachment frontmatter.
|
|
110
110
|
*/
|
|
111
111
|
readonly ticketNumber?: number;
|
|
112
|
-
/**
|
|
113
|
-
* Seat LLM ticket resolution settled as true-unbound (#635).
|
|
114
|
-
* Distinct from unbound-not-yet-resolved (field absent). Mutually exclusive
|
|
115
|
-
* with ticketNumber; both are durable one-shot conclusions so auto-resume
|
|
116
|
-
* and manual resume never re-enter hermes resolution.
|
|
117
|
-
*/
|
|
118
|
-
readonly ticketResolution?: "true-unbound";
|
|
119
112
|
/** Effective model from invocation identity; restored on resume when no CLI model is given. */
|
|
120
113
|
readonly model?: InvocationEffectiveModel;
|
|
121
114
|
};
|
|
@@ -464,8 +457,8 @@ export async function persistAdmittedSourceRunPath(
|
|
|
464
457
|
/**
|
|
465
458
|
* Bind a post-admission resolved ticketNumber onto the in-memory admitted
|
|
466
459
|
* object and both durable pages (invocation.json + admitted-request.json).
|
|
467
|
-
* Used by
|
|
468
|
-
*
|
|
460
|
+
* Used by known-ticket reuse when admission was unbound (#635 / #709).
|
|
461
|
+
* Never clears an existing binding.
|
|
469
462
|
*/
|
|
470
463
|
export async function bindAdmittedTicketNumber(
|
|
471
464
|
admitted: AdmittedRoleInvocation,
|
|
@@ -474,11 +467,6 @@ export async function bindAdmittedTicketNumber(
|
|
|
474
467
|
if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
|
|
475
468
|
throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
|
|
476
469
|
}
|
|
477
|
-
if (admitted.ticketResolution === "true-unbound") {
|
|
478
|
-
throw new Error(
|
|
479
|
-
"bindAdmittedTicketNumber refuses to replace durable true-unbound ticket resolution",
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
470
|
if (admitted.ticketNumber !== undefined) {
|
|
483
471
|
if (admitted.ticketNumber === ticketNumber) return;
|
|
484
472
|
throw new Error(
|
|
@@ -499,36 +487,6 @@ export async function bindAdmittedTicketNumber(
|
|
|
499
487
|
);
|
|
500
488
|
}
|
|
501
489
|
|
|
502
|
-
/**
|
|
503
|
-
* Persist seat LLM true-unbound conclusion onto in-memory admitted + both durable
|
|
504
|
-
* pages so later auto-resume attempts and manual resume never re-enter hermes (#635).
|
|
505
|
-
*/
|
|
506
|
-
export async function recordTrueUnboundTicketResolution(
|
|
507
|
-
admitted: AdmittedRoleInvocation,
|
|
508
|
-
): Promise<void> {
|
|
509
|
-
if (admitted.ticketNumber !== undefined) {
|
|
510
|
-
throw new Error(
|
|
511
|
-
`recordTrueUnboundTicketResolution refuses to overwrite ticket #${admitted.ticketNumber}`,
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
if (admitted.ticketResolution === "true-unbound") return;
|
|
515
|
-
(admitted as { ticketResolution?: "true-unbound" }).ticketResolution =
|
|
516
|
-
"true-unbound";
|
|
517
|
-
await mergeInvocationIdentityPage(admitted.runDirectory, {
|
|
518
|
-
ticketResolution: "true-unbound",
|
|
519
|
-
});
|
|
520
|
-
const admittedPath = admitted.admittedRequestPath;
|
|
521
|
-
const current = JSON.parse(await readFile(admittedPath, "utf8")) as Record<
|
|
522
|
-
string,
|
|
523
|
-
unknown
|
|
524
|
-
>;
|
|
525
|
-
await writeFile(
|
|
526
|
-
admittedPath,
|
|
527
|
-
`${JSON.stringify({ ...current, ticketResolution: "true-unbound" }, null, 2)}\n`,
|
|
528
|
-
"utf8",
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
490
|
/** Add the identity returned by the production Pi launch seam to its existing ledger page. */
|
|
533
491
|
export async function recordLaunchedPiIdentity(
|
|
534
492
|
runDirectory: string,
|
|
@@ -27,12 +27,25 @@ import type {
|
|
|
27
27
|
ControlledFailureCause,
|
|
28
28
|
DurablePrincipal,
|
|
29
29
|
DurablePrincipalAuthority,
|
|
30
|
+
RoleTurnContinuation,
|
|
30
31
|
RoleTurnHost,
|
|
31
32
|
RoleTurnKnownFailure,
|
|
32
33
|
RoleTurnRequest,
|
|
33
34
|
RoleTurnResult,
|
|
34
35
|
SessionCustomEntryAppender,
|
|
35
36
|
} from "../host-contracts.ts";
|
|
37
|
+
import { projectCaseDossierPointerSection } from "./case-dossier-delivery.ts";
|
|
38
|
+
|
|
39
|
+
/** Append one system section to a continuation prompt, keeping its kind. */
|
|
40
|
+
function appendContinuationSection(
|
|
41
|
+
continuation: RoleTurnContinuation,
|
|
42
|
+
section: string,
|
|
43
|
+
): RoleTurnContinuation {
|
|
44
|
+
const prompt = `${continuation.prompt}\n\n${section}`;
|
|
45
|
+
return continuation.kind === "initial"
|
|
46
|
+
? { kind: "initial", prompt }
|
|
47
|
+
: { kind: "resume", prompt };
|
|
48
|
+
}
|
|
36
49
|
import { projectHostTransitionPriorNative } from "../host-transition-prior-native.ts";
|
|
37
50
|
import type { CredentialProviders, SeatModelConfig } from "./config.ts";
|
|
38
51
|
import {
|
|
@@ -297,10 +310,10 @@ export async function dispatchPostAdmissionTurn<
|
|
|
297
310
|
admitted.principal === undefined
|
|
298
311
|
? undefined
|
|
299
312
|
: env.principalAuthority.decode(admitted.principal);
|
|
300
|
-
let
|
|
313
|
+
let hostTransition: RoleTurnRequest["hostTransition"];
|
|
301
314
|
try {
|
|
302
315
|
previousHost = await readInvocationHost(admitted.runDirectory);
|
|
303
|
-
|
|
316
|
+
hostTransition =
|
|
304
317
|
previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
|
|
305
318
|
? await projectHostTransitionPriorNative({
|
|
306
319
|
previousHost,
|
|
@@ -308,10 +321,6 @@ export async function dispatchPostAdmissionTurn<
|
|
|
308
321
|
piSessionFile: principalCoordinates.sessionFile,
|
|
309
322
|
})
|
|
310
323
|
: undefined;
|
|
311
|
-
turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
|
|
312
|
-
if (hostTransition !== undefined) {
|
|
313
|
-
turnRequest = { ...turnRequest, hostTransition };
|
|
314
|
-
}
|
|
315
324
|
} catch (error) {
|
|
316
325
|
// prior-native IO is on the public one-shot path — controlled failure, not bare throw.
|
|
317
326
|
return (await presentControlledFailure(
|
|
@@ -351,6 +360,32 @@ export async function dispatchPostAdmissionTurn<
|
|
|
351
360
|
}
|
|
352
361
|
}
|
|
353
362
|
|
|
363
|
+
// Turn request is assembled after beforeDispatch so this turn sees whatever it
|
|
364
|
+
// settled — the seat's ticket bind re-projection. Case dossier delivery
|
|
365
|
+
// (ADR 0081) rides here for every public entry: first call, same-ticket
|
|
366
|
+
// re-summons and manual resume alike. System refs append their own neutral
|
|
367
|
+
// section; caller frozen attachments and the seat's own prompt bytes are
|
|
368
|
+
// never rewritten.
|
|
369
|
+
let turnRequest: RoleTurnRequest =
|
|
370
|
+
env.signal === undefined ? request : { ...request, signal: env.signal };
|
|
371
|
+
if (hostTransition !== undefined) {
|
|
372
|
+
turnRequest = { ...turnRequest, hostTransition };
|
|
373
|
+
}
|
|
374
|
+
const dossierSection = await projectCaseDossierPointerSection({
|
|
375
|
+
ticketNumber: admitted.ticketNumber,
|
|
376
|
+
projectRoot: admitted.projectRoot,
|
|
377
|
+
home: env.home,
|
|
378
|
+
});
|
|
379
|
+
if (dossierSection !== undefined) {
|
|
380
|
+
turnRequest = {
|
|
381
|
+
...turnRequest,
|
|
382
|
+
continuation: appendContinuationSection(
|
|
383
|
+
turnRequest.continuation,
|
|
384
|
+
dossierSection,
|
|
385
|
+
),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
354
389
|
let result: RoleTurnResult;
|
|
355
390
|
try {
|
|
356
391
|
result = await env.roleTurnHost.executeTurn(turnRequest);
|
|
@@ -1128,6 +1128,34 @@ export async function findLatestRunIdForSeatTicket(input: {
|
|
|
1128
1128
|
return best;
|
|
1129
1129
|
}
|
|
1130
1130
|
|
|
1131
|
+
/**
|
|
1132
|
+
* Ticket identities this book's retained runs already record (#709).
|
|
1133
|
+
* Read-only reuse surface for seat ticket binding — no ticket number is minted here.
|
|
1134
|
+
*/
|
|
1135
|
+
export async function collectBookRunTicketNumbers(input: {
|
|
1136
|
+
readonly home: string;
|
|
1137
|
+
readonly bookKey: string;
|
|
1138
|
+
}): Promise<ReadonlySet<number>> {
|
|
1139
|
+
const ledgerHome = resolveActivationLedgerHome(input.home);
|
|
1140
|
+
const runsDir = join(
|
|
1141
|
+
activationBookDirectory(ledgerHome, input.bookKey),
|
|
1142
|
+
"runs",
|
|
1143
|
+
);
|
|
1144
|
+
let entries: string[];
|
|
1145
|
+
try {
|
|
1146
|
+
entries = await readdir(runsDir);
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
if (errorCodeOf(error) === "ENOENT") return new Set();
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
const known = new Set<number>();
|
|
1152
|
+
for (const entry of entries) {
|
|
1153
|
+
const ticketNumber = await readRunTicketNumber(join(runsDir, entry));
|
|
1154
|
+
if (ticketNumber !== undefined) known.add(ticketNumber);
|
|
1155
|
+
}
|
|
1156
|
+
return known;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1131
1159
|
type LoadedAdmittedRequestFields = {
|
|
1132
1160
|
readonly instruction: string;
|
|
1133
1161
|
readonly instructionEmpty: boolean;
|
|
@@ -1143,8 +1171,6 @@ type LoadedAdmittedRequestFields = {
|
|
|
1143
1171
|
readonly derived?: DerivedMergerEnvelope;
|
|
1144
1172
|
readonly correlationId?: string;
|
|
1145
1173
|
readonly ticketNumber?: number;
|
|
1146
|
-
/** Durable seat LLM true-unbound conclusion (#635); restored on resume. */
|
|
1147
|
-
readonly ticketResolution?: "true-unbound";
|
|
1148
1174
|
/** Collector — admitted repository/PR identity restored on resume (#633). */
|
|
1149
1175
|
readonly prNumber?: number;
|
|
1150
1176
|
readonly repository?: string;
|
|
@@ -1168,7 +1194,6 @@ function parsePersistedTicketIdentity(
|
|
|
1168
1194
|
): {
|
|
1169
1195
|
correlationId?: string;
|
|
1170
1196
|
ticketNumber?: number;
|
|
1171
|
-
ticketResolution?: "true-unbound";
|
|
1172
1197
|
} {
|
|
1173
1198
|
const correlationId =
|
|
1174
1199
|
typeof record.correlationId === "string" && record.correlationId.trim() !== ""
|
|
@@ -1180,26 +1205,19 @@ function parsePersistedTicketIdentity(
|
|
|
1180
1205
|
record.ticketNumber >= 1
|
|
1181
1206
|
? record.ticketNumber
|
|
1182
1207
|
: undefined;
|
|
1183
|
-
const ticketResolution =
|
|
1184
|
-
record.ticketResolution === "true-unbound" ? "true-unbound" : undefined;
|
|
1185
1208
|
return {
|
|
1186
1209
|
...(correlationId === undefined ? {} : { correlationId }),
|
|
1187
1210
|
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
1188
|
-
...(ticketResolution === undefined ? {} : { ticketResolution }),
|
|
1189
1211
|
};
|
|
1190
1212
|
}
|
|
1191
1213
|
|
|
1192
1214
|
function restoredTicketFields(fields: LoadedAdmittedRequestFields): {
|
|
1193
1215
|
correlationId?: string;
|
|
1194
1216
|
ticketNumber?: number;
|
|
1195
|
-
ticketResolution?: "true-unbound";
|
|
1196
1217
|
} {
|
|
1197
1218
|
return {
|
|
1198
1219
|
...(fields.correlationId === undefined ? {} : { correlationId: fields.correlationId }),
|
|
1199
1220
|
...(fields.ticketNumber === undefined ? {} : { ticketNumber: fields.ticketNumber }),
|
|
1200
|
-
...(fields.ticketResolution === undefined
|
|
1201
|
-
? {}
|
|
1202
|
-
: { ticketResolution: fields.ticketResolution }),
|
|
1203
1221
|
};
|
|
1204
1222
|
}
|
|
1205
1223
|
|
|
@@ -1249,7 +1267,6 @@ async function loadResumableRunRecord(
|
|
|
1249
1267
|
let derived: DerivedMergerEnvelope | undefined;
|
|
1250
1268
|
let correlationId: string | undefined;
|
|
1251
1269
|
let ticketNumber: number | undefined;
|
|
1252
|
-
let ticketResolution: "true-unbound" | undefined;
|
|
1253
1270
|
let prNumber: number | undefined;
|
|
1254
1271
|
let repository: string | undefined;
|
|
1255
1272
|
let repositoryDisplay: string | undefined;
|
|
@@ -1409,7 +1426,6 @@ async function loadResumableRunRecord(
|
|
|
1409
1426
|
const fromAdmitted = parsePersistedTicketIdentity(record);
|
|
1410
1427
|
correlationId = fromAdmitted.correlationId;
|
|
1411
1428
|
ticketNumber = fromAdmitted.ticketNumber;
|
|
1412
|
-
ticketResolution = fromAdmitted.ticketResolution;
|
|
1413
1429
|
}
|
|
1414
1430
|
} catch (error) {
|
|
1415
1431
|
// Preserve unique --authority-ref grammar failures; do not collapse to unreadable.
|
|
@@ -1439,17 +1455,10 @@ async function loadResumableRunRecord(
|
|
|
1439
1455
|
: {}),
|
|
1440
1456
|
};
|
|
1441
1457
|
}
|
|
1442
|
-
if (
|
|
1443
|
-
correlationId === undefined ||
|
|
1444
|
-
ticketNumber === undefined ||
|
|
1445
|
-
ticketResolution === undefined
|
|
1446
|
-
) {
|
|
1458
|
+
if (correlationId === undefined || ticketNumber === undefined) {
|
|
1447
1459
|
const fromInvocation = parsePersistedTicketIdentity(rec);
|
|
1448
1460
|
if (correlationId === undefined) correlationId = fromInvocation.correlationId;
|
|
1449
1461
|
if (ticketNumber === undefined) ticketNumber = fromInvocation.ticketNumber;
|
|
1450
|
-
if (ticketResolution === undefined) {
|
|
1451
|
-
ticketResolution = fromInvocation.ticketResolution;
|
|
1452
|
-
}
|
|
1453
1462
|
}
|
|
1454
1463
|
}
|
|
1455
1464
|
} catch (error) {
|
|
@@ -1485,7 +1494,6 @@ async function loadResumableRunRecord(
|
|
|
1485
1494
|
...(derived === undefined ? {} : { derived }),
|
|
1486
1495
|
...(correlationId === undefined ? {} : { correlationId }),
|
|
1487
1496
|
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
1488
|
-
...(ticketResolution === undefined ? {} : { ticketResolution }),
|
|
1489
1497
|
...(prNumber === undefined ? {} : { prNumber }),
|
|
1490
1498
|
...(repository === undefined ? {} : { repository }),
|
|
1491
1499
|
...(repositoryDisplay === undefined ? {} : { repositoryDisplay }),
|
|
@@ -1,151 +1,155 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared
|
|
3
|
-
* One path: unbound admission →
|
|
2
|
+
* Shared ticket identity seam for public court seats (#635 / #637 / #709 / #747).
|
|
3
|
+
* One path: unbound admission → reuse a ticket number this book already records
|
|
4
|
+
* (retained run pages + 起居录 volumes) → bindAdmittedTicketNumber.
|
|
5
|
+
* ADR 0081 `reuse-case-ticket-without-extra-llm`: no seat-side model call, no
|
|
6
|
+
* second ticket-number source of truth, no minting a number from human titles.
|
|
7
|
+
* Nothing matched is not 真无票 — the run simply stays unbound, which is lawful.
|
|
4
8
|
* No CLI --ticket and no attachment frontmatter binding.
|
|
5
|
-
* #
|
|
9
|
+
* #747: officer same-parent resume also lives here (shared seam).
|
|
6
10
|
*/
|
|
7
|
-
import {
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
|
|
8
13
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type DiaristTicketResolution,
|
|
14
|
-
} from "../diarist-ticket-resolution.ts";
|
|
15
|
-
import { resolveDiaristGithubOrigin } from "../diarist.ts";
|
|
14
|
+
ActivationGitRepositoryRequiredError,
|
|
15
|
+
resolveBookKeyFromGit,
|
|
16
|
+
} from "../activation-ledger-git.ts";
|
|
17
|
+
import { resolveTicketProvenanceVolume } from "../ticket-provenance.ts";
|
|
16
18
|
import {
|
|
17
19
|
bindAdmittedTicketNumber,
|
|
18
|
-
recordTrueUnboundTicketResolution,
|
|
19
20
|
type AdmittedRoleInvocation,
|
|
20
21
|
} from "./invocation.ts";
|
|
21
22
|
import {
|
|
23
|
+
collectBookRunTicketNumbers,
|
|
22
24
|
findLatestRunIdForSeatTicket,
|
|
23
25
|
type RoleRunRecord,
|
|
24
26
|
type SameTicketSummonsMaterials,
|
|
25
27
|
} from "./run-lifecycle.ts";
|
|
26
28
|
|
|
27
29
|
export type SeatTicketBindingEnv = {
|
|
28
|
-
readonly
|
|
30
|
+
readonly home: string;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* admit and settle the failure inside the post-admission controlled path —
|
|
36
|
-
* bare throw before admit would skip terminal settlement (失败诚实, no wash).
|
|
37
|
-
*/
|
|
38
|
-
export type InstructionTicketProbe =
|
|
39
|
-
| { readonly kind: "resolved"; readonly resolution: DiaristTicketResolution }
|
|
40
|
-
| { readonly kind: "failed"; readonly error: DiaristTicketResolutionError };
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Instruction → ticket resolution without admission (#635 / #637).
|
|
44
|
-
* Used to locate a prior same-ticket run before minting a new one.
|
|
45
|
-
* Throws on resolution failure — prefer probeInstructionTicket at seat entry.
|
|
34
|
+
* Complete decimal ticket tokens in an instruction.
|
|
35
|
+
* A digit run is one token: `82` inside `#582` is not 82, and a leading zero
|
|
36
|
+
* makes the run a different literal than the ticket number it would parse to.
|
|
46
37
|
*/
|
|
47
|
-
export
|
|
38
|
+
export function instructionTicketTokens(
|
|
48
39
|
instruction: string,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return resolveDiaristTicketFromInstruction({
|
|
59
|
-
instruction,
|
|
60
|
-
origin,
|
|
61
|
-
resolver,
|
|
62
|
-
checkExistence,
|
|
63
|
-
});
|
|
40
|
+
): readonly number[] {
|
|
41
|
+
const tokens = new Set<number>();
|
|
42
|
+
for (const run of instruction.match(/\d+/g) ?? []) {
|
|
43
|
+
if (run.startsWith("0")) continue;
|
|
44
|
+
const parsed = Number(run);
|
|
45
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) continue;
|
|
46
|
+
tokens.add(parsed);
|
|
47
|
+
}
|
|
48
|
+
return [...tokens];
|
|
64
49
|
}
|
|
65
50
|
|
|
66
51
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
52
|
+
* Ticket identity this summons reuses, or undefined when none is unambiguous.
|
|
53
|
+
* Known identities come only from records this book already holds: 起居录 volume
|
|
54
|
+
* partitions and ticket numbers on retained run pages. Zero or several known
|
|
55
|
+
* tokens in the instruction leave the run unbound — never guess, never fail.
|
|
69
56
|
*/
|
|
70
|
-
export async function
|
|
71
|
-
instruction: string
|
|
72
|
-
projectRoot: string
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
57
|
+
export async function resolveKnownTicketNumber(input: {
|
|
58
|
+
readonly instruction: string;
|
|
59
|
+
readonly projectRoot: string;
|
|
60
|
+
readonly home: string;
|
|
61
|
+
readonly bookKey?: string;
|
|
62
|
+
}): Promise<number | undefined> {
|
|
63
|
+
const tokens = instructionTicketTokens(input.instruction);
|
|
64
|
+
if (tokens.length === 0) return undefined;
|
|
65
|
+
const known: number[] = [];
|
|
66
|
+
let runTickets: ReadonlySet<number> | undefined;
|
|
67
|
+
for (const token of tokens) {
|
|
68
|
+
const volume = resolveTicketProvenanceVolume(
|
|
69
|
+
token,
|
|
70
|
+
input.projectRoot,
|
|
71
|
+
input.home,
|
|
80
72
|
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return { kind: "failed", error };
|
|
73
|
+
if (existsSync(volume.volumeDir)) {
|
|
74
|
+
known.push(token);
|
|
75
|
+
continue;
|
|
85
76
|
}
|
|
86
|
-
|
|
77
|
+
if (runTickets === undefined) {
|
|
78
|
+
runTickets = await readBookRunTickets(input);
|
|
79
|
+
}
|
|
80
|
+
if (runTickets.has(token)) known.push(token);
|
|
87
81
|
}
|
|
82
|
+
return known.length === 1 ? known[0] : undefined;
|
|
88
83
|
}
|
|
89
84
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Diarist first-summons identity (ADR 0081 `initial-court-ticket-supplied`).
|
|
87
|
+
* Prefer a book-known reuse; when the book has no record yet, the sole complete
|
|
88
|
+
* decimal token in the caller's dispatch is the supplied ticket — the working
|
|
89
|
+
* 起居郎 round establishes the volume. Several tokens or none stay unbound.
|
|
90
|
+
* Not a second ticket-number source of truth and not an extra recognizer call.
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveDiaristSummonsTicketNumber(input: {
|
|
93
|
+
readonly instruction: string;
|
|
94
|
+
readonly projectRoot: string;
|
|
95
|
+
readonly home: string;
|
|
96
|
+
readonly bookKey?: string;
|
|
97
|
+
}): Promise<number | undefined> {
|
|
98
|
+
const known = await resolveKnownTicketNumber(input);
|
|
99
|
+
if (known !== undefined) return known;
|
|
100
|
+
const tokens = instructionTicketTokens(input.instruction);
|
|
101
|
+
return tokens.length === 1 ? tokens[0] : undefined;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
105
|
+
* Ticket numbers on this book's retained runs.
|
|
106
|
+
* A directory with no book (not a git repository) simply holds no run history —
|
|
107
|
+
* admission owns that rejection face, this lookup does not pre-empt it.
|
|
102
108
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
async function readBookRunTickets(input: {
|
|
110
|
+
readonly projectRoot: string;
|
|
111
|
+
readonly home: string;
|
|
112
|
+
readonly bookKey?: string;
|
|
113
|
+
}): Promise<ReadonlySet<number>> {
|
|
114
|
+
let bookKey: string;
|
|
115
|
+
try {
|
|
116
|
+
bookKey = input.bookKey ?? resolveBookKeyFromGit(input.projectRoot);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error instanceof ActivationGitRepositoryRequiredError) return new Set();
|
|
119
|
+
throw error;
|
|
111
120
|
}
|
|
121
|
+
return await collectBookRunTicketNumbers({ home: input.home, bookKey });
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
* Failed probes rethrow — caller must be inside the controlled-failure boundary
|
|
117
|
-
* (post-admission beforeDispatch), never before admit/markRunning.
|
|
118
|
-
*/
|
|
119
|
-
export async function applyInstructionTicketProbe(
|
|
124
|
+
/** Bind a reused ticket number onto an admission that is still unbound. */
|
|
125
|
+
export async function bindReusedTicketNumber(
|
|
120
126
|
admitted: AdmittedRoleInvocation,
|
|
121
|
-
|
|
127
|
+
ticketNumber: number | undefined,
|
|
122
128
|
): Promise<void> {
|
|
123
|
-
if (
|
|
124
|
-
|
|
129
|
+
if (ticketNumber === undefined) return;
|
|
130
|
+
if (admitted.ticketNumber !== undefined) return;
|
|
131
|
+
await bindAdmittedTicketNumber(admitted, ticketNumber);
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
/**
|
|
128
|
-
*
|
|
129
|
-
* Already-
|
|
130
|
-
*
|
|
131
|
-
*
|
|
135
|
+
* Sole seat disposition for an unbound admission (#635 / #709).
|
|
136
|
+
* Already-bound admissions short-circuit — resume keeps the identity it has.
|
|
137
|
+
* Used by seats that do not pre-resolve for same-ticket resume; the seats that
|
|
138
|
+
* do (countersign / inspector / diarist) reuse the number they already resolved.
|
|
132
139
|
*/
|
|
133
140
|
export async function resolveSeatTicketBinding(
|
|
134
141
|
admitted: AdmittedRoleInvocation,
|
|
135
|
-
env: SeatTicketBindingEnv
|
|
136
|
-
): Promise<
|
|
137
|
-
if (admitted.ticketNumber !== undefined) return
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
);
|
|
147
|
-
await applyTicketResolution(admitted, resolution);
|
|
148
|
-
return resolution;
|
|
142
|
+
env: SeatTicketBindingEnv,
|
|
143
|
+
): Promise<number | undefined> {
|
|
144
|
+
if (admitted.ticketNumber !== undefined) return admitted.ticketNumber;
|
|
145
|
+
const ticketNumber = await resolveKnownTicketNumber({
|
|
146
|
+
instruction: admitted.instruction,
|
|
147
|
+
projectRoot: admitted.projectRoot,
|
|
148
|
+
home: env.home,
|
|
149
|
+
bookKey: admitted.bookKey,
|
|
150
|
+
});
|
|
151
|
+
await bindReusedTicketNumber(admitted, ticketNumber);
|
|
152
|
+
return ticketNumber;
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|