agent-relay-server 0.62.3 → 0.63.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 +4 -2
- package/public/assets/{MessageBubble-DRrxfjyT.js → MessageBubble-Cl6ufPHw.js} +2 -2
- package/public/assets/MessageBubble-Cl6ufPHw.js.map +1 -0
- package/public/assets/{PlanGraphPanel-Dp2eYazz.js → PlanGraphPanel-BnfjmkQ8.js} +2 -2
- package/public/assets/{PlanGraphPanel-Dp2eYazz.js.map → PlanGraphPanel-BnfjmkQ8.js.map} +1 -1
- package/public/assets/{activity--t0U2yzq.js → activity-aFxvGLtd.js} +2 -2
- package/public/assets/{activity--t0U2yzq.js.map → activity-aFxvGLtd.js.map} +1 -1
- package/public/assets/{agent-profiles-BMtJYzxn.js → agent-profiles-B4mleUDz.js} +2 -2
- package/public/assets/{agent-profiles-BMtJYzxn.js.map → agent-profiles-B4mleUDz.js.map} +1 -1
- package/public/assets/{agents-Ck9AePlc.js → agents-DW5djToQ.js} +2 -2
- package/public/assets/{agents-Ck9AePlc.js.map → agents-DW5djToQ.js.map} +1 -1
- package/public/assets/{analytics-CzfK387L.js → analytics-CRi6thtg.js} +2 -2
- package/public/assets/{analytics-CzfK387L.js.map → analytics-CRi6thtg.js.map} +1 -1
- package/public/assets/{automation-Cgw8vtE8.js → automation-Do4OpauI.js} +2 -2
- package/public/assets/{automation-Cgw8vtE8.js.map → automation-Do4OpauI.js.map} +1 -1
- package/public/assets/{branch-state-badge-DnEEPtOP.js → branch-state-badge-D3W3w7zz.js} +2 -2
- package/public/assets/{branch-state-badge-DnEEPtOP.js.map → branch-state-badge-D3W3w7zz.js.map} +1 -1
- package/public/assets/{channels-Dc3L2MQs.js → channels-BytdR9NS.js} +2 -2
- package/public/assets/{channels-Dc3L2MQs.js.map → channels-BytdR9NS.js.map} +1 -1
- package/public/assets/{chat-Fc6iJ7sa.js → chat-CvaQ2czL.js} +2 -2
- package/public/assets/{chat-Fc6iJ7sa.js.map → chat-CvaQ2czL.js.map} +1 -1
- package/public/assets/{connectors-Dc2ttGWs.js → connectors-BLQDp8Bc.js} +2 -2
- package/public/assets/{connectors-Dc2ttGWs.js.map → connectors-BLQDp8Bc.js.map} +1 -1
- package/public/assets/{display-DzNvOu1j.js → display-B06I5sqa.js} +3 -3
- package/public/assets/display-B06I5sqa.js.map +1 -0
- package/public/assets/{formatted-body-impl-Bg-gx7rG.js → formatted-body-impl-0Lf8e1Ic.js} +2 -2
- package/public/assets/{formatted-body-impl-Bg-gx7rG.js.map → formatted-body-impl-0Lf8e1Ic.js.map} +1 -1
- package/public/assets/{index-BeDncjN4.js → index-C3su8ezq.js} +6 -6
- package/public/assets/{index-BeDncjN4.js.map → index-C3su8ezq.js.map} +1 -1
- package/public/assets/{insights-c08aW-7n.js → insights-64uExH1E.js} +2 -2
- package/public/assets/{insights-c08aW-7n.js.map → insights-64uExH1E.js.map} +1 -1
- package/public/assets/{integrations-BwpgcqnF.js → integrations-HyC1EaIp.js} +2 -2
- package/public/assets/{integrations-BwpgcqnF.js.map → integrations-HyC1EaIp.js.map} +1 -1
- package/public/assets/{maintenance-CTKP0kWB.js → maintenance-EVBpKH07.js} +2 -2
- package/public/assets/{maintenance-CTKP0kWB.js.map → maintenance-EVBpKH07.js.map} +1 -1
- package/public/assets/{managed-agents-BofCxo8d.js → managed-agents-BLZPxGuw.js} +2 -2
- package/public/assets/{managed-agents-BofCxo8d.js.map → managed-agents-BLZPxGuw.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CQ-6BIb8.js → markdown-preview-impl-Bg6rCWYN.js} +2 -2
- package/public/assets/{markdown-preview-impl-CQ-6BIb8.js.map → markdown-preview-impl-Bg6rCWYN.js.map} +1 -1
- package/public/assets/{memory-DvfXjMoi.js → memory-Bjr8bgjj.js} +2 -2
- package/public/assets/{memory-DvfXjMoi.js.map → memory-Bjr8bgjj.js.map} +1 -1
- package/public/assets/{messages-C9VsPzZ_.js → messages-CRYw1F4z.js} +2 -2
- package/public/assets/{messages-C9VsPzZ_.js.map → messages-CRYw1F4z.js.map} +1 -1
- package/public/assets/{orchestrators-MffVueeE.js → orchestrators-D-co1vdp.js} +2 -2
- package/public/assets/{orchestrators-MffVueeE.js.map → orchestrators-D-co1vdp.js.map} +1 -1
- package/public/assets/{overview-BSVajefM.js → overview-Ddy5czri.js} +2 -2
- package/public/assets/{overview-BSVajefM.js.map → overview-Ddy5czri.js.map} +1 -1
- package/public/assets/{pairs-B5711-Cs.js → pairs-CFVM8uHL.js} +2 -2
- package/public/assets/{pairs-B5711-Cs.js.map → pairs-CFVM8uHL.js.map} +1 -1
- package/public/assets/{security-DkUwK9Vj.js → security-Df4_76bh.js} +2 -2
- package/public/assets/{security-DkUwK9Vj.js.map → security-Df4_76bh.js.map} +1 -1
- package/public/assets/{select-DrM2RRiT.js → select-Cq3BBnL9.js} +2 -2
- package/public/assets/{select-DrM2RRiT.js.map → select-Cq3BBnL9.js.map} +1 -1
- package/public/assets/{settings-DdKCb-vy.js → settings-DOLEYXZL.js} +2 -2
- package/public/assets/{settings-DdKCb-vy.js.map → settings-DOLEYXZL.js.map} +1 -1
- package/public/assets/{store-iTiheuTD.js → store-C1E8jmNE.js} +4 -4
- package/public/assets/store-C1E8jmNE.js.map +1 -0
- package/public/assets/{tasks-e1y1PFfO.js → tasks-BcsmPi9_.js} +2 -2
- package/public/assets/{tasks-e1y1PFfO.js.map → tasks-BcsmPi9_.js.map} +1 -1
- package/public/assets/{teams-BUcxoGC_.js → teams-BQrV3eKI.js} +2 -2
- package/public/assets/{teams-BUcxoGC_.js.map → teams-BQrV3eKI.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CmZjQOB2.js → terminal-viewer-impl-Dle04qpH.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CmZjQOB2.js.map → terminal-viewer-impl-Dle04qpH.js.map} +1 -1
- package/public/assets/{work-queue-CXBQx43n.js → work-queue-BLhTLg4o.js} +2 -2
- package/public/assets/{work-queue-CXBQx43n.js.map → work-queue-BLhTLg4o.js.map} +1 -1
- package/public/assets/{workspaces-D2EfZh9a.js → workspaces-4NCNTmRQ.js} +3 -3
- package/public/assets/{workspaces-D2EfZh9a.js.map → workspaces-4NCNTmRQ.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/src/adapter.ts +2 -1
- package/runner/src/config.ts +143 -5
- package/src/agent-ref.ts +8 -6
- package/src/artifact-storage.ts +3 -5
- package/src/automations/constants.ts +12 -0
- package/src/automations/cron.ts +129 -0
- package/src/automations/crud.ts +84 -0
- package/src/automations/dispatch.ts +275 -0
- package/src/automations/index.ts +5 -0
- package/src/automations/reconcile.ts +245 -0
- package/src/automations/storage.ts +181 -0
- package/src/automations/types.ts +16 -0
- package/src/automations/validation.ts +140 -0
- package/src/automations.ts +1 -1045
- package/src/bus.ts +3 -4
- package/src/cli/_shared.ts +5 -4
- package/src/cli/agent-detect.ts +6 -4
- package/src/cli/introspect.ts +3 -2
- package/src/cli/token.ts +2 -1
- package/src/cli/workspace.ts +2 -1
- package/src/config.ts +176 -1
- package/src/connectors.ts +3 -3
- package/src/db/activity.ts +1 -1
- package/src/db/agent-predicates.ts +9 -0
- package/src/db/agent-search.ts +2 -2
- package/src/db/agents.ts +13 -41
- package/src/db/artifacts.ts +8 -8
- package/src/db/channel-identity.ts +38 -0
- package/src/db/channels.ts +37 -61
- package/src/db/connection.ts +2 -1
- package/src/db/continuation-archives.ts +7 -7
- package/src/db/delivery.ts +7 -12
- package/src/db/inbox.ts +1 -1
- package/src/db/integrations.ts +4 -4
- package/src/db/issue-lifecycle.ts +6 -6
- package/src/db/mappers.ts +1 -2
- package/src/db/merge-lease.ts +3 -3
- package/src/db/message-claims.ts +16 -0
- package/src/db/message-queries.ts +8 -0
- package/src/db/message-reads.ts +26 -9
- package/src/db/messages.ts +17 -41
- package/src/db/notification-subscriptions.ts +2 -2
- package/src/db/orchestrators.ts +4 -4
- package/src/db/pair-maintenance.ts +9 -0
- package/src/db/pairs.ts +10 -18
- package/src/db/plans.ts +1 -1
- package/src/db/policy-runtime.ts +14 -0
- package/src/db/project-entries.ts +1 -1
- package/src/db/projects.ts +2 -2
- package/src/db/shared.ts +7 -0
- package/src/db/stats.ts +2 -2
- package/src/db/task-core.ts +18 -0
- package/src/db/task-maintenance.ts +24 -0
- package/src/db/tasks.ts +5 -17
- package/src/db/teams.ts +3 -3
- package/src/db/workspaces.ts +6 -6
- package/src/index.ts +6 -5
- package/src/issue-tracker/github.ts +3 -3
- package/src/maintenance/constants.ts +52 -0
- package/src/maintenance/events.ts +32 -0
- package/src/maintenance/index.ts +7 -0
- package/src/maintenance/jobs.ts +340 -0
- package/src/maintenance/orphaned-sessions.ts +86 -0
- package/src/maintenance/scheduler.ts +231 -0
- package/src/maintenance/teams.ts +65 -0
- package/src/maintenance/types.ts +31 -0
- package/src/maintenance/workspaces.ts +251 -0
- package/src/maintenance.ts +1 -1099
- package/src/mcp/identity.ts +131 -0
- package/src/mcp/index.ts +208 -0
- package/src/mcp/tools-messaging.ts +459 -0
- package/src/mcp/tools-spawn.ts +145 -0
- package/src/mcp/tools-workspace.ts +227 -0
- package/src/mcp/types.ts +24 -0
- package/src/mcp/validation.ts +104 -0
- package/src/mcp.ts +1 -1319
- package/src/notification-router.ts +2 -2
- package/src/notification-rules.ts +2 -2
- package/src/project-ingest.ts +3 -3
- package/src/project-instructions.ts +1 -1
- package/src/provider-config-store.ts +5 -4
- package/src/provider-state.ts +2 -4
- package/src/reviewer-pipeline.ts +1 -1
- package/src/routes/agent-sessions.ts +5 -4
- package/src/routes/orchestrator-proxy.ts +5 -4
- package/src/routes/workspaces.ts +5 -4
- package/src/security.ts +13 -7
- package/src/self-resume.ts +3 -7
- package/src/services/agent-action.ts +2 -2
- package/src/services/issue-activity.ts +0 -4
- package/src/services/issue-associations.ts +2 -2
- package/src/services/issue-lifecycle.ts +8 -11
- package/src/services/managed-running.ts +1 -1
- package/src/services/parity-harness.ts +3 -3
- package/src/services/plan-graph.ts +4 -7
- package/src/services/send-message.ts +16 -12
- package/src/services/shutdown-agent.ts +2 -2
- package/src/services/team-templates.ts +2 -2
- package/src/services/team.ts +2 -2
- package/src/settings/modules.ts +1 -1
- package/src/settings/registry.ts +3 -3
- package/src/spawn-command.ts +1 -1
- package/src/sse.ts +0 -4
- package/src/upgrade.ts +7 -9
- package/src/workspace-actions.ts +1 -1
- package/src/workspace-auto-merge.ts +4 -5
- package/src/workspace-claim.ts +1 -2
- package/src/workspace-orphans.ts +2 -7
- package/src/workspace-probe.ts +5 -8
- package/public/assets/MessageBubble-DRrxfjyT.js.map +0 -1
- package/public/assets/display-DzNvOu1j.js.map +0 -1
- package/public/assets/store-iTiheuTD.js.map +0 -1
package/src/db/messages.ts
CHANGED
|
@@ -20,7 +20,11 @@ import { cleanAttachmentRefs, linkAttachmentRefs, validateAttachmentRefs } from
|
|
|
20
20
|
import { ClaimError, ValidationError, getDb } from "./connection.ts";
|
|
21
21
|
import { enforceQueueLimit, isChannelAgentId, matchingDeliveryAgents } from "./delivery.ts";
|
|
22
22
|
import { MSG_SELECT, normalizeReactionEmoji, rowToMessage } from "./mappers.ts";
|
|
23
|
-
import {
|
|
23
|
+
import { claimMessageRow } from "./message-claims.ts";
|
|
24
|
+
import { getMessage } from "./message-queries.ts";
|
|
25
|
+
import { runningAgentForPolicy } from "./policy-runtime.ts";
|
|
26
|
+
import { getTask, releaseExpiredClaims } from "./tasks.ts";
|
|
27
|
+
import { insertTaskEvent } from "./task-core.ts";
|
|
24
28
|
import type {
|
|
25
29
|
AgentCard,
|
|
26
30
|
ActivityEvent,
|
|
@@ -84,6 +88,10 @@ import type {
|
|
|
84
88
|
WorkspaceStatus,
|
|
85
89
|
} from "../types";
|
|
86
90
|
|
|
91
|
+
export { claimMessageRow } from "./message-claims.ts";
|
|
92
|
+
export { getMessage } from "./message-queries.ts";
|
|
93
|
+
export { runningAgentForPolicy } from "./policy-runtime.ts";
|
|
94
|
+
|
|
87
95
|
export function findMessageByIdempotencyKey(from: string, key: string): Message | null {
|
|
88
96
|
const row = getDb()
|
|
89
97
|
.query(`${MSG_SELECT} WHERE m.from_agent = ? AND m.idempotency_key = ? LIMIT 1`)
|
|
@@ -119,51 +127,38 @@ function upsertSessionStepInPlace(existing: Message, input: SendMessageInput, no
|
|
|
119
127
|
return getMessage(existing.id);
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
function policyNameFromTarget(target: string): string | null {
|
|
123
131
|
if (!target.startsWith("policy:")) return null;
|
|
124
132
|
const name = target.slice("policy:".length).trim();
|
|
125
133
|
return name || null;
|
|
126
134
|
}
|
|
127
135
|
|
|
128
|
-
|
|
136
|
+
function spawnPolicyExists(policyName: string): boolean {
|
|
129
137
|
const row = getDb().query("SELECT 1 FROM config WHERE namespace = 'spawn-policy' AND key = ?").get(policyName);
|
|
130
138
|
return Boolean(row);
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
|
|
134
|
-
const row = getDb().query(`
|
|
135
|
-
SELECT agent_id
|
|
136
|
-
FROM managed_agent_state
|
|
137
|
-
WHERE policy_name = ? AND status = 'running' AND agent_id IS NOT NULL
|
|
138
|
-
`).get(policyName) as { agent_id?: string } | undefined;
|
|
139
|
-
if (!row?.agent_id) return null;
|
|
140
|
-
const agent = getAgent(row.agent_id);
|
|
141
|
-
if (!agent || agent.status === "offline") return null;
|
|
142
|
-
return row.agent_id;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
export function claimableAllowedForTarget(target: string): boolean {
|
|
141
|
+
function claimableAllowedForTarget(target: string): boolean {
|
|
147
142
|
return matchingDeliveryAgents(target).length > 1;
|
|
148
143
|
}
|
|
149
144
|
|
|
150
|
-
|
|
145
|
+
function shouldStoreClaimable(input: SendMessageInput): boolean {
|
|
151
146
|
if (!input.claimable) return false;
|
|
152
147
|
if (input.kind === "task" || input.kind === "system") return true;
|
|
153
148
|
if (input.kind === "channel.event") return true;
|
|
154
149
|
return claimableAllowedForTarget(input.to);
|
|
155
150
|
}
|
|
156
151
|
|
|
157
|
-
|
|
152
|
+
function inferMessageKind(input: SendMessageInput): Message["kind"] {
|
|
158
153
|
if (input.kind) return input.kind;
|
|
159
154
|
if (input.claimable) return "task";
|
|
160
155
|
if (isChannelAgentId(input.from) || isChannelAgentId(input.to)) return "channel.event";
|
|
161
156
|
return "chat";
|
|
162
157
|
}
|
|
163
158
|
|
|
164
|
-
|
|
159
|
+
const REPLY_DUPLICATE_WINDOW_MS = 2 * 60 * 1000;
|
|
165
160
|
|
|
166
|
-
|
|
161
|
+
function findRecentDuplicateReply(input: SendMessageInput, threadId: number | null, now: number, hasAttachments: boolean): Message | null {
|
|
167
162
|
if (!input.replyTo || threadId === null || hasAttachments) return null;
|
|
168
163
|
const row = getDb().query(`
|
|
169
164
|
${MSG_SELECT}
|
|
@@ -192,7 +187,7 @@ export function findRecentDuplicateReply(input: SendMessageInput, threadId: numb
|
|
|
192
187
|
// receive time — but it must be a sane epoch-ms value. Returns null (column stays NULL, so
|
|
193
188
|
// readers fall back to created_at) for absent/invalid values or anything more than a minute
|
|
194
189
|
// in the future (clock-skew guard). Only a real backfilled time is stored.
|
|
195
|
-
|
|
190
|
+
function sanitizeOccurredAt(occurredAt: number | undefined, receivedAt: number): number | null {
|
|
196
191
|
if (typeof occurredAt !== "number" || !Number.isFinite(occurredAt)) return null;
|
|
197
192
|
if (occurredAt <= 0 || occurredAt > receivedAt + 60_000) return null;
|
|
198
193
|
return Math.floor(occurredAt);
|
|
@@ -369,20 +364,6 @@ export function findMessageByTelegramSource(input: {
|
|
|
369
364
|
return row ? rowToMessage(row) : null;
|
|
370
365
|
}
|
|
371
366
|
|
|
372
|
-
export function claimMessageRow(messageId: number, agentId: string, now: number): { ok: false; error: string } | { ok: true } {
|
|
373
|
-
const expiresAt = now + CLAIM_LEASE_MS;
|
|
374
|
-
// Idempotent: a same-agent re-claim refreshes the lease (preserving the original claimed_at).
|
|
375
|
-
// Without this, a delivery failure that locally drops the claim strands the message until the
|
|
376
|
-
// lease expires (#261) — the holder's retry would otherwise 409 against its own claim.
|
|
377
|
-
const result = getDb().query(
|
|
378
|
-
"UPDATE messages SET claimed_by = ?, claimed_at = COALESCE(claimed_at, ?), claim_expires_at = ? WHERE id = ? AND (claimed_by IS NULL OR claimed_by = ?)"
|
|
379
|
-
).run(agentId, now, expiresAt, messageId, agentId);
|
|
380
|
-
|
|
381
|
-
// Atomic: if changes === 0, someone else claimed it between our read and write
|
|
382
|
-
if (result.changes === 0) return { ok: false, error: "claim race — already claimed" };
|
|
383
|
-
return { ok: true };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
367
|
export function claimMessage(messageId: number, agentId: string, guard?: AgentSessionGuard): { ok: boolean; error?: string; task?: Task } {
|
|
387
368
|
releaseExpiredClaims();
|
|
388
369
|
const session = validateAgentSession(agentId, guard);
|
|
@@ -475,8 +456,3 @@ export function renewMessageClaim(messageId: number, agentId: string, guard?: Ag
|
|
|
475
456
|
})();
|
|
476
457
|
return { ok: true, task };
|
|
477
458
|
}
|
|
478
|
-
|
|
479
|
-
export function getMessage(id: number): Message | null {
|
|
480
|
-
const row = getDb().query(`${MSG_SELECT} WHERE m.id = ?`).get(id) as any;
|
|
481
|
-
return row ? rowToMessage(row) : null;
|
|
482
|
-
}
|
|
@@ -9,10 +9,10 @@ import { getDb, ValidationError } from "./connection";
|
|
|
9
9
|
export const NOTIFICATION_SUBSCRIPTION_TYPES = ["*", ...Object.keys(DEFAULT_TTL_MS)] as Array<NotificationType | "*">;
|
|
10
10
|
export const NOTIFICATION_SUBSCRIPTION_MODES = ["opt_in", "opt_out", "mute"] as const;
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
type NotificationSubscriptionType = NotificationType | "*";
|
|
13
13
|
export type NotificationSubscriptionMode = (typeof NOTIFICATION_SUBSCRIPTION_MODES)[number];
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
interface NotificationSubscription {
|
|
16
16
|
id: string;
|
|
17
17
|
agentId: string;
|
|
18
18
|
type: NotificationSubscriptionType;
|
package/src/db/orchestrators.ts
CHANGED
|
@@ -82,7 +82,7 @@ import type {
|
|
|
82
82
|
|
|
83
83
|
// --- Orchestrators ---
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
function rowToOrchestrator(row: any): Orchestrator {
|
|
86
86
|
const meta = parseJson<Record<string, unknown>>(row.meta, {});
|
|
87
87
|
const runtimePackage = parseRuntimePackage(meta.package);
|
|
88
88
|
const version = stringValue(meta.version);
|
|
@@ -133,7 +133,7 @@ export function rowToOrchestrator(row: any): Orchestrator {
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
function parseOrchestratorUpgrade(value: unknown): OrchestratorUpgradeState | undefined {
|
|
137
137
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
138
138
|
const v = value as Record<string, unknown>;
|
|
139
139
|
const desiredVersion = stringValue(v.desiredVersion);
|
|
@@ -167,7 +167,7 @@ export function setOrchestratorUpgradeState(id: string, state: OrchestratorUpgra
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
function orchestratorHealth(version: string | undefined, compatibility: ReturnType<typeof contractCompatibility>): OrchestratorHealth {
|
|
171
171
|
const issues: OrchestratorHealth["issues"] = [];
|
|
172
172
|
if (!version) {
|
|
173
173
|
issues.push({ code: "missing-version", detail: "Orchestrator did not report a package version." });
|
|
@@ -204,7 +204,7 @@ export function orchestratorHealth(version: string | undefined, compatibility: R
|
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
function mergeOrchestratorRuntimeMeta(meta: Record<string, unknown>, input: OrchestratorRuntimeInput): Record<string, unknown> {
|
|
208
208
|
const contracts = parseRuntimeContracts(input.contracts, input.protocolVersion !== undefined ? { orchestratorProtocol: input.protocolVersion } : {});
|
|
209
209
|
const capabilities = parseRuntimeCapabilities(input.capabilities);
|
|
210
210
|
return {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getDb } from "./connection.ts";
|
|
2
|
+
|
|
3
|
+
export function closeOpenPairsForAgent(agentId: string, now: number = Date.now()): void {
|
|
4
|
+
getDb().query(`
|
|
5
|
+
UPDATE pairs
|
|
6
|
+
SET status = 'ended', ended_at = ?, ended_by = ?, updated_at = ?
|
|
7
|
+
WHERE status IN ('pending', 'active') AND (requester_id = ? OR target_id = ?)
|
|
8
|
+
`).run(now, agentId, now, agentId, agentId);
|
|
9
|
+
}
|
package/src/db/pairs.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { getAgent, listAgents } from "./agents.ts";
|
|
|
18
18
|
import { getDb } from "./connection.ts";
|
|
19
19
|
import { rowToPair } from "./mappers.ts";
|
|
20
20
|
import { sendMessage } from "./messages.ts";
|
|
21
|
+
export { closeOpenPairsForAgent } from "./pair-maintenance.ts";
|
|
21
22
|
import type {
|
|
22
23
|
AgentCard,
|
|
23
24
|
ActivityEvent,
|
|
@@ -81,24 +82,16 @@ import type {
|
|
|
81
82
|
WorkspaceStatus,
|
|
82
83
|
} from "../types";
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const OPEN_PAIR_STATUSES = ["pending", "active"] as const;
|
|
86
|
+
const DEFAULT_PAIR_TTL_MS = 5 * 60_000;
|
|
87
|
+
const MAX_PAIR_TTL_MS = DAY_MS;
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
function expirePendingPairs(now: number = Date.now()): void {
|
|
89
90
|
getDb().query("UPDATE pairs SET status = 'expired', updated_at = ?, ended_at = ? WHERE status = 'pending' AND expires_at <= ?")
|
|
90
91
|
.run(now, now, now);
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
getDb().query(`
|
|
95
|
-
UPDATE pairs
|
|
96
|
-
SET status = 'ended', ended_at = ?, ended_by = ?, updated_at = ?
|
|
97
|
-
WHERE status IN ('pending', 'active') AND (requester_id = ? OR target_id = ?)
|
|
98
|
-
`).run(now, agentId, now, agentId, agentId);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function getOpenPairForAgent(agentId: string): PairSession | null {
|
|
94
|
+
function getOpenPairForAgent(agentId: string): PairSession | null {
|
|
102
95
|
expirePendingPairs();
|
|
103
96
|
const row = getDb().query(`
|
|
104
97
|
SELECT * FROM pairs
|
|
@@ -109,15 +102,15 @@ export function getOpenPairForAgent(agentId: string): PairSession | null {
|
|
|
109
102
|
return row ? rowToPair(row) : null;
|
|
110
103
|
}
|
|
111
104
|
|
|
112
|
-
|
|
105
|
+
function pairParticipant(pair: PairSession, agentId: string): boolean {
|
|
113
106
|
return pair.requesterId === agentId || pair.targetId === agentId;
|
|
114
107
|
}
|
|
115
108
|
|
|
116
|
-
|
|
109
|
+
function pairPeer(pair: PairSession, agentId: string): string {
|
|
117
110
|
return pair.requesterId === agentId ? pair.targetId : pair.requesterId;
|
|
118
111
|
}
|
|
119
112
|
|
|
120
|
-
|
|
113
|
+
function resolvePairTarget(target: string, requesterId: string): {
|
|
121
114
|
ok: true;
|
|
122
115
|
agent: AgentCard;
|
|
123
116
|
} | {
|
|
@@ -153,7 +146,7 @@ export function resolvePairTarget(target: string, requesterId: string): {
|
|
|
153
146
|
return { ok: true, agent: available[0]! };
|
|
154
147
|
}
|
|
155
148
|
|
|
156
|
-
|
|
149
|
+
function pairSystemMessage(pair: PairSession, to: string, event: string, subject: string, body: string): Message {
|
|
157
150
|
return sendMessage({
|
|
158
151
|
from: "system",
|
|
159
152
|
to,
|
|
@@ -321,4 +314,3 @@ export function sendPairMessage(id: string, input: PairMessageInput): { ok: true
|
|
|
321
314
|
getDb().query("UPDATE pairs SET last_message_at = ?, updated_at = ? WHERE id = ?").run(now, now, id);
|
|
322
315
|
return { ok: true, pair: getPair(id)!, message };
|
|
323
316
|
}
|
|
324
|
-
|
package/src/db/plans.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getDb } from "./connection.ts";
|
|
2
|
+
|
|
3
|
+
export function runningAgentForPolicy(policyName: string): string | null {
|
|
4
|
+
const row = getDb().query(`
|
|
5
|
+
SELECT s.agent_id
|
|
6
|
+
FROM managed_agent_state s
|
|
7
|
+
JOIN agents a ON a.id = s.agent_id
|
|
8
|
+
WHERE s.policy_name = ?
|
|
9
|
+
AND s.status = 'running'
|
|
10
|
+
AND s.agent_id IS NOT NULL
|
|
11
|
+
AND a.status != 'offline'
|
|
12
|
+
`).get(policyName) as { agent_id?: string } | undefined;
|
|
13
|
+
return row?.agent_id ?? null;
|
|
14
|
+
}
|
|
@@ -203,7 +203,7 @@ const BLACKBOARD_TO_PROJECT_KIND: Record<BlackboardEntryKind, ProjectEntryKind>
|
|
|
203
203
|
ruled_out: "rule",
|
|
204
204
|
};
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
interface PromoteBlackboardEntryInput {
|
|
207
207
|
blackboardEntryId: string;
|
|
208
208
|
projectId: string;
|
|
209
209
|
}
|
package/src/db/projects.ts
CHANGED
|
@@ -76,7 +76,7 @@ export function rootsForProject(id: string): ProjectRoot[] {
|
|
|
76
76
|
return rows.map(rowToRoot);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
interface EnsureProjectInput {
|
|
80
80
|
slug: string;
|
|
81
81
|
title?: string;
|
|
82
82
|
description?: string;
|
|
@@ -265,7 +265,7 @@ export function dependencies(projectId: string): string[] {
|
|
|
265
265
|
return rows.map((row) => row.to_project);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
interface ResolveProjectEntriesOptions {
|
|
269
269
|
kind?: ProjectEntryKind;
|
|
270
270
|
includeInactive?: boolean;
|
|
271
271
|
/** When true (default), merge in entries from transitive `contains` ancestors,
|
package/src/db/shared.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { parseJson } from "../utils";
|
|
2
|
+
|
|
3
|
+
export function parseStringArray(raw: string): string[] {
|
|
4
|
+
const parsed = parseJson<unknown>(raw, []);
|
|
5
|
+
if (!Array.isArray(parsed)) return [];
|
|
6
|
+
return parsed.filter((value): value is string => typeof value === "string");
|
|
7
|
+
}
|
package/src/db/stats.ts
CHANGED
|
@@ -147,7 +147,7 @@ export type AnalyticsPeriod = keyof typeof ANALYTICS_PERIODS;
|
|
|
147
147
|
// Conversation categories. Order is significant: a claimable/channel/pair message
|
|
148
148
|
// is classified as such even when it is also a reply; only an otherwise-plain reply
|
|
149
149
|
// counts as "Replies". Session/control/system rows are EXCLUDED (they're streams 2/mechanical).
|
|
150
|
-
|
|
150
|
+
const CONVERSATION_CATEGORIES = ["Chat", "Replies", "Channel", "Pair", "Work items"] as const;
|
|
151
151
|
export type ConversationCategory = (typeof CONVERSATION_CATEGORIES)[number];
|
|
152
152
|
const CONVERSATION_CATEGORY_SQL = `
|
|
153
153
|
CASE
|
|
@@ -162,7 +162,7 @@ const CONVERSATION_FILTER = `kind NOT IN ('session','control','system') AND from
|
|
|
162
162
|
|
|
163
163
|
// Session-mirror event sub-types we surface as the "agent work" composition. (prompt
|
|
164
164
|
// is counted in the user-interaction stream; notice is mechanical chatter.)
|
|
165
|
-
|
|
165
|
+
const WORK_TYPES = ["tool", "reasoning", "narration", "response"] as const;
|
|
166
166
|
export type WorkType = (typeof WORK_TYPES)[number];
|
|
167
167
|
|
|
168
168
|
export interface AnalyticsData {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getDb } from "./connection.ts";
|
|
2
|
+
import { rowToTaskEvent } from "./mappers.ts";
|
|
3
|
+
import type { TaskEvent } from "../types";
|
|
4
|
+
|
|
5
|
+
export const TASK_SELECT = "SELECT * FROM tasks";
|
|
6
|
+
|
|
7
|
+
export function insertTaskEvent(taskId: number, event: Required<Omit<TaskEvent, "id" | "taskId" | "createdAt">>, now: number): TaskEvent {
|
|
8
|
+
const result = getDb().query(`
|
|
9
|
+
INSERT INTO task_events (task_id, source, type, severity, title, body, metadata, created_at)
|
|
10
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
11
|
+
`).run(taskId, event.source, event.type, event.severity, event.title, event.body, JSON.stringify(event.metadata), now);
|
|
12
|
+
return getTaskEvent(Number(result.lastInsertRowid))!;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getTaskEvent(id: number): TaskEvent | null {
|
|
16
|
+
const row = getDb().query("SELECT * FROM task_events WHERE id = ?").get(id) as any;
|
|
17
|
+
return row ? rowToTaskEvent(row) : null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getDb } from "./connection.ts";
|
|
2
|
+
import { TASK_SELECT, insertTaskEvent } from "./task-core.ts";
|
|
3
|
+
|
|
4
|
+
// On-demand automation tasks (targetMode=on_demand_agent) are bound to a single
|
|
5
|
+
// ephemeral agent spawned just for that task. When that agent's claim is released,
|
|
6
|
+
// the task must not return to a claimable pool that can never match it again.
|
|
7
|
+
export function settleSingleTargetOnDemandTasks(condition: string, params: any[], now: number, reason: string): void {
|
|
8
|
+
const selectSql = `${TASK_SELECT} WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`;
|
|
9
|
+
const rows = getDb().query(selectSql).all(...params) as any[];
|
|
10
|
+
if (rows.length === 0) return;
|
|
11
|
+
getDb().query(
|
|
12
|
+
`UPDATE tasks SET status = 'done', claim_expires_at = NULL, updated_at = ?, last_seen_at = ? WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`
|
|
13
|
+
).run(now, now, ...params);
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
insertTaskEvent(row.id, {
|
|
16
|
+
source: "agent-relay",
|
|
17
|
+
type: "task.auto-completed",
|
|
18
|
+
severity: row.severity,
|
|
19
|
+
title: "On-demand task auto-resolved",
|
|
20
|
+
body: `On-demand agent ${row.claimed_by ?? "(unknown)"} exited (${reason}); task resolved so it does not orphan`,
|
|
21
|
+
metadata: { agentId: row.claimed_by, reason, completedBy: "relay" },
|
|
22
|
+
}, now);
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/db/tasks.ts
CHANGED
|
@@ -14,11 +14,13 @@ import {
|
|
|
14
14
|
} from "../contracts";
|
|
15
15
|
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
|
|
16
16
|
import { matchAgents } from "../agent-ref";
|
|
17
|
-
import { getAgent,
|
|
17
|
+
import { getAgent, validateAgentSession } from "./agents.ts";
|
|
18
18
|
import { messageTaskClosed } from "./claim-sql.ts";
|
|
19
19
|
import { ClaimError, getDb } from "./connection.ts";
|
|
20
20
|
import { rowToTask, rowToTaskEvent } from "./mappers.ts";
|
|
21
|
-
import { claimMessageRow } from "./
|
|
21
|
+
import { claimMessageRow } from "./message-claims.ts";
|
|
22
|
+
import { TASK_SELECT, insertTaskEvent } from "./task-core.ts";
|
|
23
|
+
import { settleSingleTargetOnDemandTasks } from "./task-maintenance.ts";
|
|
22
24
|
import type {
|
|
23
25
|
AgentCard,
|
|
24
26
|
ActivityEvent,
|
|
@@ -82,7 +84,7 @@ import type {
|
|
|
82
84
|
WorkspaceStatus,
|
|
83
85
|
} from "../types";
|
|
84
86
|
|
|
85
|
-
export
|
|
87
|
+
export { TASK_SELECT, insertTaskEvent } from "./task-core.ts";
|
|
86
88
|
|
|
87
89
|
export function findOpenTaskByDedupe(source: string, dedupeKey: string): Task | null {
|
|
88
90
|
const row = getDb()
|
|
@@ -91,19 +93,6 @@ export function findOpenTaskByDedupe(source: string, dedupeKey: string): Task |
|
|
|
91
93
|
return row ? rowToTask(row) : null;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
export function insertTaskEvent(taskId: number, event: Required<Omit<TaskEvent, "id" | "taskId" | "createdAt">>, now: number): TaskEvent {
|
|
95
|
-
const result = getDb().query(`
|
|
96
|
-
INSERT INTO task_events (task_id, source, type, severity, title, body, metadata, created_at)
|
|
97
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
98
|
-
`).run(taskId, event.source, event.type, event.severity, event.title, event.body, JSON.stringify(event.metadata), now);
|
|
99
|
-
return getTaskEvent(Number(result.lastInsertRowid))!;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function getTaskEvent(id: number): TaskEvent | null {
|
|
103
|
-
const row = getDb().query("SELECT * FROM task_events WHERE id = ?").get(id) as any;
|
|
104
|
-
return row ? rowToTaskEvent(row) : null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
96
|
export function taskMessageBody(task: Pick<Task, "id" | "source" | "severity" | "body" | "externalUrl" | "dedupeKey">): string {
|
|
108
97
|
const lines = [
|
|
109
98
|
`[task:${task.id}] ${task.severity.toUpperCase()} from ${task.source}`,
|
|
@@ -405,4 +394,3 @@ export function listCallbackDeliveries(taskId: number): Array<{
|
|
|
405
394
|
}
|
|
406
395
|
|
|
407
396
|
// --- Pair sessions ---
|
|
408
|
-
|
package/src/db/teams.ts
CHANGED
|
@@ -26,7 +26,7 @@ interface TeamRow {
|
|
|
26
26
|
dissolved_at: number | null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
interface TeamDissolveInput {
|
|
30
30
|
outcomeStatus?: Team["outcomeStatus"];
|
|
31
31
|
outcomeSummary?: string;
|
|
32
32
|
landsClean?: boolean;
|
|
@@ -53,7 +53,7 @@ export interface TeamOutcomeCompositionAggregate {
|
|
|
53
53
|
avgWallClockMs: number | null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
interface TeamOutcomeRecent {
|
|
57
57
|
id: string;
|
|
58
58
|
title?: string;
|
|
59
59
|
outcome: Team["outcomeStatus"] | null;
|
|
@@ -274,7 +274,7 @@ export function getTeamOutcomeAggregates(input: { recentLimit?: number } = {}):
|
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
|
|
277
|
+
interface TeamDissolveResult {
|
|
278
278
|
team: Team;
|
|
279
279
|
/** Active blackboard findings not yet lifted into a project — surfaced (not auto-promoted)
|
|
280
280
|
* so the operator/agent can decide what graduates to durable project knowledge (#404). */
|
package/src/db/workspaces.ts
CHANGED
|
@@ -78,7 +78,7 @@ import type {
|
|
|
78
78
|
WorkspaceStatus,
|
|
79
79
|
} from "../types";
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
function rowToWorkspace(row: any): WorkspaceRecord {
|
|
82
82
|
return {
|
|
83
83
|
id: row.id,
|
|
84
84
|
repoRoot: row.repo_root,
|
|
@@ -266,7 +266,7 @@ export function pruneOrphanedSharedWorkspaces(): string[] {
|
|
|
266
266
|
// owner's branch-state badge" (#236). A hook, not a direct emit, because getDb().ts is
|
|
267
267
|
// the lowest layer — sse.ts imports db, so db can't import sse without a cycle. sse
|
|
268
268
|
// registers a listener at startup that re-emits the owner agent's status over SSE.
|
|
269
|
-
|
|
269
|
+
type WorkspaceChangeListener = (workspace: WorkspaceRecord) => void;
|
|
270
270
|
export const workspaceChangeListeners = new Set<WorkspaceChangeListener>();
|
|
271
271
|
export function onWorkspaceChange(listener: WorkspaceChangeListener): void {
|
|
272
272
|
workspaceChangeListeners.add(listener);
|
|
@@ -322,9 +322,9 @@ export function setWorkspaceBranch(id: string, branch: string, baseSha?: string)
|
|
|
322
322
|
|
|
323
323
|
// Workspace statuses that count as "live" for stewardship — an agent owning one
|
|
324
324
|
// of these is a candidate steward; the repo is worth coordinating.
|
|
325
|
-
|
|
325
|
+
const STEWARD_LIVE_STATUSES = "'active', 'ready', 'conflict', 'review_requested', 'merge_planned'";
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
interface RepoStewardRecord {
|
|
328
328
|
repoRoot: string;
|
|
329
329
|
stewardAgentId?: string;
|
|
330
330
|
lastStewardAgentId?: string;
|
|
@@ -332,7 +332,7 @@ export interface RepoStewardRecord {
|
|
|
332
332
|
updatedAt: number;
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
|
|
335
|
+
function rowToRepoSteward(row: any): RepoStewardRecord {
|
|
336
336
|
return {
|
|
337
337
|
repoRoot: row.repo_root,
|
|
338
338
|
stewardAgentId: row.steward_agent_id ?? undefined,
|
|
@@ -354,7 +354,7 @@ export function listRepoStewards(): RepoStewardRecord[] {
|
|
|
354
354
|
// Persist the elected steward for a repo. The row is never deleted, so a repo's
|
|
355
355
|
// stewardship survives a full all-agents-offline gap (steward goes NULL/dormant,
|
|
356
356
|
// last_steward_agent_id keeps continuity) and resumes on the next agent join.
|
|
357
|
-
|
|
357
|
+
function upsertRepoSteward(repoRoot: string, steward: string | null, now: number): void {
|
|
358
358
|
getDb().query(`
|
|
359
359
|
INSERT INTO repo_stewards (repo_root, steward_agent_id, last_steward_agent_id, elected_at, updated_at)
|
|
360
360
|
VALUES ($repoRoot, $steward, $steward, $electedAt, $now)
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,11 @@ import { startPlanLiveBindings } from "./services/plan-live-bindings";
|
|
|
15
15
|
import { resolve, sep } from "path";
|
|
16
16
|
import { gzipSync, brotliCompressSync, constants as zlibConstants } from "node:zlib";
|
|
17
17
|
import {
|
|
18
|
+
AUTOMATION_INTERVAL_MS,
|
|
19
|
+
IDLE_TIMEOUT_SECONDS,
|
|
20
|
+
LOG_REQUESTS,
|
|
18
21
|
VERSION,
|
|
22
|
+
relayToken,
|
|
19
23
|
} from "./config";
|
|
20
24
|
import { CONTRACT_VERSIONS } from "./contracts";
|
|
21
25
|
import {
|
|
@@ -69,9 +73,6 @@ function startServer(): void {
|
|
|
69
73
|
const PORT = Number(process.env.PORT) || 4850;
|
|
70
74
|
const HOST = process.env.HOST || "127.0.0.1";
|
|
71
75
|
const DB_PATH = process.env.DB_PATH || "agent-relay.db";
|
|
72
|
-
const AUTOMATION_INTERVAL_MS = Number(process.env.AGENT_RELAY_AUTOMATION_INTERVAL_MS) || 30 * 1000;
|
|
73
|
-
const IDLE_TIMEOUT_SECONDS = Number(process.env.AGENT_RELAY_IDLE_TIMEOUT_SECONDS) || 255;
|
|
74
|
-
const LOG_REQUESTS = process.env.AGENT_RELAY_LOG_REQUESTS === "1";
|
|
75
76
|
|
|
76
77
|
assertSafeNetworkConfig(HOST);
|
|
77
78
|
initDb(DB_PATH);
|
|
@@ -246,9 +247,9 @@ export function createFetchHandler(
|
|
|
246
247
|
upstream.protocol = upstream.protocol === "https:" ? "wss:" : "ws:";
|
|
247
248
|
// Pass the relay token as a header (not a query param) so it never lands in
|
|
248
249
|
// access/proxy logs (#149). The orchestrator accepts both, but headers are safer.
|
|
249
|
-
const
|
|
250
|
+
const token = relayToken();
|
|
250
251
|
const upgraded = server.upgrade(req, {
|
|
251
|
-
data: { kind: "terminal-proxy", upstreamUrl: upstream.toString(), upstreamToken:
|
|
252
|
+
data: { kind: "terminal-proxy", upstreamUrl: upstream.toString(), upstreamToken: token, pending: [] },
|
|
252
253
|
});
|
|
253
254
|
if (!upgraded) return new Response("WebSocket upgrade failed", { status: 400 });
|
|
254
255
|
return undefined;
|
|
@@ -2,7 +2,7 @@ import type { IssueRef, IssueRefStatus } from "../types";
|
|
|
2
2
|
import type { IssueTrackerAdapter, IssueTrackerCloseOptions, IssueTrackerReopenOptions, IssueTrackerStatus } from "./adapter";
|
|
3
3
|
|
|
4
4
|
export const GITHUB_FIXED_LABEL = "fixed";
|
|
5
|
-
|
|
5
|
+
const GITHUB_CREDENTIAL_REQUIREMENT = "GitHub issue tracker adapter requires gh authenticated with a token that has issues:write scope";
|
|
6
6
|
|
|
7
7
|
type GitHubApiMethod = "GET" | "PATCH" | "POST";
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ export interface GitHubApiRequest {
|
|
|
12
12
|
body?: Record<string, unknown>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
type GitHubApi = (request: GitHubApiRequest) => Promise<unknown>;
|
|
16
16
|
|
|
17
17
|
interface GitHubIssuePayload {
|
|
18
18
|
state?: unknown;
|
|
@@ -92,7 +92,7 @@ function uniqueLabels(labels: string[]): string[] {
|
|
|
92
92
|
return [...new Set(labels.map((label) => label.trim()).filter(Boolean))];
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
class GitHubIssueTrackerAdapter implements IssueTrackerAdapter {
|
|
96
96
|
constructor(private readonly api: GitHubApi = ghApi) {}
|
|
97
97
|
|
|
98
98
|
resolveLink(ref: IssueRef): string {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONFLICT_SCAN_INTERVAL_MS,
|
|
3
|
+
DAY_MS,
|
|
4
|
+
DB_MAINTENANCE_INTERVAL_MS,
|
|
5
|
+
DB_VACUUM_EVERY,
|
|
6
|
+
ORPHAN_REAPER_INTERVAL_MS,
|
|
7
|
+
OUTBOX_RETENTION_MS,
|
|
8
|
+
REAP_INTERVAL_MS,
|
|
9
|
+
TOKEN_RECORD_RETENTION_SECONDS,
|
|
10
|
+
WORKSPACE_AUTO_MERGE_INTERVAL_MS,
|
|
11
|
+
WORKSPACE_GC_INTERVAL_MS,
|
|
12
|
+
WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
|
|
13
|
+
WORKSPACE_RETENTION_MS,
|
|
14
|
+
WORKSPACE_REVIEW_TTL_MS,
|
|
15
|
+
envMsOrDefault,
|
|
16
|
+
orphanReapCooldownMs,
|
|
17
|
+
orphanReapEnabled,
|
|
18
|
+
orphanGraceMs,
|
|
19
|
+
stewardEscalationMs,
|
|
20
|
+
stewardFallbackTarget,
|
|
21
|
+
} from "../config";
|
|
22
|
+
import { READY_TO_LAND_STATUSES, TERMINAL_WORKSPACE_STATUSES } from "../workspace-phase";
|
|
23
|
+
import type { WorkspaceStatus } from "../types";
|
|
24
|
+
|
|
25
|
+
export { DAY_MS, REAP_INTERVAL_MS, TERMINAL_WORKSPACE_STATUSES };
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
28
|
+
export const SCHEDULER_TICK_MS = 10_000;
|
|
29
|
+
export const STRANDABLE_STATUSES = new Set<WorkspaceStatus>([...READY_TO_LAND_STATUSES, "conflict"]);
|
|
30
|
+
export const CONFLICT_SCAN_STATUSES = new Set<WorkspaceStatus>(["active", "ready", "review_requested", "merge_planned", "conflict"]);
|
|
31
|
+
export const LANDED_RECONCILE_STATUSES = new Set<WorkspaceStatus>(["merge_planned", "review_requested", "conflict"]);
|
|
32
|
+
|
|
33
|
+
export const teamReapGraceMs = () => envMsOrDefault("TEAM_REAP_GRACE_MS", 30 * 60 * 1000);
|
|
34
|
+
export const teamReapIntervalMs = () => envMsOrDefault("TEAM_REAP_INTERVAL_MS", 5 * 60 * 1000);
|
|
35
|
+
export {
|
|
36
|
+
CONFLICT_SCAN_INTERVAL_MS,
|
|
37
|
+
DB_MAINTENANCE_INTERVAL_MS,
|
|
38
|
+
DB_VACUUM_EVERY,
|
|
39
|
+
ORPHAN_REAPER_INTERVAL_MS,
|
|
40
|
+
OUTBOX_RETENTION_MS,
|
|
41
|
+
TOKEN_RECORD_RETENTION_SECONDS,
|
|
42
|
+
WORKSPACE_AUTO_MERGE_INTERVAL_MS,
|
|
43
|
+
WORKSPACE_GC_INTERVAL_MS,
|
|
44
|
+
WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
|
|
45
|
+
WORKSPACE_RETENTION_MS,
|
|
46
|
+
WORKSPACE_REVIEW_TTL_MS,
|
|
47
|
+
orphanGraceMs,
|
|
48
|
+
orphanReapCooldownMs,
|
|
49
|
+
orphanReapEnabled,
|
|
50
|
+
stewardEscalationMs,
|
|
51
|
+
stewardFallbackTarget,
|
|
52
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { emitRelayEvent } from "../events";
|
|
2
|
+
import { routeNotification, type NotificationScope, type NotificationType } from "../notification-router";
|
|
3
|
+
import type { Command } from "../types";
|
|
4
|
+
|
|
5
|
+
export function emitCommand(command: Command): void {
|
|
6
|
+
const suffix = command.status === "timed_out" ? "timed_out" : command.status;
|
|
7
|
+
emitRelayEvent({
|
|
8
|
+
type: `command.${suffix}`,
|
|
9
|
+
source: command.source,
|
|
10
|
+
subject: command.id,
|
|
11
|
+
data: { command },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Route a maintenance notification, swallowing failures (a stale/missing/misconfigured
|
|
16
|
+
// target must never break the sweep). Returns the target on success, null otherwise.
|
|
17
|
+
export function notifyTarget(
|
|
18
|
+
type: NotificationType,
|
|
19
|
+
scope: NotificationScope,
|
|
20
|
+
target: string,
|
|
21
|
+
subject: string,
|
|
22
|
+
body: string,
|
|
23
|
+
payload: Record<string, unknown>,
|
|
24
|
+
): string | null {
|
|
25
|
+
if (!target) return null;
|
|
26
|
+
try {
|
|
27
|
+
routeNotification({ type, scope, recipients: [target], message: { subject, body, payload } });
|
|
28
|
+
return target;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|