@akagilnc/pi-workflow-roles 0.1.5797 → 0.1.5820
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 +464 -355
- package/dist/acp-host/role-turn-host.js +484 -0
- package/dist/activation-ledger-session.js +105 -0
- package/dist/activation-ledger.js +8 -0
- package/dist/activation-trace.js +33 -0
- package/dist/audit-escalation.js +7 -0
- package/dist/audited-run-identity.js +21 -0
- package/dist/auditor-role.js +7 -0
- package/dist/book-topology-migration-placement.js +404 -0
- package/dist/book-topology-migration.js +125 -0
- package/dist/book-topology-migrators-t11.js +677 -0
- package/dist/book-topology-mixed-volume-migrators.js +322 -0
- package/dist/book-topology-partition-migrators.js +23 -0
- package/dist/book-topology-record-class-migrators.js +671 -0
- package/dist/book-topology-runs-migrator.js +272 -0
- package/dist/collector-role.js +109 -0
- package/dist/compliance-transport.js +5 -3
- package/dist/countersign-role.js +17 -0
- package/dist/diarist-role.js +8 -0
- package/dist/diarist.js +15 -0
- package/dist/doctor-role.js +80 -0
- package/dist/engine-detour-tool.js +51 -37
- package/dist/engine-detour.js +22 -11
- package/dist/factory-board.js +1478 -0
- package/dist/fixer-bash-seatbelt.js +13 -0
- package/dist/gatekeeper-role.js +1 -2
- package/dist/gh-api-runner.js +81 -0
- package/dist/gleaner-left-role.js +8 -0
- package/dist/headless-host/production-host.js +464 -355
- package/dist/headless-host/role-turn-host.js +691 -0
- package/dist/human-format.js +67 -0
- package/dist/inspector-role.js +12 -0
- package/dist/judge-recording-anti-forge.js +39 -0
- package/dist/judge-role.js +21 -3
- package/dist/merger-role.js +57 -0
- package/dist/migrate-book-topology.js +0 -2
- package/dist/navigator-role.js +6 -0
- package/dist/navigator-work-context.js +72 -0
- package/dist/notary-role.js +113 -0
- package/dist/oauth-keepalive.js +149 -0
- package/dist/package-contracts/terminating-infrastructure.js +0 -35
- package/dist/pi/adapter.js +272 -0
- package/dist/public-cli/cli.js +9 -3
- package/dist/public-cli/instruction-seat-run.js +163 -44
- package/dist/public-cli/main.js +241 -123
- package/dist/report-ticket-tool.js +24 -13
- package/dist/reviewer-role.js +33 -0
- package/dist/role-envelope.js +707 -0
- package/dist/role-runtime-dependencies.js +57 -0
- package/dist/role-runtime.js +1476 -0
- package/dist/secretariat-role.js +5 -0
- package/dist/stderr-jsonl.js +26 -0
- package/dist/submission-correctable-error.js +2 -6
- package/dist/submission-errors.js +0 -30
- package/dist/submission-gate.js +61 -54
- package/dist/submission-ledger.js +105 -98
- package/dist/ticket-snapshot.js +486 -0
- package/dist/ticket-trajectory.js +843 -0
- package/dist/worker-role.js +184 -0
- package/dist/worker-submission-gates.js +333 -0
- package/package.json +1 -1
- package/resources/engine-dispatch.md +6 -6
- package/resources/engines/agy.md +1 -1
- package/src/compliance-transport.ts +7 -3
- package/src/engine-detour-tool.ts +88 -57
- package/src/engine-detour.ts +31 -14
- package/src/gatekeeper-role.ts +1 -15
- package/src/host-contracts.ts +9 -12
- package/src/judge-role.ts +30 -7
- package/src/package-contracts/terminating-infrastructure.ts +8 -81
- package/src/pi/adapter.ts +0 -1
- package/src/public-cli/cli.ts +21 -14
- package/src/public-cli/instruction-seat-run.ts +227 -44
- package/src/report-ticket-tool.ts +27 -13
- package/src/role-envelope.ts +35 -35
- package/src/role-runtime.ts +63 -56
- package/src/submission-correctable-error.ts +1 -6
- package/src/submission-errors.ts +0 -34
- package/src/submission-gate.ts +71 -56
- package/src/submission-ledger.ts +109 -104
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { sessionDirectoryOf, sessionFileOf } from "../role-run-placement.js";
|
|
4
|
+
import { createSerializedRoleTurnHost, driveExternalRoleTurnRounds, disposeExternalRoleTurn, externalHostFailure as failure, raceAgainstHostAbort, recordTurnDelivery, retainPackageFaultBesideLivePlacement, syncTurnRequestLivePlacement, trySyncTurnRequestLivePlacement, } from "../external-host-turn-loop.js";
|
|
5
|
+
import { describeErrorIdentity } from "../public-cli/run-lifecycle.js";
|
|
6
|
+
import { projectThrownFailureLeaf } from "../public-cli/settlement.js";
|
|
7
|
+
import { copyAndRecordHostDossier, recordNativeSessionPointer, } from "../host-session-record.js";
|
|
8
|
+
import { NAVIGATOR_OUTPUT_TOOL_NAME, navigatorProseFromUnknown, } from "../package-contracts/navigator-output.js";
|
|
9
|
+
import { renderSystemPromptOverride, resolveBoundHostSessionId, } from "../prepared-role-turn.js";
|
|
10
|
+
import { acpModelId } from "./description.js";
|
|
11
|
+
import { isRecord } from "../unknown-value.js";
|
|
12
|
+
/**
|
|
13
|
+
* #959: collect free-form agent text from ACP session/update stream.
|
|
14
|
+
* Used only when the navigator seat spoke prose without calling the output tool.
|
|
15
|
+
* ACP agent speech is only agent_message / agent_message_chunk — never user,
|
|
16
|
+
* thought, or other *_message kinds (load replay must not poison the bucket).
|
|
17
|
+
*
|
|
18
|
+
* Standard ACP nests under params.update: { sessionUpdate, content }.
|
|
19
|
+
* Flat params.sessionUpdate / string params.update remain accepted for host variants.
|
|
20
|
+
*/
|
|
21
|
+
function acpAgentTextChunk(params) {
|
|
22
|
+
let kind;
|
|
23
|
+
let content;
|
|
24
|
+
let textFallback;
|
|
25
|
+
const nested = params.update;
|
|
26
|
+
if (isRecord(nested)) {
|
|
27
|
+
const record = nested;
|
|
28
|
+
kind = record.sessionUpdate;
|
|
29
|
+
content = record.content;
|
|
30
|
+
textFallback = record.text;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
kind = params.sessionUpdate ?? nested;
|
|
34
|
+
content = params.content;
|
|
35
|
+
textFallback = params.text;
|
|
36
|
+
}
|
|
37
|
+
if (kind !== "agent_message_chunk" && kind !== "agent_message")
|
|
38
|
+
return undefined;
|
|
39
|
+
if (typeof content === "string" && content.length > 0)
|
|
40
|
+
return content;
|
|
41
|
+
if (isRecord(content)) {
|
|
42
|
+
const record = content;
|
|
43
|
+
if (typeof record.text === "string" && record.text.length > 0)
|
|
44
|
+
return record.text;
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(content)) {
|
|
47
|
+
const parts = [];
|
|
48
|
+
for (const part of content) {
|
|
49
|
+
if (typeof part === "string" && part.length > 0)
|
|
50
|
+
parts.push(part);
|
|
51
|
+
else if (typeof part === "object" && part !== null) {
|
|
52
|
+
const record = part;
|
|
53
|
+
if (typeof record.text === "string" && record.text.length > 0)
|
|
54
|
+
parts.push(record.text);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (parts.length > 0)
|
|
58
|
+
return parts.join("");
|
|
59
|
+
}
|
|
60
|
+
if (typeof textFallback === "string" && textFallback.length > 0)
|
|
61
|
+
return textFallback;
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
function acpError(code, message, cause) {
|
|
65
|
+
return Object.assign(new Error(message, cause === undefined ? undefined : { cause }), { code });
|
|
66
|
+
}
|
|
67
|
+
/** One ACP JSON-RPC stdio process. Natural close/SIGTERM are its only lifecycle exits. */
|
|
68
|
+
export function connectAcpStdio(options) {
|
|
69
|
+
const child = spawn(options.binary, [...options.args], { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
70
|
+
const pending = new Map();
|
|
71
|
+
const notificationHandlers = [];
|
|
72
|
+
if (options.onNotification !== undefined)
|
|
73
|
+
notificationHandlers.push(options.onNotification);
|
|
74
|
+
let nextId = 0;
|
|
75
|
+
let closed = false;
|
|
76
|
+
let terminalError;
|
|
77
|
+
let spawned = false;
|
|
78
|
+
let stoppedByPackage = false;
|
|
79
|
+
let nativeExit;
|
|
80
|
+
let rpcFailure;
|
|
81
|
+
child.once("spawn", () => { spawned = true; });
|
|
82
|
+
// Native stderr bytes stay separate from transport exception diagnostics.
|
|
83
|
+
let stderr = "";
|
|
84
|
+
const settleClosed = (error) => {
|
|
85
|
+
if (closed)
|
|
86
|
+
return;
|
|
87
|
+
closed = true;
|
|
88
|
+
terminalError = error;
|
|
89
|
+
for (const waiter of pending.values())
|
|
90
|
+
waiter.reject(error);
|
|
91
|
+
pending.clear();
|
|
92
|
+
};
|
|
93
|
+
// Framing corruption or a known required capability with unusable shape still
|
|
94
|
+
// closes the child. Unknown client methods are answered in-band (JSON-RPC
|
|
95
|
+
// method not found) and do not terminate the leg (#760).
|
|
96
|
+
const terminate = (error) => {
|
|
97
|
+
settleClosed(error);
|
|
98
|
+
child.stdin.end();
|
|
99
|
+
stoppedByPackage = child.kill("SIGTERM") || stoppedByPackage;
|
|
100
|
+
};
|
|
101
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
102
|
+
stderr += chunk;
|
|
103
|
+
}).on("error", terminate);
|
|
104
|
+
child.on("error", settleClosed);
|
|
105
|
+
child.stdin.on("error", terminate);
|
|
106
|
+
createInterface({ input: child.stdout }).on("error", terminate).on("line", (line) => {
|
|
107
|
+
try {
|
|
108
|
+
const message = JSON.parse(line);
|
|
109
|
+
if (typeof message.method === "string") {
|
|
110
|
+
const params = typeof message.params === "object" && message.params !== null
|
|
111
|
+
? message.params : {};
|
|
112
|
+
for (const handler of notificationHandlers)
|
|
113
|
+
handler(message.method, params);
|
|
114
|
+
if (typeof message.id === "number") {
|
|
115
|
+
if (message.method !== "session/request_permission") {
|
|
116
|
+
// Vendor extensions (_x.ai/*, …) and any other unhandled client request:
|
|
117
|
+
// JSON-RPC method-not-found reply; session continues (#760).
|
|
118
|
+
child.stdin.write(`${JSON.stringify({
|
|
119
|
+
jsonrpc: "2.0",
|
|
120
|
+
id: message.id,
|
|
121
|
+
error: { code: -32601, message: `Method not found: ${message.method}` },
|
|
122
|
+
})}\n`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const choices = Array.isArray(params.options) ? params.options : [];
|
|
126
|
+
const selected = choices.find((value) => typeof value === "object" && value !== null && value.kind === "allow_once");
|
|
127
|
+
if (typeof selected?.optionId !== "string") {
|
|
128
|
+
terminate(acpError("acp-permission-missing-allow-once", "ACP permission request omitted allow_once"));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { outcome: { outcome: "selected", optionId: selected.optionId } } })}\n`);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (typeof message.id !== "number")
|
|
136
|
+
return;
|
|
137
|
+
const waiter = pending.get(message.id);
|
|
138
|
+
if (waiter === undefined)
|
|
139
|
+
return;
|
|
140
|
+
pending.delete(message.id);
|
|
141
|
+
if (message.error !== undefined) {
|
|
142
|
+
const diagnostic = typeof message.error === "object" && message.error !== null
|
|
143
|
+
&& "message" in message.error && typeof message.error.message === "string"
|
|
144
|
+
? message.error.message : JSON.stringify(message.error);
|
|
145
|
+
rpcFailure ??= { diagnostic, details: message };
|
|
146
|
+
waiter.reject(acpError("acp-upstream-error", diagnostic, message));
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
waiter.resolve((message.result ?? {}));
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
terminate(error);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const processClosed = new Promise((resolve) => {
|
|
156
|
+
child.once("close", (code, signal) => {
|
|
157
|
+
// ENOENT also emits close, but no host process was launched in that case.
|
|
158
|
+
if (spawned)
|
|
159
|
+
nativeExit = { code, timedOut: false, ...(signal === null ? {} : { signal }) };
|
|
160
|
+
settleClosed(acpError("acp-closed", "ACP process closed"));
|
|
161
|
+
resolve();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
return Promise.resolve({
|
|
165
|
+
request(method, params) {
|
|
166
|
+
if (closed)
|
|
167
|
+
return Promise.reject(terminalError);
|
|
168
|
+
const id = ++nextId;
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
pending.set(id, { resolve, reject });
|
|
171
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`, (error) => {
|
|
172
|
+
if (error === null || error === undefined)
|
|
173
|
+
return;
|
|
174
|
+
const waiter = pending.get(id);
|
|
175
|
+
if (waiter === undefined)
|
|
176
|
+
return;
|
|
177
|
+
pending.delete(id);
|
|
178
|
+
waiter.reject(error);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
notify(method, params) {
|
|
183
|
+
if (closed)
|
|
184
|
+
throw terminalError;
|
|
185
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
186
|
+
},
|
|
187
|
+
onNotification(handler) {
|
|
188
|
+
notificationHandlers.push(handler);
|
|
189
|
+
},
|
|
190
|
+
stderr() {
|
|
191
|
+
return stderr;
|
|
192
|
+
},
|
|
193
|
+
result() {
|
|
194
|
+
if (nativeExit === undefined && rpcFailure === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
return {
|
|
197
|
+
...(nativeExit ?? { code: null, timedOut: false }),
|
|
198
|
+
stderr,
|
|
199
|
+
...(rpcFailure === undefined ? {} : { knownFailure: rpcFailure }),
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
async close() {
|
|
203
|
+
if (!closed)
|
|
204
|
+
terminate(acpError("acp-connection-closed", "ACP connection is closed"));
|
|
205
|
+
await processClosed;
|
|
206
|
+
return !stoppedByPackage && nativeExit !== undefined ? { ...nativeExit, stderr } : undefined;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/** ACP last hop (#820): session open/load/close, prompt, MCP mount, capability/model. */
|
|
211
|
+
export function createAcpRoleTurnHost(config) {
|
|
212
|
+
return createSerializedRoleTurnHost(async (request) => {
|
|
213
|
+
const prepared = await config.prepare(request);
|
|
214
|
+
const systemPromptOverride = renderSystemPromptOverride(prepared.systemPrompt);
|
|
215
|
+
let connection;
|
|
216
|
+
let sessionId;
|
|
217
|
+
let sessionOpened = false;
|
|
218
|
+
let accepted = false;
|
|
219
|
+
// Re-resolve after mid-turn report-ticket may have moved principal (#1171).
|
|
220
|
+
const liveSessionParent = () => config.sessionIdentity.resolveSessionFile(request.principal);
|
|
221
|
+
let outcome = failure("session", "AcpNoOutcome", "no-outcome");
|
|
222
|
+
let reportedFailure;
|
|
223
|
+
let packageFailure;
|
|
224
|
+
try {
|
|
225
|
+
if (prepared.mcpServers.length === 0) {
|
|
226
|
+
outcome = failure("activation", "UncontrolledAcpSession", "ak-config-missing");
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
connection = await config.connect(request);
|
|
230
|
+
const rpc = (method, params) => connection.request(method, params);
|
|
231
|
+
// #959: navigator free-form agent text — only while session/prompt is in flight.
|
|
232
|
+
// session/load replays history via session/update; those must not enter the bucket
|
|
233
|
+
// (resume / set_model load would otherwise prepend prior turns as "this turn" prose).
|
|
234
|
+
const agentProseChunks = [];
|
|
235
|
+
let collectAgentProse = false;
|
|
236
|
+
connection.onNotification?.((method, params) => {
|
|
237
|
+
const isSessionUpdate = method === "session/update";
|
|
238
|
+
if (!isSessionUpdate)
|
|
239
|
+
return;
|
|
240
|
+
if (collectAgentProse) {
|
|
241
|
+
const chunk = acpAgentTextChunk(params);
|
|
242
|
+
if (chunk !== undefined)
|
|
243
|
+
agentProseChunks.push(chunk);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
const initialized = await rpc("initialize", {
|
|
247
|
+
protocolVersion: 1,
|
|
248
|
+
clientCapabilities: {},
|
|
249
|
+
});
|
|
250
|
+
const initializeMeta = initialized._meta;
|
|
251
|
+
const availableModels = Array.isArray(initializeMeta?.modelState?.availableModels)
|
|
252
|
+
? initializeMeta.modelState.availableModels
|
|
253
|
+
: undefined;
|
|
254
|
+
if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) => typeof entry === "object" && entry !== null
|
|
255
|
+
&& entry.modelId === acpModelId(config, request.model))) {
|
|
256
|
+
outcome = failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
257
|
+
provider: request.model.provider,
|
|
258
|
+
model: request.model.model,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const sessionBindParams = {
|
|
263
|
+
cwd: request.cwd,
|
|
264
|
+
mcpServers: prepared.mcpServers,
|
|
265
|
+
_meta: { systemPromptOverride, yoloMode: false },
|
|
266
|
+
};
|
|
267
|
+
const loadSession = async (bindSessionId) => {
|
|
268
|
+
const loaded = await rpc("session/load", {
|
|
269
|
+
sessionId: bindSessionId,
|
|
270
|
+
...sessionBindParams,
|
|
271
|
+
});
|
|
272
|
+
const loadedId = typeof loaded.sessionId === "string" && loaded.sessionId !== ""
|
|
273
|
+
? loaded.sessionId
|
|
274
|
+
: bindSessionId;
|
|
275
|
+
if (config.hostName !== "hermes")
|
|
276
|
+
recordNativeSessionPointer({
|
|
277
|
+
host: config.hostName, sessionId: loadedId, cwd: request.cwd, sessionParent: liveSessionParent(), home: request.home,
|
|
278
|
+
});
|
|
279
|
+
return loadedId;
|
|
280
|
+
};
|
|
281
|
+
let sessionReady = true;
|
|
282
|
+
if (request.continuation.kind === "resume") {
|
|
283
|
+
const boundSessionId = await resolveBoundHostSessionId(request, config.sessionIdentity);
|
|
284
|
+
if (boundSessionId !== undefined && boundSessionId !== "") {
|
|
285
|
+
sessionId = boundSessionId;
|
|
286
|
+
sessionId = await loadSession(boundSessionId);
|
|
287
|
+
sessionOpened = true;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
outcome = failure("session", "AcpSessionFailure", "session-id-missing", undefined, "resume requires a bound session id");
|
|
291
|
+
sessionReady = false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const session = await rpc("session/new", sessionBindParams);
|
|
296
|
+
sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
|
|
297
|
+
if (sessionId === undefined || sessionId === "") {
|
|
298
|
+
outcome = failure("session", "AcpSessionFailure", "session-id-missing");
|
|
299
|
+
sessionReady = false;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
if (config.hostName !== "hermes")
|
|
303
|
+
recordNativeSessionPointer({
|
|
304
|
+
host: config.hostName, sessionId, cwd: request.cwd, sessionParent: liveSessionParent(), home: request.home,
|
|
305
|
+
});
|
|
306
|
+
sessionOpened = true;
|
|
307
|
+
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (sessionReady) {
|
|
311
|
+
// set_model host-native catalog id (#778 / #1146); may rebuild agent — re-bind via loadSession.
|
|
312
|
+
if (config.modelPassing === "set_model" && request.model !== undefined && sessionId !== undefined) {
|
|
313
|
+
await rpc("session/set_model", {
|
|
314
|
+
sessionId,
|
|
315
|
+
modelId: acpModelId(config, request.model),
|
|
316
|
+
});
|
|
317
|
+
sessionId = await loadSession(sessionId);
|
|
318
|
+
}
|
|
319
|
+
const activeConnection = connection;
|
|
320
|
+
const activeSessionId = sessionId;
|
|
321
|
+
const turnResult = await driveExternalRoleTurnRounds(prepared, request, {
|
|
322
|
+
roundLimitName: "AcpRoundLimit",
|
|
323
|
+
currentSessionId: () => sessionId,
|
|
324
|
+
async runRound({ prompt, abortSignal }) {
|
|
325
|
+
// One history row per actual session/prompt start (催交回合各算一次).
|
|
326
|
+
// MCP advertises tool.parameters; prepared.jsonSchema is the headless
|
|
327
|
+
// draft-07 stamp of that clone — record the delivered parameters face.
|
|
328
|
+
const { $schema: _headlessStamp, ...deliveredSchema } = prepared.jsonSchema;
|
|
329
|
+
await recordTurnDelivery(await syncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority), {
|
|
330
|
+
systemPrompt: systemPromptOverride,
|
|
331
|
+
outputSchema: deliveredSchema,
|
|
332
|
+
}, "acp-host");
|
|
333
|
+
let result;
|
|
334
|
+
// Open the prose gate only for this prompt round; clear any stale chunks first.
|
|
335
|
+
agentProseChunks.length = 0;
|
|
336
|
+
collectAgentProse = prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME;
|
|
337
|
+
try {
|
|
338
|
+
result = await raceAgainstHostAbort(activeConnection.request("session/prompt", {
|
|
339
|
+
sessionId: activeSessionId,
|
|
340
|
+
prompt: [{ type: "text", text: prompt }],
|
|
341
|
+
}), abortSignal, "ACP host aborted");
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
collectAgentProse = false;
|
|
345
|
+
agentProseChunks.length = 0;
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
collectAgentProse = false;
|
|
349
|
+
if (result.stopReason === "refusal") {
|
|
350
|
+
agentProseChunks.length = 0;
|
|
351
|
+
reportedFailure = { diagnostic: result.stopReason, details: result };
|
|
352
|
+
return {
|
|
353
|
+
status: "terminal",
|
|
354
|
+
result: {
|
|
355
|
+
code: null,
|
|
356
|
+
stderr: activeConnection.stderr?.() ?? "",
|
|
357
|
+
timedOut: false,
|
|
358
|
+
knownFailure: reportedFailure,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// #959: navigator prose exit when the model spoke without the output tool.
|
|
363
|
+
// Tool path still wins via MCP; ingest is a no-op once the tool already sealed.
|
|
364
|
+
// Emptiness via shared projector; payload keeps original bytes (LLM 原话过手).
|
|
365
|
+
if (prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
366
|
+
const prose = agentProseChunks.join("");
|
|
367
|
+
agentProseChunks.length = 0;
|
|
368
|
+
if (navigatorProseFromUnknown(prose) !== undefined) {
|
|
369
|
+
await prepared.ingestStructuredOutput({ prose });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
agentProseChunks.length = 0;
|
|
374
|
+
}
|
|
375
|
+
return { status: "delivered", stderr: activeConnection.stderr?.() ?? "" };
|
|
376
|
+
},
|
|
377
|
+
async afterAccepted() {
|
|
378
|
+
await rpc("session/close", { sessionId: activeSessionId });
|
|
379
|
+
accepted = true;
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
outcome = turnResult;
|
|
383
|
+
} // sessionReady
|
|
384
|
+
} // model match else
|
|
385
|
+
} // mcpServers else
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
const native = connection?.result?.();
|
|
389
|
+
reportedFailure = native?.knownFailure;
|
|
390
|
+
outcome = native ?? { code: null, stderr: connection?.stderr?.() ?? "", timedOut: false };
|
|
391
|
+
await retainPackageFaultBesideLivePlacement(request, {
|
|
392
|
+
diagnostic: `ACP turn exception beside native host facts: ${describeErrorIdentity(error)}`,
|
|
393
|
+
error,
|
|
394
|
+
});
|
|
395
|
+
// Native abnormal exit / RPC report stays primary. Otherwise this is a
|
|
396
|
+
// real package exception, not a host cause inferred from missing output.
|
|
397
|
+
if (outcome.knownFailure === undefined && !outcome.timedOut && outcome.signal === undefined
|
|
398
|
+
&& (native === undefined || outcome.code === 0 || outcome.code === null)) {
|
|
399
|
+
packageFailure = projectThrownFailureLeaf(error);
|
|
400
|
+
outcome = { ...outcome, knownFailure: packageFailure };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
let naturalTermination = undefined;
|
|
405
|
+
if (connection !== undefined) {
|
|
406
|
+
if (sessionId !== undefined && !accepted) {
|
|
407
|
+
try {
|
|
408
|
+
connection.notify("session/cancel", { sessionId });
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
await retainPackageFaultBesideLivePlacement(request, {
|
|
412
|
+
diagnostic: `session cancel failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
413
|
+
error,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
naturalTermination = await connection.close();
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
await retainPackageFaultBesideLivePlacement(request, {
|
|
422
|
+
diagnostic: `connection close failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
423
|
+
error,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Both success and failure drain the native stderr stream, including
|
|
428
|
+
// bytes arriving after the last RPC response.
|
|
429
|
+
outcome = { ...outcome, stderr: connection?.stderr?.() ?? outcome.stderr };
|
|
430
|
+
// On abnormal paths, teardown has now drained the real close/stderr.
|
|
431
|
+
// A successful ACP protocol turn remains successful: its ordinary
|
|
432
|
+
// connection cleanup is not reclassified as a role failure.
|
|
433
|
+
const native = connection?.result?.() ?? naturalTermination;
|
|
434
|
+
if (native !== undefined && (naturalTermination !== undefined || outcome.code !== 0
|
|
435
|
+
|| outcome.knownFailure !== undefined || outcome.timedOut || outcome.signal !== undefined)) {
|
|
436
|
+
// A late native failure stays primary over a package failure. An own
|
|
437
|
+
// SIGTERM stop alone is ordinary disposal, not a guessed host cause.
|
|
438
|
+
const nativeFailed = (native.code !== null && native.code !== 0)
|
|
439
|
+
|| (naturalTermination !== undefined && (native.timedOut || native.signal !== undefined));
|
|
440
|
+
let knownFailure = outcome.knownFailure;
|
|
441
|
+
if (nativeFailed && knownFailure !== undefined && knownFailure !== reportedFailure) {
|
|
442
|
+
if (knownFailure !== packageFailure) {
|
|
443
|
+
await retainPackageFaultBesideLivePlacement(request, {
|
|
444
|
+
diagnostic: knownFailure.diagnostic ?? "ACP package failure beside late native termination",
|
|
445
|
+
error: knownFailure,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
knownFailure = reportedFailure;
|
|
449
|
+
}
|
|
450
|
+
const { knownFailure: _nativeFailure, ...facts } = native;
|
|
451
|
+
outcome = { ...facts, ...(knownFailure === undefined ? {} : { knownFailure }) };
|
|
452
|
+
}
|
|
453
|
+
if (config.hostName !== "hermes" && sessionOpened && sessionId !== undefined) {
|
|
454
|
+
const live = await trySyncTurnRequestLivePlacement(request, config.sessionIdentity.principalAuthority);
|
|
455
|
+
if ("runDirectory" in live) {
|
|
456
|
+
try {
|
|
457
|
+
copyAndRecordHostDossier({
|
|
458
|
+
host: config.hostName,
|
|
459
|
+
sessionId,
|
|
460
|
+
cwd: request.cwd,
|
|
461
|
+
sessionDirectory: sessionDirectoryOf(live.runDirectory),
|
|
462
|
+
sessionParent: sessionFileOf(live.runDirectory),
|
|
463
|
+
...(request.home !== undefined ? { home: request.home } : {}),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
await retainPackageFaultBesideLivePlacement(request, {
|
|
468
|
+
diagnostic: `host dossier copy failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
469
|
+
error,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
await retainPackageFaultBesideLivePlacement(request, {
|
|
475
|
+
diagnostic: `host dossier copy skipped: live run path resolve failed beside host terminal: ${describeErrorIdentity(live.resolveError)}`,
|
|
476
|
+
error: live.resolveError,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
outcome = await disposeExternalRoleTurn(prepared, request, outcome);
|
|
481
|
+
}
|
|
482
|
+
return outcome;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { lstatSync, realpathSync, statSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { errnoCode, errorText, } from "./activation-ledger-topology.js";
|
|
4
|
+
/**
|
|
5
|
+
* Typed missing durable session principal. Callers discriminate with instanceof/code;
|
|
6
|
+
* never by parsing message prose. Original filesystem causes are retained.
|
|
7
|
+
*/
|
|
8
|
+
export class ActivationSessionFileMissingError extends Error {
|
|
9
|
+
code = "AK_ACTIVATION_SESSION_FILE_MISSING";
|
|
10
|
+
path;
|
|
11
|
+
constructor(path, options) {
|
|
12
|
+
super(`Workflow role activation durable session file does not exist: ${path}`, options?.cause === undefined ? undefined : { cause: options.cause });
|
|
13
|
+
this.name = "ActivationSessionFileMissingError";
|
|
14
|
+
this.path = path;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function materializeDeferredSessionFile(sessionManager, resolvedFile) {
|
|
18
|
+
const header = sessionManager.getHeader?.();
|
|
19
|
+
if (header === null || header === undefined || header.type !== "session") {
|
|
20
|
+
throw new ActivationSessionFileMissingError(resolvedFile);
|
|
21
|
+
}
|
|
22
|
+
// Activation does not own record placement (ADR 0065 / #221). Parent directory is the
|
|
23
|
+
// SessionManager principal's own dir — write the header in place; do not pull ledgerHome
|
|
24
|
+
// containment or ensureRealDirectoryTree back onto this admission path.
|
|
25
|
+
try {
|
|
26
|
+
writeFileSync(resolvedFile, `${JSON.stringify(header)}\n`, { flag: "wx" });
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (errnoCode(error) !== "EEXIST") {
|
|
30
|
+
throw new Error(`Workflow role activation failed to materialize durable session file (${resolvedFile}): ${errorText(error)}`, { cause: error });
|
|
31
|
+
}
|
|
32
|
+
// Lost a create race — validate the winner below.
|
|
33
|
+
}
|
|
34
|
+
// Rebind full SessionManager so subsequent Pi appends use O_APPEND (flushed=true)
|
|
35
|
+
// instead of exclusive wx create against the file we just wrote.
|
|
36
|
+
if (typeof sessionManager.setSessionFile === "function") {
|
|
37
|
+
sessionManager.setSessionFile(resolvedFile);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Admit a durable Pi session file principal (ADR 0048/0065).
|
|
42
|
+
* Requires an existing regular file at admission (or a live SessionManager header that
|
|
43
|
+
* can be materialized onto the same path). Rejects relative paths, directories,
|
|
44
|
+
* non-files, and nonexistent paths that cannot be materialized. Original filesystem
|
|
45
|
+
* causes are retained.
|
|
46
|
+
*
|
|
47
|
+
* Record-placement (session must live under the ledger book) is NOT enforced here —
|
|
48
|
+
* that check lives on the activation ledger path. Activation only
|
|
49
|
+
* binds the durable principal identity for the role run.
|
|
50
|
+
*
|
|
51
|
+
* Upstream Pi defers exclusive create until the first assistant message. When the path
|
|
52
|
+
* is the live SessionManager principal and only the header is in memory, admission
|
|
53
|
+
* materializes that header onto the same path before the fact is written so the role
|
|
54
|
+
* fact never points at a session that may be created later.
|
|
55
|
+
*/
|
|
56
|
+
export function durableSessionPointer(sessionManager) {
|
|
57
|
+
const file = sessionManager.getSessionFile?.();
|
|
58
|
+
if (typeof file !== "string" || file.length === 0) {
|
|
59
|
+
throw new Error("Workflow role activation requires a durable Pi session file principal (getSessionFile); directory-only or --no-session invocations are rejected");
|
|
60
|
+
}
|
|
61
|
+
if (!isAbsolute(file)) {
|
|
62
|
+
throw new Error(`Workflow role activation requires an absolute durable session file path; got relative path: ${file}`);
|
|
63
|
+
}
|
|
64
|
+
const resolvedFile = resolve(file);
|
|
65
|
+
try {
|
|
66
|
+
lstatSync(resolvedFile);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (errnoCode(error) !== "ENOENT") {
|
|
70
|
+
throw new Error(`Workflow role activation failed to stat durable session file (${resolvedFile}): ${errorText(error)}`, { cause: error });
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
materializeDeferredSessionFile(sessionManager, resolvedFile);
|
|
74
|
+
}
|
|
75
|
+
catch (materializeError) {
|
|
76
|
+
// Missing principal: rethrow typed missing-file identity with the original ENOENT cause.
|
|
77
|
+
// Other materialize failures keep their own typed/native identity.
|
|
78
|
+
if (materializeError instanceof ActivationSessionFileMissingError) {
|
|
79
|
+
throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
|
|
80
|
+
}
|
|
81
|
+
throw materializeError;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let realFile;
|
|
85
|
+
try {
|
|
86
|
+
realFile = realpathSync(resolvedFile);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
|
|
90
|
+
}
|
|
91
|
+
let info;
|
|
92
|
+
try {
|
|
93
|
+
info = statSync(realFile);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new Error(`Workflow role activation failed to stat durable session file (${realFile}): ${errorText(error)}`, { cause: error });
|
|
97
|
+
}
|
|
98
|
+
if (info.isDirectory()) {
|
|
99
|
+
throw new Error(`Workflow role activation durable session principal must be a file, not a directory: ${realFile}`);
|
|
100
|
+
}
|
|
101
|
+
if (!info.isFile()) {
|
|
102
|
+
throw new Error(`Workflow role activation durable session principal is not a regular file: ${realFile}`);
|
|
103
|
+
}
|
|
104
|
+
return { kind: "session-file", path: realFile };
|
|
105
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation ledger surface: durable session principal + book/home topology.
|
|
3
|
+
* #855: two-face waiting.jsonl reconciliation deleted — this module no longer
|
|
4
|
+
* appends accepted-activation facts or own a waiting path.
|
|
5
|
+
*/
|
|
6
|
+
export { ActivationGitRepositoryRequiredError, resolveBookKeyFromGit, } from "./activation-ledger-git.js";
|
|
7
|
+
export { ActivationSessionFileMissingError, durableSessionPointer, } from "./activation-ledger-session.js";
|
|
8
|
+
export { ActivationLedgerError, activationBookDirectory, resolveActivationLedgerHome, } from "./activation-ledger-topology.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
const causeSchema = Type.Object({
|
|
3
|
+
identity: Type.String({ minLength: 1 }),
|
|
4
|
+
name: Type.String({ minLength: 1 }),
|
|
5
|
+
message: Type.String(),
|
|
6
|
+
evidenceId: Type.Optional(Type.String({ minLength: 1 })),
|
|
7
|
+
}, { additionalProperties: false });
|
|
8
|
+
export const activationTraceRecordSchema = Type.Object({
|
|
9
|
+
role: Type.String({ minLength: 1 }),
|
|
10
|
+
stageId: Type.String({ pattern: "^[a-z][a-z0-9-]*$" }),
|
|
11
|
+
status: Type.Literal("failed"),
|
|
12
|
+
timestamp: Type.String({ format: "date-time" }),
|
|
13
|
+
cause: causeSchema,
|
|
14
|
+
}, { additionalProperties: false });
|
|
15
|
+
let activationCauseEvidence = 0;
|
|
16
|
+
const retainedActivationCauses = new Map();
|
|
17
|
+
export function namedActivationCause(error) {
|
|
18
|
+
const evidenceId = `activation-cause-${++activationCauseEvidence}`;
|
|
19
|
+
retainedActivationCauses.set(evidenceId, error);
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
const code = error.code;
|
|
22
|
+
const name = error.name || "Error";
|
|
23
|
+
return { identity: typeof code === "string" && code.length > 0 ? code : name, name, message: error.message, evidenceId };
|
|
24
|
+
}
|
|
25
|
+
let message;
|
|
26
|
+
try {
|
|
27
|
+
message = typeof error === "string" ? error : JSON.stringify(error) ?? String(error);
|
|
28
|
+
return { identity: "UnknownThrownCause", name: "UnknownThrownCause", message, evidenceId };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { identity: "UnknownThrownCause", name: "UnknownThrownCause", message: String(error), evidenceId };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isRecord } from "./unknown-value.js";
|
|
2
|
+
/** Historical audit-escalation discriminator retained for reading pre-#1057 run artifacts. */
|
|
3
|
+
export const AUDIT_ESCALATION_KIND = "audit_escalation";
|
|
4
|
+
export function isAuditEscalationResult(value) {
|
|
5
|
+
return isRecord(value)
|
|
6
|
+
&& value.kind === AUDIT_ESCALATION_KIND;
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared audited-run identity for review-officer startup materials (#1166 / ADR 0087).
|
|
3
|
+
* Value form is `<runId>@<席>` only — no directory path.
|
|
4
|
+
*/
|
|
5
|
+
import { basename } from "node:path";
|
|
6
|
+
import { formatRunLeaf, parseRunLeaf } from "./role-run-placement.js";
|
|
7
|
+
export const AUDITED_RUN_IDENTITY_KIND = "audited-run-identity";
|
|
8
|
+
/** Identity leaf from a run directory basename; undefined when the leaf is not `runId@role`. */
|
|
9
|
+
export function auditedRunIdentityFromDirectory(runDirectory) {
|
|
10
|
+
const parsed = parseRunLeaf(basename(runDirectory));
|
|
11
|
+
if (parsed === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
return formatRunLeaf(parsed.runId, parsed.role);
|
|
14
|
+
}
|
|
15
|
+
export function auditedRunIdentityMaterial(identity) {
|
|
16
|
+
return { kind: AUDITED_RUN_IDENTITY_KIND, identity };
|
|
17
|
+
}
|
|
18
|
+
/** Identity from locator fields already in hand (notary source-run). */
|
|
19
|
+
export function auditedRunIdentityFromParts(runId, role) {
|
|
20
|
+
return formatRunLeaf(runId, role);
|
|
21
|
+
}
|