pi-amq 0.1.2 → 0.1.4

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/extension/runtime.d.ts.map +1 -1
  3. package/dist/extension/runtime.js +20 -12
  4. package/dist/extension/runtime.js.map +1 -1
  5. package/dist/extension/worker-runtime.d.ts.map +1 -1
  6. package/dist/extension/worker-runtime.js +20 -8
  7. package/dist/extension/worker-runtime.js.map +1 -1
  8. package/dist/extension/worker-tools.d.ts +2 -2
  9. package/dist/extension/worker-tools.d.ts.map +1 -1
  10. package/dist/extension/worker-tools.js +2 -1
  11. package/dist/extension/worker-tools.js.map +1 -1
  12. package/dist/integration/fleet-view.d.ts +21 -3
  13. package/dist/integration/fleet-view.d.ts.map +1 -1
  14. package/dist/integration/fleet-view.js +104 -9
  15. package/dist/integration/fleet-view.js.map +1 -1
  16. package/dist/integration/launch-preview.d.ts.map +1 -1
  17. package/dist/integration/launch-preview.js +21 -3
  18. package/dist/integration/launch-preview.js.map +1 -1
  19. package/dist/transport/runtime-status.d.ts.map +1 -1
  20. package/dist/transport/runtime-status.js +19 -4
  21. package/dist/transport/runtime-status.js.map +1 -1
  22. package/dist/workflow/run-store.d.ts +3 -2
  23. package/dist/workflow/run-store.d.ts.map +1 -1
  24. package/dist/workflow/run-store.js +4 -4
  25. package/dist/workflow/run-store.js.map +1 -1
  26. package/dist/workflow/worker-child-extension.d.ts.map +1 -1
  27. package/dist/workflow/worker-child-extension.js +104 -32
  28. package/dist/workflow/worker-child-extension.js.map +1 -1
  29. package/dist/workflow/worker-launch.d.ts +7 -4
  30. package/dist/workflow/worker-launch.d.ts.map +1 -1
  31. package/dist/workflow/worker-launch.js +38 -5
  32. package/dist/workflow/worker-launch.js.map +1 -1
  33. package/dist/workflow/worker-process-supervisor.d.ts +1 -0
  34. package/dist/workflow/worker-process-supervisor.d.ts.map +1 -1
  35. package/dist/workflow/worker-process-supervisor.js +131 -77
  36. package/dist/workflow/worker-process-supervisor.js.map +1 -1
  37. package/dist/workflow/workflow-types.d.ts +11 -3
  38. package/dist/workflow/workflow-types.d.ts.map +1 -1
  39. package/dist/workflow/workflow-types.js.map +1 -1
  40. package/docs/conformance.md +3 -3
  41. package/docs/contracts/deferred-findings.md +49 -0
  42. package/docs/contracts/worker-runtime.md +1 -1
  43. package/docs/design.md +4 -4
  44. package/docs/operator-guide.md +2 -2
  45. package/extension/runtime.ts +20 -11
  46. package/extension/worker-runtime.ts +21 -8
  47. package/extension/worker-tools.ts +2 -1
  48. package/integration/fleet-view.ts +138 -10
  49. package/integration/launch-preview.ts +26 -3
  50. package/package.json +1 -1
  51. package/transport/runtime-status.ts +15 -4
  52. package/workflow/run-store.ts +9 -7
  53. package/workflow/worker-child-extension.ts +116 -34
  54. package/workflow/worker-launch.ts +51 -12
  55. package/workflow/worker-process-supervisor.ts +160 -75
  56. package/workflow/workflow-types.ts +13 -3
  57. package/docs//346/250/241/345/236/213/345/274/272/345/272/246/344/270/216Harness/345/244/215/346/235/202/345/272/246.md +0 -1011
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-amq",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A daemonless AMQ bridge and governed worker runtime for pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -59,12 +59,23 @@ export function projectInboxStatus(
59
59
  if (mode === "print" || mode === "json") return { surface: "none" };
60
60
  const value = cloneStatus(status);
61
61
  if (mode === "rpc") return { surface: "rpc", value };
