@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.
@@ -12,6 +12,14 @@ import { buildResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run
12
12
  import { CliUsageError } from "./cli-errors.js";
13
13
  import { buildInstructionTransportPrompt, freezeAttachmentsIntoRun, } from "./invocation.js";
14
14
  import { pathContainedIn } from "../activation-ledger-topology.js";
15
+ import { projectCaseDossierPointerSection } from "./case-dossier-delivery.js";
16
+ /** Append one system section to a continuation prompt, keeping its kind. */
17
+ function appendContinuationSection(continuation, section) {
18
+ const prompt = `${continuation.prompt}\n\n${section}`;
19
+ return continuation.kind === "initial"
20
+ ? { kind: "initial", prompt }
21
+ : { kind: "resume", prompt };
22
+ }
15
23
  import { projectHostTransitionPriorNative } from "../host-transition-prior-native.js";
16
24
  import { missingCredentialPreDispatchFailure, postRunMissingCredentialFailure, } from "./public-run-credentials.js";
17
25
  import { acquireRunWriterLease, clearCurrentCourt, clearTypedProviderHttpObservation, markRunResumable, markRunRunning, markRunTerminal, readCurrentCourt, recordCurrentCourt, renderResumeCommand, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
@@ -115,20 +123,17 @@ export async function dispatchPostAdmissionTurn(input) {
115
123
  const principalCoordinates = admitted.principal === undefined
116
124
  ? undefined
117
125
  : env.principalAuthority.decode(admitted.principal);
118
- let turnRequest;
126
+ let hostTransition;
119
127
  try {
120
128
  previousHost = await readInvocationHost(admitted.runDirectory);
121
- const hostTransition = previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
122
- ? await projectHostTransitionPriorNative({
123
- previousHost,
124
- liveHost,
125
- piSessionFile: principalCoordinates.sessionFile,
126
- })
127
- : undefined;
128
- turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
129
- if (hostTransition !== undefined) {
130
- turnRequest = { ...turnRequest, hostTransition };
131
- }
129
+ hostTransition =
130
+ previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
131
+ ? await projectHostTransitionPriorNative({
132
+ previousHost,
133
+ liveHost,
134
+ piSessionFile: principalCoordinates.sessionFile,
135
+ })
136
+ : undefined;
132
137
  }
133
138
  catch (error) {
134
139
  // prior-native IO is on the public one-shot path — controlled failure, not bare throw.
@@ -156,6 +161,27 @@ export async function dispatchPostAdmissionTurn(input) {
156
161
  }, adapters, env.principalAuthority, io));
157
162
  }
158
163
  }
164
+ // Turn request is assembled after beforeDispatch so this turn sees whatever it
165
+ // settled — the seat's ticket bind re-projection. Case dossier delivery
166
+ // (ADR 0081) rides here for every public entry: first call, same-ticket
167
+ // re-summons and manual resume alike. System refs append their own neutral
168
+ // section; caller frozen attachments and the seat's own prompt bytes are
169
+ // never rewritten.
170
+ let turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
171
+ if (hostTransition !== undefined) {
172
+ turnRequest = { ...turnRequest, hostTransition };
173
+ }
174
+ const dossierSection = await projectCaseDossierPointerSection({
175
+ ticketNumber: admitted.ticketNumber,
176
+ projectRoot: admitted.projectRoot,
177
+ home: env.home,
178
+ });
179
+ if (dossierSection !== undefined) {
180
+ turnRequest = {
181
+ ...turnRequest,
182
+ continuation: appendContinuationSection(turnRequest.continuation, dossierSection),
183
+ };
184
+ }
159
185
  let result;
160
186
  try {
161
187
  result = await env.roleTurnHost.executeTurn(turnRequest);
@@ -819,6 +819,30 @@ export async function findLatestRunIdForSeatTicket(input) {
819
819
  }
820
820
  return best;
821
821
  }
