agent-relay-server 0.79.0 → 0.80.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 +55 -1
- package/package.json +2 -2
- package/src/db/teams.ts +18 -0
- package/src/maintenance/jobs.ts +9 -1
- package/src/maintenance/teams.ts +122 -0
- package/src/notification-types.ts +2 -0
- package/src/routes/index.ts +3 -0
- package/src/routes/teams-config.ts +15 -0
- package/src/security.ts +3 -1
- package/src/settings/registry.ts +27 -0
- package/src/teams-config-store.ts +22 -0
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.80.0",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
|
@@ -3332,6 +3332,60 @@
|
|
|
3332
3332
|
]
|
|
3333
3333
|
}
|
|
3334
3334
|
},
|
|
3335
|
+
"/api/teams/config": {
|
|
3336
|
+
"get": {
|
|
3337
|
+
"operationId": "getTeamsConfigRoute",
|
|
3338
|
+
"summary": "Get Teams Config",
|
|
3339
|
+
"tags": [
|
|
3340
|
+
"Other"
|
|
3341
|
+
],
|
|
3342
|
+
"responses": {
|
|
3343
|
+
"200": {
|
|
3344
|
+
"description": "Success",
|
|
3345
|
+
"content": {
|
|
3346
|
+
"application/json": {}
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
},
|
|
3350
|
+
"security": [
|
|
3351
|
+
{
|
|
3352
|
+
"bearerAuth": []
|
|
3353
|
+
},
|
|
3354
|
+
{
|
|
3355
|
+
"tokenHeader": []
|
|
3356
|
+
},
|
|
3357
|
+
{
|
|
3358
|
+
"tokenQuery": []
|
|
3359
|
+
}
|
|
3360
|
+
]
|
|
3361
|
+
},
|
|
3362
|
+
"put": {
|
|
3363
|
+
"operationId": "putTeamsConfigRoute",
|
|
3364
|
+
"summary": "Put Teams Config",
|
|
3365
|
+
"tags": [
|
|
3366
|
+
"Other"
|
|
3367
|
+
],
|
|
3368
|
+
"responses": {
|
|
3369
|
+
"200": {
|
|
3370
|
+
"description": "Success",
|
|
3371
|
+
"content": {
|
|
3372
|
+
"application/json": {}
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
},
|
|
3376
|
+
"security": [
|
|
3377
|
+
{
|
|
3378
|
+
"bearerAuth": []
|
|
3379
|
+
},
|
|
3380
|
+
{
|
|
3381
|
+
"tokenHeader": []
|
|
3382
|
+
},
|
|
3383
|
+
{
|
|
3384
|
+
"tokenQuery": []
|
|
3385
|
+
}
|
|
3386
|
+
]
|
|
3387
|
+
}
|
|
3388
|
+
},
|
|
3335
3389
|
"/api/teams": {
|
|
3336
3390
|
"get": {
|
|
3337
3391
|
"operationId": "getTeamsRoute",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.0",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"CONTRIBUTING.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"agent-relay-sdk": "0.2.
|
|
36
|
+
"agent-relay-sdk": "0.2.60",
|
|
37
37
|
"ajv": "^8.20.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
package/src/db/teams.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { getAgent, setAgentTeam } from "./agents.ts";
|
|
|
3
3
|
import { getDb, ValidationError } from "./connection.ts";
|
|
4
4
|
import { rowToAgent } from "./mappers.ts";
|
|
5
5
|
import { unpromotedBlackboardEntries, type UnpromotedBlackboardSummary } from "./project-entries.ts";
|
|
6
|
+
import { isProviderBlocked } from "../provider-state.ts";
|
|
6
7
|
import type { AgentCard, Team } from "../types";
|
|
7
8
|
|
|
8
9
|
interface TeamRow {
|
|
@@ -72,6 +73,11 @@ export interface TeamOutcomeAggregates {
|
|
|
72
73
|
recent: TeamOutcomeRecent[];
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
interface TeamBusyState {
|
|
77
|
+
busyMemberIds: string[];
|
|
78
|
+
lastBusyAt?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
export function teamId(): string {
|
|
76
82
|
return `team_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
77
83
|
}
|
|
@@ -132,6 +138,18 @@ export function countLiveTeamMembers(teamId: string, cutoff: number): number {
|
|
|
132
138
|
return row.n;
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
export function getTeamBusyState(teamId: string, cutoff: number): TeamBusyState {
|
|
142
|
+
const rows = (getDb().query(
|
|
143
|
+
"SELECT * FROM agents WHERE team_id = ? AND status = 'busy' AND last_seen > ? ORDER BY last_seen DESC, id ASC",
|
|
144
|
+
).all(teamId, cutoff) as any[])
|
|
145
|
+
.map(rowToAgent)
|
|
146
|
+
.filter((agent) => !isProviderBlocked(agent));
|
|
147
|
+
return {
|
|
148
|
+
busyMemberIds: rows.map((row) => row.id),
|
|
149
|
+
...(rows[0] ? { lastBusyAt: rows[0].lastSeen } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
135
153
|
export function setTeamZeroMembersSince(teamId: string, ts: number): boolean {
|
|
136
154
|
return getDb().query("UPDATE teams SET zero_members_since = ?, updated_at = ? WHERE id = ?").run(ts, ts, teamId).changes > 0;
|
|
137
155
|
}
|
package/src/maintenance/jobs.ts
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
} from "./constants";
|
|
52
52
|
import { emitCommand } from "./events";
|
|
53
53
|
import { reapOrphanedSessions } from "./orphaned-sessions";
|
|
54
|
-
import { reapZeroMemberTeams } from "./teams";
|
|
54
|
+
import { reapZeroMemberTeams, runTeamIdleWatchdog } from "./teams";
|
|
55
55
|
import { scanWorkspaceConflicts, workspaceGC } from "./workspaces";
|
|
56
56
|
import type { MaintenanceJobDefinition } from "./types";
|
|
57
57
|
|
|
@@ -208,6 +208,14 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
208
208
|
runOnStart: false,
|
|
209
209
|
handler: reapZeroMemberTeams,
|
|
210
210
|
},
|
|
211
|
+
{
|
|
212
|
+
id: "team-idle-watchdog",
|
|
213
|
+
title: "Team idle watchdog",
|
|
214
|
+
description: "Gently ping a team's current coordinator once when the team has had no busy members for the configured threshold.",
|
|
215
|
+
intervalMs: REAP_INTERVAL_MS,
|
|
216
|
+
runOnStart: false,
|
|
217
|
+
handler: runTeamIdleWatchdog,
|
|
218
|
+
},
|
|
211
219
|
{
|
|
212
220
|
id: "orchestrator-reaper",
|
|
213
221
|
title: "Orchestrator reaper",
|
package/src/maintenance/teams.ts
CHANGED
|
@@ -2,12 +2,134 @@ import {
|
|
|
2
2
|
clearTeamZeroMembersSince,
|
|
3
3
|
countLiveTeamMembers,
|
|
4
4
|
createActivityEvent,
|
|
5
|
+
getTeamBusyState,
|
|
6
|
+
getTeamRoster,
|
|
5
7
|
listActiveTeams,
|
|
6
8
|
setTeamZeroMembersSince,
|
|
7
9
|
} from "../db";
|
|
8
10
|
import { STALE_TTL_MS } from "../config";
|
|
11
|
+
import { agentProviderState } from "../provider-state";
|
|
9
12
|
import { dissolveTeamById } from "../services/team";
|
|
13
|
+
import { getTeamsConfig } from "../teams-config-store";
|
|
10
14
|
import { teamReapGraceMs } from "./constants";
|
|
15
|
+
import { notifyTarget } from "./events";
|
|
16
|
+
|
|
17
|
+
interface TeamIdleTrackerEntry {
|
|
18
|
+
firstNoBusyAt: number;
|
|
19
|
+
pinged: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const teamIdleTracker = new Map<string, TeamIdleTrackerEntry>();
|
|
23
|
+
|
|
24
|
+
export function resetTeamIdleWatchdogForTests(): void {
|
|
25
|
+
teamIdleTracker.clear();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function teamIdlePayload(teamId: string, coordinatorId: string, thresholdMinutes: number, idleForMs: number, firstNoBusyAt: number) {
|
|
29
|
+
const roster = getTeamRoster(teamId).map((member) => {
|
|
30
|
+
const providerState = agentProviderState(member);
|
|
31
|
+
return {
|
|
32
|
+
id: member.id,
|
|
33
|
+
name: member.name,
|
|
34
|
+
label: member.label,
|
|
35
|
+
status: member.status,
|
|
36
|
+
ready: member.ready,
|
|
37
|
+
lastSeen: member.lastSeen,
|
|
38
|
+
blocked: providerState?.state === "blocked",
|
|
39
|
+
providerState: providerState ?? undefined,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
kind: "team.idle",
|
|
44
|
+
teamId,
|
|
45
|
+
coordinatorId,
|
|
46
|
+
thresholdMinutes,
|
|
47
|
+
thresholdMs: thresholdMinutes * 60_000,
|
|
48
|
+
idleForMs,
|
|
49
|
+
firstNoBusyAt,
|
|
50
|
+
roster,
|
|
51
|
+
blockedMembers: roster.filter((member) => member.blocked).map((member) => ({
|
|
52
|
+
id: member.id,
|
|
53
|
+
providerState: member.providerState,
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function notifyTeamIdle(teamId: string, coordinatorId: string, thresholdMinutes: number, idleForMs: number, firstNoBusyAt: number): string | null {
|
|
59
|
+
const roundedIdleMinutes = Math.max(thresholdMinutes, Math.floor(idleForMs / 60_000));
|
|
60
|
+
const payload = teamIdlePayload(teamId, coordinatorId, thresholdMinutes, idleForMs, firstNoBusyAt);
|
|
61
|
+
return notifyTarget(
|
|
62
|
+
"team.idle",
|
|
63
|
+
{ teamId, agentId: coordinatorId },
|
|
64
|
+
coordinatorId,
|
|
65
|
+
"Team idle watchdog",
|
|
66
|
+
`Team \`${teamId}\` has had no busy members for ${roundedIdleMinutes} min. If the work is done, stand down. If not, check the team roster and restart unfinished work.`,
|
|
67
|
+
payload,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function runTeamIdleWatchdog(input: { now?: number } = {}): Record<string, unknown> {
|
|
72
|
+
const config = getTeamsConfig();
|
|
73
|
+
if (!config.enabled) {
|
|
74
|
+
const cleared = teamIdleTracker.size;
|
|
75
|
+
teamIdleTracker.clear();
|
|
76
|
+
return { enabled: false, clearedTrackedTeams: cleared };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const now = input.now ?? Date.now();
|
|
80
|
+
const cutoff = now - STALE_TTL_MS;
|
|
81
|
+
const thresholdMs = config.thresholdMinutes * 60_000;
|
|
82
|
+
const seen = new Set<string>();
|
|
83
|
+
const busyTeamIds: string[] = [];
|
|
84
|
+
const waitingTeamIds: string[] = [];
|
|
85
|
+
const pingedTeamIds: string[] = [];
|
|
86
|
+
const alreadyPingedTeamIds: string[] = [];
|
|
87
|
+
const missingCoordinatorTeamIds: string[] = [];
|
|
88
|
+
|
|
89
|
+
for (const team of listActiveTeams()) {
|
|
90
|
+
seen.add(team.id);
|
|
91
|
+
const busy = getTeamBusyState(team.id, cutoff);
|
|
92
|
+
if (busy.busyMemberIds.length > 0) {
|
|
93
|
+
teamIdleTracker.delete(team.id);
|
|
94
|
+
busyTeamIds.push(team.id);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const entry = teamIdleTracker.get(team.id) ?? { firstNoBusyAt: now, pinged: false };
|
|
99
|
+
teamIdleTracker.set(team.id, entry);
|
|
100
|
+
const idleForMs = now - entry.firstNoBusyAt;
|
|
101
|
+
if (idleForMs < thresholdMs) {
|
|
102
|
+
waitingTeamIds.push(team.id);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (entry.pinged) {
|
|
107
|
+
alreadyPingedTeamIds.push(team.id);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!team.coordinatorId) {
|
|
111
|
+
missingCoordinatorTeamIds.push(team.id);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
notifyTeamIdle(team.id, team.coordinatorId, config.thresholdMinutes, idleForMs, entry.firstNoBusyAt);
|
|
116
|
+
entry.pinged = true;
|
|
117
|
+
pingedTeamIds.push(team.id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const key of teamIdleTracker.keys()) if (!seen.has(key)) teamIdleTracker.delete(key);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
enabled: true,
|
|
124
|
+
thresholdMinutes: config.thresholdMinutes,
|
|
125
|
+
trackedTeams: teamIdleTracker.size,
|
|
126
|
+
busyTeamIds,
|
|
127
|
+
waitingTeamIds,
|
|
128
|
+
pingedTeamIds,
|
|
129
|
+
alreadyPingedTeamIds,
|
|
130
|
+
missingCoordinatorTeamIds,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
11
133
|
|
|
12
134
|
export function reapZeroMemberTeams(): Record<string, unknown> {
|
|
13
135
|
const now = Date.now();
|
|
@@ -10,6 +10,7 @@ export type NotificationType =
|
|
|
10
10
|
| "agent.resume_budget_exhausted"
|
|
11
11
|
| "agent.transient_land_hold"
|
|
12
12
|
| "agent.rate_limit_resume"
|
|
13
|
+
| "team.idle"
|
|
13
14
|
| "plan.bound_agent_exited"
|
|
14
15
|
| "workspace.steward_task"
|
|
15
16
|
| "workspace.conflict"
|
|
@@ -60,6 +61,7 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
|
|
|
60
61
|
"agent.resume_budget_exhausted": null,
|
|
61
62
|
"agent.transient_land_hold": null,
|
|
62
63
|
"agent.rate_limit_resume": 15 * MINUTES,
|
|
64
|
+
"team.idle": null,
|
|
63
65
|
"plan.bound_agent_exited": null,
|
|
64
66
|
"workspace.steward_task": null,
|
|
65
67
|
"workspace.conflict": null,
|
package/src/routes/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { deleteAutomationById, getAutomationById, getAutomationRuns, getAutomati
|
|
|
7
7
|
import { deleteCommandById, deleteProviderModelRoute, getProviderConfigRoute, getProviderConfigsRoute, getProvidersRoute, patchProviderModelRoute, postProviderConfigTestRoute, putProviderConfigRoute, putProviderModelRoute } from "./provider-config";
|
|
8
8
|
import { getProviderQuotasRoute, postProviderQuotaLeaseAcquireRoute, postProviderQuotaLeaseReleaseRoute, postProviderQuotaRoute } from "./provider-quotas";
|
|
9
9
|
import { getProviderQuotaConfigRoute, getProviderQuotaConfigsRoute, putProviderQuotaConfigRoute } from "./provider-quota-config";
|
|
10
|
+
import { getTeamsConfigRoute, putTeamsConfigRoute } from "./teams-config";
|
|
10
11
|
import { deleteConfigKey, getConfigKey, getConfigKeyHistory, getConfigNamespace, putConfigKey } from "./config";
|
|
11
12
|
import { deleteInboxDraftRoute, getChatHistoryImportsRoute, getInboxStateRoute, patchInboxThreadState, postChatHistoryImportRoute, putInboxDraft } from "./inbox";
|
|
12
13
|
import { deleteMemoryRoute, getMemories, getMemoryBrokerInfo, getMemoryById, getMemoryStats, patchMemory, postMemory, postMemoryInject, postMemorySearch } from "./memory";
|
|
@@ -161,6 +162,8 @@ const routes: Route[] = [
|
|
|
161
162
|
route("GET", "/api/merges/pauses/:id", getMergePauseById),
|
|
162
163
|
|
|
163
164
|
route("POST", "/api/system/broadcast", postSystemBroadcast),
|
|
165
|
+
route("GET", "/api/teams/config", getTeamsConfigRoute),
|
|
166
|
+
route("PUT", "/api/teams/config", putTeamsConfigRoute),
|
|
164
167
|
route("GET", "/api/teams", getTeamsRoute),
|
|
165
168
|
route("GET", "/api/teams/outcomes", getTeamOutcomesRoute),
|
|
166
169
|
route("GET", "/api/teams/me", getMyTeamRoute),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getTeamsConfig, setTeamsConfig } from "../teams-config-store";
|
|
2
|
+
import { cleanString } from "../validation";
|
|
3
|
+
import { error, json, parseBody, type Handler } from "./_shared";
|
|
4
|
+
import { isRecord } from "agent-relay-sdk";
|
|
5
|
+
|
|
6
|
+
export const getTeamsConfigRoute: Handler = () => {
|
|
7
|
+
return json(getTeamsConfig());
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const putTeamsConfigRoute: Handler = async (req) => {
|
|
11
|
+
const parsed = await parseBody<unknown>(req);
|
|
12
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
13
|
+
const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
|
|
14
|
+
return json(setTeamsConfig(parsed.body, { updatedBy }));
|
|
15
|
+
};
|
package/src/security.ts
CHANGED
|
@@ -194,6 +194,7 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
194
194
|
if (pathname.startsWith("/api/maintenance")) return "system:admin";
|
|
195
195
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|
|
196
196
|
if (pathname.startsWith("/api/pairs")) return method === "GET" ? "pairs:read" : "pairs:write";
|
|
197
|
+
if (pathname === "/api/teams/config") return method === "GET" ? "teams:read" : "settings:write:teams";
|
|
197
198
|
if (pathname.startsWith("/api/teams")) return method === "GET" ? "teams:read" : "teams:write";
|
|
198
199
|
if (pathname.startsWith("/api/blackboard")) return method === "GET" ? "blackboard:read" : "blackboard:write";
|
|
199
200
|
if (pathname.startsWith("/api/issue-associations")) return method === "GET" ? "issue:read" : "issue:write";
|
|
@@ -280,6 +281,7 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
280
281
|
if (pathname === "/api/providers/quota-config" || pathname.match(/^\/api\/providers\/[^/]+\/quota-config$/)) {
|
|
281
282
|
return method === "GET" ? "agent:read" : "settings:write:provider";
|
|
282
283
|
}
|
|
284
|
+
if (pathname === "/api/teams/config") return method === "GET" ? "teams:read" : "settings:write:teams";
|
|
283
285
|
if (pathname.startsWith("/api/messages") || pathname.startsWith("/api/inbox")) return method === "GET" ? "message:read" : "message:send";
|
|
284
286
|
if (pathname.startsWith("/api/agent-profiles")) return method === "GET" ? "agent:read" : "agent:write";
|
|
285
287
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|
|
@@ -304,7 +306,7 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
304
306
|
return "system:admin";
|
|
305
307
|
}
|
|
306
308
|
|
|
307
|
-
const CONFIG_SETTINGS_NAMESPACES = new Set(["steward", "insights", "notifications", "workspace", "landing", "provider"]);
|
|
309
|
+
const CONFIG_SETTINGS_NAMESPACES = new Set(["steward", "insights", "notifications", "workspace", "landing", "provider", "teams"]);
|
|
308
310
|
|
|
309
311
|
function requiredSettingsScopeFor(method: string, pathname: string): string | null | undefined {
|
|
310
312
|
if (pathname === "/api/settings") return method === "GET" ? "settings:read" : null;
|
package/src/settings/registry.ts
CHANGED
|
@@ -20,11 +20,14 @@ import {
|
|
|
20
20
|
APPROVAL_MODES,
|
|
21
21
|
errMessage,
|
|
22
22
|
SPAWN_PROVIDERS,
|
|
23
|
+
TEAM_IDLE_THRESHOLD_MINUTES_MAX,
|
|
24
|
+
TEAM_IDLE_THRESHOLD_MINUTES_MIN,
|
|
23
25
|
VALID_EFFORTS,
|
|
24
26
|
type JSONSchema,
|
|
25
27
|
type SettingDescriptor,
|
|
26
28
|
type SettingScope,
|
|
27
29
|
} from "agent-relay-sdk";
|
|
30
|
+
import { DEFAULT_TEAMS_CONFIG } from "agent-relay-sdk/teams-config";
|
|
28
31
|
|
|
29
32
|
/** Fail-closed default scope — a descriptor that omits scope requires admin to
|
|
30
33
|
* read and write (#385 owns the scope grammar; this layer only declares it). */
|
|
@@ -215,6 +218,20 @@ const WORKSPACE_SCHEMA: JSONSchema = {
|
|
|
215
218
|
},
|
|
216
219
|
};
|
|
217
220
|
|
|
221
|
+
const TEAMS_SCHEMA: JSONSchema = {
|
|
222
|
+
type: "object",
|
|
223
|
+
additionalProperties: false,
|
|
224
|
+
required: ["enabled", "thresholdMinutes"],
|
|
225
|
+
properties: {
|
|
226
|
+
enabled: { type: "boolean" },
|
|
227
|
+
thresholdMinutes: {
|
|
228
|
+
type: "integer",
|
|
229
|
+
minimum: TEAM_IDLE_THRESHOLD_MINUTES_MIN,
|
|
230
|
+
maximum: TEAM_IDLE_THRESHOLD_MINUTES_MAX,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
218
235
|
const LANDING_SCHEMA: JSONSchema = {
|
|
219
236
|
type: "object",
|
|
220
237
|
additionalProperties: false,
|
|
@@ -347,6 +364,16 @@ function seedBuiltInSettings(): void {
|
|
|
347
364
|
display: { group: "Workspace", label: "Landing strategy", description: "Default land strategy for workspace branches.", order: 41 },
|
|
348
365
|
});
|
|
349
366
|
|
|
367
|
+
registerSetting({
|
|
368
|
+
namespace: "teams",
|
|
369
|
+
key: "default",
|
|
370
|
+
schema: TEAMS_SCHEMA,
|
|
371
|
+
defaults: DEFAULT_TEAMS_CONFIG,
|
|
372
|
+
scope: { read: "settings:read:teams", write: "settings:write:teams" },
|
|
373
|
+
tier: "server-runtime",
|
|
374
|
+
display: { group: "Teams", label: "Team idle watchdog", description: "Coordinator nudge when a team has no busy members.", order: 45 },
|
|
375
|
+
});
|
|
376
|
+
|
|
350
377
|
registerSetting({
|
|
351
378
|
namespace: "provider",
|
|
352
379
|
key: "chatCaptureMode",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getConfig, setConfig } from "./config-store";
|
|
2
|
+
import { emitConfigChanged } from "./sse";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_TEAMS_CONFIG,
|
|
5
|
+
normalizeTeamsConfig,
|
|
6
|
+
type TeamsConfig,
|
|
7
|
+
} from "agent-relay-sdk";
|
|
8
|
+
|
|
9
|
+
const TEAMS_CONFIG_NAMESPACE = "teams";
|
|
10
|
+
const TEAMS_CONFIG_KEY = "default";
|
|
11
|
+
|
|
12
|
+
export function getTeamsConfig(): TeamsConfig {
|
|
13
|
+
const entry = getConfig<TeamsConfig>(TEAMS_CONFIG_NAMESPACE, TEAMS_CONFIG_KEY);
|
|
14
|
+
return entry ? normalizeTeamsConfig(entry.value) : { ...DEFAULT_TEAMS_CONFIG };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setTeamsConfig(value: unknown, opts: { updatedBy?: string } = {}): TeamsConfig {
|
|
18
|
+
const normalized = normalizeTeamsConfig(value);
|
|
19
|
+
const entry = setConfig(TEAMS_CONFIG_NAMESPACE, TEAMS_CONFIG_KEY, normalized, opts.updatedBy);
|
|
20
|
+
emitConfigChanged(entry.namespace, entry.key, entry.version);
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|