@haywoodmarx/agent-pager 0.1.3 → 0.2.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/README.md +4 -4
- package/dist/src/broker/address.d.ts +7 -0
- package/dist/src/broker/address.js +9 -0
- package/dist/src/broker/broker.d.ts +112 -24
- package/dist/src/broker/broker.js +604 -132
- package/dist/src/broker/operator-projection.d.ts +22 -0
- package/dist/src/broker/operator-projection.js +43 -5
- package/dist/src/broker/protocol.d.ts +1 -1
- package/dist/src/broker/protocol.js +1 -1
- package/dist/src/broker/schema.d.ts +4 -0
- package/dist/src/broker/schema.js +20 -8
- package/dist/src/broker/snapshot.d.ts +4 -2
- package/dist/src/implementations/claude/gateway-cli.js +1 -2
- package/dist/src/implementations/claude/gateway.d.ts +11 -4
- package/dist/src/implementations/claude/gateway.js +44 -14
- package/dist/src/implementations/claude/instructions.js +1 -1
- package/dist/src/implementations/model.d.ts +6 -3
- package/dist/src/implementations/opencode/plugin.d.ts +7 -2
- package/dist/src/implementations/opencode/plugin.js +103 -7
- package/dist/src/implementations/pi/broker-client.d.ts +13 -4
- package/dist/src/implementations/pi/broker-client.js +71 -9
- package/dist/src/implementations/pi/extension.d.ts +7 -2
- package/dist/src/implementations/pi/extension.js +15 -3
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/launch/broker-lock.d.ts +58 -4
- package/dist/src/launch/broker-lock.js +140 -7
- package/dist/src/launch/launch.d.ts +1 -1
- package/dist/src/launch/on-demand.d.ts +8 -8
- package/dist/src/launch/on-demand.js +54 -35
- package/dist/src/launch/runtime-composition.js +2 -0
- package/dist/src/launch/runtime.js +4 -14
- package/dist/src/launch/service.d.ts +1 -0
- package/dist/src/launch/service.js +23 -1
- package/dist/src/launch/workspace-branch.d.ts +3 -3
- package/dist/src/launch/workspace-branch.js +7 -15
- package/dist/src/launch/workspace-root.d.ts +13 -0
- package/dist/src/launch/workspace-root.js +27 -0
- package/dist/src/messaging/delivery-client.d.ts +17 -0
- package/dist/src/messaging/delivery-client.js +31 -1
- package/dist/src/messaging/identity-statement.d.ts +7 -0
- package/dist/src/messaging/identity-statement.js +8 -0
- package/dist/src/messaging/mcp.d.ts +8 -2
- package/dist/src/messaging/mcp.js +54 -1
- package/dist/src/messaging/notifications.d.ts +31 -3
- package/dist/src/messaging/notifications.js +25 -0
- package/dist/src/messaging/push-delivery.d.ts +2 -1
- package/dist/src/messaging/push-delivery.js +24 -7
- package/dist/src/messaging/tools.js +22 -2
- package/dist/src/messaging/wake.js +7 -0
- package/dist/src/operator/agent-projection.d.ts +8 -5
- package/dist/src/operator/agent-projection.js +29 -9
- package/dist/src/operator/commands.d.ts +2 -0
- package/dist/src/operator/commands.js +57 -21
- package/dist/src/operator/protocol.d.ts +34 -2
- package/dist/src/operator/protocol.js +9 -0
- package/dist/src/operator/repository-system.d.ts +29 -0
- package/dist/src/operator/repository-system.js +39 -12
- package/dist/src/operator/repository.d.ts +28 -11
- package/dist/src/operator/repository.js +37 -4
- package/dist/src/operator/transport.js +10 -5
- package/dist/src/operator/updates.js +7 -1
- package/dist/src/operator/validation.js +35 -1
- package/dist/src/shared/versions.d.ts +18 -0
- package/dist/src/shared/versions.js +26 -0
- package/dist/src/tui/agent-tree.d.ts +8 -5
- package/dist/src/tui/agent-tree.js +195 -22
- package/dist/src/tui/app.js +6 -2
- package/dist/src/tui/message-views.js +4 -3
- package/dist/src/tui/model.d.ts +25 -1
- package/dist/src/tui/model.js +33 -9
- package/dist/src/tui/presentation.d.ts +11 -2
- package/dist/src/tui/presentation.js +26 -4
- package/dist/src/version.d.ts +7 -0
- package/dist/src/version.js +14 -6
- package/package.json +1 -1
- package/skills/pager/SKILL.md +35 -45
|
@@ -4,7 +4,7 @@ import { adapterHeartbeatIntervalMs } from "../../configuration-defaults.js";
|
|
|
4
4
|
import { implementationId } from "../registry.js";
|
|
5
5
|
import { ensureBrokerService } from "../../launch/on-demand.js";
|
|
6
6
|
import { readEmbeddedRuntimeEnvironment, resolveBunExecutable } from "../../launch/runtime.js";
|
|
7
|
-
import { BrokerDeliveryClient, exactRecord, registerAgent } from "../../messaging/delivery-client.js";
|
|
7
|
+
import { BrokerDeliveryClient, exactRecord, isExpiredRefusal, registerAgent } from "../../messaging/delivery-client.js";
|
|
8
8
|
import { openPagerInbox } from "../../messaging/inbox-socket.js";
|
|
9
9
|
import { OpenCodeInboundAdapter } from "./adapter.js";
|
|
10
10
|
import { createOpenCodeSelfCloseTool } from "./self-close-tool.js";
|
|
@@ -46,7 +46,9 @@ export function createAgentMessagingPlugin(dependencies) {
|
|
|
46
46
|
}
|
|
47
47
|
else {
|
|
48
48
|
try {
|
|
49
|
-
brokerUrl = (await dependencies.ensureService(dependencies.readEnvironment())
|
|
49
|
+
brokerUrl = (await dependencies.ensureService(dependencies.readEnvironment(), (message) => {
|
|
50
|
+
void log(client, "info", message.trim()).catch(() => undefined);
|
|
51
|
+
})).mcpUrl;
|
|
50
52
|
identityPath = "self-registration";
|
|
51
53
|
}
|
|
52
54
|
catch (error) {
|
|
@@ -60,6 +62,11 @@ export function createAgentMessagingPlugin(dependencies) {
|
|
|
60
62
|
let connecting;
|
|
61
63
|
let inbox;
|
|
62
64
|
let inboxOpening;
|
|
65
|
+
let conversationName;
|
|
66
|
+
let rejoining;
|
|
67
|
+
let attachments = 0;
|
|
68
|
+
let pendingMessagingReservation;
|
|
69
|
+
let movingMessagingServer = Promise.resolve();
|
|
63
70
|
// Concurrent chat turns share one self-registration: a second registration for the same
|
|
64
71
|
// conversation displaces the first's reservations, and the messaging one is single-use.
|
|
65
72
|
let registering;
|
|
@@ -91,6 +98,7 @@ export function createAgentMessagingPlugin(dependencies) {
|
|
|
91
98
|
const initializeIdentity = async (sessionName) => {
|
|
92
99
|
if (adapter)
|
|
93
100
|
return adapter;
|
|
101
|
+
conversationName ??= sessionName;
|
|
94
102
|
if (!reservation || !agentId) {
|
|
95
103
|
registering ??= selfRegister(sessionName).catch((error) => {
|
|
96
104
|
registering = undefined;
|
|
@@ -176,6 +184,96 @@ export function createAgentMessagingPlugin(dependencies) {
|
|
|
176
184
|
});
|
|
177
185
|
return connecting;
|
|
178
186
|
};
|
|
187
|
+
/**
|
|
188
|
+
* Register the conversation again by its key, after the broker refused this process's sessions as
|
|
189
|
+
* expired. The delivery channel moves to the identity that results first, and a messaging server
|
|
190
|
+
* that cannot then be moved is moved by a later heartbeat.
|
|
191
|
+
* @param sessionName - Conversation identifier OpenCode reported, from which the key derives.
|
|
192
|
+
*/
|
|
193
|
+
const rejoinConversation = async (sessionName) => {
|
|
194
|
+
const sessionKey = harnessSessionKey(implementationId("opencode"), sessionName);
|
|
195
|
+
if (!sessionKey)
|
|
196
|
+
throw new Error("The conversation has no key to register under");
|
|
197
|
+
const registered = await registerAgent((input, init) => dependencies.fetch(input, init), brokerUrl, {
|
|
198
|
+
workspacePath: context.directory,
|
|
199
|
+
implementationId: implementationId("opencode"),
|
|
200
|
+
sessionKey,
|
|
201
|
+
});
|
|
202
|
+
reservation = registered.deliveryReservation;
|
|
203
|
+
agentId = registered.agentId;
|
|
204
|
+
deliveryClient = undefined;
|
|
205
|
+
connecting = undefined;
|
|
206
|
+
attachments += 1;
|
|
207
|
+
pendingMessagingReservation = registered.messagingReservation;
|
|
208
|
+
await moveMessagingServerToRejoinedIdentity();
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Point the messaging server at the rejoined identity, and stop trying once it is there.
|
|
212
|
+
*
|
|
213
|
+
* Moves run one after another, so a move that finds none pending, because an earlier one succeeded,
|
|
214
|
+
* adds nothing.
|
|
215
|
+
* @returns - Resolves once no move is pending.
|
|
216
|
+
* @throws {Error} When OpenCode refuses the server; the move stays pending.
|
|
217
|
+
*/
|
|
218
|
+
const moveMessagingServerToRejoinedIdentity = () => {
|
|
219
|
+
const move = movingMessagingServer.catch(() => undefined).then(addPendingMessagingServer);
|
|
220
|
+
movingMessagingServer = move;
|
|
221
|
+
return move;
|
|
222
|
+
};
|
|
223
|
+
const addPendingMessagingServer = async () => {
|
|
224
|
+
const messagingReservation = pendingMessagingReservation;
|
|
225
|
+
if (messagingReservation === undefined)
|
|
226
|
+
return;
|
|
227
|
+
const added = await client.mcp.add({
|
|
228
|
+
body: {
|
|
229
|
+
name: "pager",
|
|
230
|
+
config: {
|
|
231
|
+
type: "remote",
|
|
232
|
+
url: brokerUrl,
|
|
233
|
+
headers: { [ATTACH_HEADER]: messagingReservation },
|
|
234
|
+
oauth: false,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
query: { directory: context.directory },
|
|
238
|
+
});
|
|
239
|
+
if (added.error)
|
|
240
|
+
throw new Error("Broker messaging tools could not be moved to the rejoined identity");
|
|
241
|
+
if (pendingMessagingReservation === messagingReservation)
|
|
242
|
+
pendingMessagingReservation = undefined;
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* Run one delivery-channel call; when the broker refuses it as expired and the conversation is
|
|
246
|
+
* known, rejoin once and retry. Calls refused together share one rejoin.
|
|
247
|
+
* @param attempt - Operation to run, which is invoked again after a rejoin.
|
|
248
|
+
* @returns - Result of the first attempt that the broker accepts.
|
|
249
|
+
*/
|
|
250
|
+
const recovering = async (attempt) => {
|
|
251
|
+
await rejoining?.catch(() => undefined);
|
|
252
|
+
const presentedAttachments = attachments;
|
|
253
|
+
try {
|
|
254
|
+
return await attempt();
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
const sessionName = conversationName;
|
|
258
|
+
if (sessionName === undefined || !isExpiredRefusal(error))
|
|
259
|
+
throw error;
|
|
260
|
+
if (rejoining)
|
|
261
|
+
await rejoining;
|
|
262
|
+
else if (attachments === presentedAttachments) {
|
|
263
|
+
rejoining = rejoinConversation(sessionName).finally(() => {
|
|
264
|
+
rejoining = undefined;
|
|
265
|
+
});
|
|
266
|
+
await rejoining;
|
|
267
|
+
}
|
|
268
|
+
return attempt();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const heartbeat = async () => {
|
|
272
|
+
await moveMessagingServerToRejoinedIdentity().catch((error) => logFailure(error));
|
|
273
|
+
await recovering(async () => {
|
|
274
|
+
await (await connectDeliveryClient()).heartbeat();
|
|
275
|
+
});
|
|
276
|
+
};
|
|
179
277
|
const call = async (tool, args) => {
|
|
180
278
|
const connected = await connectDeliveryClient();
|
|
181
279
|
return connected.call(tool, args);
|
|
@@ -207,16 +305,14 @@ export function createAgentMessagingPlugin(dependencies) {
|
|
|
207
305
|
inbox = opened;
|
|
208
306
|
const connected = await connectDeliveryClient();
|
|
209
307
|
connected.presentDeliveryAddress(inbox.address);
|
|
210
|
-
await
|
|
308
|
+
await heartbeat();
|
|
211
309
|
})();
|
|
212
310
|
await inboxOpening;
|
|
213
311
|
};
|
|
214
312
|
const logFailure = (error) => log(client, "warn", error instanceof Error ? error.message : "Broker request failed");
|
|
215
313
|
const stopHeartbeat = dependencies.scheduler.repeat(adapterHeartbeatIntervalMs(dependencies.processEnv), () => {
|
|
216
314
|
if (reservation && agentId)
|
|
217
|
-
void
|
|
218
|
-
.then((connected) => connected.heartbeat())
|
|
219
|
-
.catch(logFailure);
|
|
315
|
+
void heartbeat().catch(logFailure);
|
|
220
316
|
});
|
|
221
317
|
let runtimeStopped = false;
|
|
222
318
|
stopRuntime = () => {
|
|
@@ -261,7 +357,7 @@ export function createAgentMessagingPlugin(dependencies) {
|
|
|
261
357
|
}
|
|
262
358
|
const defaultDependencies = {
|
|
263
359
|
readEnvironment: () => readEmbeddedRuntimeEnvironment(process.env, resolveBunExecutable),
|
|
264
|
-
ensureService: (environment) => ensureBrokerService(environment),
|
|
360
|
+
ensureService: (environment, report) => ensureBrokerService(environment, { report }),
|
|
265
361
|
processEnv: process.env,
|
|
266
362
|
fetch: (url, init) => globalThis.fetch(url, init),
|
|
267
363
|
scheduler: {
|
|
@@ -8,8 +8,8 @@ export type RegisterPiAgentOptions = {
|
|
|
8
8
|
brokerUrl: string;
|
|
9
9
|
/** Absolute Pi working directory. */
|
|
10
10
|
workspacePath: string;
|
|
11
|
-
/** Stable Pi session identity used as the initial alias. */
|
|
12
|
-
sessionName
|
|
11
|
+
/** Stable Pi session identity used as the initial alias; absent leaves the alias to the broker. */
|
|
12
|
+
sessionName?: string;
|
|
13
13
|
/** Resumable key that rejoins this conversation's identity after a restart. */
|
|
14
14
|
sessionKey?: string | undefined;
|
|
15
15
|
};
|
|
@@ -19,7 +19,14 @@ export type RegisterPiAgentOptions = {
|
|
|
19
19
|
* @returns - Registered identity with separate messaging and delivery attachments.
|
|
20
20
|
*/
|
|
21
21
|
export declare function registerPiAgent({ request, brokerUrl, workspacePath, sessionName, sessionKey, }: RegisterPiAgentOptions): Promise<BrokerIdentity>;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Authenticated messaging and delivery operations used by the Pi extension.
|
|
24
|
+
*
|
|
25
|
+
* When the broker refuses the attachments this client holds as `expired` and a rejoin was supplied, a
|
|
26
|
+
* call registers the conversation again, attaches both channels with the reservations that return,
|
|
27
|
+
* and is retried once; calls refused together share one rejoin. Any other refusal reaches the caller
|
|
28
|
+
* unchanged.
|
|
29
|
+
*/
|
|
23
30
|
export declare class PiBrokerClient {
|
|
24
31
|
#private;
|
|
25
32
|
private constructor();
|
|
@@ -29,9 +36,11 @@ export declare class PiBrokerClient {
|
|
|
29
36
|
* @param identity - Reserved or directly registered broker identity.
|
|
30
37
|
* @param request - HTTP operation used to reach the loopback broker.
|
|
31
38
|
* @param sessionKey - Resumable conversation key, which the broker may resolve to another identity.
|
|
39
|
+
* @param rejoin - Registers this conversation again by its key and returns the attachments for the
|
|
40
|
+
* identity that results; absent when nothing identifies the conversation to register.
|
|
32
41
|
* @returns - Connected client whose identity is the one the broker resolved, not the one requested.
|
|
33
42
|
*/
|
|
34
|
-
static connect(brokerUrl: string, identity: BrokerIdentity, request: BrokerRequest, sessionKey?: string): Promise<PiBrokerClient>;
|
|
43
|
+
static connect(brokerUrl: string, identity: BrokerIdentity, request: BrokerRequest, sessionKey?: string, rejoin?: () => Promise<BrokerIdentity>): Promise<PiBrokerClient>;
|
|
35
44
|
/**
|
|
36
45
|
* Invoke one model-callable Pager MCP tool.
|
|
37
46
|
* @param name - Public MCP tool name.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ATTACH_HEADER, isAgentId } from "../../broker/protocol.js";
|
|
2
|
-
import { adoptRotation, asRecord, BrokerDeliveryClient, exactRecord, registerAgent, rpcEnvelope, } from "../../messaging/delivery-client.js";
|
|
2
|
+
import { adoptRotation, asRecord, BrokerDeliveryClient, exactRecord, isExpiredRefusal, registerAgent, rpcEnvelope, } from "../../messaging/delivery-client.js";
|
|
3
3
|
import { PAGER_VERSION } from "../../version.js";
|
|
4
4
|
/**
|
|
5
5
|
* Register a directly started Pi session through the unauthenticated broker boundary.
|
|
@@ -10,23 +10,37 @@ export async function registerPiAgent({ request, brokerUrl, workspacePath, sessi
|
|
|
10
10
|
return registerAgent(request, brokerUrl, {
|
|
11
11
|
workspacePath,
|
|
12
12
|
implementationId: "pi",
|
|
13
|
-
sessionName,
|
|
13
|
+
...(sessionName ? { sessionName } : {}),
|
|
14
14
|
...(sessionKey ? { sessionKey } : {}),
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Authenticated messaging and delivery operations used by the Pi extension.
|
|
19
|
+
*
|
|
20
|
+
* When the broker refuses the attachments this client holds as `expired` and a rejoin was supplied, a
|
|
21
|
+
* call registers the conversation again, attaches both channels with the reservations that return,
|
|
22
|
+
* and is retried once; calls refused together share one rejoin. Any other refusal reaches the caller
|
|
23
|
+
* unchanged.
|
|
24
|
+
*/
|
|
18
25
|
export class PiBrokerClient {
|
|
19
26
|
#brokerUrl;
|
|
20
|
-
#agentId;
|
|
21
27
|
#request;
|
|
28
|
+
#sessionKey;
|
|
29
|
+
#rejoin;
|
|
30
|
+
#agentId;
|
|
22
31
|
#messaging;
|
|
23
32
|
#delivery;
|
|
24
|
-
|
|
33
|
+
#deliveryAddress;
|
|
34
|
+
#rejoining;
|
|
35
|
+
#attachments = 0;
|
|
36
|
+
constructor(brokerUrl, agentId, request, messaging, delivery, sessionKey, rejoin) {
|
|
25
37
|
this.#brokerUrl = brokerUrl;
|
|
26
38
|
this.#agentId = agentId;
|
|
27
39
|
this.#request = request;
|
|
28
40
|
this.#messaging = messaging;
|
|
29
41
|
this.#delivery = delivery;
|
|
42
|
+
this.#sessionKey = sessionKey;
|
|
43
|
+
this.#rejoin = rejoin;
|
|
30
44
|
}
|
|
31
45
|
/**
|
|
32
46
|
* Consume a Pi identity's messaging and delivery reservations.
|
|
@@ -34,9 +48,11 @@ export class PiBrokerClient {
|
|
|
34
48
|
* @param identity - Reserved or directly registered broker identity.
|
|
35
49
|
* @param request - HTTP operation used to reach the loopback broker.
|
|
36
50
|
* @param sessionKey - Resumable conversation key, which the broker may resolve to another identity.
|
|
51
|
+
* @param rejoin - Registers this conversation again by its key and returns the attachments for the
|
|
52
|
+
* identity that results; absent when nothing identifies the conversation to register.
|
|
37
53
|
* @returns - Connected client whose identity is the one the broker resolved, not the one requested.
|
|
38
54
|
*/
|
|
39
|
-
static async connect(brokerUrl, identity, request, sessionKey) {
|
|
55
|
+
static async connect(brokerUrl, identity, request, sessionKey, rejoin) {
|
|
40
56
|
const messaging = await initializeMessaging(request, brokerUrl, identity.messagingReservation, sessionKey);
|
|
41
57
|
const delivery = await BrokerDeliveryClient.attach({
|
|
42
58
|
brokerUrl,
|
|
@@ -45,7 +61,7 @@ export class PiBrokerClient {
|
|
|
45
61
|
request,
|
|
46
62
|
sessionKey,
|
|
47
63
|
});
|
|
48
|
-
return new PiBrokerClient(brokerUrl, messaging.agentId ?? delivery.agentId(), request, messaging, delivery);
|
|
64
|
+
return new PiBrokerClient(brokerUrl, messaging.agentId ?? delivery.agentId(), request, messaging, delivery, sessionKey, rejoin);
|
|
49
65
|
}
|
|
50
66
|
/**
|
|
51
67
|
* Invoke one model-callable Pager MCP tool.
|
|
@@ -53,7 +69,10 @@ export class PiBrokerClient {
|
|
|
53
69
|
* @param args - Validated Pi tool arguments.
|
|
54
70
|
* @returns - Decoded broker tool result.
|
|
55
71
|
*/
|
|
56
|
-
|
|
72
|
+
callTool(name, args) {
|
|
73
|
+
return this.#recovering(() => this.#callMessagingTool(name, args));
|
|
74
|
+
}
|
|
75
|
+
async #callMessagingTool(name, args) {
|
|
57
76
|
const response = await this.#request(this.#brokerUrl, {
|
|
58
77
|
method: "POST",
|
|
59
78
|
headers: messagingHeaders(this.#messaging),
|
|
@@ -86,11 +105,54 @@ export class PiBrokerClient {
|
|
|
86
105
|
* @param address - Address text the session's inbox produced.
|
|
87
106
|
*/
|
|
88
107
|
presentDeliveryAddress(address) {
|
|
108
|
+
this.#deliveryAddress = address;
|
|
89
109
|
this.#delivery.presentDeliveryAddress(address);
|
|
90
110
|
}
|
|
91
111
|
/** Renew both independently fenced broker attachment leases. */
|
|
92
112
|
async heartbeat() {
|
|
93
|
-
await
|
|
113
|
+
await this.#recovering(async () => {
|
|
114
|
+
await Promise.all([this.#callMessagingTool("whoami", {}), this.#delivery.heartbeat()]);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async #recovering(call) {
|
|
118
|
+
await this.#rejoining?.catch(() => undefined);
|
|
119
|
+
const presentedAttachments = this.#attachments;
|
|
120
|
+
try {
|
|
121
|
+
return await call();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const rejoin = this.#rejoin;
|
|
125
|
+
if (!rejoin || !isExpiredRefusal(error))
|
|
126
|
+
throw error;
|
|
127
|
+
if (this.#rejoining)
|
|
128
|
+
await this.#rejoining;
|
|
129
|
+
else if (this.#attachments === presentedAttachments)
|
|
130
|
+
await this.#startRejoin(rejoin);
|
|
131
|
+
return call();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
#startRejoin(rejoin) {
|
|
135
|
+
this.#rejoining = this.#registerAgain(rejoin).finally(() => {
|
|
136
|
+
this.#rejoining = undefined;
|
|
137
|
+
});
|
|
138
|
+
return this.#rejoining;
|
|
139
|
+
}
|
|
140
|
+
async #registerAgain(rejoin) {
|
|
141
|
+
const identity = await rejoin();
|
|
142
|
+
const messaging = await initializeMessaging(this.#request, this.#brokerUrl, identity.messagingReservation, this.#sessionKey);
|
|
143
|
+
const delivery = await BrokerDeliveryClient.attach({
|
|
144
|
+
brokerUrl: this.#brokerUrl,
|
|
145
|
+
agentId: identity.agentId,
|
|
146
|
+
reservation: identity.deliveryReservation,
|
|
147
|
+
request: this.#request,
|
|
148
|
+
sessionKey: this.#sessionKey,
|
|
149
|
+
});
|
|
150
|
+
if (this.#deliveryAddress)
|
|
151
|
+
delivery.presentDeliveryAddress(this.#deliveryAddress);
|
|
152
|
+
this.#messaging = messaging;
|
|
153
|
+
this.#delivery = delivery;
|
|
154
|
+
this.#agentId = messaging.agentId ?? delivery.agentId();
|
|
155
|
+
this.#attachments += 1;
|
|
94
156
|
}
|
|
95
157
|
/** Tell the broker Pi is shutting down, ending both attachments so the session name is free at once. */
|
|
96
158
|
async detach() {
|
|
@@ -34,8 +34,13 @@ export interface PiExtensionScheduler {
|
|
|
34
34
|
export type PiExtensionDependencies = {
|
|
35
35
|
/** Reads service configuration without opening the broker database. */
|
|
36
36
|
readEnvironment(): RuntimeEnvironment;
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Starts or adopts the shared broker service.
|
|
39
|
+
* @param environment - Resolved configuration naming the database the service owns.
|
|
40
|
+
* @param report - Receives what a service this call started wrote while starting.
|
|
41
|
+
* @returns - The live service's endpoints.
|
|
42
|
+
*/
|
|
43
|
+
ensureService(environment: RuntimeEnvironment, report: (message: string) => void): Promise<BrokerEndpoints>;
|
|
39
44
|
/** Supplies launch identity and runtime selection values. */
|
|
40
45
|
processEnv: NodeJS.ProcessEnv;
|
|
41
46
|
/** Performs one broker HTTP request. */
|
|
@@ -87,7 +87,14 @@ export function createPiExtension(dependencies) {
|
|
|
87
87
|
// A direct-mode registration displaced whatever the identity held, so its fresh reservations
|
|
88
88
|
// are the only working ones.
|
|
89
89
|
if (!runtime.client || runtime.connectionKey !== connection.key || connection.directCwd !== undefined) {
|
|
90
|
-
runtime.client = await PiBrokerClient.connect(connection.brokerUrl, connection.identity, dependencies.fetch, connection.sessionKey
|
|
90
|
+
runtime.client = await PiBrokerClient.connect(connection.brokerUrl, connection.identity, dependencies.fetch, connection.sessionKey, connection.sessionKey
|
|
91
|
+
? () => registerPiAgent({
|
|
92
|
+
request: dependencies.fetch,
|
|
93
|
+
brokerUrl: connection.brokerUrl,
|
|
94
|
+
workspacePath: ctx.cwd,
|
|
95
|
+
sessionKey: connection.sessionKey,
|
|
96
|
+
})
|
|
97
|
+
: undefined);
|
|
91
98
|
runtime.connectionKey = connection.key;
|
|
92
99
|
runtime.directCwd = connection.directCwd;
|
|
93
100
|
}
|
|
@@ -175,7 +182,12 @@ async function resolveConnection(dependencies, ctx) {
|
|
|
175
182
|
directCwd: undefined,
|
|
176
183
|
};
|
|
177
184
|
}
|
|
178
|
-
const endpoints = await dependencies.ensureService(dependencies.readEnvironment())
|
|
185
|
+
const endpoints = await dependencies.ensureService(dependencies.readEnvironment(), (message) => {
|
|
186
|
+
if (ctx.hasUI)
|
|
187
|
+
ctx.ui.notify(message.trim(), "info");
|
|
188
|
+
else
|
|
189
|
+
process.stderr.write(message.endsWith("\n") ? message : `${message}\n`);
|
|
190
|
+
});
|
|
179
191
|
const identity = await registerPiAgent({
|
|
180
192
|
request: dependencies.fetch,
|
|
181
193
|
brokerUrl: endpoints.mcpUrl,
|
|
@@ -236,7 +248,7 @@ function hasLaunchIdentity(environment) {
|
|
|
236
248
|
}
|
|
237
249
|
const defaultDependencies = {
|
|
238
250
|
readEnvironment: () => readEmbeddedRuntimeEnvironment(process.env, resolveBunExecutable),
|
|
239
|
-
ensureService: (environment) => ensureBrokerService(environment),
|
|
251
|
+
ensureService: (environment, report) => ensureBrokerService(environment, { report }),
|
|
240
252
|
processEnv: process.env,
|
|
241
253
|
fetch: (url, init) => globalThis.fetch(url, init),
|
|
242
254
|
runtimeSlot: systemPiRuntimeSlot(),
|
package/dist/src/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { createImplementationRegistry } from "./launch/runtime-composition.js";
|
|
|
10
10
|
export { PAGER_DEFAULTS, ConfigurationError, initializePagerStore, loadPagerConfiguration, resolvePagerStoreDirectory, resolveConfigurationSetting, type PagerConfig, type ConfigurationLoadOptions, type ConfigurationSettingPrecedence, type LoadedConfiguration, } from "./configuration.js";
|
|
11
11
|
export { DirectTerminalProvider, ForegroundTerminalProvider, LaunchService, renderLaunchResult, type AttachState, type ForegroundProcessSpawner, type LaunchDiagnostic, type LaunchRequest, type LaunchResult, type ProcessSpawner, type ProcessState, type TerminalLaunch, type TerminalProvider, type TerminalProviderName, type TerminalSession, type TerminalTaskCompletion, } from "./launch/launch.js";
|
|
12
12
|
export { BunRuntimeError, assertSupportedBunRuntime, probeBunVersion, resolveBunExecutable, satisfiesBunVersionFloor, BUN_PATH_ENVIRONMENT, MINIMUM_BUN_VERSION, RETIRED_NODE_PATH_ENVIRONMENT, type RuntimeProbe, } from "./launch/runtime.js";
|
|
13
|
-
export { BrokerService, EndpointPortUnavailableError, systemWorkspaceBranch, type BrokerServiceOptions, type BrokerServiceStatus, } from "./launch/service.js";
|
|
13
|
+
export { BrokerService, EndpointPortUnavailableError, systemWorkspaceBranch, systemWorkspaceRoot, type BrokerServiceOptions, type BrokerServiceStatus, } from "./launch/service.js";
|
|
14
14
|
export { systemBrokerServiceFilesystem, systemBrokerServiceScheduler } from "./launch/service-system.js";
|
|
15
15
|
export { BrokerLockGuard, brokerLockPath } from "./launch/broker-lock.js";
|
|
16
16
|
export { readRuntimeEnvironment, PAGER_AGENT_ID_ENVIRONMENT, PAGER_ATTACH_RESERVATION_ENVIRONMENT, PAGER_BROKER_URL_ENVIRONMENT, PAGER_MCP_URL_ENVIRONMENT, type RuntimeEnvironment, } from "./launch/environment.js";
|
|
@@ -22,6 +22,6 @@ export { PushDeliveryService, type DeliveryAddressRegistration, type PushDeliver
|
|
|
22
22
|
export { MCP_TOOLS, type McpToolDefinition } from "./messaging/tools.js";
|
|
23
23
|
export { PAGER_WAKE_TYPE, wakeLine, wakeMetadata } from "./messaging/wake.js";
|
|
24
24
|
export { classifyOperatorEvent, validateOperatorAgent, validateOperatorCommand, validateOperatorCommandResult, validateOperatorCommandResultForCommand, validateOperatorControlId, validateOperatorCursor, validateOperatorDiagnostic, validateOperatorEventEnvelope, validateOperatorMessageBody, validateOperatorRepository, validateOperatorSnapshot, validateOperatorSnapshotByteLength, validateOperatorTraffic, validateOperatorTransportRequest, OperatorValidationError, OPERATOR_ARRAY_LENGTH_LIMIT, } from "./operator/validation.js";
|
|
25
|
-
export { OPERATOR_DIAGNOSTIC_TEXT_LIMIT, OPERATOR_PROTOCOL_VERSION, OPERATOR_REJECTION_REASONS, OPERATOR_SNAPSHOT_CHUNK_BYTES, OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES, OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES, type OperatorAcceptedDelivery, type OperatorAdapterState, type OperatorAgent, type OperatorCommand, type OperatorCommandEnvelope, type OperatorCommandEnvelopeFor, type OperatorCommandResult, type OperatorCorrelatedCommandResult, type OperatorCursor, type OperatorCursorResult, type OperatorDeliveryLifecycleEntry, type OperatorDiagnostic, type OperatorDiagnosticSeverity, type OperatorEventEnvelope, type OperatorImplementation, type OperatorImplementationVersion, type OperatorJsonObject, type OperatorJsonValue, type OperatorMessageBody, type OperatorPingAudit, type OperatorRejectionReason, type OperatorRepository, type OperatorResyncReason, type OperatorSnapshot, type OperatorStateEvent, type OperatorTraffic, type OperatorTransportFrame, type OperatorTransportRequest, type OperatorWorktree, } from "./operator/protocol.js";
|
|
25
|
+
export { OPERATOR_DIAGNOSTIC_TEXT_LIMIT, OPERATOR_PROTOCOL_VERSION, OPERATOR_PURGE_TARGET_LIMIT, OPERATOR_REJECTION_REASONS, OPERATOR_SNAPSHOT_CHUNK_BYTES, OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES, OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES, OPERATOR_TRANSPORT_REQUEST_LIMIT_BYTES, type OperatorAcceptedDelivery, type OperatorAdapterState, type OperatorAgent, type OperatorCommand, type OperatorCommandEnvelope, type OperatorCommandEnvelopeFor, type OperatorCommandResult, type OperatorCorrelatedCommandResult, type OperatorCursor, type OperatorCursorResult, type OperatorDeliveryLifecycleEntry, type OperatorDiagnostic, type OperatorDiagnosticSeverity, type OperatorEventEnvelope, type OperatorImplementation, type OperatorImplementationVersion, type OperatorJsonObject, type OperatorJsonValue, type OperatorMessageBody, type OperatorPingAudit, type OperatorRejectionReason, type OperatorRepository, type OperatorResyncReason, type OperatorSnapshot, type OperatorStateEvent, type OperatorTraffic, type OperatorTransportFrame, type OperatorTransportRequest, type OperatorWorktree, } from "./operator/protocol.js";
|
|
26
26
|
export { PAGER_VERSION } from "./version.js";
|
|
27
27
|
export { shellQuote } from "./shared/shell.js";
|
package/dist/src/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { createImplementationRegistry } from "./launch/runtime-composition.js";
|
|
|
9
9
|
export { PAGER_DEFAULTS, ConfigurationError, initializePagerStore, loadPagerConfiguration, resolvePagerStoreDirectory, resolveConfigurationSetting, } from "./configuration.js";
|
|
10
10
|
export { DirectTerminalProvider, ForegroundTerminalProvider, LaunchService, renderLaunchResult, } from "./launch/launch.js";
|
|
11
11
|
export { BunRuntimeError, assertSupportedBunRuntime, probeBunVersion, resolveBunExecutable, satisfiesBunVersionFloor, BUN_PATH_ENVIRONMENT, MINIMUM_BUN_VERSION, RETIRED_NODE_PATH_ENVIRONMENT, } from "./launch/runtime.js";
|
|
12
|
-
export { BrokerService, EndpointPortUnavailableError, systemWorkspaceBranch, } from "./launch/service.js";
|
|
12
|
+
export { BrokerService, EndpointPortUnavailableError, systemWorkspaceBranch, systemWorkspaceRoot, } from "./launch/service.js";
|
|
13
13
|
export { systemBrokerServiceFilesystem, systemBrokerServiceScheduler } from "./launch/service-system.js";
|
|
14
14
|
export { BrokerLockGuard, brokerLockPath } from "./launch/broker-lock.js";
|
|
15
15
|
export { readRuntimeEnvironment, PAGER_AGENT_ID_ENVIRONMENT, PAGER_ATTACH_RESERVATION_ENVIRONMENT, PAGER_BROKER_URL_ENVIRONMENT, PAGER_MCP_URL_ENVIRONMENT, } from "./launch/environment.js";
|
|
@@ -21,6 +21,6 @@ export { PushDeliveryService, } from "./messaging/push-delivery.js";
|
|
|
21
21
|
export { MCP_TOOLS } from "./messaging/tools.js";
|
|
22
22
|
export { PAGER_WAKE_TYPE, wakeLine, wakeMetadata } from "./messaging/wake.js";
|
|
23
23
|
export { classifyOperatorEvent, validateOperatorAgent, validateOperatorCommand, validateOperatorCommandResult, validateOperatorCommandResultForCommand, validateOperatorControlId, validateOperatorCursor, validateOperatorDiagnostic, validateOperatorEventEnvelope, validateOperatorMessageBody, validateOperatorRepository, validateOperatorSnapshot, validateOperatorSnapshotByteLength, validateOperatorTraffic, validateOperatorTransportRequest, OperatorValidationError, OPERATOR_ARRAY_LENGTH_LIMIT, } from "./operator/validation.js";
|
|
24
|
-
export { OPERATOR_DIAGNOSTIC_TEXT_LIMIT, OPERATOR_PROTOCOL_VERSION, OPERATOR_REJECTION_REASONS, OPERATOR_SNAPSHOT_CHUNK_BYTES, OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES, OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES, } from "./operator/protocol.js";
|
|
24
|
+
export { OPERATOR_DIAGNOSTIC_TEXT_LIMIT, OPERATOR_PROTOCOL_VERSION, OPERATOR_PURGE_TARGET_LIMIT, OPERATOR_REJECTION_REASONS, OPERATOR_SNAPSHOT_CHUNK_BYTES, OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES, OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES, OPERATOR_TRANSPORT_REQUEST_LIMIT_BYTES, } from "./operator/protocol.js";
|
|
25
25
|
export { PAGER_VERSION } from "./version.js";
|
|
26
26
|
export { shellQuote } from "./shared/shell.js";
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { ProtocolError, type BrokerServiceIdentity } from "../broker/protocol.js";
|
|
2
2
|
/** Identifies the ownership schema understood by this release. */
|
|
3
3
|
export declare const SERVICE_PROTOCOL_VERSION = 1;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Identifies the package build that owns production service state.
|
|
6
|
+
*
|
|
7
|
+
* A source checkout carries a `+checkout` suffix, so its service and a published release of the same
|
|
8
|
+
* version never adopt each other, and neither is taken for an older release the other may replace.
|
|
9
|
+
*/
|
|
5
10
|
export declare const SERVICE_BUILD_ID: string;
|
|
6
11
|
/** Identifies the production runtime required to own SQLite. */
|
|
7
12
|
export declare const SERVICE_RUNTIME = "bun";
|
|
@@ -83,6 +88,29 @@ export declare function brokerLockPath(databasePath: string): string;
|
|
|
83
88
|
* @returns - Valid parsed lock data when the file is well-formed.
|
|
84
89
|
*/
|
|
85
90
|
export declare function readBrokerLock(lockPath: string): BrokerLock | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Report whether one service build id names an older release of this package than another.
|
|
93
|
+
*
|
|
94
|
+
* Only a build id naming an exact `major.minor.patch` version compares, so a prerelease or a source
|
|
95
|
+
* checkout on either side is never called older.
|
|
96
|
+
* @param buildId - Build id read from a lock record, possibly written by a foreign build.
|
|
97
|
+
* @param currentBuildId - Build id of the caller.
|
|
98
|
+
* @returns - True only when both name releases of this package and `buildId`'s is lower.
|
|
99
|
+
*/
|
|
100
|
+
export declare function isOlderServiceBuild(buildId: string, currentBuildId: string): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Report whether a live lock owner is one a starting service of this build may stop and replace.
|
|
103
|
+
*
|
|
104
|
+
* The lock must carry this build's lock format, runtime and wire protocol, as {@link readBrokerLock}
|
|
105
|
+
* requires, and an older release of this package must have written it, whatever its database schema.
|
|
106
|
+
* The platform must confirm that the process holding the lock's pid started when the lock records, so
|
|
107
|
+
* a reused pid, or one whose start time cannot be read, is never signalled.
|
|
108
|
+
* @param lock - Lock record, possibly written by a foreign build.
|
|
109
|
+
* @param ownerStartedAt - Start time the platform reports now for the lock's pid.
|
|
110
|
+
* @param currentBuildId - Build id of the service that would replace the owner.
|
|
111
|
+
* @returns - True only when every condition holds.
|
|
112
|
+
*/
|
|
113
|
+
export declare function brokerLockIsReplaceable(lock: BrokerLock | undefined, ownerStartedAt: ProcessStartTime, currentBuildId?: string): boolean;
|
|
86
114
|
/**
|
|
87
115
|
* Name the package version of the build that wrote a lock record.
|
|
88
116
|
* @param lock - Lock record, possibly written by a foreign build.
|
|
@@ -91,6 +119,13 @@ export declare function readBrokerLock(lockPath: string): BrokerLock | undefined
|
|
|
91
119
|
export declare function brokerLockVersion(lock: Pick<BrokerLock, "buildId">): string;
|
|
92
120
|
/** Bounds how many times acquisition re-attempts after losing a lock-file race. */
|
|
93
121
|
export declare const BROKER_LOCK_ACQUISITION_ATTEMPTS = 5;
|
|
122
|
+
/**
|
|
123
|
+
* Bounds waiting for a signalled owner to exit. It must exceed `SHUTDOWN_GRACE_MS` in `cli.ts`, the
|
|
124
|
+
* time a stopping service allows its endpoints to close before it exits on its own.
|
|
125
|
+
*/
|
|
126
|
+
export declare const BROKER_EXIT_TIMEOUT_MS = 10000;
|
|
127
|
+
/** Spaces the liveness polls made while a signalled owner shuts down. */
|
|
128
|
+
export declare const BROKER_EXIT_POLL_MS = 50;
|
|
94
129
|
/** Reports the start time of a process, or that this platform cannot supply one. */
|
|
95
130
|
export type ProcessStartTime = {
|
|
96
131
|
known: true;
|
|
@@ -108,6 +143,13 @@ export type BrokerLockPorts = {
|
|
|
108
143
|
newHolderToken(): string;
|
|
109
144
|
/** Blocks the calling thread, because lock acquisition runs before the service has an event loop. */
|
|
110
145
|
sleep(milliseconds: number): void;
|
|
146
|
+
/** Delivers a signal to an older-release owner being replaced. */
|
|
147
|
+
signal(pid: number, signal: NodeJS.Signals): void;
|
|
148
|
+
/**
|
|
149
|
+
* Tells the operator about an owner acquisition stopped on their behalf.
|
|
150
|
+
* @param message - One line naming the stopped process and the versions involved.
|
|
151
|
+
*/
|
|
152
|
+
report(message: string): void;
|
|
111
153
|
};
|
|
112
154
|
/** Reports that the lock is held by a live service of the current build. */
|
|
113
155
|
export declare class BrokerServiceAlreadyRunningError extends ProtocolError {
|
|
@@ -133,6 +175,14 @@ export declare class BrokerServiceLockOwnedError extends ProtocolError {
|
|
|
133
175
|
/** Identifies the live process that owns the lock. */
|
|
134
176
|
pid: number);
|
|
135
177
|
}
|
|
178
|
+
/** Named failure raised when a signalled broker service is still running once the exit budget is spent. */
|
|
179
|
+
export declare class BrokerServiceExitTimeoutError extends ProtocolError {
|
|
180
|
+
/**
|
|
181
|
+
* Construct the failure reported when the owner ignores or outlasts its termination signal.
|
|
182
|
+
* @param pid - Process id of the owner that was signalled.
|
|
183
|
+
*/
|
|
184
|
+
constructor(pid: number);
|
|
185
|
+
}
|
|
136
186
|
/** Reports that acquisition kept losing the lock file to another writer. */
|
|
137
187
|
export declare class BrokerLockContendedError extends ProtocolError {
|
|
138
188
|
/** Names the contended lock file. */
|
|
@@ -227,20 +277,24 @@ export declare class BrokerLockGuard implements BrokerLockOwner {
|
|
|
227
277
|
* Construct a guard over one lock file.
|
|
228
278
|
* @param lockPath - Lock file adjacent to the database this process wants to own.
|
|
229
279
|
* @param ports - Operating-system boundaries, replaced only by this module's own tests.
|
|
280
|
+
* @param buildId - Build id deciding which older-release owners this guard may replace.
|
|
230
281
|
*/
|
|
231
|
-
constructor(lockPath: string, ports?: BrokerLockPorts);
|
|
282
|
+
constructor(lockPath: string, ports?: BrokerLockPorts, buildId?: string);
|
|
232
283
|
/**
|
|
233
284
|
* Names the lock file this guard claims.
|
|
234
285
|
* @returns - Path the guard was constructed over.
|
|
235
286
|
*/
|
|
236
287
|
get path(): string;
|
|
237
288
|
/**
|
|
238
|
-
* Take exclusive ownership of the database, reclaiming a lock whose owner is gone
|
|
289
|
+
* Take exclusive ownership of the database, reclaiming a lock whose owner is gone and stopping
|
|
290
|
+
* one {@link brokerLockIsReplaceable} accepts.
|
|
239
291
|
*
|
|
240
292
|
* Terminates in every case: each attempt either wins the lock, throws because a live owner holds
|
|
241
293
|
* it, or consumes one of a fixed number of attempts.
|
|
242
294
|
* @throws {BrokerServiceAlreadyRunningError} - A live service of this build holds the lock.
|
|
243
|
-
* @throws {BrokerServiceLockOwnedError} - A live service this build must not displace holds the
|
|
295
|
+
* @throws {BrokerServiceLockOwnedError} - A live service this build must not displace holds the
|
|
296
|
+
* lock, or a replaceable one refused the signal.
|
|
297
|
+
* @throws {BrokerServiceExitTimeoutError} - A replaceable owner outlasted its termination signal.
|
|
244
298
|
* @throws {BrokerLockContendedError} - Another writer kept recreating the lock.
|
|
245
299
|
*/
|
|
246
300
|
acquire(): void;
|