pi-amq 0.1.1
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/CHANGELOG.md +66 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/THIRD_PARTY_NOTICES.md +33 -0
- package/dist/extension/guidance.d.ts +25 -0
- package/dist/extension/guidance.d.ts.map +1 -0
- package/dist/extension/guidance.js +121 -0
- package/dist/extension/guidance.js.map +1 -0
- package/dist/extension/guide.d.ts +30 -0
- package/dist/extension/guide.d.ts.map +1 -0
- package/dist/extension/guide.js +128 -0
- package/dist/extension/guide.js.map +1 -0
- package/dist/extension/index.d.ts +4 -0
- package/dist/extension/index.d.ts.map +1 -0
- package/dist/extension/index.js +6 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/extension/runtime.d.ts +48 -0
- package/dist/extension/runtime.d.ts.map +1 -0
- package/dist/extension/runtime.js +1095 -0
- package/dist/extension/runtime.js.map +1 -0
- package/dist/extension/tools.d.ts +12 -0
- package/dist/extension/tools.d.ts.map +1 -0
- package/dist/extension/tools.js +198 -0
- package/dist/extension/tools.js.map +1 -0
- package/dist/extension/worker-runtime.d.ts +34 -0
- package/dist/extension/worker-runtime.d.ts.map +1 -0
- package/dist/extension/worker-runtime.js +974 -0
- package/dist/extension/worker-runtime.js.map +1 -0
- package/dist/extension/worker-tools.d.ts +140 -0
- package/dist/extension/worker-tools.d.ts.map +1 -0
- package/dist/extension/worker-tools.js +217 -0
- package/dist/extension/worker-tools.js.map +1 -0
- package/dist/integration/confirmed-bind-onboarding.d.ts +44 -0
- package/dist/integration/confirmed-bind-onboarding.d.ts.map +1 -0
- package/dist/integration/confirmed-bind-onboarding.js +199 -0
- package/dist/integration/confirmed-bind-onboarding.js.map +1 -0
- package/dist/integration/fleet-view.d.ts +66 -0
- package/dist/integration/fleet-view.d.ts.map +1 -0
- package/dist/integration/fleet-view.js +238 -0
- package/dist/integration/fleet-view.js.map +1 -0
- package/dist/integration/fresh-session-dispatcher.d.ts +66 -0
- package/dist/integration/fresh-session-dispatcher.d.ts.map +1 -0
- package/dist/integration/fresh-session-dispatcher.js +103 -0
- package/dist/integration/fresh-session-dispatcher.js.map +1 -0
- package/dist/integration/launch-preview.d.ts +31 -0
- package/dist/integration/launch-preview.d.ts.map +1 -0
- package/dist/integration/launch-preview.js +325 -0
- package/dist/integration/launch-preview.js.map +1 -0
- package/dist/integration/transport-port.d.ts +39 -0
- package/dist/integration/transport-port.d.ts.map +1 -0
- package/dist/integration/transport-port.js +2 -0
- package/dist/integration/transport-port.js.map +1 -0
- package/dist/integration/worker-completion-notifier.d.ts +22 -0
- package/dist/integration/worker-completion-notifier.d.ts.map +1 -0
- package/dist/integration/worker-completion-notifier.js +81 -0
- package/dist/integration/worker-completion-notifier.js.map +1 -0
- package/dist/transport/amq-cli-adapter.d.ts +41 -0
- package/dist/transport/amq-cli-adapter.d.ts.map +1 -0
- package/dist/transport/amq-cli-adapter.js +291 -0
- package/dist/transport/amq-cli-adapter.js.map +1 -0
- package/dist/transport/amq-message.d.ts +47 -0
- package/dist/transport/amq-message.d.ts.map +1 -0
- package/dist/transport/amq-message.js +173 -0
- package/dist/transport/amq-message.js.map +1 -0
- package/dist/transport/amq-operations.d.ts +105 -0
- package/dist/transport/amq-operations.d.ts.map +1 -0
- package/dist/transport/amq-operations.js +320 -0
- package/dist/transport/amq-operations.js.map +1 -0
- package/dist/transport/atomic-json.d.ts +18 -0
- package/dist/transport/atomic-json.d.ts.map +1 -0
- package/dist/transport/atomic-json.js +90 -0
- package/dist/transport/atomic-json.js.map +1 -0
- package/dist/transport/binding-resolver.d.ts +46 -0
- package/dist/transport/binding-resolver.d.ts.map +1 -0
- package/dist/transport/binding-resolver.js +389 -0
- package/dist/transport/binding-resolver.js.map +1 -0
- package/dist/transport/bridge-journal.d.ts +100 -0
- package/dist/transport/bridge-journal.d.ts.map +1 -0
- package/dist/transport/bridge-journal.js +459 -0
- package/dist/transport/bridge-journal.js.map +1 -0
- package/dist/transport/dlq-retry-audit.d.ts +28 -0
- package/dist/transport/dlq-retry-audit.d.ts.map +1 -0
- package/dist/transport/dlq-retry-audit.js +66 -0
- package/dist/transport/dlq-retry-audit.js.map +1 -0
- package/dist/transport/inbox-pump.d.ts +81 -0
- package/dist/transport/inbox-pump.d.ts.map +1 -0
- package/dist/transport/inbox-pump.js +558 -0
- package/dist/transport/inbox-pump.js.map +1 -0
- package/dist/transport/index.d.ts +12 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +12 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/presence-projector.d.ts +29 -0
- package/dist/transport/presence-projector.d.ts.map +1 -0
- package/dist/transport/presence-projector.js +73 -0
- package/dist/transport/presence-projector.js.map +1 -0
- package/dist/transport/priority-delivery-scheduler.d.ts +85 -0
- package/dist/transport/priority-delivery-scheduler.d.ts.map +1 -0
- package/dist/transport/priority-delivery-scheduler.js +210 -0
- package/dist/transport/priority-delivery-scheduler.js.map +1 -0
- package/dist/transport/receiver-lease.d.ts +102 -0
- package/dist/transport/receiver-lease.d.ts.map +1 -0
- package/dist/transport/receiver-lease.js +778 -0
- package/dist/transport/receiver-lease.js.map +1 -0
- package/dist/transport/runtime-status.d.ts +48 -0
- package/dist/transport/runtime-status.d.ts.map +1 -0
- package/dist/transport/runtime-status.js +71 -0
- package/dist/transport/runtime-status.js.map +1 -0
- package/dist/workflow/atomic-storage.d.ts +12 -0
- package/dist/workflow/atomic-storage.d.ts.map +1 -0
- package/dist/workflow/atomic-storage.js +104 -0
- package/dist/workflow/atomic-storage.js.map +1 -0
- package/dist/workflow/budget.d.ts +18 -0
- package/dist/workflow/budget.d.ts.map +1 -0
- package/dist/workflow/budget.js +74 -0
- package/dist/workflow/budget.js.map +1 -0
- package/dist/workflow/index.d.ts +17 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +17 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/lifecycle-projection.d.ts +18 -0
- package/dist/workflow/lifecycle-projection.d.ts.map +1 -0
- package/dist/workflow/lifecycle-projection.js +22 -0
- package/dist/workflow/lifecycle-projection.js.map +1 -0
- package/dist/workflow/model-fallback.d.ts +2 -0
- package/dist/workflow/model-fallback.d.ts.map +1 -0
- package/dist/workflow/model-fallback.js +17 -0
- package/dist/workflow/model-fallback.js.map +1 -0
- package/dist/workflow/pi-session-fork.d.ts +18 -0
- package/dist/workflow/pi-session-fork.d.ts.map +1 -0
- package/dist/workflow/pi-session-fork.js +80 -0
- package/dist/workflow/pi-session-fork.js.map +1 -0
- package/dist/workflow/run-artifacts.d.ts +5 -0
- package/dist/workflow/run-artifacts.d.ts.map +1 -0
- package/dist/workflow/run-artifacts.js +25 -0
- package/dist/workflow/run-artifacts.js.map +1 -0
- package/dist/workflow/run-cleanup.d.ts +70 -0
- package/dist/workflow/run-cleanup.d.ts.map +1 -0
- package/dist/workflow/run-cleanup.js +222 -0
- package/dist/workflow/run-cleanup.js.map +1 -0
- package/dist/workflow/run-gate.d.ts +43 -0
- package/dist/workflow/run-gate.d.ts.map +1 -0
- package/dist/workflow/run-gate.js +70 -0
- package/dist/workflow/run-gate.js.map +1 -0
- package/dist/workflow/run-store.d.ts +208 -0
- package/dist/workflow/run-store.d.ts.map +1 -0
- package/dist/workflow/run-store.js +2965 -0
- package/dist/workflow/run-store.js.map +1 -0
- package/dist/workflow/spawn-confirmation.d.ts +44 -0
- package/dist/workflow/spawn-confirmation.d.ts.map +1 -0
- package/dist/workflow/spawn-confirmation.js +228 -0
- package/dist/workflow/spawn-confirmation.js.map +1 -0
- package/dist/workflow/system-process-inspector.d.ts +9 -0
- package/dist/workflow/system-process-inspector.d.ts.map +1 -0
- package/dist/workflow/system-process-inspector.js +129 -0
- package/dist/workflow/system-process-inspector.js.map +1 -0
- package/dist/workflow/worker-child-extension.d.ts +3 -0
- package/dist/workflow/worker-child-extension.d.ts.map +1 -0
- package/dist/workflow/worker-child-extension.js +182 -0
- package/dist/workflow/worker-child-extension.js.map +1 -0
- package/dist/workflow/worker-gate.d.ts +74 -0
- package/dist/workflow/worker-gate.d.ts.map +1 -0
- package/dist/workflow/worker-gate.js +165 -0
- package/dist/workflow/worker-gate.js.map +1 -0
- package/dist/workflow/worker-launch.d.ts +65 -0
- package/dist/workflow/worker-launch.d.ts.map +1 -0
- package/dist/workflow/worker-launch.js +338 -0
- package/dist/workflow/worker-launch.js.map +1 -0
- package/dist/workflow/worker-model-catalog.d.ts +36 -0
- package/dist/workflow/worker-model-catalog.d.ts.map +1 -0
- package/dist/workflow/worker-model-catalog.js +105 -0
- package/dist/workflow/worker-model-catalog.js.map +1 -0
- package/dist/workflow/worker-process-supervisor.d.ts +110 -0
- package/dist/workflow/worker-process-supervisor.d.ts.map +1 -0
- package/dist/workflow/worker-process-supervisor.js +1188 -0
- package/dist/workflow/worker-process-supervisor.js.map +1 -0
- package/dist/workflow/worker-prompt-capsule.d.ts +32 -0
- package/dist/workflow/worker-prompt-capsule.d.ts.map +1 -0
- package/dist/workflow/worker-prompt-capsule.js +52 -0
- package/dist/workflow/worker-prompt-capsule.js.map +1 -0
- package/dist/workflow/worker-role-presets.d.ts +38 -0
- package/dist/workflow/worker-role-presets.d.ts.map +1 -0
- package/dist/workflow/worker-role-presets.js +201 -0
- package/dist/workflow/worker-role-presets.js.map +1 -0
- package/dist/workflow/workflow-doctor.d.ts +57 -0
- package/dist/workflow/workflow-doctor.d.ts.map +1 -0
- package/dist/workflow/workflow-doctor.js +289 -0
- package/dist/workflow/workflow-doctor.js.map +1 -0
- package/dist/workflow/workflow-lease.d.ts +125 -0
- package/dist/workflow/workflow-lease.d.ts.map +1 -0
- package/dist/workflow/workflow-lease.js +689 -0
- package/dist/workflow/workflow-lease.js.map +1 -0
- package/dist/workflow/workflow-types.d.ts +335 -0
- package/dist/workflow/workflow-types.d.ts.map +1 -0
- package/dist/workflow/workflow-types.js +11 -0
- package/dist/workflow/workflow-types.js.map +1 -0
- package/dist/workflow/workspace-paths.d.ts +4 -0
- package/dist/workflow/workspace-paths.d.ts.map +1 -0
- package/dist/workflow/workspace-paths.js +22 -0
- package/dist/workflow/workspace-paths.js.map +1 -0
- package/docs/conformance.md +116 -0
- package/docs/demo-transcript.md +34 -0
- package/docs/design.md +1481 -0
- package/docs/operator-guide.md +100 -0
- package/extension/guidance.ts +162 -0
- package/extension/guide.ts +155 -0
- package/extension/index.ts +8 -0
- package/extension/runtime.ts +1224 -0
- package/extension/tools.ts +221 -0
- package/extension/worker-runtime.ts +1150 -0
- package/extension/worker-tools.ts +276 -0
- package/integration/confirmed-bind-onboarding.ts +296 -0
- package/integration/fleet-view.ts +286 -0
- package/integration/fresh-session-dispatcher.ts +166 -0
- package/integration/launch-preview.ts +396 -0
- package/integration/transport-port.ts +29 -0
- package/integration/worker-completion-notifier.ts +98 -0
- package/package.json +55 -0
- package/roles/oracle.md +9 -0
- package/roles/researcher.md +8 -0
- package/roles/reviewer.md +9 -0
- package/roles/scout.md +8 -0
- package/roles/worker.md +10 -0
- package/scripts/demo-two-terminal.sh +59 -0
- package/skills/coordination-patterns/SKILL.md +41 -0
- package/skills/coordination-patterns/references/supervised-worker-fan-out.md +12 -0
- package/skills/coordination-patterns/references/today-mail-team.md +36 -0
- package/transport/amq-cli-adapter.ts +381 -0
- package/transport/amq-message.ts +218 -0
- package/transport/amq-operations.ts +482 -0
- package/transport/atomic-json.ts +115 -0
- package/transport/binding-resolver.ts +565 -0
- package/transport/bridge-journal.ts +588 -0
- package/transport/dlq-retry-audit.ts +97 -0
- package/transport/inbox-pump.ts +729 -0
- package/transport/index.ts +11 -0
- package/transport/presence-projector.ts +104 -0
- package/transport/priority-delivery-scheduler.ts +328 -0
- package/transport/receiver-lease.ts +945 -0
- package/transport/runtime-status.ts +112 -0
- package/workflow/.gitkeep +1 -0
- package/workflow/atomic-storage.ts +120 -0
- package/workflow/budget.ts +90 -0
- package/workflow/index.ts +16 -0
- package/workflow/lifecycle-projection.ts +37 -0
- package/workflow/model-fallback.ts +17 -0
- package/workflow/pi-session-fork.ts +96 -0
- package/workflow/run-artifacts.ts +36 -0
- package/workflow/run-cleanup.ts +315 -0
- package/workflow/run-gate.ts +122 -0
- package/workflow/run-store.ts +3890 -0
- package/workflow/spawn-confirmation.ts +305 -0
- package/workflow/system-process-inspector.ts +142 -0
- package/workflow/worker-child-extension.ts +193 -0
- package/workflow/worker-gate.ts +238 -0
- package/workflow/worker-launch.ts +485 -0
- package/workflow/worker-model-catalog.ts +165 -0
- package/workflow/worker-process-supervisor.ts +1504 -0
- package/workflow/worker-prompt-capsule.ts +81 -0
- package/workflow/worker-role-presets.ts +241 -0
- package/workflow/workflow-doctor.ts +400 -0
- package/workflow/workflow-lease.ts +905 -0
- package/workflow/workflow-types.ts +367 -0
- package/workflow/workspace-paths.ts +32 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { defaultPiStateDirectory } from "./workspace-paths.js";
|
|
5
|
+
import type {
|
|
6
|
+
WorkerProposalEditableField,
|
|
7
|
+
WorkerProposalProvenance,
|
|
8
|
+
WorkerRequest,
|
|
9
|
+
} from "./workflow-types.js";
|
|
10
|
+
|
|
11
|
+
export type ConfirmSpawnsPolicy = WorkerProposalProvenance["confirmSpawns"];
|
|
12
|
+
|
|
13
|
+
export interface SpawnConfirmationConfigOptions {
|
|
14
|
+
readonly projectDirectory: string;
|
|
15
|
+
readonly projectTrusted: boolean;
|
|
16
|
+
readonly userConfigPath?: string;
|
|
17
|
+
readonly projectConfigPath?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function loadConfirmSpawnsPolicy(
|
|
21
|
+
options: SpawnConfirmationConfigOptions,
|
|
22
|
+
): Promise<ConfirmSpawnsPolicy> {
|
|
23
|
+
let policy: ConfirmSpawnsPolicy = "multi-worker-only";
|
|
24
|
+
const user = await readOptionalConfig(
|
|
25
|
+
options.userConfigPath ?? join(defaultPiStateDirectory(), "pi-amq", "config.json"),
|
|
26
|
+
);
|
|
27
|
+
if (user !== undefined) policy = user;
|
|
28
|
+
if (options.projectTrusted) {
|
|
29
|
+
const project = await readOptionalConfig(
|
|
30
|
+
options.projectConfigPath ?? join(options.projectDirectory, ".pi", "pi-amq", "config.json"),
|
|
31
|
+
);
|
|
32
|
+
if (project !== undefined) policy = project;
|
|
33
|
+
}
|
|
34
|
+
return policy;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface EditedWorkerProposal {
|
|
38
|
+
readonly request: WorkerRequest;
|
|
39
|
+
readonly humanEditedFields: readonly WorkerProposalEditableField[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type LaunchPreviewDecision =
|
|
43
|
+
| { readonly kind: "accepted"; readonly proposals: readonly EditedWorkerProposal[] }
|
|
44
|
+
| { readonly kind: "cancelled" };
|
|
45
|
+
|
|
46
|
+
export interface LaunchPreviewSubmission<T> {
|
|
47
|
+
readonly groupKey: string;
|
|
48
|
+
readonly policy: ConfirmSpawnsPolicy;
|
|
49
|
+
readonly tui: boolean;
|
|
50
|
+
readonly request: WorkerRequest;
|
|
51
|
+
readonly signal?: AbortSignal;
|
|
52
|
+
readonly preview: (requests: readonly WorkerRequest[]) => Promise<LaunchPreviewDecision>;
|
|
53
|
+
readonly launch: (accepted: WorkerRequest) => Promise<T>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type LaunchPreviewSubmissionResult<T> =
|
|
57
|
+
| { readonly kind: "launched"; readonly value: T }
|
|
58
|
+
| { readonly kind: "cancelled"; readonly reason: string };
|
|
59
|
+
|
|
60
|
+
interface PendingSubmission {
|
|
61
|
+
readonly request: WorkerRequest;
|
|
62
|
+
readonly signal: AbortSignal | undefined;
|
|
63
|
+
readonly preview: LaunchPreviewSubmission<unknown>["preview"];
|
|
64
|
+
readonly launch: LaunchPreviewSubmission<unknown>["launch"];
|
|
65
|
+
readonly resolve: (result: LaunchPreviewSubmissionResult<unknown>) => void;
|
|
66
|
+
readonly reject: (error: Error) => void;
|
|
67
|
+
active: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface PendingBatch {
|
|
71
|
+
readonly policy: ConfirmSpawnsPolicy;
|
|
72
|
+
readonly tui: boolean;
|
|
73
|
+
readonly submissions: PendingSubmission[];
|
|
74
|
+
readonly timer: ReturnType<typeof setTimeout>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface LaunchPreviewCoordinatorOptions {
|
|
78
|
+
readonly batchWindowMs?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class LaunchPreviewCoordinator {
|
|
82
|
+
readonly #batchWindowMs: number;
|
|
83
|
+
readonly #batches = new Map<string, PendingBatch>();
|
|
84
|
+
|
|
85
|
+
constructor(options: LaunchPreviewCoordinatorOptions = {}) {
|
|
86
|
+
this.#batchWindowMs = options.batchWindowMs ?? 20;
|
|
87
|
+
if (!Number.isSafeInteger(this.#batchWindowMs) || this.#batchWindowMs < 0 || this.#batchWindowMs > 1_000) {
|
|
88
|
+
throw new TypeError("Launch preview batch window must be an integer between 0 and 1000 ms");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async submit<T>(submission: LaunchPreviewSubmission<T>): Promise<LaunchPreviewSubmissionResult<T>> {
|
|
93
|
+
if (submission.signal?.aborted) throw abortError(submission.signal);
|
|
94
|
+
if (submission.policy === "off") {
|
|
95
|
+
const accepted = withProvenance(submission.request, "off", "skipped-policy-off", []);
|
|
96
|
+
return { kind: "launched", value: await submission.launch(accepted) };
|
|
97
|
+
}
|
|
98
|
+
return new Promise<LaunchPreviewSubmissionResult<T>>((resolve, reject) => {
|
|
99
|
+
const key = `${submission.groupKey}\0${submission.policy}\0${submission.tui ? "tui" : "headless"}`;
|
|
100
|
+
let batch = this.#batches.get(key);
|
|
101
|
+
if (!batch) {
|
|
102
|
+
const timer = setTimeout(() => void this.#flush(key), this.#batchWindowMs);
|
|
103
|
+
batch = {
|
|
104
|
+
policy: submission.policy,
|
|
105
|
+
tui: submission.tui,
|
|
106
|
+
submissions: [],
|
|
107
|
+
timer,
|
|
108
|
+
};
|
|
109
|
+
this.#batches.set(key, batch);
|
|
110
|
+
}
|
|
111
|
+
const pending: PendingSubmission = {
|
|
112
|
+
request: submission.request,
|
|
113
|
+
signal: submission.signal,
|
|
114
|
+
preview: submission.preview,
|
|
115
|
+
launch: submission.launch as (accepted: WorkerRequest) => Promise<unknown>,
|
|
116
|
+
resolve: resolve as (result: LaunchPreviewSubmissionResult<unknown>) => void,
|
|
117
|
+
reject: (error) => reject(error),
|
|
118
|
+
active: true,
|
|
119
|
+
};
|
|
120
|
+
batch.submissions.push(pending);
|
|
121
|
+
submission.signal?.addEventListener("abort", () => {
|
|
122
|
+
if (!pending.active) return;
|
|
123
|
+
pending.active = false;
|
|
124
|
+
pending.reject(abortError(submission.signal!));
|
|
125
|
+
}, { once: true });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async #flush(key: string): Promise<void> {
|
|
130
|
+
const batch = this.#batches.get(key);
|
|
131
|
+
if (!batch) return;
|
|
132
|
+
clearTimeout(batch.timer);
|
|
133
|
+
this.#batches.delete(key);
|
|
134
|
+
const pending = batch.submissions.filter((submission) => submission.active);
|
|
135
|
+
if (pending.length === 0) return;
|
|
136
|
+
|
|
137
|
+
if (batch.policy === "multi-worker-only" && pending.length === 1) {
|
|
138
|
+
const only = pending[0]!;
|
|
139
|
+
only.active = false;
|
|
140
|
+
await settleLaunch(
|
|
141
|
+
only,
|
|
142
|
+
withProvenance(only.request, batch.policy, "skipped-single-worker", []),
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!batch.tui) {
|
|
147
|
+
const error = new Error(
|
|
148
|
+
`confirm_spawns=${batch.policy} requires an interactive TUI for this ${pending.length}-worker proposal; no worker was accepted`,
|
|
149
|
+
);
|
|
150
|
+
for (const submission of pending) {
|
|
151
|
+
submission.active = false;
|
|
152
|
+
submission.reject(error);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let decision: LaunchPreviewDecision;
|
|
158
|
+
try {
|
|
159
|
+
decision = await pending[0]!.preview(pending.map((submission) => submission.request));
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
162
|
+
for (const submission of pending) {
|
|
163
|
+
if (!submission.active) continue;
|
|
164
|
+
submission.active = false;
|
|
165
|
+
submission.reject(normalized);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (pending.some((submission) => !submission.active)) {
|
|
170
|
+
for (const submission of pending) {
|
|
171
|
+
if (!submission.active) continue;
|
|
172
|
+
submission.active = false;
|
|
173
|
+
submission.resolve({
|
|
174
|
+
kind: "cancelled",
|
|
175
|
+
reason: "A launch-preview tool call was aborted; no worker contract was accepted or ledgered",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (decision.kind === "cancelled") {
|
|
181
|
+
for (const submission of pending) {
|
|
182
|
+
submission.active = false;
|
|
183
|
+
submission.resolve({
|
|
184
|
+
kind: "cancelled",
|
|
185
|
+
reason: "Launch preview cancelled; no worker contract was accepted or ledgered",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (decision.proposals.length !== pending.length) {
|
|
191
|
+
const error = new Error("Launch preview returned a different proposal count");
|
|
192
|
+
for (const submission of pending) {
|
|
193
|
+
submission.active = false;
|
|
194
|
+
submission.reject(error);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const keys = new Set<string>();
|
|
199
|
+
for (const proposal of decision.proposals) {
|
|
200
|
+
if (keys.has(proposal.request.requestKey)) {
|
|
201
|
+
const error = new Error("Launch preview returned duplicate worker request keys");
|
|
202
|
+
for (const submission of pending) {
|
|
203
|
+
submission.active = false;
|
|
204
|
+
submission.reject(error);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
keys.add(proposal.request.requestKey);
|
|
209
|
+
}
|
|
210
|
+
if (pending.some((submission) => !keys.has(submission.request.requestKey))) {
|
|
211
|
+
const error = new Error("Launch preview changed or dropped a worker request key");
|
|
212
|
+
for (const submission of pending) {
|
|
213
|
+
submission.active = false;
|
|
214
|
+
submission.reject(error);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
for (const submission of pending) submission.active = false;
|
|
219
|
+
await Promise.all(pending.map(async (submission) => {
|
|
220
|
+
const proposal = decision.proposals.find(
|
|
221
|
+
(candidate) => candidate.request.requestKey === submission.request.requestKey,
|
|
222
|
+
);
|
|
223
|
+
if (!proposal) {
|
|
224
|
+
submission.reject(new Error(`Launch preview dropped ${submission.request.requestKey}`));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const fields = uniqueFields(proposal.humanEditedFields);
|
|
228
|
+
await settleLaunch(
|
|
229
|
+
submission,
|
|
230
|
+
withProvenance(proposal.request, batch.policy, "accepted", fields),
|
|
231
|
+
);
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function settleLaunch(submission: PendingSubmission, request: WorkerRequest): Promise<void> {
|
|
237
|
+
try {
|
|
238
|
+
submission.resolve({ kind: "launched", value: await submission.launch(request) });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
submission.reject(error instanceof Error ? error : new Error(String(error)));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function withProvenance(
|
|
245
|
+
request: WorkerRequest,
|
|
246
|
+
policy: ConfirmSpawnsPolicy,
|
|
247
|
+
disposition: WorkerProposalProvenance["disposition"],
|
|
248
|
+
humanEditedFields: readonly WorkerProposalEditableField[],
|
|
249
|
+
): WorkerRequest {
|
|
250
|
+
return {
|
|
251
|
+
...request,
|
|
252
|
+
proposal: {
|
|
253
|
+
source: humanEditedFields.length === 0 ? "tool" : "human-edited",
|
|
254
|
+
confirmSpawns: policy,
|
|
255
|
+
disposition,
|
|
256
|
+
humanEditedFields,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function uniqueFields(
|
|
262
|
+
fields: readonly WorkerProposalEditableField[],
|
|
263
|
+
): readonly WorkerProposalEditableField[] {
|
|
264
|
+
const supported = new Set<WorkerProposalEditableField>(["role", "task", "model", "difficulty", "budget"]);
|
|
265
|
+
const unique: WorkerProposalEditableField[] = [];
|
|
266
|
+
for (const field of fields) {
|
|
267
|
+
if (!supported.has(field)) throw new TypeError(`Unknown edited proposal field ${field}`);
|
|
268
|
+
if (!unique.includes(field)) unique.push(field);
|
|
269
|
+
}
|
|
270
|
+
return unique;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function readOptionalConfig(path: string): Promise<ConfirmSpawnsPolicy | undefined> {
|
|
274
|
+
let raw: string;
|
|
275
|
+
try {
|
|
276
|
+
raw = await readFile(path, "utf8");
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (isMissing(error)) return undefined;
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
let value: unknown;
|
|
282
|
+
try {
|
|
283
|
+
value = JSON.parse(raw) as unknown;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw new TypeError(`${path}: invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
286
|
+
}
|
|
287
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
288
|
+
throw new TypeError(`${path}: pi-amq config must be an object`);
|
|
289
|
+
}
|
|
290
|
+
const policy = (value as Record<string, unknown>)["confirm_spawns"];
|
|
291
|
+
if (policy === undefined) return undefined;
|
|
292
|
+
if (policy !== "on" && policy !== "multi-worker-only" && policy !== "off") {
|
|
293
|
+
throw new TypeError(`${path}: confirm_spawns must be on, multi-worker-only, or off`);
|
|
294
|
+
}
|
|
295
|
+
return policy;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isMissing(error: unknown): boolean {
|
|
299
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
300
|
+
&& (error as { readonly code?: unknown }).code === "ENOENT";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function abortError(signal: AbortSignal): Error {
|
|
304
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Launch preview aborted");
|
|
305
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile, readlink } from "node:fs/promises";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { isAbsolute, normalize } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ProcessIdentity,
|
|
9
|
+
ProcessIdentityInspector,
|
|
10
|
+
ProcessInspection,
|
|
11
|
+
} from "./workflow-lease.js";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
/** Reads PID-reuse-resistant identity evidence without treating age or timeout as death proof. */
|
|
16
|
+
export class SystemProcessIdentityInspector implements ProcessIdentityInspector {
|
|
17
|
+
async current(): Promise<ProcessIdentity> {
|
|
18
|
+
const inspection = await this.inspect(process.pid);
|
|
19
|
+
if (inspection.kind !== "live") {
|
|
20
|
+
throw new Error(
|
|
21
|
+
inspection.kind === "unknown"
|
|
22
|
+
? `Current process identity is unavailable: ${inspection.diagnostic}`
|
|
23
|
+
: "Current process disappeared during identity inspection",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return inspection.identity;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async inspect(pid: number): Promise<ProcessInspection> {
|
|
30
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
31
|
+
return { kind: "unknown", diagnostic: "PID must be a positive integer" };
|
|
32
|
+
}
|
|
33
|
+
if (process.platform === "linux") return inspectLinux(pid);
|
|
34
|
+
if (process.platform === "darwin") return inspectDarwin(pid);
|
|
35
|
+
return {
|
|
36
|
+
kind: "unknown",
|
|
37
|
+
diagnostic: `Process start identity is unsupported on ${process.platform}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function inspectLinux(pid: number): Promise<ProcessInspection> {
|
|
43
|
+
try {
|
|
44
|
+
const [stat, executable, bootIdentity] = await Promise.all([
|
|
45
|
+
readFile(`/proc/${pid}/stat`, "utf8"),
|
|
46
|
+
readlink(`/proc/${pid}/exe`),
|
|
47
|
+
readFile("/proc/sys/kernel/random/boot_id", "utf8"),
|
|
48
|
+
]);
|
|
49
|
+
const closing = stat.lastIndexOf(")");
|
|
50
|
+
if (closing < 0) return { kind: "unknown", diagnostic: "Linux process stat is malformed" };
|
|
51
|
+
const fields = stat.slice(closing + 2).trim().split(/\s+/);
|
|
52
|
+
const startTicks = fields[19];
|
|
53
|
+
if (!startTicks) return { kind: "unknown", diagnostic: "Linux process start ticks are missing" };
|
|
54
|
+
return {
|
|
55
|
+
kind: "live",
|
|
56
|
+
identity: {
|
|
57
|
+
pid,
|
|
58
|
+
processStartIdentity: `linux-start-ticks:${startTicks}`,
|
|
59
|
+
bootIdentity: bootIdentity.trim() || null,
|
|
60
|
+
executable: normalize(executable),
|
|
61
|
+
hostname: hostname(),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return classifyInspectionFailure(error, pid);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function inspectDarwin(pid: number): Promise<ProcessInspection> {
|
|
70
|
+
try {
|
|
71
|
+
const [{ stdout }, boot] = await Promise.all([
|
|
72
|
+
execFileAsync("ps", ["-p", String(pid), "-o", "lstart=", "-o", "comm="], {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
maxBuffer: 8_192,
|
|
75
|
+
timeout: 2_000,
|
|
76
|
+
}),
|
|
77
|
+
execFileAsync("sysctl", ["-n", "kern.boottime"], {
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
maxBuffer: 8_192,
|
|
80
|
+
timeout: 2_000,
|
|
81
|
+
}),
|
|
82
|
+
]);
|
|
83
|
+
const line = stdout.trim();
|
|
84
|
+
if (line.length === 0) return { kind: "absent" };
|
|
85
|
+
const start = line.slice(0, 24).trim();
|
|
86
|
+
const psExecutable = line.slice(24).trim();
|
|
87
|
+
if (start.length === 0) {
|
|
88
|
+
return { kind: "unknown", diagnostic: "Darwin ps start identity is incomplete" };
|
|
89
|
+
}
|
|
90
|
+
const executable = selectDarwinExecutable(
|
|
91
|
+
psExecutable,
|
|
92
|
+
pid === process.pid ? process.execPath : null,
|
|
93
|
+
);
|
|
94
|
+
if (executable === null) {
|
|
95
|
+
return { kind: "unknown", diagnostic: "Darwin process executable is unavailable" };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
kind: "live",
|
|
99
|
+
identity: {
|
|
100
|
+
pid,
|
|
101
|
+
processStartIdentity: `darwin-lstart:${start}`,
|
|
102
|
+
bootIdentity: boot.stdout.trim() || null,
|
|
103
|
+
executable,
|
|
104
|
+
hostname: hostname(),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return classifyInspectionFailure(error, pid);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Uses current-process evidence only when ps reports a relative argv[0]. */
|
|
113
|
+
export function selectDarwinExecutable(
|
|
114
|
+
psExecutable: string,
|
|
115
|
+
currentExecutable: string | null,
|
|
116
|
+
): string | null {
|
|
117
|
+
const reported = psExecutable.trim();
|
|
118
|
+
if (isAbsolute(reported)) return normalize(reported);
|
|
119
|
+
if (currentExecutable !== null && isAbsolute(currentExecutable)) {
|
|
120
|
+
return normalize(currentExecutable);
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function classifyInspectionFailure(error: unknown, pid: number): ProcessInspection {
|
|
126
|
+
if (isNodeError(error) && (error.code === "ENOENT" || error.code === "ESRCH")) {
|
|
127
|
+
return { kind: "absent" };
|
|
128
|
+
}
|
|
129
|
+
if (isExecFailure(error) && error.code === 1) return { kind: "absent" };
|
|
130
|
+
return {
|
|
131
|
+
kind: "unknown",
|
|
132
|
+
diagnostic: `Could not inspect PID ${pid}: ${error instanceof Error ? error.message : String(error)}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
137
|
+
return error instanceof Error && "code" in error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isExecFailure(error: unknown): error is Error & { readonly code: number } {
|
|
141
|
+
return error instanceof Error && "code" in error && typeof error.code === "number";
|
|
142
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createReadStream, readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
parseWorkerLaunchContractV1,
|
|
8
|
+
validateWorkerChildContext,
|
|
9
|
+
type WorkerChildContext,
|
|
10
|
+
} from "./worker-launch.js";
|
|
11
|
+
import { classifyProviderFallbackFailure } from "./model-fallback.js";
|
|
12
|
+
import type { WorkerLaunchContractV1 } from "./workflow-types.js";
|
|
13
|
+
|
|
14
|
+
export default function workerChildExtension(pi: ExtensionAPI): void {
|
|
15
|
+
const loaded = loadLaunchContract();
|
|
16
|
+
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
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const contract = loaded.contract;
|
|
22
|
+
let ready = false;
|
|
23
|
+
|
|
24
|
+
pi.registerTool({
|
|
25
|
+
name: "message_worker",
|
|
26
|
+
label: "Message Worker",
|
|
27
|
+
description: "Request one directionally granted peer message through the parent supervisor.",
|
|
28
|
+
parameters: Type.Object({
|
|
29
|
+
target: Type.String({ minLength: 1, maxLength: 256 }),
|
|
30
|
+
message: Type.String({ minLength: 1, maxLength: 32_768 }),
|
|
31
|
+
correlation: Type.String({ minLength: 1, maxLength: 256 }),
|
|
32
|
+
}),
|
|
33
|
+
async execute(_id, params) {
|
|
34
|
+
writeProtocolLine({
|
|
35
|
+
type: "pi_amq_worker_peer_message",
|
|
36
|
+
runId: contract.runId,
|
|
37
|
+
workerId: contract.workerId,
|
|
38
|
+
attemptId: contract.attemptId,
|
|
39
|
+
workerAttemptGeneration: contract.workerAttemptGeneration,
|
|
40
|
+
supervisorGeneration: contract.supervisorGeneration,
|
|
41
|
+
target: params.target,
|
|
42
|
+
message: params.message,
|
|
43
|
+
correlation: params.correlation,
|
|
44
|
+
});
|
|
45
|
+
const details = { status: "requested", target: params.target, correlation: params.correlation };
|
|
46
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
pi.registerTool({
|
|
51
|
+
name: "wait_for_input",
|
|
52
|
+
label: "Wait for Input",
|
|
53
|
+
description: "Declare a typed correlated wait without inferring waiting from prose or silence.",
|
|
54
|
+
parameters: Type.Object({
|
|
55
|
+
reason: Type.String({ minLength: 1, maxLength: 2_048 }),
|
|
56
|
+
correlation: Type.String({ minLength: 1, maxLength: 256 }),
|
|
57
|
+
}),
|
|
58
|
+
async execute(_id, params) {
|
|
59
|
+
writeProtocolLine({
|
|
60
|
+
type: "pi_amq_worker_wait",
|
|
61
|
+
runId: contract.runId,
|
|
62
|
+
workerId: contract.workerId,
|
|
63
|
+
attemptId: contract.attemptId,
|
|
64
|
+
workerAttemptGeneration: contract.workerAttemptGeneration,
|
|
65
|
+
supervisorGeneration: contract.supervisorGeneration,
|
|
66
|
+
reason: params.reason,
|
|
67
|
+
correlation: params.correlation,
|
|
68
|
+
});
|
|
69
|
+
const details = {
|
|
70
|
+
status: "declared",
|
|
71
|
+
reason: params.reason,
|
|
72
|
+
correlation: params.correlation,
|
|
73
|
+
};
|
|
74
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details };
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
pi.on("session_start", (_event, ctx) => {
|
|
79
|
+
try {
|
|
80
|
+
validateWorkerChildContext(contract, childContext(ctx));
|
|
81
|
+
attachParentLiveness(contract, ctx);
|
|
82
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
83
|
+
if (!sessionFile) throw new Error("Pi worker session file is unavailable");
|
|
84
|
+
writeProtocolLine({
|
|
85
|
+
type: "pi_amq_worker_ready",
|
|
86
|
+
runId: contract.runId,
|
|
87
|
+
workerId: contract.workerId,
|
|
88
|
+
attemptId: contract.attemptId,
|
|
89
|
+
workerAttemptGeneration: contract.workerAttemptGeneration,
|
|
90
|
+
supervisorGeneration: contract.supervisorGeneration,
|
|
91
|
+
sessionFile,
|
|
92
|
+
});
|
|
93
|
+
ready = true;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
refuse(error, ctx);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
100
|
+
try {
|
|
101
|
+
if (!ready) throw new Error("Worker launch parity was not established");
|
|
102
|
+
validateWorkerChildContext(contract, childContext(ctx, event.systemPromptOptions.selectedTools));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
refuse(error, ctx);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
pi.on("model_select", (_event, ctx) => revalidateOrRefuse(contract, ctx));
|
|
109
|
+
pi.on("thinking_level_select", (_event, ctx) => revalidateOrRefuse(contract, ctx));
|
|
110
|
+
pi.on("agent_settled", () => {
|
|
111
|
+
if (!ready) return;
|
|
112
|
+
writeProtocolLine({
|
|
113
|
+
type: "pi_amq_worker_settled",
|
|
114
|
+
runId: contract.runId,
|
|
115
|
+
workerId: contract.workerId,
|
|
116
|
+
attemptId: contract.attemptId,
|
|
117
|
+
workerAttemptGeneration: contract.workerAttemptGeneration,
|
|
118
|
+
supervisorGeneration: contract.supervisorGeneration,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
pi.on("message_end", (event, ctx) => {
|
|
122
|
+
const message = event.message;
|
|
123
|
+
if (message.role !== "assistant") return;
|
|
124
|
+
const reason = classifyProviderFallbackFailure(message.stopReason, message.errorMessage);
|
|
125
|
+
if (!reason) return;
|
|
126
|
+
writeProtocolLine({
|
|
127
|
+
type: "pi_amq_worker_provider_failure",
|
|
128
|
+
runId: contract.runId,
|
|
129
|
+
workerId: contract.workerId,
|
|
130
|
+
attemptId: contract.attemptId,
|
|
131
|
+
workerAttemptGeneration: contract.workerAttemptGeneration,
|
|
132
|
+
supervisorGeneration: contract.supervisorGeneration,
|
|
133
|
+
reason,
|
|
134
|
+
});
|
|
135
|
+
ctx.shutdown();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function loadLaunchContract():
|
|
140
|
+
| { readonly ok: true; readonly contract: WorkerLaunchContractV1 }
|
|
141
|
+
| { readonly ok: false; readonly error: Error } {
|
|
142
|
+
try {
|
|
143
|
+
const path = process.env["PI_AMQ_WORKER_LAUNCH_CONTRACT"];
|
|
144
|
+
if (!path) throw new Error("PI_AMQ_WORKER_LAUNCH_CONTRACT is required");
|
|
145
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
146
|
+
return { ok: true, contract: parseWorkerLaunchContractV1(value) };
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function childContext(ctx: ExtensionContext, tools?: readonly string[]): WorkerChildContext {
|
|
153
|
+
return {
|
|
154
|
+
mode: ctx.mode,
|
|
155
|
+
cwd: ctx.cwd,
|
|
156
|
+
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
|
|
157
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
158
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
159
|
+
...(tools === undefined ? {} : { tools }),
|
|
160
|
+
environmentKeys: Object.keys(process.env),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function revalidateOrRefuse(contract: WorkerLaunchContractV1, ctx: ExtensionContext): void {
|
|
165
|
+
try {
|
|
166
|
+
validateWorkerChildContext(contract, childContext(ctx));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
refuse(error, ctx);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
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");
|
|
176
|
+
}
|
|
177
|
+
const stream = createReadStream("", { fd: configured, autoClose: false });
|
|
178
|
+
stream.on("end", () => ctx.shutdown());
|
|
179
|
+
stream.on("error", () => ctx.shutdown());
|
|
180
|
+
stream.resume();
|
|
181
|
+
}
|
|
182
|
+
|
|
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
|
+
});
|
|
188
|
+
ctx.shutdown();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function writeProtocolLine(value: unknown): void {
|
|
192
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
193
|
+
}
|