agent-relay-server 0.102.1 → 0.103.4
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 +1 -1
- package/package.json +4 -2
- package/public/assets/MessageBubble-BpiTbbnp.js +4 -0
- package/public/assets/{MessageBubble-Dppo7pil.js.map → MessageBubble-BpiTbbnp.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-DO5OY2qa.js → PlanGraphPanel-k8Aa98g5.js} +2 -2
- package/public/assets/{PlanGraphPanel-DO5OY2qa.js.map → PlanGraphPanel-k8Aa98g5.js.map} +1 -1
- package/public/assets/{activity-BhuOs5ha.js → activity-CxKi5Erb.js} +2 -2
- package/public/assets/{activity-BhuOs5ha.js.map → activity-CxKi5Erb.js.map} +1 -1
- package/public/assets/agent-profiles-DMZXdN6K.js +2 -0
- package/public/assets/agent-profiles-DMZXdN6K.js.map +1 -0
- package/public/assets/agents-BrvpH3gn.js +2 -0
- package/public/assets/agents-BrvpH3gn.js.map +1 -0
- package/public/assets/{analytics-BxGRljQc.js → analytics-1fvh-9ok.js} +2 -2
- package/public/assets/{analytics-BxGRljQc.js.map → analytics-1fvh-9ok.js.map} +1 -1
- package/public/assets/automation-BCgX5WOH.js +2 -0
- package/public/assets/automation-BCgX5WOH.js.map +1 -0
- package/public/assets/{branch-state-badge-olhanssg.js → branch-state-badge-DYA7bU0i.js} +2 -2
- package/public/assets/{branch-state-badge-olhanssg.js.map → branch-state-badge-DYA7bU0i.js.map} +1 -1
- package/public/assets/{channels-yHkm1dux.js → channels-CK80oJYG.js} +2 -2
- package/public/assets/{channels-yHkm1dux.js.map → channels-CK80oJYG.js.map} +1 -1
- package/public/assets/chat-D_2PEz06.js +2 -0
- package/public/assets/chat-D_2PEz06.js.map +1 -0
- package/public/assets/{connectors-DpR0V2hy.js → connectors-ChGyht5m.js} +2 -2
- package/public/assets/{connectors-DpR0V2hy.js.map → connectors-ChGyht5m.js.map} +1 -1
- package/public/assets/display-D1xB8NJZ.js +3 -0
- package/public/assets/display-D1xB8NJZ.js.map +1 -0
- package/public/assets/{formatted-body-impl-BTeCfK4I.js → formatted-body-impl-BJ4WCF5J.js} +2 -2
- package/public/assets/{formatted-body-impl-BTeCfK4I.js.map → formatted-body-impl-BJ4WCF5J.js.map} +1 -1
- package/public/assets/index-BMO_Sblt.css +2 -0
- package/public/assets/index-CN7QZlMb.js +23 -0
- package/public/assets/index-CN7QZlMb.js.map +1 -0
- package/public/assets/{insights-C4aYlgsY.js → insights-BCZRV0hX.js} +2 -2
- package/public/assets/{insights-C4aYlgsY.js.map → insights-BCZRV0hX.js.map} +1 -1
- package/public/assets/{integrations-CEmYa5mW.js → integrations-C7UGs00U.js} +2 -2
- package/public/assets/{integrations-CEmYa5mW.js.map → integrations-C7UGs00U.js.map} +1 -1
- package/public/assets/maintenance-ChYno3Fm.js +2 -0
- package/public/assets/{maintenance-DVx3SiC_.js.map → maintenance-ChYno3Fm.js.map} +1 -1
- package/public/assets/{managed-agents-DUyOTdz1.js → managed-agents-DjxZQNIR.js} +4 -4
- package/public/assets/managed-agents-DjxZQNIR.js.map +1 -0
- package/public/assets/{markdown-preview-impl-D3hYfzGD.js → markdown-preview-impl-DFa5HF1H.js} +2 -2
- package/public/assets/{markdown-preview-impl-D3hYfzGD.js.map → markdown-preview-impl-DFa5HF1H.js.map} +1 -1
- package/public/assets/{memory-2IfPgcnN.js → memory-CTw6kadT.js} +2 -2
- package/public/assets/{memory-2IfPgcnN.js.map → memory-CTw6kadT.js.map} +1 -1
- package/public/assets/{messages-CkOwABhw.js → messages-Cjr816D_.js} +2 -2
- package/public/assets/{messages-CkOwABhw.js.map → messages-Cjr816D_.js.map} +1 -1
- package/public/assets/{orchestrators-CEKO3eQ1.js → orchestrators-CdmFKA7K.js} +2 -2
- package/public/assets/{orchestrators-CEKO3eQ1.js.map → orchestrators-CdmFKA7K.js.map} +1 -1
- package/public/assets/overview-DBOnjfpP.js +2 -0
- package/public/assets/{overview-D9BxevAq.js.map → overview-DBOnjfpP.js.map} +1 -1
- package/public/assets/{pairs-CqT4D1uL.js → pairs-BkcdKdF7.js} +2 -2
- package/public/assets/{pairs-CqT4D1uL.js.map → pairs-BkcdKdF7.js.map} +1 -1
- package/public/assets/{projects-C_zwR-Fn.js → projects-ClKbDQB1.js} +2 -2
- package/public/assets/{projects-C_zwR-Fn.js.map → projects-ClKbDQB1.js.map} +1 -1
- package/public/assets/registry-Bsf3RtOO.js +2 -0
- package/public/assets/registry-Bsf3RtOO.js.map +1 -0
- package/public/assets/{security-8a5_4U3S.js → security-Iyu5bhJw.js} +2 -2
- package/public/assets/{security-8a5_4U3S.js.map → security-Iyu5bhJw.js.map} +1 -1
- package/public/assets/{select-s4BE-T62.js → select-Bi6tNJDx.js} +2 -2
- package/public/assets/{select-s4BE-T62.js.map → select-Bi6tNJDx.js.map} +1 -1
- package/public/assets/settings-CbHp2F9r.js +6 -0
- package/public/assets/settings-CbHp2F9r.js.map +1 -0
- package/public/assets/store-zeyDiSKE.js +9 -0
- package/public/assets/store-zeyDiSKE.js.map +1 -0
- package/public/assets/{tasks-Bcr17zR3.js → tasks-CPlf-5Tr.js} +2 -2
- package/public/assets/{tasks-Bcr17zR3.js.map → tasks-CPlf-5Tr.js.map} +1 -1
- package/public/assets/teams-D2uUNhlY.js +2 -0
- package/public/assets/{teams-B_HSkU9T.js.map → teams-D2uUNhlY.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-Ctp4csJ3.js → terminal-viewer-impl-uRzbrlXy.js} +2 -2
- package/public/assets/{terminal-viewer-impl-Ctp4csJ3.js.map → terminal-viewer-impl-uRzbrlXy.js.map} +1 -1
- package/public/assets/types-DorDDvEb.js +2 -0
- package/public/assets/types-DorDDvEb.js.map +1 -0
- package/public/assets/{user-avatar-RliaJcaG.js → user-avatar-GFWMuAGK.js} +2 -2
- package/public/assets/{user-avatar-RliaJcaG.js.map → user-avatar-GFWMuAGK.js.map} +1 -1
- package/public/assets/{work-queue-C0hpmlv8.js → work-queue-B30SNmuq.js} +2 -2
- package/public/assets/{work-queue-C0hpmlv8.js.map → work-queue-B30SNmuq.js.map} +1 -1
- package/public/assets/{workspaces-QXbQx_AG.js → workspaces-CSpmgbPT.js} +3 -3
- package/public/assets/{workspaces-QXbQx_AG.js.map → workspaces-CSpmgbPT.js.map} +1 -1
- package/public/index.html +5 -5
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +238 -16
- package/runner/src/config.ts +8 -3
- package/src/automations/validation.ts +3 -3
- package/src/cli/orchestrator.ts +7 -5
- package/src/cli/provider.ts +26 -10
- package/src/cli/upgrade.ts +5 -4
- package/src/commands-db.ts +1 -1
- package/src/config-store.ts +3 -3
- package/src/config.ts +3 -3
- package/src/context-advisory.ts +3 -3
- package/src/db/channels.ts +30 -2
- package/src/db/provider-quota-burn.ts +37 -5
- package/src/db/provider-quotas.ts +11 -2
- package/src/db/provisioning-registry.ts +49 -61
- package/src/managed-policy.ts +7 -2
- package/src/mcp/tools-spawn.ts +2 -2
- package/src/mcp-team-template-tools.ts +2 -2
- package/src/provider-catalog-store.ts +1 -1
- package/src/quota-band-transition.ts +59 -8
- package/src/recipe-validator.ts +4 -2
- package/src/routes/agent-profiles.ts +2 -2
- package/src/routes/agent-sessions.ts +18 -23
- package/src/routes/provider-config.ts +1 -1
- package/src/spawn-targets.ts +32 -4
- package/src/sse.ts +12 -1
- package/src/upgrade.ts +120 -44
- package/src/workspace-phase.ts +1 -0
- package/public/assets/MessageBubble-Dppo7pil.js +0 -4
- package/public/assets/agent-profiles-CT-vbfzd.js +0 -2
- package/public/assets/agent-profiles-CT-vbfzd.js.map +0 -1
- package/public/assets/agents-CLcgi8AD.js +0 -2
- package/public/assets/agents-CLcgi8AD.js.map +0 -1
- package/public/assets/automation-CE1i1zpw.js +0 -2
- package/public/assets/automation-CE1i1zpw.js.map +0 -1
- package/public/assets/chat-DHdFZvAg.js +0 -2
- package/public/assets/chat-DHdFZvAg.js.map +0 -1
- package/public/assets/display-DRiGi1zj.js +0 -3
- package/public/assets/display-DRiGi1zj.js.map +0 -1
- package/public/assets/index-CKhh9XRZ.css +0 -2
- package/public/assets/index-DeDosfVE.js +0 -23
- package/public/assets/index-DeDosfVE.js.map +0 -1
- package/public/assets/maintenance-DVx3SiC_.js +0 -2
- package/public/assets/managed-agents-DUyOTdz1.js.map +0 -1
- package/public/assets/overview-D9BxevAq.js +0 -2
- package/public/assets/registry-BLjwVXCW.js +0 -2
- package/public/assets/registry-BLjwVXCW.js.map +0 -1
- package/public/assets/settings-BR30UpbJ.js +0 -6
- package/public/assets/settings-BR30UpbJ.js.map +0 -1
- package/public/assets/store-DNbkrqbU.js +0 -9
- package/public/assets/store-DNbkrqbU.js.map +0 -1
- package/public/assets/teams-B_HSkU9T.js +0 -2
- package/public/assets/types-Tv67voFu.js +0 -2
- package/public/assets/types-Tv67voFu.js.map +0 -1
package/src/mcp/tools-spawn.ts
CHANGED
|
@@ -71,7 +71,7 @@ export const SPAWN_TOOLS: ToolDefinition[] = [
|
|
|
71
71
|
required: ["branch", "mode"],
|
|
72
72
|
additionalProperties: false,
|
|
73
73
|
},
|
|
74
|
-
waitForRegistrationMs: { type: "integer", minimum: 0, maximum: 30000, description: "How long to wait for the spawned agent to register before returning, so the response carries its resolved agent id (default
|
|
74
|
+
waitForRegistrationMs: { type: "integer", minimum: 0, maximum: 30000, description: "How long to wait for the spawned agent to register before returning, so the response carries its resolved agent id (default 0 = fire-and-forget; you will be PUSHED agent.ready when the child is ready)." },
|
|
75
75
|
},
|
|
76
76
|
required: ["provider"],
|
|
77
77
|
additionalProperties: false,
|
|
@@ -158,7 +158,7 @@ export async function relaySpawnAgent(auth: McpAuthContext, args: Record<string,
|
|
|
158
158
|
providerArgs: optionalStringArray(args.providerArgs, "providerArgs"),
|
|
159
159
|
spawnRequestId: optionalString(args.spawnRequestId, "spawnRequestId", 160),
|
|
160
160
|
requestedVia: "mcp",
|
|
161
|
-
waitForRegistrationMs: optionalNonNegativeInt(args.waitForRegistrationMs, "waitForRegistrationMs") ??
|
|
161
|
+
waitForRegistrationMs: optionalNonNegativeInt(args.waitForRegistrationMs, "waitForRegistrationMs") ?? 0,
|
|
162
162
|
resumeWorkspace: parseResumeWorkspace(args.resumeWorkspace),
|
|
163
163
|
};
|
|
164
164
|
const result = await spawnAgent(input, authContextFromMcp(auth));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { APPROVAL_MODES, VALID_AGENT_LIFECYCLES, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
|
|
1
|
+
import { APPROVAL_MODES, SPAWN_PROVIDERS, VALID_AGENT_LIFECYCLES, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
|
|
2
2
|
import { McpAuthError } from "./mcp-errors";
|
|
3
3
|
import { authContextFromMcp } from "./services/auth-context";
|
|
4
4
|
import { deleteTeamTemplate, getTeamTemplate, instantiateTeamTemplate, listTeamTemplates, saveTeamTemplate } from "./services/team-templates";
|
|
@@ -9,7 +9,7 @@ const COMPOSITION_ENTRY_SCHEMA = {
|
|
|
9
9
|
type: "object",
|
|
10
10
|
properties: {
|
|
11
11
|
label: { type: "string" },
|
|
12
|
-
provider: { type: "string", enum:
|
|
12
|
+
provider: { type: "string", enum: SPAWN_PROVIDERS },
|
|
13
13
|
model: { type: "string" },
|
|
14
14
|
effort: { type: "string" },
|
|
15
15
|
profile: { type: "string" },
|
|
@@ -138,7 +138,7 @@ export function mergeProviderCatalog(
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function normalizeOverrideInput(input: ProviderModelOverrideInput): ProviderModelOverrideInput {
|
|
141
|
-
if (!SPAWN_PROVIDERS.includes(input.provider)) throw new ValidationError(
|
|
141
|
+
if (!SPAWN_PROVIDERS.includes(input.provider)) throw new ValidationError(`provider must be one of: ${SPAWN_PROVIDERS.join(", ")}`);
|
|
142
142
|
const alias = input.alias.trim();
|
|
143
143
|
if (!alias) throw new ValidationError("model alias is required");
|
|
144
144
|
if (input.entry.alias !== alias) throw new ValidationError("model entry alias must match override alias");
|
|
@@ -14,17 +14,36 @@
|
|
|
14
14
|
// at most one per RECOVERY_MIN_INTERVAL_MS — degradation is the actionable
|
|
15
15
|
// signal; fleet-wake on recovery churn is suppressed.
|
|
16
16
|
//
|
|
17
|
+
// #798 — principled noisy-signal → discrete-state pipeline so a provider in a
|
|
18
|
+
// steady state stops re-emitting band-change notifications:
|
|
19
|
+
// 1. LOW-PASS (EMA): the raw penalty/slack jitters tick-to-tick (burnRatio
|
|
20
|
+
// recompute, shrinking timeToReset). An exponential moving average with a
|
|
21
|
+
// few-minute time constant is applied to the signal BEFORE it is thresholded
|
|
22
|
+
// into a band — killing the noise at the source, so a transient single-tick
|
|
23
|
+
// excursion never reaches the opposite band.
|
|
24
|
+
// 2. SCHMITT (applyHysteresis): enter/exit thresholds with a gap on the band
|
|
25
|
+
// STATE (not just the routing weight — that's where #756 fell short).
|
|
26
|
+
// 3. MIN-DWELL DEBOUNCE: the new band must hold DEBOUNCE_SAMPLES ticks before
|
|
27
|
+
// the human-facing notification fires.
|
|
28
|
+
// The idle-projection de-bias + position penalty (db/provider-quota-burn.ts,
|
|
29
|
+
// spawn-targets.ts) attack the same flap from the other end: an idle provider's
|
|
30
|
+
// band is now position-pinned, so the input the pipeline sees is already stable.
|
|
31
|
+
//
|
|
17
32
|
// Called from handleProviderQuotaAdvisory in quota-advisory.ts so the check
|
|
18
33
|
// runs on the same event hook as the utilization-threshold advisory — no
|
|
19
34
|
// parallel delivery channel.
|
|
20
35
|
import { emitRelayEvent } from "./events";
|
|
21
36
|
import { routeNotification } from "./notification-router";
|
|
22
37
|
import { getSpawnCapableAgentIds } from "./routing-policy-push";
|
|
23
|
-
import { getProviderBandInfo, windowRecommendedAction } from "./spawn-targets";
|
|
38
|
+
import { getProviderBandInfo, quotaBandFromSignal, windowRecommendedAction } from "./spawn-targets";
|
|
24
39
|
import type { ProviderBandInfo, ProviderBandWindow, QuotaBand } from "./spawn-targets";
|
|
25
40
|
import type { ProviderQuotaAdvisoryInput } from "./db/provider-quotas";
|
|
26
41
|
|
|
27
42
|
const DEBOUNCE_SAMPLES = 3;
|
|
43
|
+
// #798 — EMA time constant for the low-pass. "A few minutes": longer than the ~5-min poll-cadence
|
|
44
|
+
// tick jitter (so noise is smoothed), far shorter than the 5h/7d utilization trend (so a genuine
|
|
45
|
+
// quota move still surfaces within a poll or two). dt-based, so it is correct at any cadence.
|
|
46
|
+
const EMA_TAU_MS = 6 * 60_000;
|
|
28
47
|
// Penalty must drop this far below the band entry threshold before recovery is recognised.
|
|
29
48
|
// #760: widened 0.05 → 0.10. Live evidence (Claude 7d parked at avoidLargeAt with burn×1.4)
|
|
30
49
|
// showed the 0.05 deadband was too tight for real penalty jitter — it still ping-ponged
|
|
@@ -42,6 +61,22 @@ interface TransitionState {
|
|
|
42
61
|
pendingBand: QuotaBand | null;
|
|
43
62
|
pendingCount: number;
|
|
44
63
|
lastTransitionAt: number;
|
|
64
|
+
// #798 — low-pass state: EMA of the noisy penalty/slack signal, plus the tick time it was last
|
|
65
|
+
// updated (to size the dt-based smoothing factor). null until the first high-confidence sample.
|
|
66
|
+
emaPenalty: number | null;
|
|
67
|
+
emaSlack: number | null;
|
|
68
|
+
emaUpdatedAt: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// #798 — dt-based EMA blend factor: 1 − e^(−dt/τ). dt = 0 (or first sample) → seed verbatim.
|
|
72
|
+
function emaAlpha(dtMs: number): number {
|
|
73
|
+
if (!(dtMs > 0)) return 1;
|
|
74
|
+
return 1 - Math.exp(-dtMs / EMA_TAU_MS);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function emaStep(prev: number | null, value: number, dtMs: number): number {
|
|
78
|
+
if (prev === null) return value;
|
|
79
|
+
return prev + emaAlpha(dtMs) * (value - prev);
|
|
45
80
|
}
|
|
46
81
|
|
|
47
82
|
const bandState = new Map<string, TransitionState>();
|
|
@@ -73,17 +108,33 @@ export function processBandTransitions(input: ProviderQuotaAdvisoryInput): void
|
|
|
73
108
|
pendingBand: null,
|
|
74
109
|
pendingCount: 0,
|
|
75
110
|
lastTransitionAt: 0,
|
|
111
|
+
emaPenalty: null,
|
|
112
|
+
emaSlack: null,
|
|
113
|
+
emaUpdatedAt: 0,
|
|
76
114
|
};
|
|
77
115
|
|
|
78
|
-
// #
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
const
|
|
116
|
+
// #798 (layer 1) — low-pass the noisy penalty/slack BEFORE thresholding, then re-classify the
|
|
117
|
+
// smoothed signal into a band. A transient one-tick excursion is averaged out, so it never
|
|
118
|
+
// reaches (and re-emits) the opposite band; a sustained move still crosses within a poll or two.
|
|
119
|
+
const dtMs = state.emaUpdatedAt > 0 ? input.now - state.emaUpdatedAt : 0;
|
|
120
|
+
const emaPenalty = emaStep(state.emaPenalty, window.penalty, dtMs);
|
|
121
|
+
const emaSlack = window.slack === null ? state.emaSlack : emaStep(state.emaSlack, window.slack, dtMs);
|
|
122
|
+
state.emaPenalty = emaPenalty;
|
|
123
|
+
state.emaSlack = emaSlack;
|
|
124
|
+
state.emaUpdatedAt = input.now;
|
|
125
|
+
// Persist immediately so the low-pass state survives every early-continue below (the state is
|
|
126
|
+
// then mutated in place by reference — the EMA must carry across ticks to smooth excursions).
|
|
127
|
+
bandState.set(key, state);
|
|
128
|
+
const smoothedBand = quotaBandFromSignal(emaPenalty, emaSlack, info.quotaBands, info.preferSlackAt);
|
|
129
|
+
|
|
130
|
+
// #756 (layer 2): asymmetric hysteresis — recovery needs penalty HYSTERESIS_MARGIN below the
|
|
131
|
+
// entry threshold of the current band to prevent ping-pong at a boundary. #760 extends this with
|
|
132
|
+
// a slack deadband for the "prefer" boundary. Operates on the smoothed signal.
|
|
133
|
+
const effectiveBand = applyHysteresis(smoothedBand, emaPenalty, emaSlack, state.lastNotifiedBand, info.quotaBands, info.preferSlackAt);
|
|
82
134
|
|
|
83
135
|
if (effectiveBand === state.lastNotifiedBand) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
136
|
+
state.pendingBand = null;
|
|
137
|
+
state.pendingCount = 0;
|
|
87
138
|
continue;
|
|
88
139
|
}
|
|
89
140
|
|
package/src/recipe-validator.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MemoryType, Recipe, RecipeAgent, RecipeMemoryPolicy } from "./types";
|
|
2
2
|
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
3
|
-
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
3
|
+
import { errMessage, isRecord, isSpawnProvider, SPAWN_PROVIDERS } from "agent-relay-sdk";
|
|
4
4
|
import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
5
5
|
|
|
6
6
|
class RecipeValidationError extends Error {}
|
|
@@ -41,7 +41,9 @@ export function validateRecipe(value: unknown): Recipe {
|
|
|
41
41
|
function validateAgent(value: unknown): RecipeAgent {
|
|
42
42
|
if (!isRecord(value)) throw new RecipeValidationError("agent must be an object");
|
|
43
43
|
const provider = value.provider;
|
|
44
|
-
if (provider
|
|
44
|
+
if (!isSpawnProvider(provider)) {
|
|
45
|
+
throw new RecipeValidationError(`agent.provider must be one of: ${SPAWN_PROVIDERS.join(", ")}`);
|
|
46
|
+
}
|
|
45
47
|
const capabilities = stringArray(value.capabilities, "agent.capabilities");
|
|
46
48
|
if (capabilities.length === 0) throw new RecipeValidationError("agent.capabilities must not be empty");
|
|
47
49
|
const approvalMode = value.approvalMode;
|
|
@@ -5,7 +5,7 @@ import { deleteAgentProfile, getAgentProfile, listAgentProfiles, setAgentProfile
|
|
|
5
5
|
import { emitConfigChanged } from "../sse";
|
|
6
6
|
import { error, isRootCredentialRequest, json, normalizeConfigPathParam, parseBody, type Handler } from "./_shared";
|
|
7
7
|
import { getComponentAuth, hasComponentScope } from "../security";
|
|
8
|
-
import { isRecord } from "agent-relay-sdk";
|
|
8
|
+
import { isRecord, isSpawnProvider } from "agent-relay-sdk";
|
|
9
9
|
import { SPAWN_PROVIDERS, type AgentProfile, type SpawnProvider } from "../types";
|
|
10
10
|
|
|
11
11
|
export const getAgentProfilesRoute: Handler = () => json(listAgentProfiles());
|
|
@@ -29,7 +29,7 @@ export const getResolvedAgentProfileRoute: Handler = (req, params) => {
|
|
|
29
29
|
const entry = getAgentProfile(name);
|
|
30
30
|
if (!entry) return error("agent profile not found", 404);
|
|
31
31
|
const requestedProvider = new URL(req.url).searchParams.get("provider") ?? undefined;
|
|
32
|
-
const profileProvider = entry.value.provider
|
|
32
|
+
const profileProvider = isSpawnProvider(entry.value.provider) ? entry.value.provider : undefined;
|
|
33
33
|
const provider = optionalEnum(requestedProvider, "provider", SPAWN_PROVIDERS, profileProvider ?? "codex") as SpawnProvider;
|
|
34
34
|
const profile = withResolvedProvisioning(entry.value, provider);
|
|
35
35
|
return profile ? json({ provider, profile }) : error("agent profile not found", 404);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Auto-split from routes.ts (#299). Domain: agent-sessions.
|
|
2
|
-
import { RELAY_TOKEN_HEADER, isRecord } from "agent-relay-sdk";
|
|
2
|
+
import { RELAY_TOKEN_HEADER, isRecord, isSpawnProvider } from "agent-relay-sdk";
|
|
3
3
|
import { VALID_AGENT_ACTIONS, agentControlActionIcon, auditEvent, authAuditMetadata, authorizeAgentRoute, authorizeRoute, canViewAgentRoute, cleanAttachmentRefs, dashboardAttribution, emitCommand, error, json, metaString, parseBody, requestUserAddress, requestUserId, spawnRequestId, type AgentControlAction, type Handler } from "./_shared";
|
|
4
4
|
import { arbitrateDrive, currentDriver, drivingMessage, releaseDrive, type DriveArbitration } from "../agent-drive-arbiter";
|
|
5
5
|
import { ValidationError, getAgent, getAgentTimeline, getOrchestrator, markReady, mergeAgentMeta, sendMessage, withResolvedProvisioning } from "../db";
|
|
@@ -18,10 +18,9 @@ import { authContextFromRequest } from "../services/auth-context";
|
|
|
18
18
|
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent } from "../services/shutdown-agent";
|
|
19
19
|
import { type AgentCard, type SpawnApprovalMode } from "../types";
|
|
20
20
|
import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
21
|
+
import { getManifest } from "agent-relay-providers";
|
|
21
22
|
import { relayToken } from "../config";
|
|
22
23
|
|
|
23
|
-
const CLAUDE_RESUME_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
-
|
|
25
24
|
// 409 when a second human tries to drive an agent another user is already driving (#394). The
|
|
26
25
|
// blocked driver learns who holds the wheel so the UI can show "X is driving" + offer "request".
|
|
27
26
|
function driveBlockedResponse(arb: Extract<DriveArbitration, { allowed: false }>): Response {
|
|
@@ -61,7 +60,7 @@ function agentIsControlEligible(agent: AgentCard): boolean {
|
|
|
61
60
|
|
|
62
61
|
function agentCanReceiveControlAction(agent: AgentCard, action: AgentControlAction): boolean {
|
|
63
62
|
if (!agentIsControlEligible(agent)) return false;
|
|
64
|
-
if (action === "resume") return agentRuntimeProvider(agent)
|
|
63
|
+
if (action === "resume") return getManifest(agentRuntimeProvider(agent) ?? "")?.resume?.supported === true && (agent.status === "offline" || agent.status === "stale");
|
|
65
64
|
// Interrupt only makes sense while the provider is mid-turn, and only if the
|
|
66
65
|
// provider advertises it can be interrupted from the dashboard.
|
|
67
66
|
if (action === "interrupt") return agent.providerCapabilities?.liveSession?.interrupt === true && agent.status === "busy";
|
|
@@ -109,11 +108,11 @@ function restartSpawnParamsForAgent(
|
|
|
109
108
|
const requestedBy = opts.resumeId ? "dashboard-resume" : "dashboard-restart";
|
|
110
109
|
if (policy) {
|
|
111
110
|
const params = { ...buildManagedSpawnParams(policy.value, requestId, { createdBy: "managed-agent" }), agentId: agent.id, requestedBy };
|
|
112
|
-
return opts.resumeId ?
|
|
111
|
+
return opts.resumeId ? withResumeParams(params, opts.resumeId, agent.id) : params;
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
const provider = metaString(agent.meta, "provider");
|
|
116
|
-
if (provider
|
|
115
|
+
if (!isSpawnProvider(provider)) return undefined;
|
|
117
116
|
const cwd = metaString(agent.meta, "cwd") ?? orchestrator.baseDir;
|
|
118
117
|
const label = metaString(agent.meta, "label") ?? agent.label;
|
|
119
118
|
const approvalMode = metaString(agent.meta, "approvalMode") as SpawnApprovalMode | undefined;
|
|
@@ -155,48 +154,44 @@ function restartSpawnParamsForAgent(
|
|
|
155
154
|
requestedBy,
|
|
156
155
|
requestedAt: Date.now(),
|
|
157
156
|
});
|
|
158
|
-
return opts.resumeId ?
|
|
157
|
+
return opts.resumeId ? withResumeParams(params, opts.resumeId, agent.id) : params;
|
|
159
158
|
}
|
|
160
159
|
|
|
161
160
|
function spawnRequestIdForRestart(): string {
|
|
162
161
|
return generateSpawnRequestId();
|
|
163
162
|
}
|
|
164
163
|
|
|
165
|
-
function
|
|
164
|
+
function withResumeParams(params: Record<string, unknown>, resumeId: string, agentId: string): Record<string, unknown> {
|
|
166
165
|
return {
|
|
167
166
|
...params,
|
|
168
|
-
providerArgs:
|
|
167
|
+
providerArgs: providerArgsWithResume(metaStringArray(params as Record<string, unknown>, "providerArgs"), resumeId),
|
|
169
168
|
resumeOfAgentId: agentId,
|
|
170
169
|
claudeResumeId: resumeId,
|
|
171
170
|
};
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
function
|
|
173
|
+
function providerArgsWithResume(args: string[], resumeId: string): string[] {
|
|
175
174
|
const cleaned: string[] = [];
|
|
176
175
|
for (let i = 0; i < args.length; i += 1) {
|
|
177
176
|
const arg = args[i];
|
|
178
177
|
if (!arg) continue;
|
|
179
|
-
if (arg === "--resume") {
|
|
180
|
-
i += 1;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
178
|
+
if (arg === "--resume") { i += 1; continue; }
|
|
183
179
|
if (arg.startsWith("--resume=")) continue;
|
|
184
180
|
cleaned.push(arg);
|
|
185
181
|
}
|
|
186
182
|
return [...cleaned, "--resume", resumeId];
|
|
187
183
|
}
|
|
188
184
|
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
185
|
+
function latestResumeIdForAgent(agent: AgentCard): string | undefined {
|
|
186
|
+
const provider = agentRuntimeProvider(agent);
|
|
187
|
+
const resume = getManifest(provider ?? "")?.resume as { supported: boolean; idPattern?: string } | undefined;
|
|
188
|
+
if (!provider || !resume?.supported) return undefined;
|
|
189
|
+
const re = resume.idPattern ? new RegExp(`^${resume.idPattern}$`, "i") : undefined;
|
|
194
190
|
for (const entry of getAgentTimeline(agent.id, { limit: 50 })) {
|
|
195
191
|
const metadata = entry.metadata;
|
|
196
192
|
if (!metadata) continue;
|
|
197
|
-
const provider = metaString(metadata, "provider");
|
|
198
193
|
const resumeId = metaString(metadata, "claudeResumeId");
|
|
199
|
-
if (provider ===
|
|
194
|
+
if (metaString(metadata, "provider") === provider && resumeId && (!re || re.test(resumeId))) return resumeId;
|
|
200
195
|
}
|
|
201
196
|
return undefined;
|
|
202
197
|
}
|
|
@@ -270,9 +265,9 @@ export const postAgentAction: Handler = async (req, params) => {
|
|
|
270
265
|
const metaTmuxSession = typeof agent.meta?.tmuxSession === "string" ? agent.meta.tmuxSession : undefined;
|
|
271
266
|
const metaPolicyName = typeof agent.meta?.policyName === "string" ? agent.meta.policyName : undefined;
|
|
272
267
|
const metaSpawnRequestId = typeof agent.meta?.spawnRequestId === "string" ? agent.meta.spawnRequestId : undefined;
|
|
273
|
-
const resumeId = action === "resume" ?
|
|
268
|
+
const resumeId = action === "resume" ? latestResumeIdForAgent(agent) : undefined;
|
|
274
269
|
if (action === "resume" && !orchestrator) return error("no online orchestrator available to resume agent", 409);
|
|
275
|
-
if (action === "resume" && !resumeId) return error("no
|
|
270
|
+
if (action === "resume" && !resumeId) return error("no resume id recorded for agent", 422);
|
|
276
271
|
const denied = authorizeRoute(req, {
|
|
277
272
|
scope: "agent:write",
|
|
278
273
|
resource: { agentId: agent.id, orchestratorId: orchestrator?.id, policyName: metaPolicyName, spawnRequestId: metaSpawnRequestId },
|
|
@@ -29,7 +29,7 @@ export const getProviderConfigsRoute: Handler = (req) => {
|
|
|
29
29
|
migrateLocalProviderConfigs();
|
|
30
30
|
const entries = listProviderConfigEntries(host);
|
|
31
31
|
return json(Object.fromEntries(VALID_PROVIDER_CONFIGS.map((provider) => {
|
|
32
|
-
return [provider, providerConfigPublic(entries[provider])];
|
|
32
|
+
return [provider, providerConfigPublic(entries[provider] ?? getProviderConfigEntry(provider, host))];
|
|
33
33
|
})));
|
|
34
34
|
};
|
|
35
35
|
|
package/src/spawn-targets.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { getRoutingPolicy } from "./routing-policy-store";
|
|
|
5
5
|
import { isPathWithinBase } from "./utils";
|
|
6
6
|
import { McpNotFoundError } from "./mcp-errors";
|
|
7
7
|
import { DEFAULT_QUOTA_PREFER_SLACK_AT, DEFAULT_QUOTA_TARGET_UTILIZATION, quotaPaceMetrics } from "agent-relay-sdk/provider-quota";
|
|
8
|
-
import type
|
|
8
|
+
import { isSpawnProvider, type ProviderQuotaRoutingConfig } from "agent-relay-sdk";
|
|
9
9
|
import type { Orchestrator, ProviderCatalogSummary, ProviderQuotaBurnRole, ProviderQuotaBurnWindow, ProviderQuotaRecord, QuotaState, SpawnProvider } from "./types";
|
|
10
10
|
|
|
11
11
|
// #308 — spawn discovery + target selection. Home for the live capability matrix that
|
|
@@ -159,7 +159,7 @@ export function providerQuotaAdvisories(now = Date.now()): Map<SpawnProvider, Sp
|
|
|
159
159
|
const policy = getRoutingPolicy();
|
|
160
160
|
const byProvider = new Map<SpawnProvider, ProviderQuotaRecord[]>();
|
|
161
161
|
for (const record of listProviderQuotas({ historyLimit: 0, now }).quotas) {
|
|
162
|
-
if (record.provider
|
|
162
|
+
if (!isSpawnProvider(record.provider)) continue;
|
|
163
163
|
const provider = record.provider;
|
|
164
164
|
byProvider.set(provider, [...(byProvider.get(provider) ?? []), record]);
|
|
165
165
|
}
|
|
@@ -231,8 +231,7 @@ function spawnQuotaWindow(
|
|
|
231
231
|
// recency-weighted rate projects no early wall) replaces the blunt utilization floor, which
|
|
232
232
|
// used to hide a legitimate low-util-high-burst alarm. Operators may still opt back in.
|
|
233
233
|
const utilizationFloor = providerCfg?.burnPenaltyUtilizationFloor ?? 0;
|
|
234
|
-
const
|
|
235
|
-
const penalty = quotaWindow.utilization < utilizationFloor ? 0 : rawPenalty;
|
|
234
|
+
const pacePenalty = (earlyFraction ?? 0) * weight;
|
|
236
235
|
|
|
237
236
|
// #760 — re-derive the pace projection against the PER-PROVIDER target (the stored burn used
|
|
238
237
|
// the default). Slack is the symmetric underburn signal; prefer flags a window with no
|
|
@@ -249,6 +248,19 @@ function spawnQuotaWindow(
|
|
|
249
248
|
projectedUtilAtReset = pace.projectedUtilAtReset;
|
|
250
249
|
slack = pace.slack;
|
|
251
250
|
}
|
|
251
|
+
|
|
252
|
+
// #798 — position penalty: an idle provider whose (de-biased) projection lands AT/OVER target is
|
|
253
|
+
// already over-extended, but its forward-wall pace penalty is 0 (no burn) — so without this the
|
|
254
|
+
// band would flip to "normal" and route big jobs onto a near-exhausted provider. Derive a STABLE
|
|
255
|
+
// penalty from the overshoot magnitude (slack < 0 ⇒ projectedUtilAtReset − target), decoupled
|
|
256
|
+
// from the jittery forward-burn term. Gated to idle (observedRatePerMs collapsed to 0): an ACTIVE
|
|
257
|
+
// provider is still governed entirely by the pace penalty (effective = max of the two), so this
|
|
258
|
+
// adds a floor for the parked-high case WITHOUT changing active routing. Under-target idle ⇒ 0.
|
|
259
|
+
const idle = burnWindow.observedRatePerMs !== null && burnWindow.observedRatePerMs <= 0;
|
|
260
|
+
const overshoot = slack !== null ? Math.max(0, -slack) : 0;
|
|
261
|
+
const positionPenalty = idle ? Math.min(1, overshoot / Math.max(1e-9, 1 - target)) * weight : 0;
|
|
262
|
+
const rawPenalty = Math.max(pacePenalty, positionPenalty);
|
|
263
|
+
const penalty = quotaWindow.utilization < utilizationFloor ? 0 : rawPenalty;
|
|
252
264
|
const prefer = !blockedNow && penalty === 0
|
|
253
265
|
&& quotaPenaltyBandFor(penalty) === "normal"
|
|
254
266
|
&& slack !== null && slack >= preferSlackAt;
|
|
@@ -349,6 +361,22 @@ function quotaPenaltyBandFor(
|
|
|
349
361
|
return "normal";
|
|
350
362
|
}
|
|
351
363
|
|
|
364
|
+
// #798 — classify a (penalty, slack) signal into a band. The single home for the
|
|
365
|
+
// penalty-ladder + prefer-overlay mapping, shared by getProviderBandInfo's per-window band and the
|
|
366
|
+
// band-transition notifier, which re-classifies the EMA-smoothed signal before thresholding. The
|
|
367
|
+
// prefer overlay mirrors spawnQuotaWindow: a normal-band window projected ≥ preferSlackAt under
|
|
368
|
+
// target reads "prefer".
|
|
369
|
+
export function quotaBandFromSignal(
|
|
370
|
+
penalty: number,
|
|
371
|
+
slack: number | null,
|
|
372
|
+
quotaBands: Partial<{ cautionAt: number; avoidLargeAt: number; hardAvoidAt: number }> | undefined,
|
|
373
|
+
preferSlackAt: number,
|
|
374
|
+
): QuotaBand {
|
|
375
|
+
const base = quotaPenaltyBandFor(penalty, quotaBands);
|
|
376
|
+
if (base === "normal" && slack !== null && slack >= preferSlackAt) return "prefer";
|
|
377
|
+
return base;
|
|
378
|
+
}
|
|
379
|
+
|
|
352
380
|
function recommendedAction(input: { quotaPenaltyBand: SpawnQuotaBurnBand; blockedNow: boolean; prefer: boolean; slack: number | null; windows: SpawnQuotaBurnWindow[] }): string {
|
|
353
381
|
if (input.blockedNow) {
|
|
354
382
|
const hasSevenDayBlock = input.windows.some((window) => window.role === "7d" && window.blockedNow);
|
package/src/sse.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAgent, getOrchestrator, matchingDeliveryAgents, onWorkspaceChange, sendMessageWithResult } from "./db";
|
|
1
|
+
import { getAgent, getOrchestrator, matchingDeliveryAgents, onWorkspaceChange, sendMessageWithResult, setChannelSubscriberProbe } from "./db";
|
|
2
2
|
import { projectAgentStatusData } from "./agent-card-projection";
|
|
3
3
|
import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
|
|
4
4
|
import { messageMatchesAgent, targetMatchesAgent } from "./agent-ref";
|
|
@@ -20,6 +20,17 @@ const SSE_RETRY_MS = 5_000;
|
|
|
20
20
|
|
|
21
21
|
subscribeRelayEvents((event) => fanout(event));
|
|
22
22
|
|
|
23
|
+
// #797 — let the db layer derive outbound-channel liveness from live SSE subscriptions.
|
|
24
|
+
// A channel agent is "subscribed" iff the channels-host daemon holds an open
|
|
25
|
+
// GET /api/events?for=<channelAgentId> stream (createSSEStream stores `for` as agentId).
|
|
26
|
+
export function hasActiveSubscriber(agentId: string): boolean {
|
|
27
|
+
for (const conn of connections.values()) {
|
|
28
|
+
if (conn.agentId === agentId) return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
setChannelSubscriberProbe(hasActiveSubscriber);
|
|
33
|
+
|
|
23
34
|
export function createSSEStream(agentId: string | null): Response {
|
|
24
35
|
let connId: string;
|
|
25
36
|
|