62
- const backoff = status.nextBackoffMs === null ? "" : ` ${status.nextBackoffMs}ms`;
63
- const counts = status.staged;
64
- const error = status.lastError ? ` error=${bounded(status.lastError)}` : "";
62
+ if (status.receiver === "dormant") return { surface: "none" };
63
+ const parts = ["AMQ"];
64
+ if (status.watch === "backoff" || status.watch === "recovering") {
65
+ parts.push(status.nextBackoffMs === null
66
+ ? "reconnecting"
67
+ : `reconnecting in ${status.nextBackoffMs}ms`);
68
+ } else {
69
+ parts.push(status.receiver);
70
+ }
71
+ const counts = Object.entries(status.staged)
72
+ .filter(([, count]) => count > 0)
73
+ .map(([priority, count]) => `${count} ${priority}`);
74
+ if (counts.length > 0) parts.push(counts.join(", "));
75
+ if (status.lastError) parts.push(bounded(status.lastError));
65
76
  return {
66
77
  surface: "tui",
67
- text: `AMQ ${status.receiver}/${status.watch}${backoff} u=${counts.urgent} n=${counts.normal} l=${counts.low}${error}`,
78
+ text: parts.join(" · "),
68
79
  value,
69
80
  };
70
81
  }
@@ -47,10 +47,11 @@ import type {
47
47
  WorkerProposalProvenance,
48
48
  WorkerRequest,
49
49
  WorkerRoleContract,
50
- WorkerLaunchContractV1,
50
+ WorkerLaunchContract,
51
+ WorkerLaunchContractV2,
51
52
  WorkspaceGrant,
52
53
  } from "./workflow-types.js";
53
- import { parseWorkerLaunchContractV1 } from "./worker-launch.js";
54
+ import { parseWorkerLaunchContract, parseWorkerLaunchContractV2 } from "./worker-launch.js";
54
55
  import {
55
56
  UNLIMITED,
56
57
  budgetExceeded,
@@ -472,7 +473,8 @@ export class WorkflowRunStore {
472
473
  readonly workerAttemptGeneration: string;
473
474
  readonly leaseGeneration: string;
474
475
  readonly processIdentity: ProcessIdentity;
475
- readonly launchContract: WorkerLaunchContractV1;
476
+ /** Write side is schema 2 only; schema 1 stays readable history and never launches a child. */
477
+ readonly launchContract: WorkerLaunchContractV2;
476
478
  }): Promise<RunProjectionV2> {
477
479
  const requestKey = validateRequestKey(inputRequestKey);
478
480
  return this.#withRunMutation(requestKey, async (current, events) => {
@@ -496,7 +498,7 @@ export class WorkflowRunStore {
496
498
  workerAttemptGeneration: input.workerAttemptGeneration,
497
499
  leaseGeneration: requireNonEmpty(input.leaseGeneration, "lease generation"),
498
500
  processIdentity: parseProcessIdentity(input.processIdentity),
499
- launchContract: parseWorkerLaunchContractV1(input.launchContract),
501
+ launchContract: parseWorkerLaunchContractV2(input.launchContract),
500
502
  }),
501
503
  };
502
504
  });
