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,1095 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { CommandContextFreshSessionCapability, FreshSessionDispatcher, } from "../integration/fresh-session-dispatcher.js";
|
|
4
|
+
import { ConfirmedBindOnboarding, parseRosterInput, } from "../integration/confirmed-bind-onboarding.js";
|
|
5
|
+
import { FleetListModel, FleetDetailController, MutableFleetProjectionSource, handleFleetInput, renderFleetDetail, } from "../integration/fleet-view.js";
|
|
6
|
+
import { AmqCliAdapter } from "../transport/amq-cli-adapter.js";
|
|
7
|
+
import { AmqOperations, safeProjection } from "../transport/amq-operations.js";
|
|
8
|
+
import { BindingResolver, PI_AMQ_BINDING_ENTRY_TYPE, } from "../transport/binding-resolver.js";
|
|
9
|
+
import { BridgeJournal } from "../transport/bridge-journal.js";
|
|
10
|
+
import { InboxPump, LeaseBackedInboxAuthority } from "../transport/inbox-pump.js";
|
|
11
|
+
import { PresenceProjector } from "../transport/presence-projector.js";
|
|
12
|
+
import { PI_AMQ_CUSTOM_TYPE, planPriorityDeliveries, PriorityDeliveryScheduler, } from "../transport/priority-delivery-scheduler.js";
|
|
13
|
+
import { LocalProcessIdentityInspector, ReceiverLeaseManager } from "../transport/receiver-lease.js";
|
|
14
|
+
import { InboxStatusStore, projectInboxStatus, receiverLeaseRuntimeDisposition, } from "../transport/runtime-status.js";
|
|
15
|
+
import { ALL_AMQ_TOOLS, NON_CONSUMING_AMQ_TOOLS, registerAmqTools } from "./tools.js";
|
|
16
|
+
import { InstalledPackageGuide, registerGuideTool } from "./guide.js";
|
|
17
|
+
import { PiWorkerRuntime } from "./worker-runtime.js";
|
|
18
|
+
import { registerWorkerTools } from "./worker-tools.js";
|
|
19
|
+
import { parseCleanupAge } from "../workflow/run-cleanup.js";
|
|
20
|
+
import { appendAmqMessagingDoctrine, appendWorkerTeamDoctrine, buildBeforeAgentGuidance, buildPendingProjection, } from "./guidance.js";
|
|
21
|
+
const PENDING_PROJECTION_TYPE = "pi-amq-pending/v1";
|
|
22
|
+
const WIDGET_KEY = "pi-amq-fleet";
|
|
23
|
+
const FRESH_SESSION_COMMAND = "amq-new-task";
|
|
24
|
+
export class PiAmqRuntime {
|
|
25
|
+
#pi;
|
|
26
|
+
#workers;
|
|
27
|
+
#freshSessionPort;
|
|
28
|
+
#guide = new InstalledPackageGuide();
|
|
29
|
+
#status = new InboxStatusStore();
|
|
30
|
+
#fleet = new MutableFleetProjectionSource([]);
|
|
31
|
+
#mainFleet;
|
|
32
|
+
#workerFleet = [];
|
|
33
|
+
#adapter;
|
|
34
|
+
#resolver;
|
|
35
|
+
#binding;
|
|
36
|
+
#operations;
|
|
37
|
+
#receiver;
|
|
38
|
+
#ctx;
|
|
39
|
+
#unsubscribeStatus;
|
|
40
|
+
#unsubscribeTerminal;
|
|
41
|
+
#fleetModel;
|
|
42
|
+
#started = false;
|
|
43
|
+
#paused = false;
|
|
44
|
+
constructor(pi) {
|
|
45
|
+
this.#pi = pi;
|
|
46
|
+
this.#freshSessionPort = new PiFreshSessionReplacementPort(pi);
|
|
47
|
+
this.#workers = new PiWorkerRuntime((records) => {
|
|
48
|
+
this.#workerFleet = records;
|
|
49
|
+
this.#commitFleet();
|
|
50
|
+
}, () => this.#pi.getFlag("worker-unlimited"), async (projection) => {
|
|
51
|
+
const operations = this.#operations;
|
|
52
|
+
const recipients = (process.env.PI_AMQ_LIFECYCLE_TO ?? "")
|
|
53
|
+
.split(",")
|
|
54
|
+
.map((item) => item.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
if (!operations || recipients.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
const result = await operations.send({
|
|
59
|
+
to: recipients,
|
|
60
|
+
body: projection.body,
|
|
61
|
+
kind: "status",
|
|
62
|
+
labels: projection.labels,
|
|
63
|
+
context: projection.context,
|
|
64
|
+
});
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
this.#status.update({ lastError: `Worker lifecycle projection failed: ${result.error.message}` });
|
|
67
|
+
}
|
|
68
|
+
}, (notice) => {
|
|
69
|
+
this.#ctx?.ui.notify(notice.message, notice.level);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
register() {
|
|
73
|
+
this.#registerFlags();
|
|
74
|
+
this.#registerRenderers();
|
|
75
|
+
registerAmqTools(this.#pi, {
|
|
76
|
+
operations: () => this.#operations,
|
|
77
|
+
canConsume: () => this.#receiver !== undefined && !this.#paused,
|
|
78
|
+
canSetPresence: () => this.#receiver !== undefined && !this.#paused,
|
|
79
|
+
});
|
|
80
|
+
registerWorkerTools(this.#pi, this.#workers);
|
|
81
|
+
registerGuideTool(this.#pi, this.#guide);
|
|
82
|
+
this.#registerCommands();
|
|
83
|
+
this.#pi.on("session_start", async (_event, ctx) => {
|
|
84
|
+
this.#ctx = ctx;
|
|
85
|
+
await this.#workers.start(ctx);
|
|
86
|
+
await this.start(ctx);
|
|
87
|
+
});
|
|
88
|
+
this.#pi.on("session_shutdown", async () => {
|
|
89
|
+
await this.#workers.shutdown();
|
|
90
|
+
await this.shutdown();
|
|
91
|
+
});
|
|
92
|
+
this.#pi.on("agent_start", async (_event, ctx) => {
|
|
93
|
+
this.#updateMainFleet(ctx, "busy");
|
|
94
|
+
await this.#receiver?.presence.busy(ctx.signal);
|
|
95
|
+
});
|
|
96
|
+
this.#pi.on("agent_settled", async (_event, ctx) => {
|
|
97
|
+
this.#updateMainFleet(ctx, "idle");
|
|
98
|
+
await this.#receiver?.presence.idle(ctx.signal);
|
|
99
|
+
});
|
|
100
|
+
this.#pi.on("model_select", async (event, ctx) => {
|
|
101
|
+
await this.#workers.refreshGuidance(ctx);
|
|
102
|
+
this.#updateMainFleet(ctx, ctx.isIdle() ? "idle" : "busy", event.model.id);
|
|
103
|
+
await this.#receiver?.presence.modelNote(event.model.id, ctx.signal);
|
|
104
|
+
});
|
|
105
|
+
this.#pi.on("session_compact", async () => {
|
|
106
|
+
const records = await this.#receiver?.journal.list() ?? [];
|
|
107
|
+
this.#pi.appendEntry(PENDING_PROJECTION_TYPE, buildPendingProjection(records.filter((record) => record.phase === "pending").map((record) => record.id), this.#status.snapshot()));
|
|
108
|
+
});
|
|
109
|
+
this.#pi.on("before_agent_start", (event, ctx) => ({
|
|
110
|
+
systemPrompt: this.#workers.appendGuidance(appendWorkerTeamDoctrine(appendAmqMessagingDoctrine(event.systemPrompt))),
|
|
111
|
+
message: {
|
|
112
|
+
customType: "pi-amq-guidance",
|
|
113
|
+
display: false,
|
|
114
|
+
content: buildBeforeAgentGuidance({
|
|
115
|
+
...(this.#binding ? {
|
|
116
|
+
identity: {
|
|
117
|
+
handle: this.#binding.handle,
|
|
118
|
+
project: this.#binding.project,
|
|
119
|
+
session: this.#binding.session,
|
|
120
|
+
mode: this.#receiver ? "receiving" : "send-only",
|
|
121
|
+
},
|
|
122
|
+
} : {}),
|
|
123
|
+
envelopes: collectEnvelopeDetails(ctx),
|
|
124
|
+
status: this.#status.snapshot(),
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
this.#pi.on("input", (_event, ctx) => {
|
|
129
|
+
this.#ctx = ctx;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async start(ctx) {
|
|
133
|
+
if (this.#started)
|
|
134
|
+
await this.shutdown();
|
|
135
|
+
this.#started = true;
|
|
136
|
+
this.#ctx = ctx;
|
|
137
|
+
let binaryPath;
|
|
138
|
+
try {
|
|
139
|
+
binaryPath = await resolveAmqBinary(process.env.PI_AMQ_BINARY);
|
|
140
|
+
this.#adapter = new AmqCliAdapter({ binaryPath });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.#binding = undefined;
|
|
144
|
+
this.#operations = undefined;
|
|
145
|
+
this.#status.update({
|
|
146
|
+
receiver: "dormant",
|
|
147
|
+
watch: "disabled",
|
|
148
|
+
receiverGeneration: null,
|
|
149
|
+
lastError: `AMQ binary configuration failed: ${toError(error).message}; /amq bind`,
|
|
150
|
+
});
|
|
151
|
+
this.#activateTools("dormant");
|
|
152
|
+
this.#installSurfaces(ctx);
|
|
153
|
+
this.#updateMainFleet(ctx, "offline");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.#resolver = new BindingResolver(this.#adapter);
|
|
157
|
+
const persisted = latestBinding(ctx);
|
|
158
|
+
const resolution = await this.#resolver.resolve({
|
|
159
|
+
flags: this.#flags(),
|
|
160
|
+
environment: process.env,
|
|
161
|
+
...(persisted ? { persistedBinding: persisted } : {}),
|
|
162
|
+
});
|
|
163
|
+
if (resolution.kind === "dormant") {
|
|
164
|
+
this.#binding = undefined;
|
|
165
|
+
this.#operations = undefined;
|
|
166
|
+
this.#status.update({
|
|
167
|
+
receiver: "dormant",
|
|
168
|
+
watch: "disabled",
|
|
169
|
+
receiverGeneration: null,
|
|
170
|
+
lastError: await this.#bindingFailureDiagnostic(resolution.diagnostic),
|
|
171
|
+
});
|
|
172
|
+
this.#activateTools("dormant");
|
|
173
|
+
this.#installSurfaces(ctx);
|
|
174
|
+
this.#updateMainFleet(ctx, "offline");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
await this.#activateBound(ctx, resolution.binding);
|
|
178
|
+
}
|
|
179
|
+
async shutdown() {
|
|
180
|
+
if (!this.#started)
|
|
181
|
+
return;
|
|
182
|
+
this.#started = false;
|
|
183
|
+
this.#paused = false;
|
|
184
|
+
const receiver = this.#receiver;
|
|
185
|
+
this.#receiver = undefined;
|
|
186
|
+
if (receiver) {
|
|
187
|
+
receiver.abortController.abort(new Error("pi-amq session shutdown"));
|
|
188
|
+
await receiver.pumpTask.catch((error) => {
|
|
189
|
+
this.#status.update({ lastError: `Inbox pump shutdown failed: ${toError(error).message}` });
|
|
190
|
+
});
|
|
191
|
+
await receiver.presence.offline();
|
|
192
|
+
try {
|
|
193
|
+
const release = await receiver.lease.release(receiver.generation);
|
|
194
|
+
if (release.kind === "refused")
|
|
195
|
+
this.#status.update({ lastError: release.diagnostic });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.#status.update({ lastError: `Receiver lease release failed: ${toError(error).message}` });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this.#unsubscribeStatus?.();
|
|
202
|
+
this.#unsubscribeStatus = undefined;
|
|
203
|
+
this.#unsubscribeTerminal?.();
|
|
204
|
+
this.#unsubscribeTerminal = undefined;
|
|
205
|
+
this.#fleetModel?.dispose();
|
|
206
|
+
this.#fleetModel = undefined;
|
|
207
|
+
this.#ctx?.ui.setStatus("pi-amq", undefined);
|
|
208
|
+
this.#ctx?.ui.setWidget(WIDGET_KEY, undefined);
|
|
209
|
+
this.#status.update({ receiver: "dormant", watch: "stopped", receiverGeneration: null });
|
|
210
|
+
}
|
|
211
|
+
async #activateBound(ctx, binding) {
|
|
212
|
+
if (!this.#adapter || !this.#resolver)
|
|
213
|
+
throw new Error("AMQ runtime is not initialized");
|
|
214
|
+
this.#binding = binding;
|
|
215
|
+
this.#pi.appendEntry(PI_AMQ_BINDING_ENTRY_TYPE, binding);
|
|
216
|
+
this.#operations = new AmqOperations({
|
|
217
|
+
adapter: this.#adapter,
|
|
218
|
+
binding: {
|
|
219
|
+
root: binding.root,
|
|
220
|
+
handle: binding.handle,
|
|
221
|
+
project: binding.project,
|
|
222
|
+
session: binding.session,
|
|
223
|
+
},
|
|
224
|
+
replyRouteLookup: replyRoutes(ctx),
|
|
225
|
+
});
|
|
226
|
+
this.#installSurfaces(ctx);
|
|
227
|
+
if (ctx.mode === "print" || ctx.mode === "json") {
|
|
228
|
+
this.#status.update({
|
|
229
|
+
receiver: "send-only",
|
|
230
|
+
watch: "disabled",
|
|
231
|
+
receiverGeneration: null,
|
|
232
|
+
lastError: null,
|
|
233
|
+
});
|
|
234
|
+
this.#activateTools("send-only");
|
|
235
|
+
this.#updateMainFleet(ctx, "idle");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
239
|
+
if (!sessionFile || !isAbsolute(sessionFile)) {
|
|
240
|
+
this.#status.update({
|
|
241
|
+
receiver: "send-only",
|
|
242
|
+
watch: "disabled",
|
|
243
|
+
receiverGeneration: null,
|
|
244
|
+
lastError: "Pi session file is not disk-visible; receiver activation is deferred",
|
|
245
|
+
});
|
|
246
|
+
this.#activateTools("send-only");
|
|
247
|
+
this.#updateMainFleet(ctx, "idle");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const namespaceDirectory = join(binding.root, "agents", binding.handle, "extensions", "pi-amq-extension");
|
|
251
|
+
const lease = new ReceiverLeaseManager({
|
|
252
|
+
namespaceDirectory,
|
|
253
|
+
rootId: binding.rootId,
|
|
254
|
+
inspector: new LocalProcessIdentityInspector(),
|
|
255
|
+
});
|
|
256
|
+
let acquired;
|
|
257
|
+
try {
|
|
258
|
+
acquired = await lease.acquire({
|
|
259
|
+
piSessionId: ctx.sessionManager.getSessionId(),
|
|
260
|
+
piSessionFile: sessionFile,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
this.#status.update({
|
|
265
|
+
receiver: "send-only",
|
|
266
|
+
watch: "disabled",
|
|
267
|
+
receiverGeneration: null,
|
|
268
|
+
lastError: `Receiver lease activation failed: ${toError(error).message}`,
|
|
269
|
+
});
|
|
270
|
+
this.#activateTools("send-only");
|
|
271
|
+
this.#updateMainFleet(ctx, "idle");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const leaseDisposition = receiverLeaseRuntimeDisposition(acquired);
|
|
275
|
+
if (leaseDisposition.mode === "send-only") {
|
|
276
|
+
this.#status.update({
|
|
277
|
+
receiver: "send-only",
|
|
278
|
+
watch: "disabled",
|
|
279
|
+
receiverGeneration: null,
|
|
280
|
+
lastError: leaseDisposition.diagnostic,
|
|
281
|
+
});
|
|
282
|
+
this.#activateTools("send-only");
|
|
283
|
+
this.#updateMainFleet(ctx, "idle");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (acquired.kind !== "acquired")
|
|
287
|
+
throw new Error("Receiver lease disposition disagrees with acquisition result");
|
|
288
|
+
const journal = new BridgeJournal({ namespaceDirectory, leaseManager: lease });
|
|
289
|
+
const lookup = new PiSessionProjectionLookup(ctx);
|
|
290
|
+
const sink = new PiDeliverySink(this.#pi, lookup);
|
|
291
|
+
const scheduler = new PriorityDeliveryScheduler({ sink });
|
|
292
|
+
const authority = new LeaseBackedInboxAuthority({
|
|
293
|
+
lease,
|
|
294
|
+
generation: acquired.owner.generation,
|
|
295
|
+
verifyBinding: async () => {
|
|
296
|
+
const current = await this.#resolver.resolve({
|
|
297
|
+
flags: this.#flags(),
|
|
298
|
+
environment: process.env,
|
|
299
|
+
persistedBinding: binding,
|
|
300
|
+
});
|
|
301
|
+
return current.kind === "bound"
|
|
302
|
+
&& current.binding.rootId === binding.rootId
|
|
303
|
+
&& current.binding.handle === binding.handle
|
|
304
|
+
? { kind: "valid" }
|
|
305
|
+
: { kind: "invalid", diagnostic: current.kind === "dormant" ? current.diagnostic : "AMQ binding authority changed" };
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
const fresh = new FreshSessionDispatcher({
|
|
309
|
+
capability: new CommandContextFreshSessionCapability(this.#freshSessionPort),
|
|
310
|
+
policy: {
|
|
311
|
+
trustedProject: ctx.isProjectTrusted(),
|
|
312
|
+
allowlisted: (sender, project) => freshAllowlist().has(`${sender}@${project}`),
|
|
313
|
+
...(ctx.hasUI ? {
|
|
314
|
+
confirm: async (message) => ctx.ui.confirm("AMQ fresh session", `${message.header.from} [${message.header.fromProject || "unknown project"}] requests replacing the current session: ${message.header.subject || "(no subject)"}`),
|
|
315
|
+
} : {}),
|
|
316
|
+
canReplaceSession: async () => this.#workers.canReplaceSession(),
|
|
317
|
+
notifyOnce: (message) => ctx.ui.notify(message, "warning"),
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
const pump = new InboxPump({
|
|
321
|
+
adapter: this.#adapter,
|
|
322
|
+
binding: {
|
|
323
|
+
root: binding.root,
|
|
324
|
+
rootId: binding.rootId,
|
|
325
|
+
handle: binding.handle,
|
|
326
|
+
},
|
|
327
|
+
receiverGeneration: acquired.owner.generation,
|
|
328
|
+
targetSessionId: ctx.sessionManager.getSessionId(),
|
|
329
|
+
authority,
|
|
330
|
+
journal,
|
|
331
|
+
scheduler,
|
|
332
|
+
status: this.#status,
|
|
333
|
+
projectionLookup: lookup,
|
|
334
|
+
freshSessionDispatcher: fresh,
|
|
335
|
+
});
|
|
336
|
+
this.#operations = new AmqOperations({
|
|
337
|
+
adapter: this.#adapter,
|
|
338
|
+
binding: {
|
|
339
|
+
root: binding.root,
|
|
340
|
+
handle: binding.handle,
|
|
341
|
+
project: binding.project,
|
|
342
|
+
session: binding.session,
|
|
343
|
+
},
|
|
344
|
+
claimPort: pump,
|
|
345
|
+
replyRouteLookup: replyRoutes(ctx),
|
|
346
|
+
});
|
|
347
|
+
const presence = new PresenceProjector({
|
|
348
|
+
writer: {
|
|
349
|
+
setStatus: async (status, note, signal) => {
|
|
350
|
+
const result = await this.#operations.setStatus(status, note, signal);
|
|
351
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
const abortController = new AbortController();
|
|
356
|
+
const receiver = {
|
|
357
|
+
lease,
|
|
358
|
+
generation: acquired.owner.generation,
|
|
359
|
+
journal,
|
|
360
|
+
pump,
|
|
361
|
+
presence,
|
|
362
|
+
abortController,
|
|
363
|
+
pumpTask: Promise.resolve(),
|
|
364
|
+
};
|
|
365
|
+
this.#paused = false;
|
|
366
|
+
receiver.pumpTask = pump.run(abortController.signal).catch((error) => {
|
|
367
|
+
this.#status.update({ lastError: `Inbox pump stopped: ${toError(error).message}` });
|
|
368
|
+
});
|
|
369
|
+
this.#receiver = receiver;
|
|
370
|
+
this.#status.update({
|
|
371
|
+
receiver: "receiving",
|
|
372
|
+
receiverGeneration: acquired.owner.generation,
|
|
373
|
+
lastError: null,
|
|
374
|
+
});
|
|
375
|
+
this.#activateTools("receiving");
|
|
376
|
+
this.#updateMainFleet(ctx, "idle");
|
|
377
|
+
await presence.idle();
|
|
378
|
+
}
|
|
379
|
+
#registerFlags() {
|
|
380
|
+
this.#pi.registerFlag("amq-handle", { type: "string", description: "Explicit AMQ handle" });
|
|
381
|
+
this.#pi.registerFlag("amq-root", { type: "string", description: "Explicit physical AMQ root" });
|
|
382
|
+
this.#pi.registerFlag("amq-session", { type: "string", description: "Explicit AMQ session" });
|
|
383
|
+
this.#pi.registerFlag("amq-mode", { type: "string", description: "Opaque AMQ mode modifier" });
|
|
384
|
+
this.#pi.registerFlag("worker-unlimited", {
|
|
385
|
+
type: "string",
|
|
386
|
+
description: "Explicit comma-separated unlimited worker-run budget dimensions (or all)",
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async #bindingFailureDiagnostic(primary) {
|
|
390
|
+
if (!this.#adapter)
|
|
391
|
+
return `${primary}; /amq bind`;
|
|
392
|
+
const root = stringFlag(this.#pi.getFlag("amq-root")) ?? process.env.AM_ROOT;
|
|
393
|
+
const handle = stringFlag(this.#pi.getFlag("amq-handle")) ?? process.env.AM_ME;
|
|
394
|
+
if (!root || !handle || !isAbsolute(root))
|
|
395
|
+
return `${primary}; /amq bind`;
|
|
396
|
+
return diagnoseBindingFailure(this.#adapter, primary, root, handle);
|
|
397
|
+
}
|
|
398
|
+
#registerRenderers() {
|
|
399
|
+
this.#pi.registerMessageRenderer(PI_AMQ_CUSTOM_TYPE, (message, { expanded }, theme) => {
|
|
400
|
+
const count = message.details?.messageIds.length ?? 0;
|
|
401
|
+
const header = theme.fg("accent", `[AMQ ${message.details?.priority ?? "mail"} · ${count}]`);
|
|
402
|
+
const raw = typeof message.content === "string" ? message.content : "[non-text AMQ message]";
|
|
403
|
+
const text = expanded ? raw : raw.split("\n").slice(0, 12).join("\n");
|
|
404
|
+
return {
|
|
405
|
+
render: (width) => `${header}\n${text}`.split("\n").map((line) => bounded(line, Math.max(1, width))),
|
|
406
|
+
invalidate: () => undefined,
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
#registerCommands() {
|
|
411
|
+
this.#pi.registerCommand("amq", {
|
|
412
|
+
description: "AMQ status, inbox, bind, pause, resume, ingest, takeover, dlq, trace, or doctor",
|
|
413
|
+
handler: async (args, ctx) => this.#handleCommand(args, ctx),
|
|
414
|
+
});
|
|
415
|
+
this.#pi.registerCommand("pi-amq-guide", {
|
|
416
|
+
description: "Read installed pi-amq guidance by topic",
|
|
417
|
+
handler: async (args, ctx) => {
|
|
418
|
+
try {
|
|
419
|
+
this.#show(ctx, await this.#guide.read(args));
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
this.#show(ctx, { error: { category: "guide", message: toError(error).message } });
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
this.#pi.registerCommand("pi-amq-cleanup", {
|
|
427
|
+
description: "Dry-run or explicitly delete old terminal workflow runs",
|
|
428
|
+
handler: async (args, ctx) => {
|
|
429
|
+
try {
|
|
430
|
+
const params = parseCleanupCommand(args);
|
|
431
|
+
this.#show(ctx, await this.#workers.cleanupRuns(params, ctx.signal, ctx));
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
this.#show(ctx, { error: { category: "workflow-cleanup", message: toError(error).message } });
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
this.#pi.registerCommand("pi-amq-doctor", {
|
|
439
|
+
description: "Read-only workflow ledger, process, lease, session, and disk diagnostics",
|
|
440
|
+
handler: async (_args, ctx) => {
|
|
441
|
+
try {
|
|
442
|
+
this.#show(ctx, await this.#workers.doctorWorkflow(ctx.signal, ctx));
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
this.#show(ctx, { error: { category: "workflow-doctor", message: toError(error).message } });
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
this.#pi.registerCommand(FRESH_SESSION_COMMAND, {
|
|
450
|
+
description: "Internal: complete a fresh-session AMQ dispatch (invoked via expandPromptTemplates, not by hand)",
|
|
451
|
+
handler: async (args, ctx) => this.#freshSessionPort.handleCommand(args, ctx),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async #handleCommand(raw, ctx) {
|
|
455
|
+
const [command = "status", ...rest] = raw.trim().split(/\s+/).filter(Boolean);
|
|
456
|
+
switch (command) {
|
|
457
|
+
case "status":
|
|
458
|
+
return this.#show(ctx, {
|
|
459
|
+
binding: this.#binding ?? null,
|
|
460
|
+
mode: ctx.mode,
|
|
461
|
+
status: this.#status.snapshot(),
|
|
462
|
+
presence: this.#receiver?.presence.snapshot() ?? null,
|
|
463
|
+
lease: this.#receiver ? await this.#receiver.lease.inspectLease() : null,
|
|
464
|
+
});
|
|
465
|
+
case "inbox": {
|
|
466
|
+
if (!this.#operations)
|
|
467
|
+
return this.#show(ctx, "pi-amq is dormant; use /amq bind");
|
|
468
|
+
const [fresh, current, journal] = await Promise.all([
|
|
469
|
+
this.#operations.inbox({ box: "new", limit: 20 }, ctx.signal),
|
|
470
|
+
this.#operations.inbox({ box: "cur", limit: 20 }, ctx.signal),
|
|
471
|
+
this.#receiver?.journal.list() ?? [],
|
|
472
|
+
]);
|
|
473
|
+
return this.#show(ctx, { new: fresh, cur: current, bridge: journal });
|
|
474
|
+
}
|
|
475
|
+
case "bind":
|
|
476
|
+
return this.#bind(ctx);
|
|
477
|
+
case "pause":
|
|
478
|
+
return this.#pause(ctx);
|
|
479
|
+
case "resume":
|
|
480
|
+
return this.#resume(ctx);
|
|
481
|
+
case "ingest": {
|
|
482
|
+
if (!this.#receiver)
|
|
483
|
+
return this.#show(ctx, "No owning receiver; /amq ingest is unavailable");
|
|
484
|
+
if (this.#paused)
|
|
485
|
+
return this.#show(ctx, "AMQ receiver is paused; resume before ingesting staged mail");
|
|
486
|
+
const count = await this.#receiver.pump.ingestStaged(ctx.signal);
|
|
487
|
+
return this.#show(ctx, `Promoted ${count} staged envelope(s)`);
|
|
488
|
+
}
|
|
489
|
+
case "takeover":
|
|
490
|
+
return this.#takeover(ctx);
|
|
491
|
+
case "dlq": {
|
|
492
|
+
if (!this.#operations)
|
|
493
|
+
return this.#show(ctx, "pi-amq is dormant; use /amq bind");
|
|
494
|
+
return this.#show(ctx, await this.#operations.dlqList(20, ctx.signal));
|
|
495
|
+
}
|
|
496
|
+
case "trace": {
|
|
497
|
+
if (!this.#operations)
|
|
498
|
+
return this.#show(ctx, "pi-amq is dormant; use /amq bind");
|
|
499
|
+
if (!rest[0])
|
|
500
|
+
return this.#show(ctx, "Usage: /amq trace <id>");
|
|
501
|
+
return this.#show(ctx, await this.#operations.trace(rest[0], ctx.signal));
|
|
502
|
+
}
|
|
503
|
+
case "doctor": {
|
|
504
|
+
if (!this.#operations)
|
|
505
|
+
return this.#show(ctx, "pi-amq is dormant; use /amq bind");
|
|
506
|
+
const result = await this.#operations.doctor(ctx.signal);
|
|
507
|
+
return this.#showDoctor(ctx, result);
|
|
508
|
+
}
|
|
509
|
+
default:
|
|
510
|
+
return this.#show(ctx, "Usage: /amq status|inbox|bind|pause|resume|ingest|takeover|dlq|trace <id>|doctor");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async #bind(ctx) {
|
|
514
|
+
if (!this.#resolver || !this.#adapter)
|
|
515
|
+
return this.#show(ctx, "AMQ binary is not initialized; reload the session");
|
|
516
|
+
const resolution = await this.#resolver.resolve({ flags: this.#flags(), environment: process.env });
|
|
517
|
+
if (resolution.kind === "bound") {
|
|
518
|
+
if (!ctx.hasUI)
|
|
519
|
+
return this.#show(ctx, `Binding requires confirmation. Candidate: ${JSON.stringify(resolution.binding)}`);
|
|
520
|
+
const confirmed = await ctx.ui.confirm("Bind pi-amq", `Bind ${resolution.binding.handle} to ${resolution.binding.root} (${resolution.binding.project}/${resolution.binding.session || "base"})?`);
|
|
521
|
+
if (!confirmed)
|
|
522
|
+
return this.#show(ctx, "AMQ bind cancelled; no state changed");
|
|
523
|
+
return this.#finishConfirmedBind(ctx, resolution.binding);
|
|
524
|
+
}
|
|
525
|
+
const onboarding = new ConfirmedBindOnboarding(this.#adapter, this.#resolver);
|
|
526
|
+
let preview;
|
|
527
|
+
try {
|
|
528
|
+
preview = await this.#onboardingPreview(onboarding, ctx);
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
return this.#show(ctx, `${resolution.diagnostic}; onboarding refused: ${toError(error).message}`);
|
|
532
|
+
}
|
|
533
|
+
if (!ctx.hasUI) {
|
|
534
|
+
return this.#show(ctx, `Binding requires TUI confirmation; pi-amq remains dormant. Preview: ${renderBindPreview(preview)}`);
|
|
535
|
+
}
|
|
536
|
+
const confirmed = await ctx.ui.confirm("Provision and bind pi-amq", renderBindPreview(preview));
|
|
537
|
+
const result = await onboarding.confirm(preview, confirmed, process.env);
|
|
538
|
+
if (result.kind !== "bound")
|
|
539
|
+
return this.#show(ctx, result.diagnostic);
|
|
540
|
+
return this.#finishConfirmedBind(ctx, result.binding);
|
|
541
|
+
}
|
|
542
|
+
async #onboardingPreview(onboarding, ctx) {
|
|
543
|
+
const defaults = this.#flags();
|
|
544
|
+
let root = defaults.root ?? join(ctx.cwd, ".agent-mail");
|
|
545
|
+
let handle = defaults.handle ?? process.env.AM_ME ?? "pi-main";
|
|
546
|
+
let agents = parseRosterInput(`user,${handle}`, handle);
|
|
547
|
+
if (ctx.hasUI) {
|
|
548
|
+
const selectedRoot = await ctx.ui.input("AMQ physical root", root);
|
|
549
|
+
if (selectedRoot === undefined)
|
|
550
|
+
throw new Error("AMQ bind cancelled before root selection; no state changed");
|
|
551
|
+
root = selectedRoot;
|
|
552
|
+
const selectedHandle = await ctx.ui.input("AMQ handle", handle);
|
|
553
|
+
if (selectedHandle === undefined)
|
|
554
|
+
throw new Error("AMQ bind cancelled before handle selection; no state changed");
|
|
555
|
+
handle = selectedHandle;
|
|
556
|
+
const selectedAgents = await ctx.ui.input("Complete AMQ roster (comma-separated, including user)", `user,${handle}`);
|
|
557
|
+
if (selectedAgents === undefined)
|
|
558
|
+
throw new Error("AMQ bind cancelled before roster selection; no state changed");
|
|
559
|
+
agents = parseRosterInput(selectedAgents, handle);
|
|
560
|
+
}
|
|
561
|
+
return onboarding.preview({
|
|
562
|
+
projectDirectory: ctx.cwd,
|
|
563
|
+
root,
|
|
564
|
+
handle,
|
|
565
|
+
agents,
|
|
566
|
+
environment: process.env,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
async #finishConfirmedBind(ctx, binding) {
|
|
570
|
+
await this.shutdown();
|
|
571
|
+
this.#started = true;
|
|
572
|
+
this.#ctx = ctx;
|
|
573
|
+
this.#adapter = new AmqCliAdapter({ binaryPath: await resolveAmqBinary(process.env.PI_AMQ_BINARY) });
|
|
574
|
+
this.#resolver = new BindingResolver(this.#adapter);
|
|
575
|
+
await this.#activateBound(ctx, binding);
|
|
576
|
+
this.#show(ctx, `Bound and verified ${binding.handle}`);
|
|
577
|
+
}
|
|
578
|
+
async #pause(ctx) {
|
|
579
|
+
const receiver = this.#receiver;
|
|
580
|
+
if (!receiver)
|
|
581
|
+
return this.#show(ctx, "No owning receiver to pause");
|
|
582
|
+
this.#paused = true;
|
|
583
|
+
this.#activateTools("send-only");
|
|
584
|
+
receiver.abortController.abort(new Error("pi-amq paused"));
|
|
585
|
+
await receiver.pumpTask;
|
|
586
|
+
await receiver.presence.paused();
|
|
587
|
+
this.#status.update({ watch: "stopped" });
|
|
588
|
+
this.#updateMainFleet(ctx, "paused");
|
|
589
|
+
this.#show(ctx, "AMQ receiver paused; lease retained");
|
|
590
|
+
}
|
|
591
|
+
async #resume(ctx) {
|
|
592
|
+
const receiver = this.#receiver;
|
|
593
|
+
if (!receiver)
|
|
594
|
+
return this.#show(ctx, "No owning receiver to resume");
|
|
595
|
+
if (!receiver.abortController.signal.aborted)
|
|
596
|
+
return this.#show(ctx, "AMQ receiver is already running");
|
|
597
|
+
if (!await this.#bindingIsCurrent()) {
|
|
598
|
+
return this.#show(ctx, "AMQ binding revalidation failed; resume refused");
|
|
599
|
+
}
|
|
600
|
+
const authority = await receiver.lease.withCurrentLease(receiver.generation, async () => true).catch(() => false);
|
|
601
|
+
if (!authority)
|
|
602
|
+
return this.#show(ctx, "Receiver lease revalidation failed; resume refused");
|
|
603
|
+
this.#paused = false;
|
|
604
|
+
this.#activateTools("receiving");
|
|
605
|
+
receiver.abortController = new AbortController();
|
|
606
|
+
receiver.pumpTask = receiver.pump.run(receiver.abortController.signal).catch((error) => {
|
|
607
|
+
this.#status.update({ lastError: `Inbox pump stopped: ${toError(error).message}` });
|
|
608
|
+
});
|
|
609
|
+
await receiver.presence.idle();
|
|
610
|
+
this.#updateMainFleet(ctx, "idle");
|
|
611
|
+
this.#show(ctx, "AMQ receiver resumed after lease revalidation");
|
|
612
|
+
}
|
|
613
|
+
async #bindingIsCurrent() {
|
|
614
|
+
if (!this.#resolver || !this.#binding)
|
|
615
|
+
return false;
|
|
616
|
+
const resolution = await this.#resolver.resolve({
|
|
617
|
+
flags: this.#flags(),
|
|
618
|
+
environment: process.env,
|
|
619
|
+
persistedBinding: this.#binding,
|
|
620
|
+
});
|
|
621
|
+
return resolution.kind === "bound"
|
|
622
|
+
&& resolution.binding.rootId === this.#binding.rootId
|
|
623
|
+
&& resolution.binding.handle === this.#binding.handle;
|
|
624
|
+
}
|
|
625
|
+
async #takeover(ctx) {
|
|
626
|
+
if (this.#receiver)
|
|
627
|
+
return this.#show(ctx, "This session already owns the receiver lease");
|
|
628
|
+
if (!this.#binding)
|
|
629
|
+
return this.#show(ctx, "pi-amq is dormant; bind first");
|
|
630
|
+
if (!ctx.hasUI)
|
|
631
|
+
return this.#show(ctx, "Takeover requires UI confirmation and never forces live or unverifiable ownership");
|
|
632
|
+
if (!await ctx.ui.confirm("AMQ takeover", "Re-inspect and quarantine only a proven-stale receiver generation?")) {
|
|
633
|
+
return this.#show(ctx, "AMQ takeover cancelled");
|
|
634
|
+
}
|
|
635
|
+
await this.start(ctx);
|
|
636
|
+
this.#show(ctx, this.#receiver ? "Receiver acquired through the ordinary stale-proof path" : this.#status.snapshot().lastError ?? "Takeover refused");
|
|
637
|
+
}
|
|
638
|
+
#installSurfaces(ctx) {
|
|
639
|
+
this.#unsubscribeStatus?.();
|
|
640
|
+
this.#unsubscribeStatus = this.#status.subscribe((status) => {
|
|
641
|
+
const projected = projectInboxStatus(ctx.mode, status);
|
|
642
|
+
ctx.ui.setStatus("pi-amq", projected.surface === "tui" ? projected.text : projected.surface === "rpc" ? JSON.stringify(projected.value) : undefined);
|
|
643
|
+
});
|
|
644
|
+
const initial = projectInboxStatus(ctx.mode, this.#status.snapshot());
|
|
645
|
+
ctx.ui.setStatus("pi-amq", initial.surface === "tui" ? initial.text : initial.surface === "rpc" ? JSON.stringify(initial.value) : undefined);
|
|
646
|
+
this.#unsubscribeTerminal?.();
|
|
647
|
+
this.#fleetModel?.dispose();
|
|
648
|
+
if (ctx.mode === "tui") {
|
|
649
|
+
ctx.ui.setWidget(WIDGET_KEY, (tui) => {
|
|
650
|
+
const model = new FleetListModel(this.#fleet, () => tui.requestRender());
|
|
651
|
+
this.#fleetModel = model;
|
|
652
|
+
return {
|
|
653
|
+
render: () => model.render(),
|
|
654
|
+
invalidate: () => undefined,
|
|
655
|
+
dispose: () => model.dispose(),
|
|
656
|
+
};
|
|
657
|
+
}, { placement: "belowEditor" });
|
|
658
|
+
this.#unsubscribeTerminal = ctx.ui.onTerminalInput((data) => {
|
|
659
|
+
const list = this.#fleetModel;
|
|
660
|
+
if (!list)
|
|
661
|
+
return undefined;
|
|
662
|
+
const result = handleFleetInput({ data, editorText: ctx.ui.getEditorText(), list });
|
|
663
|
+
if (!result.consume)
|
|
664
|
+
return undefined;
|
|
665
|
+
if (result.action === "detail" && list.selected)
|
|
666
|
+
void this.#openFleetDetail(ctx, list.selected);
|
|
667
|
+
return { consume: true };
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
else if (ctx.mode === "rpc") {
|
|
671
|
+
ctx.ui.setWidget(WIDGET_KEY, this.#fleet.snapshot().flatMap(renderFleetRow), { placement: "belowEditor" });
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async #openFleetDetail(ctx, record) {
|
|
678
|
+
await ctx.ui.custom((tui, _theme, _keys, done) => {
|
|
679
|
+
const unsubscribe = this.#fleet.subscribe(() => tui.requestRender());
|
|
680
|
+
const controller = new FleetDetailController({
|
|
681
|
+
record,
|
|
682
|
+
resolve: () => this.#fleet.snapshot().find((candidate) => candidate.id === record.id),
|
|
683
|
+
steer: async (workerId, generation, message) => {
|
|
684
|
+
await this.#workers.steerFromFleet(workerId, generation, message);
|
|
685
|
+
tui.requestRender();
|
|
686
|
+
},
|
|
687
|
+
cancel: async (workerId, generation) => {
|
|
688
|
+
await this.#workers.cancelFromFleet(workerId, generation);
|
|
689
|
+
tui.requestRender();
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
return {
|
|
693
|
+
render: () => controller.render(),
|
|
694
|
+
invalidate: () => undefined,
|
|
695
|
+
handleInput: (data) => {
|
|
696
|
+
const result = controller.handleInput(data);
|
|
697
|
+
if (result.close)
|
|
698
|
+
done(undefined);
|
|
699
|
+
else if (result.consumed)
|
|
700
|
+
tui.requestRender();
|
|
701
|
+
},
|
|
702
|
+
dispose: unsubscribe,
|
|
703
|
+
};
|
|
704
|
+
}, { overlay: true });
|
|
705
|
+
}
|
|
706
|
+
#updateMainFleet(ctx, state, selectedModel) {
|
|
707
|
+
const previous = this.#mainFleet;
|
|
708
|
+
this.#mainFleet = {
|
|
709
|
+
id: ctx.sessionManager.getSessionId(),
|
|
710
|
+
handle: this.#binding?.handle ?? "main",
|
|
711
|
+
role: "main",
|
|
712
|
+
model: selectedModel ?? ctx.model?.id ?? previous?.model ?? "unselected",
|
|
713
|
+
state,
|
|
714
|
+
spend: null,
|
|
715
|
+
lastActivity: new Date().toISOString(),
|
|
716
|
+
mode: "main",
|
|
717
|
+
};
|
|
718
|
+
this.#commitFleet();
|
|
719
|
+
}
|
|
720
|
+
#commitFleet() {
|
|
721
|
+
this.#fleet.update([
|
|
722
|
+
...(this.#mainFleet ? [this.#mainFleet] : []),
|
|
723
|
+
...this.#workerFleet,
|
|
724
|
+
]);
|
|
725
|
+
if (this.#ctx?.mode === "rpc") {
|
|
726
|
+
this.#ctx.ui.setWidget(WIDGET_KEY, this.#fleet.snapshot().flatMap(renderFleetRow), { placement: "belowEditor" });
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
#activateTools(mode) {
|
|
730
|
+
const existing = this.#pi.getActiveTools().filter((name) => !ALL_AMQ_TOOLS.includes(name));
|
|
731
|
+
const enabled = mode === "dormant"
|
|
732
|
+
? []
|
|
733
|
+
: mode === "send-only"
|
|
734
|
+
? ["amq_send", "amq_reply", ...NON_CONSUMING_AMQ_TOOLS]
|
|
735
|
+
: [...ALL_AMQ_TOOLS];
|
|
736
|
+
this.#pi.setActiveTools([...existing, ...enabled]);
|
|
737
|
+
}
|
|
738
|
+
#flags() {
|
|
739
|
+
return {
|
|
740
|
+
...flag(this.#pi.getFlag("amq-handle"), "handle"),
|
|
741
|
+
...flag(this.#pi.getFlag("amq-root"), "root"),
|
|
742
|
+
...flag(this.#pi.getFlag("amq-session"), "session"),
|
|
743
|
+
...flag(this.#pi.getFlag("amq-mode"), "mode"),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
#show(ctx, value) {
|
|
747
|
+
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
748
|
+
ctx.ui.notify(bounded(text, 12 * 1024), text.includes("error") ? "warning" : "info");
|
|
749
|
+
}
|
|
750
|
+
#showDoctor(ctx, value) {
|
|
751
|
+
if (ctx.mode === "print" || ctx.mode === "json") {
|
|
752
|
+
return this.#show(ctx, "Doctor UI cards are unavailable in this mode; run `amq doctor --root <verified-root> --ops --json` manually");
|
|
753
|
+
}
|
|
754
|
+
if (typeof value === "object" && value && "ok" in value && "value" in value
|
|
755
|
+
&& value.ok) {
|
|
756
|
+
const payload = value.value;
|
|
757
|
+
const findings = Array.isArray(payload)
|
|
758
|
+
? payload
|
|
759
|
+
: payload && typeof payload === "object" && "checks" in payload && Array.isArray(payload.checks)
|
|
760
|
+
? payload.checks
|
|
761
|
+
: undefined;
|
|
762
|
+
if (findings) {
|
|
763
|
+
for (const finding of findings.slice(0, 50)) {
|
|
764
|
+
const card = finding && typeof finding === "object"
|
|
765
|
+
? finding
|
|
766
|
+
: { message: finding };
|
|
767
|
+
const status = typeof card.status === "string" ? card.status : "info";
|
|
768
|
+
const name = typeof card.name === "string" ? card.name : "AMQ doctor";
|
|
769
|
+
const message = typeof card.message === "string" ? card.message : JSON.stringify(card);
|
|
770
|
+
ctx.ui.notify(`${name}: ${message}`, status === "error" ? "error" : status === "warn" ? "warning" : "info");
|
|
771
|
+
}
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
this.#show(ctx, value);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
export function parseCleanupCommand(raw) {
|
|
779
|
+
const args = raw.trim().split(/\s+/).filter(Boolean);
|
|
780
|
+
let age;
|
|
781
|
+
let dryRun = true;
|
|
782
|
+
let yes = false;
|
|
783
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
784
|
+
const arg = args[index];
|
|
785
|
+
switch (arg) {
|
|
786
|
+
case "--older-than": {
|
|
787
|
+
const value = args[index + 1];
|
|
788
|
+
if (!value)
|
|
789
|
+
throw new TypeError("--older-than requires a duration");
|
|
790
|
+
age = parseCleanupAge(value);
|
|
791
|
+
index += 1;
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
case "--dry-run":
|
|
795
|
+
dryRun = true;
|
|
796
|
+
break;
|
|
797
|
+
case "--delete":
|
|
798
|
+
dryRun = false;
|
|
799
|
+
break;
|
|
800
|
+
case "--yes":
|
|
801
|
+
yes = true;
|
|
802
|
+
break;
|
|
803
|
+
default:
|
|
804
|
+
throw new TypeError(`Unknown cleanup option: ${arg}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (age === undefined) {
|
|
808
|
+
throw new TypeError("Usage: /pi-amq-cleanup --older-than <duration> [--delete] [--yes]");
|
|
809
|
+
}
|
|
810
|
+
return { older_than_ms: age, dry_run: dryRun, yes };
|
|
811
|
+
}
|
|
812
|
+
export async function diagnoseBindingFailure(adapter, primary, root, handle) {
|
|
813
|
+
const diagnostic = await adapter.runJson([
|
|
814
|
+
"route", "explain", "--root", root, "--me", handle, "--to", handle,
|
|
815
|
+
]);
|
|
816
|
+
const projected = diagnostic.ok
|
|
817
|
+
? safeProjection(diagnostic.json)
|
|
818
|
+
: {
|
|
819
|
+
category: diagnostic.category,
|
|
820
|
+
message: diagnostic.stderr.trim() || diagnostic.jsonError?.message || diagnostic.processError?.message,
|
|
821
|
+
};
|
|
822
|
+
return `${primary}; /amq bind; route=${JSON.stringify(projected)}`;
|
|
823
|
+
}
|
|
824
|
+
class PiSessionProjectionLookup {
|
|
825
|
+
#ctx;
|
|
826
|
+
constructor(ctx) {
|
|
827
|
+
this.#ctx = ctx;
|
|
828
|
+
}
|
|
829
|
+
async find(messageId, targetSessionId) {
|
|
830
|
+
if (this.#ctx.sessionManager.getSessionId() !== targetSessionId)
|
|
831
|
+
return undefined;
|
|
832
|
+
const sessionFile = this.#ctx.sessionManager.getSessionFile();
|
|
833
|
+
if (!sessionFile)
|
|
834
|
+
return undefined;
|
|
835
|
+
let contents;
|
|
836
|
+
try {
|
|
837
|
+
contents = await readFile(sessionFile, "utf8");
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
const found = contents.split("\n").some((line) => diskLineContainsMessage(line, messageId));
|
|
843
|
+
return found ? { messageId, sessionId: targetSessionId, diskVisible: true } : undefined;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
export function diskLineContainsMessage(line, messageId) {
|
|
847
|
+
if (line.length === 0)
|
|
848
|
+
return false;
|
|
849
|
+
try {
|
|
850
|
+
const entry = JSON.parse(line);
|
|
851
|
+
if (entry.type !== "custom_message" || entry.customType !== PI_AMQ_CUSTOM_TYPE)
|
|
852
|
+
return false;
|
|
853
|
+
const details = entry.details;
|
|
854
|
+
if (!details || typeof details !== "object" || Array.isArray(details))
|
|
855
|
+
return false;
|
|
856
|
+
const messageIds = details.messageIds;
|
|
857
|
+
return Array.isArray(messageIds) && messageIds.includes(messageId);
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
export class PiDeliverySink {
|
|
864
|
+
#pi;
|
|
865
|
+
#lookup;
|
|
866
|
+
constructor(pi, lookup) {
|
|
867
|
+
this.#pi = pi;
|
|
868
|
+
this.#lookup = lookup;
|
|
869
|
+
}
|
|
870
|
+
async deliver(request) {
|
|
871
|
+
try {
|
|
872
|
+
this.#pi.sendMessage(request.message, request.options);
|
|
873
|
+
const projections = await Promise.all(request.message.details.messageIds.map(async (messageId) => (await this.#lookup.find(messageId, request.targetSessionId, request.signal)
|
|
874
|
+
?? { messageId, sessionId: request.targetSessionId, diskVisible: false })));
|
|
875
|
+
return { kind: "accepted", projections };
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
return { kind: "indeterminate", error: toError(error) };
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const FRESH_SESSION_PROVENANCE_TYPE = "pi-amq-fresh-session-provenance/v1";
|
|
883
|
+
const DEFAULT_FRESH_SESSION_DISPATCH_TIMEOUT_MS = 30_000;
|
|
884
|
+
/**
|
|
885
|
+
* Bridges the `FreshSessionReplacementPort` contract to real pi command-context session
|
|
886
|
+
* control (design.md §8.3). `waitForIdle` cannot itself call `ctx.waitForIdle()`: that method
|
|
887
|
+
* only exists on `ExtensionCommandContext`, which pi hands over solely by dispatching the
|
|
888
|
+
* `/amq-new-task` command, and dispatch requires the message ID that `waitForIdle` never
|
|
889
|
+
* receives. So `replace()` fires that dispatch and correlates the eventual command-handler
|
|
890
|
+
* completion (which performs the real `ctx.waitForIdle()` as its first step, unconditionally)
|
|
891
|
+
* back to this call via a per-messageId handoff.
|
|
892
|
+
*/
|
|
893
|
+
export class PiFreshSessionReplacementPort {
|
|
894
|
+
#pi;
|
|
895
|
+
#dispatchTimeoutMs;
|
|
896
|
+
#pending = new Map();
|
|
897
|
+
constructor(pi, options = {}) {
|
|
898
|
+
this.#pi = pi;
|
|
899
|
+
this.#dispatchTimeoutMs = options.dispatchTimeoutMs ?? DEFAULT_FRESH_SESSION_DISPATCH_TIMEOUT_MS;
|
|
900
|
+
}
|
|
901
|
+
async waitForIdle(signal) {
|
|
902
|
+
if (signal?.aborted)
|
|
903
|
+
throw abortError(signal);
|
|
904
|
+
}
|
|
905
|
+
replace(message, signal) {
|
|
906
|
+
const messageId = message.header.id;
|
|
907
|
+
if (this.#pending.has(messageId)) {
|
|
908
|
+
return Promise.reject(new Error(`Fresh-session dispatch for ${messageId} is already in flight`));
|
|
909
|
+
}
|
|
910
|
+
return new Promise((resolve, reject) => {
|
|
911
|
+
const onAbort = () => settle.reject(abortError(signal));
|
|
912
|
+
const settle = {
|
|
913
|
+
message,
|
|
914
|
+
resolve: (result) => {
|
|
915
|
+
this.#finish(messageId, onAbort, signal);
|
|
916
|
+
resolve(result);
|
|
917
|
+
},
|
|
918
|
+
reject: (error) => {
|
|
919
|
+
this.#finish(messageId, onAbort, signal);
|
|
920
|
+
reject(error);
|
|
921
|
+
},
|
|
922
|
+
timer: setTimeout(() => {
|
|
923
|
+
settle.reject(new Error(`Fresh-session command dispatch for ${messageId} did not reach the /${FRESH_SESSION_COMMAND} `
|
|
924
|
+
+ `handler within ${this.#dispatchTimeoutMs}ms`));
|
|
925
|
+
}, this.#dispatchTimeoutMs),
|
|
926
|
+
};
|
|
927
|
+
this.#pending.set(messageId, settle);
|
|
928
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
929
|
+
try {
|
|
930
|
+
this.#pi.sendUserMessage(`/${FRESH_SESSION_COMMAND} ${messageId}`, { expandPromptTemplates: true });
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
settle.reject(toError(error));
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
#finish(messageId, onAbort, signal) {
|
|
938
|
+
const handoff = this.#pending.get(messageId);
|
|
939
|
+
if (!handoff)
|
|
940
|
+
return;
|
|
941
|
+
clearTimeout(handoff.timer);
|
|
942
|
+
signal?.removeEventListener("abort", onAbort);
|
|
943
|
+
this.#pending.delete(messageId);
|
|
944
|
+
}
|
|
945
|
+
/** Invoked by the registered `/amq-new-task` command once pi dispatches it. */
|
|
946
|
+
async handleCommand(rawArgs, ctx) {
|
|
947
|
+
const messageId = rawArgs.trim();
|
|
948
|
+
const handoff = messageId.length > 0 ? this.#pending.get(messageId) : undefined;
|
|
949
|
+
if (!handoff) {
|
|
950
|
+
ctx.ui.notify(`AMQ fresh-session dispatch has no matching pending request for "${messageId}"`, "warning");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
try {
|
|
954
|
+
handoff.resolve(await this.#replaceSession(handoff.message, ctx));
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
handoff.reject(toError(error));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async #replaceSession(message, ctx) {
|
|
961
|
+
await ctx.waitForIdle();
|
|
962
|
+
let projection;
|
|
963
|
+
const parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
964
|
+
const { cancelled } = await ctx.newSession({
|
|
965
|
+
...(parentSessionFile ? { parentSession: parentSessionFile } : {}),
|
|
966
|
+
setup: async (sessionManager) => {
|
|
967
|
+
sessionManager.appendCustomEntry(FRESH_SESSION_PROVENANCE_TYPE, {
|
|
968
|
+
schema: 1,
|
|
969
|
+
messageId: message.header.id,
|
|
970
|
+
from: message.header.from,
|
|
971
|
+
fromProject: message.header.fromProject,
|
|
972
|
+
parentSessionId: ctx.sessionManager.getSessionId(),
|
|
973
|
+
});
|
|
974
|
+
},
|
|
975
|
+
withSession: async (fresh) => {
|
|
976
|
+
const freshSessionId = fresh.sessionManager.getSessionId();
|
|
977
|
+
const [request] = planPriorityDeliveries([message], freshSessionId, {
|
|
978
|
+
deliveryOverride: { deliverAs: "followUp", triggerTurn: true },
|
|
979
|
+
});
|
|
980
|
+
if (!request)
|
|
981
|
+
throw new Error(`Fresh-session kickoff envelope for ${message.header.id} could not be built`);
|
|
982
|
+
await fresh.sendMessage(request.message, request.options);
|
|
983
|
+
// Pi persists a fresh session's file lazily: nothing is written to disk until its
|
|
984
|
+
// first assistant entry arrives (session-manager `_persist`). Immediately after the
|
|
985
|
+
// kickoff send, the session file may legitimately not exist yet, so a missing disk
|
|
986
|
+
// projection is not an error here — fall back to the in-memory branch, which pi
|
|
987
|
+
// always keeps current. Journal reconciliation later promotes this to `injected`
|
|
988
|
+
// once the projection becomes disk-visible (transport/inbox-pump.ts `#reconcileStartup`).
|
|
989
|
+
const diskProjection = await new PiSessionProjectionLookup(fresh)
|
|
990
|
+
.find(message.header.id, freshSessionId);
|
|
991
|
+
projection = diskProjection ?? (branchContainsMessage(fresh.sessionManager.getBranch(), message.header.id)
|
|
992
|
+
? { messageId: message.header.id, sessionId: freshSessionId, diskVisible: false }
|
|
993
|
+
: undefined);
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
if (cancelled)
|
|
997
|
+
return { cancelled: true };
|
|
998
|
+
if (!projection || projection.messageId !== message.header.id) {
|
|
999
|
+
throw new Error(`Fresh-session replacement for ${message.header.id} completed without a matching in-memory or `
|
|
1000
|
+
+ "disk-visible projection");
|
|
1001
|
+
}
|
|
1002
|
+
return { cancelled: false, projection };
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function branchContainsMessage(entries, messageId) {
|
|
1006
|
+
return entries.some((entry) => {
|
|
1007
|
+
if (entry.type !== "custom_message" || entry.customType !== PI_AMQ_CUSTOM_TYPE)
|
|
1008
|
+
return false;
|
|
1009
|
+
const details = entry.details;
|
|
1010
|
+
return Array.isArray(details?.messageIds) && details.messageIds.includes(messageId);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
function abortError(signal) {
|
|
1014
|
+
return signal?.reason instanceof Error ? signal.reason : new Error("Fresh-session dispatch was cancelled");
|
|
1015
|
+
}
|
|
1016
|
+
function collectEnvelopeDetails(ctx) {
|
|
1017
|
+
return ctx.sessionManager.getBranch().flatMap((entry) => {
|
|
1018
|
+
if (entry.type !== "custom_message" || entry.customType !== PI_AMQ_CUSTOM_TYPE)
|
|
1019
|
+
return [];
|
|
1020
|
+
const details = entry.details;
|
|
1021
|
+
return Array.isArray(details?.envelopes) ? details.envelopes : [];
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
function replyRoutes(ctx) {
|
|
1025
|
+
return {
|
|
1026
|
+
lookup(messageId) {
|
|
1027
|
+
const envelope = collectEnvelopeDetails(ctx).find((candidate) => candidate.id === messageId);
|
|
1028
|
+
if (!envelope)
|
|
1029
|
+
return undefined;
|
|
1030
|
+
const to = envelope.replyTo || envelope.from;
|
|
1031
|
+
if (!to)
|
|
1032
|
+
return undefined;
|
|
1033
|
+
const project = envelope.replyProject || envelope.fromProject;
|
|
1034
|
+
return { to, ...(project ? { project } : {}) };
|
|
1035
|
+
},
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function renderBindPreview(preview) {
|
|
1039
|
+
return [
|
|
1040
|
+
`kind=${preview.kind}`,
|
|
1041
|
+
`physical-root=${preview.root}`,
|
|
1042
|
+
`session=${preview.session || "base"}`,
|
|
1043
|
+
`project=${preview.project}`,
|
|
1044
|
+
`handle=${preview.handle}`,
|
|
1045
|
+
`roster=${preview.agents.join(",")}`,
|
|
1046
|
+
`command=${preview.commandText}`,
|
|
1047
|
+
...preview.effects.map((effect) => `effect=${effect}`),
|
|
1048
|
+
].join("\n");
|
|
1049
|
+
}
|
|
1050
|
+
function latestBinding(ctx) {
|
|
1051
|
+
const entries = ctx.sessionManager.getEntries();
|
|
1052
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
1053
|
+
const entry = entries[index];
|
|
1054
|
+
if (entry.type !== "custom" || entry.customType !== PI_AMQ_BINDING_ENTRY_TYPE)
|
|
1055
|
+
continue;
|
|
1056
|
+
return entry.data;
|
|
1057
|
+
}
|
|
1058
|
+
return undefined;
|
|
1059
|
+
}
|
|
1060
|
+
async function resolveAmqBinary(configured) {
|
|
1061
|
+
if (configured) {
|
|
1062
|
+
if (!isAbsolute(configured))
|
|
1063
|
+
throw new Error("PI_AMQ_BINARY must be an absolute path");
|
|
1064
|
+
return configured;
|
|
1065
|
+
}
|
|
1066
|
+
for (const candidate of ["/opt/homebrew/bin/amq", "/usr/local/bin/amq", "/usr/bin/amq"]) {
|
|
1067
|
+
try {
|
|
1068
|
+
await access(candidate);
|
|
1069
|
+
return candidate;
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
// Try the next conventional absolute path.
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return "/usr/local/bin/amq";
|
|
1076
|
+
}
|
|
1077
|
+
function freshAllowlist() {
|
|
1078
|
+
return new Set((process.env.PI_AMQ_FRESH_ALLOWLIST ?? "").split(",").map((item) => item.trim()).filter(Boolean));
|
|
1079
|
+
}
|
|
1080
|
+
function renderFleetRow(record) {
|
|
1081
|
+
return [`${record.handle}/${record.role} ${record.model} ${record.state} ${record.lastActivity}`];
|
|
1082
|
+
}
|
|
1083
|
+
function flag(value, key) {
|
|
1084
|
+
return typeof value === "string" && value.length > 0 ? { [key]: value } : {};
|
|
1085
|
+
}
|
|
1086
|
+
function stringFlag(value) {
|
|
1087
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
1088
|
+
}
|
|
1089
|
+
function bounded(value, maximum) {
|
|
1090
|
+
return value.length <= maximum ? value : `${value.slice(0, maximum - 1)}…`;
|
|
1091
|
+
}
|
|
1092
|
+
function toError(error) {
|
|
1093
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1094
|
+
}
|
|
1095
|
+
//# sourceMappingURL=runtime.js.map
|