@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.
- package/CLAUDE.md +3 -0
- package/README.md +1 -1
- package/dist/acp-host/production-host.js +658 -829
- package/dist/engine-detour-usage.js +4 -16
- package/dist/headless-host/production-host.js +777 -862
- package/dist/host-session-record.js +303 -0
- package/dist/pi/role-turn-host.js +1 -21
- package/dist/public-cli/main.js +200 -487
- package/dist/public-cli/post-admission.js +11 -60
- package/dist/sitian-appender.js +10 -155
- package/package.json +1 -1
- package/src/acp-host/role-turn-host.ts +36 -108
- package/src/engine-detour-usage.ts +4 -16
- package/src/external-host-turn-loop.ts +2 -18
- package/src/headless-host/role-turn-host.ts +67 -31
- package/src/host-contracts.ts +0 -15
- package/src/host-session-record.ts +325 -25
- package/src/notary-role.ts +0 -1
- package/src/pi/role-turn-host.ts +1 -25
- package/src/public-cli/countersign-run.ts +4 -3
- package/src/public-cli/post-admission.ts +10 -69
- package/src/public-role-summons.ts +1 -2
- package/src/role-envelope.ts +2 -2
- package/src/role-runtime.ts +0 -14
- package/src/sitian-appender.ts +16 -205
- package/dist/host-transition-prior-native.js +0 -78
- package/dist/public-cli/case-dossier-delivery.js +0 -109
- package/src/host-transition-prior-native.ts +0 -80
- package/src/pi/pi-normalization.ts +0 -291
- package/src/public-cli/case-dossier-delivery.ts +0 -147
|
@@ -1,80 +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
|
-
|
|
9
|
-
import type { RoleTurnHostTransition } from "./host-contracts.ts";
|
|
10
|
-
|
|
11
|
-
function isEnoent(error: unknown): boolean {
|
|
12
|
-
return typeof error === "object" && error !== null && (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Present Pi native session path, or empty when ENOENT. */
|
|
16
|
-
async function listPiNativeRecordPaths(sessionFile: string): Promise<string[]> {
|
|
17
|
-
try {
|
|
18
|
-
await access(sessionFile);
|
|
19
|
-
return [sessionFile];
|
|
20
|
-
} catch (error) {
|
|
21
|
-
if (isEnoent(error)) return [];
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Present sitian records.jsonl paths under sessionParent topology (#717).
|
|
28
|
-
* resolveSitianRecordPathInLedger writes dirname(sessionParent)/<category>/records.jsonl
|
|
29
|
-
* when sessionParent is inside ledger home — never session.jsonl itself.
|
|
30
|
-
*/
|
|
31
|
-
async function listSitianRecordPaths(sessionParent: string): Promise<string[]> {
|
|
32
|
-
const sessionRoot = dirname(sessionParent);
|
|
33
|
-
let entries;
|
|
34
|
-
try {
|
|
35
|
-
entries = await readdir(sessionRoot, { withFileTypes: true });
|
|
36
|
-
} catch (error) {
|
|
37
|
-
if (isEnoent(error)) return [];
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
const recordPaths: string[] = [];
|
|
41
|
-
for (const entry of entries) {
|
|
42
|
-
if (!entry.isDirectory()) continue;
|
|
43
|
-
const recordFile = join(sessionRoot, entry.name, "records.jsonl");
|
|
44
|
-
try {
|
|
45
|
-
await access(recordFile);
|
|
46
|
-
recordPaths.push(recordFile);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
if (!isEnoent(error)) throw error;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
recordPaths.sort();
|
|
52
|
-
return recordPaths;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Project one hostTransition only for a real host switch. Empty native volume
|
|
57
|
-
* still yields a typed switch (empty path list).
|
|
58
|
-
*
|
|
59
|
-
* Pi wrote its own session.jsonl; every other host's run volume is the sitian
|
|
60
|
-
* record set on the live run (ADR 0077 全宿主 session 卷宗统一直写, #717) — the CLI's
|
|
61
|
-
* own journals stay in the operator home and are never copied here.
|
|
62
|
-
*/
|
|
63
|
-
export async function projectHostTransitionPriorNative(input: {
|
|
64
|
-
readonly previousHost: string;
|
|
65
|
-
readonly liveHost: string;
|
|
66
|
-
readonly piSessionFile: string;
|
|
67
|
-
}): Promise<RoleTurnHostTransition | undefined> {
|
|
68
|
-
if (input.previousHost === input.liveHost) return undefined;
|
|
69
|
-
if (input.previousHost === "pi") {
|
|
70
|
-
return {
|
|
71
|
-
priorNativeKind: "pi-native",
|
|
72
|
-
priorNativePaths: await listPiNativeRecordPaths(input.piSessionFile),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
// Sitian path handoff only — do not read bytes.
|
|
76
|
-
return {
|
|
77
|
-
priorNativeKind: "sitian",
|
|
78
|
-
priorNativePaths: await listSitianRecordPaths(input.piSessionFile),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi adapter session normalization translation point (#520 r8 / #513).
|
|
3
|
-
* Normalizes Pi native frames at attempt settlement to minimal canonical records.
|
|
4
|
-
* Promotes and owns session parsing primitives previously in ledger-session-read.ts.
|
|
5
|
-
*/
|
|
6
|
-
import { readFile } from "node:fs/promises";
|
|
7
|
-
import { basename } from "node:path";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
sitianReport,
|
|
11
|
-
type RecordPointer,
|
|
12
|
-
type SitianSubject,
|
|
13
|
-
type SitianUsage,
|
|
14
|
-
} from "../sitian-facade.ts";
|
|
15
|
-
|
|
16
|
-
export type SessionRow = Record<string, unknown>;
|
|
17
|
-
|
|
18
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
19
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** First and last record timestamps in encounter order. */
|
|
23
|
-
export function extractSessionTimestampSpan(
|
|
24
|
-
rows: readonly SessionRow[],
|
|
25
|
-
): { startedAt?: string; endedAt?: string } {
|
|
26
|
-
let startedAt: string | undefined;
|
|
27
|
-
let endedAt: string | undefined;
|
|
28
|
-
for (const row of rows) {
|
|
29
|
-
if (typeof row.timestamp !== "string" || !row.timestamp) continue;
|
|
30
|
-
if (startedAt === undefined) startedAt = row.timestamp;
|
|
31
|
-
endedAt = row.timestamp;
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
...(startedAt !== undefined ? { startedAt } : {}),
|
|
35
|
-
...(endedAt !== undefined ? { endedAt } : {}),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Ordered-unique model ids from session frames (first-seen order).
|
|
41
|
-
* Sources:
|
|
42
|
-
* - `model_change.modelId`
|
|
43
|
-
* - assistant `message.model`
|
|
44
|
-
*/
|
|
45
|
-
export function extractSessionModelSequence(
|
|
46
|
-
rows: readonly SessionRow[],
|
|
47
|
-
): string[] {
|
|
48
|
-
const seen = new Set<string>();
|
|
49
|
-
const ordered: string[] = [];
|
|
50
|
-
const push = (raw: string): void => {
|
|
51
|
-
const model = raw.trim();
|
|
52
|
-
if (model === "" || seen.has(model)) return;
|
|
53
|
-
seen.add(model);
|
|
54
|
-
ordered.push(model);
|
|
55
|
-
};
|
|
56
|
-
for (const row of rows) {
|
|
57
|
-
if (row.type === "model_change" && typeof row.modelId === "string") {
|
|
58
|
-
push(row.modelId);
|
|
59
|
-
}
|
|
60
|
-
const message = isRecord(row.message) ? row.message : undefined;
|
|
61
|
-
if (message?.role === "assistant" && typeof message.model === "string") {
|
|
62
|
-
push(message.model);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return ordered;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** First line of a bash `command` argument (sole owner of this summary). */
|
|
69
|
-
export function bashCommandFirstLine(command: string): string {
|
|
70
|
-
const match = /^[^\r\n]*/.exec(command);
|
|
71
|
-
return match?.[0] ?? "";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export type SessionToolInterval = {
|
|
75
|
-
readonly toolCallId: string;
|
|
76
|
-
readonly toolName: string;
|
|
77
|
-
readonly startedAt: string;
|
|
78
|
-
readonly endedAt?: string | undefined;
|
|
79
|
-
readonly command?: string | undefined;
|
|
80
|
-
readonly isError?: boolean | undefined;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Pair toolCall frames → toolResult frames by toolCallId.
|
|
85
|
-
*/
|
|
86
|
-
export function extractSessionToolIntervals(
|
|
87
|
-
rows: readonly SessionRow[],
|
|
88
|
-
): SessionToolInterval[] {
|
|
89
|
-
type Open = {
|
|
90
|
-
toolCallId: string;
|
|
91
|
-
toolName: string;
|
|
92
|
-
startedAt: string;
|
|
93
|
-
endedAt?: string | undefined;
|
|
94
|
-
command?: string | undefined;
|
|
95
|
-
isError?: boolean | undefined;
|
|
96
|
-
};
|
|
97
|
-
const order: Open[] = [];
|
|
98
|
-
const openById = new Map<string, Open>();
|
|
99
|
-
|
|
100
|
-
for (const row of rows) {
|
|
101
|
-
const rowTimestamp = typeof row.timestamp === "string" ? row.timestamp : undefined;
|
|
102
|
-
const message = isRecord(row.message) ? row.message : undefined;
|
|
103
|
-
|
|
104
|
-
if (message?.role === "assistant" && Array.isArray(message.content)) {
|
|
105
|
-
const callTimestamp =
|
|
106
|
-
typeof message.timestamp === "string" && message.timestamp
|
|
107
|
-
? message.timestamp
|
|
108
|
-
: rowTimestamp;
|
|
109
|
-
for (const part of message.content) {
|
|
110
|
-
if (!isRecord(part) || part.type !== "toolCall") continue;
|
|
111
|
-
if (typeof part.id !== "string" || part.id.length === 0) continue;
|
|
112
|
-
if (typeof part.name !== "string" || part.name.length === 0) continue;
|
|
113
|
-
const timestamp = callTimestamp ?? new Date().toISOString();
|
|
114
|
-
if (openById.has(part.id)) continue;
|
|
115
|
-
|
|
116
|
-
const args = isRecord(part.arguments) ? part.arguments : undefined;
|
|
117
|
-
const command =
|
|
118
|
-
part.name === "bash" &&
|
|
119
|
-
args !== undefined &&
|
|
120
|
-
typeof args.command === "string"
|
|
121
|
-
? bashCommandFirstLine(args.command)
|
|
122
|
-
: undefined;
|
|
123
|
-
|
|
124
|
-
const interval: Open = {
|
|
125
|
-
toolCallId: part.id,
|
|
126
|
-
toolName: part.name,
|
|
127
|
-
startedAt: timestamp,
|
|
128
|
-
...(command !== undefined ? { command } : {}),
|
|
129
|
-
};
|
|
130
|
-
order.push(interval);
|
|
131
|
-
openById.set(part.id, interval);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (message?.role === "toolResult") {
|
|
136
|
-
if (typeof message.toolCallId !== "string" || message.toolCallId.length === 0) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
const resultTimestamp =
|
|
140
|
-
typeof message.timestamp === "string" && message.timestamp
|
|
141
|
-
? message.timestamp
|
|
142
|
-
: rowTimestamp ?? new Date().toISOString();
|
|
143
|
-
|
|
144
|
-
const open = openById.get(message.toolCallId);
|
|
145
|
-
if (open === undefined) {
|
|
146
|
-
const toolName =
|
|
147
|
-
typeof message.toolName === "string" && message.toolName.length > 0
|
|
148
|
-
? message.toolName
|
|
149
|
-
: "unknown";
|
|
150
|
-
order.push({
|
|
151
|
-
toolCallId: message.toolCallId,
|
|
152
|
-
toolName,
|
|
153
|
-
startedAt: resultTimestamp,
|
|
154
|
-
endedAt: resultTimestamp,
|
|
155
|
-
...(typeof message.isError === "boolean" ? { isError: message.isError } : {}),
|
|
156
|
-
});
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
open.endedAt = resultTimestamp;
|
|
160
|
-
if (typeof message.isError === "boolean") {
|
|
161
|
-
open.isError = message.isError;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return order.map((interval) => ({
|
|
167
|
-
toolCallId: interval.toolCallId,
|
|
168
|
-
toolName: interval.toolName,
|
|
169
|
-
startedAt: interval.startedAt,
|
|
170
|
-
...(interval.endedAt !== undefined ? { endedAt: interval.endedAt } : {}),
|
|
171
|
-
...(interval.command !== undefined ? { command: interval.command } : {}),
|
|
172
|
-
...(interval.isError !== undefined ? { isError: interval.isError } : {}),
|
|
173
|
-
}));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export type NormalizePiSessionAttemptOptions = {
|
|
177
|
-
readonly sessionFile: string;
|
|
178
|
-
readonly cwd?: string | undefined;
|
|
179
|
-
readonly subject?: SitianSubject | undefined;
|
|
180
|
-
readonly home?: string | undefined;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Normalizes one Pi session attempt into canonical Sitian records.
|
|
185
|
-
* Carries raw pointers back to original frames without copying full transcripts (#513).
|
|
186
|
-
* Deterministically derives canonical identities (${sessionId}:${frameId}) for idempotent persistence.
|
|
187
|
-
*/
|
|
188
|
-
export async function normalizePiSessionAttempt(
|
|
189
|
-
options: NormalizePiSessionAttemptOptions,
|
|
190
|
-
): Promise<readonly RecordPointer[]> {
|
|
191
|
-
const text = await readFile(options.sessionFile, "utf8");
|
|
192
|
-
const lines = text.split("\n");
|
|
193
|
-
const pointers: RecordPointer[] = [];
|
|
194
|
-
|
|
195
|
-
const validRows: SessionRow[] = [];
|
|
196
|
-
let sessionId = basename(options.sessionFile, ".jsonl");
|
|
197
|
-
let aggregatedUsage: SitianUsage | undefined;
|
|
198
|
-
|
|
199
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
200
|
-
const line = lines[index]!;
|
|
201
|
-
if (!line.trim()) continue;
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
const parsed = JSON.parse(line);
|
|
205
|
-
if (!isRecord(parsed)) {
|
|
206
|
-
throw new Error("non-object session frame");
|
|
207
|
-
}
|
|
208
|
-
validRows.push(parsed);
|
|
209
|
-
|
|
210
|
-
if (parsed.type === "session" && typeof parsed.id === "string" && parsed.id.length > 0) {
|
|
211
|
-
sessionId = parsed.id;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const message = isRecord(parsed.message) ? parsed.message : undefined;
|
|
215
|
-
if (message?.role === "assistant" && isRecord(message.usage)) {
|
|
216
|
-
const u = message.usage;
|
|
217
|
-
aggregatedUsage = {
|
|
218
|
-
promptTokens: (aggregatedUsage?.promptTokens ?? 0) + (typeof u.promptTokens === "number" ? u.promptTokens : 0),
|
|
219
|
-
completionTokens: (aggregatedUsage?.completionTokens ?? 0) + (typeof u.completionTokens === "number" ? u.completionTokens : 0),
|
|
220
|
-
totalTokens: (aggregatedUsage?.totalTokens ?? 0) + (typeof u.totalTokens === "number" ? u.totalTokens : 0),
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
} catch (error) {
|
|
224
|
-
// Normalization failure: record typed failure and keep traversing
|
|
225
|
-
const ptr = sitianReport({
|
|
226
|
-
level: "event",
|
|
227
|
-
kind: "normalization-failure",
|
|
228
|
-
cwd: options.cwd,
|
|
229
|
-
subject: options.subject,
|
|
230
|
-
identity: `${sessionId}:bad-line-${index + 1}`,
|
|
231
|
-
raw: { sessionFile: options.sessionFile, entryId: index + 1 },
|
|
232
|
-
payload: {
|
|
233
|
-
line: index + 1,
|
|
234
|
-
rawText: line,
|
|
235
|
-
error: error instanceof Error ? error.message : String(error),
|
|
236
|
-
},
|
|
237
|
-
source: "pi-normalization",
|
|
238
|
-
sessionParent: options.sessionFile,
|
|
239
|
-
...(options.home !== undefined ? { home: options.home } : {}),
|
|
240
|
-
});
|
|
241
|
-
pointers.push(ptr);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const models = extractSessionModelSequence(validRows);
|
|
246
|
-
const span = extractSessionTimestampSpan(validRows);
|
|
247
|
-
const toolIntervals = extractSessionToolIntervals(validRows);
|
|
248
|
-
|
|
249
|
-
// Normalize paired tool intervals into canonical tool-call event records
|
|
250
|
-
for (const interval of toolIntervals) {
|
|
251
|
-
const ptr = sitianReport({
|
|
252
|
-
level: "event",
|
|
253
|
-
kind: "tool-call",
|
|
254
|
-
cwd: options.cwd,
|
|
255
|
-
subject: options.subject,
|
|
256
|
-
identity: `${sessionId}:${interval.toolCallId}`,
|
|
257
|
-
raw: { sessionFile: options.sessionFile, entryId: interval.toolCallId },
|
|
258
|
-
payload: interval,
|
|
259
|
-
timestamp: interval.startedAt,
|
|
260
|
-
source: "pi-normalization",
|
|
261
|
-
sessionParent: options.sessionFile,
|
|
262
|
-
...(options.home !== undefined ? { home: options.home } : {}),
|
|
263
|
-
});
|
|
264
|
-
pointers.push(ptr);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Normalize attempt summary into run-summary record
|
|
268
|
-
const summaryTimestamp = span.endedAt ?? span.startedAt ?? new Date().toISOString();
|
|
269
|
-
const summaryPtr = sitianReport({
|
|
270
|
-
level: "run-summary",
|
|
271
|
-
kind: "attempt-summary",
|
|
272
|
-
cwd: options.cwd,
|
|
273
|
-
subject: options.subject,
|
|
274
|
-
identity: `${sessionId}:summary`,
|
|
275
|
-
raw: { sessionFile: options.sessionFile, entryId: "summary" },
|
|
276
|
-
payload: {
|
|
277
|
-
startedAt: span.startedAt,
|
|
278
|
-
endedAt: span.endedAt,
|
|
279
|
-
models,
|
|
280
|
-
toolIntervals,
|
|
281
|
-
},
|
|
282
|
-
...(aggregatedUsage !== undefined ? { usage: aggregatedUsage } : {}),
|
|
283
|
-
timestamp: summaryTimestamp,
|
|
284
|
-
source: "pi-normalization",
|
|
285
|
-
sessionParent: options.sessionFile,
|
|
286
|
-
...(options.home !== undefined ? { home: options.home } : {}),
|
|
287
|
-
});
|
|
288
|
-
pointers.push(summaryPtr);
|
|
289
|
-
|
|
290
|
-
return pointers;
|
|
291
|
-
}
|
|
@@ -1,147 +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
|
-
|
|
25
|
-
import { projectTicketRecordsPathShape } from "../sitian-facade.ts";
|
|
26
|
-
import { resolveTicketProvenanceVolume } from "../ticket-provenance.ts";
|
|
27
|
-
import {
|
|
28
|
-
freezeAttachmentsIntoRun,
|
|
29
|
-
type FrozenAttachment,
|
|
30
|
-
} from "./invocation.ts";
|
|
31
|
-
|
|
32
|
-
/** Bound-ticket section heading (presentation only). */
|
|
33
|
-
const CASE_DOSSIER_SECTION_HEADING = "## 本票起居录(系统随案提供)" as const;
|
|
34
|
-
|
|
35
|
-
/** Unbound path-only heading — no 本票 claim when no typed ticket is on the run. */
|
|
36
|
-
const UNBOUND_DOSSIER_PATH_HEADING = "## 起居录路径(系统随案提供)" as const;
|
|
37
|
-
|
|
38
|
-
/** Stable freeze key under run/attachments/ for station-child 0081 delivery. */
|
|
39
|
-
const CASE_DOSSIER_ATTACH_KEY = "case-dossier" as const;
|
|
40
|
-
|
|
41
|
-
/** Leaf name of the frozen pointer section (content = purpose + paths). */
|
|
42
|
-
const CASE_DOSSIER_ATTACH_FILE = "case-dossier-pointer.md" as const;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Neutral 起居录 path pointer for every public entry (ADR 0081 delivery seam).
|
|
46
|
-
* Always tells the canonical path shape so the seat LLM can read the diary
|
|
47
|
-
* itself (陛下 2026-09-15: 只需要告诉llm这个路径). When a typed ticket identity is
|
|
48
|
-
* already on the run, also project the concrete resolved file. Never claims a
|
|
49
|
-
* volume or「本票」exists when unbound; never extracts a ticket from instruction.
|
|
50
|
-
* Under-book shape comes only from sitian writer joins — no local replica.
|
|
51
|
-
*/
|
|
52
|
-
export async function projectCaseDossierPointerSection(input: {
|
|
53
|
-
readonly ticketNumber: number | undefined;
|
|
54
|
-
readonly projectRoot: string;
|
|
55
|
-
readonly home: string;
|
|
56
|
-
}): Promise<string> {
|
|
57
|
-
if (input.ticketNumber === undefined) {
|
|
58
|
-
return [
|
|
59
|
-
UNBOUND_DOSSIER_PATH_HEADING,
|
|
60
|
-
"",
|
|
61
|
-
`记录卷宗:${projectTicketRecordsPathShape()}`,
|
|
62
|
-
].join("\n");
|
|
63
|
-
}
|
|
64
|
-
const volume = resolveTicketProvenanceVolume(
|
|
65
|
-
input.ticketNumber,
|
|
66
|
-
input.projectRoot,
|
|
67
|
-
input.home,
|
|
68
|
-
);
|
|
69
|
-
return [
|
|
70
|
-
CASE_DOSSIER_SECTION_HEADING,
|
|
71
|
-
"",
|
|
72
|
-
`票号:#${input.ticketNumber}`,
|
|
73
|
-
`记录卷宗:${volume.recordFile}`,
|
|
74
|
-
].join("\n");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* ADR 0081 delivery for every public entry (#879 / #858): freeze the pointer
|
|
79
|
-
* section through the existing attachments seam. Role-runtime loads that freeze
|
|
80
|
-
* via loadCaseDossierReadingMaterial onto readingMaterial; the envelope then
|
|
81
|
-
* folds it into systemPrompt.materials. Caller / peer dialogue instruction stays
|
|
82
|
-
* opaque; never RoleTurnRequest.materials, never user-dialogue continuation.
|
|
83
|
-
* Always freezes the path pointer (canonical shape, or concrete file when a
|
|
84
|
-
* typed ticket is already bound).
|
|
85
|
-
*/
|
|
86
|
-
export async function deliverCaseDossierAsAttachment(input: {
|
|
87
|
-
readonly ticketNumber: number | undefined;
|
|
88
|
-
readonly projectRoot: string;
|
|
89
|
-
readonly home: string;
|
|
90
|
-
readonly runDirectory: string;
|
|
91
|
-
}): Promise<readonly FrozenAttachment[]> {
|
|
92
|
-
const section = await projectCaseDossierPointerSection({
|
|
93
|
-
ticketNumber: input.ticketNumber,
|
|
94
|
-
projectRoot: input.projectRoot,
|
|
95
|
-
home: input.home,
|
|
96
|
-
});
|
|
97
|
-
// Stage in OS temp only — never leave a run-local .case-dossier-stage copy.
|
|
98
|
-
const stagingDir = await mkdtemp(join(tmpdir(), "ak-case-dossier-"));
|
|
99
|
-
try {
|
|
100
|
-
const stagingPath = join(stagingDir, CASE_DOSSIER_ATTACH_FILE);
|
|
101
|
-
await writeFile(stagingPath, `${section}\n`, "utf8");
|
|
102
|
-
return await freezeAttachmentsIntoRun(
|
|
103
|
-
[stagingPath],
|
|
104
|
-
input.runDirectory,
|
|
105
|
-
CASE_DOSSIER_ATTACH_KEY,
|
|
106
|
-
);
|
|
107
|
-
} finally {
|
|
108
|
-
await rm(stagingDir, { recursive: true, force: true });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Typed reading-material face for a frozen station-child 0081 attachment. */
|
|
113
|
-
export type CaseDossierReadingMaterial = {
|
|
114
|
-
readonly kind: "case-dossier-pointer";
|
|
115
|
-
readonly frozenPath: string;
|
|
116
|
-
readonly section: string;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Load a previously frozen case-dossier attachment as reading material
|
|
121
|
-
* (existing agent-start / systemPrompt.materials fold — not dialogue prompt,
|
|
122
|
-
* not RoleTurnRequest.materials). Undefined when the run has no such freeze.
|
|
123
|
-
*
|
|
124
|
-
* #858: this join re-derives freezeAttachments' leaf naming because the
|
|
125
|
-
* agent-start handler only receives runDirectory. Replacing it requires
|
|
126
|
-
* threading deliver's typed frozenPath across the existing envelope seam
|
|
127
|
-
* (design revision) — do not copy freeze's `00-` algorithm here again.
|
|
128
|
-
*/
|
|
129
|
-
export async function loadCaseDossierReadingMaterial(
|
|
130
|
-
runDirectory: string,
|
|
131
|
-
): Promise<CaseDossierReadingMaterial | undefined> {
|
|
132
|
-
const frozenPath = join(
|
|
133
|
-
runDirectory,
|
|
134
|
-
"attachments",
|
|
135
|
-
CASE_DOSSIER_ATTACH_KEY,
|
|
136
|
-
`00-${CASE_DOSSIER_ATTACH_FILE}`,
|
|
137
|
-
);
|
|
138
|
-
let section: string;
|
|
139
|
-
try {
|
|
140
|
-
section = await readFile(frozenPath, "utf8");
|
|
141
|
-
} catch (error) {
|
|
142
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
if (section.trim() === "") return undefined;
|
|
146
|
-
return { kind: "case-dossier-pointer", frozenPath, section };
|
|
147
|
-
}
|