@akagilnc/pi-workflow-roles 0.1.4779 → 0.1.4800

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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +104 -215
  4. package/dist/headless-host/production-host.js +149 -229
  5. package/dist/migrate-book-topology.js +1 -1
  6. package/dist/pi/role-turn-host.js +1 -1
  7. package/dist/public-cli/auto-resume.js +9 -15
  8. package/dist/public-cli/countersign-run.js +75 -87
  9. package/dist/public-cli/diarist-run.js +6 -26
  10. package/dist/public-cli/inspector-run.js +1 -1
  11. package/dist/public-cli/main.js +64 -177
  12. package/dist/public-cli/notary-run.js +0 -3
  13. package/dist/public-cli/option-definitions.js +1 -1
  14. package/dist/public-cli/post-admission.js +35 -65
  15. package/dist/public-cli/reviewer-run.js +1 -1
  16. package/dist/public-cli/run-lifecycle.js +14 -46
  17. package/dist/public-cli/seat-ticket-binding.js +11 -25
  18. package/dist/public-role-summons.js +2 -0
  19. package/package.json +1 -1
  20. package/souls/countersign.md +3 -3
  21. package/souls/notary.md +4 -4
  22. package/souls/secretariat.md +16 -9
  23. package/souls/ticket-law.md +16 -4
  24. package/src/headless-host/role-turn-host.ts +49 -12
  25. package/src/pi/role-turn-host.ts +1 -1
  26. package/src/public-cli/auto-resume.ts +19 -18
  27. package/src/public-cli/countersign-run.ts +76 -129
  28. package/src/public-cli/diarist-run.ts +6 -33
  29. package/src/public-cli/inspector-run.ts +1 -1
  30. package/src/public-cli/invocation.ts +5 -0
  31. package/src/public-cli/notary-run.ts +3 -7
  32. package/src/public-cli/option-definitions.ts +1 -1
  33. package/src/public-cli/post-admission.ts +37 -69
  34. package/src/public-cli/reviewer-run.ts +1 -1
  35. package/src/public-cli/run-lifecycle.ts +18 -66
  36. package/src/public-cli/seat-ticket-binding.ts +12 -28
  37. package/src/public-role-summons.ts +15 -6
  38. package/src/role-runtime.ts +9 -0
@@ -554,7 +554,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
554
554
  args,
555
555
  cwd: request.cwd,
556
556
  env,
557
- stdin: applyMethodPrompt(prompt),
557
+ stdin: request.continuation.kind === "resume" ? prompt : applyMethodPrompt(prompt),
558
558
  ...(abortSignal === undefined ? {} : { signal: abortSignal }),
559
559
  ...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
560
560
  onStdoutLine(line) {
@@ -568,6 +568,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
568
568
  return;
569
569
  }
570
570
  // One bounded live seam owns both recording and host-specific reduction.
571
+ codexObserver?.observe(event);
571
572
  reportHostSessionEvent({
572
573
  host: config.hostName,
573
574
  cwd: request.cwd,
@@ -575,12 +576,22 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
575
576
  source: "headless-host",
576
577
  event,
577
578
  });
578
- codexObserver?.observe(event);
579
579
  },
580
580
  });