822
+ /**
823
+ * Ticket identities this book's retained runs already record (#709).
824
+ * Read-only reuse surface for seat ticket binding — no ticket number is minted here.
825
+ */
826
+ export async function collectBookRunTicketNumbers(input) {
827
+ const ledgerHome = resolveActivationLedgerHome(input.home);
828
+ const runsDir = join(activationBookDirectory(ledgerHome, input.bookKey), "runs");
829
+ let entries;
830
+ try {
831
+ entries = await readdir(runsDir);
832
+ }
833
+ catch (error) {
834
+ if (errorCodeOf(error) === "ENOENT")
835
+ return new Set();
836
+ throw error;
837
+ }
838
+ const known = new Set();
839
+ for (const entry of entries) {
840
+ const ticketNumber = await readRunTicketNumber(join(runsDir, entry));
841
+ if (ticketNumber !== undefined)
842
+ known.add(ticketNumber);
843
+ }
844
+ return known;
845
+ }
822
846
  /** Restore optional correlation + typed ticket identity from a durable admitted page. */
823
847
  function parsePersistedTicketIdentity(record) {
824
848
  const correlationId = typeof record.correlationId === "string" && record.correlationId.trim() !== ""
@@ -829,20 +853,15 @@ function parsePersistedTicketIdentity(record) {
829
853
  record.ticketNumber >= 1
830
854
  ? record.ticketNumber
831
855
  : undefined;
832
- const ticketResolution = record.ticketResolution === "true-unbound" ? "true-unbound" : undefined;
833
856
  return {
834
857
  ...(correlationId === undefined ? {} : { correlationId }),
835
858
  ...(ticketNumber === undefined ? {} : { ticketNumber }),
836
- ...(ticketResolution === undefined ? {} : { ticketResolution }),
837
859
  };
838
860
  }
839
861
  function restoredTicketFields(fields) {
840
862
  return {
841
863
  ...(fields.correlationId === undefined ? {} : { correlationId: fields.correlationId }),
842
864
  ...(fields.ticketNumber === undefined ? {} : { ticketNumber: fields.ticketNumber }),
843
- ...(fields.ticketResolution === undefined
844
- ? {}
845
- : { ticketResolution: fields.ticketResolution }),
846
865
  };
847
866
  }
848
867
  async function loadResumableRunRecord(home, runId, authority) {
@@ -880,7 +899,6 @@ async function loadResumableRunRecord(home, runId, authority) {
880
899
  let derived;
881
900
  let correlationId;
882
901
  let ticketNumber;
883
- let ticketResolution;
884
902
  let prNumber;
885
903
  let repository;
886
904
  let repositoryDisplay;
@@ -1016,7 +1034,6 @@ async function loadResumableRunRecord(home, runId, authority) {
1016
1034
  const fromAdmitted = parsePersistedTicketIdentity(record);
1017
1035
  correlationId = fromAdmitted.correlationId;
1018
1036
  ticketNumber = fromAdmitted.ticketNumber;
1019
- ticketResolution = fromAdmitted.ticketResolution;
1020
1037
  }
1021
1038
  }
1022
1039
  catch (error) {
@@ -1041,17 +1058,12 @@ async function loadResumableRunRecord(home, runId, authority) {
1041
1058
  : {}),
1042
1059
  };
1043
1060
  }
1044
- if (correlationId === undefined ||
1045
- ticketNumber === undefined ||
1046
- ticketResolution === undefined) {
1061
+ if (correlationId === undefined || ticketNumber === undefined) {
1047
1062
  const fromInvocation = parsePersistedTicketIdentity(rec);
1048
1063
  if (correlationId === undefined)
1049
1064
  correlationId = fromInvocation.correlationId;
1050
1065
  if (ticketNumber === undefined)
1051
1066
  ticketNumber = fromInvocation.ticketNumber;
1052
- if (ticketResolution === undefined) {
1053
- ticketResolution = fromInvocation.ticketResolution;
1054
- }
1055
1067
  }
1056
1068
  }
1057
1069
  }
