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,112 @@
|
|
|
1
|
+
export type PiRuntimeMode = "tui" | "rpc" | "json" | "print";
|
|
2
|
+
|
|
3
|
+
import type { ReceiverLeaseAcquireResult } from "./receiver-lease.js";
|
|
4
|
+
|
|
5
|
+
export interface InboxRuntimeStatus {
|
|
6
|
+
readonly receiver: "dormant" | "send-only" | "receiving";
|
|
7
|
+
readonly watch: "stopped" | "recovering" | "watching" | "backoff" | "disabled";
|
|
8
|
+
readonly receiverGeneration: string | null;
|
|
9
|
+
readonly backoffAttempt: number;
|
|
10
|
+
readonly nextBackoffMs: number | null;
|
|
11
|
+
readonly lastError: string | null;
|
|
12
|
+
readonly staged: {
|
|
13
|
+
readonly urgent: number;
|
|
14
|
+
readonly normal: number;
|
|
15
|
+
readonly low: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type InboxStatusProjection =
|
|
20
|
+
| { readonly surface: "tui"; readonly text: string; readonly value: InboxRuntimeStatus }
|
|
21
|
+
| { readonly surface: "rpc"; readonly value: InboxRuntimeStatus }
|
|
22
|
+
| { readonly surface: "none" };
|
|
23
|
+
|
|
24
|
+
export type ReceiverLeaseRuntimeDisposition =
|
|
25
|
+
| { readonly mode: "receiving"; readonly generation: string }
|
|
26
|
+
| { readonly mode: "send-only"; readonly diagnostic: string };
|
|
27
|
+
|
|
28
|
+
export class InboxStatusStore {
|
|
29
|
+
#status: InboxRuntimeStatus = {
|
|
30
|
+
receiver: "dormant",
|
|
31
|
+
watch: "stopped",
|
|
32
|
+
receiverGeneration: null,
|
|
33
|
+
backoffAttempt: 0,
|
|
34
|
+
nextBackoffMs: null,
|
|
35
|
+
lastError: null,
|
|
36
|
+
staged: { urgent: 0, normal: 0, low: 0 },
|
|
37
|
+
};
|
|
38
|
+
readonly #listeners = new Set<(status: InboxRuntimeStatus) => void>();
|
|
39
|
+
|
|
40
|
+
snapshot(): InboxRuntimeStatus {
|
|
41
|
+
return cloneStatus(this.#status);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
update(update: Partial<InboxRuntimeStatus>): void {
|
|
45
|
+
this.#status = cloneStatus({ ...this.#status, ...update });
|
|
46
|
+
for (const listener of this.#listeners) listener(this.snapshot());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
subscribe(listener: (status: InboxRuntimeStatus) => void): () => void {
|
|
50
|
+
this.#listeners.add(listener);
|
|
51
|
+
return () => this.#listeners.delete(listener);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function projectInboxStatus(
|
|
56
|
+
mode: PiRuntimeMode,
|
|
57
|
+
status: InboxRuntimeStatus,
|
|
58
|
+
): InboxStatusProjection {
|
|
59
|
+
if (mode === "print" || mode === "json") return { surface: "none" };
|
|
60
|
+
const value = cloneStatus(status);
|
|
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)}` : "";
|
|
65
|
+
return {
|
|
66
|
+
surface: "tui",
|
|
67
|
+
text: `AMQ ${status.receiver}/${status.watch}${backoff} u=${counts.urgent} n=${counts.normal} l=${counts.low}${error}`,
|
|
68
|
+
value,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function runInboxForMode(
|
|
73
|
+
mode: PiRuntimeMode,
|
|
74
|
+
pump: { run(signal?: AbortSignal): Promise<void> },
|
|
75
|
+
status: InboxStatusStore,
|
|
76
|
+
signal?: AbortSignal,
|
|
77
|
+
): Promise<{ readonly kind: "send-only" } | { readonly kind: "completed" }> {
|
|
78
|
+
if (mode === "print" || mode === "json") {
|
|
79
|
+
status.update({
|
|
80
|
+
receiver: "send-only",
|
|
81
|
+
watch: "disabled",
|
|
82
|
+
receiverGeneration: null,
|
|
83
|
+
nextBackoffMs: null,
|
|
84
|
+
lastError: null,
|
|
85
|
+
});
|
|
86
|
+
return { kind: "send-only" };
|
|
87
|
+
}
|
|
88
|
+
await pump.run(signal);
|
|
89
|
+
return { kind: "completed" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function receiverLeaseRuntimeDisposition(
|
|
93
|
+
result: ReceiverLeaseAcquireResult,
|
|
94
|
+
): ReceiverLeaseRuntimeDisposition {
|
|
95
|
+
if (result.kind === "acquired") {
|
|
96
|
+
return { mode: "receiving", generation: result.owner.generation };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
mode: "send-only",
|
|
100
|
+
diagnostic: result.kind === "held"
|
|
101
|
+
? `Receiver is held by pi session ${result.owner.piSessionId}`
|
|
102
|
+
: result.diagnostic,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function cloneStatus(status: InboxRuntimeStatus): InboxRuntimeStatus {
|
|
107
|
+
return { ...status, staged: { ...status.staged } };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function bounded(value: string): string {
|
|
111
|
+
return value.length <= 160 ? value : `${value.slice(0, 159)}…`;
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, link, open, readFile, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export type WorkflowAtomicWritePoint =
|
|
6
|
+
| "after-file-sync"
|
|
7
|
+
| "before-commit"
|
|
8
|
+
| "after-commit"
|
|
9
|
+
| "after-directory-sync";
|
|
10
|
+
|
|
11
|
+
export type WorkflowAtomicFaultInjector = (
|
|
12
|
+
point: WorkflowAtomicWritePoint,
|
|
13
|
+
) => void | Promise<void>;
|
|
14
|
+
|
|
15
|
+
export async function writeWorkflowJson(
|
|
16
|
+
path: string,
|
|
17
|
+
value: unknown,
|
|
18
|
+
options: {
|
|
19
|
+
readonly createOnly?: boolean;
|
|
20
|
+
readonly fault?: WorkflowAtomicFaultInjector;
|
|
21
|
+
} = {},
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const parent = dirname(path);
|
|
24
|
+
const temporaryPath = join(parent, `.pi-amq-workflow-${randomUUID()}.tmp`);
|
|
25
|
+
const contents = `${JSON.stringify(value)}\n`;
|
|
26
|
+
let committed = false;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const handle = await open(temporaryPath, "wx", 0o600);
|
|
30
|
+
try {
|
|
31
|
+
await handle.writeFile(contents, "utf8");
|
|
32
|
+
await handle.sync();
|
|
33
|
+
} finally {
|
|
34
|
+
await handle.close();
|
|
35
|
+
}
|
|
36
|
+
await chmod(temporaryPath, 0o600);
|
|
37
|
+
await options.fault?.("after-file-sync");
|
|
38
|
+
await options.fault?.("before-commit");
|
|
39
|
+
if (options.createOnly) {
|
|
40
|
+
await link(temporaryPath, path);
|
|
41
|
+
committed = true;
|
|
42
|
+
await unlink(temporaryPath);
|
|
43
|
+
} else {
|
|
44
|
+
await rename(temporaryPath, path);
|
|
45
|
+
committed = true;
|
|
46
|
+
}
|
|
47
|
+
await options.fault?.("after-commit");
|
|
48
|
+
await syncWorkflowDirectory(parent);
|
|
49
|
+
await options.fault?.("after-directory-sync");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!committed) await removeFileIfPresent(temporaryPath);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function readWorkflowJson(path: string): Promise<unknown | undefined> {
|
|
57
|
+
let contents: string;
|
|
58
|
+
try {
|
|
59
|
+
contents = await readFile(path, "utf8");
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (isNodeError(error) && error.code === "ENOENT") return undefined;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(contents) as unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function syncWorkflowDirectory(path: string): Promise<void> {
|
|
68
|
+
let handle;
|
|
69
|
+
try {
|
|
70
|
+
handle = await open(path, "r");
|
|
71
|
+
await handle.sync();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!isUnsupportedDirectorySync(error)) throw error;
|
|
74
|
+
} finally {
|
|
75
|
+
await handle?.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function canonicalJson(value: unknown): string {
|
|
80
|
+
return JSON.stringify(canonicalize(value));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function sha256(value: string): string {
|
|
84
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function canonicalize(value: unknown): unknown {
|
|
88
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
89
|
+
if (value !== null && typeof value === "object") {
|
|
90
|
+
return Object.fromEntries(
|
|
91
|
+
Object.entries(value as Record<string, unknown>)
|
|
92
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
93
|
+
.map(([key, child]) => [key, canonicalize(child)]),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
|
|
97
|
+
throw new TypeError("Workflow state must be JSON serializable");
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
100
|
+
throw new TypeError("Workflow state numbers must be finite");
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function removeFileIfPresent(path: string): Promise<void> {
|
|
106
|
+
try {
|
|
107
|
+
await unlink(path);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isUnsupportedDirectorySync(error: unknown): boolean {
|
|
114
|
+
return isNodeError(error)
|
|
115
|
+
&& (error.code === "EINVAL" || error.code === "ENOTSUP" || error.code === "EISDIR");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
119
|
+
return error instanceof Error && "code" in error;
|
|
120
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { BudgetLimit } from "./workflow-types.js";
|
|
2
|
+
|
|
3
|
+
export const UNLIMITED: BudgetLimit = Object.freeze({ kind: "unlimited" });
|
|
4
|
+
|
|
5
|
+
export function limited(value: number): BudgetLimit {
|
|
6
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
7
|
+
throw new TypeError("Budget limit must be positive and finite");
|
|
8
|
+
}
|
|
9
|
+
return { kind: "limited", value };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function limitedInteger(value: number): BudgetLimit {
|
|
13
|
+
if (!Number.isSafeInteger(value)) throw new TypeError("Budget limit must be a safe integer");
|
|
14
|
+
return limited(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseBudgetLimit(
|
|
18
|
+
value: unknown,
|
|
19
|
+
name: string,
|
|
20
|
+
options: { readonly integer: boolean; readonly allowLegacyNumber?: boolean } = { integer: false },
|
|
21
|
+
): BudgetLimit {
|
|
22
|
+
if (options.allowLegacyNumber && typeof value === "number") {
|
|
23
|
+
return options.integer ? limitedInteger(value) : limited(value);
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
26
|
+
throw new TypeError(`${name} must be limited(value) or unlimited`);
|
|
27
|
+
}
|
|
28
|
+
const object = value as Record<string, unknown>;
|
|
29
|
+
const keys = Object.keys(object);
|
|
30
|
+
if (object["kind"] === "unlimited") {
|
|
31
|
+
if (keys.length !== 1) throw new TypeError(`${name} unlimited limit has unknown fields`);
|
|
32
|
+
return UNLIMITED;
|
|
33
|
+
}
|
|
34
|
+
if (object["kind"] !== "limited" || keys.some((key) => key !== "kind" && key !== "value")) {
|
|
35
|
+
throw new TypeError(`${name} must be limited(value) or unlimited`);
|
|
36
|
+
}
|
|
37
|
+
if (keys.length !== 2) throw new TypeError(`${name} limited limit requires value`);
|
|
38
|
+
const numeric = object["value"];
|
|
39
|
+
if (typeof numeric !== "number") throw new TypeError(`${name} value must be a number`);
|
|
40
|
+
return options.integer ? limitedInteger(numeric) : limited(numeric);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isUnlimited(limit: BudgetLimit): boolean {
|
|
44
|
+
return limit.kind === "unlimited";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function budgetValue(limit: BudgetLimit): number | undefined {
|
|
48
|
+
return limit.kind === "limited" ? limit.value : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function budgetExceeded(observed: number, limit: BudgetLimit): boolean {
|
|
52
|
+
return limit.kind === "limited" && observed > limit.value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function remainingBudget(limit: BudgetLimit, used: number | "unlimited"): BudgetLimit {
|
|
56
|
+
if (limit.kind === "unlimited") return UNLIMITED;
|
|
57
|
+
if (used === "unlimited") return { kind: "limited", value: 0 };
|
|
58
|
+
return { kind: "limited", value: Math.max(0, limit.value - used) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function reserveBudget(
|
|
62
|
+
current: number | "unlimited",
|
|
63
|
+
requested: BudgetLimit,
|
|
64
|
+
): number | "unlimited" {
|
|
65
|
+
if (current === "unlimited" || requested.kind === "unlimited") return "unlimited";
|
|
66
|
+
return current + requested.value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function assertBudgetRequestFits(
|
|
70
|
+
available: BudgetLimit,
|
|
71
|
+
requested: BudgetLimit,
|
|
72
|
+
message: string,
|
|
73
|
+
): void {
|
|
74
|
+
if (available.kind === "unlimited") return;
|
|
75
|
+
if (requested.kind === "unlimited" || requested.value > available.value) {
|
|
76
|
+
throw new Error(message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function budgetExhausted(observed: number, limit: BudgetLimit): boolean {
|
|
81
|
+
return limit.kind === "limited" && observed >= limit.value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function budgetAtLeast(limit: BudgetLimit, observed: number): boolean {
|
|
85
|
+
return limit.kind === "unlimited" || limit.value >= observed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function budgetLimitLabel(limit: BudgetLimit): string {
|
|
89
|
+
return limit.kind === "unlimited" ? "unlimited" : String(limit.value);
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from "./run-store.js";
|
|
2
|
+
export * from "./run-cleanup.js";
|
|
3
|
+
export * from "./run-artifacts.js";
|
|
4
|
+
export * from "./workflow-doctor.js";
|
|
5
|
+
export * from "./run-gate.js";
|
|
6
|
+
export * from "./lifecycle-projection.js";
|
|
7
|
+
export * from "./budget.js";
|
|
8
|
+
export * from "./system-process-inspector.js";
|
|
9
|
+
export * from "./worker-launch.js";
|
|
10
|
+
export * from "./worker-gate.js";
|
|
11
|
+
export * from "./worker-model-catalog.js";
|
|
12
|
+
export * from "./worker-prompt-capsule.js";
|
|
13
|
+
export * from "./worker-process-supervisor.js";
|
|
14
|
+
export * from "./workflow-lease.js";
|
|
15
|
+
export * from "./workflow-types.js";
|
|
16
|
+
export * from "./workspace-paths.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface WorkerLifecycleProjectionV1 {
|
|
2
|
+
readonly body: string;
|
|
3
|
+
readonly labels: readonly string[];
|
|
4
|
+
readonly context: {
|
|
5
|
+
readonly orchestrator: {
|
|
6
|
+
readonly version: 1;
|
|
7
|
+
readonly name: "pi-amq";
|
|
8
|
+
readonly transport: "extension";
|
|
9
|
+
readonly event: "worker_lifecycle";
|
|
10
|
+
readonly task: { readonly id: string; readonly state: string };
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function projectWorkerLifecycle(
|
|
16
|
+
workerId: string,
|
|
17
|
+
state: string,
|
|
18
|
+
): WorkerLifecycleProjectionV1 {
|
|
19
|
+
const labels = ["orchestrator", "orchestrator:pi-amq", `task-state:${state}`];
|
|
20
|
+
if (state === "ready_for_review") labels.push("handoff");
|
|
21
|
+
if (state === "failed" || state === "interrupted" || state === "blocked") {
|
|
22
|
+
labels.push("blocking");
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
body: JSON.stringify({ workerId, state }),
|
|
26
|
+
labels,
|
|
27
|
+
context: {
|
|
28
|
+
orchestrator: {
|
|
29
|
+
version: 1,
|
|
30
|
+
name: "pi-amq",
|
|
31
|
+
transport: "extension",
|
|
32
|
+
event: "worker_lifecycle",
|
|
33
|
+
task: { id: workerId, state },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function classifyProviderFallbackFailure(
|
|
2
|
+
stopReason: unknown,
|
|
3
|
+
errorMessage: unknown,
|
|
4
|
+
): string | null {
|
|
5
|
+
if (stopReason !== "error" || typeof errorMessage !== "string") return null;
|
|
6
|
+
const diagnostic = errorMessage.trim();
|
|
7
|
+
if (diagnostic.length === 0) return null;
|
|
8
|
+
const normalized = diagnostic.toLowerCase();
|
|
9
|
+
const category = /auth|credential|api[ _-]?key|unauthori[sz]ed|forbidden/.test(normalized)
|
|
10
|
+
? "provider-auth"
|
|
11
|
+
: /quota|rate[ _-]?limit|resource exhausted|billing limit/.test(normalized)
|
|
12
|
+
? "provider-quota"
|
|
13
|
+
: /capacity|overloaded|temporarily unavailable|no available.*server/.test(normalized)
|
|
14
|
+
? "provider-capacity"
|
|
15
|
+
: null;
|
|
16
|
+
return category ? `${category}: ${diagnostic}` : null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { access, mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, dirname, normalize } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { SessionManager, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import type { WorkerForkSource } from "./workflow-types.js";
|
|
7
|
+
|
|
8
|
+
export interface PiSessionForkInput extends WorkerForkSource {
|
|
9
|
+
readonly targetSessionFile: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PiSessionForker {
|
|
13
|
+
fork(input: PiSessionForkInput): Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ForkSourceSession {
|
|
17
|
+
getSessionFile(): string | undefined;
|
|
18
|
+
getLeafId(): string | null;
|
|
19
|
+
getBranch(fromId?: string): SessionEntry[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function describeCurrentPiSessionFork(source: ForkSourceSession): WorkerForkSource {
|
|
23
|
+
const sourceSessionFile = source.getSessionFile();
|
|
24
|
+
if (!sourceSessionFile || !isAbsolute(sourceSessionFile)) {
|
|
25
|
+
throw new Error("Fork context requires a persisted absolute parent Pi session file");
|
|
26
|
+
}
|
|
27
|
+
const branchPointId = source.getLeafId();
|
|
28
|
+
if (!branchPointId) throw new Error("Fork context requires a current parent session leaf");
|
|
29
|
+
const branch = source.getBranch(branchPointId);
|
|
30
|
+
if (branch.length === 0 || !branch.some((entry) => entry.id === branchPointId)) {
|
|
31
|
+
throw new Error("Fork context branch point is absent from the parent session");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
sourceSessionFile: normalize(sourceSessionFile),
|
|
35
|
+
branchPointId,
|
|
36
|
+
inheritedEntries: branch.length,
|
|
37
|
+
inheritedBytes: branch.reduce(
|
|
38
|
+
(total, entry) => total + Buffer.byteLength(`${JSON.stringify(entry)}\n`, "utf8"),
|
|
39
|
+
0,
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class NodePiSessionForker implements PiSessionForker {
|
|
45
|
+
async fork(input: PiSessionForkInput): Promise<string> {
|
|
46
|
+
const sourceSessionFile = absolute(input.sourceSessionFile, "fork source session file");
|
|
47
|
+
const targetSessionFile = absolute(input.targetSessionFile, "fork target session file");
|
|
48
|
+
if (sourceSessionFile === targetSessionFile) {
|
|
49
|
+
throw new Error("Fork target session file must differ from its parent");
|
|
50
|
+
}
|
|
51
|
+
await assertAbsent(targetSessionFile);
|
|
52
|
+
await mkdir(dirname(targetSessionFile), { recursive: true, mode: 0o700 });
|
|
53
|
+
const sourceBefore = await readFile(sourceSessionFile);
|
|
54
|
+
const manager = SessionManager.open(sourceSessionFile, dirname(targetSessionFile));
|
|
55
|
+
if (!manager.getEntry(input.branchPointId)) {
|
|
56
|
+
throw new Error(`Fork branch point ${input.branchPointId} is absent from the parent session`);
|
|
57
|
+
}
|
|
58
|
+
let generated: string | undefined;
|
|
59
|
+
try {
|
|
60
|
+
generated = manager.createBranchedSession(input.branchPointId);
|
|
61
|
+
if (!generated) throw new Error("Pi session fork did not produce a persisted child session");
|
|
62
|
+
await access(generated);
|
|
63
|
+
await rename(generated, targetSessionFile);
|
|
64
|
+
generated = undefined;
|
|
65
|
+
} finally {
|
|
66
|
+
if (generated) await rm(generated, { force: true }).catch(() => {});
|
|
67
|
+
}
|
|
68
|
+
const child = SessionManager.open(targetSessionFile, dirname(targetSessionFile));
|
|
69
|
+
if (child.getHeader()?.parentSession !== sourceSessionFile) {
|
|
70
|
+
throw new Error("Fork child session does not record the expected parent session");
|
|
71
|
+
}
|
|
72
|
+
if (!child.getEntry(input.branchPointId)) {
|
|
73
|
+
throw new Error("Fork child session does not contain the requested branch point");
|
|
74
|
+
}
|
|
75
|
+
const sourceAfter = await readFile(sourceSessionFile);
|
|
76
|
+
if (!sourceAfter.equals(sourceBefore)) {
|
|
77
|
+
throw new Error("Forking mutated the parent Pi session file");
|
|
78
|
+
}
|
|
79
|
+
return targetSessionFile;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function absolute(value: string, label: string): string {
|
|
84
|
+
if (!isAbsolute(value)) throw new TypeError(`${label} must be absolute`);
|
|
85
|
+
return normalize(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function assertAbsent(path: string): Promise<void> {
|
|
89
|
+
try {
|
|
90
|
+
await access(path);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Fork target session file already exists: ${path}`);
|
|
96
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, normalize } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { runDirectoryForRequest } from "./run-store.js";
|
|
5
|
+
import type { WorkerRequest } from "./workflow-types.js";
|
|
6
|
+
|
|
7
|
+
export function runArtifactDirectory(namespaceDirectory: string, requestKey: string): string {
|
|
8
|
+
return join(runDirectoryForRequest(namespaceDirectory, requestKey), "artifacts");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function ensureRunArtifactDirectory(
|
|
12
|
+
namespaceDirectory: string,
|
|
13
|
+
requestKey: string,
|
|
14
|
+
): Promise<string> {
|
|
15
|
+
const directory = runArtifactDirectory(namespaceDirectory, requestKey);
|
|
16
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
17
|
+
return directory;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function withRunArtifactContext(
|
|
21
|
+
request: WorkerRequest,
|
|
22
|
+
artifactDirectory: string,
|
|
23
|
+
): WorkerRequest {
|
|
24
|
+
if (!isAbsolute(artifactDirectory)) {
|
|
25
|
+
throw new TypeError("Run artifact directory must be absolute");
|
|
26
|
+
}
|
|
27
|
+
const canonical = normalize(artifactDirectory);
|
|
28
|
+
return {
|
|
29
|
+
...request,
|
|
30
|
+
contextRefs: [
|
|
31
|
+
{ kind: "artifact", value: canonical },
|
|
32
|
+
...request.contextRefs.filter((reference) =>
|
|
33
|
+
reference.kind !== "artifact" || normalize(reference.value) !== canonical),
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|