581
581
  } catch (error) {
582
582
  if (isHostAbortedError(error)) throw error;
583
583
  const message = error instanceof Error ? error.message : String(error);
584
+ const observedHostFailure = codexObserver?.result().failureDiagnostic;
585
+ if (observedHostFailure !== undefined) {
586
+ return {
587
+ status: "terminal",
588
+ result: failure("output", "HeadlessCliError", "codex-turn-failed", {
589
+ diagnostic: observedHostFailure,
590
+ sessionRecordDiagnostic: message,
591
+ sessionId,
592
+ }, observedHostFailure),
593
+ };
594
+ }
584
595
  // SitianInfrastructureError.knownCause is session; spawn errno stays activation.
585
596
  const isRecordFailure =
586
597
  typeof error === "object"
@@ -618,23 +629,34 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
618
629
 
619
630
  if (codex) {
620
631
  const observation = codexObserver!.result();
621
- if (observation.threadId === undefined || observation.threadId === "") {
622
- return terminalFromSpawned(spawned, {
623
- cause: "session",
624
- identity: { name: "HeadlessMissingThreadId", code: "missing-thread-id" },
625
- diagnostic: "codex exec emitted no thread.started thread_id",
626
- details: { sessionId, exitCode: spawned.code },
627
- });
632
+ // #987 result 6 / 失败诚实: host-reported failure wins over a package
633
+ // missing-thread-id label or package persistence failure.
634
+ if (observation.threadId !== undefined && observation.threadId !== "") {
635
+ sessionId = observation.threadId;
636
+ }
637
+ let sessionBindingDiagnostic: string | undefined;
638
+ const hostFailed = observation.failureDiagnostic !== undefined
639
+ || (spawned.code !== 0 && spawned.code !== null);
640
+ if (hostFailed && observation.threadId !== undefined && observation.threadId !== "") {
641
+ try {
642
+ await config.sessionIdentity.bind(request.principal, observation.threadId);
643
+ } catch (error) {
644
+ // The host failure remains primary; retain persistence failure as
645
+ // secondary typed evidence instead of replacing the terminal.
646
+ sessionBindingDiagnostic = error instanceof Error ? error.message : String(error);
647
+ }
628
648
  }
629
- sessionId = observation.threadId;
630
- await config.sessionIdentity.bind(request.principal, sessionId);
631
649
 
632
650
  if (observation.failureDiagnostic !== undefined) {
633
651
  return terminalFromSpawned(spawned, {
634
652
  cause: "output",
635
653
  identity: { name: "HeadlessCliError", code: "codex-turn-failed" },
636
654
  diagnostic: observation.failureDiagnostic,
637
- details: { sessionId, exitCode: spawned.code },
655
+ details: {
656
+ sessionId,
657
+ exitCode: spawned.code,
658
+ ...(sessionBindingDiagnostic === undefined ? {} : { sessionBindingDiagnostic }),
659
+ },
638
660
  });
639
661
  }
640
662
 
@@ -644,9 +666,24 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
644
666
  cause: "output",
645
667
  identity: { name: "HeadlessCliError", code: "codex-nonzero-exit" },
646
668
  diagnostic: spawned.stderr.trim() || `codex exec exited ${String(spawned.code)}`,
669
+ details: {
670
+ sessionId,
671
+ exitCode: spawned.code,
672
+ ...(sessionBindingDiagnostic === undefined ? {} : { sessionBindingDiagnostic }),
673
+ },
674
+ });
675
+ }
676
+
677
+ if (observation.threadId === undefined || observation.threadId === "") {
678
+ return terminalFromSpawned(spawned, {
679
+ cause: "session",
680
+ identity: { name: "HeadlessMissingThreadId", code: "missing-thread-id" },
681
+ diagnostic: "codex exec emitted no thread.started thread_id",
647
682
  details: { sessionId, exitCode: spawned.code },
648
683
  });
649
684
  }
685
+ sessionId = observation.threadId;
686
+ await config.sessionIdentity.bind(request.principal, sessionId);
650
687
 
