agent-relay-server 0.121.4 → 0.121.5
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/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.121.
|
|
5
|
+
"version": "0.121.5",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.121.
|
|
3
|
+
"version": "0.121.5",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"CONTRIBUTING.md"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"agent-relay-channels-host": "0.121.
|
|
40
|
+
"agent-relay-channels-host": "0.121.5",
|
|
41
41
|
"agent-relay-providers": "0.104.3",
|
|
42
42
|
"agent-relay-sdk": "0.2.113",
|
|
43
43
|
"ajv": "^8.20.0"
|
package/src/bus.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Server, ServerWebSocket } from "bun";
|
|
2
|
-
import { ValidationError, getAgent, getDb, heartbeat, markReady, mergeAgentMeta, orphanTasksForAgent,
|
|
2
|
+
import { ValidationError, getAgent, getDb, heartbeat, markReady, mergeAgentMeta, orphanTasksForAgent, setStatus, validateAgentSession } from "./db";
|
|
3
3
|
import { getOutboxCursor, hasReplayGap, replayEventsSlice, type BusEvent } from "./bus-outbox";
|
|
4
4
|
import { projectAgentStatusData } from "./agent-card-projection";
|
|
5
5
|
import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
|
|
@@ -127,9 +127,11 @@ export function expireStaleBusAgents(graceMs = busStaleGraceMs(), lagGraceMs = 0
|
|
|
127
127
|
const rows = getDb().query("SELECT id FROM agents WHERE status = 'stale' AND last_seen < ? AND id NOT IN ('user', 'system')").all(cutoff) as Array<{ id: string }>;
|
|
128
128
|
const orphanedTasks: Task[] = [];
|
|
129
129
|
for (const row of rows) {
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
130
|
+
// #1051 — bus-grace staleness is a liveness observation, not a credential fact: the
|
|
131
|
+
// process may still be alive (relay stall / network blip). Mark offline for routing,
|
|
132
|
+
// but do NOT revoke the token — a reconnect accepts the EXISTING token and re-marks
|
|
133
|
+
// live. The #484 restart-strand revoke now fires on instance takeover in registerAgent
|
|
134
|
+
// (a fresh instanceId → epoch bump), never on a heartbeat lapse.
|
|
133
135
|
setStatus(row.id, "offline");
|
|
134
136
|
emitAgentStatusEvent(row.id);
|
|
135
137
|
// #1040 — the stale sweep is the terminal busy→offline edge for a runner that dropped without a
|
package/src/db/agents.ts
CHANGED
|
@@ -505,10 +505,13 @@ export function reapStaleAgents(ttlMs: number = STALE_TTL_MS, lagGraceMs = 0): s
|
|
|
505
505
|
)
|
|
506
506
|
.all(cutoff) as any[];
|
|
507
507
|
for (const row of rows) {
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
|
|
508
|
+
// #1051 — liveness ≠ credential validity. A lapsed heartbeat is a transient,
|
|
509
|
+
// recoverable observation (the relay may have stalled, or the network blipped);
|
|
510
|
+
// it marks status only and must NOT void a still-unexpired token. On reconnect the
|
|
511
|
+
// relay accepts the agent's EXISTING token and re-marks it live. The #484 restart-
|
|
512
|
+
// strand invariant (an orphaned prior-instance token must not stay usable) is now
|
|
513
|
+
// enforced on instance takeover instead — see revokeSupersededRuntimeTokens, fired
|
|
514
|
+
// from registerAgent when a fresh process re-registers under a new instanceId.
|
|
512
515
|
closeOpenPairsForAgent(row.id, now);
|
|
513
516
|
}
|
|
514
517
|
return rows.map((r: any) => r.id);
|
|
@@ -589,6 +592,28 @@ export function deleteAgent(id: string): { ok: boolean; error?: string } {
|
|
|
589
592
|
export function revokeRuntimeTokensForAgent(agentId: string, now = Date.now(), reason: TokenRevokeReason = "agent-removed"): string[] {
|
|
590
593
|
const row = getDb().query("SELECT meta FROM agents WHERE id = ?").get(agentId) as { meta?: string } | undefined;
|
|
591
594
|
const jtis = runtimeTokenJtisFromMeta(parseJson(row?.meta ?? "{}", {}));
|
|
595
|
+
return revokeProviderRuntimeTokensByJti(jtis, reason, now);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// #1051 — the #484 restart-strand invariant, re-homed from heartbeat-lapse to instance
|
|
599
|
+
// takeover. Staleness (a missed heartbeat) no longer revokes anything; instead, when a
|
|
600
|
+
// FRESH runner process re-registers under an existing agent id — a new instanceId, which
|
|
601
|
+
// bumps the agent epoch (see upsertAgent) — the PRIOR instance's runtime tokens are
|
|
602
|
+
// revoked "stale" here. "stale" (not a hard kill) keeps them re-mintable via the
|
|
603
|
+
// orchestrator (#484), so a live process that merely rotated instances still self-heals,
|
|
604
|
+
// while an orphaned prior-instance token can no longer authenticate a request directly.
|
|
605
|
+
// The incoming instance's own token (already stamped into the NEW meta) is preserved: a
|
|
606
|
+
// same-instance reconnect (relay-restart / lag recovery) keeps its instanceId, does not
|
|
607
|
+
// bump the epoch, and so never reaches this path — its token stays valid untouched.
|
|
608
|
+
export function revokeSupersededRuntimeTokens(prior: AgentCard, current: AgentCard, now = Date.now()): string[] {
|
|
609
|
+
const priorJtis = runtimeTokenJtisFromMeta((prior.meta ?? {}) as Record<string, unknown>);
|
|
610
|
+
if (priorJtis.length === 0) return [];
|
|
611
|
+
const keep = new Set(runtimeTokenJtisFromMeta((current.meta ?? {}) as Record<string, unknown>));
|
|
612
|
+
const superseded = priorJtis.filter((jti) => !keep.has(jti));
|
|
613
|
+
return revokeProviderRuntimeTokensByJti(superseded, "stale", now);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function revokeProviderRuntimeTokensByJti(jtis: string[], reason: TokenRevokeReason, now: number): string[] {
|
|
592
617
|
if (jtis.length === 0) return [];
|
|
593
618
|
const revokedAt = Math.floor(now / 1000);
|
|
594
619
|
const placeholders = jtis.map(() => "?").join(",");
|
package/src/managed-policy.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getManifest } from "agent-relay-providers";
|
|
2
|
+
import { DEFAULT_USER_ID } from "agent-relay-sdk";
|
|
2
3
|
import { getAgentProfile } from "./config-store";
|
|
3
|
-
import { withResolvedProvisioning } from "./db";
|
|
4
|
+
import { resolveProjectForCwd, resolveScopedAnchor, withResolvedProvisioning } from "./db";
|
|
5
|
+
import { composeSpawnInstructionsWithEvents } from "./project-instructions";
|
|
4
6
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
5
7
|
import { buildSpawnCommand, resolveSpawnModelParams } from "./spawn-command";
|
|
6
8
|
import { applyDefaultSharedMcp } from "./shared-mcp";
|
|
@@ -10,11 +12,19 @@ function managedPolicyPromptMode(policy: SpawnPolicy): "prompt" | "append-system
|
|
|
10
12
|
return getManifest(policy.provider)?.launch?.managedPolicyPrompt?.alwaysOn ?? "prompt";
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The always-on keepalive prompt an `append-system-prompt`-mode provider (Claude) carries as a
|
|
17
|
+
* system-prompt append (Codex uses the launch `prompt` channel instead — see
|
|
18
|
+
* `managedPolicyLaunchPrompt`). Returns undefined when it does not apply. #1052 folds this into
|
|
19
|
+
* `composeSpawnInstructionsWithEvents` as the caller append so there is a SINGLE ordered
|
|
20
|
+
* `--append-system-prompt` block (profile role charter + project knowledge + this keepalive),
|
|
21
|
+
* instead of a second stray `--append-system-prompt` arg that could stack on the composed one.
|
|
22
|
+
*/
|
|
23
|
+
export function managedPolicyAppendSystemPrompt(policy: SpawnPolicy): string | undefined {
|
|
14
24
|
if (policy.mode === "always-on" && policy.prompt?.trim() && managedPolicyPromptMode(policy) === "append-system-prompt") {
|
|
15
|
-
return
|
|
25
|
+
return policy.prompt.trim();
|
|
16
26
|
}
|
|
17
|
-
return
|
|
27
|
+
return undefined;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
export function managedPolicyLaunchPrompt(policy: SpawnPolicy): string | undefined {
|
|
@@ -33,10 +43,35 @@ interface ManagedSpawnContext {
|
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
export function buildManagedSpawnParams(policy: SpawnPolicy, requestId: string, ctx: ManagedSpawnContext): Record<string, unknown> {
|
|
36
|
-
const providerArgs = managedPolicyProviderArgs(policy);
|
|
37
46
|
const prompt = managedPolicyLaunchPrompt(policy);
|
|
38
47
|
const resolvedProfile = withResolvedProvisioning(policy.profile ? getAgentProfile(policy.profile)?.value : undefined, policy.provider);
|
|
39
48
|
const agentProfile = resolvedProfile ? applyDefaultSharedMcp(resolvedProfile) : undefined;
|
|
49
|
+
|
|
50
|
+
// #1052 — run the SAME instruction projection a fresh interactive spawn gets
|
|
51
|
+
// (composeSpawnInstructionsWithEvents, the single home in spawn-agent.ts), so a MANAGED agent
|
|
52
|
+
// (e.g. the always-on coordinator) is onboarded IDENTICALLY on its first launch AND on every
|
|
53
|
+
// restart / deploy-bounce relaunch — this builder feeds both `spawnAgent` and `restartAgent`.
|
|
54
|
+
// Previously the managed path forwarded only `agentProfile` (for provisioning) + providerArgs
|
|
55
|
+
// and silently dropped the profile ROLE CHARTER (`instructions.system`/`append`), project-
|
|
56
|
+
// knowledge preamble, and scoped {profile|user} memory, so a restarted coordinator reverted to
|
|
57
|
+
// provider-default IC behavior until a human re-injected the role file (#1052). The always-on
|
|
58
|
+
// keepalive prompt folds in as the caller append → one ordered `--append-system-prompt` block,
|
|
59
|
+
// no double-inject. The projection is deterministic (sorted + deduped), so re-running it on a
|
|
60
|
+
// native-resume relaunch reproduces a byte-identical block rather than stacking a second copy.
|
|
61
|
+
const callerProject = resolveProjectForCwd(policy.cwd) ?? undefined;
|
|
62
|
+
const projectInstructions = agentProfile?.projectInstructions;
|
|
63
|
+
const profileAnchor = policy.profile ? resolveScopedAnchor("profile", policy.profile) ?? undefined : undefined;
|
|
64
|
+
const userAnchor = resolveScopedAnchor("user", DEFAULT_USER_ID) ?? undefined;
|
|
65
|
+
const alwaysOnAppend = managedPolicyAppendSystemPrompt(policy);
|
|
66
|
+
const composed = composeSpawnInstructionsWithEvents({
|
|
67
|
+
...(callerProject ? { callerProject } : {}),
|
|
68
|
+
...(projectInstructions ? { projectInstructions } : {}),
|
|
69
|
+
...(profileAnchor ? { profileAnchor } : {}),
|
|
70
|
+
...(userAnchor ? { userAnchor } : {}),
|
|
71
|
+
...(agentProfile ? { profile: agentProfile } : {}),
|
|
72
|
+
...(alwaysOnAppend ? { callerSystemPromptAppend: alwaysOnAppend } : {}),
|
|
73
|
+
});
|
|
74
|
+
|
|
40
75
|
return buildSpawnCommand({
|
|
41
76
|
provider: policy.provider,
|
|
42
77
|
orchestratorId: policy.orchestratorId,
|
|
@@ -51,8 +86,10 @@ export function buildManagedSpawnParams(policy: SpawnPolicy, requestId: string,
|
|
|
51
86
|
capabilities: policy.capabilities,
|
|
52
87
|
approvalMode: policy.permissionMode,
|
|
53
88
|
permissionMode: policy.permissionMode,
|
|
54
|
-
providerArgs,
|
|
89
|
+
providerArgs: policy.providerArgs,
|
|
55
90
|
prompt: prompt || undefined,
|
|
91
|
+
...(composed.systemPromptAppend ? { systemPromptAppend: composed.systemPromptAppend } : {}),
|
|
92
|
+
...(composed.relayInjectionEvents.length ? { relayInjectionEvents: composed.relayInjectionEvents } : {}),
|
|
56
93
|
headless: true,
|
|
57
94
|
policyName: policy.name,
|
|
58
95
|
spawnRequestId: requestId,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// status broadcast, the single timeline note, the spawned-child parent-wake, and
|
|
11
11
|
// the first-registration audit row.
|
|
12
12
|
import { errMessage, isReservedAgentId } from "agent-relay-sdk";
|
|
13
|
-
import { ValidationError, createActivityEvent, deliverableTaskIdsBySpawnRequestId, getAgent, getTaskDetail, mergeAgentMeta, readSpawnReportVerbosity, seedSpawnPromptMirror, seedSpawnReplyObligation, upsertAgent } from "../db";
|
|
13
|
+
import { ValidationError, createActivityEvent, deliverableTaskIdsBySpawnRequestId, getAgent, getTaskDetail, mergeAgentMeta, readSpawnReportVerbosity, revokeSupersededRuntimeTokens, seedSpawnPromptMirror, seedSpawnReplyObligation, upsertAgent } from "../db";
|
|
14
14
|
import { emitAgentStatus, emitNewMessage } from "../sse";
|
|
15
15
|
import { noteAgentTimelineEvent } from "../compaction-watch";
|
|
16
16
|
import { agentProviderTimelineEvent } from "../provider-timeline-event";
|
|
@@ -93,6 +93,17 @@ export function registerAgent(input: RegisterAgentInput, ctx: AuthContext): Agen
|
|
|
93
93
|
if (existing) delete registrationInput.label;
|
|
94
94
|
const agent = upsertAgent(registrationInput);
|
|
95
95
|
|
|
96
|
+
// #1051/#484 — instance takeover is where the restart-strand invariant fires now that a
|
|
97
|
+
// heartbeat lapse no longer revokes anything. When a FRESH runner process re-registers
|
|
98
|
+
// under this agent id (new instanceId → epoch bump), revoke the PRIOR instance's runtime
|
|
99
|
+
// tokens "stale": they can no longer authenticate a request directly, yet stay re-mintable
|
|
100
|
+
// via the orchestrator (#484). A same-instance reconnect keeps its instanceId, so the epoch
|
|
101
|
+
// is unchanged and the live token is left valid — the whole point of #1051 (in-band recovery
|
|
102
|
+
// after lag/relay-restart without a re-mint).
|
|
103
|
+
if (existing?.instanceId && agent.instanceId && existing.instanceId !== agent.instanceId) {
|
|
104
|
+
revokeSupersededRuntimeTokens(existing, agent);
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
// Managed came-up-running reconcile — no-op for non-managed agents (the guard lives
|
|
97
108
|
// in markManagedAgentRunning). Clears backoff, flushes the policy queue, transitions
|
|
98
109
|
// managed state to running.
|