@masons/agent-network 0.5.31 → 0.5.32
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/dist/broker/broker-daemon.d.ts +20 -0
- package/dist/broker/broker-daemon.d.ts.map +1 -1
- package/dist/broker/broker-daemon.js +243 -2
- package/dist/broker/claude-code-spawn-driver.d.ts +14 -0
- package/dist/broker/claude-code-spawn-driver.d.ts.map +1 -0
- package/dist/broker/claude-code-spawn-driver.js +39 -0
- package/dist/broker/codex-spawn-driver-stub.d.ts +7 -0
- package/dist/broker/codex-spawn-driver-stub.d.ts.map +1 -0
- package/dist/broker/codex-spawn-driver-stub.js +13 -0
- package/dist/broker/control-event-dispatcher.d.ts +1 -0
- package/dist/broker/control-event-dispatcher.d.ts.map +1 -1
- package/dist/broker/control-event-types.d.ts +11 -13
- package/dist/broker/control-event-types.d.ts.map +1 -1
- package/dist/broker/entry.d.ts.map +1 -1
- package/dist/broker/entry.js +41 -0
- package/dist/broker/ipc-server.d.ts +7 -0
- package/dist/broker/ipc-server.d.ts.map +1 -1
- package/dist/broker/ipc-server.js +20 -0
- package/dist/broker/network-presence.d.ts +31 -0
- package/dist/broker/network-presence.d.ts.map +1 -0
- package/dist/broker/network-presence.js +109 -0
- package/dist/broker/services-event-client.d.ts +21 -0
- package/dist/broker/services-event-client.d.ts.map +1 -0
- package/dist/broker/services-event-client.js +221 -0
- package/dist/broker/spawn-correlation.d.ts +28 -0
- package/dist/broker/spawn-correlation.d.ts.map +1 -0
- package/dist/broker/spawn-correlation.js +77 -0
- package/dist/broker/spawn-driver.d.ts +27 -0
- package/dist/broker/spawn-driver.d.ts.map +1 -0
- package/dist/broker/spawn-driver.js +15 -0
- package/dist/broker/task-hint-handler.d.ts +21 -0
- package/dist/broker/task-hint-handler.d.ts.map +1 -0
- package/dist/broker/task-hint-handler.js +33 -0
- package/dist/broker/transition-state-retry-queue.d.ts +20 -0
- package/dist/broker/transition-state-retry-queue.d.ts.map +1 -0
- package/dist/broker/transition-state-retry-queue.js +48 -0
- package/dist/broker-client/broker-client.d.ts +1 -0
- package/dist/broker-client/broker-client.d.ts.map +1 -1
- package/dist/broker-client/broker-client.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
|
@@ -137,6 +137,26 @@ async function routeHttp(req, res, bearerToken, handlers, logger) {
|
|
|
137
137
|
sendJson(res, 200, { ok: true });
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
+
const taskHintMatch = /^\/v1\/endpoint\/([^/]+)\/task-hint$/.exec(url);
|
|
141
|
+
if (method === "POST" && taskHintMatch && taskHintMatch[1]) {
|
|
142
|
+
const endpointId = decodeURIComponent(taskHintMatch[1]);
|
|
143
|
+
const body = await readJson(req);
|
|
144
|
+
const pid = typeof body.plugin_pid === "number" ? body.plugin_pid : Number.NaN;
|
|
145
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
146
|
+
throw new BrokerHttpError(400, "plugin_pid_invalid", "plugin_pid is required and must be a positive number");
|
|
147
|
+
}
|
|
148
|
+
const hint = typeof body.task_hint === "string" ? body.task_hint : undefined;
|
|
149
|
+
if (hint === undefined) {
|
|
150
|
+
throw new BrokerHttpError(400, "task_hint_invalid", "task_hint is required and must be a string");
|
|
151
|
+
}
|
|
152
|
+
await handlers.setTaskHint({
|
|
153
|
+
endpoint_id: endpointId,
|
|
154
|
+
plugin_pid: pid,
|
|
155
|
+
task_hint: hint,
|
|
156
|
+
});
|
|
157
|
+
sendJson(res, 200, { ok: true });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
140
160
|
sendJson(res, 404, { error: "not_found", path: url });
|
|
141
161
|
}
|
|
142
162
|
catch (err) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const DEFAULT_PRESENCE_GRACE_MS: number;
|
|
2
|
+
export type NetworkPresence = "online" | "reconnecting" | "offline";
|
|
3
|
+
export type PresenceEvent = {
|
|
4
|
+
type: "connector_connected";
|
|
5
|
+
} | {
|
|
6
|
+
type: "connector_disconnected";
|
|
7
|
+
} | {
|
|
8
|
+
type: "presence_grace_expired";
|
|
9
|
+
} | {
|
|
10
|
+
type: "shutdown";
|
|
11
|
+
};
|
|
12
|
+
export type PresenceEffect = {
|
|
13
|
+
type: "start_grace_timer";
|
|
14
|
+
deadline_ms: number;
|
|
15
|
+
} | {
|
|
16
|
+
type: "cancel_grace_timer";
|
|
17
|
+
} | {
|
|
18
|
+
type: "emit_presence";
|
|
19
|
+
presence: NetworkPresence;
|
|
20
|
+
reason: string;
|
|
21
|
+
};
|
|
22
|
+
export interface PresenceTransitionResult {
|
|
23
|
+
next: NetworkPresence;
|
|
24
|
+
effects: readonly PresenceEffect[];
|
|
25
|
+
ignored?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface PresenceTransitionOptions {
|
|
28
|
+
graceMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function transitionPresence(current: NetworkPresence, event: PresenceEvent, opts?: PresenceTransitionOptions): PresenceTransitionResult;
|
|
31
|
+
//# sourceMappingURL=network-presence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-presence.d.ts","sourceRoot":"","sources":["../../src/broker/network-presence.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,yBAAyB,QAAiB,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,cAAc,GAAG,SAAS,CAAC;AAEpE,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,qBAAqB,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,wBAAwB,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,wBAAwB,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAEzB,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,oBAAoB,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAOD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,aAAa,EACpB,IAAI,GAAE,yBAA8B,GACnC,wBAAwB,CAgH1B"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export const DEFAULT_PRESENCE_GRACE_MS = 10 * 60 * 1000;
|
|
2
|
+
export function transitionPresence(current, event, opts = {}) {
|
|
3
|
+
const graceMs = opts.graceMs ?? DEFAULT_PRESENCE_GRACE_MS;
|
|
4
|
+
switch (current) {
|
|
5
|
+
case "offline": {
|
|
6
|
+
switch (event.type) {
|
|
7
|
+
case "connector_connected":
|
|
8
|
+
return {
|
|
9
|
+
next: "online",
|
|
10
|
+
effects: [
|
|
11
|
+
{
|
|
12
|
+
type: "emit_presence",
|
|
13
|
+
presence: "online",
|
|
14
|
+
reason: "connector_connected",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
case "shutdown":
|
|
19
|
+
return {
|
|
20
|
+
next: "offline",
|
|
21
|
+
effects: [
|
|
22
|
+
{
|
|
23
|
+
type: "emit_presence",
|
|
24
|
+
presence: "offline",
|
|
25
|
+
reason: "broker_shutdown",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
case "connector_disconnected":
|
|
30
|
+
case "presence_grace_expired":
|
|
31
|
+
return { next: "offline", effects: [], ignored: true };
|
|
32
|
+
}
|
|
33
|
+
return { next: current, effects: [], ignored: true };
|
|
34
|
+
}
|
|
35
|
+
case "online": {
|
|
36
|
+
switch (event.type) {
|
|
37
|
+
case "connector_disconnected":
|
|
38
|
+
return {
|
|
39
|
+
next: "reconnecting",
|
|
40
|
+
effects: [
|
|
41
|
+
{ type: "start_grace_timer", deadline_ms: graceMs },
|
|
42
|
+
{
|
|
43
|
+
type: "emit_presence",
|
|
44
|
+
presence: "reconnecting",
|
|
45
|
+
reason: "connector_disconnected",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
case "shutdown":
|
|
50
|
+
return {
|
|
51
|
+
next: "offline",
|
|
52
|
+
effects: [
|
|
53
|
+
{
|
|
54
|
+
type: "emit_presence",
|
|
55
|
+
presence: "offline",
|
|
56
|
+
reason: "broker_shutdown",
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
case "connector_connected":
|
|
61
|
+
case "presence_grace_expired":
|
|
62
|
+
return { next: "online", effects: [], ignored: true };
|
|
63
|
+
}
|
|
64
|
+
return { next: current, effects: [], ignored: true };
|
|
65
|
+
}
|
|
66
|
+
case "reconnecting": {
|
|
67
|
+
switch (event.type) {
|
|
68
|
+
case "connector_connected":
|
|
69
|
+
return {
|
|
70
|
+
next: "online",
|
|
71
|
+
effects: [
|
|
72
|
+
{ type: "cancel_grace_timer" },
|
|
73
|
+
{
|
|
74
|
+
type: "emit_presence",
|
|
75
|
+
presence: "online",
|
|
76
|
+
reason: "connector_reconnected",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
case "presence_grace_expired":
|
|
81
|
+
return {
|
|
82
|
+
next: "offline",
|
|
83
|
+
effects: [
|
|
84
|
+
{
|
|
85
|
+
type: "emit_presence",
|
|
86
|
+
presence: "offline",
|
|
87
|
+
reason: "grace_timeout",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
case "shutdown":
|
|
92
|
+
return {
|
|
93
|
+
next: "offline",
|
|
94
|
+
effects: [
|
|
95
|
+
{ type: "cancel_grace_timer" },
|
|
96
|
+
{
|
|
97
|
+
type: "emit_presence",
|
|
98
|
+
presence: "offline",
|
|
99
|
+
reason: "broker_shutdown",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
case "connector_disconnected":
|
|
104
|
+
return { next: "reconnecting", effects: [], ignored: true };
|
|
105
|
+
}
|
|
106
|
+
return { next: current, effects: [], ignored: true };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ControlEventDispatcher } from "./control-event-dispatcher.js";
|
|
2
|
+
import type { BrokerLogger } from "./logger.js";
|
|
3
|
+
export declare const DEFAULT_BACKOFF_INITIAL_MS = 1000;
|
|
4
|
+
export declare const DEFAULT_BACKOFF_MAX_MS = 30000;
|
|
5
|
+
export interface ServicesEventClientOptions {
|
|
6
|
+
apiHost: string;
|
|
7
|
+
runtimeKey: string;
|
|
8
|
+
agentId: string;
|
|
9
|
+
dispatcher: ControlEventDispatcher;
|
|
10
|
+
logger: BrokerLogger;
|
|
11
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
12
|
+
backoffInitialMs?: number;
|
|
13
|
+
backoffMaxMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ServicesEventClient {
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
lastSeenKey(): string | undefined;
|
|
19
|
+
}
|
|
20
|
+
export declare function createServicesEventClient(opts: ServicesEventClientOptions): ServicesEventClient;
|
|
21
|
+
//# sourceMappingURL=services-event-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"services-event-client.d.ts","sourceRoot":"","sources":["../../src/broker/services-event-client.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EACV,sBAAsB,EAEvB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAChD,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C,MAAM,WAAW,0BAA0B;IAEzC,OAAO,EAAE,MAAM,CAAC;IAEhB,UAAU,EAAE,MAAM,CAAC;IAEnB,OAAO,EAAE,MAAM,CAAC;IAEhB,UAAU,EAAE,sBAAsB,CAAC;IAEnC,MAAM,EAAE,YAAY,CAAC;IAErB,SAAS,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAEpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IASlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtB,WAAW,IAAI,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,0BAA0B,GAC/B,mBAAmB,CAuMrB"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
export const DEFAULT_BACKOFF_INITIAL_MS = 1_000;
|
|
2
|
+
export const DEFAULT_BACKOFF_MAX_MS = 30_000;
|
|
3
|
+
export function createServicesEventClient(opts) {
|
|
4
|
+
const { dispatcher, logger } = opts;
|
|
5
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
6
|
+
const backoffInitial = opts.backoffInitialMs ?? DEFAULT_BACKOFF_INITIAL_MS;
|
|
7
|
+
const backoffMax = opts.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS;
|
|
8
|
+
let stopped = false;
|
|
9
|
+
let lastEventId;
|
|
10
|
+
let currentAbort = null;
|
|
11
|
+
let backoffMs = backoffInitial;
|
|
12
|
+
let loopPromise = null;
|
|
13
|
+
let started = false;
|
|
14
|
+
const baseUrl = makeBaseUrl(opts.apiHost);
|
|
15
|
+
const subscribeOnce = async () => {
|
|
16
|
+
const ctrl = new AbortController();
|
|
17
|
+
currentAbort = ctrl;
|
|
18
|
+
const url = new URL(`${baseUrl}/runtime/control-events`);
|
|
19
|
+
url.searchParams.set("agent_id", opts.agentId);
|
|
20
|
+
const headers = {
|
|
21
|
+
Authorization: `Bearer ${opts.runtimeKey}`,
|
|
22
|
+
Accept: "text/event-stream",
|
|
23
|
+
};
|
|
24
|
+
if (lastEventId)
|
|
25
|
+
headers["Last-Event-ID"] = lastEventId;
|
|
26
|
+
const res = await fetchImpl(url.toString(), {
|
|
27
|
+
method: "GET",
|
|
28
|
+
headers,
|
|
29
|
+
signal: ctrl.signal,
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok || !res.body) {
|
|
32
|
+
throw new Error(`SSE subscribe failed: ${res.status}`);
|
|
33
|
+
}
|
|
34
|
+
backoffMs = backoffInitial;
|
|
35
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
36
|
+
let buffer = "";
|
|
37
|
+
while (true) {
|
|
38
|
+
const { done, value } = await reader.read();
|
|
39
|
+
if (done)
|
|
40
|
+
break;
|
|
41
|
+
buffer += value.replace(/\r\n?/g, "\n");
|
|
42
|
+
let sep = buffer.indexOf("\n\n");
|
|
43
|
+
while (sep >= 0) {
|
|
44
|
+
const frame = buffer.slice(0, sep);
|
|
45
|
+
buffer = buffer.slice(sep + 2);
|
|
46
|
+
await handleFrame(frame).catch((err) => {
|
|
47
|
+
logger.warn("sse_frame_error", {
|
|
48
|
+
err: err instanceof Error ? err.message : String(err),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
sep = buffer.indexOf("\n\n");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const handleFrame = async (frame) => {
|
|
56
|
+
let id;
|
|
57
|
+
const dataLines = [];
|
|
58
|
+
for (const line of frame.split("\n")) {
|
|
59
|
+
if (line === "" || line.startsWith(":"))
|
|
60
|
+
continue;
|
|
61
|
+
const colon = line.indexOf(":");
|
|
62
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
63
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
64
|
+
if (value.startsWith(" "))
|
|
65
|
+
value = value.slice(1);
|
|
66
|
+
if (field === "id")
|
|
67
|
+
id = value;
|
|
68
|
+
else if (field === "data")
|
|
69
|
+
dataLines.push(value);
|
|
70
|
+
}
|
|
71
|
+
if (dataLines.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
const data = dataLines.join("\n");
|
|
74
|
+
if (id)
|
|
75
|
+
lastEventId = id;
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(data);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
logger.warn("sse_frame_unparseable", { data: data.slice(0, 80) });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (hasIdempotencyKey(parsed) && hasUnsupportedVersion(parsed)) {
|
|
85
|
+
await postAck({
|
|
86
|
+
idempotency_key: parsed.idempotency_key,
|
|
87
|
+
status: "failed",
|
|
88
|
+
detail: "unsupported_protocol_version",
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!isControlEvent(parsed)) {
|
|
93
|
+
if (hasIdempotencyKey(parsed)) {
|
|
94
|
+
await postAck({
|
|
95
|
+
idempotency_key: parsed.idempotency_key,
|
|
96
|
+
status: "applied",
|
|
97
|
+
detail: "unknown_variant_dropped",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const outcome = await dispatcher.dispatch(parsed);
|
|
103
|
+
await postAck(outcomeToAck(parsed.idempotency_key, outcome));
|
|
104
|
+
};
|
|
105
|
+
const postAck = async (ack) => {
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetchImpl(`${baseUrl}/runtime/control-ack`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
Authorization: `Bearer ${opts.runtimeKey}`,
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(ack),
|
|
114
|
+
});
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
logger.warn("ack_post_failed", {
|
|
117
|
+
status: res.status,
|
|
118
|
+
idempotency_key: ack.idempotency_key,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
logger.warn("ack_post_error", {
|
|
124
|
+
err: err instanceof Error ? err.message : String(err),
|
|
125
|
+
idempotency_key: ack.idempotency_key,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const loop = async () => {
|
|
130
|
+
while (!stopped) {
|
|
131
|
+
try {
|
|
132
|
+
await subscribeOnce();
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (stopped)
|
|
136
|
+
return;
|
|
137
|
+
logger.warn("sse_reconnect", {
|
|
138
|
+
backoff_ms: backoffMs,
|
|
139
|
+
err: err instanceof Error ? err.message : String(err),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (stopped)
|
|
143
|
+
return;
|
|
144
|
+
await sleep(backoffMs);
|
|
145
|
+
backoffMs = Math.min(backoffMs * 2, backoffMax);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
async start() {
|
|
150
|
+
if (started)
|
|
151
|
+
return;
|
|
152
|
+
started = true;
|
|
153
|
+
stopped = false;
|
|
154
|
+
loopPromise = loop();
|
|
155
|
+
},
|
|
156
|
+
async stop() {
|
|
157
|
+
stopped = true;
|
|
158
|
+
if (currentAbort) {
|
|
159
|
+
try {
|
|
160
|
+
currentAbort.abort();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (loopPromise) {
|
|
166
|
+
await loopPromise.catch(() => { });
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
lastSeenKey() {
|
|
170
|
+
return lastEventId;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function outcomeToAck(idempotency_key, outcome) {
|
|
175
|
+
if (outcome.ok) {
|
|
176
|
+
if (outcome.ack_hint === "received") {
|
|
177
|
+
return { idempotency_key, status: "received" };
|
|
178
|
+
}
|
|
179
|
+
return { idempotency_key, status: "applied" };
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
idempotency_key,
|
|
183
|
+
status: "failed",
|
|
184
|
+
detail: outcome.detail,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function isControlEvent(value) {
|
|
188
|
+
if (typeof value !== "object" || value === null)
|
|
189
|
+
return false;
|
|
190
|
+
const v = value;
|
|
191
|
+
if (v.version !== 1)
|
|
192
|
+
return false;
|
|
193
|
+
if (typeof v.idempotency_key !== "string")
|
|
194
|
+
return false;
|
|
195
|
+
if (typeof v.emitted_at !== "number")
|
|
196
|
+
return false;
|
|
197
|
+
if (typeof v.type !== "string")
|
|
198
|
+
return false;
|
|
199
|
+
return (v.type === "dispatch_undispatched" ||
|
|
200
|
+
v.type === "spawn_request" ||
|
|
201
|
+
v.type === "force_unregister");
|
|
202
|
+
}
|
|
203
|
+
function hasIdempotencyKey(value) {
|
|
204
|
+
return (typeof value === "object" &&
|
|
205
|
+
value !== null &&
|
|
206
|
+
typeof value.idempotency_key === "string");
|
|
207
|
+
}
|
|
208
|
+
function hasUnsupportedVersion(value) {
|
|
209
|
+
if (typeof value !== "object" || value === null)
|
|
210
|
+
return false;
|
|
211
|
+
const v = value;
|
|
212
|
+
return typeof v.version === "number" && v.version !== 1;
|
|
213
|
+
}
|
|
214
|
+
function makeBaseUrl(apiHost) {
|
|
215
|
+
const trimmed = apiHost.replace(/\/+$/, "");
|
|
216
|
+
const origin = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
217
|
+
return `${origin}/v1`;
|
|
218
|
+
}
|
|
219
|
+
function sleep(ms) {
|
|
220
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
221
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const DEFAULT_SPAWN_TIMEOUT_MS = 30000;
|
|
2
|
+
export declare const DEFAULT_RATE_LIMIT_WINDOW_MS = 30000;
|
|
3
|
+
export declare const DEFAULT_RATE_LIMIT_MAX = 3;
|
|
4
|
+
export interface PendingSpawn {
|
|
5
|
+
spawn_token: string;
|
|
6
|
+
expires_at: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SpawnCorrelationOptions {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SpawnCorrelationManager {
|
|
12
|
+
track(spawn_token: string, onTimeout: () => void): void;
|
|
13
|
+
consume(spawn_token: string): PendingSpawn | undefined;
|
|
14
|
+
isTracked(spawn_token: string): boolean;
|
|
15
|
+
size(): number;
|
|
16
|
+
cancelAll(): void;
|
|
17
|
+
}
|
|
18
|
+
export declare function createSpawnCorrelationManager(opts?: SpawnCorrelationOptions): SpawnCorrelationManager;
|
|
19
|
+
export interface SpawnRateLimiter {
|
|
20
|
+
tryConsume(now?: number): boolean;
|
|
21
|
+
count(now?: number): number;
|
|
22
|
+
}
|
|
23
|
+
export interface SpawnRateLimiterOptions {
|
|
24
|
+
maxInWindow?: number;
|
|
25
|
+
windowMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function createSpawnRateLimiter(opts?: SpawnRateLimiterOptions): SpawnRateLimiter;
|
|
28
|
+
//# sourceMappingURL=spawn-correlation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawn-correlation.d.ts","sourceRoot":"","sources":["../../src/broker/spawn-correlation.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAC/C,eAAO,MAAM,4BAA4B,QAAS,CAAC;AACnD,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IAEpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IAOtC,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAMxD,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAEvD,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IAExC,IAAI,IAAI,MAAM,CAAC;IAEf,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,GAAE,uBAA4B,GACjC,uBAAuB,CA+CzB;AAOD,MAAM,WAAW,gBAAgB;IAE/B,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAElC,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,GAAE,uBAA4B,GACjC,gBAAgB,CA0BlB"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const DEFAULT_SPAWN_TIMEOUT_MS = 30_000;
|
|
2
|
+
export const DEFAULT_RATE_LIMIT_WINDOW_MS = 30_000;
|
|
3
|
+
export const DEFAULT_RATE_LIMIT_MAX = 3;
|
|
4
|
+
export function createSpawnCorrelationManager(opts = {}) {
|
|
5
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_SPAWN_TIMEOUT_MS;
|
|
6
|
+
const entries = new Map();
|
|
7
|
+
const timers = new Map();
|
|
8
|
+
return {
|
|
9
|
+
track(spawn_token, onTimeout) {
|
|
10
|
+
const priorTimer = timers.get(spawn_token);
|
|
11
|
+
if (priorTimer)
|
|
12
|
+
clearTimeout(priorTimer);
|
|
13
|
+
entries.set(spawn_token, {
|
|
14
|
+
spawn_token,
|
|
15
|
+
expires_at: Date.now() + timeoutMs,
|
|
16
|
+
});
|
|
17
|
+
const handle = setTimeout(() => {
|
|
18
|
+
entries.delete(spawn_token);
|
|
19
|
+
timers.delete(spawn_token);
|
|
20
|
+
onTimeout();
|
|
21
|
+
}, timeoutMs);
|
|
22
|
+
handle.unref?.();
|
|
23
|
+
timers.set(spawn_token, handle);
|
|
24
|
+
},
|
|
25
|
+
consume(spawn_token) {
|
|
26
|
+
const entry = entries.get(spawn_token);
|
|
27
|
+
if (!entry)
|
|
28
|
+
return undefined;
|
|
29
|
+
entries.delete(spawn_token);
|
|
30
|
+
const timer = timers.get(spawn_token);
|
|
31
|
+
if (timer) {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
timers.delete(spawn_token);
|
|
34
|
+
}
|
|
35
|
+
return entry;
|
|
36
|
+
},
|
|
37
|
+
isTracked(spawn_token) {
|
|
38
|
+
return entries.has(spawn_token);
|
|
39
|
+
},
|
|
40
|
+
size() {
|
|
41
|
+
return entries.size;
|
|
42
|
+
},
|
|
43
|
+
cancelAll() {
|
|
44
|
+
for (const [, t] of timers)
|
|
45
|
+
clearTimeout(t);
|
|
46
|
+
timers.clear();
|
|
47
|
+
entries.clear();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function createSpawnRateLimiter(opts = {}) {
|
|
52
|
+
const max = opts.maxInWindow ?? DEFAULT_RATE_LIMIT_MAX;
|
|
53
|
+
const windowMs = opts.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS;
|
|
54
|
+
const timestamps = [];
|
|
55
|
+
const sweep = (now) => {
|
|
56
|
+
const cutoff = now - windowMs;
|
|
57
|
+
while (timestamps.length > 0) {
|
|
58
|
+
const head = timestamps[0];
|
|
59
|
+
if (head === undefined || head >= cutoff)
|
|
60
|
+
break;
|
|
61
|
+
timestamps.shift();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
tryConsume(now = Date.now()) {
|
|
66
|
+
sweep(now);
|
|
67
|
+
if (timestamps.length >= max)
|
|
68
|
+
return false;
|
|
69
|
+
timestamps.push(now);
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
count(now = Date.now()) {
|
|
73
|
+
sweep(now);
|
|
74
|
+
return timestamps.length;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface SpawnCommand {
|
|
2
|
+
binary: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
env?: Record<string, string>;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SpawnContext {
|
|
8
|
+
prompt: string;
|
|
9
|
+
spawn_token: string;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DriverAvailability {
|
|
13
|
+
available: boolean;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SpawnDriver {
|
|
17
|
+
readonly runtime_kind: string;
|
|
18
|
+
buildSpawnCommand(ctx: SpawnContext): SpawnCommand;
|
|
19
|
+
isAvailable(): Promise<DriverAvailability>;
|
|
20
|
+
}
|
|
21
|
+
export declare class SpawnDriverRegistry {
|
|
22
|
+
private readonly drivers;
|
|
23
|
+
register(driver: SpawnDriver): void;
|
|
24
|
+
lookup(runtime_kind: string): SpawnDriver | undefined;
|
|
25
|
+
list(): readonly string[];
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=spawn-driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawn-driver.d.ts","sourceRoot":"","sources":["../../src/broker/spawn-driver.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;IAEf,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE7B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,YAAY;IAE3B,MAAM,EAAE,MAAM,CAAC;IAOf,WAAW,EAAE,MAAM,CAAC;IAEpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,OAAO,CAAC;IAGnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAOD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,YAAY,CAAC;IACnD,WAAW,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAC5C;AAGD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAG1D,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAUnC,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAKrD,IAAI,IAAI,SAAS,MAAM,EAAE;CAG1B"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class SpawnDriverRegistry {
|
|
2
|
+
drivers = new Map();
|
|
3
|
+
register(driver) {
|
|
4
|
+
if (this.drivers.has(driver.runtime_kind)) {
|
|
5
|
+
throw new Error(`driver already registered for runtime_kind: ${driver.runtime_kind}`);
|
|
6
|
+
}
|
|
7
|
+
this.drivers.set(driver.runtime_kind, driver);
|
|
8
|
+
}
|
|
9
|
+
lookup(runtime_kind) {
|
|
10
|
+
return this.drivers.get(runtime_kind);
|
|
11
|
+
}
|
|
12
|
+
list() {
|
|
13
|
+
return Array.from(this.drivers.keys());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EndpointRegistry } from "./endpoint-registry.js";
|
|
2
|
+
export declare const TASK_HINT_MAX_LENGTH = 200;
|
|
3
|
+
export interface TaskHintHandlerOptions {
|
|
4
|
+
registry: EndpointRegistry;
|
|
5
|
+
}
|
|
6
|
+
export interface TaskHintUpdate {
|
|
7
|
+
endpoint_id: string;
|
|
8
|
+
plugin_pid: number;
|
|
9
|
+
task_hint: string;
|
|
10
|
+
}
|
|
11
|
+
export type TaskHintResult = {
|
|
12
|
+
ok: true;
|
|
13
|
+
truncated: boolean;
|
|
14
|
+
effective: string;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
code: "endpoint_unknown" | "ownership_mismatch" | "task_hint_invalid";
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function updateTaskHint(opts: TaskHintHandlerOptions, update: TaskHintUpdate): TaskHintResult;
|
|
21
|
+
//# sourceMappingURL=task-hint-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-hint-handler.d.ts","sourceRoot":"","sources":["../../src/broker/task-hint-handler.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACnD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,kBAAkB,GAAG,oBAAoB,GAAG,mBAAmB,CAAC;IACtE,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN,wBAAgB,cAAc,CAC5B,IAAI,EAAE,sBAAsB,EAC5B,MAAM,EAAE,cAAc,GACrB,cAAc,CAmChB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const TASK_HINT_MAX_LENGTH = 200;
|
|
2
|
+
export function updateTaskHint(opts, update) {
|
|
3
|
+
if (typeof update.task_hint !== "string") {
|
|
4
|
+
return {
|
|
5
|
+
ok: false,
|
|
6
|
+
code: "task_hint_invalid",
|
|
7
|
+
message: "task_hint must be a string",
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const entry = opts.registry.get(update.endpoint_id);
|
|
11
|
+
if (!entry) {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
code: "endpoint_unknown",
|
|
15
|
+
message: `unknown endpoint_id: ${update.endpoint_id}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (entry.plugin_pid !== update.plugin_pid) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
code: "ownership_mismatch",
|
|
22
|
+
message: "task_hint is Plugin-write-only; plugin_pid does not match",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const raw = update.task_hint;
|
|
26
|
+
const codePoints = [...raw];
|
|
27
|
+
const truncated = codePoints.length > TASK_HINT_MAX_LENGTH;
|
|
28
|
+
const effective = truncated
|
|
29
|
+
? codePoints.slice(0, TASK_HINT_MAX_LENGTH).join("")
|
|
30
|
+
: raw;
|
|
31
|
+
entry.display_metadata.task_hint = effective;
|
|
32
|
+
return { ok: true, truncated, effective };
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TransitionRuntimeEndpointStateParams } from "../runtime-endpoint-client.js";
|
|
2
|
+
export declare const DEFAULT_QUEUE_CAPACITY = 256;
|
|
3
|
+
export interface QueuedTransition {
|
|
4
|
+
endpoint_id: string;
|
|
5
|
+
params: TransitionRuntimeEndpointStateParams;
|
|
6
|
+
enqueued_at: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TransitionStateRetryQueue {
|
|
9
|
+
enqueue(endpoint_id: string, params: TransitionRuntimeEndpointStateParams): boolean;
|
|
10
|
+
flush(emit: (entry: QueuedTransition) => Promise<void>): Promise<void>;
|
|
11
|
+
size(): number;
|
|
12
|
+
capacity(): number;
|
|
13
|
+
peek(): readonly QueuedTransition[];
|
|
14
|
+
}
|
|
15
|
+
export interface TransitionStateRetryQueueOptions {
|
|
16
|
+
capacity?: number;
|
|
17
|
+
onDrop?: (dropped: QueuedTransition) => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createTransitionStateRetryQueue(opts?: TransitionStateRetryQueueOptions): TransitionStateRetryQueue;
|
|
20
|
+
//# sourceMappingURL=transition-state-retry-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transition-state-retry-queue.d.ts","sourceRoot":"","sources":["../../src/broker/transition-state-retry-queue.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,+BAA+B,CAAC;AAE1F,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,oCAAoC,CAAC;IAE7C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IAExC,OAAO,CACL,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,oCAAoC,GAC3C,OAAO,CAAC;IAOX,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,IAAI,IAAI,MAAM,CAAC;IACf,QAAQ,IAAI,MAAM,CAAC;IAEnB,IAAI,IAAI,SAAS,gBAAgB,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC9C;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,GAAE,gCAAqC,GAC1C,yBAAyB,CA4C3B"}
|