@@ -2186,7 +2188,7 @@ function parseAttemptStartPayload(value: unknown): {
2186
2188
  readonly workerAttemptGeneration: string;
2187
2189
  readonly leaseGeneration: string;
2188
2190
  readonly processIdentity: ProcessIdentity;
2189
- readonly launchContract: WorkerLaunchContractV1;
2191
+ readonly launchContract: WorkerLaunchContract;
2190
2192
  } {
2191
2193
  const object = strictRecord(
2192
2194
  value,
@@ -2199,7 +2201,7 @@ function parseAttemptStartPayload(value: unknown): {
2199
2201
  workerAttemptGeneration: nonEmptyString(object, "workerAttemptGeneration"),
2200
2202
  leaseGeneration: nonEmptyString(object, "leaseGeneration"),
2201
2203
  processIdentity: parseProcessIdentity(object["processIdentity"]),
2202
- launchContract: parseWorkerLaunchContractV1(object["launchContract"]),
2204
+ launchContract: parseWorkerLaunchContract(object["launchContract"]),
2203
2205
  };
2204
2206
  }
2205
2207
 
@@ -2590,7 +2592,7 @@ function parseWorkerAttempt(value: unknown): WorkerAttemptProjection {
2590
2592
  },
2591
2593
  launchContract: object["launchContract"] === null
2592
2594
  ? null
2593
- : parseWorkerLaunchContractV1(object["launchContract"]),
2595
+ : parseWorkerLaunchContract(object["launchContract"]),
2594
2596
  ...(modelSelection ? {
2595
2597
  modelSelection: {
2596
2598
  model: parseModelSpec(modelSelection["model"]),
@@ -1,25 +1,39 @@
1
- import { createReadStream, readFileSync } from "node:fs";
1
+ import { createReadStream, readFileSync, writeSync } from "node:fs";
2
2
 
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { Type } from "typebox";
5
5
 
6
6
  import {
7
- parseWorkerLaunchContractV1,
7
+ parseWorkerLaunchContractV2,
8
8
  validateWorkerChildContext,
9
9
  type WorkerChildContext,
10
10
  } from "./worker-launch.js";
11
11
  import { classifyProviderFallbackFailure } from "./model-fallback.js";
12
- import type { WorkerLaunchContractV1 } from "./workflow-types.js";
12
+ import type { WorkerLaunchContractV2 } from "./workflow-types.js";
13
+
14
+ type ProtocolWriter = (value: unknown) => void;
13
15
 
14
16
  export default function workerChildExtension(pi: ExtensionAPI): void {
15
17
  const loaded = loadLaunchContract();
16
18
  if (!loaded.ok) {
17
- pi.on("session_start", (_event, ctx) => refuse(loaded.error, ctx));
18
- pi.on("before_agent_start", (_event, ctx) => refuse(loaded.error, ctx));
19
+ pi.on("session_start", (_event, ctx) => refuseWithoutProtocol(loaded.error, ctx));
20
+ pi.on("before_agent_start", (_event, ctx) => refuseWithoutProtocol(loaded.error, ctx));
19
21
  return;
20
22
  }
21
23
  const contract = loaded.contract;
24
+ let writeProtocol: ProtocolWriter;
25
+ try {
26
+ writeProtocol = createProtocolWriter(contract);
27
+ } catch (error) {
28
+ pi.on("session_start", (_event, ctx) => refuseWithoutProtocol(error, ctx));
29
+ pi.on("before_agent_start", (_event, ctx) => refuseWithoutProtocol(error, ctx));
30
+ return;
31
+ }
22
32
  let ready = false;
33
+ // The inherited liveness descriptor is single-use for this process: releasing it closes fd 3,
34
+ // so a later re-attach could never succeed. Track the state explicitly instead of detaching
35
+ // and reopening, which would surface as a silent shutdown rather than a visible refusal.
36
+ const liveness = createParentLivenessGuard(contract);
23
37
 
24
38
  pi.registerTool({
25
39
  name: "message_worker",
@@ -31,7 +45,7 @@ export default function workerChildExtension(pi: ExtensionAPI): void {
31
45
  correlation: Type.String({ minLength: 1, maxLength: 256 }),
32
46
  }),
33
47
  async execute(_id, params) {
34
- writeProtocolLine({
48
+ writeProtocol({
35
49
  type: "pi_amq_worker_peer_message",
36
50
  runId: contract.runId,
37
51
  workerId: contract.workerId,
@@ -56,7 +70,7 @@ export default function workerChildExtension(pi: ExtensionAPI): void {
56
70
  correlation: Type.String({ minLength: 1, maxLength: 256 }),
57
71
  }),
58
72
  async execute(_id, params) {
59
- writeProtocolLine({
73
+ writeProtocol({
60
74
  type: "pi_amq_worker_wait",
61
75
  runId: contract.runId,
62
76
  workerId: contract.workerId,
@@ -78,10 +92,10 @@ export default function workerChildExtension(pi: ExtensionAPI): void {
78
92
  pi.on("session_start", (_event, ctx) => {
79
93
  try {
80
94
  validateWorkerChildContext(contract, childContext(ctx));
81
- attachParentLiveness(contract, ctx);
95
+ liveness.attach(ctx);
82
96
  const sessionFile = ctx.sessionManager.getSessionFile();
83
97
  if (!sessionFile) throw new Error("Pi worker session file is unavailable");
84
- writeProtocolLine({
98
+ writeProtocol({
85
99
  type: "pi_amq_worker_ready",
86
100
  runId: contract.runId,
87
101
  workerId: contract.workerId,
@@ -92,7 +106,7 @@ export default function workerChildExtension(pi: ExtensionAPI): void {
92
106
  });
93
107
  ready = true;
94
108
  } catch (error) {
95
- refuse(error, ctx);
109
+ refuse(error, ctx, writeProtocol);
96
110
  }
97
111
  });
98
112
 
@@ -101,15 +115,16 @@ export default function workerChildExtension(pi: ExtensionAPI): void {
101
115
  if (!ready) throw new Error("Worker launch parity was not established");
102
116
  validateWorkerChildContext(contract, childContext(ctx, event.systemPromptOptions.selectedTools));
103
117
  } catch (error) {
104
- refuse(error, ctx);
118
+ refuse(error, ctx, writeProtocol);
105
119
  }
106
120
  });
107
121
 
108
- pi.on("model_select", (_event, ctx) => revalidateOrRefuse(contract, ctx));
109
- pi.on("thinking_level_select", (_event, ctx) => revalidateOrRefuse(contract, ctx));
122
+ pi.on("model_select", (_event, ctx) => revalidateOrRefuse(contract, ctx, writeProtocol));
123
+ pi.on("thinking_level_select", (_event, ctx) => revalidateOrRefuse(contract, ctx, writeProtocol));
124
+ pi.on("session_shutdown", () => liveness.release());
110
125
  pi.on("agent_settled", () => {
111
126
  if (!ready) return;
112
- writeProtocolLine({
127
+ writeProtocol({
113
128
  type: "pi_amq_worker_settled",
114
129
  runId: contract.runId,
115
130
  workerId: contract.workerId,
@@ -123,7 +138,7 @@ export default function workerChildExtension(pi: ExtensionAPI): void {
123
138
  if (message.role !== "assistant") return;
124
139
  const reason = classifyProviderFallbackFailure(message.stopReason, message.errorMessage);
125
140
  if (!reason) return;
126
- writeProtocolLine({
141
+ writeProtocol({
127
142
  type: "pi_amq_worker_provider_failure",
128
143
  runId: contract.runId,
129
144
  workerId: contract.workerId,
@@ -137,13 +152,13 @@ export default function workerChildExtension(pi: ExtensionAPI): void {
137
152
  }
138
153
 
139
154
  function loadLaunchContract():
140
- | { readonly ok: true; readonly contract: WorkerLaunchContractV1 }
155
+ | { readonly ok: true; readonly contract: WorkerLaunchContractV2 }
141
156
  | { readonly ok: false; readonly error: Error } {
142
157
  try {
143
158
  const path = process.env["PI_AMQ_WORKER_LAUNCH_CONTRACT"];
144
159
  if (!path) throw new Error("PI_AMQ_WORKER_LAUNCH_CONTRACT is required");
145
160
  const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
146
- return { ok: true, contract: parseWorkerLaunchContractV1(value) };
161
+ return { ok: true, contract: parseWorkerLaunchContractV2(value) };
147
162
  } catch (error) {
148
163
  return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
149
164
  }
@@ -161,33 +176,100 @@ function childContext(ctx: ExtensionContext, tools?: readonly string[]): WorkerC
161
176
  };
162
177
  }
163
178
 
164
- function revalidateOrRefuse(contract: WorkerLaunchContractV1, ctx: ExtensionContext): void {
179
+ function revalidateOrRefuse(
180
+ contract: WorkerLaunchContractV2,
181
+ ctx: ExtensionContext,
182
+ writeProtocol: ProtocolWriter,
183
+ ): void {
165
184
  try {
166
185
  validateWorkerChildContext(contract, childContext(ctx));
167
186
  } catch (error) {
168
- refuse(error, ctx);
187
+ refuse(error, ctx, writeProtocol);
169
188
  }
170
189
  }
171
190
 
172
- function attachParentLiveness(contract: WorkerLaunchContractV1, ctx: ExtensionContext): void {
173
- const configured = Number(process.env["PI_AMQ_PARENT_LIVENESS_FD"]);
174
- if (configured !== contract.parentLivenessFd) {
175
- throw new Error("Parent-liveness descriptor does not match the launch contract");
191
+ interface ParentLivenessGuard {
192
+ /** Idempotent: a repeated session_start keeps the existing stream instead of reopening fd 3. */
193
+ attach(ctx: ExtensionContext): void;
194
+ /** Closes the descriptor so a settled one-shot child can exit. The guard stays released. */
195
+ release(): void;
196
+ }
197
+
198
+ function createParentLivenessGuard(contract: WorkerLaunchContractV2): ParentLivenessGuard {
199
+ let state: "unattached" | "attached" | "released" = "unattached";
200
+ let detach = (): void => {};
201
+ return {
202
+ attach(ctx) {
203
+ if (state === "attached") return;
204
+ if (state === "released") {
205
+ throw new Error("Parent-liveness descriptor was already released and cannot be reattached");
206
+ }
207
+ const configured = Number(process.env["PI_AMQ_PARENT_LIVENESS_FD"]);
208
+ if (configured !== contract.parentLivenessFd) {
209
+ throw new Error("Parent-liveness descriptor does not match the launch contract");
210
+ }
211
+ const stream = createReadStream("", { fd: configured, autoClose: true });
212
+ const requestShutdown = (): void => {
213
+ try {
214
+ ctx.shutdown();
215
+ } catch (error) {
216
+ console.error(`pi-amq parent-liveness shutdown failed: ${errorMessage(error)}`);
217
+ }
218
+ };
219
+ stream.on("end", requestShutdown);
220
+ stream.on("error", requestShutdown);
221
+ stream.resume();
222
+ detach = () => {
223
+ stream.off("end", requestShutdown);
224
+ stream.off("error", requestShutdown);
225
+ stream.destroy();
226
+ };
227
+ state = "attached";
228
+ },
229
+ release() {
230
+ if (state !== "attached") return;
231
+ detach();
232
+ detach = (): void => {};
233
+ state = "released";
234
+ },
235
+ };
236
+ }
237
+
238
+ function refuse(error: unknown, ctx: ExtensionContext, writeProtocol: ProtocolWriter): void {
239
+ try {
240
+ writeProtocol({
241
+ type: "pi_amq_worker_refused",
242
+ error: error instanceof Error ? error.message : String(error),
243
+ });
244
+ } catch (protocolError) {
245
+ console.error(`pi-amq worker protocol refusal failed: ${errorMessage(protocolError)}`);
176
246
  }
177
- const stream = createReadStream("", { fd: configured, autoClose: false });
178
- stream.on("end", () => ctx.shutdown());
179
- stream.on("error", () => ctx.shutdown());
180
- stream.resume();
247
+ ctx.shutdown();
181
248
  }
182
249
 
183
- function refuse(error: unknown, ctx: ExtensionContext): void {
184
- writeProtocolLine({
185
- type: "pi_amq_worker_refused",
186
- error: error instanceof Error ? error.message : String(error),
187
- });
250
+ function refuseWithoutProtocol(error: unknown, ctx: ExtensionContext): void {
251
+ console.error(`pi-amq worker launch refused: ${errorMessage(error)}`);
188
252
  ctx.shutdown();
189
253
  }
190
254
 
191
- function writeProtocolLine(value: unknown): void {
192
- process.stdout.write(`${JSON.stringify(value)}\n`);
255
+ function createProtocolWriter(contract: WorkerLaunchContractV2): ProtocolWriter {
256
+ const configured = Number(process.env["PI_AMQ_PROTOCOL_FD"]);
257
+ if (configured !== contract.protocolFd) {
258
+ throw new Error("Worker protocol descriptor does not match the launch contract");
259
+ }
260
+ return (value) => writeProtocolLine(contract.protocolFd, value);
261
+ }
262
+
263
+ function writeProtocolLine(fd: number, value: unknown): void {
264
+ const line = Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
265
+ let offset = 0;
266
+ while (offset < line.byteLength) {
267
+ const written = writeSync(fd, line, offset, line.byteLength - offset);
268
+ if (written <= 0) throw new Error("Worker protocol descriptor accepted no bytes");
269
+ offset += written;
270
+ }
271
+ }
272
+
273
+ function errorMessage(error: unknown): string {
274
+ return error instanceof Error ? error.message : String(error);
193
275
  }
@@ -4,7 +4,9 @@ import type {
4
4
  AttemptId,
5
5
  ModelSpec,
6
6
  WorkerContract,
7
+ WorkerLaunchContract,
7
8
  WorkerLaunchContractV1,
9
+ WorkerLaunchContractV2,
8
10
  } from "./workflow-types.js";
9
11
  import { parseBudgetLimit } from "./budget.js";
10
12
 
@@ -44,12 +46,13 @@ export interface PiProcessLaunchCommand {
44
46
  readonly cwd: string;
45
47
  readonly env: Readonly<Record<string, string>>;
46
48
  readonly parentLivenessFd: 3;
47
- readonly launchContract: WorkerLaunchContractV1;
49
+ readonly protocolFd: 4;
50
+ readonly launchContract: WorkerLaunchContractV2;
48
51
  readonly launchContractPath: string;
49
52
  }
50
53
 
51
54
  export interface ResolvedWorkerLaunch {
52
- readonly contract: WorkerLaunchContractV1;
55
+ readonly contract: WorkerLaunchContractV2;
53
56
  readonly command: PiProcessLaunchCommand;
54
57
  }
55
58
 
@@ -71,19 +74,50 @@ export class WorkerLaunchValidationError extends Error {
71
74
  }
72
75
 
73
76
  export function parseWorkerLaunchContractV1(value: unknown): WorkerLaunchContractV1 {
77
+ const parsed = parseWorkerLaunchContractVersion(value, 1);
78
+ if (parsed.schema !== 1) throw new WorkerLaunchValidationError("Worker launch schema must be 1");
79
+ return parsed;
80
+ }
81
+
82
+ export function parseWorkerLaunchContractV2(value: unknown): WorkerLaunchContractV2 {
83
+ const parsed = parseWorkerLaunchContractVersion(value, 2);
84
+ if (parsed.schema !== 2) throw new WorkerLaunchValidationError("Worker launch schema must be 2");
85
+ return parsed;
86
+ }
87
+
88
+ export function parseWorkerLaunchContract(value: unknown): WorkerLaunchContract {
89
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
90
+ throw new WorkerLaunchValidationError("worker launch contract must be an object");
91
+ }
92
+ const schema = (value as Record<string, unknown>)["schema"];
93
+ if (schema === 1) return parseWorkerLaunchContractV1(value);
94
+ if (schema === 2) return parseWorkerLaunchContractV2(value);
95
+ throw new WorkerLaunchValidationError("Worker launch schema must be 1 or 2");
96
+ }
97
+
98
+ function parseWorkerLaunchContractVersion(
99
+ value: unknown,
100
+ schema: 1 | 2,
101
+ ): WorkerLaunchContract {
74
102
  const object = strictObject(value, [
75
103
  "schema", "runId", "workerId", "attemptId", "workerAttemptGeneration",
76
104
  "supervisorGeneration", "transportMode", "channel", "depth", "model", "capabilities",
77
105
  "tools", "workspace", "sessionDirectory", "projectTrust", "parentLivenessFd", "budget",
78
106
  "sideEffectPolicy", "continuedSessionFile", "forkedSessionFile",
107
+ ...(schema === 2 ? ["protocolFd"] : []),
79
108
  ], "worker launch contract");
80
- if (object["schema"] !== 1) throw new WorkerLaunchValidationError("Worker launch schema must be 1");
109
+ if (object["schema"] !== schema) {
110
+ throw new WorkerLaunchValidationError(`Worker launch schema must be ${schema}`);
111
+ }
81
112
  if (object["transportMode"] !== "send-only") {
82
113
  throw new WorkerLaunchValidationError("Worker launch transport mode must be send-only");
83
114
  }
84
115
  if (object["parentLivenessFd"] !== 3) {
85
116
  throw new WorkerLaunchValidationError("Worker parent-liveness descriptor must be 3");
86
117
  }
118
+ if (schema === 2 && object["protocolFd"] !== 4) {
119
+ throw new WorkerLaunchValidationError("Worker protocol descriptor must be 4");
120
+ }
87
121
  const model = strictObject(object["model"], ["provider", "id", "thinkingLevel"], "worker model");
88
122
  const workspace = strictObject(object["workspace"], ["path", "mode", "worktreePath"], "worker workspace");
89
123
  const budget = strictObject(
@@ -118,11 +152,10 @@ export function parseWorkerLaunchContractV1(value: unknown): WorkerLaunchContrac
118
152
  if (continuedSessionFile !== undefined && forkedSessionFile !== undefined) {
119
153
  throw new WorkerLaunchValidationError("Worker launch cannot continue and fork a session together");
120
154
  }
121
- return {
122
- schema: 1,
123
- runId: prefixedString(object["runId"], "run", "run ID") as WorkerLaunchContractV1["runId"],
124
- workerId: prefixedString(object["workerId"], "worker", "worker ID") as WorkerLaunchContractV1["workerId"],
125
- attemptId: prefixedString(object["attemptId"], "attempt", "attempt ID") as WorkerLaunchContractV1["attemptId"],
155
+ const fields: Omit<WorkerLaunchContractV1, "schema"> = {
156
+ runId: prefixedString(object["runId"], "run", "run ID") as WorkerLaunchContract["runId"],
157
+ workerId: prefixedString(object["workerId"], "worker", "worker ID") as WorkerLaunchContract["workerId"],
158
+ attemptId: prefixedString(object["attemptId"], "attempt", "attempt ID") as WorkerLaunchContract["attemptId"],
126
159
  workerAttemptGeneration: requiredString(
127
160
  object["workerAttemptGeneration"],
128
161
  "worker attempt generation",
@@ -169,6 +202,9 @@ export function parseWorkerLaunchContractV1(value: unknown): WorkerLaunchContrac
169
202
  "worker side-effect policy",
170
203
  ),
171
204
  };
205
+ return schema === 1
206
+ ? { schema: 1, ...fields }
207
+ : { schema: 2, ...fields, protocolFd: 4 };
172
208
  }
173
209
 
174
210
  export function resolveWorkerLaunch(input: {
@@ -260,8 +296,8 @@ export function resolveWorkerLaunch(input: {
260
296
  ),
261
297
  ...(contract.channel === "rpc" ? ["wait_for_input"] : []),
262
298
  ])].sort();
263
- const launchContract: WorkerLaunchContractV1 = {
264
- schema: 1,
299
+ const launchContract: WorkerLaunchContractV2 = {
300
+ schema: 2,
265
301
  runId: contract.runId,
266
302
  workerId: contract.workerId,
267
303
  attemptId: attempt.attemptId,
@@ -286,6 +322,7 @@ export function resolveWorkerLaunch(input: {
286
322
  ...(forkedSessionFile ? { forkedSessionFile } : {}),
287
323
  projectTrust,
288
324
  parentLivenessFd: 3,
325
+ protocolFd: 4,
289
326
  budget: contract.budget,
290
327
  sideEffectPolicy: contract.sideEffectPolicy,
291
328
  };
@@ -303,6 +340,7 @@ export function resolveWorkerLaunch(input: {
303
340
  cwd: absolutePath(contract.workspace.worktreePath ?? contract.workspace.path, "worker cwd"),
304
341
  env: childEnvironment(authority.inheritedEnvironment, launchContractPath),
305
342
  parentLivenessFd: 3,
343
+ protocolFd: 4,
306
344
  launchContract,
307
345
  launchContractPath,
308
346
  },
@@ -310,7 +348,7 @@ export function resolveWorkerLaunch(input: {
310
348
  }
311
349
 
312
350
  export function validateWorkerChildContext(
313
- contract: WorkerLaunchContractV1,
351
+ contract: WorkerLaunchContract,
314
352
  context: WorkerChildContext,
315
353
  ): void {
316
354
  const expectedMode = contract.channel === "json-result" ? "json" : "rpc";
@@ -352,7 +390,7 @@ export function isReceiverAuthorityEnvironmentKey(key: string): boolean {
352
390
  }
353
391
 
354
392
  function buildPiArguments(
355
- contract: WorkerLaunchContractV1,
393
+ contract: WorkerLaunchContract,
356
394
  childExtensionPath: string,
357
395
  requiresProjectResources: boolean,
358
396
  prompt: string,
@@ -400,6 +438,7 @@ function childEnvironment(
400
438
  }
401
439
  env["PI_AMQ_WORKER_LAUNCH_CONTRACT"] = launchContractPath;
402
440
  env["PI_AMQ_PARENT_LIVENESS_FD"] = "3";
441
+ env["PI_AMQ_PROTOCOL_FD"] = "4";
403
442
  return env;
404
443
  }
405
444