@akagilnc/pi-workflow-roles 0.1.5216 → 0.1.5241

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.
@@ -45,8 +45,6 @@ import type {
45
45
  SessionCustomEntryAppender,
46
46
  } from "../host-contracts.ts";
47
47
  import { isOfficerReviewSeat } from "../packaged-role-registry.ts";
48
- import { deliverCaseDossierAsAttachment } from "./case-dossier-delivery.ts";
49
-
50
48
  /** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
51
49
  function describeCaughtError(error: unknown): { name?: string; message: string; code?: string | number } {
52
50
  if (error instanceof Error) {
@@ -58,8 +56,8 @@ function describeCaughtError(error: unknown): { name?: string; message: string;
58
56
 
59
57
  /**
60
58
  * Nested gate summons (station child) on an officer seat: dialogue content is
61
- * peer words only (#879). ADR 0081 case dossier still hangs via the existing
62
- * attachments freeze seam — never into the peer body, never RoleTurnRequest.materials.
59
+ * peer words only (#879). Never RoleTurnRequest.materials; #1092 drops code-side
60
+ * 起居录 path pointer freeze — roles locate records by ticket number.
63
61
  */
64
62
  function isStationChildOfficerDialogue(
65
63
  role: string,
@@ -69,10 +67,8 @@ function isStationChildOfficerDialogue(
69
67
  }
70
68
  import {
71
69
  mintEngineDetourInvocationScope,
72
- readInvocationSelectedHost,
73
70
  withEngineDetourInvocationScope,
74
71
  } from "../engine-detour-usage.ts";
75
- import { projectHostTransitionPriorNative } from "../host-transition-prior-native.ts";
76
72
  import type { CredentialProviders, SeatModelConfig } from "./config.ts";
77
73
  import {
78
74
  clearCurrentCourt,
@@ -92,6 +88,7 @@ import {
92
88
  type CatchableProcessSignal,
93
89
  } from "./process-cancel.ts";
94
90
  import { homeFromRunDirectory } from "../activation-ledger-topology.ts";
91
+ import { recordRunStart } from "../host-session-record.ts";
95
92
  import {
96
93
  attemptProducedFreshSubmission,
97
94
  classifyPostAdmissionFailure,
@@ -808,53 +805,11 @@ export async function dispatchPostAdmissionTurn<
808
805
  try {
809
806
  // #987: do not pre-block or classify host dispatch from AK credential
810
807
  // catalog facts. Per-invocation runner/host evidence owns provider identity.
811
- // #617 DK-4: capture previous invocation host before markRunRunning overwrites it.
812
- // Single authority projectHostTransitionPriorNative classifies the prior native volume.
813
- // Same-run resume (#637) keeps host identity on the run's invocation page.
814
- let previousHost: string | undefined;
815
- const liveHost = env.host;
816
- const principalCoordinates =
817
- admitted.principal === undefined
818
- ? undefined
819
- : env.principalAuthority.decode(admitted.principal);
820
- let hostTransition: RoleTurnRequest["hostTransition"];
821
- try {
822
- previousHost = readInvocationSelectedHost(admitted.runDirectory);
823
- hostTransition =
824
- previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
825
- ? await projectHostTransitionPriorNative({
826
- previousHost,
827
- liveHost,
828
- piSessionFile: principalCoordinates.sessionFile,
829
- })
830
- : undefined;
831
- } catch (error) {
832
- // prior-native IO is on the public one-shot path — controlled failure, not bare throw.
833
- return {
834
- ...(await presentControlledFailure(
835
- admitted,
836
- withEngineDetourInvocationScope({
837
- timedOut: false,
838
- code: null,
839
- stderr: "",
840
- thrown: error,
841
- }, request.invocationScopeId),
842
- adapters,
843
- env.principalAuthority,
844
- io,
845
- persistRunState,
846
- )) as { exitCode: number; admitted: A; terminal: T },
847
- ...deferredPersist,
848
- };
849
- }
850
-
851
808
  await clearTypedProviderHttpObservation(admitted.runDirectory);
852
809
  // The authoritative host write (markRunRunning) is delayed to just before
853
810
  // executeTurn — not merely past beforeDispatch (#840 r9 判词 class 2). Any
854
- // pre-turn retry (beforeDispatch, dossier projection, continuation
855
- // assembly) must still see the prior invocation host on its next attempt;
856
- // committing env.host earlier would make readInvocationSelectedHost read back the
857
- // new host on the retry and silently drop hostTransition.
811
+ // pre-turn retry (beforeDispatch, continuation assembly) must not commit a host
812
+ // identity until the turn is ready.
858
813
  // Failures settle the run (presentControlledFailure); they must not leave it
859
814
  // permanently running. Call-local auto-resume retries this hook until it
860
815
  // succeeds; only an exhausted nested station child skips the parent loop
@@ -888,10 +843,8 @@ export async function dispatchPostAdmissionTurn<
888
843
 
889
844
  // Turn request is assembled after beforeDispatch so this turn sees whatever it
890
845
  // settled — the seat's ticket bind re-projection and any court diarist station
891
- // writes (#742). Case dossier delivery (ADR 0081 / #709 / #858) rides here once
892
- // for every public entry on the existing attachments → readingMaterial face
893
- // (station-child and ordinary share one seam). Dialogue continuation stays
894
- // caller/peer opaque — never splice system path sections into user dialogue.
846
+ // writes (#742). Dialogue continuation stays caller/peer opaque. #1092: no
847
+ // code-side 起居录 path freeze or readingMaterial inject — roles read by ticket.
895
848
  // No package-resume parallel face or typed resume identity.
896
849
  // #990: one-shot / auto-resume may freeze principal+runDirectory before
897
850
  // relocate; always take durable identity from the live admitted object.
@@ -903,24 +856,11 @@ export async function dispatchPostAdmissionTurn<
903
856
  if (env.stationChild !== undefined) {
904
857
  turnRequest = { ...turnRequest, stationChild: env.stationChild };
905
858
  }
906
- if (hostTransition !== undefined) {
907
- turnRequest = { ...turnRequest, hostTransition };
908
- }
909
859
  // Selected host axis rides the shared Host envelope for in-turn tools
910
860
  // (detour usage ledger) — never a pre-spawn invocation.json reread.
911
- if (typeof liveHost === "string" && liveHost.trim() !== "") {
912
- turnRequest = { ...turnRequest, host: liveHost.trim() };
861
+ if (typeof env.host === "string" && env.host.trim() !== "") {
862
+ turnRequest = { ...turnRequest, host: env.host.trim() };
913
863
  }
914
- // 0081 non-dialogue face: freeze pointer section under run/attachments/.
915
- // Seat consumes via loadCaseDossierReadingMaterial → existing agent-start
916
- // readingMaterial / systemPrompt.materials fold. Caller instruction, empty
917
- // request, and resume --message stay verbatim.
918
- await deliverCaseDossierAsAttachment({
919
- ticketNumber: admitted.ticketNumber,
920
- projectRoot: admitted.projectRoot,
921
- home: env.home,
922
- runDirectory: admitted.runDirectory,
923
- });
924
864
 
925
865
  // Authoritative host write happens here, at the real dispatch boundary —
926
866
  // immediately before the turn actually starts, after every retryable
@@ -939,6 +879,7 @@ export async function dispatchPostAdmissionTurn<
939
879
 
940
880
  let result: RoleTurnResult;
941
881
  try {
882
+ recordRunStart(admitted.runDirectory);
942
883
  result = await env.roleTurnHost.executeTurn(turnRequest);
943
884
  } catch (error) {
944
885
  const processCancelName = processCancelSignalName(env.signal);
@@ -796,8 +796,7 @@ export async function summonGateOfficer(options: {
796
796
  // Hard path resolve: fail loud — never fall through to packageMachineHome (#604 / #675).
797
797
  home = homeFromRunDirectory(options.sourceRunDirectory);
798
798
  }
799
- // Officer host is seat-owned only (#821). Parent invocation.json.host stays a
800
- // hostTransition consumer in post-admission — not an officer override channel.
799
+ // Officer host is seat-owned only (#821), not a parent override channel.
801
800
  // #879: binding pointer = parent run directory; dialogue content = submission body.
802
801
  // Conclusion re-ask keeps sole ownership of reviewReask when present.
803
802
  let gateReviewInstruction: string | undefined;
@@ -677,8 +677,8 @@ export async function prepareRoleEnvelope(options: {
677
677
  return [value.systemPrompt];
678
678
  });
679
679
  const systemPromptBody = systemPromptParts.join("\n\n");
680
- // Typed reading materials from agent-start handlers (incl. single shared
681
- // case-dossier owner). Folded into provider-visible systemPrompt at send.
680
+ // Typed reading materials from agent-start handlers. Folded into
681
+ // provider-visible systemPrompt at send.
682
682
  const readingMaterials: unknown[] = [];
683
683
  for (const value of promptResults) {
684
684
  if (typeof value !== "object" || value === null) continue;
@@ -1256,20 +1256,6 @@ export function createRoleRuntimeExtension(
1256
1256
  },
1257
1257
  };
1258
1258
  });
1259
- // #879: single shared owner of station-child 0081 case-dossier → readingMaterial fold.
1260
- // post-admission freezes under run/attachments/case-dossier/; this handler alone
1261
- // projects it onto the existing agent-start materials face (Pi + envelope collect).
1262
- // Role modules must not re-read the freeze (ADR 0018 lifecycle; no duplicate fold).
1263
- roleHost.on("before_agent_start", async (_event, ctx) => {
1264
- const runDir = runDirectoryFromHostContext(ctx);
1265
- if (runDir === undefined) return;
1266
- const { loadCaseDossierReadingMaterial } = await import(
1267
- "./public-cli/case-dossier-delivery.ts"
1268
- );
1269
- const caseDossier = await loadCaseDossierReadingMaterial(runDir);
1270
- if (caseDossier === undefined) return;
1271
- return { readingMaterial: caseDossier };
1272
- });
1273
1259
  roleHost.on("tool_result", async (event) => {
1274
1260
  const role = selectedRole;
1275
1261
  if (role === undefined) return;
@@ -1,16 +1,10 @@
1
1
  /**
2
- * Sitian Appender kernel (ADR 0065).
2
+ * Sitian Appender kernel (ADR 0065 / ADR 0086).
3
3
  * Computes destination automatically from ledger topology without destination parameters.
4
- * Owns volume open, torn-tail recovery, entry-level idempotency, and commit boundary.
4
+ * Log4j-style append-only record sink: appends one row, no deduplication, no read-back, no torn-tail repair.
5
5
  */
6
- import { createHash, randomUUID } from "node:crypto";
7
- import {
8
- appendFileSync,
9
- existsSync,
10
- readFileSync,
11
- unlinkSync,
12
- writeFileSync,
13
- } from "node:fs";
6
+ import { randomUUID } from "node:crypto";
7
+ import { appendFileSync } from "node:fs";
14
8
  import { dirname, join } from "node:path";
15
9
 
16
10
  import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
@@ -29,59 +23,13 @@ import {
29
23
  type SitianRecordInput,
30
24
  } from "./sitian-contracts.ts";
31
25
 
32
- function isRecord(value: unknown): value is Record<string, unknown> {
33
- return typeof value === "object" && value !== null && !Array.isArray(value);
34
- }
35
-
36
- function errorCodeOf(error: unknown): unknown {
37
- return (error as { code?: unknown }).code;
38
- }
39
-
40
- function identityClaimPath(recordFile: string, identity: string): string {
41
- const digest = createHash("sha256").update(identity, "utf8").digest("hex");
42
- return `${recordFile}.id-${digest}`;
43
- }
44
-
45
- /**
46
- * Exclusive create at `path` exactly once across processes.
47
- * Reuses the package's existing `wx` / O_EXCL zero-content occupancy primitive
48
- * (same shape as activation-ledger-session / archivist-record-entry).
49
- */
50
- function createExclusiveFile(path: string, contents: string): void {
51
- writeFileSync(path, contents, { encoding: "utf8", flag: "wx" });
52
- }
53
-
54
- function sealTornTail(recordFile: string): void {
55
- if (!existsSync(recordFile)) return;
56
- const buffer = readFileSync(recordFile);
57
- if (buffer.length > 0 && buffer[buffer.length - 1] !== 0x0a) {
58
- appendFileSync(recordFile, "\n", "utf8");
59
- }
60
- }
61
-
62
26
  /** Assign already-recorded lines, preserving their original text and malformed lines. */
63
27
  export function appendSitianRecordBlock(input: SitianRecordInput, block: string): void {
64
28
  if (block === "") return;
65
29
  try {
66
30
  const { sessionDir, recordFile, ledgerHome } = resolveSitianRecordPath(input);
67
31
  ensureRealDirectoryTree(ledgerHome, sessionDir);
68
- appendFileSync(recordFile, "", "utf8");
69
- sealTornTail(recordFile);
70
- const identityOf = (line: string): string | undefined => {
71
- try {
72
- const row: unknown = JSON.parse(line);
73
- return isRecord(row) && typeof row.identity === "string" ? row.identity : undefined;
74
- } catch {
75
- return undefined;
76
- }
77
- };
78
- const identities = new Set(readFileSync(recordFile, "utf8").split("\n").map(identityOf).filter((id): id is string => id !== undefined));
79
- for (const line of block.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
80
- const identity = identityOf(line);
81
- if (identity !== undefined && identities.has(identity)) continue;
82
- appendFileSync(recordFile, line, "utf8");
83
- if (identity !== undefined) identities.add(identity);
84
- }
32
+ appendFileSync(recordFile, block, "utf8");
85
33
  } catch (error) {
86
34
  if (error instanceof SitianInfrastructureError) throw error;
87
35
  throw new SitianInfrastructureError(
@@ -91,134 +39,6 @@ export function appendSitianRecordBlock(input: SitianRecordInput, block: string)
91
39
  }
92
40
  }
93
41
 
94
- function findIdentityPointer(
95
- recordFile: string,
96
- identity: string,
97
- kind: string,
98
- level: SitianRecord["level"],
99
- ): RecordPointer | undefined {
100
- if (!existsSync(recordFile)) return undefined;
101
- const text = readFileSync(recordFile, "utf8");
102
- for (const line of text.split("\n")) {
103
- const trimmed = line.trim();
104
- if (!trimmed) continue;
105
- try {
106
- const parsed = JSON.parse(trimmed);
107
- if (isRecord(parsed) && parsed.identity === identity) {
108
- return { identity, recordFile, kind, level };
109
- }
110
- } catch {
111
- // Malformed lines (including substate b preserved bad lines) are ignored during self-check
112
- }
113
- }
114
- return undefined;
115
- }
116
-
117
- function unlinkAbsentOk(path: string): Error | undefined {
118
- try {
119
- unlinkSync(path);
120
- return undefined;
121
- } catch (error) {
122
- if (errorCodeOf(error) === "ENOENT") return undefined;
123
- return error instanceof Error ? error : new Error(errorText(error));
124
- }
125
- }
126
-
127
- /**
128
- * Same-identity uniqueness under normal concurrent writers.
129
- *
130
- * Exclusive per-identity claim (`wx`) makes check+append atomic for one
131
- * identity. The winner appends under the claim and always releases it on both
132
- * normal and throwing exits. A loser that already sees the published row
133
- * returns that pointer; a loser that hits claim EEXIST without a published row
134
- * fails as SitianInfrastructureError (knownCause session). Crash residue can
135
- * leave that one identity blocked — identity-scoped, never a volume lock. No
136
- * wait loop, write-ahead recovery, PID reclaim, or compare-and-unlink.
137
- */
138
- function appendWithIdentityClaim(
139
- recordFile: string,
140
- record: SitianRecord,
141
- row: string,
142
- ): RecordPointer {
143
- sealTornTail(recordFile);
144
- const claimPath = identityClaimPath(recordFile, record.identity);
145
- const existing = findIdentityPointer(
146
- recordFile,
147
- record.identity,
148
- record.kind,
149
- record.level,
150
- );
151
- if (existing !== undefined) {
152
- // Best-effort: drop leftover claim after a prior crash-after-append.
153
- unlinkAbsentOk(claimPath);
154
- return existing;
155
- }
156
-
157
- try {
158
- createExclusiveFile(claimPath, "");
159
- } catch (error) {
160
- if (errorCodeOf(error) !== "EEXIST") throw error;
161
- sealTornTail(recordFile);
162
- const published = findIdentityPointer(
163
- recordFile,
164
- record.identity,
165
- record.kind,
166
- record.level,
167
- );
168
- if (published !== undefined) {
169
- unlinkAbsentOk(claimPath);
170
- return published;
171
- }
172
- throw new SitianInfrastructureError(
173
- `Sitian identity claim at ${claimPath} already exists for identity ${record.identity}`,
174
- { cause: error },
175
- );
176
- }
177
-
178
- let primaryFailure: unknown;
179
- let result: RecordPointer | undefined;
180
- let cleanupFailure: Error | undefined;
181
- try {
182
- sealTornTail(recordFile);
183
- const raced = findIdentityPointer(
184
- recordFile,
185
- record.identity,
186
- record.kind,
187
- record.level,
188
- );
189
- if (raced !== undefined) {
190
- result = raced;
191
- } else {
192
- appendFileSync(recordFile, row, "utf8");
193
- result = {
194
- identity: record.identity,
195
- recordFile,
196
- kind: record.kind,
197
- level: record.level,
198
- };
199
- }
200
- } catch (error) {
201
- primaryFailure = error;
202
- } finally {
203
- cleanupFailure = unlinkAbsentOk(claimPath);
204
- }
205
-
206
- if (primaryFailure !== undefined) {
207
- if (primaryFailure instanceof SitianInfrastructureError) throw primaryFailure;
208
- throw new SitianInfrastructureError(
209
- `Sitian appender persistence failure: ${errorText(primaryFailure)}`,
210
- { cause: primaryFailure },
211
- );
212
- }
213
- if (cleanupFailure !== undefined) {
214
- throw new SitianInfrastructureError(
215
- `Sitian identity claim at ${claimPath} could not be released after append for identity ${record.identity}: ${errorText(cleanupFailure)}`,
216
- { cause: cleanupFailure },
217
- );
218
- }
219
- return result as RecordPointer;
220
- }
221
-
222
42
  /** Authorized S4 submission ledger kinds that share a common run submission volume. */
223
43
  export const S4_SUBMISSION_LEDGER_KINDS = new Set([
224
44
  "candidate",
@@ -307,20 +127,6 @@ export function resolveSitianRecordPathInLedger(
307
127
  return { sessionDir, recordFile, ledgerHome };
308
128
  }
309
129
 
310
- /**
311
- * Owner-visible ticket 起居录 path shape via the writer ticket joins
312
- * (ticketProvenanceUnderBookPaths). Ledger leaf is the package-owned `.ak-roles`
313
- * name (ADR 0048); variable slots keep owner labels; no fake-home path reverse.
314
- */
315
- export function projectTicketRecordsPathShape(): string {
316
- const { recordFile } = ticketProvenanceUnderBookPaths(
317
- join("~", ".ak-roles"),
318
- "<簿>",
319
- "<票号>",
320
- );
321
- return recordFile.replace(/\\/g, "/");
322
- }
323
-
324
130
  /** Compute a write destination from ambient ledger topology (ADR 0065). */
325
131
  export function resolveSitianRecordPath(input: SitianRecordInput): SitianRecordPath {
326
132
  const ledgerHome =
@@ -331,11 +137,10 @@ export function resolveSitianRecordPath(input: SitianRecordInput): SitianRecordP
331
137
  }
332
138
 
333
139
  /**
334
- * Appends a canonical record to its self-computed volume under the Sitian contract.
335
- * - Idempotency: checks volume by deterministic canonical identity; returns existing pointer on hit.
336
- * - Torn-tail recovery: checks file tail; un-terminated trailing bytes are sealed with a newline and re-parsed.
337
- * Substate a (valid JSON): committed on recovery, returns existing pointer.
338
- * Substate b (malformed): preserved as bad line, check misses, appends new row.
140
+ * Appends a canonical record to its self-computed volume under the Sitian contract (ADR 0086).
141
+ * - Log4j-style append-only record sink: O(1) append, unconditionally writes a new line.
142
+ * - Zero read-back, zero per-append identity check / idempotency deduplication.
143
+ * - Zero torn-tail repair.
339
144
  * - Commit point: full JSON string ending with newline.
340
145
  */
341
146
  export function appendSitianRecord(input: SitianRecordInput): RecordPointer {
@@ -362,7 +167,13 @@ export function appendSitianRecord(input: SitianRecordInput): RecordPointer {
362
167
  };
363
168
 
364
169
  const row = `${JSON.stringify(record)}\n`;
365
- return appendWithIdentityClaim(recordFile, record, row);
170
+ appendFileSync(recordFile, row, "utf8");
171
+ return {
172
+ identity: record.identity,
173
+ recordFile,
174
+ kind: record.kind,
175
+ level: record.level,
176
+ };
366
177
  } catch (error) {
367
178
  if (error instanceof SitianInfrastructureError) throw error;
368
179
  throw new SitianInfrastructureError(
@@ -1,78 +0,0 @@
1
- /**
2
- * Single authority for #617 DK-4 cross-host prior-native projection.
3
- * Classifies the prior volume into the two record families that exist
4
- * (Pi native session file / sitian run records), never by host name.
5
- */
6
- import { access, readdir } from "node:fs/promises";
7
- import { dirname, join } from "node:path";
8
- function isEnoent(error) {
9
- return typeof error === "object" && error !== null && error.code === "ENOENT";
10
- }
11
- /** Present Pi native session path, or empty when ENOENT. */
12
- async function listPiNativeRecordPaths(sessionFile) {
13
- try {
14
- await access(sessionFile);
15
- return [sessionFile];
16
- }
17
- catch (error) {
18
- if (isEnoent(error))
19
- return [];
20
- throw error;
21
- }
22
- }
23
- /**
24
- * Present sitian records.jsonl paths under sessionParent topology (#717).
25
- * resolveSitianRecordPathInLedger writes dirname(sessionParent)/<category>/records.jsonl
26
- * when sessionParent is inside ledger home — never session.jsonl itself.
27
- */
28
- async function listSitianRecordPaths(sessionParent) {
29
- const sessionRoot = dirname(sessionParent);
30
- let entries;
31
- try {
32
- entries = await readdir(sessionRoot, { withFileTypes: true });
33
- }
34
- catch (error) {
35
- if (isEnoent(error))
36
- return [];
37
- throw error;
38
- }
39
- const recordPaths = [];
40
- for (const entry of entries) {
41
- if (!entry.isDirectory())
42
- continue;
43
- const recordFile = join(sessionRoot, entry.name, "records.jsonl");
44
- try {
45
- await access(recordFile);
46
- recordPaths.push(recordFile);
47
- }
48
- catch (error) {
49
- if (!isEnoent(error))
50
- throw error;
51
- }
52
- }
53
- recordPaths.sort();
54
- return recordPaths;
55
- }
56
- /**
57
- * Project one hostTransition only for a real host switch. Empty native volume
58
- * still yields a typed switch (empty path list).
59
- *
60
- * Pi wrote its own session.jsonl; every other host's run volume is the sitian
61
- * record set on the live run (ADR 0077 全宿主 session 卷宗统一直写, #717) — the CLI's
62
- * own journals stay in the operator home and are never copied here.
63
- */
64
- export async function projectHostTransitionPriorNative(input) {
65
- if (input.previousHost === input.liveHost)
66
- return undefined;
67
- if (input.previousHost === "pi") {
68
- return {
69
- priorNativeKind: "pi-native",
70
- priorNativePaths: await listPiNativeRecordPaths(input.piSessionFile),
71
- };
72
- }
73
- // Sitian path handoff only — do not read bytes.
74
- return {
75
- priorNativeKind: "sitian",
76
- priorNativePaths: await listSitianRecordPaths(input.piSessionFile),
77
- };
78
- }
@@ -1,109 +0,0 @@
1
- /**
2
- * 系统随案递送起居录路径的中立指针段(ADR 0081:公共入口直接挂案卷,Soul 只说明用途不负责派送;
3
- * #709 公共入口通用递送与 #742 给事中受理链路同源;#858 告诉 LLM 路径,不另建取号/lookup)。
4
- * 指针输入沿 ADR 0079(指针是绑定材料由代码精准递送),不把卷宗正文塞进提示词。
5
- * 只告诉路径:不刷新、不生成、不校验内容、不新增拒收或停工条件、不从 instruction 抽票号。
6
- * 机器文本仅中立标识材料(ADR 0073),用途说明归角色材料所有。
7
- *
8
- * 递送挂载点唯一:`post-admission` 在 beforeDispatch 之后为每个公共入口挂载。
9
- * 全部公共入口(ordinary 已绑定/未绑定与 station-child)统一走 attachments 冻结 +
10
- * role-runtime `loadCaseDossierReadingMaterial` → readingMaterial →
11
- * systemPrompt.materials fold(#879 / #858:对话 instruction、空请求、resume
12
- * message 保持调用者原文;起居录作独立附件面,不新造 RoleTurnRequest.materials,
13
- * 不拼进 user-dialogue continuation)。
14
- *
15
- * #858: deliver returns typed FrozenAttachment[]; load currently re-derives the
16
- * freeze leaf under runDirectory because agent-start only has HostContext.runDirectory.
17
- * Consuming the typed frozenPath across that process boundary needs a design
18
- * revision of the existing envelope/persistence seam — not a local replica of
19
- * freezeAttachments' `00-` naming, not a drift throw, not readdir guessing.
20
- */
21
- import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
22
- import { tmpdir } from "node:os";
23
- import { join } from "node:path";
24
- import { projectTicketRecordsPathShape } from "../sitian-facade.js";
25
- import { resolveTicketProvenanceVolume } from "../ticket-provenance.js";
26
- import { freezeAttachmentsIntoRun, } from "./invocation.js";
27
- /** Bound-ticket section heading (presentation only). */
28
- const CASE_DOSSIER_SECTION_HEADING = "## 本票起居录(系统随案提供)";
29
- /** Unbound path-only heading — no 本票 claim when no typed ticket is on the run. */
30
- const UNBOUND_DOSSIER_PATH_HEADING = "## 起居录路径(系统随案提供)";
31
- /** Stable freeze key under run/attachments/ for station-child 0081 delivery. */
32
- const CASE_DOSSIER_ATTACH_KEY = "case-dossier";
33
- /** Leaf name of the frozen pointer section (content = purpose + paths). */
34
- const CASE_DOSSIER_ATTACH_FILE = "case-dossier-pointer.md";
35
- /**
36
- * Neutral 起居录 path pointer for every public entry (ADR 0081 delivery seam).
37
- * Always tells the canonical path shape so the seat LLM can read the diary
38
- * itself (陛下 2026-09-15: 只需要告诉llm这个路径). When a typed ticket identity is
39
- * already on the run, also project the concrete resolved file. Never claims a
40
- * volume or「本票」exists when unbound; never extracts a ticket from instruction.
41
- * Under-book shape comes only from sitian writer joins — no local replica.
42
- */
43
- export async function projectCaseDossierPointerSection(input) {
44
- if (input.ticketNumber === undefined) {
45
- return [
46
- UNBOUND_DOSSIER_PATH_HEADING,
47
- "",
48
- `记录卷宗:${projectTicketRecordsPathShape()}`,
49
- ].join("\n");
50
- }
51
- const volume = resolveTicketProvenanceVolume(input.ticketNumber, input.projectRoot, input.home);
52
- return [
53
- CASE_DOSSIER_SECTION_HEADING,
54
- "",
55
- `票号:#${input.ticketNumber}`,
56
- `记录卷宗:${volume.recordFile}`,
57
- ].join("\n");
58
- }
59
- /**
60
- * ADR 0081 delivery for every public entry (#879 / #858): freeze the pointer
61
- * section through the existing attachments seam. Role-runtime loads that freeze
62
- * via loadCaseDossierReadingMaterial onto readingMaterial; the envelope then
63
- * folds it into systemPrompt.materials. Caller / peer dialogue instruction stays
64
- * opaque; never RoleTurnRequest.materials, never user-dialogue continuation.
65
- * Always freezes the path pointer (canonical shape, or concrete file when a
66
- * typed ticket is already bound).
67
- */
68
- export async function deliverCaseDossierAsAttachment(input) {
69
- const section = await projectCaseDossierPointerSection({
70
- ticketNumber: input.ticketNumber,
71
- projectRoot: input.projectRoot,
72
- home: input.home,
73
- });
74
- // Stage in OS temp only — never leave a run-local .case-dossier-stage copy.
75
- const stagingDir = await mkdtemp(join(tmpdir(), "ak-case-dossier-"));
76
- try {
77
- const stagingPath = join(stagingDir, CASE_DOSSIER_ATTACH_FILE);
78
- await writeFile(stagingPath, `${section}\n`, "utf8");
79
- return await freezeAttachmentsIntoRun([stagingPath], input.runDirectory, CASE_DOSSIER_ATTACH_KEY);
80
- }
81
- finally {
82
- await rm(stagingDir, { recursive: true, force: true });
83
- }
84
- }
85
- /**
86
- * Load a previously frozen case-dossier attachment as reading material
87
- * (existing agent-start / systemPrompt.materials fold — not dialogue prompt,
88
- * not RoleTurnRequest.materials). Undefined when the run has no such freeze.
89
- *
90
- * #858: this join re-derives freezeAttachments' leaf naming because the
91
- * agent-start handler only receives runDirectory. Replacing it requires
92
- * threading deliver's typed frozenPath across the existing envelope seam
93
- * (design revision) — do not copy freeze's `00-` algorithm here again.
94
- */
95
- export async function loadCaseDossierReadingMaterial(runDirectory) {
96
- const frozenPath = join(runDirectory, "attachments", CASE_DOSSIER_ATTACH_KEY, `00-${CASE_DOSSIER_ATTACH_FILE}`);
97
- let section;
98
- try {
99
- section = await readFile(frozenPath, "utf8");
100
- }
101
- catch (error) {
102
- if (error.code === "ENOENT")
103
- return undefined;
104
- throw error;
105
- }
106
- if (section.trim() === "")
107
- return undefined;
108
- return { kind: "case-dossier-pointer", frozenPath, section };
109
- }