agent-relay-server 0.121.5 → 0.122.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +1 -1
- package/package.json +6 -6
- package/public/assets/{MessageBubble-CLqxCG9b.js → MessageBubble-DRdUD-kW.js} +2 -2
- package/public/assets/{MessageBubble-CLqxCG9b.js.map → MessageBubble-DRdUD-kW.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BkkToFEb.js → PlanGraphPanel-siKMPCln.js} +2 -2
- package/public/assets/{PlanGraphPanel-BkkToFEb.js.map → PlanGraphPanel-siKMPCln.js.map} +1 -1
- package/public/assets/{activity-D-ocGQGs.js → activity-D3MNcSaV.js} +2 -2
- package/public/assets/{activity-D-ocGQGs.js.map → activity-D3MNcSaV.js.map} +1 -1
- package/public/assets/{agent-profiles-CRPgYTyb.js → agent-profiles-CKzttq6G.js} +2 -2
- package/public/assets/{agent-profiles-CRPgYTyb.js.map → agent-profiles-CKzttq6G.js.map} +1 -1
- package/public/assets/{agent-type-icon-CeEoMouY.js → agent-type-icon-0ZJRP1Vy.js} +2 -2
- package/public/assets/{agent-type-icon-CeEoMouY.js.map → agent-type-icon-0ZJRP1Vy.js.map} +1 -1
- package/public/assets/{agents-Chw5E_2q.js → agents-CuVj_rfG.js} +2 -2
- package/public/assets/{agents-Chw5E_2q.js.map → agents-CuVj_rfG.js.map} +1 -1
- package/public/assets/{analytics-BmNDG0hR.js → analytics-iob6C27d.js} +2 -2
- package/public/assets/{analytics-BmNDG0hR.js.map → analytics-iob6C27d.js.map} +1 -1
- package/public/assets/{automation-D7g90kTv.js → automation-B2ixxs4q.js} +2 -2
- package/public/assets/{automation-D7g90kTv.js.map → automation-B2ixxs4q.js.map} +1 -1
- package/public/assets/{branch-state-badge-Bv7DhLBZ.js → branch-state-badge-CafBJWKo.js} +2 -2
- package/public/assets/{branch-state-badge-Bv7DhLBZ.js.map → branch-state-badge-CafBJWKo.js.map} +1 -1
- package/public/assets/{channels-CLwPeEPi.js → channels-C-WsbL2H.js} +2 -2
- package/public/assets/{channels-CLwPeEPi.js.map → channels-C-WsbL2H.js.map} +1 -1
- package/public/assets/{chat-Cgj7VKzc.js → chat-DeoVTVob.js} +2 -2
- package/public/assets/{chat-Cgj7VKzc.js.map → chat-DeoVTVob.js.map} +1 -1
- package/public/assets/{connectors-Br6fiTny.js → connectors-C2Ac03LJ.js} +2 -2
- package/public/assets/{connectors-Br6fiTny.js.map → connectors-C2Ac03LJ.js.map} +1 -1
- package/public/assets/display-CJzSoTMq.js +3 -0
- package/public/assets/display-CJzSoTMq.js.map +1 -0
- package/public/assets/{formatted-body-CmkuD23M.js → formatted-body-35XBOY5L.js} +3 -3
- package/public/assets/{formatted-body-CmkuD23M.js.map → formatted-body-35XBOY5L.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DbdFmbwW.js → formatted-body-impl-RmDxI5Ux.js} +2 -2
- package/public/assets/{formatted-body-impl-DbdFmbwW.js.map → formatted-body-impl-RmDxI5Ux.js.map} +1 -1
- package/public/assets/index-D0xMQQb_.js +25 -0
- package/public/assets/{index-D9OogaB5.js.map → index-D0xMQQb_.js.map} +1 -1
- package/public/assets/{insights-qvaPqWCV.js → insights-B643SFvm.js} +2 -2
- package/public/assets/{insights-qvaPqWCV.js.map → insights-B643SFvm.js.map} +1 -1
- package/public/assets/{integrations-DYEGSqq_.js → integrations-hFIHeCF4.js} +2 -2
- package/public/assets/{integrations-DYEGSqq_.js.map → integrations-hFIHeCF4.js.map} +1 -1
- package/public/assets/{maintenance-C0HIEB-J.js → maintenance-CURvdp_Z.js} +2 -2
- package/public/assets/{maintenance-C0HIEB-J.js.map → maintenance-CURvdp_Z.js.map} +1 -1
- package/public/assets/{managed-agents-DdS7aI38.js → managed-agents-DrZtdlSn.js} +2 -2
- package/public/assets/{managed-agents-DdS7aI38.js.map → managed-agents-DrZtdlSn.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BnXMH1z1.js → markdown-preview-impl-CVDO--ek.js} +2 -2
- package/public/assets/{markdown-preview-impl-BnXMH1z1.js.map → markdown-preview-impl-CVDO--ek.js.map} +1 -1
- package/public/assets/{memory-CSwx7bz8.js → memory-Bm7dt_Qf.js} +2 -2
- package/public/assets/{memory-CSwx7bz8.js.map → memory-Bm7dt_Qf.js.map} +1 -1
- package/public/assets/{messages-Be9CmvDv.js → messages-DTq4i2KZ.js} +2 -2
- package/public/assets/{messages-Be9CmvDv.js.map → messages-DTq4i2KZ.js.map} +1 -1
- package/public/assets/{orchestrators-BgtvvHvo.js → orchestrators-Dra318Ap.js} +2 -2
- package/public/assets/{orchestrators-BgtvvHvo.js.map → orchestrators-Dra318Ap.js.map} +1 -1
- package/public/assets/{overview-BvhbPWtF.js → overview-yr1Vmk64.js} +2 -2
- package/public/assets/{overview-BvhbPWtF.js.map → overview-yr1Vmk64.js.map} +1 -1
- package/public/assets/{pairs-CpXSOMx0.js → pairs-mnlKDBGp.js} +2 -2
- package/public/assets/{pairs-CpXSOMx0.js.map → pairs-mnlKDBGp.js.map} +1 -1
- package/public/assets/projects-Dzdf-gkK.js +2 -0
- package/public/assets/{projects-DdPNPFJI.js.map → projects-Dzdf-gkK.js.map} +1 -1
- package/public/assets/prompt-settings-ByL8SNCi.js +2 -0
- package/public/assets/{prompt-settings-BpZse7-X.js.map → prompt-settings-ByL8SNCi.js.map} +1 -1
- package/public/assets/{registry-pb1gTZEg.js → registry-CXWyOtpr.js} +2 -2
- package/public/assets/{registry-pb1gTZEg.js.map → registry-CXWyOtpr.js.map} +1 -1
- package/public/assets/{security-BgcX1n9D.js → security-0SBsc_4W.js} +2 -2
- package/public/assets/{security-BgcX1n9D.js.map → security-0SBsc_4W.js.map} +1 -1
- package/public/assets/{select-DZPVpKPv.js → select-Bw5z2KJ_.js} +2 -2
- package/public/assets/{select-DZPVpKPv.js.map → select-Bw5z2KJ_.js.map} +1 -1
- package/public/assets/{settings-hd5kzdRB.js → settings-78Cyq_tA.js} +4 -4
- package/public/assets/{settings-hd5kzdRB.js.map → settings-78Cyq_tA.js.map} +1 -1
- package/public/assets/{store-DKTLubBT.js → store-BPrVuKiv.js} +4 -4
- package/public/assets/store-BPrVuKiv.js.map +1 -0
- package/public/assets/{tasks-C5d2LU5E.js → tasks-Cg2MMeri.js} +2 -2
- package/public/assets/{tasks-C5d2LU5E.js.map → tasks-Cg2MMeri.js.map} +1 -1
- package/public/assets/teams-DWv6UwFV.js +2 -0
- package/public/assets/{teams-B8f2pGJe.js.map → teams-DWv6UwFV.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CxAscH0U.js → terminal-viewer-impl-DBLDjbIg.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CxAscH0U.js.map → terminal-viewer-impl-DBLDjbIg.js.map} +1 -1
- package/public/assets/{types-uVOXgvMT.js → types-BeJUSfDj.js} +2 -2
- package/public/assets/types-BeJUSfDj.js.map +1 -0
- package/public/assets/{user-avatar-Dw6mzWhv.js → user-avatar-BsOLyVJj.js} +2 -2
- package/public/assets/{user-avatar-Dw6mzWhv.js.map → user-avatar-BsOLyVJj.js.map} +1 -1
- package/public/assets/{work-queue-9imc7aNK.js → work-queue-DmwvnF6F.js} +2 -2
- package/public/assets/{work-queue-9imc7aNK.js.map → work-queue-DmwvnF6F.js.map} +1 -1
- package/public/assets/{workspaces-CLYc3yF3.js → workspaces-dIBivJN8.js} +2 -2
- package/public/assets/{workspaces-CLYc3yF3.js.map → workspaces-dIBivJN8.js.map} +1 -1
- package/public/index.html +6 -6
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +44 -938
- package/runner/src/adapter.ts +53 -18
- package/runner/src/adapters/claude-delivery.ts +127 -0
- package/runner/src/adapters/claude-quota-harvest.ts +92 -0
- package/runner/src/adapters/claude-session-probe.ts +225 -0
- package/runner/src/adapters/claude-status-detectors.ts +147 -0
- package/runner/src/adapters/claude-transcript-tail.ts +128 -0
- package/runner/src/adapters/claude-transcript.ts +414 -0
- package/runner/src/adapters/claude.ts +672 -0
- package/runner/src/adapters/codex-agent-message-capture.ts +139 -0
- package/runner/src/adapters/codex-client.ts +279 -0
- package/runner/src/adapters/codex.ts +1345 -0
- package/runner/src/attachment-cache.ts +189 -0
- package/runner/src/busy-reconciler.ts +102 -0
- package/runner/src/claim-tracker.ts +140 -0
- package/runner/src/claude-prompt-gates.ts +268 -0
- package/runner/src/config.ts +1 -4
- package/runner/src/context-probe-state.ts +6 -0
- package/runner/src/continuation-archive.ts +179 -0
- package/runner/src/control-server.ts +639 -0
- package/runner/src/index.ts +396 -0
- package/runner/src/launch-assembly.ts +508 -0
- package/runner/src/liveness.ts +50 -0
- package/runner/src/logger.ts +99 -0
- package/runner/src/mcp-outbox.ts +120 -0
- package/runner/src/message-body-config/config.ts +3 -0
- package/runner/src/message-body-config.ts +1 -0
- package/runner/src/native-self-resume.ts +202 -0
- package/runner/src/outbox.ts +342 -0
- package/runner/src/process-meta.ts +9 -0
- package/runner/src/profile-home.ts +260 -0
- package/runner/src/profile-projection.ts +305 -0
- package/runner/src/providers.ts +20 -0
- package/runner/src/provisioning.ts +314 -0
- package/runner/src/quota.ts +40 -0
- package/runner/src/rate-limit.ts +189 -0
- package/runner/src/relay-injection-events.ts +102 -0
- package/runner/src/relay-instructions.ts +60 -0
- package/runner/src/relay-mcp-proxy.ts +383 -0
- package/runner/src/relay-mcp.ts +156 -0
- package/runner/src/reply-obligation-cache.ts +136 -0
- package/runner/src/response-capture-report.ts +63 -0
- package/runner/src/runner-core.ts +2409 -0
- package/runner/src/runner-helpers.ts +435 -0
- package/runner/src/runner-insights.ts +105 -0
- package/runner/src/runner.ts +14 -0
- package/runner/src/session-destroy.ts +22 -0
- package/runner/src/session-insights.ts +118 -0
- package/runner/src/session-scratch.ts +271 -0
- package/runner/src/template-resolver.ts +29 -0
- package/runner/src/version.ts +43 -0
- package/src/agent-card-projection.ts +5 -1
- package/src/agent-command-activity.ts +39 -0
- package/src/automations/reconcile.ts +2 -2
- package/src/cli/workspace.ts +3 -1
- package/src/config.ts +10 -1
- package/src/db/agents.ts +118 -37
- package/src/db/message-reads.ts +50 -0
- package/src/db/messages.ts +48 -9
- package/src/db/migrations.ts +54 -0
- package/src/db/provider-quota-burn.ts +96 -67
- package/src/db/provider-quotas.ts +29 -7
- package/src/db/scheduled-timers.ts +21 -0
- package/src/db/schema.ts +1 -0
- package/src/db/tasks.ts +33 -3
- package/src/db/work-evidence.ts +10 -7
- package/src/event-loop-lag.ts +15 -0
- package/src/gate-resolver.ts +19 -1
- package/src/maintenance/jobs.ts +23 -6
- package/src/managed-policy.ts +5 -12
- package/src/mcp-plan-tools.ts +13 -1
- package/src/project-instructions.ts +36 -2
- package/src/quota-advisory.ts +6 -4
- package/src/quota-band-transition.ts +23 -14
- package/src/routes/agent-sessions.ts +39 -7
- package/src/routes/agents.ts +3 -2
- package/src/routes/tasks.ts +16 -4
- package/src/routes/workspaces.ts +35 -10
- package/src/services/plan-graph.ts +27 -9
- package/src/services/register-agent.ts +12 -12
- package/src/services/send-message.ts +43 -15
- package/src/services/spawn-agent.ts +3 -0
- package/src/spawn-command.ts +8 -0
- package/src/spawn-targets.ts +19 -34
- package/src/sse.ts +25 -3
- package/src/workspace-merge.ts +25 -5
- package/src/workspace-orphans.ts +13 -6
- package/public/assets/display-exQDWum3.js +0 -3
- package/public/assets/display-exQDWum3.js.map +0 -1
- package/public/assets/index-D9OogaB5.js +0 -25
- package/public/assets/projects-DdPNPFJI.js +0 -2
- package/public/assets/prompt-settings-BpZse7-X.js +0 -2
- package/public/assets/store-DKTLubBT.js.map +0 -1
- package/public/assets/teams-B8f2pGJe.js +0 -2
- package/public/assets/types-uVOXgvMT.js.map +0 -1
|
@@ -1,28 +1,38 @@
|
|
|
1
|
-
import { DEFAULT_QUOTA_TARGET_UTILIZATION, quotaPaceMetrics, quotaWindowMetrics } from "agent-relay-sdk/provider-quota";
|
|
1
|
+
import { DEFAULT_QUOTA_TARGET_UTILIZATION, quotaPaceMetrics, quotaTrendProjection, quotaWindowMetrics } from "agent-relay-sdk/provider-quota";
|
|
2
2
|
import type { ProviderQuotaBurn, ProviderQuotaBurnRole, ProviderQuotaBurnWindow, ProviderQuotaSnapshot, QuotaState } from "../types";
|
|
3
3
|
|
|
4
4
|
const QUOTA_BURN_MIN_SAMPLE_SPAN_MS = 10 * 60_000;
|
|
5
5
|
|
|
6
|
-
// #760 — nominal window length per role,
|
|
6
|
+
// #760 — nominal window length per role. windowStartAt = resetAt − this, and it scales the
|
|
7
|
+
// evidence-confidence horizon below.
|
|
7
8
|
const WINDOW_NOMINAL_MS: Record<ProviderQuotaBurnRole, number> = {
|
|
8
9
|
"5h": 5 * 3_600_000,
|
|
9
10
|
"7d": 7 * 24 * 3_600_000,
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
// #
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
// #1072 — SLOW EWMA time constant (dt-correct). This is the sustained-trend requirement made
|
|
14
|
+
// mechanical: a 15-min onset burst barely moves a 24h-τ average, so a transient spawn-burst spike
|
|
15
|
+
// cannot flip the band — only a trend that holds for hours does. Drives the band projection.
|
|
16
|
+
const EWMA_TAU_SLOW_MS: Record<ProviderQuotaBurnRole, number> = {
|
|
17
|
+
"5h": 2 * 3_600_000,
|
|
18
|
+
"7d": 24 * 3_600_000,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// #1072 — continuous trend confidence reaches 1 once the observed span covers this horizon (the
|
|
22
|
+
// slow-EWMA τ). A fresh window (tiny observed span) ⇒ ~0 confidence ⇒ the projected (not the
|
|
23
|
+
// measured) overshoot cannot hard-escalate the band; by the time the span covers τ, a sustained
|
|
24
|
+
// trend is real. Absorbs the #1073 confidence-gate stopgap into the model.
|
|
25
|
+
const CONFIDENCE_FULL_EVIDENCE_MS: Record<ProviderQuotaBurnRole, number> = {
|
|
26
|
+
"5h": 2 * 3_600_000,
|
|
27
|
+
"7d": 24 * 3_600_000,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// #798 — idle collapse, retained for STABILITY (not for the penalty). Once utilization has held
|
|
31
|
+
// flat for this long, the residual slow-EWMA rate is a stale-burst tail: the slow average decays
|
|
32
|
+
// but never reaches exactly 0 in finite time, so a small rate × a multi-day horizon would still
|
|
33
|
+
// creep the projection tick-to-tick (the band walk from #798). Pinning the rate to 0 once idle
|
|
34
|
+
// this long fixes the projection at current utilization so the band is flat across ticks. The
|
|
35
|
+
// threshold is absolute (a 5h window resets long before reaching it, so this only affects 7d).
|
|
26
36
|
const IDLE_FORWARD_COLLAPSE_MS = 6 * 3_600_000;
|
|
27
37
|
const IDLE_UTILIZATION_EPSILON = 1e-9;
|
|
28
38
|
|
|
@@ -63,32 +73,38 @@ function lowConfidenceBurnWindow(role: ProviderQuotaBurnRole, window: QuotaWindo
|
|
|
63
73
|
projectedUtilAtReset: null,
|
|
64
74
|
slack: null,
|
|
65
75
|
target: null,
|
|
76
|
+
confidenceScore: null,
|
|
66
77
|
};
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
// #
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
80
|
+
// #1072 — dt-correct EWMA of the consumption rate (utilization-fraction per ms). Each consecutive
|
|
81
|
+
// interval contributes its mean rate Δutil/Δt, held constant over Δt and decayed by exp(−Δt/τ), so
|
|
82
|
+
// a long idle interval (Δt≫τ, rate ~0) drives the average toward 0 while a short burst barely
|
|
83
|
+
// moves a long-τ average. The EWMA is SEEDED AT 0 (not at the first interval's raw slope) so a
|
|
84
|
+
// sustained trend is required to build the rate up — a single onset interval can never set the
|
|
85
|
+
// full slope. Interval rates are clamped ≥0 (a within-window utilization drop is treated as no
|
|
86
|
+
// consumption, not negative burn).
|
|
87
|
+
function ewmaRatePerMs(
|
|
74
88
|
samples: Array<{ capturedAt: number; utilization: number }>,
|
|
75
89
|
now: number,
|
|
76
|
-
|
|
90
|
+
tauMs: number,
|
|
77
91
|
): number {
|
|
78
|
-
let
|
|
79
|
-
let weightedDt = 0;
|
|
92
|
+
let r = 0;
|
|
80
93
|
for (let i = 1; i < samples.length; i++) {
|
|
81
|
-
const
|
|
82
|
-
const cur = samples[i]!;
|
|
83
|
-
const dt = cur.capturedAt - prev.capturedAt;
|
|
94
|
+
const dt = samples[i]!.capturedAt - samples[i - 1]!.capturedAt;
|
|
84
95
|
if (dt <= 0) continue;
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
const rate = Math.max(0, (samples[i]!.utilization - samples[i - 1]!.utilization) / dt);
|
|
97
|
+
const decay = Math.exp(-dt / tauMs);
|
|
98
|
+
r = decay * r + (1 - decay) * rate;
|
|
99
|
+
}
|
|
100
|
+
// Idle observability: extend to `now` with a zero-rate interval so a deduped idle stretch (no new
|
|
101
|
+
// distinct samples arrive while utilization holds) still decays the average instead of freezing
|
|
102
|
+
// the last observed slope — the same synthetic-tail trick #760 used, applied to the EWMA.
|
|
103
|
+
const last = samples[samples.length - 1];
|
|
104
|
+
if (last && now > last.capturedAt) {
|
|
105
|
+
r *= Math.exp(-(now - last.capturedAt) / tauMs);
|
|
89
106
|
}
|
|
90
|
-
|
|
91
|
-
return Math.max(0, weightedDelta / weightedDt);
|
|
107
|
+
return Math.max(0, r);
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
// #798 — when did utilization last differ from its current value? Scanning back from the newest
|
|
@@ -148,62 +164,75 @@ function deriveProviderQuotaBurnWindow(input: {
|
|
|
148
164
|
const timeToResetMs = resetAt - input.now;
|
|
149
165
|
if (remainingQuota <= 0 || timeToResetMs <= 0) return low;
|
|
150
166
|
|
|
151
|
-
// #
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
const rateSamples = input.now > current.capturedAt
|
|
161
|
-
? [...samples, { capturedAt: input.now, utilization: utilizationNow }]
|
|
162
|
-
: samples;
|
|
163
|
-
const observedRatePerMs = recencyWeightedRatePerMs(rateSamples, input.now, halfLifeMs);
|
|
164
|
-
// #798 — idle de-bias: if utilization has held flat (no change vs. now) for IDLE_FORWARD_COLLAPSE_MS,
|
|
165
|
-
// treat the forward-burn term as a stale-burst artifact and collapse it to 0 so the projection
|
|
166
|
-
// pins to current utilization instead of walking the band boundary. lastChangeAt is the moment
|
|
167
|
-
// utilization last differed from the current value (the synthetic "still X as of now" interval is
|
|
168
|
-
// what makes a deduped idle stretch observable here).
|
|
167
|
+
// #1072 — the SLOW EWMA is the burn rate: a sustained trend is required by construction, so a
|
|
168
|
+
// transient onset burst (100% of a young window's evidence) can't drive the band. The EWMA walks
|
|
169
|
+
// the real sample timeline and extends a zero-rate tail to `now`, so a deduped idle stretch
|
|
170
|
+
// decays the rate instead of freezing the last observed slope (the #760 burn×6.3 stale-slope bug).
|
|
171
|
+
const rSlow = ewmaRatePerMs(samples, input.now, EWMA_TAU_SLOW_MS[role]);
|
|
172
|
+
// #798 idle collapse (stability only): once utilization has held flat ≥ IDLE_FORWARD_COLLAPSE_MS
|
|
173
|
+
// the residual slow rate is a stale-burst tail — pin it to 0 so the projection == current
|
|
174
|
+
// utilization and stays flat tick-to-tick (no band walk). lastChangeAt is when utilization last
|
|
175
|
+
// differed from the current value.
|
|
169
176
|
const lastChangeAt = lastUtilizationChangeAt(samples, utilizationNow);
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
//
|
|
177
|
+
const bandRate = input.now - lastChangeAt >= IDLE_FORWARD_COLLAPSE_MS ? 0 : rSlow;
|
|
178
|
+
|
|
179
|
+
// #1072 — evidence span and continuous trend confidence. evidenceMs is how long we've actually
|
|
180
|
+
// observed this window instance (bounded by both the window's own start and our first sample);
|
|
181
|
+
// it caps the projection horizon and drives the confidence that gates hard-escalation.
|
|
182
|
+
const windowStartAt = resetAt - WINDOW_NOMINAL_MS[role];
|
|
183
|
+
const evidenceMs = Math.max(0, input.now - Math.max(windowStartAt, oldest.capturedAt));
|
|
184
|
+
const confidence = Math.max(0, Math.min(1, evidenceMs / CONFIDENCE_FULL_EVIDENCE_MS[role]));
|
|
185
|
+
|
|
186
|
+
// #1072 — evidence-capped band projection off the slow rate. The query layer (spawn-targets)
|
|
187
|
+
// re-derives slack/penalty against the per-provider target from this (target-independent)
|
|
188
|
+
// projection + confidenceScore; the ingest-time values use the default target so direct burn
|
|
189
|
+
// consumers (dashboard) have a number.
|
|
190
|
+
const trend = quotaTrendProjection({
|
|
191
|
+
utilization: utilizationNow,
|
|
192
|
+
slowRatePerMs: bandRate,
|
|
193
|
+
timeToResetMs,
|
|
194
|
+
evidenceMs,
|
|
195
|
+
confidence,
|
|
196
|
+
target: DEFAULT_QUOTA_TARGET_UTILIZATION,
|
|
197
|
+
});
|
|
198
|
+
// burnRatio (display) paces the same slow rate against the target, so the widget's burn number
|
|
199
|
+
// and the routing band always agree — a bursty spike shows in the raw sparkline, not here.
|
|
174
200
|
const pace = quotaPaceMetrics({
|
|
175
201
|
utilization: utilizationNow,
|
|
176
|
-
observedRatePerMs:
|
|
202
|
+
observedRatePerMs: bandRate,
|
|
177
203
|
timeToResetMs,
|
|
178
204
|
target: DEFAULT_QUOTA_TARGET_UTILIZATION,
|
|
179
205
|
});
|
|
180
|
-
if (
|
|
181
|
-
// Idle (decayed-to-idle, or #798 idle-collapsed): no projected wall, but
|
|
182
|
-
//
|
|
206
|
+
if (bandRate <= 0) {
|
|
207
|
+
// Idle (flat, decayed-to-idle, or #798 idle-collapsed): no projected wall, but the projection
|
|
208
|
+
// still pins to current utilization and slack flags headroom (positive) or the over-target
|
|
209
|
+
// position the penalty avoids (negative).
|
|
183
210
|
return {
|
|
184
211
|
...low,
|
|
185
212
|
confidence: "high",
|
|
186
213
|
burnRatio: 0,
|
|
187
214
|
observedRatePerMs: 0,
|
|
188
|
-
projectedUtilAtReset:
|
|
189
|
-
slack:
|
|
190
|
-
target:
|
|
215
|
+
projectedUtilAtReset: trend.projectedUtilAtReset,
|
|
216
|
+
slack: trend.slack,
|
|
217
|
+
target: trend.target,
|
|
218
|
+
confidenceScore: confidence,
|
|
191
219
|
};
|
|
192
220
|
}
|
|
193
|
-
const timeToWallMs = remainingQuota /
|
|
221
|
+
const timeToWallMs = remainingQuota / bandRate;
|
|
194
222
|
const projectedWallAt = input.now + timeToWallMs;
|
|
195
223
|
return {
|
|
196
224
|
...low,
|
|
197
225
|
confidence: "high",
|
|
198
226
|
burnRatio: pace.burnRatio,
|
|
199
|
-
observedRatePerMs:
|
|
227
|
+
observedRatePerMs: bandRate,
|
|
200
228
|
timeToWallMs,
|
|
201
229
|
projectedWallAt,
|
|
202
230
|
// #760 (Defect 2) — symmetric: negative means the wall lands AFTER reset (headroom left).
|
|
203
231
|
earlyWallMs: resetAt - projectedWallAt,
|
|
204
|
-
projectedUtilAtReset:
|
|
205
|
-
slack:
|
|
206
|
-
target:
|
|
232
|
+
projectedUtilAtReset: trend.projectedUtilAtReset,
|
|
233
|
+
slack: trend.slack,
|
|
234
|
+
target: trend.target,
|
|
235
|
+
confidenceScore: confidence,
|
|
207
236
|
};
|
|
208
237
|
}
|
|
209
238
|
|
|
@@ -26,12 +26,22 @@ export interface ProviderQuotaAdvisoryInput {
|
|
|
26
26
|
quota: QuotaState;
|
|
27
27
|
sourceAgentIds: string[];
|
|
28
28
|
previousAdvisoryState?: string | null;
|
|
29
|
+
// #1074 — serialized quota-band hysteresis state (quota_band_state column). Threaded in so the
|
|
30
|
+
// band-transition pipeline hydrates instead of re-seeding to "normal" on every relay restart.
|
|
31
|
+
previousBandState?: string | null;
|
|
29
32
|
now: number;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
// #1074 — the handler returns BOTH persisted blobs: the utilization advisory state and the
|
|
36
|
+
// band-transition hysteresis state. Both are written back to their respective columns.
|
|
37
|
+
export interface ProviderQuotaAdvisoryResult {
|
|
38
|
+
advisoryState: string;
|
|
39
|
+
bandState: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let providerQuotaAdvisoryHandler: ((input: ProviderQuotaAdvisoryInput) => ProviderQuotaAdvisoryResult) | undefined;
|
|
33
43
|
|
|
34
|
-
export function setProviderQuotaAdvisoryHandler(handler: ((input: ProviderQuotaAdvisoryInput) =>
|
|
44
|
+
export function setProviderQuotaAdvisoryHandler(handler: ((input: ProviderQuotaAdvisoryInput) => ProviderQuotaAdvisoryResult) | undefined): void {
|
|
35
45
|
providerQuotaAdvisoryHandler = handler;
|
|
36
46
|
}
|
|
37
47
|
|
|
@@ -42,6 +52,7 @@ type ProviderQuotaRow = {
|
|
|
42
52
|
codex_reset_credits?: string | null;
|
|
43
53
|
source_agent_ids?: string | null;
|
|
44
54
|
quota_advisory_state?: string | null;
|
|
55
|
+
quota_band_state?: string | null;
|
|
45
56
|
last_updated_at?: number | null;
|
|
46
57
|
last_attempt_at: number;
|
|
47
58
|
last_error?: string | null;
|
|
@@ -182,6 +193,13 @@ function newestQuotaAdvisoryState(rows: ProviderQuotaRow[]): string {
|
|
|
182
193
|
return row?.quota_advisory_state ?? "{}";
|
|
183
194
|
}
|
|
184
195
|
|
|
196
|
+
function newestQuotaBandState(rows: ProviderQuotaRow[]): string {
|
|
197
|
+
const row = rows
|
|
198
|
+
.filter((candidate) => candidate.quota_band_state)
|
|
199
|
+
.sort((a, b) => b.updated_at - a.updated_at)[0];
|
|
200
|
+
return row?.quota_band_state ?? "{}";
|
|
201
|
+
}
|
|
202
|
+
|
|
185
203
|
function mergeProviderQuotaRows(provider: string, accountKey: string, oldAccountKeys: string[], now: number): number {
|
|
186
204
|
const keys = [...new Set(oldAccountKeys.filter((key) => key && key !== accountKey))];
|
|
187
205
|
if (keys.length === 0) return 0;
|
|
@@ -203,9 +221,9 @@ function mergeProviderQuotaRows(provider: string, accountKey: string, oldAccount
|
|
|
203
221
|
getDb().query(`
|
|
204
222
|
INSERT INTO provider_quotas (
|
|
205
223
|
provider, account_key, quota_state, codex_reset_credits, source_agent_ids, last_updated_at,
|
|
206
|
-
last_attempt_at, last_error, quota_advisory_state, updated_at, created_at
|
|
224
|
+
last_attempt_at, last_error, quota_advisory_state, quota_band_state, updated_at, created_at
|
|
207
225
|
)
|
|
208
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
226
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
209
227
|
ON CONFLICT(provider, account_key) DO UPDATE SET
|
|
210
228
|
quota_state = excluded.quota_state,
|
|
211
229
|
codex_reset_credits = excluded.codex_reset_credits,
|
|
@@ -214,6 +232,7 @@ function mergeProviderQuotaRows(provider: string, accountKey: string, oldAccount
|
|
|
214
232
|
last_attempt_at = excluded.last_attempt_at,
|
|
215
233
|
last_error = excluded.last_error,
|
|
216
234
|
quota_advisory_state = excluded.quota_advisory_state,
|
|
235
|
+
quota_band_state = excluded.quota_band_state,
|
|
217
236
|
updated_at = excluded.updated_at
|
|
218
237
|
`).run(
|
|
219
238
|
provider,
|
|
@@ -225,6 +244,7 @@ function mergeProviderQuotaRows(provider: string, accountKey: string, oldAccount
|
|
|
225
244
|
lastAttemptAt || now,
|
|
226
245
|
newestLastError(rows),
|
|
227
246
|
newestQuotaAdvisoryState(rows),
|
|
247
|
+
newestQuotaBandState(rows),
|
|
228
248
|
updatedAt,
|
|
229
249
|
createdAt,
|
|
230
250
|
);
|
|
@@ -552,17 +572,19 @@ export function upsertProviderQuota(input: ProviderQuotaUpdateInput, now = Date.
|
|
|
552
572
|
recordProviderQuotaUnavailable({ provider, accountKey, error: input.lastError, sourceAgentId: input.sourceAgentId }, now);
|
|
553
573
|
}
|
|
554
574
|
if (hasFreshSuccessfulProviderQuota(input, now) && mergedQuota?.acceptedIncomingWindow) {
|
|
555
|
-
const
|
|
575
|
+
const advisoryResult = providerQuotaAdvisoryHandler?.({
|
|
556
576
|
provider,
|
|
557
577
|
accountKey,
|
|
558
578
|
previousQuota: existingQuota ? publicQuotaState(existingQuota) : undefined,
|
|
559
579
|
quota: publicQuotaState(mergedQuota.quota),
|
|
560
580
|
sourceAgentIds,
|
|
561
581
|
previousAdvisoryState: existing?.quota_advisory_state,
|
|
582
|
+
previousBandState: existing?.quota_band_state,
|
|
562
583
|
now,
|
|
563
584
|
});
|
|
564
|
-
if (
|
|
565
|
-
getDb().query("UPDATE provider_quotas SET quota_advisory_state = ? WHERE provider = ? AND account_key = ?")
|
|
585
|
+
if (advisoryResult !== undefined) {
|
|
586
|
+
getDb().query("UPDATE provider_quotas SET quota_advisory_state = ?, quota_band_state = ? WHERE provider = ? AND account_key = ?")
|
|
587
|
+
.run(advisoryResult.advisoryState, advisoryResult.bandState, provider, accountKey);
|
|
566
588
|
}
|
|
567
589
|
}
|
|
568
590
|
if (!unstableProviderQuotaAccountKey(accountKey)) {
|
|
@@ -185,6 +185,27 @@ export function countActiveScheduledTimers(createdBy: string): number {
|
|
|
185
185
|
return row.count;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// #1004 — a command timer whose background command is still pending/running is active work
|
|
189
|
+
// executing on the creator's behalf (a shell command on an orchestrator), even though its
|
|
190
|
+
// provider turn already ended and reports idle. A prompt-wakeup timer (no command) is just a
|
|
191
|
+
// future alarm and never counts. These back the running-command → busy DISPLAY derivation; the
|
|
192
|
+
// timer/command row is the source of truth, so the signal self-heals when the command settles.
|
|
193
|
+
const RUNNING_COMMAND_TIMER_PREDICATE =
|
|
194
|
+
"enabled = 1 AND command IS NOT NULL AND json_extract(command, '$.status') IN ('pending', 'running')";
|
|
195
|
+
|
|
196
|
+
export function agentHasRunningCommandTimer(createdBy: string): boolean {
|
|
197
|
+
return getDb()
|
|
198
|
+
.query(`SELECT 1 FROM scheduled_timers WHERE created_by = ? AND ${RUNNING_COMMAND_TIMER_PREDICATE} LIMIT 1`)
|
|
199
|
+
.get(createdBy) != null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function agentsWithRunningCommandTimers(): Set<string> {
|
|
203
|
+
const rows = getDb()
|
|
204
|
+
.query(`SELECT DISTINCT created_by FROM scheduled_timers WHERE ${RUNNING_COMMAND_TIMER_PREDICATE}`)
|
|
205
|
+
.all() as Array<{ created_by: string }>;
|
|
206
|
+
return new Set(rows.map((row) => row.created_by));
|
|
207
|
+
}
|
|
208
|
+
|
|
188
209
|
export function listDueScheduledTimerIds(now: number, limit = 25): string[] {
|
|
189
210
|
const rows = getDb().query(`
|
|
190
211
|
SELECT id FROM scheduled_timers
|
package/src/db/schema.ts
CHANGED
|
@@ -366,6 +366,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
366
366
|
codex_reset_credits TEXT,
|
|
367
367
|
source_agent_ids TEXT NOT NULL DEFAULT '[]',
|
|
368
368
|
quota_advisory_state TEXT NOT NULL DEFAULT '{}',
|
|
369
|
+
quota_band_state TEXT NOT NULL DEFAULT '{}',
|
|
369
370
|
last_updated_at INTEGER,
|
|
370
371
|
last_attempt_at INTEGER NOT NULL,
|
|
371
372
|
last_error TEXT,
|
package/src/db/tasks.ts
CHANGED
|
@@ -327,15 +327,45 @@ export function renewTaskClaim(taskId: number, agentId: string, guard?: AgentSes
|
|
|
327
327
|
return { ok: true, task: getTask(taskId)! };
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
|
|
330
|
+
/**
|
|
331
|
+
* The AUTHENTICATED principal behind a status write, derived at the transport boundary from the
|
|
332
|
+
* signed token (`ctx.callerAgentId` / scopes) — NEVER from the request body. The ownership fence
|
|
333
|
+
* keys on this, not `input.agentId`, so omitting/spoofing the body field cannot advance another
|
|
334
|
+
* agent's claim (#1063).
|
|
335
|
+
*/
|
|
336
|
+
export interface TaskStatusActor {
|
|
337
|
+
/** The caller's own agent id (`ctx.callerAgentId`). undefined for full-reach/system/multi-agent tokens. */
|
|
338
|
+
agentId?: string;
|
|
339
|
+
/** Full-reach `*`, internal `system`, or a task-admin scope: may advance a task it does not own. */
|
|
340
|
+
privileged?: boolean;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function updateTaskStatus(taskId: number, input: TaskStatusInput, actor?: TaskStatusActor): { ok: boolean; error?: string; conflict?: boolean; task?: Task; event?: TaskEvent } {
|
|
331
344
|
const task = getTask(taskId);
|
|
332
345
|
if (!task) return { ok: false, error: "task not found" };
|
|
333
346
|
// Fence: the claim-lane status updater writes `claimed_by`; deliverable Tasks must never
|
|
334
347
|
// be mutated here. They transition through setTaskStatus (src/db/task-entity.ts), which
|
|
335
348
|
// never touches claimed_by. (#834)
|
|
336
349
|
if (task.kind === DELIVERABLE_TASK_KIND) return { ok: false, error: "deliverable task: use the task-entity status API" };
|
|
350
|
+
// Ownership fence (#1061, hardened #1063): once a task is claimed, only its current owner may
|
|
351
|
+
// advance the status. The acting identity is the AUTHENTICATED caller (`actor.agentId`, resolved
|
|
352
|
+
// from the signed token at the route) — NEVER the client-supplied `input.agentId`. Trusting the
|
|
353
|
+
// body let any task:write holder omit or spoof it and steal another agent's claim (#1063).
|
|
354
|
+
// A privileged caller (full-reach `*`, internal `system`, or a task-admin scope) may advance a
|
|
355
|
+
// task it does not own — the same escape hatch #1043's `ensureCanMintThread` grants. An undefined
|
|
356
|
+
// `actor` is an in-process trusted caller (automations, tests) and is likewise privileged.
|
|
357
|
+
// A null claimed_by still self-claims (fresh status write).
|
|
358
|
+
const privileged = actor?.privileged ?? actor === undefined;
|
|
359
|
+
const callerAgentId = actor?.agentId;
|
|
360
|
+
if (!privileged && task.claimedBy && callerAgentId !== task.claimedBy) {
|
|
361
|
+
return { ok: false, error: `task claimed by ${task.claimedBy}`, conflict: true };
|
|
362
|
+
}
|
|
337
363
|
const now = Date.now();
|
|
338
|
-
|
|
364
|
+
// Effective owner/session identity. A privileged caller may act on behalf of the supplied
|
|
365
|
+
// `input.agentId` (or the existing owner — the automation/system self-claim path); an ordinary
|
|
366
|
+
// caller is bound to its authenticated identity and must NOT fall back to `claimedBy` — that
|
|
367
|
+
// fallback was the #1063 bypass that let a missing `agentId` silently inherit the owner.
|
|
368
|
+
const agentId = privileged ? (input.agentId ?? task.claimedBy ?? null) : (callerAgentId ?? null);
|
|
339
369
|
if (agentId) {
|
|
340
370
|
const session = validateAgentSession(agentId, input);
|
|
341
371
|
if (!session.ok) return { ok: false, error: session.error };
|
|
@@ -360,7 +390,7 @@ export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok:
|
|
|
360
390
|
}
|
|
361
391
|
}
|
|
362
392
|
const event = insertTaskEvent(taskId, {
|
|
363
|
-
source:
|
|
393
|
+
source: agentId ?? "agent-relay",
|
|
364
394
|
type: `status:${input.status}`,
|
|
365
395
|
severity: task.severity,
|
|
366
396
|
title: `Task ${input.status}`,
|
package/src/db/work-evidence.ts
CHANGED
|
@@ -24,13 +24,16 @@ export function hasArtifactCreatedBy(createdBy: string, since: number = 0): bool
|
|
|
24
24
|
|
|
25
25
|
export function listFinalResponseCaptureMessages(from: string, since: number = 0, limit: number = 20): Message[] {
|
|
26
26
|
const safeLimit = Math.min(Math.max(Math.floor(limit), 1), 100);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
const rows = getDb().query(`
|
|
28
|
+
${MSG_SELECT}
|
|
29
|
+
WHERE m.from_agent = ?
|
|
30
|
+
AND m.kind = 'chat'
|
|
31
|
+
AND m.created_at >= ?
|
|
32
|
+
AND json_extract(m.payload, '$.responseCapture.type') = 'response'
|
|
33
|
+
AND (
|
|
34
|
+
json_extract(m.payload, '$.responseCapture.turnFinal') = 1
|
|
35
|
+
OR json_extract(m.payload, '$.responseCapture.final') = 1
|
|
36
|
+
)
|
|
34
37
|
ORDER BY m.id DESC
|
|
35
38
|
LIMIT ?
|
|
36
39
|
`).all(from, since, safeLimit) as any[];
|
package/src/event-loop-lag.ts
CHANGED
|
@@ -53,6 +53,21 @@ export function eventLoopLagMaxMs(): number {
|
|
|
53
53
|
return maxMs;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Cumulative retained lag — the SUM of every sample in the ring, i.e. the total
|
|
57
|
+
// event-loop processing debt across the retained window. SIDE-EFFECT-FREE (like
|
|
58
|
+
// eventLoopLagMaxMs it never touches maxSinceLastReadMs, so reading it doesn't steal
|
|
59
|
+
// the /health poller's spike). This is the stale/bus reaper's grace (#1055): the max
|
|
60
|
+
// SINGLE sample (#1048) covers one contiguous stall (the 337s incident) but UNDER-
|
|
61
|
+
// covers sustained NON-contiguous degradation — many sub-TTL stalls whose backlog
|
|
62
|
+
// still sums past STALE_TTL. Summing every stall in the window extends the grace to the
|
|
63
|
+
// real accumulated heartbeat backlog. Always >= eventLoopLagMaxMs(), so it can only
|
|
64
|
+
// WIDEN the #1048 grace, never shrink it — no new false-reap risk introduced.
|
|
65
|
+
export function eventLoopLagGraceMs(): number {
|
|
66
|
+
let total = 0;
|
|
67
|
+
for (let i = 0; i < sampleCount; i += 1) total += samples[i] ?? 0;
|
|
68
|
+
return total;
|
|
69
|
+
}
|
|
70
|
+
|
|
56
71
|
export function getEventLoopLagSnapshot(): EventLoopLagSnapshot {
|
|
57
72
|
const maxMs = eventLoopLagMaxMs();
|
|
58
73
|
const maxSinceLastRead = maxSinceLastReadMs;
|
package/src/gate-resolver.ts
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
// outright. A project that declared gates knows better than any heuristic.
|
|
10
10
|
// 2. Derive from `package.json` — run the repo's DECLARED scripts via its detected
|
|
11
11
|
// package manager (from `packageManager` field or lockfile). A repo whose
|
|
12
|
-
// `scripts.test` is `jest` gets `<pm> run test`, never `bun test`.
|
|
12
|
+
// `scripts.test` is `jest` gets `<pm> run test`, never `bun test`. When the repo
|
|
13
|
+
// declares a full-gate script (`ci:land` preferred, else `ci`), that IS its gate
|
|
14
|
+
// (the set the repo wants enforced before land/release): the steward runs it
|
|
15
|
+
// wholesale so land gates == release gates (#1044/#1089), instead of a
|
|
16
|
+
// file-relevance subset that would let drift/lint/doc breakage land.
|
|
13
17
|
// 3. Default — the legacy bun heuristic, only when there's nothing to derive from
|
|
14
18
|
// (no repo path, or no readable package.json).
|
|
15
19
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -98,6 +102,20 @@ export function resolveStewardChecks(files: string[], repoRoot?: string): Stewar
|
|
|
98
102
|
const pkg = repoRoot ? readPackageInfo(repoRoot) : null;
|
|
99
103
|
if (pkg) {
|
|
100
104
|
const runScript = (name: string) => `${pkg.pm} run ${name}`;
|
|
105
|
+
// 2a. Land==release parity (#1044): a repo that declares its full validation gate as
|
|
106
|
+
// a script has ALREADY named the set the release enforces. Run that ONE gate wholesale
|
|
107
|
+
// instead of a file-relevance subset (typecheck+test), so a land can't introduce
|
|
108
|
+
// drift/lint/doc breakage that only a narrower gate misses and that would otherwise
|
|
109
|
+
// surface at release time (failing `*:check`/lint gates).
|
|
110
|
+
//
|
|
111
|
+
// Prefer an explicit `ci:land` script when the repo declares one: it is the repo's
|
|
112
|
+
// named pre-land full gate, so the steward should respect that contract over a more
|
|
113
|
+
// generic `ci`. Repos that want exact parity can make `ci` delegate to `ci:land`;
|
|
114
|
+
// repos that need a different land-safe full gate still have a first-class name for it.
|
|
115
|
+
const fullGate = pkg.scripts["ci:land"] ? "ci:land" : pkg.scripts.ci ? "ci" : undefined;
|
|
116
|
+
if (fullGate && files.length) {
|
|
117
|
+
return [{ command: runScript(fullGate), reason: `repo declares a full "${fullGate}" gate (land==release parity)` }];
|
|
118
|
+
}
|
|
101
119
|
if (has(/\.(ts|tsx|mts|cts)$/) && pkg.scripts.typecheck) {
|
|
102
120
|
checks.push({ command: runScript("typecheck"), reason: "TypeScript files changed" });
|
|
103
121
|
}
|
package/src/maintenance/jobs.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getArtifactStorage } from "../artifact-storage";
|
|
2
2
|
import { deriveAgentBusyHealth } from "../agent-busy-health";
|
|
3
3
|
import { expireStaleBusAgents } from "../bus";
|
|
4
|
-
import {
|
|
4
|
+
import { eventLoopLagGraceMs } from "../event-loop-lag";
|
|
5
5
|
import { pruneOutbox } from "../bus-outbox";
|
|
6
6
|
import { expireCommands } from "../commands-db";
|
|
7
7
|
import { getRetentionConfig } from "../retention-config-store";
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
releaseExpiredClaims,
|
|
23
23
|
releaseExpiredDriveLeases,
|
|
24
24
|
releaseOrphanedTasks,
|
|
25
|
+
revokeOrphanedRuntimeTokens,
|
|
25
26
|
runDbMaintenanceAsync,
|
|
26
27
|
sweepArtifacts,
|
|
27
28
|
} from "../db";
|
|
@@ -278,7 +279,7 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
278
279
|
intervalMs: REAP_INTERVAL_MS,
|
|
279
280
|
runOnStart: true,
|
|
280
281
|
handler() {
|
|
281
|
-
const expired = expireStaleBusAgents(undefined,
|
|
282
|
+
const expired = expireStaleBusAgents(undefined, eventLoopLagGraceMs());
|
|
282
283
|
for (const id of expired.agentIds) {
|
|
283
284
|
getLifecycleManager().onAgentDisappeared(id);
|
|
284
285
|
// #636/#659 — only after the stale reconnect grace expires do we turn a bare
|
|
@@ -301,11 +302,13 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
301
302
|
intervalMs: REAP_INTERVAL_MS,
|
|
302
303
|
runOnStart: true,
|
|
303
304
|
handler() {
|
|
304
|
-
// #1048 — gate staleness on the relay's OWN event-loop health.
|
|
305
|
-
// is side-effect-free (does not consume the /health poller's
|
|
306
|
-
//
|
|
305
|
+
// #1048/#1055 — gate staleness on the relay's OWN event-loop health.
|
|
306
|
+
// eventLoopLagGraceMs() is side-effect-free (does not consume the /health poller's
|
|
307
|
+
// spike max) and sums the retained window's lag, so BOTH one contiguous stall and
|
|
308
|
+
// sustained non-contiguous overload (many sub-TTL stalls whose backlog sums past the
|
|
309
|
+
// TTL) extend the grace. Keeps live agents whose heartbeats we simply couldn't
|
|
307
310
|
// process from being reaped as dead.
|
|
308
|
-
const lagGraceMs =
|
|
311
|
+
const lagGraceMs = eventLoopLagGraceMs();
|
|
309
312
|
const reapedAgentIds = reapStaleAgents(STALE_TTL_MS, lagGraceMs);
|
|
310
313
|
for (const id of reapedAgentIds) {
|
|
311
314
|
emitAgentStatus(id);
|
|
@@ -336,6 +339,20 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
336
339
|
return { prunedAgentIds };
|
|
337
340
|
},
|
|
338
341
|
},
|
|
342
|
+
{
|
|
343
|
+
id: "orphan-token-revoker",
|
|
344
|
+
title: "Orphaned runtime-token revoker",
|
|
345
|
+
description: "Revoke ('stale', re-mintable) the runtime tokens of agents whose process the orchestrator confirmed dead and that have not been heard from since.",
|
|
346
|
+
intervalMs: REAP_INTERVAL_MS,
|
|
347
|
+
runOnStart: false,
|
|
348
|
+
handler() {
|
|
349
|
+
// #1057(1) — same lag discipline as the stale reaper (#1048/#1055): while the relay's own
|
|
350
|
+
// event loop was stalled, a live replacement's heartbeats may still be unprocessed, so the
|
|
351
|
+
// grace widens by the observed lag instead of misreading the backlog as post-mortem silence.
|
|
352
|
+
const revokedOrphanTokens = revokeOrphanedRuntimeTokens(STALE_TTL_MS, eventLoopLagGraceMs());
|
|
353
|
+
return { revokedOrphanTokens };
|
|
354
|
+
},
|
|
355
|
+
},
|
|
339
356
|
{
|
|
340
357
|
id: "orphaned-session-reaper",
|
|
341
358
|
title: "Orphaned session reaper",
|
package/src/managed-policy.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { getManifest } from "agent-relay-providers";
|
|
2
|
-
import { DEFAULT_USER_ID } from "agent-relay-sdk";
|
|
3
2
|
import { getAgentProfile } from "./config-store";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { withResolvedProvisioning } from "./db";
|
|
4
|
+
import { composeProfileSpawnInstructions } from "./project-instructions";
|
|
6
5
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
7
6
|
import { buildSpawnCommand, resolveSpawnModelParams } from "./spawn-command";
|
|
8
7
|
import { applyDefaultSharedMcp } from "./shared-mcp";
|
|
@@ -58,16 +57,10 @@ export function buildManagedSpawnParams(policy: SpawnPolicy, requestId: string,
|
|
|
58
57
|
// keepalive prompt folds in as the caller append → one ordered `--append-system-prompt` block,
|
|
59
58
|
// no double-inject. The projection is deterministic (sorted + deduped), so re-running it on a
|
|
60
59
|
// 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
60
|
const alwaysOnAppend = managedPolicyAppendSystemPrompt(policy);
|
|
66
|
-
const composed =
|
|
67
|
-
|
|
68
|
-
...(
|
|
69
|
-
...(profileAnchor ? { profileAnchor } : {}),
|
|
70
|
-
...(userAnchor ? { userAnchor } : {}),
|
|
61
|
+
const composed = composeProfileSpawnInstructions({
|
|
62
|
+
cwd: policy.cwd,
|
|
63
|
+
...(policy.profile ? { profileName: policy.profile } : {}),
|
|
71
64
|
...(agentProfile ? { profile: agentProfile } : {}),
|
|
72
65
|
...(alwaysOnAppend ? { callerSystemPromptAppend: alwaysOnAppend } : {}),
|
|
73
66
|
});
|
package/src/mcp-plan-tools.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { McpNotFoundError } from "./mcp-errors";
|
|
1
|
+
import { McpAuthError, McpNotFoundError } from "./mcp-errors";
|
|
2
2
|
import { ValidationError } from "./db";
|
|
3
|
+
import { ServiceAuthError } from "./services/errors";
|
|
3
4
|
import { authContextFromMcp } from "./services/auth-context";
|
|
4
5
|
import {
|
|
5
6
|
addPlanEdge,
|
|
@@ -126,6 +127,17 @@ function stringArg(value: unknown, field: string): string {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
export function relayPlanTool(auth: McpAuthLike, name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
130
|
+
try {
|
|
131
|
+
return dispatchPlanTool(auth, name, args);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
// A service-layer auth failure (e.g. #1043's message:send gate on the auto-mint) is an auth
|
|
134
|
+
// error on the wire — map it like the messaging tools do so the MCP edge returns 403.
|
|
135
|
+
if (e instanceof ServiceAuthError) throw new McpAuthError(e.message);
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function dispatchPlanTool(auth: McpAuthLike, name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
129
141
|
const ctx = authContextFromMcp(auth);
|
|
130
142
|
if (name === "relay_plan_create") return createPlan(args as unknown as Parameters<typeof createPlan>[0], ctx) as unknown as Record<string, unknown>;
|
|
131
143
|
if (name === "relay_plan_add_node") {
|