@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
|
@@ -5,15 +5,23 @@
|
|
|
5
5
|
* initial role facades before entering; manual resume never re-admits.
|
|
6
6
|
* Role runners supply only turn request projection and narrow settlement adapters.
|
|
7
7
|
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
8
9
|
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
buildResumeContinuationPrompt,
|
|
13
14
|
type PublicResumeRequest,
|
|
15
|
+
type SameTicketSummonsMaterials,
|
|
14
16
|
} from "./run-lifecycle.ts";
|
|
15
17
|
import { CliUsageError } from "./cli-errors.ts";
|
|
16
18
|
import type { RoleTurnRequestProjectionOptions } from "./turn-request.ts";
|
|
19
|
+
import {
|
|
20
|
+
buildInstructionTransportPrompt,
|
|
21
|
+
freezeAttachmentsIntoRun,
|
|
22
|
+
} from "./invocation.ts";
|
|
23
|
+
import { pathContainedIn } from "../activation-ledger-topology.ts";
|
|
24
|
+
import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
|
|
17
25
|
|
|
18
26
|
import type {
|
|
19
27
|
ControlledFailureCause,
|
|
@@ -26,11 +34,6 @@ import type {
|
|
|
26
34
|
SessionCustomEntryAppender,
|
|
27
35
|
} from "../host-contracts.ts";
|
|
28
36
|
import { projectHostTransitionPriorNative } from "../host-transition-prior-native.ts";
|
|
29
|
-
import {
|
|
30
|
-
isTicketSeatMemoryBound,
|
|
31
|
-
readTicketSeatMemoryLastHost,
|
|
32
|
-
writeTicketSeatMemoryLastHost,
|
|
33
|
-
} from "../ticket-seat-memory.ts";
|
|
34
37
|
import type { CredentialProviders, SeatModelConfig } from "./config.ts";
|
|
35
38
|
import {
|
|
36
39
|
missingCredentialPreDispatchFailure,
|
|
@@ -38,16 +41,22 @@ import {
|
|
|
38
41
|
} from "./public-run-credentials.ts";
|
|
39
42
|
import {
|
|
40
43
|
acquireRunWriterLease,
|
|
44
|
+
clearCurrentCourt,
|
|
41
45
|
clearTypedProviderHttpObservation,
|
|
42
46
|
markRunResumable,
|
|
43
47
|
markRunRunning,
|
|
44
48
|
markRunTerminal,
|
|
49
|
+
readCurrentCourt,
|
|
50
|
+
recordCurrentCourt,
|
|
45
51
|
renderResumeCommand,
|
|
46
52
|
RunWriterLeaseHeldError,
|
|
53
|
+
type CurrentCourtState,
|
|
47
54
|
type RunWriterLease,
|
|
48
55
|
type TypedProviderHttpObservation,
|
|
49
56
|
type WriterLeaseDiagnosticKind,
|
|
50
57
|
} from "./run-lifecycle.ts";
|
|
58
|
+
import { homeFromRunDirectory } from "../activation-ledger-topology.ts";
|
|
59
|
+
import { readSealedSubmission } from "../submission-ledger.ts";
|
|
51
60
|
import {
|
|
52
61
|
classifyPostAdmissionFailure,
|
|
53
62
|
controlledFailureInputFromResolution,
|
|
@@ -59,12 +68,10 @@ import {
|
|
|
59
68
|
isLawfulTypedTerminalOutcome,
|
|
60
69
|
presentFailureTerminal,
|
|
61
70
|
presentStructuralRejection,
|
|
62
|
-
projectThrownFailureLeaf,
|
|
63
71
|
resolveAuditedRunnerFailureResolution,
|
|
64
72
|
resolveControlledFailureResumeObservation,
|
|
65
73
|
settleFailureTerminalResult,
|
|
66
74
|
sealedAcceptanceRedispatchDisposition,
|
|
67
|
-
type ControlledFailure,
|
|
68
75
|
} from "./settlement.ts";
|
|
69
76
|
import type { CliIo } from "./cli-io.ts";
|
|
70
77
|
import type { AdmittedRoleInvocation } from "./invocation.ts";
|
|
@@ -113,7 +120,12 @@ export type PostAdmissionAdapters<
|
|
|
113
120
|
A extends AdmittedRoleInvocation = AdmittedRoleInvocation,
|
|
114
121
|
T extends TerminalResult = TerminalResult,
|
|
115
122
|
> = {
|
|
116
|
-
trySettle: (
|
|
123
|
+
trySettle: (
|
|
124
|
+
admitted: A,
|
|
125
|
+
authority: DurablePrincipalAuthority,
|
|
126
|
+
/** Current court turn scope (#637); omit for run-scoped sealed reads. */
|
|
127
|
+
scope?: { readonly courtAttemptId?: string },
|
|
128
|
+
) => Promise<T | undefined>;
|
|
117
129
|
/** Default: isLawfulTypedTerminalOutcome(terminal.roleOutcome). */
|
|
118
130
|
shouldPresentSettled?: (terminal: T) => boolean;
|
|
119
131
|
resolveRunnerKnownFailure?: (input: {
|
|
@@ -177,8 +189,7 @@ export async function presentControlledFailure<
|
|
|
177
189
|
admitted.principal !== undefined
|
|
178
190
|
? await inspectJudgeSession(authority.decode(admitted.principal).sessionFile)
|
|
179
191
|
: undefined;
|
|
180
|
-
// knownFailure channel owns details when present; otherwise caller knownDetails
|
|
181
|
-
// (e.g. last-host write concurrent leaf on timeout/activation dual failure).
|
|
192
|
+
// knownFailure channel owns details when present; otherwise caller knownDetails.
|
|
182
193
|
const fromKnownFailure =
|
|
183
194
|
explicitInternalKnownFailureClassificationInput(knownFailure);
|
|
184
195
|
const failure = classifyPostAdmissionFailure({
|
|
@@ -233,127 +244,6 @@ export async function presentControlledFailure<
|
|
|
233
244
|
};
|
|
234
245
|
}
|
|
235
246
|
|
|
236
|
-
/** Serialize one failure leaf for details.concurrentFailures (sole leaf owner = settlement). */
|
|
237
|
-
function concurrentFailureLeafRecord(leaf: ControlledFailure): {
|
|
238
|
-
readonly cause: ControlledFailureCause;
|
|
239
|
-
readonly diagnostic: string;
|
|
240
|
-
readonly identity?: { readonly name?: string; readonly code?: string | number };
|
|
241
|
-
readonly details?: Readonly<Record<string, unknown>>;
|
|
242
|
-
} {
|
|
243
|
-
return {
|
|
244
|
-
cause: leaf.cause,
|
|
245
|
-
diagnostic: leaf.diagnostic,
|
|
246
|
-
...(leaf.identity === undefined ? {} : { identity: leaf.identity }),
|
|
247
|
-
...(leaf.details === undefined ? {} : { details: leaf.details }),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function existingConcurrentFailureLeaves(
|
|
252
|
-
details: Readonly<Record<string, unknown>> | undefined,
|
|
253
|
-
): unknown[] {
|
|
254
|
-
const value = details?.concurrentFailures;
|
|
255
|
-
return Array.isArray(value) ? [...value] : [];
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Attach last-host write failure as concurrent secondary evidence on an existing
|
|
260
|
-
* returned-path ControlledFailureInput — append, never replace prior leaves.
|
|
261
|
-
*/
|
|
262
|
-
function withLastHostWriteConcurrentFailure(
|
|
263
|
-
input: ControlledFailureInput,
|
|
264
|
-
writeError: unknown,
|
|
265
|
-
): ControlledFailureInput {
|
|
266
|
-
const writeLeaf = concurrentFailureLeafRecord(projectThrownFailureLeaf(writeError));
|
|
267
|
-
if (input.knownFailure !== undefined) {
|
|
268
|
-
const failure = input.knownFailure;
|
|
269
|
-
return {
|
|
270
|
-
...input,
|
|
271
|
-
knownFailure: {
|
|
272
|
-
cause: failure.cause,
|
|
273
|
-
...(failure.identity === undefined ? {} : { identity: failure.identity }),
|
|
274
|
-
...(failure.diagnostic === undefined
|
|
275
|
-
? {}
|
|
276
|
-
: { diagnostic: failure.diagnostic }),
|
|
277
|
-
details: {
|
|
278
|
-
...(failure.details ?? {}),
|
|
279
|
-
concurrentFailures: [
|
|
280
|
-
...existingConcurrentFailureLeaves(failure.details),
|
|
281
|
-
writeLeaf,
|
|
282
|
-
],
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
return {
|
|
288
|
-
...input,
|
|
289
|
-
knownDetails: {
|
|
290
|
-
...(input.knownDetails ?? {}),
|
|
291
|
-
concurrentFailures: [
|
|
292
|
-
...existingConcurrentFailureLeaves(input.knownDetails),
|
|
293
|
-
writeLeaf,
|
|
294
|
-
],
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Settled failure roleOutcome already narrowed by kind at the sole call site. */
|
|
300
|
-
type SettledFailureOutcome = Extract<
|
|
301
|
-
TerminalResult["roleOutcome"],
|
|
302
|
-
{ kind: "failure" }
|
|
303
|
-
>;
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Rebuild the settled failure as knownFailure so concurrent last-host write can
|
|
307
|
-
* append without replacing cause/identity/diagnostic/details.
|
|
308
|
-
* Caller narrows by kind; no second runtime re-check.
|
|
309
|
-
*/
|
|
310
|
-
function knownFailureFromSettledFailureOutcome(
|
|
311
|
-
outcome: SettledFailureOutcome,
|
|
312
|
-
): RoleTurnKnownFailure {
|
|
313
|
-
const facts = outcome.decisiveFacts;
|
|
314
|
-
const secondary = facts.secondaryEvidence;
|
|
315
|
-
const details =
|
|
316
|
-
secondary !== undefined &&
|
|
317
|
-
typeof secondary === "object" &&
|
|
318
|
-
secondary !== null &&
|
|
319
|
-
!Array.isArray(secondary)
|
|
320
|
-
? (secondary as Readonly<Record<string, unknown>>)
|
|
321
|
-
: undefined;
|
|
322
|
-
const identity: { name?: string; code?: string | number } = {};
|
|
323
|
-
if (typeof facts.errorName === "string") identity.name = facts.errorName;
|
|
324
|
-
if (
|
|
325
|
-
typeof facts.errorCode === "string" ||
|
|
326
|
-
typeof facts.errorCode === "number"
|
|
327
|
-
) {
|
|
328
|
-
identity.code = facts.errorCode;
|
|
329
|
-
}
|
|
330
|
-
return {
|
|
331
|
-
cause: outcome.cause,
|
|
332
|
-
diagnostic: outcome.diagnostic,
|
|
333
|
-
...(Object.keys(identity).length > 0 ? { identity } : {}),
|
|
334
|
-
...(details === undefined ? {} : { details }),
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Presence envelope for a caught last-host write failure.
|
|
340
|
-
* Own-key presence is distinct from the caught value: `throw undefined` must stay a
|
|
341
|
-
* real failure fact (same rule as settlement thrown own-key), never a missing-write sentinel.
|
|
342
|
-
*/
|
|
343
|
-
type LastHostWriteFailure = { readonly error: unknown };
|
|
344
|
-
|
|
345
|
-
/** Keep primary throw identity; attach last-host write as a real AggregateError leaf. */
|
|
346
|
-
function withOptionalLastHostWriteThrow(
|
|
347
|
-
primary: unknown,
|
|
348
|
-
writeFailure: LastHostWriteFailure | undefined,
|
|
349
|
-
message: string,
|
|
350
|
-
): unknown {
|
|
351
|
-
if (writeFailure === undefined) return primary;
|
|
352
|
-
return new AggregateError([primary, writeFailure.error], message, {
|
|
353
|
-
cause: primary,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
247
|
export async function dispatchPostAdmissionTurn<
|
|
358
248
|
A extends AdmittedRoleInvocation,
|
|
359
249
|
T extends TerminalResult = TerminalResult,
|
|
@@ -389,54 +279,16 @@ export async function dispatchPostAdmissionTurn<
|
|
|
389
279
|
}
|
|
390
280
|
// #617 DK-4: capture previous invocation host before markRunRunning overwrites it.
|
|
391
281
|
// Single authority projectHostTransitionPriorNative owns known-host prior native paths.
|
|
392
|
-
//
|
|
393
|
-
// Open is not turn return/throw: production Grok notes the open fact at bind success;
|
|
394
|
-
// last-host records when the host returned a result OR when native home actually opened.
|
|
395
|
-
// Pre-open throws claim nothing; post-open throws retain the opened run.
|
|
396
|
-
// Side effects are gated by explicit ticket+seat binding — never by directory-outside-run guessing.
|
|
282
|
+
// Same-run resume (#637) keeps host identity on the run's invocation page.
|
|
397
283
|
let previousHost: string | undefined;
|
|
398
|
-
let previousRunDirectory: string | undefined;
|
|
399
|
-
let nativeHomeRunDirectory: string | undefined;
|
|
400
|
-
let openedNativeHomeRunDirectory: string | undefined;
|
|
401
284
|
const liveHost = env.host;
|
|
402
285
|
const principalCoordinates =
|
|
403
286
|
admitted.principal === undefined
|
|
404
287
|
? undefined
|
|
405
288
|
: env.principalAuthority.decode(admitted.principal);
|
|
406
|
-
const ticketSeatMemoryBound = isTicketSeatMemoryBound({
|
|
407
|
-
role: admitted.role,
|
|
408
|
-
...(admitted.ticketNumber === undefined ? {} : { ticketNumber: admitted.ticketNumber }),
|
|
409
|
-
});
|
|
410
289
|
let turnRequest: RoleTurnRequest;
|
|
411
290
|
try {
|
|
412
|
-
|
|
413
|
-
// Per-run invocation.host must not override: old-run retry after a cross-host
|
|
414
|
-
// intermediate would otherwise keep the stale run host and drop the transition.
|
|
415
|
-
// Non-ticket-seat paths still read the per-run invocation mark (#617).
|
|
416
|
-
if (ticketSeatMemoryBound && principalCoordinates !== undefined) {
|
|
417
|
-
const lastHost = await readTicketSeatMemoryLastHost(
|
|
418
|
-
principalCoordinates.sessionDirectory,
|
|
419
|
-
);
|
|
420
|
-
if (lastHost !== undefined) {
|
|
421
|
-
previousHost = lastHost.host;
|
|
422
|
-
// runDirectory on last-host is the established Grok native isolation run when present
|
|
423
|
-
// (preserved across non-Grok hosts so return-to-Grok can reopen it).
|
|
424
|
-
if (
|
|
425
|
-
typeof lastHost.runDirectory === "string" &&
|
|
426
|
-
lastHost.runDirectory.length > 0
|
|
427
|
-
) {
|
|
428
|
-
previousRunDirectory = lastHost.runDirectory;
|
|
429
|
-
if (
|
|
430
|
-
liveHost === "grok-build" &&
|
|
431
|
-
request.continuation.kind === "resume"
|
|
432
|
-
) {
|
|
433
|
-
nativeHomeRunDirectory = lastHost.runDirectory;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
} else {
|
|
438
|
-
previousHost = await readInvocationHost(admitted.runDirectory);
|
|
439
|
-
}
|
|
291
|
+
previousHost = await readInvocationHost(admitted.runDirectory);
|
|
440
292
|
const hostTransition =
|
|
441
293
|
previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
|
|
442
294
|
? await projectHostTransitionPriorNative({
|
|
@@ -444,27 +296,14 @@ export async function dispatchPostAdmissionTurn<
|
|
|
444
296
|
liveHost,
|
|
445
297
|
runDirectory: admitted.runDirectory,
|
|
446
298
|
piSessionFile: principalCoordinates.sessionFile,
|
|
447
|
-
...(previousRunDirectory === undefined ? {} : { previousRunDirectory }),
|
|
448
299
|
})
|
|
449
300
|
: undefined;
|
|
450
301
|
turnRequest = request;
|
|
451
302
|
if (hostTransition !== undefined) {
|
|
452
303
|
turnRequest = { ...turnRequest, hostTransition };
|
|
453
304
|
}
|
|
454
|
-
if (nativeHomeRunDirectory !== undefined) {
|
|
455
|
-
turnRequest = { ...turnRequest, nativeHomeRunDirectory };
|
|
456
|
-
}
|
|
457
|
-
// Capture the actual Grok open event from production isolation (bind success).
|
|
458
|
-
if (ticketSeatMemoryBound) {
|
|
459
|
-
turnRequest = {
|
|
460
|
-
...turnRequest,
|
|
461
|
-
noteNativeHomeOpened: (runDirectory) => {
|
|
462
|
-
openedNativeHomeRunDirectory = runDirectory;
|
|
463
|
-
},
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
305
|
} catch (error) {
|
|
467
|
-
//
|
|
306
|
+
// prior-native IO is on the public one-shot path — controlled failure, not bare throw.
|
|
468
307
|
return (await presentControlledFailure(
|
|
469
308
|
admitted,
|
|
470
309
|
{
|
|
@@ -502,66 +341,17 @@ export async function dispatchPostAdmissionTurn<
|
|
|
502
341
|
}
|
|
503
342
|
}
|
|
504
343
|
|
|
505
|
-
|
|
506
|
-
let turnOutcome:
|
|
507
|
-
| { readonly kind: "returned"; readonly result: RoleTurnResult }
|
|
508
|
-
| { readonly kind: "thrown"; readonly error: unknown };
|
|
344
|
+
let result: RoleTurnResult;
|
|
509
345
|
try {
|
|
510
|
-
|
|
511
|
-
kind: "returned",
|
|
512
|
-
result: await env.roleTurnHost.executeTurn(turnRequest),
|
|
513
|
-
};
|
|
346
|
+
result = await env.roleTurnHost.executeTurn(turnRequest);
|
|
514
347
|
} catch (error) {
|
|
515
|
-
turnOutcome = { kind: "thrown", error };
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// last-host authority: host returned a turn result OR native home actually opened.
|
|
519
|
-
// Grok open fact comes from production bind (noteNativeHomeOpened), not from return/throw.
|
|
520
|
-
// Mock hosts that return without opening still record via the returned path.
|
|
521
|
-
// Pre-open throws: neither → no claim. Post-open throws: open fact → retain.
|
|
522
|
-
// Spans returned failure / same-run retry / return-to-Grok / post-open body+cleanup throws.
|
|
523
|
-
// last-host write failure is pending secondary evidence only — never forks settlement.
|
|
524
|
-
// Thrown dual: AggregateError leaves. Returned dual: same shared settle/resolve chain,
|
|
525
|
-
// write leaf attached at the single present boundary (ADR 0080 single-settlement-disposition).
|
|
526
|
-
// Presence is the envelope, not the caught value — `throw undefined` stays a failure fact.
|
|
527
|
-
let lastHostWriteFailure: LastHostWriteFailure | undefined;
|
|
528
|
-
const hostEngaged =
|
|
529
|
-
turnOutcome.kind === "returned" || openedNativeHomeRunDirectory !== undefined;
|
|
530
|
-
if (
|
|
531
|
-
hostEngaged &&
|
|
532
|
-
liveHost !== undefined &&
|
|
533
|
-
principalCoordinates !== undefined &&
|
|
534
|
-
ticketSeatMemoryBound
|
|
535
|
-
) {
|
|
536
|
-
try {
|
|
537
|
-
await writeTicketSeatMemoryLastHost(
|
|
538
|
-
principalCoordinates.sessionDirectory,
|
|
539
|
-
liveHost,
|
|
540
|
-
liveHost === "grok-build"
|
|
541
|
-
? (openedNativeHomeRunDirectory ??
|
|
542
|
-
nativeHomeRunDirectory ??
|
|
543
|
-
admitted.runDirectory)
|
|
544
|
-
: previousRunDirectory,
|
|
545
|
-
);
|
|
546
|
-
} catch (error) {
|
|
547
|
-
lastHostWriteFailure = { error };
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if (turnOutcome.kind === "thrown") {
|
|
552
|
-
// Preserve original throw/cause (pre-open or post-open). last-host already
|
|
553
|
-
// retained open fact above when bind had succeeded; write fail stays concurrent.
|
|
554
348
|
return (await presentControlledFailure(
|
|
555
349
|
admitted,
|
|
556
350
|
{
|
|
557
351
|
timedOut: false,
|
|
558
352
|
code: null,
|
|
559
353
|
stderr: "",
|
|
560
|
-
thrown:
|
|
561
|
-
turnOutcome.error,
|
|
562
|
-
lastHostWriteFailure,
|
|
563
|
-
"host turn and ticket-seat last-host write failed",
|
|
564
|
-
),
|
|
354
|
+
thrown: error,
|
|
565
355
|
},
|
|
566
356
|
adapters,
|
|
567
357
|
env.principalAuthority,
|
|
@@ -569,8 +359,6 @@ export async function dispatchPostAdmissionTurn<
|
|
|
569
359
|
)) as { exitCode: number; admitted: A; terminal: T };
|
|
570
360
|
}
|
|
571
361
|
|
|
572
|
-
const result = turnOutcome.result;
|
|
573
|
-
|
|
574
362
|
try {
|
|
575
363
|
await writeFile(
|
|
576
364
|
join(admitted.runDirectory, "stderr.log"),
|
|
@@ -582,8 +370,14 @@ export async function dispatchPostAdmissionTurn<
|
|
|
582
370
|
}
|
|
583
371
|
|
|
584
372
|
let settled: T | undefined;
|
|
373
|
+
// Same-ticket re-summons carry courtAttemptId — settle only that attempt so a
|
|
374
|
+
// prior sealed pass cannot wash this turn's missing/escalated/failed result.
|
|
375
|
+
const courtScope =
|
|
376
|
+
request.courtAttemptId === undefined || request.courtAttemptId.length === 0
|
|
377
|
+
? undefined
|
|
378
|
+
: { courtAttemptId: request.courtAttemptId };
|
|
585
379
|
try {
|
|
586
|
-
settled = await adapters.trySettle(admitted, env.principalAuthority);
|
|
380
|
+
settled = await adapters.trySettle(admitted, env.principalAuthority, courtScope);
|
|
587
381
|
} catch (error) {
|
|
588
382
|
// Settle throw is a real failure fact — never swallow into undefined.
|
|
589
383
|
return (await presentControlledFailure(
|
|
@@ -592,11 +386,7 @@ export async function dispatchPostAdmissionTurn<
|
|
|
592
386
|
timedOut: false,
|
|
593
387
|
code: result.code,
|
|
594
388
|
stderr: result.stderr,
|
|
595
|
-
thrown:
|
|
596
|
-
error,
|
|
597
|
-
lastHostWriteFailure,
|
|
598
|
-
"settlement and ticket-seat last-host write failed",
|
|
599
|
-
),
|
|
389
|
+
thrown: error,
|
|
600
390
|
},
|
|
601
391
|
adapters,
|
|
602
392
|
env.principalAuthority,
|
|
@@ -604,44 +394,14 @@ export async function dispatchPostAdmissionTurn<
|
|
|
604
394
|
)) as { exitCode: number; admitted: A; terminal: T };
|
|
605
395
|
}
|
|
606
396
|
if (settled !== undefined && shouldPresent(settled)) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
{
|
|
615
|
-
timedOut: result.timedOut,
|
|
616
|
-
code: result.code,
|
|
617
|
-
stderr: result.stderr,
|
|
618
|
-
knownFailure: knownFailureFromSettledFailureOutcome(
|
|
619
|
-
settled.roleOutcome,
|
|
620
|
-
),
|
|
621
|
-
},
|
|
622
|
-
lastHostWriteFailure.error,
|
|
623
|
-
),
|
|
624
|
-
adapters,
|
|
625
|
-
env.principalAuthority,
|
|
626
|
-
io,
|
|
627
|
-
)) as { exitCode: number; admitted: A; terminal: T };
|
|
628
|
-
}
|
|
629
|
-
// No existing failure terminal — last-host write is the sole public failure.
|
|
630
|
-
// thrown key is always set so `throw undefined` stays own-key present.
|
|
631
|
-
return (await presentControlledFailure(
|
|
632
|
-
admitted,
|
|
633
|
-
{
|
|
634
|
-
timedOut: result.timedOut,
|
|
635
|
-
code: result.code,
|
|
636
|
-
stderr: result.stderr,
|
|
637
|
-
thrown: lastHostWriteFailure.error,
|
|
638
|
-
},
|
|
639
|
-
adapters,
|
|
640
|
-
env.principalAuthority,
|
|
641
|
-
io,
|
|
642
|
-
)) as { exitCode: number; admitted: A; terminal: T };
|
|
397
|
+
// This court sealed — drop open-court pointer so bare resume is run-scoped idempotent.
|
|
398
|
+
if (
|
|
399
|
+
settled.roleOutcome.kind === "accepted" &&
|
|
400
|
+
request.courtAttemptId !== undefined &&
|
|
401
|
+
request.courtAttemptId.length > 0
|
|
402
|
+
) {
|
|
403
|
+
await clearCurrentCourt(admitted.runDirectory, request.courtAttemptId);
|
|
643
404
|
}
|
|
644
|
-
// last-host recorded after host returned a turn result (shared failure/retry/return fact).
|
|
645
405
|
await markRunTerminal(admitted.runDirectory);
|
|
646
406
|
io.stdout(formatTerminalResult(settled));
|
|
647
407
|
return {
|
|
@@ -670,20 +430,14 @@ export async function dispatchPostAdmissionTurn<
|
|
|
670
430
|
credential: credentialFailure,
|
|
671
431
|
runDirectory: admitted.runDirectory,
|
|
672
432
|
});
|
|
673
|
-
const hostFailureInput: ControlledFailureInput = {
|
|
674
|
-
timedOut: result.timedOut,
|
|
675
|
-
code: result.code,
|
|
676
|
-
stderr: result.stderr,
|
|
677
|
-
...controlledFailureInputFromResolution(resolution),
|
|
678
|
-
};
|
|
679
433
|
return (await presentControlledFailure(
|
|
680
434
|
admitted,
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
:
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
435
|
+
{
|
|
436
|
+
timedOut: result.timedOut,
|
|
437
|
+
code: result.code,
|
|
438
|
+
stderr: result.stderr,
|
|
439
|
+
...controlledFailureInputFromResolution(resolution),
|
|
440
|
+
},
|
|
687
441
|
adapters,
|
|
688
442
|
env.principalAuthority,
|
|
689
443
|
io,
|
|
@@ -694,15 +448,63 @@ export async function dispatchPostAdmissionTurn<
|
|
|
694
448
|
}
|
|
695
449
|
|
|
696
450
|
/**
|
|
697
|
-
* Shared resume continuation projection (#471 / #600 / #633): seat-table
|
|
698
|
-
* model/engine/timeout axes, restored correlation, and
|
|
699
|
-
* envelope
|
|
451
|
+
* Shared resume continuation projection (#471 / #600 / #633 / #637): seat-table
|
|
452
|
+
* model/engine/timeout axes, restored correlation, and either
|
|
453
|
+
* - manual resume: package envelope / optional caller message (unchanged), or
|
|
454
|
+
* - same-ticket summons: this turn's instruction + frozen attachment paths.
|
|
455
|
+
* Caller message and summons materials each keep their place: a resume message
|
|
456
|
+
* keeps manual-resume prompt semantics while open-court frozen attachments still
|
|
457
|
+
* ride; summons instruction is only the prompt base when no caller message.
|
|
458
|
+
* Seats add only their activation projection. Call prepareSummonsResumeMaterials
|
|
459
|
+
* first when request.summons carries attachment paths.
|
|
700
460
|
*/
|
|
701
461
|
export function resumeTurnRequestProjectionOptions(
|
|
702
462
|
admitted: AdmittedRoleInvocation,
|
|
703
463
|
request: PublicResumeRequest,
|
|
704
464
|
env: PostAdmissionEnv,
|
|
465
|
+
summonsPrepared?: {
|
|
466
|
+
readonly instruction: string;
|
|
467
|
+
readonly instructionEmpty: boolean;
|
|
468
|
+
readonly attachments: readonly { frozenPath: string }[];
|
|
469
|
+
},
|
|
705
470
|
): RoleTurnRequestProjectionOptions {
|
|
471
|
+
const engineMaterial = engineSessionMaterialFromOptions({
|
|
472
|
+
...(env.engine === undefined ? {} : { engine: env.engine }),
|
|
473
|
+
packageRoot: env.packageRoot,
|
|
474
|
+
});
|
|
475
|
+
let prompt: string;
|
|
476
|
+
if (request.message !== undefined) {
|
|
477
|
+
// Manual resume caller-message semantics stay authoritative for the prompt:
|
|
478
|
+
// message present → base bytes unchanged (including blank/whitespace).
|
|
479
|
+
// Open-court frozen attachments still continue when present; attachment
|
|
480
|
+
// projection must not re-interpret the caller message as instructionEmpty.
|
|
481
|
+
if (
|
|
482
|
+
summonsPrepared !== undefined &&
|
|
483
|
+
summonsPrepared.attachments.length > 0
|
|
484
|
+
) {
|
|
485
|
+
prompt = buildInstructionTransportPrompt(
|
|
486
|
+
{
|
|
487
|
+
instruction: request.message,
|
|
488
|
+
instructionEmpty: false,
|
|
489
|
+
attachments: summonsPrepared.attachments,
|
|
490
|
+
},
|
|
491
|
+
engineMaterial,
|
|
492
|
+
);
|
|
493
|
+
} else {
|
|
494
|
+
prompt = buildResumeContinuationPrompt({
|
|
495
|
+
packageRoot: env.packageRoot,
|
|
496
|
+
...(env.engine === undefined ? {} : { engine: env.engine }),
|
|
497
|
+
message: request.message,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
} else if (summonsPrepared !== undefined) {
|
|
501
|
+
prompt = buildInstructionTransportPrompt(summonsPrepared, engineMaterial);
|
|
502
|
+
} else {
|
|
503
|
+
prompt = buildResumeContinuationPrompt({
|
|
504
|
+
packageRoot: env.packageRoot,
|
|
505
|
+
...(env.engine === undefined ? {} : { engine: env.engine }),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
706
508
|
return {
|
|
707
509
|
packageRoot: env.packageRoot,
|
|
708
510
|
home: env.home,
|
|
@@ -715,20 +517,67 @@ export function resumeTurnRequestProjectionOptions(
|
|
|
715
517
|
: { correlationId: admitted.correlationId ?? env.correlationId }),
|
|
716
518
|
continuation: {
|
|
717
519
|
kind: "resume",
|
|
718
|
-
prompt
|
|
719
|
-
packageRoot: env.packageRoot,
|
|
720
|
-
...(env.engine === undefined ? {} : { engine: env.engine }),
|
|
721
|
-
...(request.message === undefined ? {} : { message: request.message }),
|
|
722
|
-
}),
|
|
520
|
+
prompt,
|
|
723
521
|
},
|
|
724
522
|
};
|
|
725
523
|
}
|
|
726
524
|
|
|
525
|
+
function isAlreadyFrozenSummonsAttachment(
|
|
526
|
+
runDirectory: string,
|
|
527
|
+
attachmentPath: string,
|
|
528
|
+
): boolean {
|
|
529
|
+
const absolute = isAbsolute(attachmentPath)
|
|
530
|
+
? attachmentPath
|
|
531
|
+
: resolve(attachmentPath);
|
|
532
|
+
return pathContainedIn(join(runDirectory, "attachments"), absolute);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Freeze same-ticket summons attachments into the retained run directory (#637).
|
|
537
|
+
* No-op materials (no paths / instruction-only) skip the freeze.
|
|
538
|
+
* Paths already under this run's attachments/ are the accepted freeze identity —
|
|
539
|
+
* reuse them; do not re-freeze from external originals on bare resume.
|
|
540
|
+
* Manual resume never calls this — old attachment semantics stay intact.
|
|
541
|
+
*/
|
|
542
|
+
export async function prepareSummonsResumeMaterials(
|
|
543
|
+
runDirectory: string,
|
|
544
|
+
summons: SameTicketSummonsMaterials | undefined,
|
|
545
|
+
): Promise<
|
|
546
|
+
| {
|
|
547
|
+
readonly instruction: string;
|
|
548
|
+
readonly instructionEmpty: boolean;
|
|
549
|
+
readonly attachments: readonly { frozenPath: string }[];
|
|
550
|
+
}
|
|
551
|
+
| undefined
|
|
552
|
+
> {
|
|
553
|
+
if (summons === undefined) return undefined;
|
|
554
|
+
if (summons.instruction === undefined && (summons.attachmentPaths?.length ?? 0) === 0) {
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
const instruction = summons.instruction ?? "";
|
|
558
|
+
const instructionEmpty =
|
|
559
|
+
summons.instructionEmpty ?? instruction.trim() === "";
|
|
560
|
+
let attachments: readonly { frozenPath: string }[] = [];
|
|
561
|
+
if (summons.attachmentPaths !== undefined && summons.attachmentPaths.length > 0) {
|
|
562
|
+
const alreadyFrozen = summons.attachmentPaths.every((path) =>
|
|
563
|
+
isAlreadyFrozenSummonsAttachment(runDirectory, path),
|
|
564
|
+
);
|
|
565
|
+
attachments = alreadyFrozen
|
|
566
|
+
? summons.attachmentPaths.map((frozenPath) => ({ frozenPath }))
|
|
567
|
+
: await freezeAttachmentsIntoRun(summons.attachmentPaths, runDirectory);
|
|
568
|
+
}
|
|
569
|
+
return { instruction, instructionEmpty, attachments };
|
|
570
|
+
}
|
|
571
|
+
|
|
727
572
|
/**
|
|
728
573
|
* Shared manual-resume orchestration for seats whose continuation is the
|
|
729
574
|
* package resume envelope (#599 / #633): load → structural rejection → seat
|
|
730
575
|
* turn projection → runPostAdmissionManualResume. Seat-owned loader validation,
|
|
731
576
|
* turn builder, and adapters stay on the seat.
|
|
577
|
+
*
|
|
578
|
+
* Court open/recovery transaction (#637): under the existing writer lease,
|
|
579
|
+
* read currentCourt, judge seal, clear (bound to the judged court id), freeze,
|
|
580
|
+
* and record. No pre-lease clear or stale court-snapshot consumption.
|
|
732
581
|
*/
|
|
733
582
|
export async function runPostAdmissionSeatResume<
|
|
734
583
|
A extends AdmittedRoleInvocation,
|
|
@@ -737,14 +586,23 @@ export async function runPostAdmissionSeatResume<
|
|
|
737
586
|
request: PublicResumeRequest;
|
|
738
587
|
env: PostAdmissionEnv;
|
|
739
588
|
io: CliIo;
|
|
740
|
-
|
|
741
|
-
|
|
589
|
+
/** Load admitted state; receives the effective resume request (may carry rehydrated summons). */
|
|
590
|
+
load: (request: PublicResumeRequest) => Promise<{ admitted: A }>;
|
|
591
|
+
/** Build turn from admitted + effective request (summons ride existing projection). */
|
|
592
|
+
buildTurnRequest: (
|
|
593
|
+
admitted: A,
|
|
594
|
+
request: PublicResumeRequest,
|
|
595
|
+
) => RoleTurnRequest | Promise<RoleTurnRequest>;
|
|
742
596
|
adapters: PostAdmissionAdapters<A, T>;
|
|
743
597
|
effectiveEngine?: string;
|
|
744
598
|
}): Promise<{ exitCode: number; admitted?: A; terminal?: T }> {
|
|
599
|
+
let request = input.request;
|
|
600
|
+
|
|
601
|
+
// Load once for runDirectory / structural rejection; court identity is judged
|
|
602
|
+
// only after the writer lease is held (below).
|
|
745
603
|
let loaded;
|
|
746
604
|
try {
|
|
747
|
-
loaded = await input.load();
|
|
605
|
+
loaded = await input.load(request);
|
|
748
606
|
} catch (error) {
|
|
749
607
|
if (error instanceof CliUsageError) {
|
|
750
608
|
presentStructuralRejection(error, input.io);
|
|
@@ -752,14 +610,124 @@ export async function runPostAdmissionSeatResume<
|
|
|
752
610
|
}
|
|
753
611
|
throw error;
|
|
754
612
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
613
|
+
|
|
614
|
+
// Entire court recovery / open path runs after lease. forceContinuation skips
|
|
615
|
+
// the pre-lease sealed short-circuit; when the after-lease builder leaves no
|
|
616
|
+
// courtAttemptId, manual resume still presents run-scoped sealed idempotence
|
|
617
|
+
// under the same held lease.
|
|
618
|
+
try {
|
|
619
|
+
return await runPostAdmissionManualResume({
|
|
620
|
+
admitted: loaded.admitted,
|
|
621
|
+
env: input.env,
|
|
622
|
+
io: input.io,
|
|
623
|
+
adapters: input.adapters,
|
|
624
|
+
...(input.effectiveEngine === undefined
|
|
625
|
+
? {}
|
|
626
|
+
: { effectiveEngine: input.effectiveEngine }),
|
|
627
|
+
forceContinuation: true,
|
|
628
|
+
buildRequestAfterLease: async () => {
|
|
629
|
+
let openCourtAttemptId: string | undefined;
|
|
630
|
+
// Build uses the admitted judged under this lease (rehydrated when open
|
|
631
|
+
// court materials ride). Settlement identity stays on the outer admitted.
|
|
632
|
+
let admittedForBuild = loaded.admitted;
|
|
633
|
+
|
|
634
|
+
// Bare resume: read + seal-judge + bound clear only under the held lease.
|
|
635
|
+
if (request.summons === undefined) {
|
|
636
|
+
const openCourt = await readCurrentCourt(admittedForBuild.runDirectory);
|
|
637
|
+
if (openCourt !== undefined) {
|
|
638
|
+
const sealedForOpen = await readSealedSubmission(
|
|
639
|
+
admittedForBuild.projectRoot,
|
|
640
|
+
admittedForBuild.runId,
|
|
641
|
+
{
|
|
642
|
+
home: homeFromRunDirectory(admittedForBuild.runDirectory),
|
|
643
|
+
attemptId: openCourt.courtAttemptId,
|
|
644
|
+
},
|
|
645
|
+
);
|
|
646
|
+
if (sealedForOpen === undefined) {
|
|
647
|
+
// Continue open court: same summons materials + existing courtAttemptId.
|
|
648
|
+
// Caller message (if any) stays on the request — projection keeps it.
|
|
649
|
+
openCourtAttemptId = openCourt.courtAttemptId;
|
|
650
|
+
request = {
|
|
651
|
+
runId: request.runId,
|
|
652
|
+
...(request.message === undefined
|
|
653
|
+
? {}
|
|
654
|
+
: { message: request.message }),
|
|
655
|
+
...(openCourt.summons === undefined
|
|
656
|
+
? {}
|
|
657
|
+
: { summons: openCourt.summons }),
|
|
658
|
+
};
|
|
659
|
+
if (openCourt.summons !== undefined) {
|
|
660
|
+
const reloaded = await input.load(request);
|
|
661
|
+
admittedForBuild = reloaded.admitted;
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
// Open court already sealed — clear only the court id just judged.
|
|
665
|
+
await clearCurrentCourt(
|
|
666
|
+
admittedForBuild.runDirectory,
|
|
667
|
+
openCourt.courtAttemptId,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Freeze external paths once; rewrite summons to the frozen identity so
|
|
674
|
+
// currentCourt + later bare resume reuse the accepted snapshot.
|
|
675
|
+
if (request.summons !== undefined) {
|
|
676
|
+
const prepared = await prepareSummonsResumeMaterials(
|
|
677
|
+
admittedForBuild.runDirectory,
|
|
678
|
+
request.summons,
|
|
679
|
+
);
|
|
680
|
+
if (
|
|
681
|
+
prepared !== undefined &&
|
|
682
|
+
(request.summons.attachmentPaths?.length ?? 0) > 0
|
|
683
|
+
) {
|
|
684
|
+
request = {
|
|
685
|
+
...request,
|
|
686
|
+
summons: {
|
|
687
|
+
...request.summons,
|
|
688
|
+
attachmentPaths: prepared.attachments.map(
|
|
689
|
+
(attachment) => attachment.frozenPath,
|
|
690
|
+
),
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let turnRequest = await input.buildTurnRequest(admittedForBuild, request);
|
|
697
|
+
|
|
698
|
+
// Court path only when continuing an open court or opening a new summons court.
|
|
699
|
+
// Bare resume with no open court (or cleared sealed pointer) keeps no
|
|
700
|
+
// courtAttemptId so run-scoped sealed idempotence can present under lease.
|
|
701
|
+
if (openCourtAttemptId !== undefined || request.summons !== undefined) {
|
|
702
|
+
const courtAttemptId =
|
|
703
|
+
openCourtAttemptId ??
|
|
704
|
+
(turnRequest.courtAttemptId !== undefined &&
|
|
705
|
+
turnRequest.courtAttemptId.length > 0
|
|
706
|
+
? turnRequest.courtAttemptId
|
|
707
|
+
: randomUUID());
|
|
708
|
+
turnRequest = { ...turnRequest, courtAttemptId };
|
|
709
|
+
if (openCourtAttemptId === undefined) {
|
|
710
|
+
const court: CurrentCourtState = {
|
|
711
|
+
courtAttemptId,
|
|
712
|
+
...(request.summons === undefined
|
|
713
|
+
? {}
|
|
714
|
+
: { summons: request.summons }),
|
|
715
|
+
};
|
|
716
|
+
await recordCurrentCourt(admittedForBuild.runDirectory, court);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return turnRequest;
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
} catch (error) {
|
|
723
|
+
// Open-court rehydrate load under lease may still surface seat structural
|
|
724
|
+
// rejection (e.g. notary rejects caller message) — same exit face as pre-lease.
|
|
725
|
+
if (error instanceof CliUsageError) {
|
|
726
|
+
presentStructuralRejection(error, input.io);
|
|
727
|
+
return { exitCode: 2 };
|
|
728
|
+
}
|
|
729
|
+
throw error;
|
|
730
|
+
}
|
|
763
731
|
}
|
|
764
732
|
|
|
765
733
|
/**
|
|
@@ -858,37 +826,22 @@ export async function runPostAdmissionResumable<
|
|
|
858
826
|
}
|
|
859
827
|
|
|
860
828
|
/**
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
829
|
+
* Single authority for manual-resume run-scoped sealed-accepted presentation
|
|
830
|
+
* (#599 / #648 / #672 / #637). Used both before lease (eager request path) and
|
|
831
|
+
* after court-recovery builder under lease when no courtAttemptId remains.
|
|
832
|
+
* Returns undefined to fall through to dispatch; never rebuilds the gate.
|
|
865
833
|
*/
|
|
866
|
-
|
|
834
|
+
async function presentSealedAcceptedManualResumeIfAny<
|
|
867
835
|
A extends AdmittedRoleInvocation,
|
|
868
|
-
T extends TerminalResult
|
|
836
|
+
T extends TerminalResult,
|
|
869
837
|
>(input: {
|
|
870
838
|
admitted: A;
|
|
871
839
|
env: PostAdmissionEnv;
|
|
872
840
|
io: CliIo;
|
|
873
|
-
request: RoleTurnRequest;
|
|
874
841
|
adapters: PostAdmissionAdapters<A, T>;
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
exitCode: number;
|
|
879
|
-
admitted?: A;
|
|
880
|
-
terminal?: T;
|
|
881
|
-
staleWriterLeaseReclaimed?: true;
|
|
882
|
-
}> {
|
|
883
|
-
const { admitted, env, io, request, adapters, effectiveEngine } = input;
|
|
884
|
-
// #617 DK-3: manual resume writes the live seat/env model (same as new legs).
|
|
885
|
-
const effectiveModel = env.model;
|
|
886
|
-
const shouldPresent =
|
|
887
|
-
adapters.shouldPresentSettled ??
|
|
888
|
-
((terminal: T) => isLawfulTypedTerminalOutcome(terminal.roleOutcome));
|
|
889
|
-
|
|
890
|
-
// Sealed accepted receipt only — audit_escalation / residual failure must not
|
|
891
|
-
// short-circuit; those still need a real continuation turn.
|
|
842
|
+
shouldPresent: (terminal: T) => boolean;
|
|
843
|
+
}): Promise<{ exitCode: number; admitted: A; terminal?: T } | undefined> {
|
|
844
|
+
const { admitted, env, io, adapters, shouldPresent } = input;
|
|
892
845
|
try {
|
|
893
846
|
const existing = await adapters.trySettle(admitted, env.principalAuthority);
|
|
894
847
|
if (
|
|
@@ -905,9 +858,8 @@ export async function runPostAdmissionManualResume<
|
|
|
905
858
|
};
|
|
906
859
|
}
|
|
907
860
|
} catch (error) {
|
|
908
|
-
// Settlement-owned sealed disposition
|
|
909
|
-
//
|
|
910
|
-
// preserves true cause. Manual resume only presents — does not rebuild the gate.
|
|
861
|
+
// Settlement-owned sealed disposition: sealed accepted + publication/settle
|
|
862
|
+
// throw fail closed without redispatch; authority failure preserves cause.
|
|
911
863
|
const disposition = await sealedAcceptanceRedispatchDisposition(admitted);
|
|
912
864
|
if (disposition.kind === "block") {
|
|
913
865
|
return (await presentControlledFailure(
|
|
@@ -930,6 +882,77 @@ export async function runPostAdmissionManualResume<
|
|
|
930
882
|
// proof of seal; fall through to dispatch so the attempt path can settle
|
|
931
883
|
// or fail honestly.
|
|
932
884
|
}
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Shared post-admission manual resume path: acquire writer lease and dispatch turn.
|
|
890
|
+
* When the submission ledger is already sealed, project that accepted terminal
|
|
891
|
+
* idempotently — do not dispatch a doomed turn that would append
|
|
892
|
+
* post-seal-anomaly and erase the sealed read (#599; keep #416 open load).
|
|
893
|
+
* Same-ticket re-summons (#637) pass forceContinuation + courtAttemptId so a new
|
|
894
|
+
* court turn still runs with this summons' materials despite a prior sealed
|
|
895
|
+
* acceptance, while submission-ledger sole-final stays per-attempt.
|
|
896
|
+
*/
|
|
897
|
+
export async function runPostAdmissionManualResume<
|
|
898
|
+
A extends AdmittedRoleInvocation,
|
|
899
|
+
T extends TerminalResult = TerminalResult,
|
|
900
|
+
>(input: {
|
|
901
|
+
admitted: A;
|
|
902
|
+
env: PostAdmissionEnv;
|
|
903
|
+
io: CliIo;
|
|
904
|
+
/** Eager turn request (non-court path / seats that build before lease). */
|
|
905
|
+
request?: RoleTurnRequest;
|
|
906
|
+
/**
|
|
907
|
+
* Court-opening path (#637): build turn + freeze + record currentCourt only
|
|
908
|
+
* after the writer lease is held. Mutually exclusive with a prebuilt request
|
|
909
|
+
* when forceContinuation is set.
|
|
910
|
+
*/
|
|
911
|
+
buildRequestAfterLease?: () => Promise<RoleTurnRequest>;
|
|
912
|
+
adapters: PostAdmissionAdapters<A, T>;
|
|
913
|
+
/** Seat-table engine axis on resume (#600); written onto invocation.json when present. */
|
|
914
|
+
effectiveEngine?: string;
|
|
915
|
+
/** Same-ticket re-summons: skip sealed-accepted short-circuit and dispatch. */
|
|
916
|
+
forceContinuation?: boolean;
|
|
917
|
+
}): Promise<{
|
|
918
|
+
exitCode: number;
|
|
919
|
+
admitted?: A;
|
|
920
|
+
terminal?: T;
|
|
921
|
+
staleWriterLeaseReclaimed?: true;
|
|
922
|
+
}> {
|
|
923
|
+
const {
|
|
924
|
+
admitted,
|
|
925
|
+
env,
|
|
926
|
+
io,
|
|
927
|
+
adapters,
|
|
928
|
+
effectiveEngine,
|
|
929
|
+
forceContinuation,
|
|
930
|
+
buildRequestAfterLease,
|
|
931
|
+
} = input;
|
|
932
|
+
let request = input.request;
|
|
933
|
+
// #617 DK-3: manual resume writes the live seat/env model (same as new legs).
|
|
934
|
+
const effectiveModel = env.model;
|
|
935
|
+
const shouldPresent =
|
|
936
|
+
adapters.shouldPresentSettled ??
|
|
937
|
+
((terminal: T) => isLawfulTypedTerminalOutcome(terminal.roleOutcome));
|
|
938
|
+
const sealedIdempotenceInput = {
|
|
939
|
+
admitted,
|
|
940
|
+
env,
|
|
941
|
+
io,
|
|
942
|
+
adapters,
|
|
943
|
+
shouldPresent,
|
|
944
|
+
} as const;
|
|
945
|
+
|
|
946
|
+
// Sealed accepted receipt only — audit_escalation / residual failure must not
|
|
947
|
+
// short-circuit; those still need a real continuation turn.
|
|
948
|
+
// Same-ticket re-summons / court recovery forceContinuation skips the pre-lease
|
|
949
|
+
// face; post-builder reuses the same presenter when no courtAttemptId remains.
|
|
950
|
+
if (forceContinuation !== true && request !== undefined) {
|
|
951
|
+
const presented = await presentSealedAcceptedManualResumeIfAny(
|
|
952
|
+
sealedIdempotenceInput,
|
|
953
|
+
);
|
|
954
|
+
if (presented !== undefined) return presented;
|
|
955
|
+
}
|
|
933
956
|
|
|
934
957
|
let lease: RunWriterLease;
|
|
935
958
|
let staleWriterLeaseReclaimed: true | undefined;
|
|
@@ -957,24 +980,65 @@ export async function runPostAdmissionManualResume<
|
|
|
957
980
|
throw error;
|
|
958
981
|
}
|
|
959
982
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
983
|
+
// Court open/recovery under held lease until dispatch owns release (finally
|
|
984
|
+
// below). Builder, sealed presenter, and any throw on this seam must release
|
|
985
|
+
// here — dispatch's finally only runs after handoff.
|
|
986
|
+
let handedOffToDispatch = false;
|
|
987
|
+
try {
|
|
988
|
+
if (request === undefined) {
|
|
989
|
+
if (buildRequestAfterLease === undefined) {
|
|
990
|
+
throw new Error(
|
|
991
|
+
"runPostAdmissionManualResume requires request or buildRequestAfterLease",
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
request = await buildRequestAfterLease();
|
|
995
|
+
|
|
996
|
+
if (
|
|
997
|
+
request.courtAttemptId === undefined ||
|
|
998
|
+
request.courtAttemptId.length === 0
|
|
999
|
+
) {
|
|
1000
|
+
const presented = await presentSealedAcceptedManualResumeIfAny(
|
|
1001
|
+
sealedIdempotenceInput,
|
|
1002
|
+
);
|
|
1003
|
+
if (presented !== undefined) {
|
|
1004
|
+
return {
|
|
1005
|
+
...presented,
|
|
1006
|
+
...(staleWriterLeaseReclaimed === true
|
|
1007
|
+
? { staleWriterLeaseReclaimed: true as const }
|
|
1008
|
+
: {}),
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
handedOffToDispatch = true;
|
|
1015
|
+
const result = await dispatchPostAdmissionTurn({
|
|
1016
|
+
admitted,
|
|
1017
|
+
env: {
|
|
1018
|
+
...env,
|
|
1019
|
+
...(effectiveModel === undefined ? {} : { model: effectiveModel }),
|
|
1020
|
+
...(admitted.correlationId === undefined
|
|
1021
|
+
? {}
|
|
1022
|
+
: { correlationId: admitted.correlationId }),
|
|
1023
|
+
},
|
|
1024
|
+
io,
|
|
1025
|
+
request,
|
|
1026
|
+
lease,
|
|
1027
|
+
adapters,
|
|
1028
|
+
...(effectiveEngine === undefined ? {} : { effectiveEngine }),
|
|
1029
|
+
});
|
|
1030
|
+
if (result.terminal !== undefined) {
|
|
1031
|
+
(result.terminal as { autoResumeCount?: number }).autoResumeCount = 0;
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
...result,
|
|
1035
|
+
...(staleWriterLeaseReclaimed === true
|
|
1036
|
+
? { staleWriterLeaseReclaimed: true as const }
|
|
1037
|
+
: {}),
|
|
1038
|
+
};
|
|
1039
|
+
} finally {
|
|
1040
|
+
if (!handedOffToDispatch) {
|
|
1041
|
+
await lease.release();
|
|
1042
|
+
}
|
|
975
1043
|
}
|
|
976
|
-
return {
|
|
977
|
-
...result,
|
|
978
|
-
...(staleWriterLeaseReclaimed === true ? { staleWriterLeaseReclaimed: true as const } : {}),
|
|
979
|
-
};
|
|
980
1044
|
}
|