agent-relay-server 0.75.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +94 -1
- package/package.json +2 -2
- package/public/assets/MessageBubble-Ct4RjObq.js +3 -0
- package/public/assets/{MessageBubble-opNe8Lu2.js.map → MessageBubble-Ct4RjObq.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-_mpO-kEh.js → PlanGraphPanel-BQcLye6d.js} +2 -2
- package/public/assets/{PlanGraphPanel-_mpO-kEh.js.map → PlanGraphPanel-BQcLye6d.js.map} +1 -1
- package/public/assets/{activity-B079s-aD.js → activity-DeQhQ17b.js} +2 -2
- package/public/assets/{activity-B079s-aD.js.map → activity-DeQhQ17b.js.map} +1 -1
- package/public/assets/{agent-profiles-D1X5BBW_.js → agent-profiles-DwR6zuEJ.js} +2 -2
- package/public/assets/{agent-profiles-D1X5BBW_.js.map → agent-profiles-DwR6zuEJ.js.map} +1 -1
- package/public/assets/{agents-kkvfOptR.js → agents-coulRN38.js} +2 -2
- package/public/assets/{agents-kkvfOptR.js.map → agents-coulRN38.js.map} +1 -1
- package/public/assets/{analytics-e_HOZBPx.js → analytics-BTq4XQiF.js} +3 -3
- package/public/assets/{analytics-e_HOZBPx.js.map → analytics-BTq4XQiF.js.map} +1 -1
- package/public/assets/{automation-B4CAzi64.js → automation-V97mnoZp.js} +2 -2
- package/public/assets/{automation-B4CAzi64.js.map → automation-V97mnoZp.js.map} +1 -1
- package/public/assets/{badge-YQhT30dy.js → badge-BxG6_yfp.js} +2 -2
- package/public/assets/{badge-YQhT30dy.js.map → badge-BxG6_yfp.js.map} +1 -1
- package/public/assets/branch-state-badge-DaCW_qGP.js +2 -0
- package/public/assets/{branch-state-badge-B7T81Oct.js.map → branch-state-badge-DaCW_qGP.js.map} +1 -1
- package/public/assets/{button-CaFI76Ja.js → button-D5k3GbF6.js} +2 -2
- package/public/assets/{button-CaFI76Ja.js.map → button-D5k3GbF6.js.map} +1 -1
- package/public/assets/{card-B_fQtKce.js → card-DKyFtrez.js} +2 -2
- package/public/assets/{card-B_fQtKce.js.map → card-DKyFtrez.js.map} +1 -1
- package/public/assets/{channels-BAD_kIo7.js → channels-EHZtUB_U.js} +2 -2
- package/public/assets/{channels-BAD_kIo7.js.map → channels-EHZtUB_U.js.map} +1 -1
- package/public/assets/chat-B8fs8r1x.js +2 -0
- package/public/assets/{chat-ZfR8BvTL.js.map → chat-B8fs8r1x.js.map} +1 -1
- package/public/assets/{connectors-Cdjtxly2.js → connectors-B3BbETXb.js} +2 -2
- package/public/assets/{connectors-Cdjtxly2.js.map → connectors-B3BbETXb.js.map} +1 -1
- package/public/assets/copy-button-D3XAkVC2.js +2 -0
- package/public/assets/{copy-button-m00JmflM.js.map → copy-button-D3XAkVC2.js.map} +1 -1
- package/public/assets/dist-B9_RgXN9.js +2 -0
- package/public/assets/{dist-DfQP9-VC.js.map → dist-B9_RgXN9.js.map} +1 -1
- package/public/assets/dist-BuYGwGZ-.js +2 -0
- package/public/assets/{dist-CydxJz3m.js.map → dist-BuYGwGZ-.js.map} +1 -1
- package/public/assets/{dist-ChP4KvKi.js → dist-CAu7-DrN.js} +2 -2
- package/public/assets/{dist-ChP4KvKi.js.map → dist-CAu7-DrN.js.map} +1 -1
- package/public/assets/{dist-DeAdFM5_.js → dist-CF2meFCY.js} +2 -2
- package/public/assets/{dist-DeAdFM5_.js.map → dist-CF2meFCY.js.map} +1 -1
- package/public/assets/{dist-B_yrHbsb.js → dist-CR7zKC1F.js} +2 -2
- package/public/assets/{dist-B_yrHbsb.js.map → dist-CR7zKC1F.js.map} +1 -1
- package/public/assets/{dist-C-viWls-.js → dist-CUxt3gHd.js} +2 -2
- package/public/assets/{dist-C-viWls-.js.map → dist-CUxt3gHd.js.map} +1 -1
- package/public/assets/{dist-D8Yr1AIJ.js → dist-D1Bwx4cc.js} +2 -2
- package/public/assets/{dist-D8Yr1AIJ.js.map → dist-D1Bwx4cc.js.map} +1 -1
- package/public/assets/{dist-Cnlh_NpI.js → dist-D32lF-j6.js} +2 -2
- package/public/assets/{dist-Cnlh_NpI.js.map → dist-D32lF-j6.js.map} +1 -1
- package/public/assets/{dist-DeM72J6a.js → dist-DDrB3y1I.js} +2 -2
- package/public/assets/{dist-DeM72J6a.js.map → dist-DDrB3y1I.js.map} +1 -1
- package/public/assets/dist-DPQKhU73.js +2 -0
- package/public/assets/{dist-CYS2puT9.js.map → dist-DPQKhU73.js.map} +1 -1
- package/public/assets/{dist-D7K3qgp7.js → dist-YjHj0NpU.js} +2 -2
- package/public/assets/{dist-D7K3qgp7.js.map → dist-YjHj0NpU.js.map} +1 -1
- package/public/assets/{es2015-C3DnXBqu.js → es2015-WF4MRcZQ.js} +2 -2
- package/public/assets/{es2015-C3DnXBqu.js.map → es2015-WF4MRcZQ.js.map} +1 -1
- package/public/assets/{formatted-body-impl-Bunmh796.js → formatted-body-impl-CQ-8HQ7u.js} +2 -2
- package/public/assets/{formatted-body-impl-Bunmh796.js.map → formatted-body-impl-CQ-8HQ7u.js.map} +1 -1
- package/public/assets/index-DJtda8l2.js +22 -0
- package/public/assets/index-DJtda8l2.js.map +1 -0
- package/public/assets/index-DvNy9IVg.css +2 -0
- package/public/assets/{input-Cqqq0OFl.js → input-DSJQbsdS.js} +2 -2
- package/public/assets/{input-Cqqq0OFl.js.map → input-DSJQbsdS.js.map} +1 -1
- package/public/assets/{insights-D9KEB80Q.js → insights-CtT5cEif.js} +2 -2
- package/public/assets/{insights-D9KEB80Q.js.map → insights-CtT5cEif.js.map} +1 -1
- package/public/assets/{integrations-DzRla8mj.js → integrations-BWBxWl3k.js} +2 -2
- package/public/assets/{integrations-DzRla8mj.js.map → integrations-BWBxWl3k.js.map} +1 -1
- package/public/assets/{lib-6L4m0E3M.js → lib-BxQSXiQ7.js} +3 -3
- package/public/assets/{lib-6L4m0E3M.js.map → lib-BxQSXiQ7.js.map} +1 -1
- package/public/assets/{lucide-react-DI8kTavp.js → lucide-react-BH2ROV8n.js} +2 -2
- package/public/assets/{lucide-react-DI8kTavp.js.map → lucide-react-BH2ROV8n.js.map} +1 -1
- package/public/assets/{maintenance-C0sGj_Na.js → maintenance-BWLMjX8h.js} +2 -2
- package/public/assets/{maintenance-C0sGj_Na.js.map → maintenance-BWLMjX8h.js.map} +1 -1
- package/public/assets/{managed-agents-BQq9EXOq.js → managed-agents-KxHV0VcQ.js} +2 -2
- package/public/assets/{managed-agents-BQq9EXOq.js.map → managed-agents-KxHV0VcQ.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-B6xOjT1c.js → markdown-preview-impl-C5b5cBM0.js} +2 -2
- package/public/assets/{markdown-preview-impl-B6xOjT1c.js.map → markdown-preview-impl-C5b5cBM0.js.map} +1 -1
- package/public/assets/{memory-CB6zkEnm.js → memory-Bh3UgQCk.js} +2 -2
- package/public/assets/{memory-CB6zkEnm.js.map → memory-Bh3UgQCk.js.map} +1 -1
- package/public/assets/{messages-DhQjlDGk.js → messages-Dg7TSxpZ.js} +2 -2
- package/public/assets/{messages-DhQjlDGk.js.map → messages-Dg7TSxpZ.js.map} +1 -1
- package/public/assets/{orchestrators-Ze5VJVFK.js → orchestrators-BBKW7bO0.js} +2 -2
- package/public/assets/{orchestrators-Ze5VJVFK.js.map → orchestrators-BBKW7bO0.js.map} +1 -1
- package/public/assets/{overview-KxPTf_aQ.js → overview-a8lG62AZ.js} +2 -2
- package/public/assets/{overview-KxPTf_aQ.js.map → overview-a8lG62AZ.js.map} +1 -1
- package/public/assets/{pairs-CKAQN-Mp.js → pairs-ChSYXFwu.js} +2 -2
- package/public/assets/{pairs-CKAQN-Mp.js.map → pairs-ChSYXFwu.js.map} +1 -1
- package/public/assets/{projects-DluLrkwe.js → projects-DIN_O7AJ.js} +2 -2
- package/public/assets/{projects-DluLrkwe.js.map → projects-DIN_O7AJ.js.map} +1 -1
- package/public/assets/{react-dom-Ct8qXivQ.js → react-dom-B_tGtv3Y.js} +2 -2
- package/public/assets/{react-dom-Ct8qXivQ.js.map → react-dom-B_tGtv3Y.js.map} +1 -1
- package/public/assets/{security-BfN02cIU.js → security-D0obEjDT.js} +2 -2
- package/public/assets/{security-BfN02cIU.js.map → security-D0obEjDT.js.map} +1 -1
- package/public/assets/{select-2k89h2cU.js → select-DwCP2Jhh.js} +2 -2
- package/public/assets/{select-2k89h2cU.js.map → select-DwCP2Jhh.js.map} +1 -1
- package/public/assets/settings-f1l2R0e8.js +4 -0
- package/public/assets/{settings-CxHa6IHI.js.map → settings-f1l2R0e8.js.map} +1 -1
- package/public/assets/{store-D1K4_xId.js → store-m74m5eNF.js} +3 -3
- package/public/assets/{store-D1K4_xId.js.map → store-m74m5eNF.js.map} +1 -1
- package/public/assets/{tasks-B7S6eTJJ.js → tasks-DDVfmUst.js} +2 -2
- package/public/assets/{tasks-B7S6eTJJ.js.map → tasks-DDVfmUst.js.map} +1 -1
- package/public/assets/{teams-DPEBGRiy.js → teams-COZKriLe.js} +2 -2
- package/public/assets/{teams-DPEBGRiy.js.map → teams-COZKriLe.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-PeLYPKpp.js → terminal-viewer-impl-CjOZ__aw.js} +3 -3
- package/public/assets/{terminal-viewer-impl-PeLYPKpp.js.map → terminal-viewer-impl-CjOZ__aw.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-DxmCAbQi.js → use-keyboard-viewport-BVAU-Ud8.js} +2 -2
- package/public/assets/{use-keyboard-viewport-DxmCAbQi.js.map → use-keyboard-viewport-BVAU-Ud8.js.map} +1 -1
- package/public/assets/work-queue-BC7heUfD.js +2 -0
- package/public/assets/{work-queue-BI_d8G0U.js.map → work-queue-BC7heUfD.js.map} +1 -1
- package/public/assets/workspaces-CIGMb3ST.js +3 -0
- package/public/assets/{workspaces-B_nIm6Tw.js.map → workspaces-CIGMb3ST.js.map} +1 -1
- package/public/index.html +21 -21
- package/src/db/agents.ts +4 -46
- package/src/db/index.ts +1 -0
- package/src/db/migrations.ts +91 -1
- package/src/db/provider-quotas.ts +220 -0
- package/src/db/schema.ts +16 -1
- package/src/lifecycle-context-pressure.ts +98 -0
- package/src/lifecycle-manager.ts +21 -31
- package/src/quota.ts +14 -1
- package/src/routes/index.ts +3 -0
- package/src/routes/provider-quotas.ts +48 -0
- package/src/security.ts +1 -0
- package/src/token-db.ts +3 -3
- package/src/upgrade-local-id.ts +23 -0
- package/src/upgrade.ts +19 -27
- package/public/assets/MessageBubble-opNe8Lu2.js +0 -3
- package/public/assets/branch-state-badge-B7T81Oct.js +0 -2
- package/public/assets/chat-ZfR8BvTL.js +0 -2
- package/public/assets/copy-button-m00JmflM.js +0 -2
- package/public/assets/dist-CYS2puT9.js +0 -2
- package/public/assets/dist-CydxJz3m.js +0 -2
- package/public/assets/dist-DfQP9-VC.js +0 -2
- package/public/assets/index-CQQTCZxg.css +0 -2
- package/public/assets/index-DePqIpr0.js +0 -23
- package/public/assets/index-DePqIpr0.js.map +0 -1
- package/public/assets/settings-CxHa6IHI.js +0 -4
- package/public/assets/work-queue-BI_d8G0U.js +0 -2
- package/public/assets/workspaces-B_nIm6Tw.js +0 -3
package/src/db/migrations.ts
CHANGED
|
@@ -602,13 +602,103 @@ export function applyMigrations(): void {
|
|
|
602
602
|
getDb().run(`
|
|
603
603
|
CREATE TABLE IF NOT EXISTS quota_snapshots (
|
|
604
604
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
605
|
-
|
|
605
|
+
provider TEXT NOT NULL,
|
|
606
|
+
account_key TEXT NOT NULL,
|
|
607
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
606
608
|
windows TEXT NOT NULL,
|
|
607
609
|
source TEXT NOT NULL,
|
|
608
610
|
quota_state TEXT NOT NULL,
|
|
609
611
|
captured_at INTEGER NOT NULL
|
|
610
612
|
)
|
|
611
613
|
`);
|
|
614
|
+
const quotaSnapshotCols = (getDb().query("PRAGMA table_info(quota_snapshots)").all() as Array<{ name: string }>).map((col) => col.name);
|
|
615
|
+
if (!quotaSnapshotCols.includes("provider") || !quotaSnapshotCols.includes("account_key")) {
|
|
616
|
+
getDb().run("ALTER TABLE quota_snapshots RENAME TO quota_snapshots_legacy_agent");
|
|
617
|
+
getDb().run(`
|
|
618
|
+
CREATE TABLE quota_snapshots (
|
|
619
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
620
|
+
provider TEXT NOT NULL,
|
|
621
|
+
account_key TEXT NOT NULL,
|
|
622
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
623
|
+
windows TEXT NOT NULL,
|
|
624
|
+
source TEXT NOT NULL,
|
|
625
|
+
quota_state TEXT NOT NULL,
|
|
626
|
+
captured_at INTEGER NOT NULL
|
|
627
|
+
)
|
|
628
|
+
`);
|
|
629
|
+
getDb().run(`
|
|
630
|
+
INSERT INTO quota_snapshots (id, provider, account_key, agent_id, windows, source, quota_state, captured_at)
|
|
631
|
+
SELECT
|
|
632
|
+
qs.id,
|
|
633
|
+
CASE
|
|
634
|
+
WHEN json_extract(a.meta, '$.provider') IS NOT NULL THEN lower(json_extract(a.meta, '$.provider'))
|
|
635
|
+
WHEN a.tags LIKE '%"codex"%' OR lower(a.id) LIKE '%codex%' OR lower(a.name) LIKE '%codex%' THEN 'codex'
|
|
636
|
+
ELSE 'claude'
|
|
637
|
+
END,
|
|
638
|
+
coalesce(
|
|
639
|
+
nullif(json_extract(a.meta, '$.accountKey'), ''),
|
|
640
|
+
nullif(json_extract(a.meta, '$.accountId'), ''),
|
|
641
|
+
nullif(json_extract(a.meta, '$.orgId'), ''),
|
|
642
|
+
'host:' || coalesce(a.machine, a.rig, 'unknown')
|
|
643
|
+
),
|
|
644
|
+
qs.agent_id,
|
|
645
|
+
qs.windows,
|
|
646
|
+
qs.source,
|
|
647
|
+
qs.quota_state,
|
|
648
|
+
qs.captured_at
|
|
649
|
+
FROM quota_snapshots_legacy_agent qs
|
|
650
|
+
LEFT JOIN agents a ON a.id = qs.agent_id
|
|
651
|
+
`);
|
|
652
|
+
getDb().run("DROP TABLE quota_snapshots_legacy_agent");
|
|
653
|
+
}
|
|
654
|
+
getDb().run(`
|
|
655
|
+
CREATE TABLE IF NOT EXISTS provider_quotas (
|
|
656
|
+
provider TEXT NOT NULL,
|
|
657
|
+
account_key TEXT NOT NULL,
|
|
658
|
+
quota_state TEXT,
|
|
659
|
+
source_agent_ids TEXT NOT NULL DEFAULT '[]',
|
|
660
|
+
last_updated_at INTEGER,
|
|
661
|
+
last_attempt_at INTEGER NOT NULL,
|
|
662
|
+
last_error TEXT,
|
|
663
|
+
updated_at INTEGER NOT NULL,
|
|
664
|
+
created_at INTEGER NOT NULL,
|
|
665
|
+
PRIMARY KEY (provider, account_key)
|
|
666
|
+
)
|
|
667
|
+
`);
|
|
668
|
+
getDb().run(`
|
|
669
|
+
INSERT INTO provider_quotas (
|
|
670
|
+
provider, account_key, quota_state, source_agent_ids, last_updated_at,
|
|
671
|
+
last_attempt_at, last_error, updated_at, created_at
|
|
672
|
+
)
|
|
673
|
+
SELECT
|
|
674
|
+
CASE
|
|
675
|
+
WHEN json_extract(meta, '$.provider') IS NOT NULL THEN lower(json_extract(meta, '$.provider'))
|
|
676
|
+
WHEN tags LIKE '%"codex"%' OR lower(id) LIKE '%codex%' OR lower(name) LIKE '%codex%' THEN 'codex'
|
|
677
|
+
ELSE 'claude'
|
|
678
|
+
END AS provider,
|
|
679
|
+
coalesce(
|
|
680
|
+
nullif(json_extract(meta, '$.accountKey'), ''),
|
|
681
|
+
nullif(json_extract(meta, '$.accountId'), ''),
|
|
682
|
+
nullif(json_extract(meta, '$.orgId'), ''),
|
|
683
|
+
'host:' || coalesce(machine, rig, 'unknown')
|
|
684
|
+
) AS account_key,
|
|
685
|
+
quota_state,
|
|
686
|
+
json_array(id),
|
|
687
|
+
coalesce(json_extract(quota_state, '$.updatedAt'), last_seen),
|
|
688
|
+
coalesce(json_extract(quota_state, '$.updatedAt'), last_seen),
|
|
689
|
+
NULL,
|
|
690
|
+
last_seen,
|
|
691
|
+
created_at
|
|
692
|
+
FROM agents
|
|
693
|
+
WHERE quota_state IS NOT NULL
|
|
694
|
+
ON CONFLICT(provider, account_key) DO UPDATE SET
|
|
695
|
+
quota_state = excluded.quota_state,
|
|
696
|
+
source_agent_ids = excluded.source_agent_ids,
|
|
697
|
+
last_updated_at = max(coalesce(provider_quotas.last_updated_at, 0), coalesce(excluded.last_updated_at, 0)),
|
|
698
|
+
last_attempt_at = max(provider_quotas.last_attempt_at, excluded.last_attempt_at),
|
|
699
|
+
updated_at = max(provider_quotas.updated_at, excluded.updated_at)
|
|
700
|
+
`);
|
|
701
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_quota_snapshots_account_time ON quota_snapshots(provider, account_key, captured_at DESC)");
|
|
612
702
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_quota_snapshots_agent_time ON quota_snapshots(agent_id, captured_at DESC)");
|
|
613
703
|
|
|
614
704
|
getDb().run(`
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { DAY_MS } from "../config";
|
|
2
|
+
import { parseJson } from "../utils";
|
|
3
|
+
import { getDb } from "./connection.ts";
|
|
4
|
+
import { parseStringArray } from "./shared.ts";
|
|
5
|
+
import type { AgentCard, ProviderQuotaError, ProviderQuotaMatrix, ProviderQuotaRecord, ProviderQuotaSnapshot, ProviderQuotaUpdateInput, QuotaState } from "../types";
|
|
6
|
+
|
|
7
|
+
const PROVIDER_QUOTA_STALE_AFTER_MS = 10 * 60_000;
|
|
8
|
+
|
|
9
|
+
type ProviderQuotaRow = {
|
|
10
|
+
provider: string;
|
|
11
|
+
account_key: string;
|
|
12
|
+
quota_state?: string | null;
|
|
13
|
+
source_agent_ids?: string | null;
|
|
14
|
+
last_updated_at?: number | null;
|
|
15
|
+
last_attempt_at: number;
|
|
16
|
+
last_error?: string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ProviderQuotaSnapshotRow = {
|
|
20
|
+
provider: string;
|
|
21
|
+
account_key: string;
|
|
22
|
+
agent_id?: string | null;
|
|
23
|
+
quota_state: string;
|
|
24
|
+
captured_at: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function cleanKey(value: string): string {
|
|
28
|
+
return value.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeProvider(value: string): string {
|
|
32
|
+
return cleanKey(value).toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeSourceAgentIds(value: unknown): string[] {
|
|
36
|
+
return [...new Set(parseStringArray(typeof value === "string" ? value : JSON.stringify(value ?? [])).filter(Boolean))];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function quotaFingerprint(quota: QuotaState): string {
|
|
40
|
+
return JSON.stringify(quota.windows.map((window) => ({
|
|
41
|
+
name: window.name,
|
|
42
|
+
unit: window.unit,
|
|
43
|
+
utilization: window.utilization,
|
|
44
|
+
})).sort((a, b) => a.name.localeCompare(b.name) || a.unit.localeCompare(b.unit)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rowToSnapshot(row: ProviderQuotaSnapshotRow): ProviderQuotaSnapshot {
|
|
48
|
+
return {
|
|
49
|
+
provider: row.provider,
|
|
50
|
+
accountKey: row.account_key,
|
|
51
|
+
capturedAt: row.captured_at,
|
|
52
|
+
...(row.agent_id ? { sourceAgentId: row.agent_id } : {}),
|
|
53
|
+
quota: parseJson<QuotaState>(row.quota_state, { windows: [], source: "probe", updatedAt: row.captured_at }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function rowToRecord(row: ProviderQuotaRow, history: ProviderQuotaSnapshot[], now: number): ProviderQuotaRecord {
|
|
58
|
+
const lastError = parseJson<ProviderQuotaError | undefined>(row.last_error, undefined);
|
|
59
|
+
const lastUpdatedAt = row.last_updated_at ?? undefined;
|
|
60
|
+
return {
|
|
61
|
+
provider: row.provider,
|
|
62
|
+
accountKey: row.account_key,
|
|
63
|
+
...(row.quota_state ? { quota: parseJson<QuotaState>(row.quota_state, { windows: [], source: "probe", updatedAt: lastUpdatedAt ?? row.last_attempt_at }) } : {}),
|
|
64
|
+
sourceAgentIds: normalizeSourceAgentIds(row.source_agent_ids),
|
|
65
|
+
...(lastUpdatedAt !== undefined ? { lastUpdatedAt } : {}),
|
|
66
|
+
lastAttemptAt: row.last_attempt_at,
|
|
67
|
+
...(lastError ? { lastError } : {}),
|
|
68
|
+
stale: lastUpdatedAt === undefined || now - lastUpdatedAt > PROVIDER_QUOTA_STALE_AFTER_MS,
|
|
69
|
+
history,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function latestSnapshotFingerprint(provider: string, accountKey: string): string | undefined {
|
|
74
|
+
const row = getDb().query(`
|
|
75
|
+
SELECT quota_state FROM quota_snapshots
|
|
76
|
+
WHERE provider = ? AND account_key = ?
|
|
77
|
+
ORDER BY captured_at DESC, id DESC
|
|
78
|
+
LIMIT 1
|
|
79
|
+
`).get(provider, accountKey) as { quota_state: string } | undefined;
|
|
80
|
+
return row ? quotaFingerprint(parseJson<QuotaState>(row.quota_state, { windows: [], source: "probe", updatedAt: 0 })) : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function providerQuotaIdentityForAgent(agent: Pick<AgentCard, "id" | "name" | "tags" | "machine" | "rig" | "meta">): { provider: string; accountKey: string } {
|
|
84
|
+
const meta = agent.meta ?? {};
|
|
85
|
+
const provider = typeof meta.provider === "string" && meta.provider.trim()
|
|
86
|
+
? meta.provider
|
|
87
|
+
: agent.tags.includes("codex") || agent.id.toLowerCase().includes("codex") || agent.name.toLowerCase().includes("codex")
|
|
88
|
+
? "codex"
|
|
89
|
+
: "claude";
|
|
90
|
+
const accountKey = typeof meta.accountKey === "string" && meta.accountKey.trim()
|
|
91
|
+
? meta.accountKey
|
|
92
|
+
: typeof meta.accountId === "string" && meta.accountId.trim()
|
|
93
|
+
? meta.accountId
|
|
94
|
+
: typeof meta.orgId === "string" && meta.orgId.trim()
|
|
95
|
+
? meta.orgId
|
|
96
|
+
: `host:${agent.machine ?? agent.rig ?? "unknown"}`;
|
|
97
|
+
return { provider: normalizeProvider(provider), accountKey: cleanKey(accountKey) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function upsertProviderQuota(input: ProviderQuotaUpdateInput, now = Date.now()): ProviderQuotaRecord {
|
|
101
|
+
const provider = normalizeProvider(input.provider);
|
|
102
|
+
const accountKey = cleanKey(input.accountKey);
|
|
103
|
+
if (!provider) throw new Error("provider required");
|
|
104
|
+
if (!accountKey) throw new Error("accountKey required");
|
|
105
|
+
const existing = getDb().query("SELECT * FROM provider_quotas WHERE provider = ? AND account_key = ?").get(provider, accountKey) as ProviderQuotaRow | undefined;
|
|
106
|
+
const sourceAgentIds = normalizeSourceAgentIds(existing?.source_agent_ids);
|
|
107
|
+
if (input.sourceAgentId && !sourceAgentIds.includes(input.sourceAgentId)) sourceAgentIds.push(input.sourceAgentId);
|
|
108
|
+
const lastUpdatedAt = input.quota ? input.quota.updatedAt : existing?.last_updated_at ?? undefined;
|
|
109
|
+
const lastAttemptAt = input.lastAttemptAt ?? input.quota?.updatedAt ?? now;
|
|
110
|
+
const lastError = input.lastError === undefined
|
|
111
|
+
? input.quota ? null : existing?.last_error ?? null
|
|
112
|
+
: input.lastError;
|
|
113
|
+
const quotaState = input.quota ? JSON.stringify(input.quota) : existing?.quota_state ?? null;
|
|
114
|
+
|
|
115
|
+
getDb().query(`
|
|
116
|
+
INSERT INTO provider_quotas (
|
|
117
|
+
provider, account_key, quota_state, source_agent_ids, last_updated_at,
|
|
118
|
+
last_attempt_at, last_error, updated_at, created_at
|
|
119
|
+
)
|
|
120
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
121
|
+
ON CONFLICT(provider, account_key) DO UPDATE SET
|
|
122
|
+
quota_state = excluded.quota_state,
|
|
123
|
+
source_agent_ids = excluded.source_agent_ids,
|
|
124
|
+
last_updated_at = excluded.last_updated_at,
|
|
125
|
+
last_attempt_at = excluded.last_attempt_at,
|
|
126
|
+
last_error = excluded.last_error,
|
|
127
|
+
updated_at = excluded.updated_at
|
|
128
|
+
`).run(
|
|
129
|
+
provider,
|
|
130
|
+
accountKey,
|
|
131
|
+
quotaState,
|
|
132
|
+
JSON.stringify(sourceAgentIds),
|
|
133
|
+
lastUpdatedAt ?? null,
|
|
134
|
+
lastAttemptAt,
|
|
135
|
+
lastError ? JSON.stringify(lastError) : null,
|
|
136
|
+
now,
|
|
137
|
+
now,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (input.quota && latestSnapshotFingerprint(provider, accountKey) !== quotaFingerprint(input.quota)) {
|
|
141
|
+
recordProviderQuotaSnapshot({ provider, accountKey, quota: input.quota, sourceAgentId: input.sourceAgentId }, now);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return listProviderQuotas({ provider, accountKey, historyLimit: 24, now }).quotas[0]!;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function recordProviderQuotaSnapshot(input: { provider: string; accountKey: string; quota: QuotaState; sourceAgentId?: string }, now: number): void {
|
|
148
|
+
const sourceAgentId = input.sourceAgentId && getDb().query("SELECT 1 FROM agents WHERE id = ?").get(input.sourceAgentId)
|
|
149
|
+
? input.sourceAgentId
|
|
150
|
+
: undefined;
|
|
151
|
+
getDb().query(`
|
|
152
|
+
INSERT INTO quota_snapshots (provider, account_key, agent_id, windows, source, quota_state, captured_at)
|
|
153
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
154
|
+
`).run(
|
|
155
|
+
input.provider,
|
|
156
|
+
input.accountKey,
|
|
157
|
+
sourceAgentId ?? null,
|
|
158
|
+
JSON.stringify(input.quota.windows),
|
|
159
|
+
input.quota.source,
|
|
160
|
+
JSON.stringify(input.quota),
|
|
161
|
+
now,
|
|
162
|
+
);
|
|
163
|
+
pruneProviderQuotaSnapshots(input.provider, input.accountKey, now - DAY_MS);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function pruneProviderQuotaSnapshots(provider?: string, accountKey?: string, olderThan = Date.now() - DAY_MS): number {
|
|
167
|
+
const result = provider && accountKey
|
|
168
|
+
? getDb().query("DELETE FROM quota_snapshots WHERE provider = ? AND account_key = ? AND captured_at < ?").run(provider, accountKey, olderThan)
|
|
169
|
+
: getDb().query("DELETE FROM quota_snapshots WHERE captured_at < ?").run(olderThan);
|
|
170
|
+
return result.changes;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function listProviderQuotaSnapshots(provider: string, accountKey: string, options: { since?: number; limit?: number } = {}): ProviderQuotaSnapshot[] {
|
|
174
|
+
const limit = Math.max(1, Math.min(options.limit ?? 288, 1000));
|
|
175
|
+
const params: any[] = [normalizeProvider(provider), cleanKey(accountKey)];
|
|
176
|
+
let sql = "SELECT * FROM quota_snapshots WHERE provider = ? AND account_key = ?";
|
|
177
|
+
if (options.since !== undefined) {
|
|
178
|
+
sql += " AND captured_at >= ?";
|
|
179
|
+
params.push(options.since);
|
|
180
|
+
}
|
|
181
|
+
sql += " ORDER BY captured_at ASC, id ASC LIMIT ?";
|
|
182
|
+
params.push(limit);
|
|
183
|
+
return (getDb().query(sql).all(...params) as ProviderQuotaSnapshotRow[]).map(rowToSnapshot);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function listAgentQuotaSnapshotsFromProviderStore(agent: AgentCard, options: { since?: number; limit?: number } = {}) {
|
|
187
|
+
const identity = providerQuotaIdentityForAgent(agent);
|
|
188
|
+
return listProviderQuotaSnapshots(identity.provider, identity.accountKey, options).map((snapshot) => ({
|
|
189
|
+
agentId: agent.id,
|
|
190
|
+
capturedAt: snapshot.capturedAt,
|
|
191
|
+
windows: snapshot.quota.windows,
|
|
192
|
+
source: snapshot.quota.source,
|
|
193
|
+
updatedAt: snapshot.quota.updatedAt,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function listProviderQuotas(options: { provider?: string; accountKey?: string; since?: number; historyLimit?: number; now?: number } = {}): ProviderQuotaMatrix {
|
|
198
|
+
const historyLimit = Math.max(0, Math.min(options.historyLimit ?? 24, 288));
|
|
199
|
+
const now = options.now ?? Date.now();
|
|
200
|
+
const params: any[] = [];
|
|
201
|
+
let sql = "SELECT * FROM provider_quotas WHERE 1=1";
|
|
202
|
+
if (options.provider) {
|
|
203
|
+
sql += " AND provider = ?";
|
|
204
|
+
params.push(normalizeProvider(options.provider));
|
|
205
|
+
}
|
|
206
|
+
if (options.accountKey) {
|
|
207
|
+
sql += " AND account_key = ?";
|
|
208
|
+
params.push(cleanKey(options.accountKey));
|
|
209
|
+
}
|
|
210
|
+
sql += " ORDER BY provider ASC, account_key ASC";
|
|
211
|
+
const rows = getDb().query(sql).all(...params) as ProviderQuotaRow[];
|
|
212
|
+
return {
|
|
213
|
+
staleAfterMs: PROVIDER_QUOTA_STALE_AFTER_MS,
|
|
214
|
+
quotas: rows.map((row) => rowToRecord(
|
|
215
|
+
row,
|
|
216
|
+
historyLimit > 0 ? listProviderQuotaSnapshots(row.provider, row.account_key, { since: options.since, limit: historyLimit }) : [],
|
|
217
|
+
now,
|
|
218
|
+
)),
|
|
219
|
+
};
|
|
220
|
+
}
|
package/src/db/schema.ts
CHANGED
|
@@ -292,7 +292,9 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
292
292
|
|
|
293
293
|
CREATE TABLE IF NOT EXISTS quota_snapshots (
|
|
294
294
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
295
|
-
|
|
295
|
+
provider TEXT NOT NULL,
|
|
296
|
+
account_key TEXT NOT NULL,
|
|
297
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
296
298
|
windows TEXT NOT NULL,
|
|
297
299
|
source TEXT NOT NULL,
|
|
298
300
|
quota_state TEXT NOT NULL,
|
|
@@ -300,6 +302,19 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
300
302
|
);
|
|
301
303
|
CREATE INDEX IF NOT EXISTS idx_quota_snapshots_agent_time ON quota_snapshots(agent_id, captured_at DESC);
|
|
302
304
|
|
|
305
|
+
CREATE TABLE IF NOT EXISTS provider_quotas (
|
|
306
|
+
provider TEXT NOT NULL,
|
|
307
|
+
account_key TEXT NOT NULL,
|
|
308
|
+
quota_state TEXT,
|
|
309
|
+
source_agent_ids TEXT NOT NULL DEFAULT '[]',
|
|
310
|
+
last_updated_at INTEGER,
|
|
311
|
+
last_attempt_at INTEGER NOT NULL,
|
|
312
|
+
last_error TEXT,
|
|
313
|
+
updated_at INTEGER NOT NULL,
|
|
314
|
+
created_at INTEGER NOT NULL,
|
|
315
|
+
PRIMARY KEY (provider, account_key)
|
|
316
|
+
);
|
|
317
|
+
|
|
303
318
|
CREATE TABLE IF NOT EXISTS agent_continuations (
|
|
304
319
|
agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
|
|
305
320
|
objective TEXT NOT NULL,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createCommand } from "./commands-db";
|
|
2
|
+
import { getAgent, listAgents } from "./db";
|
|
3
|
+
import { getManagedAgentState } from "./config-store";
|
|
4
|
+
import { emitCommandEvent } from "./command-events";
|
|
5
|
+
import { handleContextPressure } from "./context-advisory";
|
|
6
|
+
import type { AgentCard, Command, SpawnPolicy } from "./types";
|
|
7
|
+
|
|
8
|
+
interface LifecycleContextPressureInput {
|
|
9
|
+
policies: SpawnPolicy[];
|
|
10
|
+
now: () => number;
|
|
11
|
+
hasActiveCommand: (target: string, type: string) => boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CompactAgentInput {
|
|
15
|
+
agent: AgentCard;
|
|
16
|
+
reason: string;
|
|
17
|
+
policyName?: string;
|
|
18
|
+
alwaysReload?: string[];
|
|
19
|
+
now: () => number;
|
|
20
|
+
hasActiveCommand: (target: string, type: string) => boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function reconcileLifecycleContextPressure(input: LifecycleContextPressureInput): Set<string> {
|
|
24
|
+
const byPolicy = new Map(input.policies.map((policy) => [policy.name, policy]));
|
|
25
|
+
const candidates = new Map<string, { agent: AgentCard; policy?: SpawnPolicy }>();
|
|
26
|
+
|
|
27
|
+
for (const policy of input.policies) {
|
|
28
|
+
const state = getManagedAgentState(policy.name);
|
|
29
|
+
if (state?.status !== "running" || !state.agentId) continue;
|
|
30
|
+
const agent = getAgent(state.agentId);
|
|
31
|
+
if (!agent || agent.status === "offline") continue;
|
|
32
|
+
candidates.set(agent.id, { agent, policy });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const agent of listAgents()) {
|
|
36
|
+
if (!isLiveRunnerManagedAgent(agent) || candidates.has(agent.id)) continue;
|
|
37
|
+
const policyName = typeof agent.meta?.policyName === "string" ? agent.meta.policyName : undefined;
|
|
38
|
+
candidates.set(agent.id, { agent, policy: policyName ? byPolicy.get(policyName) : undefined });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handled = new Set<string>();
|
|
42
|
+
for (const { agent, policy } of candidates.values()) {
|
|
43
|
+
if (handleContextPressure({
|
|
44
|
+
agent,
|
|
45
|
+
now: input.now,
|
|
46
|
+
hasActiveCommand: input.hasActiveCommand,
|
|
47
|
+
compact: (reason) => requestAgentCompact({
|
|
48
|
+
agent,
|
|
49
|
+
reason,
|
|
50
|
+
policyName: policy?.name,
|
|
51
|
+
alwaysReload: alwaysReloadTags(agent.tags),
|
|
52
|
+
now: input.now,
|
|
53
|
+
hasActiveCommand: input.hasActiveCommand,
|
|
54
|
+
}),
|
|
55
|
+
})) {
|
|
56
|
+
handled.add(agent.id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return handled;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function requestAgentCompact(input: CompactAgentInput): Command | null {
|
|
63
|
+
if (input.hasActiveCommand(input.agent.id, "agent.compact")) return null;
|
|
64
|
+
const spawnRequestId = typeof input.agent.meta?.spawnRequestId === "string" ? input.agent.meta.spawnRequestId : undefined;
|
|
65
|
+
const sessionName = typeof input.agent.meta?.sessionName === "string" ? input.agent.meta.sessionName : undefined;
|
|
66
|
+
const tmuxSession = typeof input.agent.meta?.tmuxSession === "string" ? input.agent.meta.tmuxSession : undefined;
|
|
67
|
+
const command = createCommand({
|
|
68
|
+
type: "agent.compact",
|
|
69
|
+
source: "system",
|
|
70
|
+
target: input.agent.id,
|
|
71
|
+
correlationId: spawnRequestId,
|
|
72
|
+
params: {
|
|
73
|
+
action: "compact",
|
|
74
|
+
agentId: input.agent.id,
|
|
75
|
+
...(input.policyName ? { policyName: input.policyName } : {}),
|
|
76
|
+
...(spawnRequestId ? { spawnRequestId } : {}),
|
|
77
|
+
...(sessionName ? { sessionName } : {}),
|
|
78
|
+
...(tmuxSession ? { tmuxSession } : {}),
|
|
79
|
+
reason: input.reason,
|
|
80
|
+
memory: input.alwaysReload?.length ? { alwaysReload: input.alwaysReload } : undefined,
|
|
81
|
+
requestedBy: "lifecycle-manager",
|
|
82
|
+
requestedAt: input.now(),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
emitCommandEvent(command, "command.requested");
|
|
86
|
+
return command;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function alwaysReloadTags(tags: string[]): string[] {
|
|
90
|
+
return tags
|
|
91
|
+
.filter((tag) => tag.startsWith("memory-reload:"))
|
|
92
|
+
.map((tag) => tag.slice("memory-reload:".length).trim())
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isLiveRunnerManagedAgent(agent: AgentCard): boolean {
|
|
97
|
+
return agent.meta?.runnerManaged === true && agent.status !== "offline" && agent.status !== "stale";
|
|
98
|
+
}
|
package/src/lifecycle-manager.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
upsertManagedAgentState,
|
|
10
10
|
} from "./config-store";
|
|
11
11
|
import { emitRelayEvent } from "./events";
|
|
12
|
-
import {
|
|
12
|
+
import { alwaysReloadTags, reconcileLifecycleContextPressure, requestAgentCompact } from "./lifecycle-context-pressure";
|
|
13
13
|
import { markManagedAgentRunning, emitManagedState } from "./services/managed-running";
|
|
14
14
|
import { authContextFromSystem } from "./services/auth-context";
|
|
15
15
|
import { emitCommandEvent } from "./command-events";
|
|
@@ -66,8 +66,10 @@ export class LifecycleManager {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
async tick(): Promise<void> {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const policies = this.loadPolicies();
|
|
70
|
+
const contextHandled = this.reconcileContextPressure(policies);
|
|
71
|
+
for (const policy of policies) {
|
|
72
|
+
this.reconcilePolicy(policy, contextHandled);
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -175,7 +177,7 @@ export class LifecycleManager {
|
|
|
175
177
|
onConfigChanged(namespace: string, key: string): void {
|
|
176
178
|
if (namespace !== "spawn-policy") return;
|
|
177
179
|
const policy = this.loadPolicies().find((item) => item.name === key);
|
|
178
|
-
if (policy) this.reconcilePolicy(policy);
|
|
180
|
+
if (policy) this.reconcilePolicy(policy, this.reconcileContextPressure([policy]));
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
onRelayUpdated(_newVersion: string): void {
|
|
@@ -309,7 +311,7 @@ export class LifecycleManager {
|
|
|
309
311
|
return command;
|
|
310
312
|
}
|
|
311
313
|
|
|
312
|
-
private reconcilePolicy(policy: SpawnPolicy): void {
|
|
314
|
+
private reconcilePolicy(policy: SpawnPolicy, contextHandled = new Set<string>()): void {
|
|
313
315
|
const state = getManagedAgentState(policy.name);
|
|
314
316
|
const enabled = policy.enabled !== false;
|
|
315
317
|
if (!state) {
|
|
@@ -354,9 +356,16 @@ export class LifecycleManager {
|
|
|
354
356
|
this.stopAgent(policy, true, "idle-timeout");
|
|
355
357
|
return;
|
|
356
358
|
}
|
|
357
|
-
if (
|
|
359
|
+
if (contextHandled.has(agent.id)) return;
|
|
358
360
|
if (this.shouldCompact(agent)) {
|
|
359
|
-
|
|
361
|
+
requestAgentCompact({
|
|
362
|
+
agent,
|
|
363
|
+
reason: "context-threshold",
|
|
364
|
+
policyName: policy.name,
|
|
365
|
+
alwaysReload: alwaysReloadTags(agent.tags),
|
|
366
|
+
now: this.now,
|
|
367
|
+
hasActiveCommand: (target, type) => this.hasActiveCommand(target, type),
|
|
368
|
+
});
|
|
360
369
|
return;
|
|
361
370
|
}
|
|
362
371
|
if (policy.scheduledDailyRestart && this.dailyRestartDue(policy, state) && this.isIdle(policy, state, 3600)) {
|
|
@@ -384,24 +393,12 @@ export class LifecycleManager {
|
|
|
384
393
|
}
|
|
385
394
|
}
|
|
386
395
|
|
|
387
|
-
private
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
target: agentId,
|
|
393
|
-
params: {
|
|
394
|
-
action: "compact",
|
|
395
|
-
agentId,
|
|
396
|
-
policyName: policy.name,
|
|
397
|
-
reason,
|
|
398
|
-
memory: alwaysReload.length ? { alwaysReload } : undefined,
|
|
399
|
-
requestedBy: "lifecycle-manager",
|
|
400
|
-
requestedAt: this.now(),
|
|
401
|
-
},
|
|
396
|
+
private reconcileContextPressure(policies: SpawnPolicy[]): Set<string> {
|
|
397
|
+
return reconcileLifecycleContextPressure({
|
|
398
|
+
policies,
|
|
399
|
+
now: this.now,
|
|
400
|
+
hasActiveCommand: (target, type) => this.hasActiveCommand(target, type),
|
|
402
401
|
});
|
|
403
|
-
emitCommandEvent(command, "command.requested");
|
|
404
|
-
return command;
|
|
405
402
|
}
|
|
406
403
|
|
|
407
404
|
private ensureStoppedState(policy: SpawnPolicy): void {
|
|
@@ -654,10 +651,3 @@ function agentTerminalFailureMessage(agent: ReturnType<typeof getAgent> | null):
|
|
|
654
651
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
655
652
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
656
653
|
}
|
|
657
|
-
|
|
658
|
-
function alwaysReloadTags(tags: string[]): string[] {
|
|
659
|
-
return tags
|
|
660
|
-
.filter((tag) => tag.startsWith("memory-reload:"))
|
|
661
|
-
.map((tag) => tag.slice("memory-reload:".length).trim())
|
|
662
|
-
.filter(Boolean);
|
|
663
|
-
}
|
package/src/quota.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRecord } from "agent-relay-sdk";
|
|
2
|
-
import type { QuotaState, QuotaWindowName, QuotaSource, QuotaUnit } from "./types";
|
|
2
|
+
import type { ProviderQuotaError, QuotaState, QuotaWindowName, QuotaSource, QuotaUnit } from "./types";
|
|
3
3
|
|
|
4
4
|
const QUOTA_SOURCES = new Set<QuotaSource>(["header", "probe", "ingest"]);
|
|
5
5
|
const QUOTA_UNITS = new Set<QuotaUnit>(["tokens", "requests", "%"]);
|
|
@@ -38,3 +38,16 @@ export function quotaStateFromUnknown(value: unknown): QuotaState | undefined {
|
|
|
38
38
|
if (windows.length === 0) return undefined;
|
|
39
39
|
return { windows, source, updatedAt };
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
export function providerQuotaErrorFromUnknown(value: unknown): ProviderQuotaError | undefined {
|
|
43
|
+
if (!isRecord(value)) return undefined;
|
|
44
|
+
const type = typeof value.type === "string" && value.type.trim() ? value.type.trim() : undefined;
|
|
45
|
+
const message = typeof value.message === "string" && value.message.trim() ? value.message.trim() : undefined;
|
|
46
|
+
if (!type || !message) return undefined;
|
|
47
|
+
const retryAfterMs = numberValue(value.retryAfterMs);
|
|
48
|
+
return {
|
|
49
|
+
type,
|
|
50
|
+
message,
|
|
51
|
+
...(retryAfterMs !== undefined && retryAfterMs >= 0 ? { retryAfterMs } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/routes/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { deleteAgentTerminalSession, postAgentAction, postAgentPermissionDecisio
|
|
|
5
5
|
import { deleteArtifactById, getArtifactById, getArtifactContent, getArtifacts, headArtifactContent, postArtifact } from "./artifacts";
|
|
6
6
|
import { deleteAutomationById, getAutomationById, getAutomationRuns, getAutomations, patchAutomation, postAutomation, postAutomationRun } from "./automations";
|
|
7
7
|
import { deleteCommandById, deleteProviderModelRoute, getProviderConfigRoute, getProviderConfigsRoute, getProvidersRoute, patchProviderModelRoute, postProviderConfigTestRoute, putProviderConfigRoute, putProviderModelRoute } from "./provider-config";
|
|
8
|
+
import { getProviderQuotasRoute, postProviderQuotaRoute } from "./provider-quotas";
|
|
8
9
|
import { deleteConfigKey, getConfigKey, getConfigKeyHistory, getConfigNamespace, putConfigKey } from "./config";
|
|
9
10
|
import { deleteInboxDraftRoute, getChatHistoryImportsRoute, getInboxStateRoute, patchInboxThreadState, postChatHistoryImportRoute, putInboxDraft } from "./inbox";
|
|
10
11
|
import { deleteMemoryRoute, getMemories, getMemoryBrokerInfo, getMemoryById, getMemoryStats, patchMemory, postMemory, postMemoryInject, postMemorySearch } from "./memory";
|
|
@@ -87,6 +88,8 @@ const routes: Route[] = [
|
|
|
87
88
|
route("GET", "/api/agents", getAgents),
|
|
88
89
|
route("GET", "/api/agents/find", findAgents),
|
|
89
90
|
route("POST", "/api/route", postRouteAdvice),
|
|
91
|
+
route("GET", "/api/provider-quotas", getProviderQuotasRoute),
|
|
92
|
+
route("POST", "/api/provider-quotas", postProviderQuotaRoute),
|
|
90
93
|
route("GET", "/api/agents/spawn/directories", getHostDirectories),
|
|
91
94
|
route("GET", "/api/agents/:id/chat", getAgentChatHistory),
|
|
92
95
|
route("GET", "/api/agents/:id/context-snapshots", getAgentContextSnapshots),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isRecord } from "agent-relay-sdk";
|
|
2
|
+
import { listProviderQuotas, upsertProviderQuota } from "../db";
|
|
3
|
+
import { quotaStateFromUnknown, providerQuotaErrorFromUnknown } from "../quota";
|
|
4
|
+
import { cleanString } from "../validation";
|
|
5
|
+
import { error, json, parseBody, parseQueryInt, type Handler } from "./_shared";
|
|
6
|
+
import type { ProviderQuotaUpdateInput } from "../types";
|
|
7
|
+
|
|
8
|
+
function cleanProviderQuotaUpdate(body: unknown): ProviderQuotaUpdateInput | string {
|
|
9
|
+
if (!isRecord(body)) return "JSON object body required";
|
|
10
|
+
const provider = cleanString(body.provider, "provider", { required: true, max: 80 })!;
|
|
11
|
+
const accountKey = cleanString(body.accountKey, "accountKey", { required: true, max: 240 })!;
|
|
12
|
+
const quota = quotaStateFromUnknown(body.quota);
|
|
13
|
+
const lastAttemptAt = typeof body.lastAttemptAt === "number" && Number.isFinite(body.lastAttemptAt) ? body.lastAttemptAt : undefined;
|
|
14
|
+
const lastError = body.lastError === null ? null : providerQuotaErrorFromUnknown(body.lastError);
|
|
15
|
+
if (!quota && lastError === undefined) return "quota or lastError required";
|
|
16
|
+
const sourceAgentId = cleanString(body.sourceAgentId, "sourceAgentId", { max: 200 });
|
|
17
|
+
return {
|
|
18
|
+
provider,
|
|
19
|
+
accountKey,
|
|
20
|
+
...(quota ? { quota } : {}),
|
|
21
|
+
...(lastAttemptAt !== undefined ? { lastAttemptAt } : {}),
|
|
22
|
+
...(lastError !== undefined ? { lastError } : {}),
|
|
23
|
+
...(sourceAgentId ? { sourceAgentId } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const getProviderQuotasRoute: Handler = (req) => {
|
|
28
|
+
const url = new URL(req.url);
|
|
29
|
+
const historyLimit = parseQueryInt(url.searchParams.get("historyLimit") ?? url.searchParams.get("limit"), { min: 0, max: 288, clamp: true });
|
|
30
|
+
if (Number.isNaN(historyLimit)) return error("historyLimit must be an integer between 0 and 288");
|
|
31
|
+
const sinceRaw = url.searchParams.get("since");
|
|
32
|
+
const since = sinceRaw ? Number(sinceRaw) : undefined;
|
|
33
|
+
if (sinceRaw && (!Number.isFinite(since) || since! < 0)) return error("since must be a positive timestamp");
|
|
34
|
+
return json(listProviderQuotas({
|
|
35
|
+
provider: url.searchParams.get("provider") ?? undefined,
|
|
36
|
+
accountKey: url.searchParams.get("accountKey") ?? undefined,
|
|
37
|
+
since,
|
|
38
|
+
historyLimit: historyLimit ?? undefined,
|
|
39
|
+
}));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const postProviderQuotaRoute: Handler = async (req) => {
|
|
43
|
+
const parsed = await parseBody<unknown>(req);
|
|
44
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
45
|
+
const input = cleanProviderQuotaUpdate(parsed.body);
|
|
46
|
+
if (typeof input === "string") return error(input);
|
|
47
|
+
return json(upsertProviderQuota(input), 201);
|
|
48
|
+
};
|
package/src/security.ts
CHANGED
|
@@ -274,6 +274,7 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
274
274
|
// Spawn requires the spawn capability (see requiredScopeFor above) — matches the in-service gate.
|
|
275
275
|
if (pathname === "/api/agents/spawn") return "command:spawn";
|
|
276
276
|
if (pathname.startsWith("/api/agents")) return method === "GET" ? "agent:read" : "agent:write";
|
|
277
|
+
if (pathname === "/api/provider-quotas") return method === "GET" ? "agent:read" : "quota:write";
|
|
277
278
|
if (pathname.startsWith("/api/messages") || pathname.startsWith("/api/inbox")) return method === "GET" ? "message:read" : "message:send";
|
|
278
279
|
if (pathname.startsWith("/api/agent-profiles")) return method === "GET" ? "agent:read" : "agent:write";
|
|
279
280
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|