@@ -1081,7 +1093,6 @@ async function loadResumableRunRecord(home, runId, authority) {
1081
1093
  ...(derived === undefined ? {} : { derived }),
1082
1094
  ...(correlationId === undefined ? {} : { correlationId }),
1083
1095
  ...(ticketNumber === undefined ? {} : { ticketNumber }),
1084
- ...(ticketResolution === undefined ? {} : { ticketResolution }),
1085
1096
  ...(prNumber === undefined ? {} : { prNumber }),
1086
1097
  ...(repository === undefined ? {} : { repository }),
1087
1098
  ...(repositoryDisplay === undefined ? {} : { repositoryDisplay }),
@@ -1,94 +1,117 @@
1
1
  /**
2
- * Shared LLM ticket binding for public court seats (#635).
3
- * One path: unbound admission → diarist instruction assertion → bindAdmittedTicketNumber.
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
- * #637: same-ticket prior-run lookup → resume decision also lives here (shared seam).
9
+ * #747: officer same-parent resume also lives here (shared seam).
6
10
  */
7
- import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
8
- import { createGhTicketExistenceChecker, createHermesDiaristTicketResolver, DiaristTicketResolutionError, resolveDiaristTicketFromInstruction, } from "../diarist-ticket-resolution.js";
9
- import { resolveDiaristGithubOrigin } from "../diarist.js";
10
- import { bindAdmittedTicketNumber, recordTrueUnboundTicketResolution, } from "./invocation.js";
11
- import { findLatestRunIdForSeatTicket, } from "./run-lifecycle.js";
11
+ import { existsSync } from "node:fs";
12
+ import { ActivationGitRepositoryRequiredError, resolveBookKeyFromGit, } from "../activation-ledger-git.js";
13
+ import { resolveTicketProvenanceVolume } from "../ticket-provenance.js";
14
+ import { bindAdmittedTicketNumber, } from "./invocation.js";
15
+ import { collectBookRunTicketNumbers, findLatestRunIdForSeatTicket, } from "./run-lifecycle.js";
12
16
  /**
13
- * Instruction → ticket resolution without admission (#635 / #637).
14
- * Used to locate a prior same-ticket run before minting a new one.
15
- * Throws on resolution failure — prefer probeInstructionTicket at seat entry.
17
+ * Complete decimal ticket tokens in an instruction.
18
+ * A digit run is one token: `82` inside `#582` is not 82, and a leading zero
19
+ * makes the run a different literal than the ticket number it would parse to.
16
20
  */
17
- export async function resolveInstructionTicket(instruction, projectRoot, env = {}) {
18
- const resolver = createHermesDiaristTicketResolver({
19
- ...(env.packageRoot === undefined ? {} : { packageRoot: env.packageRoot }),
20
- cwd: projectRoot,
21
- });
22
- const checkExistence = createGhTicketExistenceChecker();
23
- const origin = resolveDiaristGithubOrigin(projectRoot);
24
- return resolveDiaristTicketFromInstruction({
25
- instruction,
26
- origin,
27
- resolver,
28
- checkExistence,
29
- });
21
+ export function instructionTicketTokens(instruction) {
22
+ const tokens = new Set();
23
+ for (const run of instruction.match(/\d+/g) ?? []) {
24
+ if (run.startsWith("0"))
25
+ continue;
26
+ const parsed = Number(run);
27
+ if (!Number.isSafeInteger(parsed) || parsed < 1)
28
+ continue;
29
+ tokens.add(parsed);
30
+ }
31
+ return [...tokens];
30
32
  }
31
33
  /**
32
- * Pre-admit probe: resolution success or captured DiaristTicketResolutionError.
33
- * Other errors still propagate (launch/infrastructure outside this class).
34
+ * Ticket identity this summons reuses, or undefined when none is unambiguous.
35
+ * Known identities come only from records this book already holds: 起居录 volume
36
+ * partitions and ticket numbers on retained run pages. Zero or several known
37
+ * tokens in the instruction leave the run unbound — never guess, never fail.
34
38
  */
35
- export async function probeInstructionTicket(instruction, projectRoot, env = {}) {
36
- try {
37
- const resolution = await resolveInstructionTicket(instruction, projectRoot, env);
38
- return { kind: "resolved", resolution };
39
- }
40
- catch (error) {
41
- if (error instanceof DiaristTicketResolutionError) {
42
- return { kind: "failed", error };
39
+ export async function resolveKnownTicketNumber(input) {
40
+ const tokens = instructionTicketTokens(input.instruction);
41
+ if (tokens.length === 0)
42
+ return undefined;
43
+ const known = [];
44
+ let runTickets;
45
+ for (const token of tokens) {
46
+ const volume = resolveTicketProvenanceVolume(token, input.projectRoot, input.home);
47
+ if (existsSync(volume.volumeDir)) {
48
+ known.push(token);
49
+ continue;
43
50
  }
44
- throw error;
51
+ if (runTickets === undefined) {
52
+ runTickets = await readBookRunTickets(input);
53
+ }
54
+ if (runTickets.has(token))
55
+ known.push(token);
45
56
  }
57
+ return known.length === 1 ? known[0] : undefined;
46
58
  }
47
- /** Ticket number from a successful ticket probe; undefined for unbound/failed. */
48
- export function ticketNumberFromProbe(probe) {
49
- if (probe.kind !== "resolved")
50
- return undefined;
51
- if (probe.resolution.kind !== "ticket")
52
- return undefined;
53
- return probe.resolution.ticketNumber;
59
+ /**
60
+ * Diarist first-summons identity (ADR 0081 `initial-court-ticket-supplied`).
61
+ * Prefer a book-known reuse; when the book has no record yet, the sole complete
62
+ * decimal token in the caller's dispatch is the supplied ticket — the working
63
+ * 起居郎 round establishes the volume. Several tokens or none stay unbound.
64
+ * Not a second ticket-number source of truth and not an extra recognizer call.
65
+ */
66
+ export async function resolveDiaristSummonsTicketNumber(input) {
67
+ const known = await resolveKnownTicketNumber(input);
68
+ if (known !== undefined)
69
+ return known;
70
+ const tokens = instructionTicketTokens(input.instruction);
71
+ return tokens.length === 1 ? tokens[0] : undefined;
54
72
  }
55
73
  /**
56
- * Sole disposition of a resolved ticket onto an unbound admission (#635 / #637).
57
- * Countersign / inspector / resolveSeatTicketBinding share this — no parallel branches.
74
+ * Ticket numbers on this book's retained runs.
75
+ * A directory with no book (not a git repository) simply holds no run history —
76
+ * admission owns that rejection face, this lookup does not pre-empt it.
58
77
  */
59
- export async function applyTicketResolution(admitted, resolution) {
60
- if (resolution.kind === "ticket") {
61
- await bindAdmittedTicketNumber(admitted, resolution.ticketNumber);
78
+ async function readBookRunTickets(input) {
79
+ let bookKey;
80
+ try {
81
+ bookKey = input.bookKey ?? resolveBookKeyFromGit(input.projectRoot);
62
82
  }
63
- else {
64
- await recordTrueUnboundTicketResolution(admitted);
83
+ catch (error) {
84
+ if (error instanceof ActivationGitRepositoryRequiredError)
85
+ return new Set();
86
+ throw error;
65
87
  }
88
+ return await collectBookRunTicketNumbers({ home: input.home, bookKey });
66
89
  }
67
- /**
68
- * Apply a pre-admit probe onto an unbound admission.
69
- * Failed probes rethrow — caller must be inside the controlled-failure boundary
70
- * (post-admission beforeDispatch), never before admit/markRunning.
71
- */
72
- export async function applyInstructionTicketProbe(admitted, probe) {
73
- if (probe.kind === "failed")
74
- throw probe.error;
75
- await applyTicketResolution(admitted, probe.resolution);
90
+ /** Bind a reused ticket number onto an admission that is still unbound. */
91
+ export async function bindReusedTicketNumber(admitted, ticketNumber) {
92
+ if (ticketNumber === undefined)
93
+ return;
94
+ if (admitted.ticketNumber !== undefined)
95
+ return;
96
+ await bindAdmittedTicketNumber(admitted, ticketNumber);
76
97
  }
77
98
  /**
78
- * Bind ticketNumber onto an unbound admitted seat via LLM instruction recognition.
79
- * Already-settled admissions (ticketNumber or durable true-unbound) short-circuit —
80
- * no re-entry into hermes on auto-resume attempts or manual resume (#635).
81
- * true-unbound is persisted once; failed verification throws.
99
+ * Sole seat disposition for an unbound admission (#635 / #709).
100
+ * Already-bound admissions short-circuit — resume keeps the identity it has.
101
+ * Used by seats that do not pre-resolve for same-ticket resume; the seats that
102
+ * do (countersign / inspector / diarist) reuse the number they already resolved.
82
103
  */
83
- export async function resolveSeatTicketBinding(admitted, env = {}) {
104
+ export async function resolveSeatTicketBinding(admitted, env) {
84
105
  if (admitted.ticketNumber !== undefined)
85
- return undefined;
86
- if (admitted.ticketResolution === "true-unbound") {
87
- return { kind: "true-unbound" };
88
- }
89
- const resolution = await resolveInstructionTicket(admitted.instruction, admitted.projectRoot, env);
90
- await applyTicketResolution(admitted, resolution);
91
- return resolution;
106
+ return admitted.ticketNumber;
107
+ const ticketNumber = await resolveKnownTicketNumber({
108
+ instruction: admitted.instruction,
109
+ projectRoot: admitted.projectRoot,
110
+ home: env.home,
111
+ bookKey: admitted.bookKey,
112
+ });
113
+ await bindReusedTicketNumber(admitted, ticketNumber);
114
+ return ticketNumber;
92
115
  }
93
116
  /**
94
117
  * Sole same-seat → resume decision (#637 / #724 / #747).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3652",
3
+ "version": "0.1.3661",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 系统随案递送本票起居录的中立指针段(ADR 0081 `automatic-case-material`;
3
+ * 指针输入沿 ADR 0079 `summons-pointer-input`,不把卷宗正文塞进提示词)。
4
+ * 只读已有案卷:不刷新、不生成、不校验内容、不新增拒收或停工条件。
5
+ * 机器文本仅中立标识材料(ADR 0073),用途说明归角色材料所有。
6
+ */
7
+ import { stat } from "node:fs/promises";
8
+
9
+ import { resolveTicketProvenanceVolume } from "../ticket-provenance.ts";
10
+
11
+ /** Section heading of the system-delivered dossier pointer. */
12
+ export const CASE_DOSSIER_SECTION_HEADING = "## 本票起居录(系统随案提供)" as const;
13
+
14
+ /** Honest one-line state of one dossier file: present, absent, or unreadable. */
15
+ async function describeDossierFile(path: string): Promise<string> {
16
+ try {
17
+ const stats = await stat(path);
18
+ if (!stats.isFile()) return `不可读(非普通文件):${path}`;
19
+ return path;
20
+ } catch (error) {
21
+ const code = (error as NodeJS.ErrnoException).code;
22
+ if (code === "ENOENT") return `尚未生成:${path}`;
23
+ return `不可读(${code ?? "未知错误"}):${path}`;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Pointer section for a bound ticket's existing 起居录, or undefined when the
29
+ * run carries no ticket identity (unbound calls stay legal and get no dossier).
30
+ * A bound ticket whose volume is missing or unreadable is stated as such —
31
+ * the section never claims a dossier that is not there.
32
+ */
33
+ export async function projectCaseDossierPointerSection(input: {
34
+ readonly ticketNumber: number | undefined;
35
+ readonly projectRoot: string;
36
+ readonly home: string;
37
+ }): Promise<string | undefined> {
38
+ if (input.ticketNumber === undefined) return undefined;
39
+ const volume = resolveTicketProvenanceVolume(
40
+ input.ticketNumber,
41
+ input.projectRoot,
42
+ input.home,
43
+ );
44
+ return [
45
+ CASE_DOSSIER_SECTION_HEADING,
46
+ "",
47
+ `票号:#${input.ticketNumber}`,
48
+ `人读视图:${await describeDossierFile(volume.humanViewFile)}`,
49
+ `记录卷宗:${await describeDossierFile(volume.recordFile)}`,
50
+ ].join("\n");
51
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Public Countersign Role run: admit ticket materials → shared post-admission
3
3
  * coordinator → settle Terminal result (#572 / ADR 0074). #599: manual resume
4
- * continues the exact session. Unbound admission resolves its ticket via the
5
- * shared seat LLM bind (#635). Who runs 起居郎, and when, is the caller's
6
- * composition (ADR 0010 / ADR 0075 `no-call-rule`) — not this seat's business.
4
+ * continues the exact session. Unbound admission reuses a known ticket identity
5
+ * (#709). Who runs 起居郎, and when, is the caller's composition
6
+ * (ADR 0010 / ADR 0075 `no-call-rule`) — not this seat's business.
7
7
  */
8
8
  import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
9
9
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
@@ -15,9 +15,8 @@ import {
15
15
  type ParseCountersignArgvResult,
16
16
  } from "./invocation.ts";
17
17
  import {
18
- applyInstructionTicketProbe,
19
- probeInstructionTicket,
20
- ticketNumberFromProbe,
18
+ bindReusedTicketNumber,
19
+ resolveKnownTicketNumber,
21
20
  tryResumeSameTicketSeatRun,
22
21
  } from "./seat-ticket-binding.ts";
23
22
  import {
@@ -91,17 +90,15 @@ export async function runPublicCountersign(
91
90
  }
92
91
 
93
92
  // #637: same ticket → resume prior countersign run with this summons' materials.
94
- // Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
95
- // controlled failure (bare pre-admit throw skips terminal settlement).
93
+ // #709: identity is reused from records this book already holds — no seat model call.
96
94
  // No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
97
95
  const projectRoot = parsed.project ?? env.cwd;
98
- const ticketProbe = await probeInstructionTicket(
99
- parsed.instruction,
96
+ const reusedTicketNumber = await resolveKnownTicketNumber({
97
+ instruction: parsed.instruction,
100
98
  projectRoot,
101
- env,
102
- );
103
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
104
- if (probedTicketNumber !== undefined) {
99
+ home: env.home,
100
+ });
101
+ if (reusedTicketNumber !== undefined) {
105
102
  const summons: SameTicketSummonsMaterials = {
106
103
  instruction: parsed.instruction,
107
104
  instructionEmpty: parsed.instruction.trim() === "",
@@ -111,7 +108,7 @@ export async function runPublicCountersign(
111
108
  home: env.home,
112
109
  projectRoot,
113
110
  role: "countersign",
114
- ticketNumber: probedTicketNumber,
111
+ ticketNumber: reusedTicketNumber,
115
112
  freshSummons: env.freshSummons,
116
113
  summons,
117
114
  resume: (runId, materials) =>
@@ -178,8 +175,8 @@ export async function runPublicCountersign(
178
175
  request: turnRequest,
179
176
  adapters: countersignAdapters({
180
177
  beforeDispatch: async (admitted) => {
181
- // #635/#637: apply pre-admit probe inside controlled-failure boundary.
182
- await applyInstructionTicketProbe(admitted, ticketProbe);
178
+ // #635/#709: bind the reused identity inside the controlled-failure boundary.
179
+ await bindReusedTicketNumber(admitted, reusedTicketNumber);
183
180
  Object.assign(
184
181
  turnRequest,
185
182
  buildCountersignTurnRequest(admitted, turnProjection),
@@ -24,9 +24,8 @@ import {
24
24
  type ParseDiaristArgvResult,
25
25
  } from "./invocation.ts";
26
26
  import {
27
- applyInstructionTicketProbe,
28
- probeInstructionTicket,
29
- ticketNumberFromProbe,
27
+ bindReusedTicketNumber,
28
+ resolveDiaristSummonsTicketNumber,
30
29
  tryResumeSameTicketSeatRun,
31
30
  } from "./seat-ticket-binding.ts";
32
31
  import {
@@ -170,16 +169,15 @@ export async function runPublicDiarist(
170
169
  }
171
170
 
172
171
  // #637: same ticket → resume this seat's prior run with this summons' materials.
173
- // Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
174
- // controlled failure (bare pre-admit throw skips terminal settlement).
172
+ // #709 / ADR 0081: reuse book records, or take the caller's sole dispatch token
173
+ // on a first summons that has not yet minted a volume — no seat recognizer call.
175
174
  const projectRoot = parsed.project ?? env.cwd;
176
- const ticketProbe = await probeInstructionTicket(
177
- parsed.instruction,
175
+ const reusedTicketNumber = await resolveDiaristSummonsTicketNumber({
176
+ instruction: parsed.instruction,
178
177
  projectRoot,
179
- env,
180
- );
181
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
182
- if (probedTicketNumber !== undefined) {
178
+ home: env.home,
179
+ });
180
+ if (reusedTicketNumber !== undefined) {
183
181
  const summons: SameTicketSummonsMaterials = {
184
182
  instruction: parsed.instruction,
185
183
  instructionEmpty: parsed.instruction.trim() === "",
@@ -189,7 +187,7 @@ export async function runPublicDiarist(
189
187
  home: env.home,
190
188
  projectRoot,
191
189
  role: "diarist",
192
- ticketNumber: probedTicketNumber,
190
+ ticketNumber: reusedTicketNumber,
193
191
  freshSummons: env.freshSummons,
194
192
  summons,
195
193
  resume: (runId, materials) =>
@@ -256,7 +254,7 @@ export async function runPublicDiarist(
256
254
  request: turnRequest,
257
255
  adapters: diaristAdapters({
258
256
  beforeDispatch: async (admittedSeat) => {
259
- await applyInstructionTicketProbe(admittedSeat, ticketProbe);
257
+ await bindReusedTicketNumber(admittedSeat, reusedTicketNumber);
260
258
  const sourcesPath = await freezeDiaristSourceCatalog(admittedSeat, env);
261
259
  Object.assign(
262
260
  turnRequest,
@@ -9,8 +9,8 @@ import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contrac
9
9
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
10
10
  import { CliUsageError } from "./cli-errors.ts";
11
11
  import {
12
- applyInstructionTicketProbe,
13
- probeInstructionTicket,
12
+ bindReusedTicketNumber,
13
+ resolveKnownTicketNumber,
14
14
  tryResumeSameTicketSeatRun,
15
15
  } from "./seat-ticket-binding.ts";
16
16
  import {
@@ -88,15 +88,14 @@ export async function runPublicInspector(
88
88
  }
89
89
 
90
90
  // #747: same parent (卷宗指针) → resume prior inspector run with this summons' materials.
91
- // Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
92
- // controlled failure (bare pre-admit throw skips terminal settlement).
91
+ // #709: ticket identity is reused from records this book already holds — no seat model call.
93
92
  // No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
94
93
  const projectRoot = parsed.project ?? env.cwd;
95
- const ticketProbe = await probeInstructionTicket(
96
- parsed.instruction,
94
+ const reusedTicketNumber = await resolveKnownTicketNumber({
95
+ instruction: parsed.instruction,
97
96
  projectRoot,
98
- env,
99
- );
97
+ home: env.home,
98
+ });
100
99
  const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
101
100
  if (parentRunPath !== undefined) {
102
101
  const summons: SameTicketSummonsMaterials = {
@@ -171,8 +170,8 @@ export async function runPublicInspector(
171
170
  request: turnRequest,
172
171
  adapters: inspectorAdapters({
173
172
  beforeDispatch: async (admittedSeat) => {
174
- // #635/#637: apply pre-admit probe inside controlled-failure boundary.
175
- await applyInstructionTicketProbe(admittedSeat, ticketProbe);
173
+ // #635/#709: bind the reused identity inside the controlled-failure boundary.
174
+ await bindReusedTicketNumber(admittedSeat, reusedTicketNumber);
176
175
  },
177
176
  }),
178
177
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),