651
688
  if (!observation.turnCompleted) {
652
689
  return terminalFromSpawned(spawned, {
@@ -164,7 +164,7 @@ function piUserDialogueBody(request: RoleTurnRequest): string {
164
164
  const _exhaustive: never = request.continuation;
165
165
  return _exhaustive;
166
166
  })();
167
- return request.continuation.kind === "resume" && request.activation.role === "reviewer"
167
+ return request.continuation.kind === "resume"
168
168
  ? rawPrompt
169
169
  : applyPiNativeSkillInvocation(request.methods, rawPrompt);
170
170
  }
@@ -37,7 +37,6 @@ import { isLawfulTypedTerminalOutcome, formatTerminalResult, type TerminalArtifa
37
37
  import {
38
38
  attachRecordedSubmissions,
39
39
  presentFailureTerminal,
40
- presentStructuralRejection,
41
40
  resolveControlledFailureResumeObservation,
42
41
  } from "./settlement.ts";
43
42
  import type { CliIo } from "./cli-io.ts";
@@ -481,7 +480,17 @@ export async function runWithAutoResumeLoop<
481
480
  signal?: AbortSignal;
482
481
  buildInitialPayload: () => TPayload;
483
482
  buildResumePayload: () => TPayload;
484
- dispatch: (payload: TPayload, lease: RunWriterLease, isFirst: boolean, attemptIo: CliIo) => Promise<T>;
483
+ /**
484
+ * #987: the initial turn retains the existing writer lease/liveness guard;
485
+ * actual resume attempts pass no lease so a live holder cannot pre-block the
486
+ * host CLI's own continuation contract.
487
+ */
488
+ dispatch: (
489
+ payload: TPayload,
490
+ lease: RunWriterLease | undefined,
491
+ isFirst: boolean,
492
+ attemptIo: CliIo,
493
+ ) => Promise<T>;
485
494
  }): Promise<T> {
486
495
  // #422 single-point resolution + domain validation. NaN would bypass every
487
496
  // `attempts >= limit` comparison (always false) — reject here, before any dispatch.
@@ -499,19 +508,6 @@ export async function runWithAutoResumeLoop<
499
508
  const retainedErrorFiles: string[] = [];
500
509
 
501
510
  while (true) {
502
- let lease: RunWriterLease;
503
- try {
504
- lease = await acquireRunWriterLease(options.admitted.runDirectory, (diagnostic) =>
505
- options.io.stderr(diagnostic),
506
- );
507
- } catch (error) {
508
- if (error instanceof RunWriterLeaseHeldError) {
509
- presentStructuralRejection(error, options.io);
510
- return { exitCode: 2 } as T;
511
- }
512
- throw error;
513
- }
514
-
515
511
  let result: T | undefined;
516
512
  // Set only when the caught throw is a TurnDispatchedFailure (#840 r9 判词
517
513
  // class 1): the host turn genuinely started this attempt even though
@@ -519,13 +515,18 @@ export async function runWithAutoResumeLoop<
519
515
  // not a replay of the initial one.
520
516
  let turnStartedBeforeThrow = false;
521
517
  try {
518
+ // #987: guard the initial new turn with the existing lease, but do not
519
+ // pre-block a real host CLI resume on a package-side live holder.
520
+ const lease = isFirst
521
+ ? await acquireRunWriterLease(options.admitted.runDirectory)
522
+ : undefined;
522
523
  result = await options.dispatch(currentPayload, lease, isFirst, dummyIo);
523
524
  } catch (error) {
524
525
  // Owner 2026-08-23: 「出了异常,就原地记录错误信息,然后重试。」
525
526
  // Retain the whole exception in place (per-attempt full file + dossier
526
527
  // pointer); recording failure must not break the retry path (PR #418
527
- // diagnostic-sink-isolation precedent). The dispatcher owns lease release
528
- // in its own finally, so the retry round starts with the lock free.
528
+ // diagnostic-sink-isolation precedent). When a lease is held inside
529
+ // dispatch, that path owns release in its own finally.
529
530
  lastThrownError = error;
530
531
  turnStartedBeforeThrow = error instanceof TurnDispatchedFailure;
531
532
  const attempt = dispatchOrdinal;
@@ -680,7 +681,7 @@ export async function runWithAutoResumeLoop<
680
681
  // throws and beforeDispatch failures retry the initial payload (#840 / #416).
681
682
  if (result?.turnDispatched === true || turnStartedBeforeThrow) {
682
683
  currentPayload = options.buildResumePayload();
684
+ isFirst = false;
683
685
  }
684
- isFirst = false;
685
686
  }
686
687
  }
@@ -2,25 +2,29 @@
2
2
  * Public Countersign Role run: admit ticket materials → court-pipeline prior
3
3
  * station (起居郎) → shared post-admission coordinator → settle Terminal result
4
4
  * (#572 / ADR 0074 / ADR 0075 / #742 / #771). #599: manual resume continues the
5
- * exact session. ADR 0079: same-ticket re-summons resume the seat's previous run;
6
- * explicit `ak-role new` mints fresh (显式派新腿入口与显式 resume 并列).
5
+ * exact session via explicit package runId. #987 Result 7: public entry no longer
6
+ * selects a prior run by ticket number; gate same-parent re-summons keep
7
+ * parentRunPath resume (#747 / ADR 0079 gate face). Explicit `ak-role new` mints
8
+ * fresh; explicit `ak-role resume <runId>` continues a named run.
7
9
  *
8
10
  * Court admission auto-runs 起居郎 so the 起居郎 LLM asserts the court target;
9
11
  * mechanical layer only verifies; countersign reuses that typed identity for bind
10
- * and same-ticket resume lookup (ADR 0075 / 0081 / 0079). Code never matches
11
- * instruction text against book-known numbers. Who may call 起居郎 and in what
12
- * order is not written into law (ADR 0075 不规定谁调用起居郎、顺序归调用者); the present admission
13
- * effect is what this seat currently does. 起居录 path delivery is owned once by
14
- * post-admission (#709 / ADR 0081).
12
+ * (ADR 0075 / 0081). Code never matches instruction text against book-known
13
+ * numbers. Who may call 起居郎 and in what order is not written into law
14
+ * (ADR 0075 不规定谁调用起居郎、顺序归调用者); the present admission effect is what this
15
+ * seat currently does. 起居录 path delivery is owned once by post-admission
16
+ * (#709 / ADR 0081). Court refresh may run on resume; it is not a resume
17
+ * precondition and does not rewrite or reject host resume (#987).
15
18
  *
16
- * Wiring (#771 / #863): resolve typed identity before materializing the admitted
17
- * run; same-ticket resume writes only to the retained run. 起居郎 escalate (认不出)
18
- * and typed failure terminals
19
+ * Wiring (#771 / #863 / #987): gate parentRunPath resume (when present) runs
20
+ * before identity mint; public path without parentRunPath always materializes a
21
+ * new run after identity. 起居郎 escalate (认不出) and typed failure terminals
19
22
  * (incl. verification failure) settle as countersign controlled failure — never
20
23
  * wash into 真无票. Only a true missing lawful typed terminal stays unbound and
21
24
  * continues the body (r5 unbound-continue). Bound refresh hands the typed key to
22
25
  * 起居郎 so freeze loads issue face (ADR 0075: 每次过庭都跑是调用者用法 / typed handoff).
23
26
  */
27
+ import { resolve } from "node:path";
24
28
  import type {
25
29
  DurablePrincipalAuthority,
26
30
  RoleTurnRequest,
@@ -38,8 +42,8 @@ import {
38
42
  bindAdmittedTicketNumber,
39
43
  bindCourtTicketNumbersOnAdmitted,
40
44
  buildCountersignTransportPrompt,
41
- freezePreparedAttachmentsIntoRun,
42
45
  materializeCountersignInvocation,
46
+ persistAdmittedSourceRunPath,
43
47
  relocateAdmittedRunToTicket,
44
48
  withPreparedAttachments,
45
49
  type AdmittedCountersignInvocation,
@@ -59,7 +63,6 @@ import {
59
63
  markRunAdmitted,
60
64
  type PublicResumeRequest,
61
65
  type RunWriterLease,
62
- type SameTicketSummonsMaterials,
63
66
  } from "./run-lifecycle.ts";
64
67
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts";
65
68
  import {
@@ -83,12 +86,6 @@ export type CountersignRunEnv = PostAdmissionEnv & {
83
86
  runCourtDiaristStation?: (
84
87
  admitted: AdmittedCountersignInvocation,
85
88
  ) => Promise<void>;
86
- /**
87
- * #871: typed co-review set from the identity 起居郎 turn, applied onto the
88
- * resumed run before bound refresh (same-ticket re-summons path). Whole-set
89
- * replace — never union. Production leaves unset outside that handoff.
90
- */
91
- pendingCourtTicketNumbers?: readonly number[];
92
89
  /**
93
90
  * #969 gate path: plain-language re-ask when prior 给事中 reply was not three-state.
94
91
  * Wins over gateReviewInstruction when both present (notary/inspector precedent).
@@ -100,11 +97,15 @@ export type CountersignRunEnv = PostAdmissionEnv & {
100
97
  */
101
98
  gateReviewInstruction?: string;
102
99
  /**
103
- * #969 gate path: parent run durable board ticket handoff. Same-ticket resume
104
- * key and identity 起居郎 bind under this key — never re-derived from receipt
105
- * ticketNumber (optional / omit-able payload field).
100
+ * #969 / #987 gate path: parent run durable board ticket handoff for bind only.
101
+ * Resume lookup uses parentRunPath — never this ticket number (#987 Result 7).
106
102
  */
107
103
  boundTicketNumber?: number;
104
+ /**
105
+ * #747 / #987 gate same-parent resume key (Secretariat source run directory).
106
+ * When set, re-summons resume the prior 给事中 under this parent before mint.
107
+ */
108
+ parentRunPath?: string;
108
109
  };
109
110
 
110
111
  /** Project admitted invocation onto the host-neutral turn request. */
@@ -474,6 +475,33 @@ export async function runPublicCountersign(
474
475
  }
475
476
 
476
477
  let admitted: AdmittedCountersignInvocation;
478
+ const gateParentRunPath =
479
+ typeof env.parentRunPath === "string" && env.parentRunPath.trim() !== ""
480
+ ? env.parentRunPath
481
+ : undefined;
482
+ if (gateParentRunPath !== undefined) {
483
+ const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction ?? parsed.instruction;
484
+ const resumed = await tryResumeSameTicketSeatRun({
485
+ home: env.home,
486
+ projectRoot: resolve(parsed.project ?? env.cwd),
487
+ role: "countersign",
488
+ parentRunPath: gateParentRunPath,
489
+ freshSummons: env.freshSummons,
490
+ summons: {
491
+ sourceRunPath: gateParentRunPath,
492
+ instruction: resumeInstruction,
493
+ instructionEmpty: resumeInstruction.trim() === "",
494
+ },
495
+ resume: (runId, materials) =>
496
+ runPublicCountersignResume(
497
+ { runId, ...(materials === undefined ? {} : { summons: materials }) },
498
+ env,
499
+ io,
500
+ ),
501
+ });
502
+ if (resumed !== undefined) return resumed;
503
+ }
504
+
477
505
  try {
478
506
  admitted = await admitCountersignInvocation({
479
507
  home: env.home,
@@ -514,13 +542,15 @@ export async function runPublicCountersign(
514
542
  });
515
543
  };
516
544
 
517
- // #637 / #771 / ADR 0079: ticket identity is the 起居郎 LLM typed assertion
518
- // (never mechanical matching of summons text). Resolve that typed key before
519
- // materializing a run: same-ticket re-summons write only to the retained run.
520
- // Controlled failures materialize below so they still have a durable page. The test
521
- // seam `runCourtDiaristStation` defers identity to beforeDispatch; generic
522
- // hook failures stay on the parent call-local budget, exhausted nested
523
- // station children still skip parent auto-resume (#840 父子不层叠).
545
+ // #747 / #987: gate same-parent resume before identity mint. Public entry
546
+ // without parentRunPath never selects a prior run by ticket number.
547
+ // #637 / #771: ticket identity is the 起居郎 LLM typed assertion (never
548
+ // mechanical matching of summons text). Resolve that typed key before
549
+ // materializing a first-mint run. Controlled failures materialize below so
550
+ // they still have a durable page. The test seam `runCourtDiaristStation`
551
+ // defers identity to beforeDispatch; generic hook failures stay on the
552
+ // parent call-local budget, exhausted nested station children still skip
553
+ // parent auto-resume (#840 父子不层叠).
524
554
  let typedTicket: number | undefined;
525
555
  let typedCourtTicketNumbers: readonly number[] | undefined;
526
556
  let identityDiaristRan = false;
@@ -528,15 +558,12 @@ export async function runPublicCountersign(
528
558
  if (env.runCourtDiaristStation === undefined) {
529
559
  let outcome: CourtDiaristInvocationResult;
530
560
  try {
531
- // The identity child cannot name the newly minted countersign id as parent:
532
- // same-ticket lookup may intentionally never materialize that run. A selected
533
- // retained run receives its normally correlated refresh child during resume.
534
561
  outcome = await invokeCourtDiarist(
535
562
  {
536
563
  instruction: parsed.instruction,
537
564
  projectRoot: admitted.projectRoot,
538
565
  failureLabel: "unbound summons",
539
- // #969: parent durable ticket is the resume/bind key (ADR 0079).
566
+ // #969: parent durable ticket is the bind key (not a resume lookup).
540
567
  ...(env.boundTicketNumber === undefined
541
568
  ? {}
542
569
  : { boundTicketNumber: env.boundTicketNumber }),
@@ -602,7 +629,7 @@ export async function runPublicCountersign(
602
629
  // failure above; bound refresh still fails honest via station.
603
630
  if (outcome.identity.kind === "ticket") {
604
631
  // #969: parent durable handoff wins over 起居郎 re-assert when both
605
- // present (receipt/assert mismatch → parent resume key).
632
+ // present (receipt/assert mismatch → parent bind key).
606
633
  typedTicket = isSafePositiveTicketNumber(env.boundTicketNumber)
607
634
  ? env.boundTicketNumber
608
635
  : outcome.identity.ticketNumber;
@@ -610,81 +637,22 @@ export async function runPublicCountersign(
610
637
  typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
611
638
  } else if (isSafePositiveTicketNumber(env.boundTicketNumber)) {
612
639
  // Parent board handoff present; 起居郎 returned true-unbound — still
613
- // resume/bind under the parent key (omitted receipt ticketNumber path).
640
+ // bind under the parent key (omitted receipt ticketNumber path).
614
641
  typedTicket = env.boundTicketNumber;
615
642
  }
616
- if (typedTicket !== undefined) {
617
- // #969/#879: gate re-ask / parent submission body share summons.instruction
618
- // (reask wins; argv instruction remains the 起居郎 identity face above).
619
- const resumeInstruction =
620
- env.reviewReask ?? env.gateReviewInstruction ?? parsed.instruction;
621
- const summons: SameTicketSummonsMaterials = {
622
- instruction: resumeInstruction,
623
- instructionEmpty: resumeInstruction.trim() === "",
624
- };
625
- const resumed = await tryResumeSameTicketSeatRun({
626
- home: env.home,
627
- projectRoot: admitted.projectRoot,
628
- role: "countersign",
629
- ticketNumber: typedTicket,
630
- freshSummons: env.freshSummons,
631
- summons,
632
- resume: async (runId, materials) => {
633
- // Consume the same pre-identity snapshot into the retained run; never
634
- // reopen caller paths after identity has run.
635
- const retained = await loadResumableCountersignRun(
636
- env.home,
637
- runId,
638
- env.principalAuthority,
639
- );
640
- if (retained.admitted === undefined) {
641
- throw new Error(
642
- `retained countersign run disappeared before resume: ${runId}`,
643
- );
644
- }
645
- const frozenPaths = (
646
- await freezePreparedAttachmentsIntoRun(
647
- preparedAttachments,
648
- retained.admitted.runDirectory,
649
- `s-${Date.now().toString(36)}`,
650
- )
651
- ).map((attachment) => attachment.frozenPath);
652
- const preparedMaterials: SameTicketSummonsMaterials = {
653
- ...(materials ?? {}),
654
- ...(frozenPaths.length === 0
655
- ? {}
656
- : { attachmentPaths: frozenPaths }),
657
- };
658
- // Identity 起居郎 asserted unbound (no issue face). Resume still runs
659
- // the bound refresh station under the typed key (ADR 0075: 每次过庭都跑是调用者用法).
660
- // #871: hand the identity set so resume can whole-replace the run fact
661
- // when this summons produced a new typed set (never union).
662
- return await runPublicCountersignResume(
663
- {
664
- runId,
665
- summons: preparedMaterials,
666
- },
667
- {
668
- ...env,
669
- ...(typedCourtTicketNumbers === undefined
670
- ? {}
671
- : { pendingCourtTicketNumbers: typedCourtTicketNumbers }),
672
- },
673
- io,
674
- );
675
- },
676
- });
677
- if (resumed !== undefined) {
678
- return resumed;
679
- }
680
- }
681
643
  }
682
644
 
683
- // No prior run was selected. Materialize this invocation now; true-unbound,
684
- // first-ticket and deferred test-seam paths all retain their own durable page.
645
+ // No prior same-parent run was selected. Materialize this invocation now;
646
+ // true-unbound, first-ticket and deferred test-seam paths all retain their
647
+ // own durable page. Public re-summons without parentRunPath always mint.
685
648
  await materializeAdmission();
686
649
  await markRunAdmitted(admitted, env.principalAuthority);
687
650
 
651
+ if (gateParentRunPath !== undefined) {
652
+ await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
653
+ admitted = { ...admitted, sourceRunPath: gateParentRunPath };
654
+ }
655
+
688
656
  if (identityDiaristRan && typedTicket !== undefined) {
689
657
  try {
690
658
  await bindAdmittedTicketNumber(admitted, typedTicket);
@@ -803,13 +771,12 @@ function countersignAdapters(options?: {
803
771
  }
804
772
 
805
773
  /**
806
- * Resume a previously admitted Countersign run (#599 / DK-3 / #637).
807
- * Restores role/ticket/session identity. Bound court re-entry runs the diarist
808
- * refresh station first (ADR 0075 每次过庭都跑); unbound skips refresh.
809
- * Same-ticket summons deliver this turn's instruction + frozen attachments on
810
- * the resume prompt; manual resume keeps package-envelope / caller-message
811
- * semantics and birth attachments. 起居录 path delivery remains post-admission's
812
- * single mount (#709).
774
+ * Resume a previously admitted Countersign run (#599 / DK-3 / #637 / #987).
775
+ * Restores role/ticket/session identity. Gate
776
+ * same-parent and explicit `ak-role resume <runId>` share this entry; summons
777
+ * may carry this turn's instruction. Manual resume forwards only the caller's
778
+ * message bytes. 起居录 path delivery remains
779
+ * post-admission's single mount (#709).
813
780
  */
814
781
  export async function runPublicCountersignResume(
815
782
  request: PublicResumeRequest,
@@ -845,27 +812,7 @@ export async function runPublicCountersignResume(
845
812
  ),
846
813
  );
847
814
  },
848
- adapters: countersignAdapters({
849
- beforeDispatch: async (admitted) => {
850
- // #871 B7: durable set damage already identified at load — settle as
851
- // station-child exhausted → presentControlledFailure (not structural exit 2).
852
- if (admitted.courtTicketNumbersDamage !== undefined) {
853
- throw new StationChildExhaustedError(
854
- admitted.courtTicketNumbersDamage,
855
- );
856
- }
857
- // #871: same-ticket re-summons may hand a fresh typed set from identity.
858
- // Whole-set replace onto this run fact; no new set → keep stored set.
859
- // Manual resume (no pending) keeps the durable set and never invents one.
860
- if (env.pendingCourtTicketNumbers !== undefined) {
861
- await bindCourtTicketNumbersOnAdmitted(
862
- admitted,
863
- env.pendingCourtTicketNumbers,
864
- );
865
- }
866
- await runCountersignCourtDiaristStation(admitted, env, io);
867
- },
868
- }),
815
+ adapters: countersignAdapters(),
869
816
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
870
817
  });
871
818
  }
@@ -30,9 +30,7 @@ import {
30
30
  markRunAdmitted,
31
31
  type PublicResumeRequest,
32
32
  type RunWriterLease,
33
- type SameTicketSummonsMaterials,
34
33
  } from "./run-lifecycle.ts";
35
- import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts";
36
34
  import {
37
35
  presentStructuralRejection,
38
36
  trySettleDiaristTerminalResult,
@@ -128,39 +126,14 @@ export async function runPublicDiarist(
128
126
  throw error;
129
127
  }
130
128
 
131
- // #637 / #771 / ADR 0075 / #779: ticket identity is the LLM's typed assertion
132
- // on this turn, OR a typed handoff key already held by the caller (countersign
133
- // refresh). Code never pre-judges the summons text and never freezes a
134
- // candidate catalog. First summons without handoff stays unbound until assert.
129
+ // #637 / #771 / ADR 0075 / #779 / #987: ticket identity is the LLM's typed
130
+ // assertion on this turn, OR a typed handoff key already held by the caller
131
+ // (countersign refresh bind). Code never pre-judges the summons text and never
132
+ // freezes a candidate catalog. Public same-ticket resume-by-ticketNumber was
133
+ // deleted (#987 Result 7); callers continue an existing diarist run only via
134
+ // explicit `ak-role resume <runId>`. Handoff ticket remains a bind key only.
135
135
 
136
- const projectRoot = parsed.project ?? env.cwd;
137
136
  const handoffTicket = env.boundTicketNumber;
138
- if (
139
- typeof handoffTicket === "number" &&
140
- Number.isSafeInteger(handoffTicket) &&
141
- handoffTicket >= 1
142
- ) {
143
- const summons: SameTicketSummonsMaterials = {
144
- instruction: parsed.instruction,
145
- instructionEmpty: parsed.instruction.trim() === "",
146
- attachmentPaths: parsed.attachmentPaths,
147
- };
148
- const resumed = await tryResumeSameTicketSeatRun({
149
- home: env.home,
150
- projectRoot,
151
- role: "diarist",
152
- ticketNumber: handoffTicket,
153
- freshSummons: env.freshSummons,
154
- summons,
155
- resume: (runId, materials) =>
156
- runPublicDiaristResume(
157
- { runId, ...(materials === undefined ? {} : { summons: materials }) },
158
- env,
159
- io,
160
- ),
161
- });
162
- if (resumed !== undefined) return resumed;
163
- }
164
137
 
165
138
  let admitted: AdmittedDiaristInvocation;
166
139
  try {
@@ -219,7 +219,7 @@ function inspectorAdapters(options?: {
219
219
  /**
220
220
  * Resume a previously admitted Inspector run (#633 / #637); the session principal reopens.
221
221
  * Same-ticket summons deliver this turn's instruction + frozen attachments; manual
222
- * resume keeps package-envelope / caller-message semantics and birth attachments.
222
+ * resume forwards only caller-supplied message bytes.
223
223
  */
224
224
  export async function runPublicInspectorResume(
225
225
  request: PublicResumeRequest,
@@ -153,6 +153,11 @@ export type AdmittedCountersignInvocation = AdmittedRoleInvocationBase & {
153
153
  * countersign controlled failure with this text — never structural exit 2 or main-only.
154
154
  */
155
155
  courtTicketNumbersDamage?: string;
156
+ /**
157
+ * Parent run directory (#747 / #987 gate same-parent resume). Persisted on first
158
+ * mint when gate supplies parentRunPath; independent of ticket-number lookup.
159
+ */
160
+ readonly sourceRunPath?: string;
156
161
  };
157
162
 
158
163
  export type AdmittedGleanerLeftInvocation = AdmittedRoleInvocationBase & {
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Public Notary Role run: admit source-run locator → shared post-admission coordinator
3
- * → settle Terminal result (#448 / #517). Zero caller prompt/attachment. Lifecycle is
4
- * the shared post-admission seam; this module keeps only Notary adapters.
3
+ * → settle Terminal result (#448 / #517). Explicit new has zero caller
4
+ * prompt/attachment; explicit resume accepts the shared optional caller message.
5
+ * Lifecycle is the shared post-admission seam; this module keeps only Notary adapters.
5
6
  * #637 / #747: same-parent (--source-run) re-summons resume the seat's previous run.
6
7
  */
7
8
  import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
@@ -231,11 +232,6 @@ export async function runPublicNotaryResume(
231
232
  env,
232
233
  io,
233
234
  load: async (effective) => {
234
- if (effective.message !== undefined) {
235
- throw new CliUsageError(
236
- "notary rejects caller prompt/instruction; only zero caller-prompt continuation admitted",
237
- );
238
- }
239
235
  const loaded = await loadResumableNotaryRun(
240
236
  env.home,
241
237
  effective.runId,
@@ -1379,7 +1379,7 @@ const SUPPORT_COMMAND_HELP = {
1379
1379
  resume: {
1380
1380
  command: "resume",
1381
1381
  summary:
1382
- "Resume a role run under the live seat table (model/host/engine); session principal must still exist. [message] applies only to seats that accept caller instruction; Notary/符宝郎 must omit message and derives evidence from the existing source-run/dossier binding. Global --model/--thinking/--host/--engine must be placed before <runId> (either before `resume` or between `resume` and <runId>); the one argv after <runId> is the opaque message, not a flag position (#471).",
1382
+ "Resume a role run under the live seat table (model/host/engine); session principal must still exist. Every callable seat, including Notary/符宝郎, accepts the optional caller message and passes it through unchanged as the continuation prompt. Notary explicit new still accepts only its source-run locator, not a caller prompt. Global --model/--thinking/--host/--engine must be placed before <runId> (either before `resume` or between `resume` and <runId>); the one argv after <runId> is the opaque message, not a flag position (#471).",
1383
1383
  usage: ["ak-role resume <runId> [message]"],
1384
1384
  examples: [
1385
1385
  "ak-role resume 01abc…",