@desplega.ai/agent-swarm 1.113.0 → 1.115.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/README.md +2 -2
- package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
- package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
- package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
- package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
- package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
- package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
- package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
- package/dist/cli-0pygq9r1.js +429 -0
- package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
- package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
- package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
- package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
- package/dist/cli-52fg7s7x.js +269 -0
- package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
- package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
- package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
- package/dist/cli-86fdnerz.js +405 -0
- package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
- package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
- package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
- package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
- package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
- package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
- package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
- package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
- package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
- package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
- package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
- package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
- package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
- package/dist/cli-jkhwmmgg.js +136 -0
- package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
- package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
- package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
- package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
- package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
- package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
- package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
- package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
- package/dist/cli-t0vqevnm.js +393 -0
- package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
- package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
- package/dist/cli-wh91zby6.js +187222 -0
- package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
- package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
- package/dist/cli.js +32 -15
- package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
- package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
- package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
- package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
- package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
- package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
- package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
- package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
- package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
- package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
- package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
- package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
- package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
- package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
- package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
- package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
- package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
- package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
- package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
- package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
- package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
- package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
- package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
- package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
- package/dist/rbac-roles-kfesjy9y.js +28 -0
- package/dist/rbac-roles-mwcwhbxc.js +283 -0
- package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
- package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
- package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
- package/openapi.json +1327 -20
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +16 -6
- package/src/agentmail/templates.ts +25 -5
- package/src/be/audit-user.ts +28 -4
- package/src/be/db-queries/oauth.ts +42 -0
- package/src/be/db.ts +314 -28
- package/src/be/identity.ts +79 -0
- package/src/be/mcp-proxy.ts +132 -0
- package/src/be/migrations/109_rbac_roles.sql +79 -0
- package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
- package/src/be/migrations/112_script_connections_graphql.sql +96 -0
- package/src/be/migrations/113_task_routing_affinity.sql +9 -0
- package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
- package/src/be/modelsdev-cache.json +135 -0
- package/src/be/oauth-credential-bindings.ts +35 -0
- package/src/be/oauth-refresh-sweep.ts +159 -0
- package/src/be/rbac-audit.ts +52 -13
- package/src/be/rbac-roles.ts +424 -0
- package/src/be/script-connections.ts +459 -40
- package/src/be/script-credential-broker.ts +29 -3
- package/src/be/scripts/typecheck.ts +51 -7
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
- package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
- package/src/be/users.ts +24 -0
- package/src/cli.tsx +17 -0
- package/src/github/handlers.ts +10 -9
- package/src/github/templates.ts +45 -9
- package/src/gitlab/handlers.ts +20 -8
- package/src/gitlab/templates.ts +18 -6
- package/src/heartbeat/heartbeat.ts +143 -21
- package/src/http/all-routes.ts +3 -0
- package/src/http/approval-requests.ts +12 -0
- package/src/http/config.ts +44 -10
- package/src/http/core.ts +30 -3
- package/src/http/favorites.ts +1 -5
- package/src/http/index.ts +25 -1
- package/src/http/mcp-oauth.ts +2 -0
- package/src/http/mcp-servers.ts +44 -3
- package/src/http/oauth-generic.ts +105 -0
- package/src/http/poll.ts +20 -3
- package/src/http/script-connection-proxy.ts +116 -0
- package/src/http/script-connections.ts +1657 -0
- package/src/http/scripts.ts +27 -7
- package/src/http/skills.ts +11 -0
- package/src/http/tasks.ts +27 -13
- package/src/http/x.ts +6 -2
- package/src/integrations/kapso/inbound.ts +23 -6
- package/src/jira/sync.ts +111 -17
- package/src/jira/templates.ts +10 -2
- package/src/mcp-client/http-client.ts +194 -0
- package/src/oauth/app-validation.ts +16 -0
- package/src/oauth/ensure-token.ts +69 -14
- package/src/oauth/mcp-wrapper.ts +15 -0
- package/src/oauth/wrapper.ts +48 -20
- package/src/prompts/session-templates.ts +3 -3
- package/src/providers/codex-models.ts +100 -30
- package/src/providers/modelsdev-reasoning.json +51 -0
- package/src/providers/pi-mono-mcp-client.ts +2 -133
- package/src/providers/reasoning-effort.ts +11 -5
- package/src/rbac/admission.ts +68 -0
- package/src/rbac/index.ts +2 -0
- package/src/rbac/legacy-policy.ts +12 -0
- package/src/rbac/permissions.ts +48 -0
- package/src/scheduler/scheduler.ts +9 -1
- package/src/scripts-runtime/api-client.ts +151 -19
- package/src/scripts-runtime/api-types.ts +38 -6
- package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
- package/src/scripts-runtime/credential-broker/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/eval-harness.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -1
- package/src/scripts-runtime/mcp-client.ts +87 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +6 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
- package/src/server-user.ts +114 -59
- package/src/server.ts +12 -0
- package/src/slack/assistant.ts +14 -6
- package/src/slack/enrich.ts +17 -0
- package/src/slack/handlers.ts +8 -22
- package/src/slack/responses.ts +91 -1
- package/src/slack/thread-buffer.ts +6 -3
- package/src/slack/watcher.ts +9 -6
- package/src/tasks/worker-follow-up.ts +100 -3
- package/src/tests/agentmail-handlers.test.ts +32 -3
- package/src/tests/approval-requests.test.ts +61 -0
- package/src/tests/audit-user.test.ts +47 -1
- package/src/tests/codex-adapter.test.ts +46 -23
- package/src/tests/credential-broker.test.ts +5 -2
- package/src/tests/delete-page-tool.test.ts +197 -0
- package/src/tests/github-handlers.test.ts +28 -2
- package/src/tests/gitlab-handlers.test.ts +125 -0
- package/src/tests/heartbeat.test.ts +48 -3
- package/src/tests/identity.test.ts +117 -0
- package/src/tests/jira-sync.test.ts +99 -1
- package/src/tests/kapso-inbound.test.ts +7 -0
- package/src/tests/mcp-tools-user.test.ts +60 -6
- package/src/tests/model-control.test.ts +8 -2
- package/src/tests/oauth-credential-bindings.test.ts +490 -0
- package/src/tests/oauth-refresh-sweep.test.ts +160 -0
- package/src/tests/pool-affinity.test.ts +401 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -2
- package/src/tests/rbac-admission-e2e.test.ts +386 -0
- package/src/tests/rbac-admission.test.ts +492 -0
- package/src/tests/rbac-e2e-helpers.ts +3 -0
- package/src/tests/rbac-engine.test.ts +50 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
- package/src/tests/rbac-roles.test.ts +328 -0
- package/src/tests/reasoning-effort.test.ts +15 -2
- package/src/tests/schedule-target-type.test.ts +50 -0
- package/src/tests/scheduled-tasks.test.ts +40 -0
- package/src/tests/script-apis-mcp.test.ts +37 -13
- package/src/tests/script-connections-http.test.ts +1158 -0
- package/src/tests/script-connections-mcp.test.ts +894 -0
- package/src/tests/script-connections.test.ts +754 -6
- package/src/tests/scripts-external-api.test.ts +49 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
- package/src/tests/scripts-runtime.test.ts +291 -0
- package/src/tests/seed-scripts.test.ts +36 -0
- package/src/tests/slack-identity-resolution.test.ts +37 -3
- package/src/tests/slack-inline-output.test.ts +65 -0
- package/src/tests/slack-thread-buffer.test.ts +28 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/user-token-rest-auth.test.ts +140 -1
- package/src/tests/workflow-swarm-script.test.ts +44 -0
- package/src/tests/workflow-triggers-v2.test.ts +300 -1
- package/src/tools/credential-bindings/tool.ts +277 -18
- package/src/tools/delete-page.ts +145 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
- package/src/tools/request-human-input.ts +2 -0
- package/src/tools/resolve-user.ts +77 -27
- package/src/tools/script-apis.ts +62 -3
- package/src/tools/script-connections/tool.ts +275 -31
- package/src/tools/send-task.ts +15 -1
- package/src/tools/task-action.ts +34 -2
- package/src/tools/templates.ts +53 -1
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +2 -0
- package/src/tools/workflows/create-workflow.ts +8 -1
- package/src/tools/workflows/update-workflow.ts +11 -2
- package/src/types.ts +90 -11
- package/src/utils/scoped-resource.ts +25 -0
- package/src/workflows/executors/human-in-the-loop.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +7 -1
- package/src/workflows/triggers.ts +146 -18
- package/templates/skills/swarm-scripts/content.md +33 -8
- package/dist/cli-bgef578c.js +0 -771
- package/dist/cli-c9mtm09b.js +0 -43
- package/dist/cli-ceewqajw.js +0 -1698
- package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
- package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
- package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
- package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
- package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
- package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
- package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
- package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
- package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
package/src/be/db.ts
CHANGED
|
@@ -62,6 +62,7 @@ import type {
|
|
|
62
62
|
ProviderName,
|
|
63
63
|
ReasoningEffort,
|
|
64
64
|
RepoGuidelines,
|
|
65
|
+
RoutingAffinity,
|
|
65
66
|
ScheduledTask,
|
|
66
67
|
ScheduledTaskSummary,
|
|
67
68
|
ScriptRun,
|
|
@@ -108,6 +109,7 @@ import {
|
|
|
108
109
|
type ModelTier,
|
|
109
110
|
parseModelTier,
|
|
110
111
|
ReasoningEffortSchema,
|
|
112
|
+
RoutingAffinitySchema,
|
|
111
113
|
} from "../types";
|
|
112
114
|
import { deriveProviderFromKeyType } from "../utils/credentials";
|
|
113
115
|
import type { RateLimitWindowTelemetry } from "../utils/error-tracker";
|
|
@@ -1028,6 +1030,75 @@ export function updateAgentStatusFromCapacity(agentId: string): void {
|
|
|
1028
1030
|
}
|
|
1029
1031
|
}
|
|
1030
1032
|
|
|
1033
|
+
// ============================================================================
|
|
1034
|
+
// Routing Affinity (interrupted/pooled task role & capability gating)
|
|
1035
|
+
// ============================================================================
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Kill-switch for the pool eligibility gate (`isAgentEligibleForTask` and its
|
|
1039
|
+
* callers: `claimTask`, `assignUnassignedTaskPending`,
|
|
1040
|
+
* `getUnassignedTaskIdsForAgent`). ON by default. Set to `0` to restore
|
|
1041
|
+
* pre-affinity behavior verbatim — mirrors the `HEARTBEAT_PIN_*_RESUME`
|
|
1042
|
+
* rollback convention. A function (read dynamically), not a module-load-time
|
|
1043
|
+
* const, so it can be toggled mid-test (see `isGracefulResumePinEnabled` in
|
|
1044
|
+
* src/tasks/worker-follow-up.ts for the same pattern).
|
|
1045
|
+
*/
|
|
1046
|
+
export function isPoolAffinityEnforcementEnabled(): boolean {
|
|
1047
|
+
return process.env.POOL_AFFINITY_ENFORCEMENT !== "0";
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Snapshot an agent's role/harness/capabilities into a `RoutingAffinity`
|
|
1052
|
+
* blob, for stamping onto a continuation task (resume, retry) at the moment
|
|
1053
|
+
* of interruption. Returns `null` when the agent row is already gone —
|
|
1054
|
+
* callers fall back to the parent's own (inherited) `routingAffinity` via
|
|
1055
|
+
* `createTaskExtended`'s parentTaskId inheritance block.
|
|
1056
|
+
*/
|
|
1057
|
+
export function buildRoutingAffinityFromAgent(agentId: string): RoutingAffinity | null {
|
|
1058
|
+
const agent = getAgentById(agentId);
|
|
1059
|
+
if (!agent) return null;
|
|
1060
|
+
return {
|
|
1061
|
+
sourceAgentId: agent.id,
|
|
1062
|
+
role: agent.role,
|
|
1063
|
+
harnessProvider: agent.harnessProvider ?? agent.provider ?? undefined,
|
|
1064
|
+
capabilities: agent.capabilities ?? [],
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* The single eligibility gate every pool consumer (poll auto-claim,
|
|
1070
|
+
* `task-action claim`, `autoAssignPoolTasks`) MUST use before handing a task
|
|
1071
|
+
* to an agent. Exact-match on the snapshotted role string (no keyword
|
|
1072
|
+
* taxonomy in v1); `harnessProvider` is informational only and never
|
|
1073
|
+
* enforced (native session resume is deprecated). Missing role data on
|
|
1074
|
+
* either side is treated as INELIGIBLE — never fail-open to "anyone" — so a
|
|
1075
|
+
* capability-only requirement (no `role` set) can only ever be claimed by
|
|
1076
|
+
* its `sourceAgentId`, and otherwise queues until the starvation escalation
|
|
1077
|
+
* hands it to the Lead.
|
|
1078
|
+
*/
|
|
1079
|
+
export function isAgentEligibleForTask(
|
|
1080
|
+
agent: Pick<Agent, "id" | "role" | "capabilities">,
|
|
1081
|
+
task: Pick<AgentTask, "routingAffinity">,
|
|
1082
|
+
): boolean {
|
|
1083
|
+
if (!isPoolAffinityEnforcementEnabled()) return true;
|
|
1084
|
+
|
|
1085
|
+
const affinity = task.routingAffinity;
|
|
1086
|
+
if (!affinity) return true; // Untagged task — unchanged behavior.
|
|
1087
|
+
|
|
1088
|
+
if (affinity.sourceAgentId && affinity.sourceAgentId === agent.id) return true; // Own work.
|
|
1089
|
+
|
|
1090
|
+
if (!agent.role || !affinity.role) return false; // Missing role data — no fail-open.
|
|
1091
|
+
if (agent.role !== affinity.role) return false;
|
|
1092
|
+
|
|
1093
|
+
const requiredCapabilities = affinity.capabilities ?? [];
|
|
1094
|
+
if (requiredCapabilities.length > 0) {
|
|
1095
|
+
const agentCapabilities = new Set(agent.capabilities ?? []);
|
|
1096
|
+
if (!requiredCapabilities.every((cap) => agentCapabilities.has(cap))) return false;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1031
1102
|
// ============================================================================
|
|
1032
1103
|
// AgentTask Queries
|
|
1033
1104
|
// ============================================================================
|
|
@@ -1100,6 +1171,7 @@ type AgentTaskRow = {
|
|
|
1100
1171
|
harnessVariant: string | null;
|
|
1101
1172
|
harnessVariantMeta: string | null;
|
|
1102
1173
|
totalCostUsd?: number | null;
|
|
1174
|
+
routingAffinity: string | null;
|
|
1103
1175
|
};
|
|
1104
1176
|
|
|
1105
1177
|
function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
@@ -1124,6 +1196,26 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
1124
1196
|
}
|
|
1125
1197
|
}
|
|
1126
1198
|
|
|
1199
|
+
let routingAffinity: RoutingAffinity | undefined;
|
|
1200
|
+
if (row.routingAffinity) {
|
|
1201
|
+
try {
|
|
1202
|
+
const parsed = RoutingAffinitySchema.safeParse(JSON.parse(row.routingAffinity));
|
|
1203
|
+
if (parsed.success) {
|
|
1204
|
+
routingAffinity = parsed.data;
|
|
1205
|
+
} else {
|
|
1206
|
+
console.warn(
|
|
1207
|
+
`[db] Ignoring invalid agent_tasks.routingAffinity for task ${row.id}:`,
|
|
1208
|
+
parsed.error.message,
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
console.warn(
|
|
1213
|
+
`[db] Ignoring malformed agent_tasks.routingAffinity for task ${row.id}:`,
|
|
1214
|
+
error instanceof Error ? error.message : String(error),
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1127
1219
|
return {
|
|
1128
1220
|
id: row.id,
|
|
1129
1221
|
agentId: row.agentId,
|
|
@@ -1194,6 +1286,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
1194
1286
|
harnessVariant: row.harnessVariant ?? undefined,
|
|
1195
1287
|
harnessVariantMeta: row.harnessVariantMeta ? JSON.parse(row.harnessVariantMeta) : undefined,
|
|
1196
1288
|
totalCostUsd: row.totalCostUsd ?? undefined,
|
|
1289
|
+
routingAffinity,
|
|
1197
1290
|
};
|
|
1198
1291
|
}
|
|
1199
1292
|
|
|
@@ -1359,6 +1452,29 @@ export function getPendingTaskForAgent(agentId: string): AgentTask | null {
|
|
|
1359
1452
|
}
|
|
1360
1453
|
|
|
1361
1454
|
export function assignUnassignedTaskPending(taskId: string, agentId: string): AgentTask | null {
|
|
1455
|
+
// Eligibility pre-check (routing affinity) — defense in depth for the
|
|
1456
|
+
// heartbeat's `autoAssignPoolTasks`, which already filters candidates via
|
|
1457
|
+
// `isAgentEligibleForTask` before calling this, but any other caller gets
|
|
1458
|
+
// the same guard for free.
|
|
1459
|
+
if (isPoolAffinityEnforcementEnabled()) {
|
|
1460
|
+
const task = getTaskById(taskId);
|
|
1461
|
+
const agent = getAgentById(agentId);
|
|
1462
|
+
if (task && agent && !isAgentEligibleForTask(agent, task)) {
|
|
1463
|
+
try {
|
|
1464
|
+
createLogEntry({
|
|
1465
|
+
eventType: "task_claim_rejected_affinity",
|
|
1466
|
+
agentId,
|
|
1467
|
+
taskId,
|
|
1468
|
+
metadata: {
|
|
1469
|
+
agentRole: agent.role ?? null,
|
|
1470
|
+
requiredRole: task.routingAffinity?.role ?? null,
|
|
1471
|
+
},
|
|
1472
|
+
});
|
|
1473
|
+
} catch {}
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1362
1478
|
const now = new Date().toISOString();
|
|
1363
1479
|
const row = getDb()
|
|
1364
1480
|
.prepare<AgentTaskRow, [string, string, string]>(
|
|
@@ -1636,8 +1752,12 @@ export interface TaskFilters {
|
|
|
1636
1752
|
source?: AgentTaskSource[];
|
|
1637
1753
|
/** ISO 8601 timestamp; only return tasks where createdAt >= this. */
|
|
1638
1754
|
createdAfter?: string;
|
|
1755
|
+
/** ISO 8601 timestamp; only return tasks where createdAt < this. */
|
|
1756
|
+
createdBefore?: string;
|
|
1639
1757
|
/** Only return tasks requested by this canonical user. NULL rows are excluded. */
|
|
1640
1758
|
requestedByUserId?: string;
|
|
1759
|
+
/** Sort list rows for either table freshness or timeline paging. */
|
|
1760
|
+
orderBy?: "lastUpdatedAt" | "createdAt";
|
|
1641
1761
|
limit?: number;
|
|
1642
1762
|
offset?: number;
|
|
1643
1763
|
includeHeartbeat?: boolean;
|
|
@@ -1721,6 +1841,11 @@ export function getAllTasks(
|
|
|
1721
1841
|
params.push(filters.createdAfter);
|
|
1722
1842
|
}
|
|
1723
1843
|
|
|
1844
|
+
if (filters?.createdBefore) {
|
|
1845
|
+
conditions.push("createdAt < ?");
|
|
1846
|
+
params.push(filters.createdBefore);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1724
1849
|
if (filters?.requestedByUserId) {
|
|
1725
1850
|
conditions.push("requestedByUserId = ?");
|
|
1726
1851
|
params.push(filters.requestedByUserId);
|
|
@@ -1739,10 +1864,14 @@ export function getAllTasks(
|
|
|
1739
1864
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1740
1865
|
const limit = filters?.limit ?? 25;
|
|
1741
1866
|
const offset = filters?.offset ?? 0;
|
|
1867
|
+
const orderBy =
|
|
1868
|
+
filters?.orderBy === "createdAt"
|
|
1869
|
+
? "createdAt DESC, rowid DESC"
|
|
1870
|
+
: "lastUpdatedAt DESC, priority DESC";
|
|
1742
1871
|
const query = `SELECT agent_tasks.*,
|
|
1743
1872
|
(SELECT SUM(totalCostUsd) FROM session_costs WHERE session_costs.taskId = agent_tasks.id) AS totalCostUsd
|
|
1744
1873
|
FROM agent_tasks ${whereClause}
|
|
1745
|
-
ORDER BY
|
|
1874
|
+
ORDER BY ${orderBy} LIMIT ${limit} OFFSET ${offset}`;
|
|
1746
1875
|
|
|
1747
1876
|
const rows = getDb()
|
|
1748
1877
|
.prepare<AgentTaskRow, (string | AgentTaskStatus)[]>(query)
|
|
@@ -1838,6 +1967,16 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
|
|
|
1838
1967
|
params.push(filters.createdAfter);
|
|
1839
1968
|
}
|
|
1840
1969
|
|
|
1970
|
+
if (filters?.createdBefore) {
|
|
1971
|
+
conditions.push("createdAt < ?");
|
|
1972
|
+
params.push(filters.createdBefore);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
if (filters?.requestedByUserId) {
|
|
1976
|
+
conditions.push("requestedByUserId = ?");
|
|
1977
|
+
params.push(filters.requestedByUserId);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1841
1980
|
// Exclude system/heartbeat tasks by default. The flag is still called
|
|
1842
1981
|
// `includeHeartbeat` for backward compat with existing API callers, but we
|
|
1843
1982
|
// also gate boot-triage + heartbeat-checklist behind it since those are
|
|
@@ -3243,6 +3382,12 @@ export interface CreateTaskOptions {
|
|
|
3243
3382
|
followUpConfig?: FollowUpConfig;
|
|
3244
3383
|
requestedByUserId?: string;
|
|
3245
3384
|
contextKey?: string;
|
|
3385
|
+
/**
|
|
3386
|
+
* Routing-affinity snapshot gating pool eligibility (see
|
|
3387
|
+
* `isAgentEligibleForTask`). Inherited from the parent (via `parentTaskId`)
|
|
3388
|
+
* when not explicitly set — same treatment as `vcsRepo`/`contextKey`.
|
|
3389
|
+
*/
|
|
3390
|
+
routingAffinity?: RoutingAffinity;
|
|
3246
3391
|
}
|
|
3247
3392
|
|
|
3248
3393
|
/**
|
|
@@ -3414,6 +3559,9 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
3414
3559
|
if (parent.followUpConfig && !options.followUpConfig) {
|
|
3415
3560
|
options.followUpConfig = parent.followUpConfig;
|
|
3416
3561
|
}
|
|
3562
|
+
if (parent.routingAffinity && !options.routingAffinity) {
|
|
3563
|
+
options.routingAffinity = parent.routingAffinity;
|
|
3564
|
+
}
|
|
3417
3565
|
}
|
|
3418
3566
|
}
|
|
3419
3567
|
|
|
@@ -3448,8 +3596,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
3448
3596
|
vcsInstallationId, vcsNodeId,
|
|
3449
3597
|
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
3450
3598
|
mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, effort, scheduleId,
|
|
3451
|
-
workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, contextKey, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
|
|
3452
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
3599
|
+
workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
|
|
3600
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
3453
3601
|
)
|
|
3454
3602
|
.get(
|
|
3455
3603
|
id,
|
|
@@ -3493,6 +3641,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
3493
3641
|
options?.followUpConfig ? JSON.stringify(options.followUpConfig) : null,
|
|
3494
3642
|
options?.requestedByUserId ?? null,
|
|
3495
3643
|
options?.contextKey ?? null,
|
|
3644
|
+
options?.routingAffinity ? JSON.stringify(options.routingAffinity) : null,
|
|
3496
3645
|
pkg.version,
|
|
3497
3646
|
now,
|
|
3498
3647
|
now,
|
|
@@ -3542,6 +3691,28 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
3542
3691
|
}
|
|
3543
3692
|
|
|
3544
3693
|
export function claimTask(taskId: string, agentId: string): AgentTask | null {
|
|
3694
|
+
// Eligibility pre-check (routing affinity): static per (agent, task), so
|
|
3695
|
+
// pre-filtering here does NOT reopen the claim race — the atomic UPDATE
|
|
3696
|
+
// below still arbitrates concurrent claims by eligible agents.
|
|
3697
|
+
if (isPoolAffinityEnforcementEnabled()) {
|
|
3698
|
+
const task = getTaskById(taskId);
|
|
3699
|
+
const agent = getAgentById(agentId);
|
|
3700
|
+
if (task && agent && !isAgentEligibleForTask(agent, task)) {
|
|
3701
|
+
try {
|
|
3702
|
+
createLogEntry({
|
|
3703
|
+
eventType: "task_claim_rejected_affinity",
|
|
3704
|
+
agentId,
|
|
3705
|
+
taskId,
|
|
3706
|
+
metadata: {
|
|
3707
|
+
agentRole: agent.role ?? null,
|
|
3708
|
+
requiredRole: task.routingAffinity?.role ?? null,
|
|
3709
|
+
},
|
|
3710
|
+
});
|
|
3711
|
+
} catch {}
|
|
3712
|
+
return null;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3545
3716
|
// Atomic claim: single UPDATE with WHERE guard ensures exactly-once claiming.
|
|
3546
3717
|
// No pre-read needed — the WHERE clause handles the race condition.
|
|
3547
3718
|
// Status goes directly to 'in_progress' because the claiming session is
|
|
@@ -3806,6 +3977,61 @@ export function getUnassignedTaskIds(limit = 10): string[] {
|
|
|
3806
3977
|
return rows.map((r) => r.id);
|
|
3807
3978
|
}
|
|
3808
3979
|
|
|
3980
|
+
/**
|
|
3981
|
+
* Batch size and hard cap for the paginated eligibility scans in
|
|
3982
|
+
* `getUnassignedTaskIdsForAgent` (and `autoAssignPoolTasks` in
|
|
3983
|
+
* src/heartbeat/heartbeat.ts, which mirrors this pattern). A fixed single
|
|
3984
|
+
* window used to mean N ineligible affinity-tagged tasks at the head of the
|
|
3985
|
+
* priority order could hide all eligible work behind them forever — see
|
|
3986
|
+
* PR #954 review. Scanning continues page-by-page until `limit` eligible
|
|
3987
|
+
* candidates are found or the pool is exhausted; the cap bounds worst-case
|
|
3988
|
+
* DB load when eligible work is buried deep or genuinely absent.
|
|
3989
|
+
*/
|
|
3990
|
+
const ELIGIBILITY_SCAN_BATCH_SIZE = Number(process.env.ELIGIBILITY_SCAN_BATCH_SIZE) || 25;
|
|
3991
|
+
const ELIGIBILITY_SCAN_CAP = Number(process.env.ELIGIBILITY_SCAN_CAP) || 500;
|
|
3992
|
+
|
|
3993
|
+
/**
|
|
3994
|
+
* Same ordering as `getUnassignedTaskIds`, filtered through
|
|
3995
|
+
* `isAgentEligibleForTask` for the requesting agent. Used by the poll
|
|
3996
|
+
* auto-claim path so an ineligible candidate is never even offered to the
|
|
3997
|
+
* budget gate / claim loop. Paginates through the unassigned pool in
|
|
3998
|
+
* `ELIGIBILITY_SCAN_BATCH_SIZE`-row windows (filtering in JS to avoid
|
|
3999
|
+
* JSON-parsing `routingAffinity` in SQL) until `limit` eligible tasks are
|
|
4000
|
+
* found or the pool is exhausted, capped at `ELIGIBILITY_SCAN_CAP` rows
|
|
4001
|
+
* scanned so a pool full of ineligible tasks can't turn every poll into an
|
|
4002
|
+
* unbounded scan.
|
|
4003
|
+
*/
|
|
4004
|
+
export function getUnassignedTaskIdsForAgent(agentId: string, limit = 10): string[] {
|
|
4005
|
+
const agent = getAgentById(agentId);
|
|
4006
|
+
if (!agent) return [];
|
|
4007
|
+
|
|
4008
|
+
const batchSize = Math.max(limit * 5, ELIGIBILITY_SCAN_BATCH_SIZE);
|
|
4009
|
+
const eligible: string[] = [];
|
|
4010
|
+
let offset = 0;
|
|
4011
|
+
|
|
4012
|
+
while (eligible.length < limit && offset < ELIGIBILITY_SCAN_CAP) {
|
|
4013
|
+
const rows = getDb()
|
|
4014
|
+
.prepare<AgentTaskRow, [number, number]>(
|
|
4015
|
+
"SELECT * FROM agent_tasks WHERE status = 'unassigned' ORDER BY priority DESC, createdAt ASC, rowid ASC LIMIT ? OFFSET ?",
|
|
4016
|
+
)
|
|
4017
|
+
.all(batchSize, offset);
|
|
4018
|
+
if (rows.length === 0) break;
|
|
4019
|
+
|
|
4020
|
+
for (const row of rows) {
|
|
4021
|
+
const task = rowToAgentTask(row);
|
|
4022
|
+
if (isAgentEligibleForTask(agent, task)) {
|
|
4023
|
+
eligible.push(task.id);
|
|
4024
|
+
if (eligible.length >= limit) break;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
offset += rows.length;
|
|
4029
|
+
if (rows.length < batchSize) break; // Exhausted the pool.
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
return eligible;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
3809
4035
|
// ============================================================================
|
|
3810
4036
|
// Dependency Checking
|
|
3811
4037
|
// ============================================================================
|
|
@@ -6936,16 +7162,22 @@ export function getStalledInProgressTasks(thresholdMinutes: number = 30): AgentT
|
|
|
6936
7162
|
}
|
|
6937
7163
|
|
|
6938
7164
|
/**
|
|
6939
|
-
* Genuine same-agent protected
|
|
6940
|
-
* `graceful-shutdown-pin
|
|
6941
|
-
*
|
|
7165
|
+
* Genuine same-agent protected pins — resumes tagged `crash-recovery-pin` /
|
|
7166
|
+
* `graceful-shutdown-pin`, OR a reboot-retry child tagged `reboot-retry-pin`
|
|
7167
|
+
* (routing-affinity Phase 3) — that are still `pending` `graceMin` minutes
|
|
7168
|
+
* after creation. The heartbeat reaper escalates these to a Lead
|
|
7169
|
+
* reroute-decision.
|
|
6942
7170
|
*
|
|
6943
|
-
*
|
|
6944
|
-
* -
|
|
6945
|
-
*
|
|
6946
|
-
*
|
|
6947
|
-
*
|
|
6948
|
-
*
|
|
7171
|
+
* Scoping clauses, each load-bearing:
|
|
7172
|
+
* - `taskType = 'resume' AND (crash/graceful pin tags)` OR `reboot-retry-pin`
|
|
7173
|
+
* tag alone — restricts to work actually pinned to its original agent on a
|
|
7174
|
+
* protected path. A reboot-retry-pin task is a FRESH task (`taskType`
|
|
7175
|
+
* mirrors the original work, not `'resume'`), so it needs its own
|
|
7176
|
+
* disjunct rather than reusing the `taskType = 'resume'` gate. Without
|
|
7177
|
+
* this, a *pooled* resume that `autoAssignPoolTasks` flips to `pending`
|
|
7178
|
+
* earlier in the SAME sweep (keeping its old `createdAt`) would be reaped
|
|
7179
|
+
* and cancelled before the assigned worker polls; it also keeps
|
|
7180
|
+
* `context_limits` / `manual_supersede` pins from being escalated under
|
|
6949
7181
|
* the protected-pin label. (Literals must match the pin tag constants in
|
|
6950
7182
|
* src/tasks/worker-follow-up.ts.)
|
|
6951
7183
|
* - `status = 'pending'` — the "currently unreclaimed" discriminator: when the
|
|
@@ -6966,8 +7198,11 @@ export function getStalePinnedResumes(graceMin: number): AgentTask[] {
|
|
|
6966
7198
|
return getDb()
|
|
6967
7199
|
.prepare<AgentTaskRow, [string]>(
|
|
6968
7200
|
`SELECT * FROM agent_tasks
|
|
6969
|
-
WHERE
|
|
6970
|
-
AND (
|
|
7201
|
+
WHERE status = 'pending'
|
|
7202
|
+
AND (
|
|
7203
|
+
(taskType = 'resume' AND (tags LIKE '%"crash-recovery-pin"%' OR tags LIKE '%"graceful-shutdown-pin"%'))
|
|
7204
|
+
OR tags LIKE '%"reboot-retry-pin"%'
|
|
7205
|
+
)
|
|
6971
7206
|
AND createdAt < ?
|
|
6972
7207
|
ORDER BY createdAt ASC`,
|
|
6973
7208
|
)
|
|
@@ -7037,18 +7272,39 @@ export function getIdleWorkersWithCapacity(): Agent[] {
|
|
|
7037
7272
|
}
|
|
7038
7273
|
|
|
7039
7274
|
/**
|
|
7040
|
-
* Get unassigned pool tasks ordered by priority (DESC)
|
|
7041
|
-
*
|
|
7275
|
+
* Get unassigned pool tasks ordered by priority (DESC), creation time (ASC),
|
|
7276
|
+
* then `rowid` (ASC) as a stable tiebreaker. The `rowid` tiebreaker matters
|
|
7277
|
+
* once `offset` is used for pagination (`autoAssignPoolTasks` in
|
|
7278
|
+
* src/heartbeat/heartbeat.ts) — without it, rows sharing a `createdAt` could
|
|
7279
|
+
* be skipped or repeated across pages. Used by the heartbeat for
|
|
7280
|
+
* auto-assignment and status reporting.
|
|
7042
7281
|
*/
|
|
7043
|
-
export function getUnassignedPoolTasks(limit: number = 10): AgentTask[] {
|
|
7282
|
+
export function getUnassignedPoolTasks(limit: number = 10, offset: number = 0): AgentTask[] {
|
|
7044
7283
|
return getDb()
|
|
7045
|
-
.prepare<AgentTaskRow, [number]>(
|
|
7284
|
+
.prepare<AgentTaskRow, [number, number]>(
|
|
7046
7285
|
`SELECT * FROM agent_tasks
|
|
7047
7286
|
WHERE status = 'unassigned'
|
|
7048
|
-
ORDER BY priority DESC, createdAt ASC
|
|
7049
|
-
LIMIT ?`,
|
|
7287
|
+
ORDER BY priority DESC, createdAt ASC, rowid ASC
|
|
7288
|
+
LIMIT ? OFFSET ?`,
|
|
7050
7289
|
)
|
|
7051
|
-
.all(limit)
|
|
7290
|
+
.all(limit, offset)
|
|
7291
|
+
.map(rowToAgentTask);
|
|
7292
|
+
}
|
|
7293
|
+
|
|
7294
|
+
/**
|
|
7295
|
+
* Affinity-tagged pool tasks that have sat `unassigned` past `cutoffIso` —
|
|
7296
|
+
* the starvation-escalation candidate set (routing-affinity Phase 3). Callers
|
|
7297
|
+
* MUST separately confirm zero registered agents satisfy
|
|
7298
|
+
* `isAgentEligibleForTask` before escalating; this only narrows by tag age.
|
|
7299
|
+
*/
|
|
7300
|
+
export function getStaleUnassignedAffinityTasks(cutoffIso: string): AgentTask[] {
|
|
7301
|
+
return getDb()
|
|
7302
|
+
.prepare<AgentTaskRow, [string]>(
|
|
7303
|
+
`SELECT * FROM agent_tasks
|
|
7304
|
+
WHERE status = 'unassigned' AND routingAffinity IS NOT NULL AND createdAt < ?
|
|
7305
|
+
ORDER BY createdAt ASC`,
|
|
7306
|
+
)
|
|
7307
|
+
.all(cutoffIso)
|
|
7052
7308
|
.map(rowToAgentTask);
|
|
7053
7309
|
}
|
|
7054
7310
|
|
|
@@ -8081,8 +8337,12 @@ export function updatePage(
|
|
|
8081
8337
|
}
|
|
8082
8338
|
|
|
8083
8339
|
export function deletePage(id: string): boolean {
|
|
8084
|
-
|
|
8085
|
-
|
|
8340
|
+
const result = getDb().transaction(() => {
|
|
8341
|
+
getDb().run("DELETE FROM user_favorites WHERE itemType = 'page' AND itemId = ?", [id]);
|
|
8342
|
+
getDb().run("DELETE FROM kv_entries WHERE namespace = ?", [`task:page:${id}`]);
|
|
8343
|
+
// ON DELETE CASCADE on page_versions.pageId handles history cleanup.
|
|
8344
|
+
return getDb().run("DELETE FROM pages WHERE id = ?", [id]);
|
|
8345
|
+
})();
|
|
8086
8346
|
return result.changes > 0;
|
|
8087
8347
|
}
|
|
8088
8348
|
|
|
@@ -8894,6 +9154,7 @@ export interface ApprovalRequest {
|
|
|
8894
9154
|
timeoutSeconds: number | null;
|
|
8895
9155
|
expiresAt: string | null;
|
|
8896
9156
|
notificationChannels: unknown[] | null;
|
|
9157
|
+
createdBy?: string;
|
|
8897
9158
|
createdAt: string;
|
|
8898
9159
|
updatedAt: string;
|
|
8899
9160
|
}
|
|
@@ -8913,6 +9174,7 @@ interface ApprovalRequestRow {
|
|
|
8913
9174
|
timeoutSeconds: number | null;
|
|
8914
9175
|
expiresAt: string | null;
|
|
8915
9176
|
notificationChannels: string | null;
|
|
9177
|
+
created_by: string | null;
|
|
8916
9178
|
createdAt: string;
|
|
8917
9179
|
updatedAt: string;
|
|
8918
9180
|
}
|
|
@@ -8933,6 +9195,7 @@ function rowToApprovalRequest(row: ApprovalRequestRow): ApprovalRequest {
|
|
|
8933
9195
|
timeoutSeconds: row.timeoutSeconds,
|
|
8934
9196
|
expiresAt: normalizeDate(row.expiresAt),
|
|
8935
9197
|
notificationChannels: row.notificationChannels ? JSON.parse(row.notificationChannels) : null,
|
|
9198
|
+
createdBy: row.created_by ?? undefined,
|
|
8936
9199
|
createdAt: normalizeDateRequired(row.createdAt),
|
|
8937
9200
|
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
8938
9201
|
};
|
|
@@ -8948,6 +9211,7 @@ export function createApprovalRequest(data: {
|
|
|
8948
9211
|
sourceTaskId?: string;
|
|
8949
9212
|
timeoutSeconds?: number;
|
|
8950
9213
|
notificationChannels?: unknown[];
|
|
9214
|
+
createdBy?: string;
|
|
8951
9215
|
}): ApprovalRequest {
|
|
8952
9216
|
const now = new Date().toISOString();
|
|
8953
9217
|
const expiresAt = data.timeoutSeconds
|
|
@@ -8968,12 +9232,13 @@ export function createApprovalRequest(data: {
|
|
|
8968
9232
|
number | null,
|
|
8969
9233
|
string | null,
|
|
8970
9234
|
string | null,
|
|
9235
|
+
string | null,
|
|
8971
9236
|
string,
|
|
8972
9237
|
string,
|
|
8973
9238
|
]
|
|
8974
9239
|
>(
|
|
8975
|
-
`INSERT INTO approval_requests (id, title, questions, workflowRunId, workflowRunStepId, sourceTaskId, approvers, timeoutSeconds, expiresAt, notificationChannels, createdAt, updatedAt)
|
|
8976
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9240
|
+
`INSERT INTO approval_requests (id, title, questions, workflowRunId, workflowRunStepId, sourceTaskId, approvers, timeoutSeconds, expiresAt, notificationChannels, created_by, createdAt, updatedAt)
|
|
9241
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8977
9242
|
RETURNING *`,
|
|
8978
9243
|
)
|
|
8979
9244
|
.get(
|
|
@@ -8987,6 +9252,7 @@ export function createApprovalRequest(data: {
|
|
|
8987
9252
|
data.timeoutSeconds ?? null,
|
|
8988
9253
|
expiresAt,
|
|
8989
9254
|
data.notificationChannels ? JSON.stringify(data.notificationChannels) : null,
|
|
9255
|
+
data.createdBy ?? null,
|
|
8990
9256
|
now,
|
|
8991
9257
|
now,
|
|
8992
9258
|
);
|
|
@@ -10288,9 +10554,29 @@ export function updateMcpServer(
|
|
|
10288
10554
|
return row ? rowToMcpServer(row) : null;
|
|
10289
10555
|
}
|
|
10290
10556
|
|
|
10291
|
-
export
|
|
10292
|
-
|
|
10293
|
-
|
|
10557
|
+
export type DeleteMcpServerResult = {
|
|
10558
|
+
deleted: boolean;
|
|
10559
|
+
deletedScriptConnectionCount: number;
|
|
10560
|
+
};
|
|
10561
|
+
|
|
10562
|
+
export function deleteMcpServer(id: string): DeleteMcpServerResult {
|
|
10563
|
+
const db = getDb();
|
|
10564
|
+
const existing = db
|
|
10565
|
+
.prepare<{ id: string }, [string]>("SELECT id FROM mcp_servers WHERE id = ?")
|
|
10566
|
+
.get(id);
|
|
10567
|
+
if (!existing) return { deleted: false, deletedScriptConnectionCount: 0 };
|
|
10568
|
+
|
|
10569
|
+
const tx = db.transaction(() => {
|
|
10570
|
+
const deletedConnections = db
|
|
10571
|
+
.prepare("DELETE FROM script_connections WHERE mcp_server_id = ?")
|
|
10572
|
+
.run(id);
|
|
10573
|
+
const deletedServer = db.prepare("DELETE FROM mcp_servers WHERE id = ?").run(id);
|
|
10574
|
+
return {
|
|
10575
|
+
deleted: deletedServer.changes > 0,
|
|
10576
|
+
deletedScriptConnectionCount: deletedConnections.changes,
|
|
10577
|
+
};
|
|
10578
|
+
});
|
|
10579
|
+
return tx();
|
|
10294
10580
|
}
|
|
10295
10581
|
|
|
10296
10582
|
export function getMcpServerById(id: string): McpServer | null {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework's single identity-resolution primitive.
|
|
3
|
+
*
|
|
4
|
+
* Invariant (Rule 33 / provenance-or-silence): the swarm's sole responsibility
|
|
5
|
+
* for identity is the reverse lookup `(kind, externalId)` (or email) → linked
|
|
6
|
+
* user → canonical name, via `findUserByExternalId` / `findUserByEmail`. If
|
|
7
|
+
* the lookup points nowhere, callers render the explicit UNKNOWN sentinel —
|
|
8
|
+
* NEVER a provider display name, NEVER a guess. Every provider adapter that
|
|
9
|
+
* renders a human identity into agent-visible text MUST go through
|
|
10
|
+
* `renderIdentity(resolveIdentity(...))` (or `resolveIdentityByEmail`).
|
|
11
|
+
*
|
|
12
|
+
* Pure DB reads — zero provider API calls. `unknown` is a value, not an
|
|
13
|
+
* error: callers branch on `status`, never throw or fall back to a
|
|
14
|
+
* provider-supplied label.
|
|
15
|
+
*
|
|
16
|
+
* This module is API-side ONLY (see the DB-boundary note in `./users.ts`).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { findUserByEmail, findUserByExternalId } from "./users";
|
|
20
|
+
|
|
21
|
+
export type IdentityResolution =
|
|
22
|
+
| {
|
|
23
|
+
status: "resolved";
|
|
24
|
+
kind: string;
|
|
25
|
+
externalId: string;
|
|
26
|
+
userId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
email?: string;
|
|
29
|
+
}
|
|
30
|
+
| { status: "unknown"; kind: string; externalId: string };
|
|
31
|
+
|
|
32
|
+
/** Reverse lookup by `(kind, externalId)` — e.g. `resolveIdentity('slack', 'U016H7XKZGS')`. */
|
|
33
|
+
export function resolveIdentity(kind: string, externalId: string): IdentityResolution {
|
|
34
|
+
const user = findUserByExternalId(kind, externalId);
|
|
35
|
+
if (!user) {
|
|
36
|
+
return { status: "unknown", kind, externalId };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
status: "resolved",
|
|
40
|
+
kind,
|
|
41
|
+
externalId,
|
|
42
|
+
userId: user.id,
|
|
43
|
+
name: user.name,
|
|
44
|
+
email: user.email,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reverse lookup by email (primary or alias). Email is a `users.email`
|
|
50
|
+
* attribute, not an external-id kind — rendered with `kind: "email"` so
|
|
51
|
+
* `renderIdentity` produces a consistent pair form regardless of provider.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveIdentityByEmail(email: string): IdentityResolution {
|
|
54
|
+
const user = findUserByEmail(email);
|
|
55
|
+
if (!user) {
|
|
56
|
+
return { status: "unknown", kind: "email", externalId: email };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
status: "resolved",
|
|
60
|
+
kind: "email",
|
|
61
|
+
externalId: email,
|
|
62
|
+
userId: user.id,
|
|
63
|
+
name: user.name,
|
|
64
|
+
email: user.email,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render an `IdentityResolution` into agent-visible text — the only two
|
|
70
|
+
* shapes an identity may ever take. The sentinel keeps the raw id but never
|
|
71
|
+
* a name: an unresolved identity must never be confused for a real one.
|
|
72
|
+
*/
|
|
73
|
+
export function renderIdentity(resolution: IdentityResolution): string {
|
|
74
|
+
const pair = `${resolution.kind}:${resolution.externalId}`;
|
|
75
|
+
if (resolution.status === "resolved") {
|
|
76
|
+
return `${resolution.name} (${pair})`;
|
|
77
|
+
}
|
|
78
|
+
return `${pair} (unknown user)`;
|
|
79
|
+
}
|