@akagilnc/pi-workflow-roles 0.1.5762 → 0.1.5797
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/dist/acp-host/production-host.js +685 -285
- package/dist/external-host-turn-loop.js +3 -0
- package/dist/headless-host/production-host.js +658 -258
- package/dist/migrate-book-topology.js +70 -29
- package/dist/navigator-attendance.js +1 -1
- package/dist/packaged-role-registry.js +3 -2
- package/dist/pi/role-turn-host.js +6 -1
- package/dist/public-cli/instruction-seat-run.js +152 -60
- package/dist/public-cli/invocation.js +98 -46
- package/dist/public-cli/main.js +482 -222
- package/dist/public-cli/option-definitions.js +4 -4
- package/dist/public-cli/post-admission.js +19 -28
- package/dist/public-cli/run-lifecycle.js +10 -21
- package/dist/public-cli/turn-request.js +3 -0
- package/dist/public-role-summons.js +11 -8
- package/dist/report-ticket-tool.js +3 -3
- package/dist/role-run-placement.js +14 -2
- package/dist/role-run-relocation.js +2 -2
- package/dist/run-dossier.js +5 -4
- package/dist/run-terminal-artifacts.js +0 -7
- package/dist/session-identity.js +3 -2
- package/dist/sitian-appender.js +12 -3
- package/dist/submission-ledger.js +24 -0
- package/dist/ticket-progress.js +306 -0
- package/dist/user-dialogue-stdin.js +23 -17
- package/package.json +1 -1
- package/src/external-host-turn-loop.ts +7 -1
- package/src/host-contracts.ts +1 -1
- package/src/navigator-attendance.ts +2 -1
- package/src/packaged-role-registry.ts +3 -2
- package/src/pi/adapter.ts +57 -45
- package/src/pi/role-turn-host.ts +6 -1
- package/src/prepared-role-turn.ts +6 -0
- package/src/public-cli/instruction-seat-run.ts +166 -68
- package/src/public-cli/invocation.ts +120 -46
- package/src/public-cli/option-definitions.ts +5 -4
- package/src/public-cli/post-admission.ts +19 -30
- package/src/public-cli/run-lifecycle.ts +10 -27
- package/src/public-cli/turn-request.ts +3 -0
- package/src/public-role-summons.ts +27 -16
- package/src/report-ticket-tool.ts +7 -3
- package/src/role-envelope.ts +5 -0
- package/src/role-run-placement.ts +19 -2
- package/src/role-run-relocation.ts +2 -2
- package/src/role-runtime.ts +19 -2
- package/src/run-dossier.ts +5 -3
- package/src/run-terminal-artifacts.ts +0 -8
- package/src/session-identity.ts +3 -2
- package/src/sitian-appender.ts +14 -3
- package/src/submission-ledger.ts +27 -0
- package/src/ticket-progress.ts +383 -0
- package/src/user-dialogue-stdin.ts +21 -14
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1199 ticket-level progress: current.jsonl + leg-local receipts/session.
|
|
3
|
+
* Write at seal (createSubmissionLedgerHost → projectClosure), never by
|
|
4
|
+
* post-turn history guessing. Per-leg dossiers stay until #1202.
|
|
5
|
+
*
|
|
6
|
+
* Originals stay in the leg directory (owner 14b58f6c / a8d2e72d):
|
|
7
|
+
* `runs/<leg>/receipts/<n>.json`, `runs/<leg>/session/<原件>`.
|
|
8
|
+
* Unbound legs stage rows in `progress.jsonl` (round empty); bind renames the
|
|
9
|
+
* whole leg, then this module fills rounds and appends ticket current.jsonl.
|
|
10
|
+
* Round authority is progress lines only — no receipt-filename counting,
|
|
11
|
+
* occupancy placeholders, or claim-retry loops. Reviewer dual-lens series
|
|
12
|
+
* count independently (`<n>.<lens>`).
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
16
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
17
|
+
import { resolveHostDossierLandingPath } from "./host-session-record.js";
|
|
18
|
+
import { runDirectoryFromHostContext } from "./host-contracts.js";
|
|
19
|
+
import { acceptedFacts, isTerminatingToolName, } from "./package-contracts/terminating-tools.js";
|
|
20
|
+
import { packagedRoleOutputTool } from "./packaged-role-registry.js";
|
|
21
|
+
import { isUnboundRunDirectory, sessionDirectoryOf, sessionFileOf, subjectDirectoryOfRun, } from "./role-run-placement.js";
|
|
22
|
+
import { readPageSync } from "./run-dossier.js";
|
|
23
|
+
import { parseSitianRecordText } from "./sitian-reader.js";
|
|
24
|
+
import { isRecord } from "./unknown-value.js";
|
|
25
|
+
const CURRENT_FILENAME = "current.jsonl";
|
|
26
|
+
const RECEIPTS_DIRNAME = "receipts";
|
|
27
|
+
const LEG_PROGRESS_FILENAME = "progress.jsonl";
|
|
28
|
+
/**
|
|
29
|
+
* Append-only progress JSONL — same posture as run-dossier row files:
|
|
30
|
+
* sole Sitian decoder keeps reachable rows; malformed lines leave diagnostics
|
|
31
|
+
* on stderr and do not abort seal/relocate.
|
|
32
|
+
*/
|
|
33
|
+
function parseProgressText(text, pathForDiagnostic) {
|
|
34
|
+
if (text.trim() === "")
|
|
35
|
+
return [];
|
|
36
|
+
const { records, diagnostics } = parseSitianRecordText(text);
|
|
37
|
+
for (const diagnostic of diagnostics) {
|
|
38
|
+
process.stderr.write(`[ticket-progress] ${pathForDiagnostic} has malformed row(s); keeps reachable rows and retains the diagnostic: line ${diagnostic.line}: ${diagnostic.error}\n`);
|
|
39
|
+
}
|
|
40
|
+
const lines = [];
|
|
41
|
+
for (const record of records) {
|
|
42
|
+
const parsed = record;
|
|
43
|
+
if (!isRecord(parsed))
|
|
44
|
+
continue;
|
|
45
|
+
if (typeof parsed.seat !== "string" || typeof parsed.round !== "string")
|
|
46
|
+
continue;
|
|
47
|
+
lines.push(parsed);
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
function readProgressFile(path) {
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
return [];
|
|
54
|
+
return parseProgressText(readFileSync(path, "utf8"), path);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read progress rows under a subject.
|
|
58
|
+
* Ticket: append-only `current.jsonl`.
|
|
59
|
+
* Unbound: each leg's `runs/<leaf>/progress.jsonl` (no shared file).
|
|
60
|
+
*/
|
|
61
|
+
export function readTicketProgressLines(subjectDirectory) {
|
|
62
|
+
if (basename(subjectDirectory) === "unbound") {
|
|
63
|
+
const runsDir = join(subjectDirectory, "runs");
|
|
64
|
+
if (!existsSync(runsDir))
|
|
65
|
+
return [];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const name of readdirSync(runsDir).sort()) {
|
|
68
|
+
out.push(...readProgressFile(join(runsDir, name, LEG_PROGRESS_FILENAME)));
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
return readProgressFile(join(subjectDirectory, CURRENT_FILENAME));
|
|
73
|
+
}
|
|
74
|
+
function roundBaseNumber(round) {
|
|
75
|
+
if (round.trim() === "")
|
|
76
|
+
return 0;
|
|
77
|
+
const base = round.includes(".") ? round.slice(0, round.indexOf(".")) : round;
|
|
78
|
+
const n = Number(base);
|
|
79
|
+
return Number.isSafeInteger(n) && n > 0 ? n : 0;
|
|
80
|
+
}
|
|
81
|
+
function roundLens(round) {
|
|
82
|
+
return round.includes(".") ? round.slice(round.indexOf(".") + 1) : undefined;
|
|
83
|
+
}
|
|
84
|
+
function nextRoundFromLines(lines, seat, lens) {
|
|
85
|
+
const want = lens === undefined || lens.trim() === "" ? undefined : lens;
|
|
86
|
+
let max = 0;
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (line.seat !== seat)
|
|
89
|
+
continue;
|
|
90
|
+
if (roundLens(line.round) !== want)
|
|
91
|
+
continue;
|
|
92
|
+
max = Math.max(max, roundBaseNumber(line.round));
|
|
93
|
+
}
|
|
94
|
+
const base = max + 1;
|
|
95
|
+
return want === undefined ? String(base) : `${base}.${want}`;
|
|
96
|
+
}
|
|
97
|
+
/** Leg-local receipt ordinal: `receipts/<n>.json` (not seat#round). */
|
|
98
|
+
function nextLegReceiptIndex(runDirectory) {
|
|
99
|
+
const dir = join(runDirectory, RECEIPTS_DIRNAME);
|
|
100
|
+
if (!existsSync(dir))
|
|
101
|
+
return 1;
|
|
102
|
+
let max = 0;
|
|
103
|
+
for (const name of readdirSync(dir)) {
|
|
104
|
+
const match = /^(\d+)\.json$/.exec(name);
|
|
105
|
+
if (match === null)
|
|
106
|
+
continue;
|
|
107
|
+
const n = Number(match[1]);
|
|
108
|
+
if (Number.isSafeInteger(n) && n > max)
|
|
109
|
+
max = n;
|
|
110
|
+
}
|
|
111
|
+
return max + 1;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Copy structured status/summary from the seat receipt.
|
|
115
|
+
* Status leaves reuse acceptedFacts (terminating-tools authority) via the seat's
|
|
116
|
+
* current output tool — legacy judgeStatus/countersignStatus keys are unreachable
|
|
117
|
+
* at this seal seam (tool is the live registry name, not ak_judge_output).
|
|
118
|
+
* summary is seat-authored prose on the receipt and is not part of AcceptedFacts.
|
|
119
|
+
* Missing fields stay absent — never invent "" or "accepted".
|
|
120
|
+
*
|
|
121
|
+
* Not receiptStatusFromRegistry: that scans every registry key for navigator
|
|
122
|
+
* projection when the seat is unknown; here the seal already knows the seat.
|
|
123
|
+
*/
|
|
124
|
+
function statusAndSummaryFromPayload(seat, payload) {
|
|
125
|
+
if (!isRecord(payload))
|
|
126
|
+
return {};
|
|
127
|
+
const toolName = packagedRoleOutputTool(seat);
|
|
128
|
+
const status = toolName !== undefined && isTerminatingToolName(toolName)
|
|
129
|
+
? acceptedFacts(toolName, payload).status
|
|
130
|
+
: undefined;
|
|
131
|
+
const summary = typeof payload.summary === "string" ? payload.summary : undefined;
|
|
132
|
+
return {
|
|
133
|
+
...(status === undefined ? {} : { status }),
|
|
134
|
+
...(summary === undefined ? {} : { summary }),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Worktree HEAD at seal. On failure head is empty and fault carries the true cause. */
|
|
138
|
+
export function captureWorktreeHead(projectRoot) {
|
|
139
|
+
try {
|
|
140
|
+
const result = spawnSync("git", ["rev-parse", "HEAD"], {
|
|
141
|
+
cwd: projectRoot,
|
|
142
|
+
encoding: "utf8",
|
|
143
|
+
maxBuffer: 1024 * 1024,
|
|
144
|
+
});
|
|
145
|
+
if (result.error != null) {
|
|
146
|
+
const err = result.error;
|
|
147
|
+
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
148
|
+
return { head: "", fault: `git rev-parse HEAD spawn failed cwd=${projectRoot}: ${detail}` };
|
|
149
|
+
}
|
|
150
|
+
if (result.status !== 0) {
|
|
151
|
+
const detail = [result.stderr, result.stdout]
|
|
152
|
+
.map((part) => (typeof part === "string" ? part.trim() : ""))
|
|
153
|
+
.filter((part) => part !== "")
|
|
154
|
+
.join(" | ");
|
|
155
|
+
const status = result.status === null ? "null" : String(result.status);
|
|
156
|
+
const signal = result.signal == null ? "" : ` signal=${result.signal}`;
|
|
157
|
+
return {
|
|
158
|
+
head: "",
|
|
159
|
+
fault: detail === ""
|
|
160
|
+
? `git rev-parse HEAD failed (status=${status}${signal}) cwd=${projectRoot}`
|
|
161
|
+
: `git rev-parse HEAD failed (status=${status}${signal}) cwd=${projectRoot}: ${detail}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { head: (result.stdout ?? "").trim() };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
168
|
+
return { head: "", fault: `git rev-parse HEAD threw cwd=${projectRoot}: ${message}` };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function progressSessionAbsolute(runDirectory, context) {
|
|
172
|
+
const host = typeof context.host === "string" ? context.host.trim() : "";
|
|
173
|
+
const sessionDirectory = sessionDirectoryOf(runDirectory);
|
|
174
|
+
// External hosts: ADR 0086 copy landing (codex.jsonl / grok-build / …), not the
|
|
175
|
+
// principal wire file. Responsibility unchanged (#1199 R3 disposition).
|
|
176
|
+
if (host !== "" && host !== "pi" && host !== "hermes") {
|
|
177
|
+
return resolveHostDossierLandingPath({ host, sessionDirectory });
|
|
178
|
+
}
|
|
179
|
+
// Pi/hermes: prefer the issued durable coordinate on the live handle.
|
|
180
|
+
const issued = context.sessionManager.getSessionFile?.();
|
|
181
|
+
if (typeof issued === "string" && issued.trim() !== "") {
|
|
182
|
+
return issued;
|
|
183
|
+
}
|
|
184
|
+
return sessionFileOf(runDirectory);
|
|
185
|
+
}
|
|
186
|
+
function lensFromRunDirectory(runDirectory, role) {
|
|
187
|
+
if (role !== "reviewer")
|
|
188
|
+
return undefined;
|
|
189
|
+
const lens = readPageSync(runDirectory, "admitted")?.lens;
|
|
190
|
+
return lens === "completeness" || lens === "correctness" ? lens : undefined;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Seal-time landing from createSubmissionLedgerHost (before projectClosure).
|
|
194
|
+
* Missing run directory → no-op (ledger still sealed).
|
|
195
|
+
*/
|
|
196
|
+
export function landTicketProgressForSealedSubmission(input) {
|
|
197
|
+
const runDirectory = runDirectoryFromHostContext(input.context);
|
|
198
|
+
if (runDirectory === undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
const subjectDirectory = subjectDirectoryOfRun(runDirectory);
|
|
201
|
+
if (subjectDirectory === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
const lens = (input.lens === "completeness" || input.lens === "correctness"
|
|
204
|
+
? input.lens
|
|
205
|
+
: lensFromRunDirectory(runDirectory, input.role));
|
|
206
|
+
const leaf = basename(runDirectory);
|
|
207
|
+
const unbound = basename(subjectDirectory) === "unbound";
|
|
208
|
+
const progressPath = unbound
|
|
209
|
+
? join(runDirectory, LEG_PROGRESS_FILENAME)
|
|
210
|
+
: join(subjectDirectory, CURRENT_FILENAME);
|
|
211
|
+
// Original receipt first — independent of derived progress read/append.
|
|
212
|
+
// Progress I/O failure must not erase an already-accepted payload (#1199 F1).
|
|
213
|
+
const receiptIndex = nextLegReceiptIndex(runDirectory);
|
|
214
|
+
const relativePath = `runs/${leaf}/${RECEIPTS_DIRNAME}/${receiptIndex}.json`;
|
|
215
|
+
const absolutePath = join(subjectDirectory, relativePath);
|
|
216
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
217
|
+
writeFileSync(absolutePath, `${JSON.stringify(input.accepted, null, 2)}\n`, "utf8");
|
|
218
|
+
// #1199 R1: bound leg with leftover progress.jsonl must publish before this
|
|
219
|
+
// seal takes the next ticket round — optional ticketNumber must not be required.
|
|
220
|
+
if (!unbound) {
|
|
221
|
+
relocateTicketProgressForLeg({ runDirectory });
|
|
222
|
+
}
|
|
223
|
+
const existing = unbound
|
|
224
|
+
? readProgressFile(progressPath)
|
|
225
|
+
: readTicketProgressLines(subjectDirectory);
|
|
226
|
+
// Unbound: round stays empty until whole-leg bind publishes into ticket current.
|
|
227
|
+
const round = unbound ? "" : nextRoundFromLines(existing, input.role, lens);
|
|
228
|
+
const sessionFile = progressSessionAbsolute(runDirectory, input.context);
|
|
229
|
+
const sessionRelative = relative(subjectDirectory, sessionFile).split(sep).join("/");
|
|
230
|
+
const { status, summary } = statusAndSummaryFromPayload(input.role, input.accepted);
|
|
231
|
+
const instruction = typeof input.context.summonsInstruction === "string"
|
|
232
|
+
? input.context.summonsInstruction
|
|
233
|
+
: "";
|
|
234
|
+
const line = {
|
|
235
|
+
at: new Date().toISOString(),
|
|
236
|
+
seat: input.role,
|
|
237
|
+
round,
|
|
238
|
+
instruction,
|
|
239
|
+
head: input.sealHead.head,
|
|
240
|
+
...(status === undefined ? {} : { status }),
|
|
241
|
+
...(summary === undefined ? {} : { summary }),
|
|
242
|
+
receipt: relativePath,
|
|
243
|
+
session: sessionRelative,
|
|
244
|
+
};
|
|
245
|
+
mkdirSync(dirname(progressPath), { recursive: true });
|
|
246
|
+
appendFileSync(progressPath, `${JSON.stringify(line)}\n`, "utf8");
|
|
247
|
+
// HEAD capture fault: keep the raw cause on stderr. Do not invent a second
|
|
248
|
+
// dossier custom-frame channel beside post-admission's existing retain path.
|
|
249
|
+
if (input.sealHead.fault !== undefined) {
|
|
250
|
+
process.stderr.write(`${input.sealHead.fault}\n`);
|
|
251
|
+
}
|
|
252
|
+
return line;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* After whole-leg rename (or retry when already under the ticket): fill rounds
|
|
256
|
+
* on staged `progress.jsonl`, append ticket `current.jsonl`, then drop staging.
|
|
257
|
+
* Receipt / session / head / instruction keep seal-time values and relative
|
|
258
|
+
* paths — no copy, delete-source, or rewrite of relative pointers.
|
|
259
|
+
*
|
|
260
|
+
* Unbound staging leaves `round` empty; publish takes the leg's sealed lens from
|
|
261
|
+
* the admitted page (`lensFromRunDirectory`), never from receipt prose.
|
|
262
|
+
*/
|
|
263
|
+
export function relocateTicketProgressForLeg(input) {
|
|
264
|
+
if (isUnboundRunDirectory(input.runDirectory))
|
|
265
|
+
return;
|
|
266
|
+
const subjectDirectory = subjectDirectoryOfRun(input.runDirectory);
|
|
267
|
+
if (subjectDirectory === undefined)
|
|
268
|
+
return;
|
|
269
|
+
const stagedPath = join(input.runDirectory, LEG_PROGRESS_FILENAME);
|
|
270
|
+
const mine = readProgressFile(stagedPath);
|
|
271
|
+
if (mine.length === 0) {
|
|
272
|
+
if (existsSync(stagedPath)) {
|
|
273
|
+
rmSync(stagedPath, { force: true });
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const ticketLines = [...readTicketProgressLines(subjectDirectory)];
|
|
278
|
+
// Receipt path is the seal identity. Append-then-unlink partial success must
|
|
279
|
+
// not reassign rounds or re-append the same originals on re-entry (#1199 F2).
|
|
280
|
+
const publishedReceipts = new Set(ticketLines
|
|
281
|
+
.map((line) => line.receipt)
|
|
282
|
+
.filter((receipt) => typeof receipt === "string" && receipt !== ""));
|
|
283
|
+
const pending = mine.filter((line) => !publishedReceipts.has(line.receipt));
|
|
284
|
+
if (pending.length === 0) {
|
|
285
|
+
rmSync(stagedPath, { force: true });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const movedLines = [];
|
|
289
|
+
for (const line of pending) {
|
|
290
|
+
// Current unbound staging publishes empty rounds; lens comes only from this
|
|
291
|
+
// leg's admitted page. No legacy staged-round mirror-suffix branch.
|
|
292
|
+
const lens = lensFromRunDirectory(input.runDirectory, line.seat);
|
|
293
|
+
const round = nextRoundFromLines(ticketLines, line.seat, lens);
|
|
294
|
+
const moved = {
|
|
295
|
+
...line,
|
|
296
|
+
round,
|
|
297
|
+
};
|
|
298
|
+
movedLines.push(moved);
|
|
299
|
+
ticketLines.push(moved);
|
|
300
|
+
}
|
|
301
|
+
const newCurrent = join(subjectDirectory, CURRENT_FILENAME);
|
|
302
|
+
mkdirSync(dirname(newCurrent), { recursive: true });
|
|
303
|
+
appendFileSync(newCurrent, `${movedLines.map((line) => JSON.stringify(line)).join("\n")}\n`, "utf8");
|
|
304
|
+
// Append succeeded — drop staging. Failure above leaves progress.jsonl in place.
|
|
305
|
+
rmSync(stagedPath, { force: true });
|
|
306
|
+
}
|
|
@@ -2,33 +2,39 @@
|
|
|
2
2
|
* Typed user-dialogue stdin transport (#879 / #471).
|
|
3
3
|
* Pi's piped-stdin reader trims the wrapper; the body field keeps original bytes.
|
|
4
4
|
* No size cap and no path/pointer substitute — the whole dialogue rides the envelope.
|
|
5
|
+
*
|
|
6
|
+
* #1199: progress-line instruction is these same body bytes (what the seat
|
|
7
|
+
* actually receives). No parallel 传召词 field on the envelope.
|
|
5
8
|
*/
|
|
6
9
|
import { isRecord } from "./unknown-value.js";
|
|
7
10
|
export const USER_DIALOGUE_STDIN_KIND = "ak-user-dialogue";
|
|
8
11
|
export function encodeUserDialogueStdin(body) {
|
|
9
|
-
|
|
12
|
+
const envelope = { kind: USER_DIALOGUE_STDIN_KIND, body };
|
|
13
|
+
return JSON.stringify(envelope);
|
|
10
14
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return false;
|
|
14
|
-
const record = value;
|
|
15
|
-
return record.kind === USER_DIALOGUE_STDIN_KIND && typeof record.body === "string";
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Host read: recover original dialogue after a trim-capable pipe.
|
|
19
|
-
* Non-envelope text (interactive Pi, officer JSON, headless raw body) is unchanged.
|
|
20
|
-
*/
|
|
21
|
-
export function readUserDialogueStdin(raw) {
|
|
15
|
+
/** One parse for the model-facing body — callers must not re-implement. */
|
|
16
|
+
export function tryParseUserDialogueStdin(raw) {
|
|
22
17
|
const trimmed = raw.trim();
|
|
23
18
|
if (!trimmed.startsWith("{"))
|
|
24
|
-
return
|
|
19
|
+
return undefined;
|
|
25
20
|
try {
|
|
26
21
|
const parsed = JSON.parse(trimmed);
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
22
|
+
if (!isRecord(parsed))
|
|
23
|
+
return undefined;
|
|
24
|
+
const record = parsed;
|
|
25
|
+
if (record.kind !== USER_DIALOGUE_STDIN_KIND || typeof record.body !== "string") {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return { kind: USER_DIALOGUE_STDIN_KIND, body: record.body };
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
31
|
-
|
|
31
|
+
return undefined;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Host read: recover original dialogue after a trim-capable pipe.
|
|
36
|
+
* Non-envelope text (interactive Pi, officer JSON, headless raw body) is unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function readUserDialogueStdin(raw) {
|
|
39
|
+
return tryParseUserDialogueStdin(raw)?.body ?? raw;
|
|
34
40
|
}
|
package/package.json
CHANGED
|
@@ -129,7 +129,10 @@ export async function recordTurnDelivery(
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
export type ExternalPreparedTurn = Pick<
|
|
132
|
+
export type ExternalPreparedTurn = Pick<
|
|
133
|
+
PreparedRoleTurn,
|
|
134
|
+
"prompt" | "abortSignal" | "closeRound" | "recordSentInstruction"
|
|
135
|
+
>;
|
|
133
136
|
|
|
134
137
|
export type ExternalHostRoundOutcome =
|
|
135
138
|
| {
|
|
@@ -318,6 +321,9 @@ export async function driveExternalRoleTurnRounds(
|
|
|
318
321
|
}
|
|
319
322
|
}
|
|
320
323
|
prompt = closure.retry.message;
|
|
324
|
+
// #1199: same-envelope 催交 / correctable retry — progress instruction is
|
|
325
|
+
// the bytes actually sent this round, not the first-mint summons.
|
|
326
|
+
prepared.recordSentInstruction?.(prompt);
|
|
321
327
|
driver.afterRetry?.();
|
|
322
328
|
}
|
|
323
329
|
|
package/src/host-contracts.ts
CHANGED
|
@@ -264,7 +264,7 @@ export interface DurablePrincipalAuthority {
|
|
|
264
264
|
type HostSessionManager = { getLeafEntry(): HostSessionEntry | undefined; getLeafId(): string | null | undefined; getEntries(): Iterable<HostSessionEntry>; getSessionDir(): string; getSessionFile(): string | undefined; getHeader?(): { readonly type: string; readonly id?: string } | null; setSessionFile?(path: string): void; appendCustomEntry?(customType: string, data?: unknown): unknown; };
|
|
265
265
|
|
|
266
266
|
/** Context supplied by a host for one activation and its interceptable events. */
|
|
267
|
-
export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; /** Per-turn admitted run directory (#879); never process-global env. */ runDirectory?: string; /** Per-turn court attempt (#879); never process-global env. */ courtAttemptId?: string; /** Public-invocation scope (#537); never process-global env. */ invocationScopeId?: string; /** Selected host axis (#537 / ADR 0082); never process-global env invent. */ host?: string; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
|
|
267
|
+
export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; /** Per-turn admitted run directory (#879); never process-global env. */ runDirectory?: string; /** Per-turn court attempt (#879); never process-global env. */ courtAttemptId?: string; /** Public-invocation scope (#537); never process-global env. */ invocationScopeId?: string; /** Selected host axis (#537 / ADR 0082); never process-global env invent. */ host?: string; /** #1199 bytes actually sent to the seat this turn (progress instruction); input-seam / in-process only — never env/argv body. */ summonsInstruction?: string; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
|
|
268
268
|
|
|
269
269
|
/** Per-turn run directory; adapters must project any child-process identity. */
|
|
270
270
|
export function runDirectoryFromHostContext(context: HostContext): string | undefined {
|
|
@@ -444,9 +444,10 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
444
444
|
};
|
|
445
445
|
await promptAllowingRejectedPrepare(request);
|
|
446
446
|
// Correction after rejected prepare on the sole model round (#1160).
|
|
447
|
+
// #1208: resend this turn's prepare request; delivery-state stays on policy facts.
|
|
447
448
|
while (output === undefined && prepareBatchRejected && delivery.nextAction() === "request-delivery") {
|
|
448
449
|
delivery.recordDeliveryRequest();
|
|
449
|
-
await promptAllowingRejectedPrepare(
|
|
450
|
+
await promptAllowingRejectedPrepare(request);
|
|
450
451
|
}
|
|
451
452
|
if (output === undefined && delivery.nextAction() === "request-delivery") {
|
|
452
453
|
delivery.closeBudget();
|
|
@@ -294,9 +294,8 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
294
294
|
bareCommand: false,
|
|
295
295
|
outputTool: NOTARY_OUTPUT_TOOL_NAME,
|
|
296
296
|
settlement: "accepted",
|
|
297
|
-
/**
|
|
297
|
+
/** Dialogue via shared resolveReviewSeatDialogueBody (review = fixed dispatch). */
|
|
298
298
|
reaskPrompt: true,
|
|
299
|
-
transportPrompt: "fixed-kickoff",
|
|
300
299
|
gateStageLabel: "符宝郎",
|
|
301
300
|
/** Gate summon binds --source-run and keeps dialogue off the argv. */
|
|
302
301
|
gateSummon: "source-run",
|
|
@@ -319,6 +318,8 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
319
318
|
phases: [null],
|
|
320
319
|
outputTool: COUNTERSIGN_OUTPUT_TOOL_NAME,
|
|
321
320
|
settlement: "accepted",
|
|
321
|
+
/** Same shared dialogue resolver as other review seats (#1208 C1). */
|
|
322
|
+
reaskPrompt: true,
|
|
322
323
|
gateStageLabel: "给事中",
|
|
323
324
|
/** Gate summon carries the parent payload as the instruction and a parent run id. */
|
|
324
325
|
gateSummon: "parent-instruction",
|
package/src/pi/adapter.ts
CHANGED
|
@@ -45,44 +45,6 @@ export function createPiRoleRuntimeExtension(
|
|
|
45
45
|
return (pi) => createRoleRuntimeExtension(dependencies)(createPiRoleHostAdapter(pi, options));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/** Project Pi's activation context onto the package-owned host contract. */
|
|
49
|
-
function projectPiContext(context: ExtensionContext, transcriptFromContext?: (context: ExtensionContext) => string): HostContext {
|
|
50
|
-
const sessionManager = context.sessionManager as SessionManager;
|
|
51
|
-
const host: HostContext = {
|
|
52
|
-
cwd: context.cwd,
|
|
53
|
-
mode: context.mode,
|
|
54
|
-
model: context.model === undefined ? undefined : { provider: context.model.provider },
|
|
55
|
-
...(typeof process.env.AK_ROLE_RUN_DIR === "string" && process.env.AK_ROLE_RUN_DIR.trim() !== ""
|
|
56
|
-
? { runDirectory: process.env.AK_ROLE_RUN_DIR }
|
|
57
|
-
: {}),
|
|
58
|
-
...(typeof process.env.AK_ROLE_COURT_ATTEMPT === "string" && process.env.AK_ROLE_COURT_ATTEMPT.trim() !== ""
|
|
59
|
-
? { courtAttemptId: process.env.AK_ROLE_COURT_ATTEMPT }
|
|
60
|
-
: {}),
|
|
61
|
-
...(typeof process.env.AK_ROLE_INVOCATION_SCOPE === "string" && process.env.AK_ROLE_INVOCATION_SCOPE.trim() !== ""
|
|
62
|
-
? { invocationScopeId: process.env.AK_ROLE_INVOCATION_SCOPE }
|
|
63
|
-
: {}),
|
|
64
|
-
...(typeof process.env.AK_ROLE_HOST === "string" && process.env.AK_ROLE_HOST.trim() !== ""
|
|
65
|
-
? { host: process.env.AK_ROLE_HOST.trim() }
|
|
66
|
-
: {}),
|
|
67
|
-
sessionManager: {
|
|
68
|
-
getLeafEntry: () => context.sessionManager.getLeafEntry(),
|
|
69
|
-
getLeafId: () => context.sessionManager.getLeafId(),
|
|
70
|
-
getEntries: () => context.sessionManager.getEntries(),
|
|
71
|
-
getSessionDir: () => context.sessionManager.getSessionDir(),
|
|
72
|
-
getSessionFile: () => context.sessionManager.getSessionFile(),
|
|
73
|
-
getHeader: () => context.sessionManager.getHeader(),
|
|
74
|
-
setSessionFile: (path) => sessionManager.setSessionFile(path),
|
|
75
|
-
appendCustomEntry: (customType, data) => sessionManager.appendCustomEntry(customType, data),
|
|
76
|
-
},
|
|
77
|
-
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
78
|
-
...(context.ui === undefined ? {} : { ui: { notify: (message, type) => context.ui.notify(message, type) } }),
|
|
79
|
-
...(transcriptFromContext === undefined ? {} : { transcript: () => transcriptFromContext(context) }),
|
|
80
|
-
abort: () => context.abort(),
|
|
81
|
-
};
|
|
82
|
-
piContexts.set(host, context);
|
|
83
|
-
return host;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
48
|
/** Recover the Pi context only at the Pi composition boundary. */
|
|
87
49
|
export function toPiContext(context: HostContext): ExtensionContext {
|
|
88
50
|
const piContext = piContexts.get(context);
|
|
@@ -158,10 +120,63 @@ export function createPiRoleHostAdapter(
|
|
|
158
120
|
let beforeAgentStartHandlers = 0;
|
|
159
121
|
// Parameters of registered terminating tools — the schema Pi was handed.
|
|
160
122
|
const registeredToolParameters = new Map<string, unknown>();
|
|
161
|
-
//
|
|
162
|
-
//
|
|
123
|
+
// Pi createContext() is fresh per emit/tool (runner.js). Do not cache HostContext by
|
|
124
|
+
// ExtensionContext identity. Share only this-turn 传召词 / 催交 on the adapter instance;
|
|
125
|
+
// model and identity pointers stay live on each fresh projection (#1199).
|
|
126
|
+
let turnSummonsInstruction: string | undefined;
|
|
127
|
+
const transcriptFromContext = options.transcriptFromContext;
|
|
128
|
+
const projectPiContext = (context: ExtensionContext): HostContext => {
|
|
129
|
+
const sessionManager = context.sessionManager as SessionManager;
|
|
130
|
+
const host: HostContext = {
|
|
131
|
+
cwd: context.cwd,
|
|
132
|
+
mode: context.mode,
|
|
133
|
+
model: context.model === undefined ? undefined : { provider: context.model.provider },
|
|
134
|
+
...(typeof process.env.AK_ROLE_RUN_DIR === "string" && process.env.AK_ROLE_RUN_DIR.trim() !== ""
|
|
135
|
+
? { runDirectory: process.env.AK_ROLE_RUN_DIR }
|
|
136
|
+
: {}),
|
|
137
|
+
...(typeof process.env.AK_ROLE_COURT_ATTEMPT === "string" && process.env.AK_ROLE_COURT_ATTEMPT.trim() !== ""
|
|
138
|
+
? { courtAttemptId: process.env.AK_ROLE_COURT_ATTEMPT }
|
|
139
|
+
: {}),
|
|
140
|
+
...(typeof process.env.AK_ROLE_INVOCATION_SCOPE === "string" && process.env.AK_ROLE_INVOCATION_SCOPE.trim() !== ""
|
|
141
|
+
? { invocationScopeId: process.env.AK_ROLE_INVOCATION_SCOPE }
|
|
142
|
+
: {}),
|
|
143
|
+
...(typeof process.env.AK_ROLE_HOST === "string" && process.env.AK_ROLE_HOST.trim() !== ""
|
|
144
|
+
? { host: process.env.AK_ROLE_HOST.trim() }
|
|
145
|
+
: {}),
|
|
146
|
+
sessionManager: {
|
|
147
|
+
getLeafEntry: () => context.sessionManager.getLeafEntry(),
|
|
148
|
+
getLeafId: () => context.sessionManager.getLeafId(),
|
|
149
|
+
getEntries: () => context.sessionManager.getEntries(),
|
|
150
|
+
getSessionDir: () => context.sessionManager.getSessionDir(),
|
|
151
|
+
getSessionFile: () => context.sessionManager.getSessionFile(),
|
|
152
|
+
getHeader: () => context.sessionManager.getHeader(),
|
|
153
|
+
setSessionFile: (path) => sessionManager.setSessionFile(path),
|
|
154
|
+
appendCustomEntry: (customType, data) => sessionManager.appendCustomEntry(customType, data),
|
|
155
|
+
},
|
|
156
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
157
|
+
...(context.ui === undefined ? {} : { ui: { notify: (message, type) => context.ui.notify(message, type) } }),
|
|
158
|
+
...(transcriptFromContext === undefined ? {} : { transcript: () => transcriptFromContext(context) }),
|
|
159
|
+
abort: () => context.abort(),
|
|
160
|
+
};
|
|
161
|
+
// exactOptionalPropertyTypes: wire via accessors so undefined stays "absent value"
|
|
162
|
+
// while every fresh projection shares the adapter-instance turn field.
|
|
163
|
+
Object.defineProperty(host, "summonsInstruction", {
|
|
164
|
+
enumerable: true,
|
|
165
|
+
configurable: true,
|
|
166
|
+
get: () => turnSummonsInstruction,
|
|
167
|
+
set: (value: string | undefined) => {
|
|
168
|
+
turnSummonsInstruction = value;
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
piContexts.set(host, context);
|
|
172
|
+
return host;
|
|
173
|
+
};
|
|
174
|
+
// Decode transport exactly once before Pi's native handler chain. Progress
|
|
175
|
+
// instruction is the model-facing body (what the seat actually receives) —
|
|
176
|
+
// no parallel 传召词 channel. Every input refreshes the turn field.
|
|
163
177
|
pi.on("input", (value) => {
|
|
164
178
|
const text = readUserDialogueStdin(value.text);
|
|
179
|
+
turnSummonsInstruction = text;
|
|
165
180
|
return text === value.text ? { action: "continue" } : { action: "transform", text };
|
|
166
181
|
});
|
|
167
182
|
const host: RoleHost = {
|
|
@@ -172,10 +187,7 @@ export function createPiRoleHostAdapter(
|
|
|
172
187
|
getFlag: (name) => pi.getFlag(name),
|
|
173
188
|
registerTool: (tool) => {
|
|
174
189
|
registeredToolParameters.set(tool.name, tool.parameters);
|
|
175
|
-
return pi.registerTool(toPiToolDefinition(
|
|
176
|
-
tool,
|
|
177
|
-
(context) => projectPiContext(context, options.transcriptFromContext),
|
|
178
|
-
));
|
|
190
|
+
return pi.registerTool(toPiToolDefinition(tool, projectPiContext));
|
|
179
191
|
},
|
|
180
192
|
getAllTools: () => pi.getAllTools().map(({ name, sourceInfo }) => ({
|
|
181
193
|
name,
|
|
@@ -184,7 +196,7 @@ export function createPiRoleHostAdapter(
|
|
|
184
196
|
setActiveTools: (names) => pi.setActiveTools(names),
|
|
185
197
|
getActiveTools: () => pi.getActiveTools(),
|
|
186
198
|
on(...registration: HostEventRegistration) {
|
|
187
|
-
const context =
|
|
199
|
+
const context = projectPiContext;
|
|
188
200
|
if (registration[0] === "before_agent_start") {
|
|
189
201
|
const [, handler] = registration;
|
|
190
202
|
const position = beforeAgentStartHandlers;
|
package/src/pi/role-turn-host.ts
CHANGED
|
@@ -438,7 +438,9 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
438
438
|
config.extraPiArgs ?? [],
|
|
439
439
|
);
|
|
440
440
|
const args = buildExplicitInternalActivationArgs(roleEntry, extraArgs);
|
|
441
|
-
const
|
|
441
|
+
const transportBody = piUserDialogueBody(turnRequest);
|
|
442
|
+
// #1199: progress instruction is these same body bytes (actual seat input).
|
|
443
|
+
const stdin = encodeUserDialogueStdin(transportBody);
|
|
442
444
|
// Shared envelope isolates this call's court identity: omitting courtAttemptId
|
|
443
445
|
// must not inherit a parent process.env.AK_ROLE_COURT_ATTEMPT (#637).
|
|
444
446
|
const env: NodeJS.ProcessEnv = {
|
|
@@ -458,6 +460,9 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
458
460
|
// Selected host axis (#537 / ADR 0082): omit must not inherit a parent env value.
|
|
459
461
|
if (request.host === undefined || request.host.trim() === "") delete env.AK_ROLE_HOST;
|
|
460
462
|
else env.AK_ROLE_HOST = request.host.trim();
|
|
463
|
+
// #1199: 传召词 is unknown-length body — never argv/env (execve ARG_MAX / E2BIG).
|
|
464
|
+
// Typed stdin envelope carries dialogue + optional progress 传召词.
|
|
465
|
+
delete env.AK_ROLE_SUMMONS_INSTRUCTION;
|
|
461
466
|
// #1132: the one effective delivery-request ceiling travels to the child
|
|
462
467
|
// on its own env so the in-child role runtime and the worker submission
|
|
463
468
|
// gate count with the same configured number the AK seam resolved. Omit
|
|
@@ -15,6 +15,12 @@ export type PreparedRoleTurn = Readonly<{
|
|
|
15
15
|
systemPrompt: { readonly body: string; readonly materials: readonly unknown[] };
|
|
16
16
|
/** Effective user prompt after host-side input transform (canonical Skill invocation). */
|
|
17
17
|
prompt: string;
|
|
18
|
+
/**
|
|
19
|
+
* #1199: book the bytes actually sent this round as progress instruction
|
|
20
|
+
* (same-envelope 催交 / correctable retry). Production prepareRoleEnvelope
|
|
21
|
+
* always provides this; test stubs may omit.
|
|
22
|
+
*/
|
|
23
|
+
recordSentInstruction?(text: string): void;
|
|
18
24
|
/**
|
|
19
25
|
* Host abort signal armed only by typed infrastructure failure (envelope
|
|
20
26
|
* rememberInfrastructureFailure / non-correctable MCP catch). Lawful
|