agent-relay-server 0.19.3 → 0.21.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 +44 -8
- package/package.json +2 -2
- package/public/index.html +565 -136
- package/runner/src/adapter.ts +3 -1
- package/scripts/install-bin-shim.cjs +16 -3
- package/src/agent-ref.ts +217 -0
- package/src/automations.ts +4 -1
- package/src/cli.ts +8 -2
- package/src/config-store.ts +37 -1
- package/src/context-router.ts +7 -7
- package/src/db.ts +218 -29
- package/src/managed-policy.ts +4 -1
- package/src/mcp.ts +208 -67
- package/src/routes.ts +28 -3
- package/src/runtime-tokens.ts +26 -1
- package/src/security.ts +3 -1
- package/src/token-db.ts +3 -3
package/src/db.ts
CHANGED
|
@@ -74,6 +74,7 @@ import type {
|
|
|
74
74
|
WorkspaceStatus,
|
|
75
75
|
} from "./types";
|
|
76
76
|
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "./config";
|
|
77
|
+
import { matchAgents } from "./agent-ref";
|
|
77
78
|
|
|
78
79
|
let db: Database;
|
|
79
80
|
const CONTEXT_SNAPSHOT_DEBOUNCE_MS = 60_000;
|
|
@@ -210,6 +211,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
210
211
|
provider_capabilities TEXT,
|
|
211
212
|
context_state TEXT,
|
|
212
213
|
meta TEXT NOT NULL DEFAULT '{}',
|
|
214
|
+
spawned_by TEXT,
|
|
213
215
|
last_seen INTEGER NOT NULL,
|
|
214
216
|
created_at INTEGER NOT NULL
|
|
215
217
|
);
|
|
@@ -1047,6 +1049,9 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
1047
1049
|
if (!agentColNames.includes("context_state")) {
|
|
1048
1050
|
db.run("ALTER TABLE agents ADD COLUMN context_state TEXT");
|
|
1049
1051
|
}
|
|
1052
|
+
if (!agentColNames.includes("spawned_by")) {
|
|
1053
|
+
db.run("ALTER TABLE agents ADD COLUMN spawned_by TEXT");
|
|
1054
|
+
}
|
|
1050
1055
|
db.run(`
|
|
1051
1056
|
CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
1052
1057
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -1245,6 +1250,7 @@ function rowToAgent(row: any): AgentCard {
|
|
|
1245
1250
|
providerCapabilities: parseJson<ProviderCapabilities | undefined>(row.provider_capabilities, undefined),
|
|
1246
1251
|
context: parseJson<ContextState | undefined>(row.context_state, undefined),
|
|
1247
1252
|
meta: parseJson(row.meta, {}),
|
|
1253
|
+
spawnedBy: row.spawned_by ?? undefined,
|
|
1248
1254
|
lastSeen: row.last_seen,
|
|
1249
1255
|
createdAt: row.created_at,
|
|
1250
1256
|
};
|
|
@@ -1747,8 +1753,8 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
|
1747
1753
|
const readyProvided = Object.prototype.hasOwnProperty.call(input, "ready");
|
|
1748
1754
|
const instanceProvided = Boolean(input.instanceId);
|
|
1749
1755
|
const stmt = db.query(`
|
|
1750
|
-
INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, meta, last_seen, created_at)
|
|
1751
|
-
VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $now, $now)
|
|
1756
|
+
INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, meta, spawned_by, last_seen, created_at)
|
|
1757
|
+
VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $spawnedBy, $now, $now)
|
|
1752
1758
|
ON CONFLICT(id) DO UPDATE SET
|
|
1753
1759
|
name = $name,
|
|
1754
1760
|
kind = $kind,
|
|
@@ -1767,6 +1773,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
|
1767
1773
|
provider_capabilities = coalesce($providerCapabilities, agents.provider_capabilities),
|
|
1768
1774
|
context_state = coalesce($contextState, agents.context_state),
|
|
1769
1775
|
meta = $meta,
|
|
1776
|
+
spawned_by = coalesce($spawnedBy, agents.spawned_by),
|
|
1770
1777
|
last_seen = $now
|
|
1771
1778
|
`);
|
|
1772
1779
|
|
|
@@ -1789,6 +1796,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
|
1789
1796
|
$providerCapabilities: input.providerCapabilities ? JSON.stringify(input.providerCapabilities) : null,
|
|
1790
1797
|
$contextState: input.context ? JSON.stringify(input.context) : null,
|
|
1791
1798
|
$meta: JSON.stringify(input.meta ?? {}),
|
|
1799
|
+
$spawnedBy: input.spawnedBy ?? null,
|
|
1792
1800
|
$now: now,
|
|
1793
1801
|
});
|
|
1794
1802
|
if (input.context) recordContextSnapshot(input.id, input.context, now);
|
|
@@ -1858,6 +1866,106 @@ export function listAgents(filter?: {
|
|
|
1858
1866
|
return (db.query(sql).all(...params) as any[]).map(rowToAgent);
|
|
1859
1867
|
}
|
|
1860
1868
|
|
|
1869
|
+
export interface AgentSearchFilter {
|
|
1870
|
+
/** Substring match on label OR name (case-insensitive). */
|
|
1871
|
+
label?: string;
|
|
1872
|
+
/** One or more capabilities (OR within the field), via json_each(capabilities). */
|
|
1873
|
+
capability?: string | string[];
|
|
1874
|
+
/** One or more tags (OR within the field), via json_each(tags). */
|
|
1875
|
+
tag?: string | string[];
|
|
1876
|
+
/** Real model provider — providerCapabilities.model.provider (e.g. "claude", "codex"). */
|
|
1877
|
+
provider?: string | string[];
|
|
1878
|
+
machine?: string | string[];
|
|
1879
|
+
/** provider/orchestrator/channel/etc. */
|
|
1880
|
+
kind?: string | string[];
|
|
1881
|
+
/** Specific status(es), the "running" shorthand (live + ready + fresh), or "all". */
|
|
1882
|
+
status?: string | string[];
|
|
1883
|
+
/** Exact parent agent id — backs the `spawnedBy:` / `spawnedBy:me` fleet filter (#221). */
|
|
1884
|
+
spawnedBy?: string;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
export interface AgentSearchSort {
|
|
1888
|
+
sortBy?: "lastActive" | "created" | "label";
|
|
1889
|
+
order?: "asc" | "desc";
|
|
1890
|
+
limit?: number;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const AGENT_SEARCH_MAX_LIMIT = 200;
|
|
1894
|
+
|
|
1895
|
+
function searchValues(value: string | string[] | undefined): string[] {
|
|
1896
|
+
if (value == null) return [];
|
|
1897
|
+
return (Array.isArray(value) ? value : [value]).filter((v): v is string => typeof v === "string" && v.length > 0);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// Unified fleet search: all filters optional and AND-combined; array values OR within a
|
|
1901
|
+
// field. One parameterized query (no JS post-filter, no injection). Backs relay_find_agents
|
|
1902
|
+
// (#219) and the running-only relay_agent_status default (#218). "running" mirrors the
|
|
1903
|
+
// canonical liveness predicate (see db.ts delivery/steward checks + isAgentOnline).
|
|
1904
|
+
export function searchAgents(filter: AgentSearchFilter = {}, sort: AgentSearchSort = {}): AgentCard[] {
|
|
1905
|
+
const clauses: string[] = ["1=1"];
|
|
1906
|
+
const params: any[] = [];
|
|
1907
|
+
const inList = (column: string, values: string[]) => {
|
|
1908
|
+
clauses.push(`${column} IN (${values.map(() => "?").join(",")})`);
|
|
1909
|
+
params.push(...values);
|
|
1910
|
+
};
|
|
1911
|
+
|
|
1912
|
+
if (filter.label) {
|
|
1913
|
+
clauses.push("(label LIKE ? OR name LIKE ?)");
|
|
1914
|
+
params.push(`%${filter.label}%`, `%${filter.label}%`);
|
|
1915
|
+
}
|
|
1916
|
+
const caps = searchValues(filter.capability);
|
|
1917
|
+
if (caps.length) {
|
|
1918
|
+
clauses.push(`EXISTS (SELECT 1 FROM json_each(capabilities) WHERE value IN (${caps.map(() => "?").join(",")}))`);
|
|
1919
|
+
params.push(...caps);
|
|
1920
|
+
}
|
|
1921
|
+
const tags = searchValues(filter.tag);
|
|
1922
|
+
if (tags.length) {
|
|
1923
|
+
clauses.push(`EXISTS (SELECT 1 FROM json_each(tags) WHERE value IN (${tags.map(() => "?").join(",")}))`);
|
|
1924
|
+
params.push(...tags);
|
|
1925
|
+
}
|
|
1926
|
+
const providers = searchValues(filter.provider);
|
|
1927
|
+
if (providers.length) {
|
|
1928
|
+
clauses.push(`json_extract(provider_capabilities, '$.model.provider') IN (${providers.map(() => "?").join(",")})`);
|
|
1929
|
+
params.push(...providers);
|
|
1930
|
+
}
|
|
1931
|
+
const machines = searchValues(filter.machine);
|
|
1932
|
+
if (machines.length) inList("machine", machines);
|
|
1933
|
+
const kinds = searchValues(filter.kind);
|
|
1934
|
+
if (kinds.length) inList("kind", kinds);
|
|
1935
|
+
if (filter.spawnedBy) {
|
|
1936
|
+
clauses.push("spawned_by = ?");
|
|
1937
|
+
params.push(filter.spawnedBy);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
const statuses = searchValues(filter.status);
|
|
1941
|
+
if (!statuses.includes("all")) {
|
|
1942
|
+
if (statuses.includes("running")) {
|
|
1943
|
+
clauses.push("status NOT IN ('offline', 'stale') AND ready = 1 AND last_seen > ?");
|
|
1944
|
+
params.push(Date.now() - STALE_TTL_MS);
|
|
1945
|
+
}
|
|
1946
|
+
const explicit = statuses.filter((s) => s !== "running" && s !== "all");
|
|
1947
|
+
if (explicit.length) inList("status", explicit);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
const sortColumn = sort.sortBy === "created" ? "created_at" : sort.sortBy === "label" ? "label COLLATE NOCASE" : "last_seen";
|
|
1951
|
+
const order = sort.order === "asc" ? "ASC" : "DESC";
|
|
1952
|
+
const limit = Math.max(1, Math.min(sort.limit ?? 50, AGENT_SEARCH_MAX_LIMIT));
|
|
1953
|
+
params.push(limit);
|
|
1954
|
+
|
|
1955
|
+
const sql = `SELECT * FROM agents WHERE ${clauses.join(" AND ")} ORDER BY ${sortColumn} ${order} LIMIT ?`;
|
|
1956
|
+
return (db.query(sql).all(...params) as any[]).map(rowToAgent);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// Count of a parent's currently-LIVE children — the spawn quota numerator (#221). "Live"
|
|
1960
|
+
// mirrors the running predicate (not offline/stale, ready, fresh); a child that exits frees
|
|
1961
|
+
// a slot. Uncapped COUNT (unlike searchAgents' LIMIT) so the quota is exact at any fleet size.
|
|
1962
|
+
export function countLiveSpawnedAgents(parentId: string, now: number = Date.now()): number {
|
|
1963
|
+
const row = db.query(
|
|
1964
|
+
"SELECT count(*) AS n FROM agents WHERE spawned_by = ? AND status NOT IN ('offline', 'stale') AND ready = 1 AND last_seen > ?",
|
|
1965
|
+
).get(parentId, now - STALE_TTL_MS) as { n: number };
|
|
1966
|
+
return row.n;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1861
1969
|
export function setStatus(id: string, status: AgentCard["status"], guard?: AgentSessionGuard): boolean {
|
|
1862
1970
|
if (!validateAgentSession(id, guard).ok) return false;
|
|
1863
1971
|
const now = Date.now();
|
|
@@ -2350,17 +2458,6 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
|
|
|
2350
2458
|
})();
|
|
2351
2459
|
}
|
|
2352
2460
|
|
|
2353
|
-
export function findAgentsByCapability(capability: string, onlineOnly = true): AgentCard[] {
|
|
2354
|
-
let sql = `SELECT * FROM agents WHERE EXISTS (SELECT 1 FROM json_each(capabilities) WHERE value = ?)`;
|
|
2355
|
-
const params: any[] = [capability];
|
|
2356
|
-
if (onlineOnly) {
|
|
2357
|
-
sql += ` AND status != 'offline' AND last_seen > ?`;
|
|
2358
|
-
params.push(Date.now() - STALE_TTL_MS);
|
|
2359
|
-
}
|
|
2360
|
-
sql += " ORDER BY last_seen DESC";
|
|
2361
|
-
return (db.query(sql).all(...params) as any[]).map(rowToAgent);
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
2461
|
export function deleteAgent(id: string): { ok: boolean; error?: string } {
|
|
2365
2462
|
if (id === "user" || id === "system") {
|
|
2366
2463
|
return { ok: false, error: "built-in agent cannot be deleted" };
|
|
@@ -3023,21 +3120,6 @@ function pairPeer(pair: PairSession, agentId: string): string {
|
|
|
3023
3120
|
return pair.requesterId === agentId ? pair.targetId : pair.requesterId;
|
|
3024
3121
|
}
|
|
3025
3122
|
|
|
3026
|
-
function agentMatchesPairTarget(agent: AgentCard, target: string): boolean {
|
|
3027
|
-
if (target.startsWith("id:")) return agent.id === target.slice(3);
|
|
3028
|
-
if (target.startsWith("label:")) return agent.label === target.slice(6);
|
|
3029
|
-
if (target.startsWith("tag:")) return agent.tags.includes(target.slice(4));
|
|
3030
|
-
if (target.startsWith("cap:")) return agent.capabilities.includes(target.slice(4));
|
|
3031
|
-
if (target.startsWith("rig:")) return agent.rig === target.slice(4);
|
|
3032
|
-
if (target.startsWith("machine:")) return agent.machine === target.slice(8);
|
|
3033
|
-
return agent.id === target ||
|
|
3034
|
-
agent.label === target ||
|
|
3035
|
-
agent.tags.includes(target) ||
|
|
3036
|
-
agent.capabilities.includes(target) ||
|
|
3037
|
-
agent.rig === target ||
|
|
3038
|
-
agent.name === target;
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
3123
|
function resolvePairTarget(target: string, requesterId: string): {
|
|
3042
3124
|
ok: true;
|
|
3043
3125
|
agent: AgentCard;
|
|
@@ -3048,7 +3130,7 @@ function resolvePairTarget(target: string, requesterId: string): {
|
|
|
3048
3130
|
matches?: AgentCard[];
|
|
3049
3131
|
busy?: Array<{ agent: AgentCard; pair: PairSession }>;
|
|
3050
3132
|
} {
|
|
3051
|
-
const matches = listAgents()
|
|
3133
|
+
const matches = matchAgents(target, listAgents(), { excludeId: requesterId });
|
|
3052
3134
|
if (matches.length === 0) return { ok: false, code: "not_found", error: `no agent matches ${target}` };
|
|
3053
3135
|
|
|
3054
3136
|
const live = matches.filter((agent) => agent.status !== "offline" && agent.ready);
|
|
@@ -4678,6 +4760,113 @@ export function getStats(): {
|
|
|
4678
4760
|
return { version: VERSION, agents, online, messages, messagesLast24h, tasks, openTasks };
|
|
4679
4761
|
}
|
|
4680
4762
|
|
|
4763
|
+
// Analytics aggregation periods. Bucket counts mirror the dashboard chart
|
|
4764
|
+
// granularity so the client renders pre-aggregated buckets directly. This is the
|
|
4765
|
+
// single source of truth — the dashboard no longer windows raw messages (which
|
|
4766
|
+
// capped it at the last ~100 rows ≈ 3h on a busy relay; the data was always
|
|
4767
|
+
// retained server-side per RETENTION_DAYS). See issue #212 for the Layer-2 rollup
|
|
4768
|
+
// that will let stats outlive the message-retention window.
|
|
4769
|
+
export const ANALYTICS_PERIODS = {
|
|
4770
|
+
"1h": { ms: 3_600_000, buckets: 12 },
|
|
4771
|
+
"6h": { ms: 21_600_000, buckets: 12 },
|
|
4772
|
+
"24h": { ms: 86_400_000, buckets: 24 },
|
|
4773
|
+
"7d": { ms: 604_800_000, buckets: 14 },
|
|
4774
|
+
"14d": { ms: 1_209_600_000, buckets: 14 },
|
|
4775
|
+
"30d": { ms: 2_592_000_000, buckets: 15 },
|
|
4776
|
+
} as const;
|
|
4777
|
+
|
|
4778
|
+
export type AnalyticsPeriod = keyof typeof ANALYTICS_PERIODS;
|
|
4779
|
+
|
|
4780
|
+
// Message → category, the ONE place this mapping lives (server-side SQL). Order is
|
|
4781
|
+
// significant: a claimable/system/pair/channel message is classified as such even
|
|
4782
|
+
// when it is also a reply; only an otherwise-plain reply counts as "Replies".
|
|
4783
|
+
export const ANALYTICS_CATEGORIES = ["Messages", "Replies", "Work items", "System", "Pair", "Channel"] as const;
|
|
4784
|
+
export type AnalyticsCategory = (typeof ANALYTICS_CATEGORIES)[number];
|
|
4785
|
+
const ANALYTICS_CATEGORY_SQL = `
|
|
4786
|
+
CASE
|
|
4787
|
+
WHEN claimable = 1 THEN 'Work items'
|
|
4788
|
+
WHEN kind = 'system' OR from_agent = 'system' THEN 'System'
|
|
4789
|
+
WHEN kind = 'pair' THEN 'Pair'
|
|
4790
|
+
WHEN kind = 'channel.event' THEN 'Channel'
|
|
4791
|
+
WHEN reply_to IS NOT NULL THEN 'Replies'
|
|
4792
|
+
ELSE 'Messages'
|
|
4793
|
+
END`;
|
|
4794
|
+
|
|
4795
|
+
export interface AnalyticsData {
|
|
4796
|
+
period: AnalyticsPeriod;
|
|
4797
|
+
start: number;
|
|
4798
|
+
now: number;
|
|
4799
|
+
bucketMs: number;
|
|
4800
|
+
bucketCount: number;
|
|
4801
|
+
// Per-bucket category counts, oldest → newest, length === bucketCount.
|
|
4802
|
+
buckets: Array<{ start: number; counts: Record<AnalyticsCategory, number> }>;
|
|
4803
|
+
// Category totals over the whole period (powers the breakdown donut).
|
|
4804
|
+
categories: Record<AnalyticsCategory, number>;
|
|
4805
|
+
// Busiest-hours grid: heatmap[dayOfWeek 0=Sun][hour 0-23], in server-local time.
|
|
4806
|
+
heatmap: number[][];
|
|
4807
|
+
totalMessages: number;
|
|
4808
|
+
totalReactions: number;
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
function emptyCategoryCounts(): Record<AnalyticsCategory, number> {
|
|
4812
|
+
return { Messages: 0, Replies: 0, "Work items": 0, System: 0, Pair: 0, Channel: 0 };
|
|
4813
|
+
}
|
|
4814
|
+
|
|
4815
|
+
export function getAnalytics(period: AnalyticsPeriod, now: number = Date.now()): AnalyticsData {
|
|
4816
|
+
const { ms, buckets: bucketCount } = ANALYTICS_PERIODS[period] ?? ANALYTICS_PERIODS["24h"];
|
|
4817
|
+
const start = now - ms;
|
|
4818
|
+
const bucketMs = ms / bucketCount;
|
|
4819
|
+
|
|
4820
|
+
const buckets = Array.from({ length: bucketCount }, (_, i) => ({
|
|
4821
|
+
start: start + i * bucketMs,
|
|
4822
|
+
counts: emptyCategoryCounts(),
|
|
4823
|
+
}));
|
|
4824
|
+
const categories = emptyCategoryCounts();
|
|
4825
|
+
|
|
4826
|
+
// One pass: bucket index × category counts. CAST floors toward zero; created_at
|
|
4827
|
+
// in (start, now] keeps the index in [0, bucketCount).
|
|
4828
|
+
const volumeRows = db
|
|
4829
|
+
.query(
|
|
4830
|
+
`SELECT CAST((created_at - ?) / ? AS INTEGER) AS bucket, ${ANALYTICS_CATEGORY_SQL} AS category, COUNT(*) AS c
|
|
4831
|
+
FROM messages WHERE created_at > ? AND created_at <= ? GROUP BY bucket, category`,
|
|
4832
|
+
)
|
|
4833
|
+
.all(start, bucketMs, start, now) as Array<{ bucket: number; category: AnalyticsCategory; c: number }>;
|
|
4834
|
+
for (const row of volumeRows) {
|
|
4835
|
+
const idx = Math.min(Math.max(row.bucket, 0), bucketCount - 1);
|
|
4836
|
+
const bucket = buckets[idx];
|
|
4837
|
+
if (bucket && row.category in bucket.counts) {
|
|
4838
|
+
bucket.counts[row.category] += row.c;
|
|
4839
|
+
categories[row.category] += row.c;
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
const totalMessages = Object.values(categories).reduce((sum, v) => sum + v, 0);
|
|
4843
|
+
|
|
4844
|
+
// Busiest-hours heatmap (day-of-week × hour), server-local time.
|
|
4845
|
+
const heatmap: number[][] = Array.from({ length: 7 }, () => new Array(24).fill(0));
|
|
4846
|
+
const heatRows = db
|
|
4847
|
+
.query(
|
|
4848
|
+
`SELECT CAST(strftime('%w', created_at / 1000, 'unixepoch', 'localtime') AS INTEGER) AS dow,
|
|
4849
|
+
CAST(strftime('%H', created_at / 1000, 'unixepoch', 'localtime') AS INTEGER) AS hour, COUNT(*) AS c
|
|
4850
|
+
FROM messages WHERE created_at > ? AND created_at <= ? GROUP BY dow, hour`,
|
|
4851
|
+
)
|
|
4852
|
+
.all(start, now) as Array<{ dow: number; hour: number; c: number }>;
|
|
4853
|
+
for (const row of heatRows) {
|
|
4854
|
+
const day = heatmap[row.dow];
|
|
4855
|
+
if (day && row.hour >= 0 && row.hour < 24) day[row.hour] = row.c;
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
const totalReactions = (
|
|
4859
|
+
db
|
|
4860
|
+
.query(
|
|
4861
|
+
`SELECT COUNT(*) AS c FROM message_reactions r
|
|
4862
|
+
JOIN messages m ON m.id = r.message_id WHERE m.created_at > ? AND m.created_at <= ?`,
|
|
4863
|
+
)
|
|
4864
|
+
.get(start, now) as { c: number }
|
|
4865
|
+
).c;
|
|
4866
|
+
|
|
4867
|
+
return { period, start, now, bucketMs, bucketCount, buckets, categories, heatmap, totalMessages, totalReactions };
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4681
4870
|
export function getHealth(now: number = Date.now()): HealthReport {
|
|
4682
4871
|
const checks: HealthCheck[] = [];
|
|
4683
4872
|
|
package/src/managed-policy.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAgentProfile } from "./config-store";
|
|
1
|
+
import { getAgentProfile, spawnGrantForProfile } from "./config-store";
|
|
2
2
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
3
3
|
import { buildSpawnCommand, resolveSpawnModelParams } from "./spawn-command";
|
|
4
4
|
import type { SpawnPolicy, WorkspaceMode } from "./types";
|
|
@@ -29,6 +29,7 @@ export function buildManagedSpawnParams(policy: SpawnPolicy, requestId: string,
|
|
|
29
29
|
const providerArgs = managedPolicyProviderArgs(policy);
|
|
30
30
|
const prompt = managedPolicyLaunchPrompt(policy);
|
|
31
31
|
const agentProfile = policy.profile ? getAgentProfile(policy.profile)?.value : undefined;
|
|
32
|
+
const grant = spawnGrantForProfile(policy.profile);
|
|
32
33
|
return buildSpawnCommand({
|
|
33
34
|
provider: policy.provider,
|
|
34
35
|
cwd: policy.cwd,
|
|
@@ -55,6 +56,8 @@ export function buildManagedSpawnParams(policy: SpawnPolicy, requestId: string,
|
|
|
55
56
|
policyName: policy.name,
|
|
56
57
|
spawnRequestId: requestId,
|
|
57
58
|
createdBy: ctx.createdBy,
|
|
59
|
+
canSpawn: grant.canSpawn,
|
|
60
|
+
maxSpawnedAgents: grant.maxSpawnedAgents,
|
|
58
61
|
}),
|
|
59
62
|
requestedBy: ctx.createdBy,
|
|
60
63
|
requestedAt: ctx.requestedAt ?? Date.now(),
|