agent-relay-server 0.23.0 → 0.24.0
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/package.json +1 -1
- package/public/index.html +59 -55
- package/runner/src/adapter.ts +10 -0
- package/src/connectors.ts +37 -1
- package/src/lifecycle-manager.ts +1 -0
- package/src/managed-policy.ts +1 -0
- package/src/mcp.ts +9 -5
- package/src/memory-broker-base.ts +161 -0
- package/src/memory-command-broker.ts +10 -119
- package/src/memory-http-broker.ts +11 -141
- package/src/routes.ts +11 -8
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -11391,6 +11391,14 @@ function agentPresence(now, agent, attention, pair) {
|
|
|
11391
11391
|
reconnecting: false,
|
|
11392
11392
|
badges
|
|
11393
11393
|
};
|
|
11394
|
+
if (lifecycleAction.startsWith("finalizing-")) return {
|
|
11395
|
+
label: `wrapping up (${lifecycleAction.slice(11)})`,
|
|
11396
|
+
tone: "warning",
|
|
11397
|
+
icon: "Hourglass",
|
|
11398
|
+
stale,
|
|
11399
|
+
reconnecting,
|
|
11400
|
+
badges
|
|
11401
|
+
};
|
|
11394
11402
|
if (lifecycleAction === "shutting-down") return {
|
|
11395
11403
|
label: "shutting down",
|
|
11396
11404
|
tone: "warning",
|
|
@@ -11764,6 +11772,7 @@ function toneToColor(tone) {
|
|
|
11764
11772
|
function statusDotColor(agent, now) {
|
|
11765
11773
|
if (!agent) return "bg-zinc-500 opacity-50";
|
|
11766
11774
|
if (agent.status === "offline") return "bg-zinc-500 opacity-50";
|
|
11775
|
+
if (typeof agent.meta?.lifecycleAction === "string" && agent.meta.lifecycleAction.startsWith("finalizing-")) return "bg-amber-500 animate-pulse";
|
|
11767
11776
|
if (agent.meta?.lifecycleAction === "shutting-down" || agent.meta?.lifecycleAction === "restarting") return "bg-yellow-500 animate-pulse";
|
|
11768
11777
|
if (agent.meta?.lifecycleAction === "killing") return "bg-red-500 animate-pulse";
|
|
11769
11778
|
if (providerBlockedState(agent)) return "bg-red-500 animate-pulse";
|
|
@@ -77597,6 +77606,49 @@ function OverviewView() {
|
|
|
77597
77606
|
});
|
|
77598
77607
|
}
|
|
77599
77608
|
//#endregion
|
|
77609
|
+
//#region src/hooks/use-agent-terminal.ts
|
|
77610
|
+
/**
|
|
77611
|
+
* Open/close lifecycle for an agent's terminal session. Shared by the agent
|
|
77612
|
+
* card and the chat header — both POST a terminal-session, track the target,
|
|
77613
|
+
* and DELETE guest sessions on close. Pass null/undefined to no-op (e.g. when
|
|
77614
|
+
* no agent is selected).
|
|
77615
|
+
*/
|
|
77616
|
+
function useAgentTerminal(agentId) {
|
|
77617
|
+
const showError = useRelayStore((s) => s.showError);
|
|
77618
|
+
const [terminalOpen, setTerminalOpen] = (0, import_react.useState)(false);
|
|
77619
|
+
const [terminalTarget, setTerminalTarget] = (0, import_react.useState)(null);
|
|
77620
|
+
const [terminalOpening, setTerminalOpening] = (0, import_react.useState)(false);
|
|
77621
|
+
async function openTerminal() {
|
|
77622
|
+
if (!agentId || terminalOpening) return;
|
|
77623
|
+
setTerminalOpening(true);
|
|
77624
|
+
try {
|
|
77625
|
+
setTerminalTarget(await api("POST", "/agents/" + encodeURIComponent(agentId) + "/terminal-session"));
|
|
77626
|
+
setTerminalOpen(true);
|
|
77627
|
+
} catch (e) {
|
|
77628
|
+
showError("Terminal Failed", e.message);
|
|
77629
|
+
} finally {
|
|
77630
|
+
setTerminalOpening(false);
|
|
77631
|
+
}
|
|
77632
|
+
}
|
|
77633
|
+
function closeTerminal(open) {
|
|
77634
|
+
if (open) {
|
|
77635
|
+
setTerminalOpen(true);
|
|
77636
|
+
return;
|
|
77637
|
+
}
|
|
77638
|
+
setTerminalOpen(false);
|
|
77639
|
+
const target = terminalTarget;
|
|
77640
|
+
setTerminalTarget(null);
|
|
77641
|
+
if (agentId && target?.mode === "guest") api("DELETE", "/agents/" + encodeURIComponent(agentId) + "/terminal-session/" + encodeURIComponent(target.session)).catch(() => {});
|
|
77642
|
+
}
|
|
77643
|
+
return {
|
|
77644
|
+
terminalOpen,
|
|
77645
|
+
terminalTarget,
|
|
77646
|
+
terminalOpening,
|
|
77647
|
+
openTerminal,
|
|
77648
|
+
closeTerminal
|
|
77649
|
+
};
|
|
77650
|
+
}
|
|
77651
|
+
//#endregion
|
|
77600
77652
|
//#region node_modules/comma-separated-tokens/index.js
|
|
77601
77653
|
/**
|
|
77602
77654
|
* Serialize an array of strings or numbers to comma-separated tokens.
|
|
@@ -127279,9 +127331,6 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
127279
127331
|
const pendingAttachmentsRef = (0, import_react.useRef)([]);
|
|
127280
127332
|
const [pendingAttachments, setPendingAttachments] = (0, import_react.useState)([]);
|
|
127281
127333
|
const [dragActive, setDragActive] = (0, import_react.useState)(false);
|
|
127282
|
-
const [terminalOpen, setTerminalOpen] = (0, import_react.useState)(false);
|
|
127283
|
-
const [terminalTarget, setTerminalTarget] = (0, import_react.useState)(null);
|
|
127284
|
-
const [terminalOpening, setTerminalOpening] = (0, import_react.useState)(false);
|
|
127285
127334
|
const [filePreview, setFilePreview] = (0, import_react.useState)(null);
|
|
127286
127335
|
const [floatingPreview, setFloatingPreview] = (0, import_react.useState)(null);
|
|
127287
127336
|
const [filePreviewError, setFilePreviewError] = (0, import_react.useState)("");
|
|
@@ -127302,6 +127351,8 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
127302
127351
|
selectedInboxThread
|
|
127303
127352
|
]);
|
|
127304
127353
|
const agent = agentsById[selectedInboxThread] || null;
|
|
127354
|
+
const agentFinalizing = typeof agent?.meta?.lifecycleAction === "string" && agent.meta.lifecycleAction.startsWith("finalizing-");
|
|
127355
|
+
const { terminalOpen, terminalTarget, terminalOpening, openTerminal: handleOpenTerminal, closeTerminal: handleCloseTerminal } = useAgentTerminal(agent?.id);
|
|
127305
127356
|
const agentSpawnRequestId = typeof agent?.meta?.spawnRequestId === "string" ? agent.meta.spawnRequestId : "";
|
|
127306
127357
|
const importedHistory = (0, import_react.useMemo)(() => {
|
|
127307
127358
|
return chatHistoryImports.filter((history) => history.targetAgentId === selectedInboxThread || Boolean(agentSpawnRequestId && history.targetSpawnRequestId === agentSpawnRequestId));
|
|
@@ -127382,18 +127433,6 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
127382
127433
|
agent?.createdAt,
|
|
127383
127434
|
importedHistory
|
|
127384
127435
|
]);
|
|
127385
|
-
async function handleOpenTerminal() {
|
|
127386
|
-
if (!agent || terminalOpening) return;
|
|
127387
|
-
setTerminalOpening(true);
|
|
127388
|
-
try {
|
|
127389
|
-
setTerminalTarget(await api("POST", "/agents/" + encodeURIComponent(agent.id) + "/terminal-session"));
|
|
127390
|
-
setTerminalOpen(true);
|
|
127391
|
-
} catch (e) {
|
|
127392
|
-
showError("Terminal Failed", e.message);
|
|
127393
|
-
} finally {
|
|
127394
|
-
setTerminalOpening(false);
|
|
127395
|
-
}
|
|
127396
|
-
}
|
|
127397
127436
|
const clearFilePreviewCloseTimer = (0, import_react.useCallback)(() => {
|
|
127398
127437
|
if (filePreviewCloseTimer.current === null) return;
|
|
127399
127438
|
window.clearTimeout(filePreviewCloseTimer.current);
|
|
@@ -127475,16 +127514,6 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
127475
127514
|
setFilePreview(null);
|
|
127476
127515
|
setFilePreviewError("");
|
|
127477
127516
|
}
|
|
127478
|
-
function handleCloseTerminal(open) {
|
|
127479
|
-
if (open) {
|
|
127480
|
-
setTerminalOpen(true);
|
|
127481
|
-
return;
|
|
127482
|
-
}
|
|
127483
|
-
setTerminalOpen(false);
|
|
127484
|
-
const target = terminalTarget;
|
|
127485
|
-
setTerminalTarget(null);
|
|
127486
|
-
if (agent && target?.mode === "guest") api("DELETE", "/agents/" + encodeURIComponent(agent.id) + "/terminal-session/" + encodeURIComponent(target.session)).catch(() => {});
|
|
127487
|
-
}
|
|
127488
127517
|
async function uploadFiles(files) {
|
|
127489
127518
|
const list = Array.from(files).filter((file) => file.size > 0);
|
|
127490
127519
|
if (!list.length) return;
|
|
@@ -127530,7 +127559,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
127530
127559
|
setPendingAttachments([]);
|
|
127531
127560
|
}
|
|
127532
127561
|
function handleSend() {
|
|
127533
|
-
if (!draft.trim() && readyAttachments.length === 0 || !selectedInboxThread || chatSending || hasPendingUploads) return;
|
|
127562
|
+
if (!draft.trim() && readyAttachments.length === 0 || !selectedInboxThread || chatSending || hasPendingUploads || agentFinalizing) return;
|
|
127534
127563
|
const attachments = readyAttachments.map((item) => ({
|
|
127535
127564
|
artifactId: item.artifact.id,
|
|
127536
127565
|
kind: item.artifact.kind,
|
|
@@ -127988,7 +128017,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
127988
128017
|
}),
|
|
127989
128018
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
127990
128019
|
size: "icon",
|
|
127991
|
-
disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
|
|
128020
|
+
disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending || agentFinalizing,
|
|
127992
128021
|
onClick: handleSend,
|
|
127993
128022
|
className: "shrink-0 mb-0.5 rounded-xl h-[42px] w-[42px]",
|
|
127994
128023
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Send, { className: "w-4 h-4" })
|
|
@@ -128038,7 +128067,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
128038
128067
|
})]
|
|
128039
128068
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
128040
128069
|
size: "icon",
|
|
128041
|
-
disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
|
|
128070
|
+
disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending || agentFinalizing,
|
|
128042
128071
|
onClick: handleSend,
|
|
128043
128072
|
className: "rounded-xl h-9 w-9",
|
|
128044
128073
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Send, { className: "w-4 h-4" })
|
|
@@ -128047,7 +128076,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
128047
128076
|
}),
|
|
128048
128077
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
128049
128078
|
className: "text-xs text-muted-foreground mt-1.5 hidden md:block",
|
|
128050
|
-
children: "Enter to send · Shift+Enter for newline"
|
|
128079
|
+
children: agentFinalizing ? `${agent ? displayName(agent) : "Agent"} is wrapping up (${(agent?.meta?.lifecycleAction).slice(11)}) — messaging paused` : "Enter to send · Shift+Enter for newline"
|
|
128051
128080
|
})
|
|
128052
128081
|
]
|
|
128053
128082
|
})
|
|
@@ -128176,13 +128205,10 @@ function AgentCard({ agent }) {
|
|
|
128176
128205
|
const openPairInvite = useRelayStore((s) => s.openPairInvite);
|
|
128177
128206
|
const openRename = useRelayStore((s) => s.openRename);
|
|
128178
128207
|
const openConfirm = useRelayStore((s) => s.openConfirm);
|
|
128179
|
-
const showError = useRelayStore((s) => s.showError);
|
|
128180
128208
|
const doAgentAction = useRelayStore((s) => s.doAgentAction);
|
|
128181
128209
|
const doDeleteAgent = useRelayStore((s) => s.doDeleteAgent);
|
|
128182
128210
|
const openFilesForAgent = useRelayStore((s) => s.openFilesForAgent);
|
|
128183
|
-
const
|
|
128184
|
-
const [terminalTarget, setTerminalTarget] = (0, import_react.useState)(null);
|
|
128185
|
-
const [terminalOpening, setTerminalOpening] = (0, import_react.useState)(false);
|
|
128211
|
+
const { terminalOpen, terminalTarget, terminalOpening, openTerminal: handleOpenTerminal, closeTerminal: handleCloseTerminal } = useAgentTerminal(agent.id);
|
|
128186
128212
|
const pairsMap = usePairsByAgentId();
|
|
128187
128213
|
const rawAttention = useAgentAttention(agent);
|
|
128188
128214
|
const pair = pairsMap[agent.id] || null;
|
|
@@ -128196,28 +128222,6 @@ function AgentCard({ agent }) {
|
|
|
128196
128222
|
const canOpenTerminal = Boolean(agent.providerCapabilities?.terminal?.live?.read || agent.providerCapabilities?.terminal?.attach?.create);
|
|
128197
128223
|
const canWriteTerminal = Boolean(agent.providerCapabilities?.terminal?.live?.write || agent.providerCapabilities?.model?.provider === "claude");
|
|
128198
128224
|
const canForget = agentCanBeForgotten(agent);
|
|
128199
|
-
async function handleOpenTerminal() {
|
|
128200
|
-
if (terminalOpening) return;
|
|
128201
|
-
setTerminalOpening(true);
|
|
128202
|
-
try {
|
|
128203
|
-
setTerminalTarget(await api("POST", "/agents/" + encodeURIComponent(agent.id) + "/terminal-session"));
|
|
128204
|
-
setTerminalOpen(true);
|
|
128205
|
-
} catch (e) {
|
|
128206
|
-
showError("Terminal Failed", e.message);
|
|
128207
|
-
} finally {
|
|
128208
|
-
setTerminalOpening(false);
|
|
128209
|
-
}
|
|
128210
|
-
}
|
|
128211
|
-
function handleCloseTerminal(open) {
|
|
128212
|
-
if (open) {
|
|
128213
|
-
setTerminalOpen(true);
|
|
128214
|
-
return;
|
|
128215
|
-
}
|
|
128216
|
-
setTerminalOpen(false);
|
|
128217
|
-
const target = terminalTarget;
|
|
128218
|
-
setTerminalTarget(null);
|
|
128219
|
-
if (target?.mode === "guest") api("DELETE", "/agents/" + encodeURIComponent(agent.id) + "/terminal-session/" + encodeURIComponent(target.session)).catch(() => {});
|
|
128220
|
-
}
|
|
128221
128225
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Card, {
|
|
128222
128226
|
className: "group hover:border-zinc-600 transition-colors cursor-pointer",
|
|
128223
128227
|
onClick: () => openAgentDetail(agent),
|
package/runner/src/adapter.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentProfile, Message } from "agent-relay-sdk";
|
|
2
2
|
import { isRecord } from "agent-relay-sdk";
|
|
3
|
+
import type { SessionEvent } from "./session-insights";
|
|
3
4
|
|
|
4
5
|
export type SemanticStatus = "idle" | "busy" | "offline" | "error";
|
|
5
6
|
type ProviderWorkKind = "provider-turn" | "subagent";
|
|
@@ -133,6 +134,15 @@ export interface ProviderAdapter {
|
|
|
133
134
|
shutdown(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void>;
|
|
134
135
|
compact?(process: ManagedProcess): Promise<Record<string, unknown> | void>;
|
|
135
136
|
clearContext?(process: ManagedProcess): Promise<Record<string, unknown> | void>;
|
|
137
|
+
// Normalize the session so far into the provider-agnostic SessionEvent stream the
|
|
138
|
+
// Insights context-ratio signal (#183/#184) reduces. Called by the runner's
|
|
139
|
+
// pre-session-destroy seam before any compact/clear/restart/shutdown. The runner owns
|
|
140
|
+
// the per-segment cursor (it slices events since the last capture), so this returns the
|
|
141
|
+
// full ordered event list for the current process lifetime. `ctx.transcriptPath` is
|
|
142
|
+
// supplied for transcript-backed providers (Claude); event-stream providers (Codex)
|
|
143
|
+
// ignore it and return their accumulated log. Return null when there is nothing to
|
|
144
|
+
// measure. Best-effort: may be omitted by providers without a session view yet.
|
|
145
|
+
collectSessionEvents?(process: ManagedProcess, ctx: { transcriptPath?: string }): Promise<SessionEvent[] | null>;
|
|
136
146
|
// Interrupt the in-flight turn without ending the session (ESC for Claude's
|
|
137
147
|
// tmux pane, turn/interrupt for the Codex app-server). Provider-independent at
|
|
138
148
|
// the runner boundary; each adapter does what its provider actually supports.
|
package/src/connectors.ts
CHANGED
|
@@ -243,10 +243,46 @@ async function refreshConnectorStatus(id: string): Promise<void> {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
export function startConnectorStatusPoller(): void {
|
|
246
|
-
|
|
246
|
+
// Relaunch connectors that were running before this process restarted (e.g. a
|
|
247
|
+
// deploy that took down the relay/orchestrator tree) BEFORE the first status
|
|
248
|
+
// poll overwrites their persisted "running" flag with the now-dead reality.
|
|
249
|
+
void reconcileConnectorsOnBoot().finally(() => void refreshAllConnectorStatuses());
|
|
247
250
|
setInterval(() => void refreshAllConnectorStatuses(), STATUS_POLL_INTERVAL_MS);
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
// On boot, bring back any connector whose last persisted state says it was
|
|
254
|
+
// running. `start` is idempotent — a daemon that survived the restart reports
|
|
255
|
+
// "already running"; one that died with the process tree is relaunched. A
|
|
256
|
+
// connector the operator deliberately stopped has running:false persisted, so
|
|
257
|
+
// it stays down. Best-effort: failures are swallowed; the poll reflects truth.
|
|
258
|
+
export async function reconcileConnectorsOnBoot(): Promise<void> {
|
|
259
|
+
for (const connector of listConnectors()) {
|
|
260
|
+
const wasRunning = connector.state?.running === true;
|
|
261
|
+
const canStart = Boolean(connector.manifest.commands.start?.length);
|
|
262
|
+
if (!wasRunning || !canStart) continue;
|
|
263
|
+
try {
|
|
264
|
+
await runConnectorAction(connector.id, "start");
|
|
265
|
+
} catch { /* boot relaunch is best-effort; status poll will surface failures */ }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Flip a connector to running:false when the relay proves its advertised
|
|
270
|
+
// endpoint is unreachable (e.g. the daemon died but state.json still claims
|
|
271
|
+
// running:true). Stops the dashboard from lying until the 60s status poll runs.
|
|
272
|
+
export function markConnectorUnreachable(id: string, detail: string): void {
|
|
273
|
+
const path = join(connectorDir(id), "state.json");
|
|
274
|
+
const current = readRecordFile(path);
|
|
275
|
+
if (!current || current.running === false) return;
|
|
276
|
+
const next = {
|
|
277
|
+
...current,
|
|
278
|
+
status: "error",
|
|
279
|
+
detail,
|
|
280
|
+
running: false,
|
|
281
|
+
updatedAt: new Date().toISOString(),
|
|
282
|
+
};
|
|
283
|
+
writeFileSync(path, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
|
|
284
|
+
}
|
|
285
|
+
|
|
250
286
|
async function refreshAllConnectorStatuses(): Promise<void> {
|
|
251
287
|
for (const connector of listConnectors()) {
|
|
252
288
|
try {
|
package/src/lifecycle-manager.ts
CHANGED
package/src/managed-policy.ts
CHANGED
|
@@ -32,6 +32,7 @@ export function buildManagedSpawnParams(policy: SpawnPolicy, requestId: string,
|
|
|
32
32
|
const grant = spawnGrantForProfile(policy.profile);
|
|
33
33
|
return buildSpawnCommand({
|
|
34
34
|
provider: policy.provider,
|
|
35
|
+
orchestratorId: policy.orchestratorId,
|
|
35
36
|
cwd: policy.cwd,
|
|
36
37
|
workspaceMode: effectiveManagedPolicyWorkspaceMode(policy),
|
|
37
38
|
rig: policy.rig || undefined,
|
package/src/mcp.ts
CHANGED
|
@@ -515,11 +515,14 @@ function senderIdentity(auth: McpAuthContext): string | undefined {
|
|
|
515
515
|
return agents?.length === 1 ? agents[0] : undefined;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
//
|
|
518
|
+
// THE caller-identity resolver: the agent id behind this token, for `from`-autofill,
|
|
519
|
+
// relay_whoami, and spawn/shutdown gating (#221, #243). `senderIdentity` covers
|
|
519
520
|
// identity-bearing tokens (interactive/mcp, constraints.agents). Managed agents spawned by
|
|
520
521
|
// the orchestrator authenticate with a runner token that carries no `agents` constraint but
|
|
521
|
-
// DOES carry its spawnRequestId/policy — resolve those back to the registered agent card
|
|
522
|
-
// Returns undefined for server/admin tokens (unrestricted by
|
|
522
|
+
// DOES carry its spawnRequestId/policy — resolve those back to the registered agent card so
|
|
523
|
+
// they never need to pass `from`. Returns undefined for server/admin tokens (unrestricted by
|
|
524
|
+
// design) and multi-agent tokens. Keep `resolveSender`/`relayWhoami` on THIS, not the narrower
|
|
525
|
+
// `senderIdentity`, or managed agents silently lose implicit identity again (the #243 drift).
|
|
523
526
|
function callerAgentId(auth: McpAuthContext): string | undefined {
|
|
524
527
|
const direct = senderIdentity(auth);
|
|
525
528
|
if (direct) return direct;
|
|
@@ -536,14 +539,15 @@ function callerAgentId(auth: McpAuthContext): string | undefined {
|
|
|
536
539
|
|
|
537
540
|
function resolveSender(auth: McpAuthContext, rawFrom: unknown): string {
|
|
538
541
|
// Token identity wins and cannot be spoofed; any provided `from` is ignored when known.
|
|
539
|
-
|
|
542
|
+
// Resolves both constraints.agents tokens AND spawn/policy-managed agents (#243).
|
|
543
|
+
const identity = callerAgentId(auth);
|
|
540
544
|
if (identity) return identity;
|
|
541
545
|
// Server/integration/multi-agent tokens carry no single identity — keep requiring `from`.
|
|
542
546
|
return stringField(rawFrom, "from", { required: true, max: 200 });
|
|
543
547
|
}
|
|
544
548
|
|
|
545
549
|
function relayWhoami(auth: McpAuthContext): Record<string, unknown> {
|
|
546
|
-
const agentId =
|
|
550
|
+
const agentId = callerAgentId(auth);
|
|
547
551
|
const agent = agentId ? getAgent(agentId) : null;
|
|
548
552
|
return {
|
|
549
553
|
agentId: agentId ?? null,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActiveMemoryClearReason,
|
|
3
|
+
ContextPackage,
|
|
4
|
+
ContextPackageRequest,
|
|
5
|
+
CreateMemoryInput,
|
|
6
|
+
Memory,
|
|
7
|
+
MemoryBrokerCapabilities,
|
|
8
|
+
MemoryBrokerContext,
|
|
9
|
+
MemoryQuery,
|
|
10
|
+
MemorySearchResult,
|
|
11
|
+
MemoryStats,
|
|
12
|
+
UpdateMemoryInput,
|
|
13
|
+
} from "./types";
|
|
14
|
+
import type { MemoryBroker } from "./memory-broker-contract";
|
|
15
|
+
import {
|
|
16
|
+
MemoryBrokerContractError,
|
|
17
|
+
normalizeMemory,
|
|
18
|
+
normalizeMemoryBrokerCapabilities,
|
|
19
|
+
normalizeMemorySearchResult,
|
|
20
|
+
} from "./memory-broker-contract";
|
|
21
|
+
import { isRecord } from "agent-relay-sdk";
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_MEMORY_BROKER_TIMEOUT_MS = 10_000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shared scaffolding for transport-backed memory brokers. The command and HTTP
|
|
27
|
+
* brokers differ only in how a single request is dispatched — everything else
|
|
28
|
+
* (operation methods, response normalization, error/result unwrapping) is
|
|
29
|
+
* identical. Subclasses implement {@link call} and supply a {@link label} used
|
|
30
|
+
* in error messages.
|
|
31
|
+
*/
|
|
32
|
+
export abstract class AbstractMemoryBroker implements MemoryBroker {
|
|
33
|
+
/** Human-readable transport name, e.g. "command" or "http". */
|
|
34
|
+
protected abstract readonly label: string;
|
|
35
|
+
|
|
36
|
+
/** Dispatch a single broker operation and return its raw (un-unwrapped) result. */
|
|
37
|
+
protected abstract call(operation: string, payload: Record<string, unknown>): Promise<unknown>;
|
|
38
|
+
|
|
39
|
+
async capabilities(): Promise<MemoryBrokerCapabilities> {
|
|
40
|
+
return normalizeMemoryBrokerCapabilities(await this.call("capabilities", {}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async create(input: CreateMemoryInput, ctx: MemoryBrokerContext): Promise<Memory> {
|
|
44
|
+
return normalizeMemory(await this.call("create", { input, ctx }), { now: ctx.now });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async get(id: string, ctx: MemoryBrokerContext): Promise<Memory | null> {
|
|
48
|
+
const value = await this.call("get", { id, ctx });
|
|
49
|
+
return value === null ? null : normalizeMemory(value, { now: ctx.now });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async search(query: MemoryQuery, ctx: MemoryBrokerContext): Promise<MemorySearchResult> {
|
|
53
|
+
return normalizeMemorySearchResult(await this.call("search", { query, ctx }), { now: ctx.now });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async update(id: string, patch: UpdateMemoryInput, ctx: MemoryBrokerContext): Promise<Memory> {
|
|
57
|
+
return normalizeMemory(await this.call("update", { id, patch, ctx }), { now: ctx.now });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async delete(id: string, ctx: MemoryBrokerContext): Promise<void> {
|
|
61
|
+
await this.call("delete", { id, ctx });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async stats(ctx: MemoryBrokerContext): Promise<MemoryStats> {
|
|
65
|
+
return normalizeStats(await this.call("stats", { ctx }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async assemble(request: ContextPackageRequest, ctx: MemoryBrokerContext): Promise<ContextPackage> {
|
|
69
|
+
return normalizeContextPackage(await this.call("assemble", { request, ctx }), ctx);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async markInjected(agentId: string, memoryIds: string[], ctx: MemoryBrokerContext): Promise<void> {
|
|
73
|
+
await this.call("mark-injected", { agentId, memoryIds, ctx });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async clearActive(agentId: string, reason: ActiveMemoryClearReason, ctx: MemoryBrokerContext): Promise<void> {
|
|
77
|
+
await this.call("clear-active", { agentId, reason, ctx });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async listActive(agentId: string, ctx: MemoryBrokerContext): Promise<Memory[]> {
|
|
81
|
+
return normalizeMemoryList(await this.call("list-active", { agentId, ctx }), ctx);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Parse a broker response body, treating empty output as null. */
|
|
85
|
+
protected parseJson(text: string, operation: string): unknown {
|
|
86
|
+
if (!text.trim()) return null;
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(text);
|
|
89
|
+
} catch {
|
|
90
|
+
throw new MemoryBrokerContractError(`${this.label} memory broker ${operation} returned invalid JSON`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve a broker payload to its result. A `{ error }` field is treated as a
|
|
96
|
+
* failure (even on an otherwise-successful transport response), and a
|
|
97
|
+
* `{ result }` envelope is unwrapped; anything else is returned as-is.
|
|
98
|
+
*/
|
|
99
|
+
protected unwrapResult(payload: unknown): unknown {
|
|
100
|
+
if (isRecord(payload) && Object.hasOwn(payload, "error") && typeof payload.error === "string") {
|
|
101
|
+
throw new MemoryBrokerContractError(`${this.label} memory broker failed: ${payload.error}`);
|
|
102
|
+
}
|
|
103
|
+
if (isRecord(payload) && Object.hasOwn(payload, "result")) return payload.result;
|
|
104
|
+
return payload;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeContextPackage(value: unknown, ctx: MemoryBrokerContext): ContextPackage {
|
|
109
|
+
const record = requireRecord(value, "context package");
|
|
110
|
+
const rawMemories = record.memories;
|
|
111
|
+
if (!Array.isArray(rawMemories)) throw new MemoryBrokerContractError("context package.memories must be an array");
|
|
112
|
+
return {
|
|
113
|
+
memories: rawMemories.map((item) => {
|
|
114
|
+
const packaged = requireRecord(item, "packaged memory");
|
|
115
|
+
const priority = packaged.priority;
|
|
116
|
+
if (priority !== 1 && priority !== 2 && priority !== 3) throw new MemoryBrokerContractError("packaged memory.priority must be 1, 2, or 3");
|
|
117
|
+
return {
|
|
118
|
+
memory: normalizeMemory(packaged.memory, { now: ctx.now }),
|
|
119
|
+
reason: typeof packaged.reason === "string" ? packaged.reason : "broker selected",
|
|
120
|
+
priority,
|
|
121
|
+
score: typeof packaged.score === "number" && Number.isFinite(packaged.score) ? packaged.score : undefined,
|
|
122
|
+
};
|
|
123
|
+
}),
|
|
124
|
+
estimatedTokens: typeof record.estimatedTokens === "number" && Number.isFinite(record.estimatedTokens) ? record.estimatedTokens : 0,
|
|
125
|
+
rolePrompt: typeof record.rolePrompt === "string" ? record.rolePrompt : undefined,
|
|
126
|
+
recentContext: typeof record.recentContext === "string" ? record.recentContext : undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeMemoryList(value: unknown, ctx: MemoryBrokerContext): Memory[] {
|
|
131
|
+
if (Array.isArray(value)) return value.map((item) => normalizeMemory(item, { now: ctx.now }));
|
|
132
|
+
if (isRecord(value) && Array.isArray(value.memories)) return value.memories.map((item) => normalizeMemory(item, { now: ctx.now }));
|
|
133
|
+
throw new MemoryBrokerContractError("memory list-active result must be an array or object with memories array");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeStats(value: unknown): MemoryStats {
|
|
137
|
+
const record = requireRecord(value, "memory stats");
|
|
138
|
+
return {
|
|
139
|
+
total: numberField(record.total, "memory stats.total"),
|
|
140
|
+
byType: objectField(record.byType, "memory stats.byType"),
|
|
141
|
+
byScope: objectField(record.byScope, "memory stats.byScope"),
|
|
142
|
+
bySensitivity: objectField(record.bySensitivity, "memory stats.bySensitivity"),
|
|
143
|
+
} as MemoryStats;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function objectField(value: unknown, field: string): Record<string, number> {
|
|
147
|
+
const record = requireRecord(value, field);
|
|
148
|
+
const out: Record<string, number> = {};
|
|
149
|
+
for (const [key, count] of Object.entries(record)) out[key] = numberField(count, `${field}.${key}`);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function numberField(value: unknown, field: string): number {
|
|
154
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new MemoryBrokerContractError(`${field} must be a number`);
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requireRecord(value: unknown, field: string): Record<string, unknown> {
|
|
159
|
+
if (!isRecord(value)) throw new MemoryBrokerContractError(`${field} must be an object`);
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
@@ -1,82 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
ContextPackageRequest,
|
|
6
|
-
CreateMemoryInput,
|
|
7
|
-
Memory,
|
|
8
|
-
MemoryBrokerCapabilities,
|
|
9
|
-
MemoryBrokerContext,
|
|
10
|
-
MemoryQuery,
|
|
11
|
-
MemorySearchResult,
|
|
12
|
-
MemoryStats,
|
|
13
|
-
UpdateMemoryInput,
|
|
14
|
-
} from "./types";
|
|
15
|
-
import type { MemoryBroker } from "./memory-broker-contract";
|
|
16
|
-
import {
|
|
17
|
-
MemoryBrokerContractError,
|
|
18
|
-
normalizeMemory,
|
|
19
|
-
normalizeMemoryBrokerCapabilities,
|
|
20
|
-
normalizeMemorySearchResult,
|
|
21
|
-
} from "./memory-broker-contract";
|
|
22
|
-
import { normalizeContextPackage } from "./memory-http-broker";
|
|
23
|
-
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
1
|
+
import type { CommandMemoryBrokerConfig } from "./types";
|
|
2
|
+
import { MemoryBrokerContractError } from "./memory-broker-contract";
|
|
3
|
+
import { AbstractMemoryBroker, DEFAULT_MEMORY_BROKER_TIMEOUT_MS } from "./memory-broker-base";
|
|
4
|
+
import { errMessage } from "agent-relay-sdk";
|
|
24
5
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export class CommandMemoryBroker implements MemoryBroker {
|
|
6
|
+
export class CommandMemoryBroker extends AbstractMemoryBroker {
|
|
7
|
+
protected readonly label = "command";
|
|
28
8
|
private readonly timeoutMs: number;
|
|
29
9
|
|
|
30
10
|
constructor(private readonly config: CommandMemoryBrokerConfig) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
async capabilities(): Promise<MemoryBrokerCapabilities> {
|
|
35
|
-
return normalizeMemoryBrokerCapabilities(await this.call("capabilities", {}));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async create(input: CreateMemoryInput, ctx: MemoryBrokerContext): Promise<Memory> {
|
|
39
|
-
return normalizeMemory(await this.call("create", { input, ctx }), { now: ctx.now });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async get(id: string, ctx: MemoryBrokerContext): Promise<Memory | null> {
|
|
43
|
-
const value = await this.call("get", { id, ctx });
|
|
44
|
-
return value === null ? null : normalizeMemory(value, { now: ctx.now });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async search(query: MemoryQuery, ctx: MemoryBrokerContext): Promise<MemorySearchResult> {
|
|
48
|
-
return normalizeMemorySearchResult(await this.call("search", { query, ctx }), { now: ctx.now });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async update(id: string, patch: UpdateMemoryInput, ctx: MemoryBrokerContext): Promise<Memory> {
|
|
52
|
-
return normalizeMemory(await this.call("update", { id, patch, ctx }), { now: ctx.now });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async delete(id: string, ctx: MemoryBrokerContext): Promise<void> {
|
|
56
|
-
await this.call("delete", { id, ctx });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async stats(ctx: MemoryBrokerContext): Promise<MemoryStats> {
|
|
60
|
-
return normalizeStats(await this.call("stats", { ctx }));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async assemble(request: ContextPackageRequest, ctx: MemoryBrokerContext): Promise<ContextPackage> {
|
|
64
|
-
return normalizeContextPackage(await this.call("assemble", { request, ctx }), ctx);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async markInjected(agentId: string, memoryIds: string[], ctx: MemoryBrokerContext): Promise<void> {
|
|
68
|
-
await this.call("mark-injected", { agentId, memoryIds, ctx });
|
|
11
|
+
super();
|
|
12
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_MEMORY_BROKER_TIMEOUT_MS;
|
|
69
13
|
}
|
|
70
14
|
|
|
71
|
-
async
|
|
72
|
-
await this.call("clear-active", { agentId, reason, ctx });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async listActive(agentId: string, ctx: MemoryBrokerContext): Promise<Memory[]> {
|
|
76
|
-
return normalizeMemoryList(await this.call("list-active", { agentId, ctx }), ctx);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private async call(operation: string, payload: Record<string, unknown>): Promise<unknown> {
|
|
15
|
+
protected async call(operation: string, payload: Record<string, unknown>): Promise<unknown> {
|
|
80
16
|
const proc = Bun.spawn([this.config.command, ...(this.config.args ?? [])], {
|
|
81
17
|
stdin: "pipe",
|
|
82
18
|
stdout: "pipe",
|
|
@@ -102,7 +38,7 @@ export class CommandMemoryBroker implements MemoryBroker {
|
|
|
102
38
|
if (exitCode !== 0) {
|
|
103
39
|
throw new MemoryBrokerContractError(`command memory broker ${operation} failed: exit ${exitCode}${stderr.trim() ? ` ${stderr.trim()}` : ""}`);
|
|
104
40
|
}
|
|
105
|
-
return unwrapResult(parseJson(stdout, operation));
|
|
41
|
+
return this.unwrapResult(this.parseJson(stdout, operation));
|
|
106
42
|
} catch (error) {
|
|
107
43
|
if (error instanceof MemoryBrokerContractError) throw error;
|
|
108
44
|
throw new MemoryBrokerContractError(`command memory broker ${operation} failed: ${errMessage(error)}`);
|
|
@@ -111,48 +47,3 @@ export class CommandMemoryBroker implements MemoryBroker {
|
|
|
111
47
|
}
|
|
112
48
|
}
|
|
113
49
|
}
|
|
114
|
-
|
|
115
|
-
function parseJson(text: string, operation: string): unknown {
|
|
116
|
-
if (!text.trim()) return null;
|
|
117
|
-
try {
|
|
118
|
-
return JSON.parse(text);
|
|
119
|
-
} catch {
|
|
120
|
-
throw new MemoryBrokerContractError(`command memory broker ${operation} returned invalid JSON`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function unwrapResult(payload: unknown): unknown {
|
|
125
|
-
if (isRecord(payload) && Object.hasOwn(payload, "error") && typeof payload.error === "string") {
|
|
126
|
-
throw new MemoryBrokerContractError(`command memory broker failed: ${payload.error}`);
|
|
127
|
-
}
|
|
128
|
-
if (isRecord(payload) && Object.hasOwn(payload, "result")) return payload.result;
|
|
129
|
-
return payload;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function normalizeMemoryList(value: unknown, ctx: MemoryBrokerContext): Memory[] {
|
|
133
|
-
if (Array.isArray(value)) return value.map((item) => normalizeMemory(item, { now: ctx.now }));
|
|
134
|
-
if (isRecord(value) && Array.isArray(value.memories)) return value.memories.map((item) => normalizeMemory(item, { now: ctx.now }));
|
|
135
|
-
throw new MemoryBrokerContractError("memory list-active result must be an array or object with memories array");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function normalizeStats(value: unknown): MemoryStats {
|
|
139
|
-
if (!isRecord(value)) throw new MemoryBrokerContractError("memory stats must be an object");
|
|
140
|
-
return {
|
|
141
|
-
total: numberField(value.total, "memory stats.total"),
|
|
142
|
-
byType: objectField(value.byType, "memory stats.byType"),
|
|
143
|
-
byScope: objectField(value.byScope, "memory stats.byScope"),
|
|
144
|
-
bySensitivity: objectField(value.bySensitivity, "memory stats.bySensitivity"),
|
|
145
|
-
} as MemoryStats;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function objectField(value: unknown, field: string): Record<string, number> {
|
|
149
|
-
if (!isRecord(value)) throw new MemoryBrokerContractError(`${field} must be an object`);
|
|
150
|
-
const out: Record<string, number> = {};
|
|
151
|
-
for (const [key, count] of Object.entries(value)) out[key] = numberField(count, `${field}.${key}`);
|
|
152
|
-
return out;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function numberField(value: unknown, field: string): number {
|
|
156
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new MemoryBrokerContractError(`${field} must be a number`);
|
|
157
|
-
return value;
|
|
158
|
-
}
|
|
@@ -1,83 +1,22 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ContextPackageRequest,
|
|
5
|
-
CreateMemoryInput,
|
|
6
|
-
HttpMemoryBrokerConfig,
|
|
7
|
-
Memory,
|
|
8
|
-
MemoryBrokerCapabilities,
|
|
9
|
-
MemoryBrokerContext,
|
|
10
|
-
MemoryQuery,
|
|
11
|
-
MemorySearchResult,
|
|
12
|
-
MemoryStats,
|
|
13
|
-
UpdateMemoryInput,
|
|
14
|
-
} from "./types";
|
|
15
|
-
import type { MemoryBroker } from "./memory-broker-contract";
|
|
16
|
-
import {
|
|
17
|
-
MemoryBrokerContractError,
|
|
18
|
-
normalizeMemory,
|
|
19
|
-
normalizeMemoryBrokerCapabilities,
|
|
20
|
-
normalizeMemorySearchResult,
|
|
21
|
-
} from "./memory-broker-contract";
|
|
1
|
+
import type { HttpMemoryBrokerConfig } from "./types";
|
|
2
|
+
import { MemoryBrokerContractError } from "./memory-broker-contract";
|
|
3
|
+
import { AbstractMemoryBroker, DEFAULT_MEMORY_BROKER_TIMEOUT_MS } from "./memory-broker-base";
|
|
22
4
|
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
23
5
|
|
|
24
|
-
|
|
6
|
+
export { normalizeContextPackage } from "./memory-broker-base";
|
|
25
7
|
|
|
26
|
-
export class HttpMemoryBroker
|
|
8
|
+
export class HttpMemoryBroker extends AbstractMemoryBroker {
|
|
9
|
+
protected readonly label = "http";
|
|
27
10
|
private readonly baseUrl: string;
|
|
28
11
|
private readonly timeoutMs: number;
|
|
29
12
|
|
|
30
13
|
constructor(private readonly config: HttpMemoryBrokerConfig) {
|
|
14
|
+
super();
|
|
31
15
|
this.baseUrl = config.url.replace(/\/+$/, "");
|
|
32
|
-
this.timeoutMs = config.timeoutMs ??
|
|
16
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_MEMORY_BROKER_TIMEOUT_MS;
|
|
33
17
|
}
|
|
34
18
|
|
|
35
|
-
async
|
|
36
|
-
return normalizeMemoryBrokerCapabilities(await this.call("capabilities", {}));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async create(input: CreateMemoryInput, ctx: MemoryBrokerContext): Promise<Memory> {
|
|
40
|
-
return normalizeMemory(await this.call("create", { input, ctx }), { now: ctx.now });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async get(id: string, ctx: MemoryBrokerContext): Promise<Memory | null> {
|
|
44
|
-
const value = await this.call("get", { id, ctx });
|
|
45
|
-
return value === null ? null : normalizeMemory(value, { now: ctx.now });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async search(query: MemoryQuery, ctx: MemoryBrokerContext): Promise<MemorySearchResult> {
|
|
49
|
-
return normalizeMemorySearchResult(await this.call("search", { query, ctx }), { now: ctx.now });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async update(id: string, patch: UpdateMemoryInput, ctx: MemoryBrokerContext): Promise<Memory> {
|
|
53
|
-
return normalizeMemory(await this.call("update", { id, patch, ctx }), { now: ctx.now });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async delete(id: string, ctx: MemoryBrokerContext): Promise<void> {
|
|
57
|
-
await this.call("delete", { id, ctx });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async stats(ctx: MemoryBrokerContext): Promise<MemoryStats> {
|
|
61
|
-
return normalizeStats(await this.call("stats", { ctx }));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async assemble(request: ContextPackageRequest, ctx: MemoryBrokerContext): Promise<ContextPackage> {
|
|
65
|
-
return normalizeContextPackage(await this.call("assemble", { request, ctx }), ctx);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async markInjected(agentId: string, memoryIds: string[], ctx: MemoryBrokerContext): Promise<void> {
|
|
69
|
-
await this.call("mark-injected", { agentId, memoryIds, ctx });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async clearActive(agentId: string, reason: ActiveMemoryClearReason, ctx: MemoryBrokerContext): Promise<void> {
|
|
73
|
-
await this.call("clear-active", { agentId, reason, ctx });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async listActive(agentId: string, ctx: MemoryBrokerContext): Promise<Memory[]> {
|
|
77
|
-
return normalizeMemoryList(await this.call("list-active", { agentId, ctx }), ctx);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private async call(operation: string, body: Record<string, unknown>): Promise<unknown> {
|
|
19
|
+
protected async call(operation: string, body: Record<string, unknown>): Promise<unknown> {
|
|
81
20
|
const controller = new AbortController();
|
|
82
21
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
83
22
|
try {
|
|
@@ -88,12 +27,12 @@ export class HttpMemoryBroker implements MemoryBroker {
|
|
|
88
27
|
signal: controller.signal,
|
|
89
28
|
});
|
|
90
29
|
const text = await res.text();
|
|
91
|
-
const payload = parseJson(text, operation);
|
|
30
|
+
const payload = this.parseJson(text, operation);
|
|
92
31
|
if (!res.ok) {
|
|
93
32
|
const detail = isRecord(payload) && typeof payload.error === "string" ? payload.error : text.trim();
|
|
94
33
|
throw new MemoryBrokerContractError(`http memory broker ${operation} failed: ${res.status}${detail ? ` ${detail}` : ""}`);
|
|
95
34
|
}
|
|
96
|
-
return unwrapResult(payload);
|
|
35
|
+
return this.unwrapResult(payload);
|
|
97
36
|
} catch (error) {
|
|
98
37
|
if (error instanceof MemoryBrokerContractError) throw error;
|
|
99
38
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
@@ -114,72 +53,3 @@ export class HttpMemoryBroker implements MemoryBroker {
|
|
|
114
53
|
return headers;
|
|
115
54
|
}
|
|
116
55
|
}
|
|
117
|
-
|
|
118
|
-
function parseJson(text: string, operation: string): unknown {
|
|
119
|
-
if (!text.trim()) return null;
|
|
120
|
-
try {
|
|
121
|
-
return JSON.parse(text);
|
|
122
|
-
} catch {
|
|
123
|
-
throw new MemoryBrokerContractError(`http memory broker ${operation} returned invalid JSON`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function unwrapResult(payload: unknown): unknown {
|
|
128
|
-
if (isRecord(payload) && Object.hasOwn(payload, "result")) return payload.result;
|
|
129
|
-
return payload;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function normalizeContextPackage(value: unknown, ctx: MemoryBrokerContext): ContextPackage {
|
|
133
|
-
const record = requireRecord(value, "context package");
|
|
134
|
-
const rawMemories = record.memories;
|
|
135
|
-
if (!Array.isArray(rawMemories)) throw new MemoryBrokerContractError("context package.memories must be an array");
|
|
136
|
-
return {
|
|
137
|
-
memories: rawMemories.map((item) => {
|
|
138
|
-
const packaged = requireRecord(item, "packaged memory");
|
|
139
|
-
const priority = packaged.priority;
|
|
140
|
-
if (priority !== 1 && priority !== 2 && priority !== 3) throw new MemoryBrokerContractError("packaged memory.priority must be 1, 2, or 3");
|
|
141
|
-
return {
|
|
142
|
-
memory: normalizeMemory(packaged.memory, { now: ctx.now }),
|
|
143
|
-
reason: typeof packaged.reason === "string" ? packaged.reason : "broker selected",
|
|
144
|
-
priority,
|
|
145
|
-
score: typeof packaged.score === "number" && Number.isFinite(packaged.score) ? packaged.score : undefined,
|
|
146
|
-
};
|
|
147
|
-
}),
|
|
148
|
-
estimatedTokens: typeof record.estimatedTokens === "number" && Number.isFinite(record.estimatedTokens) ? record.estimatedTokens : 0,
|
|
149
|
-
rolePrompt: typeof record.rolePrompt === "string" ? record.rolePrompt : undefined,
|
|
150
|
-
recentContext: typeof record.recentContext === "string" ? record.recentContext : undefined,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function normalizeMemoryList(value: unknown, ctx: MemoryBrokerContext): Memory[] {
|
|
155
|
-
if (Array.isArray(value)) return value.map((item) => normalizeMemory(item, { now: ctx.now }));
|
|
156
|
-
if (isRecord(value) && Array.isArray(value.memories)) return value.memories.map((item) => normalizeMemory(item, { now: ctx.now }));
|
|
157
|
-
throw new MemoryBrokerContractError("memory list-active result must be an array or object with memories array");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function normalizeStats(value: unknown): MemoryStats {
|
|
161
|
-
const record = requireRecord(value, "memory stats");
|
|
162
|
-
return {
|
|
163
|
-
total: numberField(record.total, "memory stats.total"),
|
|
164
|
-
byType: objectField(record.byType, "memory stats.byType"),
|
|
165
|
-
byScope: objectField(record.byScope, "memory stats.byScope"),
|
|
166
|
-
bySensitivity: objectField(record.bySensitivity, "memory stats.bySensitivity"),
|
|
167
|
-
} as MemoryStats;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function objectField(value: unknown, field: string): Record<string, number> {
|
|
171
|
-
const record = requireRecord(value, field);
|
|
172
|
-
const out: Record<string, number> = {};
|
|
173
|
-
for (const [key, count] of Object.entries(record)) out[key] = numberField(count, `${field}.${key}`);
|
|
174
|
-
return out;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function numberField(value: unknown, field: string): number {
|
|
178
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new MemoryBrokerContractError(`${field} must be a number`);
|
|
179
|
-
return value;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function requireRecord(value: unknown, field: string): Record<string, unknown> {
|
|
183
|
-
if (!isRecord(value)) throw new MemoryBrokerContractError(`${field} must be an object`);
|
|
184
|
-
return value;
|
|
185
|
-
}
|
package/src/routes.ts
CHANGED
|
@@ -139,6 +139,7 @@ import { createToken, deleteTokenProfile, getToken, getTokenProfile, listTokenPr
|
|
|
139
139
|
import {
|
|
140
140
|
getConnector,
|
|
141
141
|
listConnectors,
|
|
142
|
+
markConnectorUnreachable,
|
|
142
143
|
readConnectorConfig,
|
|
143
144
|
registerConnectorManifest,
|
|
144
145
|
runConnectorAction,
|
|
@@ -2197,7 +2198,7 @@ function restartSpawnParamsForAgent(
|
|
|
2197
2198
|
const policy = policyName ? getSpawnPolicy(policyName) : null;
|
|
2198
2199
|
const requestedBy = opts.resumeId ? "dashboard-resume" : "dashboard-restart";
|
|
2199
2200
|
if (policy) {
|
|
2200
|
-
const params = { ...
|
|
2201
|
+
const params = { ...buildManagedSpawnParams(policy.value, requestId, { createdBy: "managed-agent" }), agentId: agent.id, requestedBy };
|
|
2201
2202
|
return opts.resumeId ? withClaudeResumeParams(params, opts.resumeId, agent.id) : params;
|
|
2202
2203
|
}
|
|
2203
2204
|
|
|
@@ -4811,11 +4812,6 @@ function policyStatusPayload(policy: SpawnPolicy) {
|
|
|
4811
4812
|
};
|
|
4812
4813
|
}
|
|
4813
4814
|
|
|
4814
|
-
function managedSpawnParams(policy: SpawnPolicy, requestId: string): Record<string, unknown> {
|
|
4815
|
-
return buildManagedSpawnParams(policy, requestId, { createdBy: "managed-agent" });
|
|
4816
|
-
}
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
4815
|
function requirePolicyAndOrchestrator(name: string): { policy: SpawnPolicy; orch: NonNullable<ReturnType<typeof getOrchestrator>> } | Response {
|
|
4820
4816
|
const entry = getSpawnPolicy(name);
|
|
4821
4817
|
if (!entry) return error("spawn policy not found", 404);
|
|
@@ -4861,7 +4857,7 @@ function enqueuePolicyStart(policy: SpawnPolicy, reason: string): Command | Resp
|
|
|
4861
4857
|
target: orch.agentId,
|
|
4862
4858
|
correlationId: requestId,
|
|
4863
4859
|
params: {
|
|
4864
|
-
...
|
|
4860
|
+
...buildManagedSpawnParams(policy, requestId, { createdBy: "managed-agent" }),
|
|
4865
4861
|
reason,
|
|
4866
4862
|
orchestratorId: orch.id,
|
|
4867
4863
|
},
|
|
@@ -4875,7 +4871,7 @@ function enqueuePolicyStop(policy: SpawnPolicy, action: "shutdown" | "restart",
|
|
|
4875
4871
|
if (!orch) return error("orchestrator not found", 404);
|
|
4876
4872
|
const state = getManagedAgentState(policy.name);
|
|
4877
4873
|
const restartRequestId = action === "restart" ? spawnRequestId() : undefined;
|
|
4878
|
-
const restartSpawn = restartRequestId ?
|
|
4874
|
+
const restartSpawn = restartRequestId ? buildManagedSpawnParams(policy, restartRequestId, { createdBy: "managed-agent" }) : undefined;
|
|
4879
4875
|
const nextState = upsertManagedAgentState({
|
|
4880
4876
|
policyName: policy.name,
|
|
4881
4877
|
status: "stopping",
|
|
@@ -5784,6 +5780,13 @@ const postConnectorCall: Handler = async (req, params) => {
|
|
|
5784
5780
|
headers: { "content-type": res.headers.get("content-type") || "application/json" },
|
|
5785
5781
|
});
|
|
5786
5782
|
} catch (e) {
|
|
5783
|
+
// A connection error (refused/reset/DNS) means the advertised endpoint is
|
|
5784
|
+
// genuinely down — reconcile the stale running:true so the dashboard stops
|
|
5785
|
+
// claiming the connector is healthy. A timeout (AbortError) may just be a
|
|
5786
|
+
// slow daemon, so leave its state alone.
|
|
5787
|
+
if ((e as Error).name !== "AbortError") {
|
|
5788
|
+
markConnectorUnreachable(params.id!, `endpoint unreachable: ${(e as Error).message}`);
|
|
5789
|
+
}
|
|
5787
5790
|
return error(`failed to reach connector: ${(e as Error).message}`, 502);
|
|
5788
5791
|
}
|
|
5789
5792
|
};
|