agent-relay-server 0.62.2 → 0.63.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 +1 -1
- package/package.json +2 -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/scripts/orchestrator-spawn-smoke.ts +87 -67
- package/src/agent-ref.ts +8 -6
- 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/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/continuation-archives.ts +4 -4
- package/src/db/delivery.ts +75 -30
- 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 +157 -18
- package/src/db/messages.ts +23 -48
- 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/issue-tracker/github.ts +3 -3
- package/src/maintenance/constants.ts +35 -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 +3 -3
- package/src/provider-state.ts +1 -1
- package/src/reviewer-pipeline.ts +1 -1
- 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 +7 -7
- package/src/services/managed-running.ts +1 -1
- package/src/services/parity-harness.ts +3 -3
- package/src/services/plan-graph.ts +3 -3
- 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/workspace-actions.ts +1 -1
- package/src/workspace-probe.ts +3 -7
- 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/inbox.ts
CHANGED
|
@@ -212,7 +212,7 @@ export function createChatHistoryImport(input: {
|
|
|
212
212
|
return getChatHistoryImport(id)!;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
function getChatHistoryImport(id: string): ChatHistoryImport | null {
|
|
216
216
|
const row = getDb().query("SELECT * FROM chat_history_imports WHERE id = ?").get(id) as any | undefined;
|
|
217
217
|
if (!row) return null;
|
|
218
218
|
const entries = (getDb().query(
|
package/src/db/integrations.ts
CHANGED
|
@@ -212,7 +212,7 @@ export function listIntegrationTaskStats(): IntegrationTaskStats[] {
|
|
|
212
212
|
}));
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
type IntegrationRegistryInput = {
|
|
216
216
|
name: string;
|
|
217
217
|
displayName?: string;
|
|
218
218
|
description?: string;
|
|
@@ -231,7 +231,7 @@ export type IntegrationRegistryInput = {
|
|
|
231
231
|
source?: "api" | "env" | "observed";
|
|
232
232
|
};
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
function rowToIntegrationRegistry(row: any): Omit<IntegrationSummary, "configured" | "observed" | "callbackConfigured" | "rateLimit" | "taskStats"> & {
|
|
235
235
|
source: "api" | "env" | "observed";
|
|
236
236
|
lastEventAt?: number;
|
|
237
237
|
lastTaskAt?: number;
|
|
@@ -312,7 +312,7 @@ export function upsertIntegrationRegistry(input: IntegrationRegistryInput): Retu
|
|
|
312
312
|
return getIntegrationRegistry(name)!;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
|
|
315
|
+
function getIntegrationRegistry(name: string): ReturnType<typeof rowToIntegrationRegistry> | null {
|
|
316
316
|
const row = getDb().query("SELECT * FROM integration_registry WHERE name = ?").get(name) as any;
|
|
317
317
|
return row ? rowToIntegrationRegistry(row) : null;
|
|
318
318
|
}
|
|
@@ -321,7 +321,7 @@ export function listIntegrationRegistry(): ReturnType<typeof rowToIntegrationReg
|
|
|
321
321
|
return (getDb().query("SELECT * FROM integration_registry ORDER BY display_name COLLATE NOCASE, name COLLATE NOCASE").all() as any[]).map(rowToIntegrationRegistry);
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
|
|
324
|
+
function updateIntegrationObserved(name: string, eventAt: number): void {
|
|
325
325
|
const now = Date.now();
|
|
326
326
|
getDb().query(`
|
|
327
327
|
INSERT INTO integration_registry (name, enabled, scopes, targets, channels, tags, manifest, source, created_at, updated_at, last_event_at, last_task_at)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { getDb, ValidationError } from "./connection.ts";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const ISSUE_LIFECYCLE_ACTIONS = ["close", "reopen"] as const;
|
|
4
|
+
const ISSUE_LIFECYCLE_STATUSES = ["succeeded", "failed"] as const;
|
|
5
|
+
type IssueLifecycleAction = (typeof ISSUE_LIFECYCLE_ACTIONS)[number];
|
|
6
|
+
type IssueLifecycleStatus = (typeof ISSUE_LIFECYCLE_STATUSES)[number];
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface IssueLifecycleActionRecord {
|
|
9
9
|
id: number;
|
|
10
10
|
issueRefId: string;
|
|
11
11
|
action: IssueLifecycleAction;
|
|
@@ -24,7 +24,7 @@ export interface IssueLifecycleActionRecord {
|
|
|
24
24
|
updatedAt: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
interface UpsertIssueLifecycleActionInput {
|
|
28
28
|
issueRefId: string;
|
|
29
29
|
action: IssueLifecycleAction;
|
|
30
30
|
landKey: string;
|
package/src/db/mappers.ts
CHANGED
|
@@ -14,8 +14,7 @@ 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 { parseStringArray } from "./
|
|
18
|
-
import { artifactId } from "./artifacts.ts";
|
|
17
|
+
import { parseStringArray } from "./shared.ts";
|
|
19
18
|
import type {
|
|
20
19
|
AgentCard,
|
|
21
20
|
ActivityEvent,
|
package/src/db/merge-lease.ts
CHANGED
|
@@ -80,7 +80,7 @@ import type {
|
|
|
80
80
|
|
|
81
81
|
// --- Per-repo merge serialization lease (issue #157) -----------------------
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
interface MergeLeaseRecord {
|
|
84
84
|
repoRoot: string;
|
|
85
85
|
workspaceId: string;
|
|
86
86
|
commandId?: string;
|
|
@@ -89,7 +89,7 @@ export interface MergeLeaseRecord {
|
|
|
89
89
|
expiresAt: number;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
function rowToMergeLease(row: any): MergeLeaseRecord {
|
|
93
93
|
return {
|
|
94
94
|
repoRoot: row.repo_root,
|
|
95
95
|
workspaceId: row.workspace_id,
|
|
@@ -171,7 +171,7 @@ export interface MergePauseRecord {
|
|
|
171
171
|
expiresAt: number;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
function rowToMergePause(row: any): MergePauseRecord {
|
|
175
175
|
return {
|
|
176
176
|
id: row.id,
|
|
177
177
|
repoRoot: row.repo_root,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CLAIM_LEASE_MS } from "../config";
|
|
2
|
+
import { getDb } from "./connection.ts";
|
|
3
|
+
|
|
4
|
+
export function claimMessageRow(messageId: number, agentId: string, now: number): { ok: false; error: string } | { ok: true } {
|
|
5
|
+
const expiresAt = now + CLAIM_LEASE_MS;
|
|
6
|
+
// Idempotent: a same-agent re-claim refreshes the lease (preserving the original claimed_at).
|
|
7
|
+
// Without this, a delivery failure that locally drops the claim strands the message until the
|
|
8
|
+
// lease expires (#261) — the holder's retry would otherwise 409 against its own claim.
|
|
9
|
+
const result = getDb().query(
|
|
10
|
+
"UPDATE messages SET claimed_by = ?, claimed_at = COALESCE(claimed_at, ?), claim_expires_at = ? WHERE id = ? AND (claimed_by IS NULL OR claimed_by = ?)"
|
|
11
|
+
).run(agentId, now, expiresAt, messageId, agentId);
|
|
12
|
+
|
|
13
|
+
// Atomic: if changes === 0, someone else claimed it between our read and write
|
|
14
|
+
if (result.changes === 0) return { ok: false, error: "claim race — already claimed" };
|
|
15
|
+
return { ok: true };
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getDb } from "./connection.ts";
|
|
2
|
+
import { MSG_SELECT, rowToMessage } from "./mappers.ts";
|
|
3
|
+
import type { Message } from "../types";
|
|
4
|
+
|
|
5
|
+
export function getMessage(id: number): Message | null {
|
|
6
|
+
const row = getDb().query(`${MSG_SELECT} WHERE m.id = ?`).get(id) as any;
|
|
7
|
+
return row ? rowToMessage(row) : null;
|
|
8
|
+
}
|
package/src/db/message-reads.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
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
17
|
import { getAgent } from "./agents.ts";
|
|
18
|
-
import { sendMessageWithResult } from "./messages.ts";
|
|
18
|
+
import { getMessage, sendMessageWithResult } from "./messages.ts";
|
|
19
19
|
import { deleteArtifactLinksForEntity } from "./artifacts.ts";
|
|
20
20
|
import { getDb, timedQuery } from "./connection.ts";
|
|
21
21
|
import { legacyChannelTargets, isChannelAgentId } from "./delivery.ts";
|
|
@@ -159,6 +159,124 @@ export function listAgentChatHistory(query: {
|
|
|
159
159
|
return ascending ? messages : messages.reverse();
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function agentParticipationClauses(
|
|
163
|
+
alias: string,
|
|
164
|
+
agent: AgentCard,
|
|
165
|
+
legacyTargets: string[],
|
|
166
|
+
params: any[],
|
|
167
|
+
): string[] {
|
|
168
|
+
const clauses = [
|
|
169
|
+
`${alias}.from_agent = ?`,
|
|
170
|
+
`${alias}.to_target = ?`,
|
|
171
|
+
`${alias}.resolved_to_agent = ?`,
|
|
172
|
+
];
|
|
173
|
+
params.push(agent.id, agent.id, agent.id);
|
|
174
|
+
for (const tag of agent.tags) {
|
|
175
|
+
clauses.push(`${alias}.to_target = ?`);
|
|
176
|
+
params.push(`tag:${tag}`);
|
|
177
|
+
}
|
|
178
|
+
for (const cap of agent.capabilities) {
|
|
179
|
+
clauses.push(`${alias}.to_target = ?`);
|
|
180
|
+
params.push(`cap:${cap}`);
|
|
181
|
+
}
|
|
182
|
+
if (agent.label) {
|
|
183
|
+
clauses.push(`${alias}.to_target = ?`);
|
|
184
|
+
params.push(`label:${agent.label}`);
|
|
185
|
+
}
|
|
186
|
+
if (agent.teamId) {
|
|
187
|
+
clauses.push(`${alias}.to_target = ?`);
|
|
188
|
+
params.push(`team:${agent.teamId}`);
|
|
189
|
+
}
|
|
190
|
+
for (const target of legacyTargets) {
|
|
191
|
+
clauses.push(`${alias}.to_target = ?`);
|
|
192
|
+
params.push(target);
|
|
193
|
+
}
|
|
194
|
+
return clauses;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function scopedBroadcastClause(agent: AgentCard | null | undefined, legacyTargets: string[], params: any[]): string {
|
|
198
|
+
const senderGlobal = "NOT EXISTS (SELECT 1 FROM agents sender WHERE sender.id = m.from_agent AND sender.team_id IS NOT NULL)";
|
|
199
|
+
const noChannel = agent?.teamId
|
|
200
|
+
? `(m.channel IS NULL AND (${senderGlobal} OR EXISTS (SELECT 1 FROM agents sender WHERE sender.id = m.from_agent AND sender.team_id = ?)))`
|
|
201
|
+
: `(m.channel IS NULL AND ${senderGlobal})`;
|
|
202
|
+
if (agent?.teamId) params.push(agent.teamId);
|
|
203
|
+
if (!agent) return noChannel;
|
|
204
|
+
|
|
205
|
+
const participantParams: any[] = [];
|
|
206
|
+
const participantClauses = agentParticipationClauses("cp", agent, legacyTargets, participantParams);
|
|
207
|
+
const channel = `(m.channel IS NOT NULL AND (m.from_agent = ? OR EXISTS (
|
|
208
|
+
SELECT 1 FROM messages cp
|
|
209
|
+
WHERE cp.channel = m.channel
|
|
210
|
+
AND cp.id <> m.id
|
|
211
|
+
AND (${participantClauses.join(" OR ")})
|
|
212
|
+
)))`;
|
|
213
|
+
params.push(agent.id, ...participantParams);
|
|
214
|
+
return `(${noChannel} OR ${channel})`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function freshForSessionClause(agent: AgentCard | null | undefined, params: any[]): string {
|
|
218
|
+
// Same-ms registration+send can happen in tests and on fast local loops; >= preserves genuine
|
|
219
|
+
// post-registration messages while still excluding historical fan-out backlog for fresh sessions.
|
|
220
|
+
params.push(agent?.createdAt ?? Date.now());
|
|
221
|
+
return "(m.claimable = 1 OR m.created_at >= ?)";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function freshFanoutTargetClause(target: string, agent: AgentCard | null | undefined, params: any[]): string {
|
|
225
|
+
params.push(target);
|
|
226
|
+
return `(m.to_target = ? AND ${freshForSessionClause(agent, params)})`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Emojis that count as an explicit acknowledgement of a message thread (#437).
|
|
231
|
+
* A reaction with one of these suppresses the reply-reminder on subsequent deliveries.
|
|
232
|
+
* Policy: thumbs-up (👍) and check-mark (✅) classes only — passing reactions like ❤️
|
|
233
|
+
* or 👀 are not settling and must never cause false suppression.
|
|
234
|
+
*/
|
|
235
|
+
export const ACK_REACTION_EMOJIS: readonly string[] = ["👍", "✅"];
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Annotates messages with `replyExpected: false` when this agent has already settled them
|
|
239
|
+
* via a direct/thread reply OR an ack-class reaction. Used at poll time so the runner's
|
|
240
|
+
* delivery formatting naturally drops them from the reply-reminder anchor (#437).
|
|
241
|
+
*
|
|
242
|
+
* A single batch SQL query covers all replyable messages — no per-message round-trips.
|
|
243
|
+
*/
|
|
244
|
+
function annotateAnsweredMessages(messages: Message[], agentId: string): Message[] {
|
|
245
|
+
const replyableIds = messages
|
|
246
|
+
.filter((m) => m.replyExpected !== false && Number.isSafeInteger(m.id) && m.id > 0)
|
|
247
|
+
.map((m) => m.id);
|
|
248
|
+
if (replyableIds.length === 0) return messages;
|
|
249
|
+
|
|
250
|
+
const idPlaceholders = replyableIds.map(() => "?").join(", ");
|
|
251
|
+
const ackPlaceholders = ACK_REACTION_EMOJIS.map(() => "?").join(", ");
|
|
252
|
+
|
|
253
|
+
const answeredRows = getDb().query(`
|
|
254
|
+
SELECT m.id FROM messages m
|
|
255
|
+
WHERE m.id IN (${idPlaceholders})
|
|
256
|
+
AND (
|
|
257
|
+
EXISTS (
|
|
258
|
+
SELECT 1 FROM messages r
|
|
259
|
+
WHERE r.from_agent = ?
|
|
260
|
+
AND (
|
|
261
|
+
r.reply_to = m.id
|
|
262
|
+
OR (m.thread_id IS NOT NULL AND r.thread_id = m.thread_id AND r.id > m.id)
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
OR EXISTS (
|
|
266
|
+
SELECT 1 FROM message_reactions mr
|
|
267
|
+
WHERE mr.message_id = m.id
|
|
268
|
+
AND mr.actor_id = ?
|
|
269
|
+
AND mr.emoji IN (${ackPlaceholders})
|
|
270
|
+
AND mr.created_at >= m.created_at
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
`).all(...replyableIds, agentId, agentId, ...ACK_REACTION_EMOJIS) as Array<{ id: number }>;
|
|
274
|
+
|
|
275
|
+
if (answeredRows.length === 0) return messages;
|
|
276
|
+
const answeredIds = new Set(answeredRows.map((r) => r.id));
|
|
277
|
+
return messages.map((m) => (answeredIds.has(m.id) ? { ...m, replyExpected: false as const } : m));
|
|
278
|
+
}
|
|
279
|
+
|
|
162
280
|
export function pollMessages(query: PollQuery): Message[] {
|
|
163
281
|
const agent = getAgent(query.for);
|
|
164
282
|
const agentTags = agent?.tags ?? [];
|
|
@@ -170,24 +288,28 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
170
288
|
const conditions: string[] = [];
|
|
171
289
|
const params: any[] = [];
|
|
172
290
|
|
|
173
|
-
// Build target matching: direct + broadcast + tag + capability + label.
|
|
291
|
+
// Build target matching: direct + scoped broadcast + tag + capability + label.
|
|
174
292
|
// Channel agents also accept legacy bare provider targets (for example
|
|
175
293
|
// "telegram") so older clients keep working after canonical IDs became
|
|
176
294
|
// provider:account ("telegram:default").
|
|
177
|
-
const targetClauses = ["to_target = ?", "resolved_to_agent = ?"
|
|
295
|
+
const targetClauses = ["to_target = ?", "resolved_to_agent = ?"];
|
|
178
296
|
params.push(query.for, query.for);
|
|
297
|
+
{
|
|
298
|
+
const broadcastParams: any[] = [];
|
|
299
|
+
const broadcastScope = scopedBroadcastClause(agent, agentLegacyChannelTargets, broadcastParams);
|
|
300
|
+
const fresh = freshForSessionClause(agent, broadcastParams);
|
|
301
|
+
targetClauses.push(`(m.to_target = 'broadcast' AND ${broadcastScope} AND ${fresh})`);
|
|
302
|
+
params.push(...broadcastParams);
|
|
303
|
+
}
|
|
179
304
|
|
|
180
305
|
for (const tag of agentTags) {
|
|
181
|
-
targetClauses.push(
|
|
182
|
-
params.push(`tag:${tag}`);
|
|
306
|
+
targetClauses.push(freshFanoutTargetClause(`tag:${tag}`, agent, params));
|
|
183
307
|
}
|
|
184
308
|
for (const cap of agentCaps) {
|
|
185
|
-
targetClauses.push(
|
|
186
|
-
params.push(`cap:${cap}`);
|
|
309
|
+
targetClauses.push(freshFanoutTargetClause(`cap:${cap}`, agent, params));
|
|
187
310
|
}
|
|
188
311
|
if (agentLabel) {
|
|
189
|
-
targetClauses.push(
|
|
190
|
-
params.push(`label:${agentLabel}`);
|
|
312
|
+
targetClauses.push(freshFanoutTargetClause(`label:${agentLabel}`, agent, params));
|
|
191
313
|
}
|
|
192
314
|
if (agentTeamId) {
|
|
193
315
|
targetClauses.push("to_target = ?");
|
|
@@ -244,10 +366,10 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
244
366
|
params.push(limit);
|
|
245
367
|
|
|
246
368
|
const rows = timedQuery("pollMessages", () => getDb().query(sql).all(...params) as any[]);
|
|
247
|
-
return rows.map(rowToMessage);
|
|
369
|
+
return annotateAnsweredMessages(rows.map(rowToMessage), query.for);
|
|
248
370
|
}
|
|
249
371
|
|
|
250
|
-
|
|
372
|
+
function messageRequiresReply(message: Message): boolean {
|
|
251
373
|
// Server-owned notification flag (#283) wins over every kind/sender heuristic below: an
|
|
252
374
|
// explicit replyExpected:false is a fire-and-forget message that must never become an obligation.
|
|
253
375
|
if (message.replyExpected === false) return false;
|
|
@@ -265,13 +387,13 @@ export function recordField(value: unknown): Record<string, unknown> | undefined
|
|
|
265
387
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
266
388
|
}
|
|
267
389
|
|
|
268
|
-
|
|
390
|
+
function relayConversationId(message: Message): string | undefined {
|
|
269
391
|
const conversation = recordField(message.payload?.conversation);
|
|
270
392
|
const replyContext = recordField(message.payload?.replyContext);
|
|
271
393
|
return stringField(replyContext?.conversationId) ?? stringField(conversation?.id);
|
|
272
394
|
}
|
|
273
395
|
|
|
274
|
-
|
|
396
|
+
function isCoveredByLaterAgentResponse(message: Message, agentId: string): boolean {
|
|
275
397
|
// Order by id, not created_at: ids are monotonic insertion order, so this is
|
|
276
398
|
// robust when a reply lands in the same millisecond as the message it covers
|
|
277
399
|
// (created_at > … strictly would miss it, leaving the message wrongly pending).
|
|
@@ -292,7 +414,7 @@ export function isCoveredByLaterAgentResponse(message: Message, agentId: string)
|
|
|
292
414
|
});
|
|
293
415
|
}
|
|
294
416
|
|
|
295
|
-
|
|
417
|
+
function replyObligationFromMessage(message: Message, agentId: string): ReplyObligation {
|
|
296
418
|
return {
|
|
297
419
|
messageId: message.id,
|
|
298
420
|
agentId,
|
|
@@ -360,6 +482,25 @@ export function channelObligationAnsweredBy(
|
|
|
360
482
|
return open.length === 1 ? open[0]!.messageId : null;
|
|
361
483
|
}
|
|
362
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Spawn report-back linking (#492): a spawned worker's actual task is delivered as the provider
|
|
487
|
+
* prompt, while `seedSpawnReplyObligation` creates a short Stop-hook contract message so the worker
|
|
488
|
+
* cannot go idle without reporting back. Workers commonly send their natural completion report
|
|
489
|
+
* directly to the spawning coordinator; when there is exactly one open spawn obligation from that
|
|
490
|
+
* coordinator, that send is the answer. Link it to the obligation at send time so the Stop hook does
|
|
491
|
+
* not nag the worker into re-sending the same verdict through `/reply`.
|
|
492
|
+
*/
|
|
493
|
+
export function spawnObligationAnsweredBy(
|
|
494
|
+
agentId: string,
|
|
495
|
+
spawnerAgentId: string,
|
|
496
|
+
): number | null {
|
|
497
|
+
const open = listPendingReplyObligations(agentId, 50).filter((obligation) => {
|
|
498
|
+
if (obligation.from !== spawnerAgentId) return false;
|
|
499
|
+
return getMessage(obligation.messageId)?.payload?.source === "spawn-obligation";
|
|
500
|
+
});
|
|
501
|
+
return open.length === 1 ? open[0]!.messageId : null;
|
|
502
|
+
}
|
|
503
|
+
|
|
363
504
|
/**
|
|
364
505
|
* #413 / vent #121 — seed a pending reply obligation for a freshly-registered spawned worker so
|
|
365
506
|
* it cannot strand its verdict. A spawned agent gets its task as the positional spawn prompt, NOT
|
|
@@ -368,7 +509,7 @@ export function channelObligationAnsweredBy(
|
|
|
368
509
|
* spawn command by its `spawnRequestId` (same lookup as `seedContinuationObjectiveFromSpawnPrompt`),
|
|
369
510
|
* and — only when the spawn carried `expectReply` — posts a message FROM the spawner TO the worker
|
|
370
511
|
* and marks it read, making it an obligation immediately. The Stop hook then refuses to let the
|
|
371
|
-
* worker idle until it
|
|
512
|
+
* worker idle until it reports back; a natural direct send to the spawner is linked at send time.
|
|
372
513
|
*
|
|
373
514
|
* Call only on first registration; idempotent regardless (idempotencyKey dedups a double-fire).
|
|
374
515
|
* Never throws — a failure here must not abort registration. Returns the obligation message id,
|
|
@@ -393,15 +534,13 @@ export function seedSpawnReplyObligation(
|
|
|
393
534
|
const params = row ? parseJson<unknown>(row.params, {}) : {};
|
|
394
535
|
if (!isRecord(params) || params.expectReply !== true) return null;
|
|
395
536
|
|
|
396
|
-
const objective = typeof params.prompt === "string" ? params.prompt.trim() : "";
|
|
397
537
|
const contract = `Report-back requested by ${spawnedBy}. When you finish your spawned task, reply here with your result so the coordinator is notified — you should not go idle before delivering it.`;
|
|
398
|
-
const body = objective ? `${contract}\n\nYour task was:\n${objective}` : contract;
|
|
399
538
|
|
|
400
539
|
const { message } = sendMessageWithResult({
|
|
401
540
|
from: spawnedBy,
|
|
402
541
|
to: agentId,
|
|
403
542
|
subject: "Report your result when done",
|
|
404
|
-
body,
|
|
543
|
+
body: contract,
|
|
405
544
|
replyExpected: true,
|
|
406
545
|
// `payload.source` is the server-owned "this requires a reply" signal (see messageRequiresReply).
|
|
407
546
|
payload: { source: "spawn-obligation", spawnRequestId },
|
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);
|
|
@@ -265,13 +260,13 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
265
260
|
}
|
|
266
261
|
}
|
|
267
262
|
|
|
268
|
-
// #451 — moderate default TTL for
|
|
269
|
-
// stale broadcast/tag/cap/label/team
|
|
263
|
+
// #451/#461 — moderate default TTL for non-claimable FAN-OUT messages the sender didn't bound,
|
|
264
|
+
// so stale broadcast/tag/cap/label/team chat cannot wake a late-joining agent hours later. Exempt
|
|
270
265
|
// (never auto-expire, preserving store-ahead #234): direct DMs and policy targets (single
|
|
271
|
-
// recipient),
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
if (ttlUnspecified &&
|
|
266
|
+
// recipient), claimable work, and any message that already carried an explicit TTL — including an
|
|
267
|
+
// explicit `null` from routeNotification, whose per-type decision (e.g. a store-ahead
|
|
268
|
+
// `plan.bound_agent_exited` to `team:`) must win here.
|
|
269
|
+
if (ttlUnspecified && !claimable && isFanoutTarget(input.to)) {
|
|
275
270
|
maxAgeSeconds = DEFAULT_FANOUT_TTL_SECONDS;
|
|
276
271
|
}
|
|
277
272
|
|
|
@@ -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,9 +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
|
-
}
|
|
483
|
-
|
|
@@ -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
|
+
}
|