@desplega.ai/agent-swarm 1.131.0 → 1.133.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/README.md +2 -2
- package/dist/{actions-w53mbns9.js → actions-cskpayd7.js} +11 -9
- package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
- package/dist/{app-m9gwqsqv.js → app-ang77nbb.js} +8 -6
- package/dist/{assistant-bp65m6f3.js → assistant-b4exebdw.js} +16 -13
- package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
- package/dist/{boot-reembed-62p0at2k.js → boot-reembed-ps2aftpb.js} +9 -7
- package/dist/{boot-reembed-xmfp6wdh.js → boot-reembed-znyk3nxb.js} +10 -8
- package/dist/{boot-scrub-logs-rr8q2dms.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
- package/dist/{claude-adapter-2y47mrkd.js → claude-adapter-3ct8kgxw.js} +13 -9
- package/dist/{claude-managed-adapter-gv19tdew.js → claude-managed-adapter-akx4ntf2.js} +2 -2
- package/dist/{cli-t1xze5qh.js → cli-13w2hwkh.js} +1 -1
- package/dist/{cli-efxfnq6q.js → cli-1532szb1.js} +8 -6
- package/dist/{cli-d2tfkcga.js → cli-23d3rseb.js} +33 -17
- package/dist/{cli-nm5gtp1x.js → cli-2a75z9c5.js} +196 -7
- package/dist/{cli-fywms69x.js → cli-2t30rj0d.js} +1 -1
- package/dist/{cli-cvgjwnh1.js → cli-3c3tdgab.js} +10 -10
- package/dist/{cli-pm6xbh84.js → cli-3zzxa9jz.js} +46 -16
- package/dist/{cli-cdeg08wx.js → cli-4k6233gc.js} +1 -1
- package/dist/{cli-t55yw3a1.js → cli-5p2tz05r.js} +2 -2
- package/dist/{cli-0rcyq6d9.js → cli-5z2yhzvd.js} +125 -47
- package/dist/{cli-3kf51j15.js → cli-6zfq31nj.js} +9 -3
- package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
- package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
- package/dist/{cli-72q8m8gk.js → cli-9nfc2es8.js} +6 -6
- package/dist/{cli-5ze9rv97.js → cli-9ta3pwms.js} +11 -4
- package/dist/{cli-78xwvkhj.js → cli-avr0ns7z.js} +2 -2
- package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
- package/dist/{cli-2q72qz2s.js → cli-dgk20ft7.js} +15 -6
- package/dist/cli-et1bbwqf.js +13 -0
- package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
- package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
- package/dist/{cli-e7ckcg4v.js → cli-ftwpv451.js} +170 -387
- package/dist/{cli-g6b8x1h8.js → cli-g854s2dw.js} +1 -1
- package/dist/{cli-5kx36evn.js → cli-ggmdxeqa.js} +13 -8
- package/dist/cli-hkwfe3qj.js +530 -0
- package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
- package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
- package/dist/{cli-5v4rw6yd.js → cli-pvzqhbs9.js} +2 -2
- package/dist/cli-pzq5rjym.js +15 -0
- package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
- package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
- package/dist/cli-qys3rhhz.js +27 -0
- package/dist/{cli-rzbpmxdr.js → cli-r6tpm37h.js} +2 -2
- package/dist/{cli-qv61hp37.js → cli-rch80c76.js} +2 -2
- package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
- package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
- package/dist/{cli-psh3c4va.js → cli-sfd9v7sj.js} +1 -1
- package/dist/{cli-70xqj0z3.js → cli-sh3ytsyj.js} +49 -39
- package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
- package/dist/{cli-d3x3x1c9.js → cli-xdb4mpnp.js} +2 -2
- package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
- package/dist/{cli-yw8vghx2.js → cli-xv8t8p3c.js} +65 -32
- package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
- package/dist/{cli-t76vw5db.js → cli-y08jvxzx.js} +601 -140
- package/dist/{cli-a4jzdpa3.js → cli-zfbf3srj.js} +650 -245
- package/dist/{cli-a40rnf71.js → cli-zmq89k34.js} +6 -6
- package/dist/{cli-qmdnhpej.js → cli-zr9q011v.js} +23 -13
- package/dist/cli.js +22 -22
- package/dist/{codex-adapter-d24q75wk.js → codex-adapter-bef93pq4.js} +9 -7
- package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
- package/dist/{codex-session-runner-g50vcsex.js → codex-session-runner-5pkxys7x.js} +9 -7
- package/dist/{commands-c3nrc5hg.js → commands-e23s1bff.js} +9 -7
- package/dist/{db-nt7fg3tw.js → db-m3nabatn.js} +17 -5
- package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
- package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
- package/dist/{handlers-fkr6swat.js → handlers-akazedvr.js} +16 -13
- package/dist/{hook-77wpvnjc.js → hook-ze4cpf0v.js} +55 -41
- package/dist/{hook-g019yehy.js → hook-zfqyyenw.js} +10 -7
- package/dist/{http-jewj0syk.js → http-sja0n6kw.js} +730 -150
- package/dist/index-0gr8vd6k.js +39 -0
- package/dist/{index-7n2xpbd4.js → index-354vkhdv.js} +154 -74
- package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
- package/dist/{index-hfvggd0z.js → index-95t3cqq5.js} +16 -14
- package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
- package/dist/{index-6gm5he8h.js → index-vpn36mma.js} +15 -13
- package/dist/{keepalive-j0fjvqfd.js → keepalive-3sp3meqj.js} +16 -10
- package/dist/{lead-1n8x30xe.js → lead-mefeewkf.js} +39 -36
- package/dist/{maintenance-gvev11sa.js → maintenance-8tbrbjfg.js} +10 -8
- package/dist/mistral-conversations-00zrp7g4.js +717 -0
- package/dist/{oauth-refresh-sweep-s9ecsvh0.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
- package/dist/{onboard-pvm5b38w.js → onboard-mt2tgyjg.js} +7 -7
- package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
- package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
- package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
- package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
- package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
- package/dist/{otel-impl-p6jvgd9c.js → otel-impl-z6pxt3j9.js} +224 -9
- package/dist/{pi-mono-adapter-t091s27g.js → pi-mono-adapter-btvqmj58.js} +1138 -408
- package/dist/{pricing-refresh-k49rcyed.js → pricing-refresh-kyq8cezh.js} +9 -7
- package/dist/{rbac-roles-cr80g96t.js → rbac-roles-3ryfrfna.js} +8 -6
- package/dist/{rbac-roles-30pspvy7.js → rbac-roles-6qfvbee1.js} +9 -7
- package/dist/{render-v2-xm457ng1.js → render-v2-p61ppjey.js} +8 -6
- package/dist/{seed-pricing-yhpv35fr.js → seed-pricing-j56bbj4y.js} +8 -6
- package/dist/{setup-nytnmwwr.js → setup-tpr6c1kn.js} +2 -2
- package/dist/{worker-msytr2cw.js → worker-9w1v0mbb.js} +39 -36
- package/openapi.json +542 -16
- package/package.json +6 -6
- package/src/be/budget-refusal-notify.ts +14 -7
- package/src/be/db-queries/task-artifacts.ts +182 -0
- package/src/be/db.ts +783 -169
- package/src/be/events.ts +5 -0
- package/src/be/migrations/130_scratch_script_retention_grace.sql +6 -0
- package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
- package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
- package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
- package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
- package/src/be/migrations/136_task_requester_provenance.sql +37 -0
- package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
- package/src/be/multi-runtime.ts +430 -0
- package/src/be/scripts/db.ts +43 -1
- package/src/be/scripts/extract-schema.ts +3 -1
- package/src/be/scripts/retention.ts +164 -0
- package/src/be/scripts/run-saved.ts +16 -1
- package/src/be/scripts/typecheck.ts +3 -2
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/steering.ts +1 -1
- package/src/be/swarm-config-guard.ts +18 -0
- package/src/commands/credential-wait.ts +54 -0
- package/src/commands/onboard/steps/core-credentials.tsx +1 -1
- package/src/commands/onboard/steps/custom-templates.tsx +1 -1
- package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
- package/src/commands/onboard/steps/health-check.tsx +1 -1
- package/src/commands/onboard/steps/start.tsx +1 -1
- package/src/commands/profile-sync-audit.ts +56 -0
- package/src/commands/profile-sync.ts +287 -15
- package/src/commands/provider-credentials.ts +46 -19
- package/src/commands/runner.ts +196 -33
- package/src/commands/templates.ts +22 -0
- package/src/github/handlers.ts +55 -9
- package/src/heartbeat/heartbeat.ts +71 -12
- package/src/hooks/hook.ts +49 -33
- package/src/http/active-sessions.ts +11 -1
- package/src/http/agents.ts +261 -30
- package/src/http/all-routes.ts +1 -0
- package/src/http/config.ts +13 -2
- package/src/http/core.ts +102 -4
- package/src/http/db-query-bounded.ts +313 -0
- package/src/http/db-query-shared.ts +249 -0
- package/src/http/db-query.ts +54 -42
- package/src/http/events.ts +2 -0
- package/src/http/index.ts +27 -2
- package/src/http/mcp-bridge.ts +37 -3
- package/src/http/mcp.ts +12 -0
- package/src/http/memory.ts +13 -2
- package/src/http/openapi.ts +1 -0
- package/src/http/poll.ts +70 -27
- package/src/http/route-def.ts +43 -9
- package/src/http/schedules.ts +2 -2
- package/src/http/scripts.ts +31 -4
- package/src/http/session-data.ts +44 -0
- package/src/http/tasks.ts +2 -2
- package/src/http/users.ts +74 -52
- package/src/http/webhooks.ts +5 -2
- package/src/http/workflows.ts +1 -4
- package/src/http/x.ts +14 -0
- package/src/integrations/kapso/inbound.ts +1 -1
- package/src/jira/outbound.ts +39 -8
- package/src/jira/webhook-lifecycle.ts +13 -2
- package/src/linear/outbound.ts +43 -10
- package/src/oauth/keepalive.ts +13 -2
- package/src/prompts/base-prompt.ts +1 -1
- package/src/prompts/session-templates.ts +45 -7
- package/src/providers/claude-adapter.ts +6 -0
- package/src/providers/claude-managed-models.ts +34 -10
- package/src/providers/codex-adapter.ts +6 -0
- package/src/providers/opencode-adapter.ts +6 -0
- package/src/providers/pi-mono-adapter.ts +8 -0
- package/src/providers/pricing-sources.md +24 -0
- package/src/queue-stall-alarm.ts +216 -0
- package/src/scheduler/scheduler.ts +7 -0
- package/src/script-workflows/executor.ts +2 -1
- package/src/scripts-runtime/executors/native.ts +2 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/loader.ts +5 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/tests/budget-claim-gate.test.ts +20 -0
- package/src/tests/claude-adapter.test.ts +32 -0
- package/src/tests/claude-managed-adapter.test.ts +38 -7
- package/src/tests/codex-adapter.test.ts +30 -0
- package/src/tests/create-task-validation.test.ts +128 -0
- package/src/tests/credential-wait.test.ts +67 -1
- package/src/tests/db-query.test.ts +711 -2
- package/src/tests/http-api-integration.test.ts +81 -0
- package/src/tests/http-users.test.ts +166 -1
- package/src/tests/identity-field-budget.test.ts +176 -0
- package/src/tests/mcp-tools-user.test.ts +34 -0
- package/src/tests/mcp-user-route.test.ts +17 -0
- package/src/tests/metrics-http.test.ts +44 -0
- package/src/tests/multi-runtime-registration.test.ts +2703 -0
- package/src/tests/openapi-response-contract.test.ts +24 -0
- package/src/tests/openapi-route-headers.test.ts +83 -0
- package/src/tests/opencode-adapter.test.ts +40 -0
- package/src/tests/outbound-listener-rejection.test.ts +138 -0
- package/src/tests/pi-mono-adapter.test.ts +98 -0
- package/src/tests/profile-sync-warning.test.ts +277 -0
- package/src/tests/profile-sync.test.ts +232 -6
- package/src/tests/prompt-template-session.test.ts +32 -3
- package/src/tests/queue-stall-alarm.test.ts +226 -0
- package/src/tests/requester-comms.test.ts +32 -0
- package/src/tests/runner-requester-profile.test.ts +38 -0
- package/src/tests/sandboxed-process.test.ts +45 -0
- package/src/tests/schedule-target-type.test.ts +38 -0
- package/src/tests/script-connections.test.ts +16 -3
- package/src/tests/scripts-external-api.test.ts +8 -3
- package/src/tests/scripts-http.test.ts +12 -0
- package/src/tests/scripts-retention.test.ts +384 -0
- package/src/tests/scripts-runtime-identity.test.ts +375 -0
- package/src/tests/scripts-runtime.test.ts +4 -3
- package/src/tests/sdk-allowlist.test.ts +47 -0
- package/src/tests/send-task-requested-by.test.ts +9 -0
- package/src/tests/session-costs.test.ts +506 -1
- package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
- package/src/tests/steering-core.test.ts +28 -0
- package/src/tests/task-artifacts.test.ts +386 -0
- package/src/tests/task-attachments-schema.test.ts +17 -1
- package/src/tests/task-completion-idempotency.test.ts +57 -0
- package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
- package/src/tests/update-profile-api.test.ts +67 -39
- package/src/tests/update-profile-auth.test.ts +48 -4
- package/src/tests/user-attribution-migration.test.ts +54 -0
- package/src/tests/user-identity.test.ts +72 -0
- package/src/tests/workflow-async-v2.test.ts +39 -1
- package/src/tests/workflow-http-v2.test.ts +3 -2
- package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
- package/src/tests/workflow-retry-v2.test.ts +45 -1
- package/src/tests/workflow-swarm-script.test.ts +14 -5
- package/src/tools/db-query.ts +18 -8
- package/src/tools/get-task-details.ts +24 -3
- package/src/tools/manage-user.ts +36 -3
- package/src/tools/poll-task.ts +93 -14
- package/src/tools/script-common.ts +5 -0
- package/src/tools/send-task.ts +1 -4
- package/src/tools/store-progress.ts +13 -2
- package/src/tools/swarm-config/delete-config.ts +9 -0
- package/src/tools/swarm-config/set-config.ts +3 -2
- package/src/tools/task-action.ts +30 -0
- package/src/tools/task-tool-ctx.ts +8 -1
- package/src/tools/update-profile.ts +16 -21
- package/src/tools/utils.ts +5 -0
- package/src/tools/workflows/trigger-workflow.ts +7 -1
- package/src/types.ts +165 -1
- package/src/utils/github-pull-request.ts +76 -0
- package/src/utils/identity-field-budget.ts +73 -0
- package/src/utils/multi-runtime.ts +20 -0
- package/src/utils/requester-comms.ts +24 -0
- package/src/utils/sandboxed-process.ts +40 -14
- package/src/workflows/engine.ts +22 -2
- package/src/workflows/executors/swarm-script.ts +24 -1
- package/src/workflows/resume.ts +19 -11
- package/src/workflows/retry-poller.ts +1 -0
- package/src/workflows/triggers.ts +4 -0
- package/templates/schedules/daily-compounding-reflection/content.md +12 -3
- package/templates/skills/db-query-guidance/config.json +14 -0
- package/templates/skills/db-query-guidance/content.md +57 -0
- package/dist/cli-3te4jdkm.js +0 -225
- package/dist/cli-bmze986d.js +0 -7
- package/dist/cli-kh3ba2zf.js +0 -607
- package/dist/index-hp1kdbjj.js +0 -36
- package/dist/mistral-conversations-81fwav0j.js +0 -30578
- package/dist/otel-nqa12wf3.js +0 -3149
- package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
package/src/be/db.ts
CHANGED
|
@@ -116,17 +116,26 @@ import type {
|
|
|
116
116
|
} from "../types";
|
|
117
117
|
import {
|
|
118
118
|
AgentAvatarSchema,
|
|
119
|
+
type CreateTaskOptions,
|
|
120
|
+
CreateTaskOptionsSchema,
|
|
119
121
|
FollowUpConfigSchema,
|
|
120
122
|
isTerminalTaskStatus,
|
|
121
123
|
type ModelTier,
|
|
122
124
|
parseModelTier,
|
|
123
125
|
ReasoningEffortSchema,
|
|
124
126
|
RoutingAffinitySchema,
|
|
127
|
+
SERVER_GENERATED_ATTACHMENT_CAPABILITY,
|
|
125
128
|
SessionCostModelBreakdownSchema,
|
|
126
129
|
} from "../types";
|
|
127
130
|
import { deriveProviderFromKeyType } from "../utils/credentials";
|
|
128
131
|
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
129
132
|
import type { RateLimitWindowTelemetry } from "../utils/error-tracker";
|
|
133
|
+
import { extractGitHubPullRequestUrls } from "../utils/github-pull-request";
|
|
134
|
+
import {
|
|
135
|
+
type BudgetedIdentityField,
|
|
136
|
+
checkIdentityFieldBudget,
|
|
137
|
+
IdentityFieldBudgetError,
|
|
138
|
+
} from "../utils/identity-field-budget";
|
|
130
139
|
import { getCurrentRequestUserId } from "../utils/request-auth-context";
|
|
131
140
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
132
141
|
import { auditAssetKeys, enforceAssetKeyStartupAudit } from "./asset-key-audit";
|
|
@@ -181,6 +190,40 @@ export function isSqliteVecAvailable(): boolean {
|
|
|
181
190
|
return sqliteVecAvailable;
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Resolve the sqlite-vec loadable extension path without opening a Database.
|
|
195
|
+
* Mirrors loadSqliteVec's env-var-first, npm-fallback resolution, but returns
|
|
196
|
+
* the path instead of loading it: the bounded db-query child process (see
|
|
197
|
+
* src/http/db-query-bounded.ts) opens its own read-only connection and must
|
|
198
|
+
* load the extension itself, so the parent resolves the path once and passes
|
|
199
|
+
* it along. The result is memoized — `undefined` is a valid resolved answer
|
|
200
|
+
* (extension genuinely unavailable) and is cached too, so a failing
|
|
201
|
+
* `require` doesn't retry on every query.
|
|
202
|
+
*/
|
|
203
|
+
let cachedSqliteVecExtensionPath: string | undefined | null = null;
|
|
204
|
+
|
|
205
|
+
export function resolveSqliteVecExtensionPath(): string | undefined {
|
|
206
|
+
if (cachedSqliteVecExtensionPath !== null) return cachedSqliteVecExtensionPath;
|
|
207
|
+
const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH;
|
|
208
|
+
if (extensionPath) {
|
|
209
|
+
cachedSqliteVecExtensionPath = extensionPath;
|
|
210
|
+
return cachedSqliteVecExtensionPath;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
cachedSqliteVecExtensionPath = (
|
|
214
|
+
require("sqlite-vec") as { getLoadablePath(): string }
|
|
215
|
+
).getLoadablePath();
|
|
216
|
+
} catch {
|
|
217
|
+
cachedSqliteVecExtensionPath = undefined;
|
|
218
|
+
}
|
|
219
|
+
return cachedSqliteVecExtensionPath;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Test-only: clear the memoized sqlite-vec extension path cache. */
|
|
223
|
+
export function __resetSqliteVecExtensionPathCacheForTests(): void {
|
|
224
|
+
cachedSqliteVecExtensionPath = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
184
227
|
function loadSqliteVec(database: Database): void {
|
|
185
228
|
sqliteVecAvailable = false;
|
|
186
229
|
try {
|
|
@@ -432,6 +475,13 @@ const VERSIONABLE_FIELDS: VersionableField[] = [
|
|
|
432
475
|
"heartbeatMd",
|
|
433
476
|
];
|
|
434
477
|
|
|
478
|
+
const BUDGETED_IDENTITY_FIELDS: BudgetedIdentityField[] = [
|
|
479
|
+
"soulMd",
|
|
480
|
+
"identityMd",
|
|
481
|
+
"claudeMd",
|
|
482
|
+
"toolsMd",
|
|
483
|
+
];
|
|
484
|
+
|
|
435
485
|
function ensureAgentProfileColumns(database: Database): void {
|
|
436
486
|
const existingColumns = new Set(
|
|
437
487
|
database
|
|
@@ -775,6 +825,19 @@ export function updateAgentCredentialState(
|
|
|
775
825
|
return row ? rowToAgent(row) : null;
|
|
776
826
|
}
|
|
777
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Record which env vars a worker is missing without touching status — the
|
|
830
|
+
* logical status is derived from runtime readiness in multi-runtime mode.
|
|
831
|
+
*/
|
|
832
|
+
export function updateAgentCredentialMissing(agentId: string, missing: string[] | null): void {
|
|
833
|
+
const json = missing && missing.length > 0 ? JSON.stringify(missing) : null;
|
|
834
|
+
getDb()
|
|
835
|
+
.prepare(
|
|
836
|
+
"UPDATE agents SET credentialMissing = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?",
|
|
837
|
+
)
|
|
838
|
+
.run(json, agentId);
|
|
839
|
+
}
|
|
840
|
+
|
|
778
841
|
export function createAgent(
|
|
779
842
|
agent: Omit<Agent, "id" | "createdAt" | "lastUpdatedAt"> & { id?: string },
|
|
780
843
|
): Agent {
|
|
@@ -1014,12 +1077,22 @@ export function deleteAgent(id: string): boolean {
|
|
|
1014
1077
|
* Get the count of active (in_progress) tasks for an agent.
|
|
1015
1078
|
* Used to determine current capacity usage.
|
|
1016
1079
|
*/
|
|
1080
|
+
/**
|
|
1081
|
+
* Tasks occupying one of the agent's concurrency slots.
|
|
1082
|
+
*
|
|
1083
|
+
* A claimed offer counts too — omitting it let a second concurrent poll take
|
|
1084
|
+
* another task past the limit. It is counted only through `offeredTo`, the
|
|
1085
|
+
* agent actually reviewing it: `agentId` on an offer may still be the lead
|
|
1086
|
+
* that created it, which would otherwise consume the lead's own capacity.
|
|
1087
|
+
*/
|
|
1017
1088
|
export function getActiveTaskCount(agentId: string): number {
|
|
1018
1089
|
const result = getDb()
|
|
1019
|
-
.prepare<{ count: number }, [string]>(
|
|
1020
|
-
|
|
1090
|
+
.prepare<{ count: number }, [string, string]>(
|
|
1091
|
+
`SELECT COUNT(*) as count FROM agent_tasks
|
|
1092
|
+
WHERE (agentId = ? AND status = 'in_progress')
|
|
1093
|
+
OR (offeredTo = ? AND status = 'reviewing')`,
|
|
1021
1094
|
)
|
|
1022
|
-
.get(agentId);
|
|
1095
|
+
.get(agentId, agentId);
|
|
1023
1096
|
return result?.count ?? 0;
|
|
1024
1097
|
}
|
|
1025
1098
|
|
|
@@ -1202,6 +1275,7 @@ type AgentTaskRow = {
|
|
|
1202
1275
|
credentialKeySuffix: string | null;
|
|
1203
1276
|
credentialKeyType: string | null;
|
|
1204
1277
|
requestedByUserId: string | null;
|
|
1278
|
+
requestedByUserIdInherited: number;
|
|
1205
1279
|
swarmVersion: string | null;
|
|
1206
1280
|
provider: string | null;
|
|
1207
1281
|
providerMeta: string | null;
|
|
@@ -2143,13 +2217,29 @@ export function updateTaskVcs(
|
|
|
2143
2217
|
vcsUrl: string;
|
|
2144
2218
|
},
|
|
2145
2219
|
): AgentTask | null {
|
|
2146
|
-
const row = getDb()
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2220
|
+
const row = getDb().transaction(() => {
|
|
2221
|
+
const updated = getDb()
|
|
2222
|
+
.prepare<AgentTaskRow, [string, string, number, string, string, string]>(
|
|
2223
|
+
`UPDATE agent_tasks
|
|
2224
|
+
SET vcsProvider = ?, vcsRepo = ?, vcsNumber = ?, vcsUrl = ?, lastUpdatedAt = ?
|
|
2225
|
+
WHERE id = ? RETURNING *`,
|
|
2226
|
+
)
|
|
2227
|
+
.get(
|
|
2228
|
+
vcs.vcsProvider,
|
|
2229
|
+
vcs.vcsRepo,
|
|
2230
|
+
vcs.vcsNumber,
|
|
2231
|
+
vcs.vcsUrl,
|
|
2232
|
+
new Date().toISOString(),
|
|
2233
|
+
taskId,
|
|
2234
|
+
);
|
|
2235
|
+
if (updated) {
|
|
2236
|
+
reconcileTaskPullRequestAttachments(taskId, updated.agentId, [
|
|
2237
|
+
updated.output,
|
|
2238
|
+
vcs.vcsProvider === "github" ? vcs.vcsUrl : null,
|
|
2239
|
+
]);
|
|
2240
|
+
}
|
|
2241
|
+
return updated;
|
|
2242
|
+
})();
|
|
2153
2243
|
return row ? rowToAgentTask(row) : null;
|
|
2154
2244
|
}
|
|
2155
2245
|
|
|
@@ -2931,13 +3021,23 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
2931
3021
|
return null;
|
|
2932
3022
|
}
|
|
2933
3023
|
|
|
2934
|
-
const
|
|
2935
|
-
|
|
2936
|
-
|
|
3024
|
+
const row = getDb().transaction(() => {
|
|
3025
|
+
const finishedAt = new Date().toISOString();
|
|
3026
|
+
let completed = taskQueries.updateStatus().get("completed", finishedAt, id);
|
|
3027
|
+
if (!completed) return null;
|
|
2937
3028
|
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
3029
|
+
if (output) {
|
|
3030
|
+
completed = taskQueries.setOutput().get(scrubSecrets(output), id);
|
|
3031
|
+
}
|
|
3032
|
+
if (completed) {
|
|
3033
|
+
reconcileTaskPullRequestAttachments(id, completed.agentId, [
|
|
3034
|
+
completed.output,
|
|
3035
|
+
completed.vcsProvider === "github" ? completed.vcsUrl : null,
|
|
3036
|
+
]);
|
|
3037
|
+
}
|
|
3038
|
+
return completed;
|
|
3039
|
+
})();
|
|
3040
|
+
if (!row) return null;
|
|
2941
3041
|
|
|
2942
3042
|
if (row && oldTask) {
|
|
2943
3043
|
emitTaskLifecycleTelemetryAfterCommit(
|
|
@@ -2961,8 +3061,8 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
2961
3061
|
newValue: "completed",
|
|
2962
3062
|
});
|
|
2963
3063
|
} catch {}
|
|
2964
|
-
|
|
2965
|
-
|
|
3064
|
+
import("../workflows/event-bus")
|
|
3065
|
+
.then(({ workflowEventBus }) => {
|
|
2966
3066
|
workflowEventBus.emit("task.completed", {
|
|
2967
3067
|
taskId: id,
|
|
2968
3068
|
output,
|
|
@@ -2970,8 +3070,13 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
2970
3070
|
workflowRunId: row.workflowRunId,
|
|
2971
3071
|
workflowRunStepId: row.workflowRunStepId,
|
|
2972
3072
|
});
|
|
2973
|
-
})
|
|
2974
|
-
|
|
3073
|
+
})
|
|
3074
|
+
.catch((err) =>
|
|
3075
|
+
console.error(
|
|
3076
|
+
"[db] task.completed event not emitted:",
|
|
3077
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3078
|
+
),
|
|
3079
|
+
);
|
|
2975
3080
|
try {
|
|
2976
3081
|
promotePendingSteeringForTask(id, "Task completed before steering was delivered");
|
|
2977
3082
|
} catch (error) {
|
|
@@ -3022,8 +3127,8 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
3022
3127
|
metadata: { reason: scrubbedReason },
|
|
3023
3128
|
});
|
|
3024
3129
|
} catch {}
|
|
3025
|
-
|
|
3026
|
-
|
|
3130
|
+
import("../workflows/event-bus")
|
|
3131
|
+
.then(({ workflowEventBus }) => {
|
|
3027
3132
|
workflowEventBus.emit("task.failed", {
|
|
3028
3133
|
taskId: id,
|
|
3029
3134
|
failureReason: reason,
|
|
@@ -3031,8 +3136,13 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
3031
3136
|
workflowRunId: row.workflowRunId,
|
|
3032
3137
|
workflowRunStepId: row.workflowRunStepId,
|
|
3033
3138
|
});
|
|
3034
|
-
})
|
|
3035
|
-
|
|
3139
|
+
})
|
|
3140
|
+
.catch((err) =>
|
|
3141
|
+
console.error(
|
|
3142
|
+
"[db] task.failed event not emitted:",
|
|
3143
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3144
|
+
),
|
|
3145
|
+
);
|
|
3036
3146
|
try {
|
|
3037
3147
|
promotePendingSteeringForTask(id, "Task failed before steering was delivered");
|
|
3038
3148
|
} catch (error) {
|
|
@@ -3070,7 +3180,16 @@ export function overwriteTerminalTaskResultText(
|
|
|
3070
3180
|
patch.failureReason !== undefined
|
|
3071
3181
|
? scrubSecrets(patch.failureReason)
|
|
3072
3182
|
: (task.failureReason ?? null);
|
|
3073
|
-
const row =
|
|
3183
|
+
const row = getDb().transaction(() => {
|
|
3184
|
+
const updated = taskQueries.setTerminalResultText().get(output, failureReason, id) ?? null;
|
|
3185
|
+
if (updated && patch.output !== undefined) {
|
|
3186
|
+
reconcileTaskPullRequestAttachments(id, updated.agentId, [
|
|
3187
|
+
updated.output,
|
|
3188
|
+
updated.vcsProvider === "github" ? updated.vcsUrl : null,
|
|
3189
|
+
]);
|
|
3190
|
+
}
|
|
3191
|
+
return updated;
|
|
3192
|
+
})();
|
|
3074
3193
|
|
|
3075
3194
|
return row ? rowToAgentTask(row) : task;
|
|
3076
3195
|
}
|
|
@@ -3113,16 +3232,21 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
3113
3232
|
metadata: reason ? { reason } : undefined,
|
|
3114
3233
|
});
|
|
3115
3234
|
} catch {}
|
|
3116
|
-
|
|
3117
|
-
|
|
3235
|
+
import("../workflows/event-bus")
|
|
3236
|
+
.then(({ workflowEventBus }) => {
|
|
3118
3237
|
workflowEventBus.emit("task.cancelled", {
|
|
3119
3238
|
taskId: id,
|
|
3120
3239
|
agentId: row.agentId,
|
|
3121
3240
|
workflowRunId: row.workflowRunId,
|
|
3122
3241
|
workflowRunStepId: row.workflowRunStepId,
|
|
3123
3242
|
});
|
|
3124
|
-
})
|
|
3125
|
-
|
|
3243
|
+
})
|
|
3244
|
+
.catch((err) =>
|
|
3245
|
+
console.error(
|
|
3246
|
+
"[db] task.cancelled event not emitted:",
|
|
3247
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3248
|
+
),
|
|
3249
|
+
);
|
|
3126
3250
|
try {
|
|
3127
3251
|
promotePendingSteeringForTask(id, "Task was cancelled before steering was delivered");
|
|
3128
3252
|
} catch (error) {
|
|
@@ -3203,8 +3327,8 @@ export function supersedeTask(
|
|
|
3203
3327
|
metadata: { reason: args.reason, resumeTaskId: args.resumeTaskId },
|
|
3204
3328
|
});
|
|
3205
3329
|
} catch {}
|
|
3206
|
-
|
|
3207
|
-
|
|
3330
|
+
import("../workflows/event-bus")
|
|
3331
|
+
.then(({ workflowEventBus }) => {
|
|
3208
3332
|
workflowEventBus.emit("task.superseded", {
|
|
3209
3333
|
taskId: id,
|
|
3210
3334
|
reason: args.reason,
|
|
@@ -3213,8 +3337,13 @@ export function supersedeTask(
|
|
|
3213
3337
|
workflowRunId: row.workflowRunId,
|
|
3214
3338
|
workflowRunStepId: row.workflowRunStepId,
|
|
3215
3339
|
});
|
|
3216
|
-
})
|
|
3217
|
-
|
|
3340
|
+
})
|
|
3341
|
+
.catch((err) =>
|
|
3342
|
+
console.error(
|
|
3343
|
+
"[db] task.superseded event not emitted:",
|
|
3344
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3345
|
+
),
|
|
3346
|
+
);
|
|
3218
3347
|
|
|
3219
3348
|
try {
|
|
3220
3349
|
cascadeFailDependents(id, "superseded");
|
|
@@ -3443,15 +3572,20 @@ export function updateTaskProgress(id: string, progress: string): AgentTask | nu
|
|
|
3443
3572
|
newValue: scrubbedProgress,
|
|
3444
3573
|
});
|
|
3445
3574
|
} catch {}
|
|
3446
|
-
|
|
3447
|
-
|
|
3575
|
+
import("../workflows/event-bus")
|
|
3576
|
+
.then(({ workflowEventBus }) => {
|
|
3448
3577
|
workflowEventBus.emit("task.progress", {
|
|
3449
3578
|
taskId: id,
|
|
3450
3579
|
progress: scrubbedProgress,
|
|
3451
3580
|
agentId: row.agentId,
|
|
3452
3581
|
});
|
|
3453
|
-
})
|
|
3454
|
-
|
|
3582
|
+
})
|
|
3583
|
+
.catch((err) =>
|
|
3584
|
+
console.error(
|
|
3585
|
+
"[db] task.progress event not emitted:",
|
|
3586
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3587
|
+
),
|
|
3588
|
+
);
|
|
3455
3589
|
}
|
|
3456
3590
|
return row ? rowToAgentTask(row) : null;
|
|
3457
3591
|
}
|
|
@@ -3592,6 +3726,101 @@ export function insertTaskAttachment(input: InsertTaskAttachmentInput): TaskAtta
|
|
|
3592
3726
|
})();
|
|
3593
3727
|
}
|
|
3594
3728
|
|
|
3729
|
+
const GENERATED_PULL_REQUEST_PROVIDER_ID = "github";
|
|
3730
|
+
const GENERATED_PULL_REQUEST_INTENT = "task-deliverable";
|
|
3731
|
+
const GENERATED_PULL_REQUEST_DESCRIPTION = "Pull request shipped by this task";
|
|
3732
|
+
const GENERATED_PULL_REQUEST_SOURCE = "task-pull-request-recorder";
|
|
3733
|
+
|
|
3734
|
+
function pullRequestKey(pullRequest: { owner: string; repo: string; number: number }): string {
|
|
3735
|
+
return `${pullRequest.owner.toLowerCase()}/${pullRequest.repo.toLowerCase()}#${pullRequest.number}`;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
/**
|
|
3739
|
+
* Persist PRs detected by server-owned task lifecycle paths. Existing URL
|
|
3740
|
+
* attachments win regardless of display name, so an agent-authored row and an
|
|
3741
|
+
* automatic row never duplicate the same task deliverable. A capability key
|
|
3742
|
+
* rejected by AttachmentInputSchema marks server-generated rows so
|
|
3743
|
+
* reconciliation cannot mistake caller-authored metadata for provenance.
|
|
3744
|
+
*/
|
|
3745
|
+
export function recordTaskPullRequestAttachments(
|
|
3746
|
+
taskId: string,
|
|
3747
|
+
agentId: string | null,
|
|
3748
|
+
text: string | null | undefined,
|
|
3749
|
+
): TaskAttachment[] {
|
|
3750
|
+
const pullRequests = extractGitHubPullRequestUrls(text);
|
|
3751
|
+
if (pullRequests.length === 0) return [];
|
|
3752
|
+
|
|
3753
|
+
const existingPullRequests = new Set(
|
|
3754
|
+
getDb()
|
|
3755
|
+
.prepare<{ url: string }, [string]>(
|
|
3756
|
+
"SELECT url FROM task_attachments WHERE task_id = ? AND kind = 'url' AND url IS NOT NULL",
|
|
3757
|
+
)
|
|
3758
|
+
.all(taskId)
|
|
3759
|
+
.flatMap((row) => extractGitHubPullRequestUrls(row.url))
|
|
3760
|
+
.map(pullRequestKey),
|
|
3761
|
+
);
|
|
3762
|
+
const stored: TaskAttachment[] = [];
|
|
3763
|
+
for (const pullRequest of pullRequests) {
|
|
3764
|
+
const dedupeKey = pullRequestKey(pullRequest);
|
|
3765
|
+
if (existingPullRequests.has(dedupeKey)) continue;
|
|
3766
|
+
stored.push(
|
|
3767
|
+
insertTaskAttachment({
|
|
3768
|
+
taskId,
|
|
3769
|
+
agentId,
|
|
3770
|
+
name: `GitHub pull request #${pullRequest.number}`,
|
|
3771
|
+
kind: "url",
|
|
3772
|
+
url: pullRequest.url,
|
|
3773
|
+
providerId: GENERATED_PULL_REQUEST_PROVIDER_ID,
|
|
3774
|
+
capabilities: {
|
|
3775
|
+
[SERVER_GENERATED_ATTACHMENT_CAPABILITY]: GENERATED_PULL_REQUEST_SOURCE,
|
|
3776
|
+
},
|
|
3777
|
+
intent: GENERATED_PULL_REQUEST_INTENT,
|
|
3778
|
+
description: GENERATED_PULL_REQUEST_DESCRIPTION,
|
|
3779
|
+
}),
|
|
3780
|
+
);
|
|
3781
|
+
existingPullRequests.add(dedupeKey);
|
|
3782
|
+
}
|
|
3783
|
+
return stored;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
/** Reconcile only lifecycle-generated PR rows against their current source text. */
|
|
3787
|
+
function reconcileTaskPullRequestAttachments(
|
|
3788
|
+
taskId: string,
|
|
3789
|
+
agentId: string | null,
|
|
3790
|
+
sourceTexts: Array<string | null | undefined>,
|
|
3791
|
+
): TaskAttachment[] {
|
|
3792
|
+
const desiredPullRequests = sourceTexts.flatMap(extractGitHubPullRequestUrls);
|
|
3793
|
+
const desiredKeys = new Set(desiredPullRequests.map(pullRequestKey));
|
|
3794
|
+
const generatedRows = getDb()
|
|
3795
|
+
.prepare<{ id: string; name: string; url: string }, [string, string]>(
|
|
3796
|
+
`SELECT id, name, url
|
|
3797
|
+
FROM task_attachments
|
|
3798
|
+
WHERE task_id = ?
|
|
3799
|
+
AND kind = 'url'
|
|
3800
|
+
AND url IS NOT NULL
|
|
3801
|
+
AND json_extract(capabilities, '$.${SERVER_GENERATED_ATTACHMENT_CAPABILITY}') = ?`,
|
|
3802
|
+
)
|
|
3803
|
+
.all(taskId, GENERATED_PULL_REQUEST_SOURCE);
|
|
3804
|
+
|
|
3805
|
+
for (const row of generatedRows) {
|
|
3806
|
+
const pullRequest = extractGitHubPullRequestUrls(row.url)[0];
|
|
3807
|
+
const expectedName = pullRequest ? `GitHub pull request #${pullRequest.number}` : null;
|
|
3808
|
+
if (
|
|
3809
|
+
!pullRequest ||
|
|
3810
|
+
row.name !== expectedName ||
|
|
3811
|
+
!desiredKeys.has(pullRequestKey(pullRequest))
|
|
3812
|
+
) {
|
|
3813
|
+
getDb().run("DELETE FROM task_attachments WHERE id = ?", [row.id]);
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
return recordTaskPullRequestAttachments(
|
|
3818
|
+
taskId,
|
|
3819
|
+
agentId,
|
|
3820
|
+
desiredPullRequests.map((pullRequest) => pullRequest.url).join("\n"),
|
|
3821
|
+
);
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3595
3824
|
function insertTaskAttachmentRow(input: InsertTaskAttachmentInput): TaskAttachment {
|
|
3596
3825
|
const db = getDb();
|
|
3597
3826
|
|
|
@@ -4638,89 +4867,11 @@ export function getAllLogs(limit?: number): AgentLog[] {
|
|
|
4638
4867
|
// Task Pool Operations
|
|
4639
4868
|
// ============================================================================
|
|
4640
4869
|
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
taskType?: string;
|
|
4647
|
-
tags?: string[];
|
|
4648
|
-
priority?: number;
|
|
4649
|
-
dependsOn?: string[];
|
|
4650
|
-
offeredTo?: string;
|
|
4651
|
-
status?: "backlog" | "unassigned"; // Explicitly set initial status
|
|
4652
|
-
slackChannelId?: string;
|
|
4653
|
-
slackThreadTs?: string;
|
|
4654
|
-
/** Exact Slack message that directly triggered this task; never inherited. */
|
|
4655
|
-
slackTriggerMessageTs?: string;
|
|
4656
|
-
slackUserId?: string;
|
|
4657
|
-
/**
|
|
4658
|
-
* Opt out of the residual Slack/contextKey normalization below (see the
|
|
4659
|
-
* "Residual-mismatch guard" comment near the INSERT): a deliberate
|
|
4660
|
-
* cross-channel/thread dispatch (e.g. `send-task`'s
|
|
4661
|
-
* `overrideSlackContext: true`) sets this so its explicit slackChannelId/
|
|
4662
|
-
* slackThreadTs survive even when they disagree with a slack-family
|
|
4663
|
-
* `contextKey`/parent. Trusted callers that don't set this get normalized —
|
|
4664
|
-
* this boundary must not let a caller silently persist a mismatch.
|
|
4665
|
-
*/
|
|
4666
|
-
overrideSlackContext?: boolean;
|
|
4667
|
-
vcsProvider?: "github" | "gitlab";
|
|
4668
|
-
vcsRepo?: string;
|
|
4669
|
-
vcsEventType?: string;
|
|
4670
|
-
vcsNumber?: number;
|
|
4671
|
-
vcsCommentId?: number;
|
|
4672
|
-
vcsAuthor?: string;
|
|
4673
|
-
vcsUrl?: string;
|
|
4674
|
-
vcsInstallationId?: number;
|
|
4675
|
-
vcsNodeId?: string;
|
|
4676
|
-
agentmailInboxId?: string;
|
|
4677
|
-
agentmailMessageId?: string;
|
|
4678
|
-
agentmailThreadId?: string;
|
|
4679
|
-
mentionMessageId?: string;
|
|
4680
|
-
mentionChannelId?: string;
|
|
4681
|
-
dir?: string;
|
|
4682
|
-
parentTaskId?: string;
|
|
4683
|
-
model?: string;
|
|
4684
|
-
modelTier?: ModelTier;
|
|
4685
|
-
effort?: ReasoningEffort;
|
|
4686
|
-
scheduleId?: string;
|
|
4687
|
-
workflowRunId?: string;
|
|
4688
|
-
workflowRunStepId?: string;
|
|
4689
|
-
sourceTaskId?: string;
|
|
4690
|
-
/**
|
|
4691
|
-
* Optional JSON Schema the agent's final output must conform to.
|
|
4692
|
-
*
|
|
4693
|
-
* Enforced via the MCP `store-progress` tool (validated in
|
|
4694
|
-
* `src/tools/store-progress.ts`). NOT enforced when the task runs on
|
|
4695
|
-
* default-mode Devin (no MCP) — see runbooks/harness-providers.md
|
|
4696
|
-
* ("Per-task outputSchema support"). Callers reading `task.output` for
|
|
4697
|
-
* a schema'd task should be defensive about JSON parsing.
|
|
4698
|
-
*/
|
|
4699
|
-
outputSchema?: Record<string, unknown>;
|
|
4700
|
-
/**
|
|
4701
|
-
* When a `parentTaskId` is set, the child inherits the parent's `outputSchema`
|
|
4702
|
-
* by default. Set this to `false` to opt out — used by control-plane children
|
|
4703
|
-
* (e.g. the Lead `reroute-decision` task) that must inherit Slack/VCS context
|
|
4704
|
-
* from the parent but must NOT be forced to satisfy the original work's output
|
|
4705
|
-
* contract on completion (which would block the control task — DES-523).
|
|
4706
|
-
*/
|
|
4707
|
-
inheritParentOutputSchema?: boolean;
|
|
4708
|
-
followUpConfig?: FollowUpConfig;
|
|
4709
|
-
requestedByUserId?: string;
|
|
4710
|
-
contextKey?: string;
|
|
4711
|
-
/**
|
|
4712
|
-
* Internal control-plane escape hatch for continuations that MUST coexist
|
|
4713
|
-
* with their active Linear parent (for example promoted steering). General
|
|
4714
|
-
* task creation keeps tracker-context dedup enabled.
|
|
4715
|
-
*/
|
|
4716
|
-
bypassTrackerContextDedup?: boolean;
|
|
4717
|
-
/**
|
|
4718
|
-
* Routing-affinity snapshot gating pool eligibility (see
|
|
4719
|
-
* `isAgentEligibleForTask`). Inherited from the parent (via `parentTaskId`)
|
|
4720
|
-
* when not explicitly set — same treatment as `vcsRepo`/`contextKey`.
|
|
4721
|
-
*/
|
|
4722
|
-
routingAffinity?: RoutingAffinity;
|
|
4723
|
-
}
|
|
4870
|
+
// The runtime contract for task creation lives in src/types.ts
|
|
4871
|
+
// (`CreateTaskOptionsSchema`); `createTaskExtended` parses options against it.
|
|
4872
|
+
// The type is re-exported so existing importers (src/tasks/sibling-awareness.ts)
|
|
4873
|
+
// keep resolving it from this module.
|
|
4874
|
+
export type { CreateTaskOptions } from "../types";
|
|
4724
4875
|
|
|
4725
4876
|
/**
|
|
4726
4877
|
* Find recent tasks within a time window for deduplication checks.
|
|
@@ -4759,6 +4910,15 @@ export function findRecentSimilarTasks(opts: {
|
|
|
4759
4910
|
}
|
|
4760
4911
|
|
|
4761
4912
|
export function createTaskExtended(task: string, options?: CreateTaskOptions): AgentTask {
|
|
4913
|
+
if (typeof task !== "string" || task.trim().length === 0) {
|
|
4914
|
+
throw new Error("createTaskExtended: 'task' must be a non-empty string");
|
|
4915
|
+
}
|
|
4916
|
+
// Single runtime enforcement point for every task write (REST, MCP,
|
|
4917
|
+
// scripts bridge, webhooks, scheduler, internal callers). Reject, never
|
|
4918
|
+
// coerce: a bad shape throws before anything reaches the INSERT; absent
|
|
4919
|
+
// fields keep the `??` defaults at the bind site below.
|
|
4920
|
+
options = CreateTaskOptionsSchema.parse(options ?? {});
|
|
4921
|
+
let requestedByUserIdInherited = false;
|
|
4762
4922
|
const id = crypto.randomUUID();
|
|
4763
4923
|
const now = new Date().toISOString();
|
|
4764
4924
|
const status: AgentTaskStatus = options?.offeredTo
|
|
@@ -4894,6 +5054,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
4894
5054
|
// Attribution
|
|
4895
5055
|
if (parent.requestedByUserId && !options.requestedByUserId) {
|
|
4896
5056
|
options.requestedByUserId = parent.requestedByUserId;
|
|
5057
|
+
requestedByUserIdInherited = true;
|
|
4897
5058
|
}
|
|
4898
5059
|
if (parent.key && !options.key) {
|
|
4899
5060
|
options.key = parent.key;
|
|
@@ -5009,8 +5170,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
5009
5170
|
vcsInstallationId, vcsNodeId,
|
|
5010
5171
|
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
5011
5172
|
mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, effort, scheduleId,
|
|
5012
|
-
workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
|
|
5013
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
5173
|
+
workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, requestedByUserIdInherited, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
|
|
5174
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
5014
5175
|
)
|
|
5015
5176
|
.get(
|
|
5016
5177
|
id,
|
|
@@ -5055,6 +5216,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
5055
5216
|
options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
|
|
5056
5217
|
options?.followUpConfig ? JSON.stringify(options.followUpConfig) : null,
|
|
5057
5218
|
options?.requestedByUserId ?? null,
|
|
5219
|
+
requestedByUserIdInherited ? 1 : 0,
|
|
5058
5220
|
options?.contextKey ?? null,
|
|
5059
5221
|
options?.routingAffinity ? JSON.stringify(options.routingAffinity) : null,
|
|
5060
5222
|
pkg.version,
|
|
@@ -5088,8 +5250,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
5088
5250
|
(task) => task !== null,
|
|
5089
5251
|
);
|
|
5090
5252
|
|
|
5091
|
-
|
|
5092
|
-
|
|
5253
|
+
import("../workflows/event-bus")
|
|
5254
|
+
.then(({ workflowEventBus }) => {
|
|
5093
5255
|
workflowEventBus.emit("task.created", {
|
|
5094
5256
|
taskId: row.id,
|
|
5095
5257
|
task: row.task,
|
|
@@ -5099,8 +5261,13 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
5099
5261
|
workflowRunId: row.workflowRunId,
|
|
5100
5262
|
workflowRunStepId: row.workflowRunStepId,
|
|
5101
5263
|
});
|
|
5102
|
-
})
|
|
5103
|
-
|
|
5264
|
+
})
|
|
5265
|
+
.catch((err) =>
|
|
5266
|
+
console.error(
|
|
5267
|
+
"[db] task.created event not emitted:",
|
|
5268
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
5269
|
+
),
|
|
5270
|
+
);
|
|
5104
5271
|
|
|
5105
5272
|
return rowToAgentTask(row);
|
|
5106
5273
|
}
|
|
@@ -5556,6 +5723,7 @@ export {
|
|
|
5556
5723
|
export function updateAgentProfile(
|
|
5557
5724
|
id: string,
|
|
5558
5725
|
updates: {
|
|
5726
|
+
name?: string;
|
|
5559
5727
|
description?: string;
|
|
5560
5728
|
role?: string;
|
|
5561
5729
|
capabilities?: string[];
|
|
@@ -5579,6 +5747,31 @@ export function updateAgentProfile(
|
|
|
5579
5747
|
.get(id);
|
|
5580
5748
|
if (!current) return null;
|
|
5581
5749
|
|
|
5750
|
+
for (const field of BUDGETED_IDENTITY_FIELDS) {
|
|
5751
|
+
const nextValue = updates[field];
|
|
5752
|
+
if (nextValue === undefined) continue;
|
|
5753
|
+
|
|
5754
|
+
const result = checkIdentityFieldBudget({
|
|
5755
|
+
field,
|
|
5756
|
+
currentValue: current[field] ?? "",
|
|
5757
|
+
nextValue,
|
|
5758
|
+
});
|
|
5759
|
+
if (!result.ok) {
|
|
5760
|
+
throw new IdentityFieldBudgetError(
|
|
5761
|
+
result,
|
|
5762
|
+
computeContentHash(current[field] ?? ""),
|
|
5763
|
+
computeContentHash(nextValue),
|
|
5764
|
+
);
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5767
|
+
|
|
5768
|
+
if (updates.name !== undefined) {
|
|
5769
|
+
const existingAgent = database
|
|
5770
|
+
.prepare<AgentRow, [string, string]>("SELECT * FROM agents WHERE name = ? AND id != ?")
|
|
5771
|
+
.get(updates.name, id);
|
|
5772
|
+
if (existingAgent) throw new Error("Agent name already exists");
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5582
5775
|
// Create context versions for changed fields
|
|
5583
5776
|
for (const field of VERSIONABLE_FIELDS) {
|
|
5584
5777
|
const newValue = updates[field];
|
|
@@ -5631,6 +5824,7 @@ export function updateAgentProfile(
|
|
|
5631
5824
|
string | null,
|
|
5632
5825
|
string | null,
|
|
5633
5826
|
string | null,
|
|
5827
|
+
string | null,
|
|
5634
5828
|
number,
|
|
5635
5829
|
string | null,
|
|
5636
5830
|
string,
|
|
@@ -5638,6 +5832,7 @@ export function updateAgentProfile(
|
|
|
5638
5832
|
]
|
|
5639
5833
|
>(
|
|
5640
5834
|
`UPDATE agents SET
|
|
5835
|
+
name = COALESCE(?, name),
|
|
5641
5836
|
description = COALESCE(?, description),
|
|
5642
5837
|
role = COALESCE(?, role),
|
|
5643
5838
|
capabilities = COALESCE(?, capabilities),
|
|
@@ -5652,6 +5847,7 @@ export function updateAgentProfile(
|
|
|
5652
5847
|
WHERE id = ? RETURNING *`,
|
|
5653
5848
|
)
|
|
5654
5849
|
.get(
|
|
5850
|
+
updates.name ?? null,
|
|
5655
5851
|
updates.description ?? null,
|
|
5656
5852
|
updates.role ?? null,
|
|
5657
5853
|
updates.capabilities ? JSON.stringify(updates.capabilities) : null,
|
|
@@ -5672,23 +5868,7 @@ export function updateAgentProfile(
|
|
|
5672
5868
|
}
|
|
5673
5869
|
|
|
5674
5870
|
export function updateAgentName(id: string, newName: string): Agent | null {
|
|
5675
|
-
|
|
5676
|
-
const existingAgent = getDb()
|
|
5677
|
-
.prepare<AgentRow, [string, string]>("SELECT * FROM agents WHERE name = ? AND id != ?")
|
|
5678
|
-
.get(newName, id);
|
|
5679
|
-
|
|
5680
|
-
if (existingAgent) {
|
|
5681
|
-
throw new Error("Agent name already exists");
|
|
5682
|
-
}
|
|
5683
|
-
|
|
5684
|
-
const now = new Date().toISOString();
|
|
5685
|
-
const row = getDb()
|
|
5686
|
-
.prepare<AgentRow, [string, string, string]>(
|
|
5687
|
-
"UPDATE agents SET name = ?, lastUpdatedAt = ? WHERE id = ? RETURNING *",
|
|
5688
|
-
)
|
|
5689
|
-
.get(newName, now, id);
|
|
5690
|
-
|
|
5691
|
-
return row ? rowToAgent(row) : null;
|
|
5871
|
+
return updateAgentProfile(id, { name: newName });
|
|
5692
5872
|
}
|
|
5693
5873
|
|
|
5694
5874
|
// ============================================================================
|
|
@@ -6873,8 +7053,30 @@ export interface SessionCostSummaryTotals {
|
|
|
6873
7053
|
totalDurationMs: number;
|
|
6874
7054
|
totalSessions: number;
|
|
6875
7055
|
avgCostPerSession: number;
|
|
6876
|
-
/**
|
|
7056
|
+
/**
|
|
7057
|
+
* Cost of tasks with a human requester, over `attributableCostUsd` (not
|
|
7058
|
+
* `totalCostUsd`) — see `attributableCostUsd` for why. Also excludes cost on
|
|
7059
|
+
* a structurally-human-free task that happens to carry a stale/inherited
|
|
7060
|
+
* `requestedByUserId` (measured 2026-08-19: ~5% of the naive-attributed
|
|
7061
|
+
* population), so the two totals stay a consistent partition.
|
|
7062
|
+
*/
|
|
6877
7063
|
attributedCostUsd: number;
|
|
7064
|
+
/**
|
|
7065
|
+
* The corrected coverage denominator: `totalCostUsd` minus the cost of
|
|
7066
|
+
* structurally-human-free tasks (heartbeat/boot-triage, scheduled runs, and
|
|
7067
|
+
* `source='system'` follow-ups whose parent itself had no human requester —
|
|
7068
|
+
* i.e. follow-ups of the swarm's own self-maintenance, not of human work).
|
|
7069
|
+
* These tasks have no human requester *by construction*; stamping one on
|
|
7070
|
+
* them would be a lie, not coverage, so they don't belong in the
|
|
7071
|
+
* denominator at all. `attributedCostUsd / attributableCostUsd` is the
|
|
7072
|
+
* number to show as "coverage" — dividing by `totalCostUsd` instead
|
|
7073
|
+
* silently deflates it with a population that could never have scored.
|
|
7074
|
+
*/
|
|
7075
|
+
attributableCostUsd: number;
|
|
7076
|
+
/** Cost of the structurally-human-free population excluded from `attributableCostUsd`. */
|
|
7077
|
+
excludedCostUsd: number;
|
|
7078
|
+
/** Distinct tasks behind `excludedCostUsd` — surfaced so the UI can name the exclusion count, not just wave at a percentage. */
|
|
7079
|
+
excludedTaskCount: number;
|
|
6878
7080
|
}
|
|
6879
7081
|
|
|
6880
7082
|
export interface SessionCostDailyRow {
|
|
@@ -6907,6 +7109,60 @@ export interface SessionCostByUserRow {
|
|
|
6907
7109
|
/** `opts.userId` sentinel selecting spend with no human requester. */
|
|
6908
7110
|
export const UNATTRIBUTED_USER_ID = "unattributed";
|
|
6909
7111
|
|
|
7112
|
+
// Structurally-human-free tasks and their descendants. An explicitly supplied
|
|
7113
|
+
// requester is a human handoff and stops propagation; a requester copied from
|
|
7114
|
+
// the parent does not.
|
|
7115
|
+
const HUMAN_FREE_TASKS_CTE = `human_free_tasks(id) AS (
|
|
7116
|
+
SELECT task.id
|
|
7117
|
+
FROM agent_tasks task
|
|
7118
|
+
LEFT JOIN agent_tasks parent ON parent.id = task.parentTaskId
|
|
7119
|
+
WHERE COALESCE(task.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
|
|
7120
|
+
OR COALESCE(task.tags, '[]') LIKE '%"heartbeat"%'
|
|
7121
|
+
OR (COALESCE(task.source, '') = 'schedule' AND task.requestedByUserId IS NULL)
|
|
7122
|
+
OR (
|
|
7123
|
+
task.parentTaskId IS NULL
|
|
7124
|
+
AND COALESCE(task.source, '') = 'workflow'
|
|
7125
|
+
AND task.requestedByUserId IS NULL
|
|
7126
|
+
AND EXISTS (
|
|
7127
|
+
SELECT 1
|
|
7128
|
+
FROM workflow_runs run
|
|
7129
|
+
WHERE run.id = task.workflowRunId
|
|
7130
|
+
AND run.triggerType = 'schedule'
|
|
7131
|
+
AND run.created_by IS NULL
|
|
7132
|
+
)
|
|
7133
|
+
)
|
|
7134
|
+
OR (
|
|
7135
|
+
COALESCE(task.source, '') = 'system'
|
|
7136
|
+
AND parent.id IS NOT NULL
|
|
7137
|
+
AND parent.requestedByUserId IS NULL
|
|
7138
|
+
)
|
|
7139
|
+
|
|
7140
|
+
UNION
|
|
7141
|
+
|
|
7142
|
+
SELECT child.id
|
|
7143
|
+
FROM agent_tasks child
|
|
7144
|
+
JOIN human_free_tasks parent ON child.parentTaskId = parent.id
|
|
7145
|
+
WHERE child.requestedByUserId IS NULL
|
|
7146
|
+
OR child.requestedByUserIdInherited = 1
|
|
7147
|
+
)`;
|
|
7148
|
+
const HUMAN_FREE_SQL = "EXISTS (SELECT 1 FROM human_free_tasks WHERE id = t.id)";
|
|
7149
|
+
const ROOT_HUMAN_FREE_SQL = `(
|
|
7150
|
+
COALESCE(t.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
|
|
7151
|
+
OR COALESCE(t.tags, '[]') LIKE '%"heartbeat"%'
|
|
7152
|
+
OR (COALESCE(t.source, '') = 'schedule' AND t.requestedByUserId IS NULL)
|
|
7153
|
+
OR (
|
|
7154
|
+
COALESCE(t.source, '') = 'workflow'
|
|
7155
|
+
AND t.requestedByUserId IS NULL
|
|
7156
|
+
AND EXISTS (
|
|
7157
|
+
SELECT 1
|
|
7158
|
+
FROM workflow_runs run
|
|
7159
|
+
WHERE run.id = t.workflowRunId
|
|
7160
|
+
AND run.triggerType = 'schedule'
|
|
7161
|
+
AND run.created_by IS NULL
|
|
7162
|
+
)
|
|
7163
|
+
)
|
|
7164
|
+
)`;
|
|
7165
|
+
|
|
6910
7166
|
export function getSessionCostSummary(opts: {
|
|
6911
7167
|
startDate?: string;
|
|
6912
7168
|
endDate?: string;
|
|
@@ -6925,6 +7181,15 @@ export function getSessionCostSummary(opts: {
|
|
|
6925
7181
|
// through the task (same shape as `getDailySpendForUser`). Every column is
|
|
6926
7182
|
// `sc.`-qualified because `createdAt`/`agentId` exist on both sides.
|
|
6927
7183
|
const from = "FROM session_costs sc LEFT JOIN agent_tasks t ON t.id = sc.taskId";
|
|
7184
|
+
|
|
7185
|
+
// Structurally-human-free: the swarm maintaining itself, with no human
|
|
7186
|
+
// requester by construction — heartbeat/boot-triage tasks, scheduled runs
|
|
7187
|
+
// without a human creator (including workflow roots launched by such a
|
|
7188
|
+
// schedule), and `source='system'` follow-ups whose parent itself has no
|
|
7189
|
+
// human requester. That classification propagates through descendants while
|
|
7190
|
+
// they remain unattributed, so autonomous fan-out cannot leak back into the
|
|
7191
|
+
// denominator. An explicitly attributed child is an independent handoff and
|
|
7192
|
+
// stops propagation down that branch.
|
|
6928
7193
|
const conditions: string[] = [];
|
|
6929
7194
|
const params: string[] = [];
|
|
6930
7195
|
|
|
@@ -6941,9 +7206,9 @@ export function getSessionCostSummary(opts: {
|
|
|
6941
7206
|
params.push(opts.agentId);
|
|
6942
7207
|
}
|
|
6943
7208
|
if (opts.userId === UNATTRIBUTED_USER_ID) {
|
|
6944
|
-
conditions.push(
|
|
7209
|
+
conditions.push(`(t.requestedByUserId IS NULL OR ${HUMAN_FREE_SQL})`);
|
|
6945
7210
|
} else if (opts.userId) {
|
|
6946
|
-
conditions.push(
|
|
7211
|
+
conditions.push(`(t.requestedByUserId = ? AND NOT ${HUMAN_FREE_SQL})`);
|
|
6947
7212
|
params.push(opts.userId);
|
|
6948
7213
|
}
|
|
6949
7214
|
|
|
@@ -6959,11 +7224,15 @@ export function getSessionCostSummary(opts: {
|
|
|
6959
7224
|
totalDurationMs: number;
|
|
6960
7225
|
totalSessions: number;
|
|
6961
7226
|
attributedCostUsd: number;
|
|
7227
|
+
attributableCostUsd: number;
|
|
7228
|
+
excludedCostUsd: number;
|
|
7229
|
+
excludedTaskCount: number;
|
|
6962
7230
|
};
|
|
6963
7231
|
|
|
6964
7232
|
const totalsRow = getDb()
|
|
6965
7233
|
.prepare<TotalsRow, string[]>(
|
|
6966
|
-
`
|
|
7234
|
+
`WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
|
|
7235
|
+
SELECT
|
|
6967
7236
|
COALESCE(SUM(sc.totalCostUsd), 0) as totalCostUsd,
|
|
6968
7237
|
COALESCE(SUM(sc.inputTokens), 0) as totalInputTokens,
|
|
6969
7238
|
COALESCE(SUM(sc.outputTokens), 0) as totalOutputTokens,
|
|
@@ -6971,8 +7240,13 @@ export function getSessionCostSummary(opts: {
|
|
|
6971
7240
|
COALESCE(SUM(sc.cacheWriteTokens), 0) as totalCacheWriteTokens,
|
|
6972
7241
|
COALESCE(SUM(sc.durationMs), 0) as totalDurationMs,
|
|
6973
7242
|
COUNT(*) as totalSessions,
|
|
6974
|
-
COALESCE(SUM(CASE WHEN t.requestedByUserId IS NOT NULL
|
|
6975
|
-
as attributedCostUsd
|
|
7243
|
+
COALESCE(SUM(CASE WHEN t.requestedByUserId IS NOT NULL AND NOT ${HUMAN_FREE_SQL}
|
|
7244
|
+
THEN sc.totalCostUsd ELSE 0 END), 0) as attributedCostUsd,
|
|
7245
|
+
COALESCE(SUM(CASE WHEN NOT ${HUMAN_FREE_SQL}
|
|
7246
|
+
THEN sc.totalCostUsd ELSE 0 END), 0) as attributableCostUsd,
|
|
7247
|
+
COALESCE(SUM(CASE WHEN ${HUMAN_FREE_SQL}
|
|
7248
|
+
THEN sc.totalCostUsd ELSE 0 END), 0) as excludedCostUsd,
|
|
7249
|
+
COUNT(DISTINCT CASE WHEN ${HUMAN_FREE_SQL} THEN t.id END) as excludedTaskCount
|
|
6976
7250
|
${from} ${where}`,
|
|
6977
7251
|
)
|
|
6978
7252
|
.get(...params);
|
|
@@ -6993,6 +7267,9 @@ export function getSessionCostSummary(opts: {
|
|
|
6993
7267
|
totalSessions: 0,
|
|
6994
7268
|
avgCostPerSession: 0,
|
|
6995
7269
|
attributedCostUsd: 0,
|
|
7270
|
+
attributableCostUsd: 0,
|
|
7271
|
+
excludedCostUsd: 0,
|
|
7272
|
+
excludedTaskCount: 0,
|
|
6996
7273
|
};
|
|
6997
7274
|
|
|
6998
7275
|
// Daily breakdown
|
|
@@ -7010,7 +7287,8 @@ export function getSessionCostSummary(opts: {
|
|
|
7010
7287
|
},
|
|
7011
7288
|
string[]
|
|
7012
7289
|
>(
|
|
7013
|
-
`
|
|
7290
|
+
`WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
|
|
7291
|
+
SELECT
|
|
7014
7292
|
DATE(sc.createdAt) as date,
|
|
7015
7293
|
COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
|
|
7016
7294
|
COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
|
|
@@ -7038,7 +7316,8 @@ export function getSessionCostSummary(opts: {
|
|
|
7038
7316
|
},
|
|
7039
7317
|
string[]
|
|
7040
7318
|
>(
|
|
7041
|
-
`
|
|
7319
|
+
`WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
|
|
7320
|
+
SELECT
|
|
7042
7321
|
sc.agentId as agentId,
|
|
7043
7322
|
COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
|
|
7044
7323
|
COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
|
|
@@ -7052,22 +7331,22 @@ export function getSessionCostSummary(opts: {
|
|
|
7052
7331
|
.all(...params);
|
|
7053
7332
|
}
|
|
7054
7333
|
|
|
7055
|
-
// Per-requester breakdown.
|
|
7056
|
-
//
|
|
7057
|
-
// must stay visible rather than being folded into a person.
|
|
7334
|
+
// Per-requester breakdown. Structurally-human-free work is assigned to the
|
|
7335
|
+
// autonomous bucket even if a stale requester id survives on the task.
|
|
7058
7336
|
let byUser: SessionCostByUserRow[] = [];
|
|
7059
7337
|
if (groupBy === "user" || groupBy === "both") {
|
|
7060
7338
|
byUser = getDb()
|
|
7061
7339
|
.prepare<SessionCostByUserRow, string[]>(
|
|
7062
|
-
`
|
|
7063
|
-
|
|
7340
|
+
`WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
|
|
7341
|
+
SELECT
|
|
7342
|
+
CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END as userId,
|
|
7064
7343
|
COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
|
|
7065
7344
|
COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
|
|
7066
7345
|
COALESCE(SUM(sc.outputTokens), 0) as outputTokens,
|
|
7067
7346
|
COUNT(DISTINCT sc.taskId) as tasks,
|
|
7068
7347
|
COALESCE(SUM(sc.durationMs), 0) as durationMs
|
|
7069
7348
|
${from} ${where}
|
|
7070
|
-
GROUP BY t.requestedByUserId
|
|
7349
|
+
GROUP BY CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END
|
|
7071
7350
|
ORDER BY costUsd DESC`,
|
|
7072
7351
|
)
|
|
7073
7352
|
.all(...params);
|
|
@@ -7076,6 +7355,235 @@ export function getSessionCostSummary(opts: {
|
|
|
7076
7355
|
return { totals, daily, byAgent, byUser };
|
|
7077
7356
|
}
|
|
7078
7357
|
|
|
7358
|
+
// --- Per-person attribution (four-metric view) ---
|
|
7359
|
+
|
|
7360
|
+
export interface AttributionByPersonRow {
|
|
7361
|
+
userId: string;
|
|
7362
|
+
/** Root tasks (`parentTaskId IS NULL`) only — counting fan-out children would let
|
|
7363
|
+
* whoever triggers the biggest decomposition win by accident. */
|
|
7364
|
+
problemsInitiated: number;
|
|
7365
|
+
/** Roots that completed AND show shippable evidence — see `getAttributionByPerson` doc. */
|
|
7366
|
+
problemsShipped: number;
|
|
7367
|
+
/** Distinct agents engaged across the person's entire task tree (not root-scoped —
|
|
7368
|
+
* a root that fans out to nine agents is nine agents of reach). */
|
|
7369
|
+
agentsReached: number;
|
|
7370
|
+
reposReached: number;
|
|
7371
|
+
surfacesReached: number;
|
|
7372
|
+
/**
|
|
7373
|
+
* Share of root tasks completed without a re-dispatch or a human correction.
|
|
7374
|
+
* Deliberately `null`: distinguishing a legitimate multi-agent fan-out from a
|
|
7375
|
+
* correction-driven re-dispatch isn't computable from a single query pass
|
|
7376
|
+
* (would need to inspect *why* a child task was created, which isn't a stored
|
|
7377
|
+
* fact), and a task-count-based proxy would misclassify normal delegation as
|
|
7378
|
+
* rework. Render as "not yet computed" — do not invent a stand-in.
|
|
7379
|
+
*/
|
|
7380
|
+
firstPassYield: null;
|
|
7381
|
+
}
|
|
7382
|
+
|
|
7383
|
+
/**
|
|
7384
|
+
* Four metrics per human requester, reported side by side — never summed into
|
|
7385
|
+
* a composite score. Scope is root tasks (`problemsInitiated`/`problemsShipped`)
|
|
7386
|
+
* or the person's entire task tree (`*Reached`); `requestedByUserId IS NULL`
|
|
7387
|
+
* (autonomous work) and structurally-human-free rows with a stale/inherited
|
|
7388
|
+
* requester (see `HUMAN_FREE_SQL` in `getSessionCostSummary`) are excluded —
|
|
7389
|
+
* neither belongs to a person.
|
|
7390
|
+
*
|
|
7391
|
+
* "Problems shipped" detection walks each root's full task tree, preferring a
|
|
7392
|
+
* `task_attachments` row (`kind='url'` matching a GitHub PR or GitLab MR URL,
|
|
7393
|
+
* or `kind='page'` for a published artifact) and falling back to the same
|
|
7394
|
+
* provider-specific URL match on any task output. GitLab matching is host
|
|
7395
|
+
* agnostic so self-hosted instances are covered. This does NOT detect a closed ticket (no
|
|
7396
|
+
* Linear/Jira issue-state table exists locally) — "shipped" undercounts
|
|
7397
|
+
* ticket-only outcomes until the real artifacts join lands.
|
|
7398
|
+
*/
|
|
7399
|
+
export function getAttributionByPerson(opts: {
|
|
7400
|
+
startDate?: string;
|
|
7401
|
+
endDate?: string;
|
|
7402
|
+
}): AttributionByPersonRow[] {
|
|
7403
|
+
const conditions: string[] = ["t.requestedByUserId IS NOT NULL"];
|
|
7404
|
+
const params: string[] = [];
|
|
7405
|
+
if (opts.startDate) {
|
|
7406
|
+
conditions.push("t.createdAt >= ?");
|
|
7407
|
+
params.push(opts.startDate);
|
|
7408
|
+
}
|
|
7409
|
+
if (opts.endDate) {
|
|
7410
|
+
// A date-only value represents the whole UTC day. Preserve inclusive
|
|
7411
|
+
// timestamp semantics for callers that provide an exact instant.
|
|
7412
|
+
const dateOnlyEnd = /^\d{4}-\d{2}-\d{2}$/.test(opts.endDate)
|
|
7413
|
+
? new Date(`${opts.endDate}T00:00:00.000Z`)
|
|
7414
|
+
: null;
|
|
7415
|
+
if (
|
|
7416
|
+
dateOnlyEnd &&
|
|
7417
|
+
!Number.isNaN(dateOnlyEnd.getTime()) &&
|
|
7418
|
+
dateOnlyEnd.toISOString().slice(0, 10) === opts.endDate
|
|
7419
|
+
) {
|
|
7420
|
+
dateOnlyEnd.setUTCDate(dateOnlyEnd.getUTCDate() + 1);
|
|
7421
|
+
conditions.push("t.createdAt < ?");
|
|
7422
|
+
params.push(dateOnlyEnd.toISOString());
|
|
7423
|
+
} else {
|
|
7424
|
+
conditions.push("t.createdAt <= ?");
|
|
7425
|
+
params.push(opts.endDate);
|
|
7426
|
+
}
|
|
7427
|
+
}
|
|
7428
|
+
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
7429
|
+
|
|
7430
|
+
type RootRow = { userId: string; initiated: number; shipped: number };
|
|
7431
|
+
const rootRows = getDb()
|
|
7432
|
+
.prepare<RootRow, string[]>(
|
|
7433
|
+
`WITH RECURSIVE selected_roots(id, requestedByUserId, status, output) AS (
|
|
7434
|
+
SELECT t.id, t.requestedByUserId, t.status, t.output
|
|
7435
|
+
FROM agent_tasks t
|
|
7436
|
+
${where} AND t.parentTaskId IS NULL AND NOT ${ROOT_HUMAN_FREE_SQL}
|
|
7437
|
+
),
|
|
7438
|
+
task_tree(rootId, taskId, output) AS (
|
|
7439
|
+
SELECT id, id, output
|
|
7440
|
+
FROM selected_roots
|
|
7441
|
+
|
|
7442
|
+
UNION ALL
|
|
7443
|
+
|
|
7444
|
+
SELECT tree.rootId, child.id, child.output
|
|
7445
|
+
FROM agent_tasks child
|
|
7446
|
+
JOIN task_tree tree ON child.parentTaskId = tree.taskId
|
|
7447
|
+
)
|
|
7448
|
+
SELECT
|
|
7449
|
+
t.requestedByUserId as userId,
|
|
7450
|
+
COUNT(*) as initiated,
|
|
7451
|
+
SUM(CASE WHEN t.status = 'completed' AND (
|
|
7452
|
+
EXISTS (
|
|
7453
|
+
SELECT 1
|
|
7454
|
+
FROM task_tree tree
|
|
7455
|
+
JOIN task_attachments ta ON ta.task_id = tree.taskId
|
|
7456
|
+
WHERE tree.rootId = t.id
|
|
7457
|
+
AND ta.kind = 'url'
|
|
7458
|
+
AND (
|
|
7459
|
+
ta.url LIKE '%github.com/%/pull/%'
|
|
7460
|
+
OR ta.url LIKE '%/-/merge_requests/%'
|
|
7461
|
+
)
|
|
7462
|
+
)
|
|
7463
|
+
OR EXISTS (
|
|
7464
|
+
SELECT 1 FROM task_attachments ta
|
|
7465
|
+
JOIN task_tree tree ON tree.taskId = ta.task_id
|
|
7466
|
+
WHERE tree.rootId = t.id AND ta.kind = 'page'
|
|
7467
|
+
)
|
|
7468
|
+
OR EXISTS (
|
|
7469
|
+
SELECT 1 FROM task_tree tree
|
|
7470
|
+
WHERE tree.rootId = t.id
|
|
7471
|
+
AND (
|
|
7472
|
+
tree.output LIKE '%github.com/%/pull/%'
|
|
7473
|
+
OR tree.output LIKE '%/-/merge_requests/%'
|
|
7474
|
+
)
|
|
7475
|
+
)
|
|
7476
|
+
) THEN 1 ELSE 0 END) as shipped
|
|
7477
|
+
FROM selected_roots t
|
|
7478
|
+
GROUP BY t.requestedByUserId`,
|
|
7479
|
+
)
|
|
7480
|
+
.all(...params);
|
|
7481
|
+
|
|
7482
|
+
type ReachRow = {
|
|
7483
|
+
userId: string;
|
|
7484
|
+
agentsReached: number;
|
|
7485
|
+
reposReached: number;
|
|
7486
|
+
surfacesReached: number;
|
|
7487
|
+
};
|
|
7488
|
+
const reachRows = getDb()
|
|
7489
|
+
.prepare<ReachRow, string[]>(
|
|
7490
|
+
`WITH RECURSIVE report_tasks AS (
|
|
7491
|
+
SELECT t.*
|
|
7492
|
+
FROM agent_tasks t
|
|
7493
|
+
${where}
|
|
7494
|
+
),
|
|
7495
|
+
task_ancestry(
|
|
7496
|
+
taskId, id, parentTaskId, requestedByUserId, requestedByUserIdInherited,
|
|
7497
|
+
taskType, tags, source, workflowRunId
|
|
7498
|
+
) AS (
|
|
7499
|
+
SELECT
|
|
7500
|
+
id, id, parentTaskId, requestedByUserId, requestedByUserIdInherited,
|
|
7501
|
+
taskType, tags, source, workflowRunId
|
|
7502
|
+
FROM report_tasks
|
|
7503
|
+
|
|
7504
|
+
UNION ALL
|
|
7505
|
+
|
|
7506
|
+
SELECT
|
|
7507
|
+
child.taskId, parent.id, parent.parentTaskId, parent.requestedByUserId,
|
|
7508
|
+
parent.requestedByUserIdInherited, parent.taskType, parent.tags,
|
|
7509
|
+
parent.source, parent.workflowRunId
|
|
7510
|
+
FROM agent_tasks parent
|
|
7511
|
+
JOIN task_ancestry child ON parent.id = child.parentTaskId
|
|
7512
|
+
WHERE child.requestedByUserId IS NULL
|
|
7513
|
+
OR child.requestedByUserIdInherited = 1
|
|
7514
|
+
),
|
|
7515
|
+
human_free_report_tasks(id) AS (
|
|
7516
|
+
SELECT DISTINCT ancestor.taskId
|
|
7517
|
+
FROM task_ancestry ancestor
|
|
7518
|
+
LEFT JOIN agent_tasks parent ON parent.id = ancestor.parentTaskId
|
|
7519
|
+
WHERE COALESCE(ancestor.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
|
|
7520
|
+
OR COALESCE(ancestor.tags, '[]') LIKE '%"heartbeat"%'
|
|
7521
|
+
OR (COALESCE(ancestor.source, '') = 'schedule' AND ancestor.requestedByUserId IS NULL)
|
|
7522
|
+
OR (
|
|
7523
|
+
ancestor.parentTaskId IS NULL
|
|
7524
|
+
AND COALESCE(ancestor.source, '') = 'workflow'
|
|
7525
|
+
AND ancestor.requestedByUserId IS NULL
|
|
7526
|
+
AND EXISTS (
|
|
7527
|
+
SELECT 1
|
|
7528
|
+
FROM workflow_runs run
|
|
7529
|
+
WHERE run.id = ancestor.workflowRunId
|
|
7530
|
+
AND run.triggerType = 'schedule'
|
|
7531
|
+
AND run.created_by IS NULL
|
|
7532
|
+
)
|
|
7533
|
+
)
|
|
7534
|
+
OR (
|
|
7535
|
+
COALESCE(ancestor.source, '') = 'system'
|
|
7536
|
+
AND parent.id IS NOT NULL
|
|
7537
|
+
AND parent.requestedByUserId IS NULL
|
|
7538
|
+
)
|
|
7539
|
+
)
|
|
7540
|
+
SELECT
|
|
7541
|
+
t.requestedByUserId as userId,
|
|
7542
|
+
COUNT(DISTINCT t.agentId) as agentsReached,
|
|
7543
|
+
COUNT(DISTINCT t.vcsRepo) as reposReached,
|
|
7544
|
+
COUNT(DISTINCT t.source) as surfacesReached
|
|
7545
|
+
FROM report_tasks t
|
|
7546
|
+
WHERE NOT EXISTS (SELECT 1 FROM human_free_report_tasks WHERE id = t.id)
|
|
7547
|
+
GROUP BY t.requestedByUserId`,
|
|
7548
|
+
)
|
|
7549
|
+
.all(...params);
|
|
7550
|
+
|
|
7551
|
+
const byUser = new Map<string, AttributionByPersonRow>();
|
|
7552
|
+
for (const r of rootRows) {
|
|
7553
|
+
byUser.set(r.userId, {
|
|
7554
|
+
userId: r.userId,
|
|
7555
|
+
problemsInitiated: r.initiated,
|
|
7556
|
+
problemsShipped: r.shipped,
|
|
7557
|
+
agentsReached: 0,
|
|
7558
|
+
reposReached: 0,
|
|
7559
|
+
surfacesReached: 0,
|
|
7560
|
+
firstPassYield: null,
|
|
7561
|
+
});
|
|
7562
|
+
}
|
|
7563
|
+
for (const r of reachRows) {
|
|
7564
|
+
const existing = byUser.get(r.userId);
|
|
7565
|
+
if (existing) {
|
|
7566
|
+
existing.agentsReached = r.agentsReached;
|
|
7567
|
+
existing.reposReached = r.reposReached;
|
|
7568
|
+
existing.surfacesReached = r.surfacesReached;
|
|
7569
|
+
} else {
|
|
7570
|
+
// Reach with no root task in-window (e.g. only fan-out children whose
|
|
7571
|
+
// root predates the window) — still a real person row.
|
|
7572
|
+
byUser.set(r.userId, {
|
|
7573
|
+
userId: r.userId,
|
|
7574
|
+
problemsInitiated: 0,
|
|
7575
|
+
problemsShipped: 0,
|
|
7576
|
+
agentsReached: r.agentsReached,
|
|
7577
|
+
reposReached: r.reposReached,
|
|
7578
|
+
surfacesReached: r.surfacesReached,
|
|
7579
|
+
firstPassYield: null,
|
|
7580
|
+
});
|
|
7581
|
+
}
|
|
7582
|
+
}
|
|
7583
|
+
|
|
7584
|
+
return Array.from(byUser.values());
|
|
7585
|
+
}
|
|
7586
|
+
|
|
7079
7587
|
// --- Dashboard cost summary (P4) ---
|
|
7080
7588
|
|
|
7081
7589
|
export interface DashboardCostSummary {
|
|
@@ -8565,6 +9073,7 @@ export function insertActiveSession(session: {
|
|
|
8565
9073
|
inboxMessageId?: string;
|
|
8566
9074
|
taskDescription?: string;
|
|
8567
9075
|
runnerSessionId?: string;
|
|
9076
|
+
runtimeInstanceId?: string;
|
|
8568
9077
|
}): ActiveSession {
|
|
8569
9078
|
const id = crypto.randomUUID();
|
|
8570
9079
|
const now = new Date().toISOString();
|
|
@@ -8580,12 +9089,13 @@ export function insertActiveSession(session: {
|
|
|
8580
9089
|
string | null,
|
|
8581
9090
|
string | null,
|
|
8582
9091
|
string | null,
|
|
9092
|
+
string | null,
|
|
8583
9093
|
string,
|
|
8584
9094
|
string,
|
|
8585
9095
|
]
|
|
8586
9096
|
>(
|
|
8587
|
-
`INSERT INTO active_sessions (id, agentId, taskId, triggerType, inboxMessageId, taskDescription, runnerSessionId, startedAt, lastHeartbeatAt)
|
|
8588
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9097
|
+
`INSERT INTO active_sessions (id, agentId, taskId, triggerType, inboxMessageId, taskDescription, runnerSessionId, runtimeInstanceId, startedAt, lastHeartbeatAt)
|
|
9098
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8589
9099
|
RETURNING *`,
|
|
8590
9100
|
)
|
|
8591
9101
|
.get(
|
|
@@ -8596,6 +9106,7 @@ export function insertActiveSession(session: {
|
|
|
8596
9106
|
session.inboxMessageId ?? null,
|
|
8597
9107
|
session.taskDescription ?? null,
|
|
8598
9108
|
session.runnerSessionId ?? null,
|
|
9109
|
+
session.runtimeInstanceId ?? null,
|
|
8599
9110
|
now,
|
|
8600
9111
|
now,
|
|
8601
9112
|
);
|
|
@@ -9208,9 +9719,11 @@ type WorkflowRunRow = {
|
|
|
9208
9719
|
id: string;
|
|
9209
9720
|
workflowId: string;
|
|
9210
9721
|
status: string;
|
|
9722
|
+
triggerType: string;
|
|
9211
9723
|
triggerData: string | null;
|
|
9212
9724
|
context: string | null;
|
|
9213
9725
|
error: string | null;
|
|
9726
|
+
created_by: string | null;
|
|
9214
9727
|
startedAt: string;
|
|
9215
9728
|
lastUpdatedAt: string;
|
|
9216
9729
|
finishedAt: string | null;
|
|
@@ -9224,6 +9737,7 @@ function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
|
|
|
9224
9737
|
triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
|
|
9225
9738
|
context: row.context ? (JSON.parse(row.context) as Record<string, unknown>) : undefined,
|
|
9226
9739
|
error: row.error ?? undefined,
|
|
9740
|
+
createdBy: row.created_by ?? undefined,
|
|
9227
9741
|
startedAt: normalizeDateRequired(row.startedAt),
|
|
9228
9742
|
lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
|
|
9229
9743
|
finishedAt: normalizeDate(row.finishedAt) ?? undefined,
|
|
@@ -9233,14 +9747,23 @@ function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
|
|
|
9233
9747
|
export function createWorkflowRun(data: {
|
|
9234
9748
|
id: string;
|
|
9235
9749
|
workflowId: string;
|
|
9750
|
+
triggerType?: "schedule" | "manual" | "event" | "api";
|
|
9236
9751
|
triggerData?: unknown;
|
|
9752
|
+
createdBy?: string;
|
|
9237
9753
|
}): WorkflowRun {
|
|
9238
9754
|
const now = new Date().toISOString();
|
|
9239
9755
|
const row = getDb()
|
|
9240
|
-
.prepare<WorkflowRunRow, [string, string, string, string | null]>(
|
|
9241
|
-
`INSERT INTO workflow_runs (id, workflowId, startedAt, triggerData) VALUES (?, ?, ?, ?) RETURNING *`,
|
|
9756
|
+
.prepare<WorkflowRunRow, [string, string, string, string, string | null, string | null]>(
|
|
9757
|
+
`INSERT INTO workflow_runs (id, workflowId, triggerType, startedAt, triggerData, created_by) VALUES (?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
9242
9758
|
)
|
|
9243
|
-
.get(
|
|
9759
|
+
.get(
|
|
9760
|
+
data.id,
|
|
9761
|
+
data.workflowId,
|
|
9762
|
+
data.triggerType ?? "manual",
|
|
9763
|
+
now,
|
|
9764
|
+
data.triggerData ? JSON.stringify(data.triggerData) : null,
|
|
9765
|
+
data.createdBy ?? null,
|
|
9766
|
+
);
|
|
9244
9767
|
if (!row) throw new Error("Failed to create workflow run");
|
|
9245
9768
|
return rowToWorkflowRun(row);
|
|
9246
9769
|
}
|
|
@@ -13161,13 +13684,104 @@ export function updateUser(
|
|
|
13161
13684
|
return row ? rowToUser(row) : null;
|
|
13162
13685
|
}
|
|
13163
13686
|
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
return
|
|
13687
|
+
type UserReferenceRow = {
|
|
13688
|
+
tableName: string;
|
|
13689
|
+
columnName: string;
|
|
13690
|
+
};
|
|
13691
|
+
|
|
13692
|
+
function quoteSqlIdentifier(identifier: string): string {
|
|
13693
|
+
return `"${identifier.replaceAll('"', '""')}"`;
|
|
13694
|
+
}
|
|
13695
|
+
|
|
13696
|
+
export function deleteUser(id: string, replacementUserId?: string): boolean {
|
|
13697
|
+
if (replacementUserId === id) {
|
|
13698
|
+
throw new Error("Replacement user must differ from deleted user");
|
|
13699
|
+
}
|
|
13700
|
+
|
|
13701
|
+
const database = getDb();
|
|
13702
|
+
return database.transaction(() => {
|
|
13703
|
+
const userExists = database
|
|
13704
|
+
.prepare<{ present: number }, [string]>(
|
|
13705
|
+
"SELECT EXISTS(SELECT 1 FROM users WHERE id = ?) AS present",
|
|
13706
|
+
)
|
|
13707
|
+
.get(id)?.present;
|
|
13708
|
+
if (!userExists) return false;
|
|
13709
|
+
|
|
13710
|
+
if (replacementUserId) {
|
|
13711
|
+
const replacementExists = database
|
|
13712
|
+
.prepare<{ present: number }, [string]>(
|
|
13713
|
+
"SELECT EXISTS(SELECT 1 FROM users WHERE id = ?) AS present",
|
|
13714
|
+
)
|
|
13715
|
+
.get(replacementUserId)?.present;
|
|
13716
|
+
if (!replacementExists) throw new Error("Replacement user not found");
|
|
13717
|
+
}
|
|
13718
|
+
|
|
13719
|
+
// Preserve rows that carry user attribution but lack ON DELETE semantics.
|
|
13720
|
+
// Schema discovery keeps this correct as new nullable user audit columns are added.
|
|
13721
|
+
// Migration 103 accidentally dropped the scheduled_tasks audit FKs while
|
|
13722
|
+
// recreating that table, so discover those two known logical references as
|
|
13723
|
+
// well until the table is next rebuilt with its original constraints.
|
|
13724
|
+
const references = database
|
|
13725
|
+
.prepare<UserReferenceRow, []>(
|
|
13726
|
+
`SELECT tables.name AS tableName, foreign_keys."from" AS columnName
|
|
13727
|
+
FROM sqlite_schema AS tables
|
|
13728
|
+
JOIN pragma_foreign_key_list(tables.name) AS foreign_keys
|
|
13729
|
+
JOIN pragma_table_info(tables.name) AS columns
|
|
13730
|
+
ON columns.name = foreign_keys."from"
|
|
13731
|
+
WHERE tables.type = 'table'
|
|
13732
|
+
AND foreign_keys."table" = 'users'
|
|
13733
|
+
AND foreign_keys.on_delete IN ('NO ACTION', 'RESTRICT')
|
|
13734
|
+
AND columns."notnull" = 0
|
|
13735
|
+
UNION ALL
|
|
13736
|
+
SELECT 'scheduled_tasks' AS tableName, columns.name AS columnName
|
|
13737
|
+
FROM pragma_table_info('scheduled_tasks') AS columns
|
|
13738
|
+
WHERE columns.name IN ('created_by', 'updated_by')
|
|
13739
|
+
AND columns."notnull" = 0
|
|
13740
|
+
AND NOT EXISTS (
|
|
13741
|
+
SELECT 1
|
|
13742
|
+
FROM pragma_foreign_key_list('scheduled_tasks') AS foreign_keys
|
|
13743
|
+
WHERE foreign_keys."from" = columns.name
|
|
13744
|
+
AND foreign_keys."table" = 'users'
|
|
13745
|
+
)`,
|
|
13746
|
+
)
|
|
13747
|
+
.all();
|
|
13748
|
+
const replacement = replacementUserId ?? null;
|
|
13749
|
+
for (const reference of references) {
|
|
13750
|
+
const table = quoteSqlIdentifier(reference.tableName);
|
|
13751
|
+
const column = quoteSqlIdentifier(reference.columnName);
|
|
13752
|
+
database
|
|
13753
|
+
.prepare<unknown, [string | null, string]>(
|
|
13754
|
+
`UPDATE ${table} SET ${column} = ? WHERE ${column} = ?`,
|
|
13755
|
+
)
|
|
13756
|
+
.run(replacement, id);
|
|
13757
|
+
}
|
|
13758
|
+
|
|
13759
|
+
// Workflow context is persisted JSON rather than a relational column, but
|
|
13760
|
+
// it exposes the same requester identity to downstream interpolation. Keep
|
|
13761
|
+
// it consistent with workflow_runs.created_by inside this transaction.
|
|
13762
|
+
if (replacementUserId) {
|
|
13763
|
+
database
|
|
13764
|
+
.prepare<unknown, [string, string]>(
|
|
13765
|
+
`UPDATE workflow_runs
|
|
13766
|
+
SET context = json_set(context, '$.swarm.requestedByUserId', ?)
|
|
13767
|
+
WHERE json_valid(context)
|
|
13768
|
+
AND json_extract(context, '$.swarm.requestedByUserId') = ?`,
|
|
13769
|
+
)
|
|
13770
|
+
.run(replacementUserId, id);
|
|
13771
|
+
} else {
|
|
13772
|
+
database
|
|
13773
|
+
.prepare<unknown, [string]>(
|
|
13774
|
+
`UPDATE workflow_runs
|
|
13775
|
+
SET context = json_remove(context, '$.swarm.requestedByUserId')
|
|
13776
|
+
WHERE json_valid(context)
|
|
13777
|
+
AND json_extract(context, '$.swarm.requestedByUserId') = ?`,
|
|
13778
|
+
)
|
|
13779
|
+
.run(id);
|
|
13780
|
+
}
|
|
13781
|
+
|
|
13782
|
+
const result = database.prepare("DELETE FROM users WHERE id = ?").run(id);
|
|
13783
|
+
return result.changes > 0;
|
|
13784
|
+
})();
|
|
13171
13785
|
}
|
|
13172
13786
|
|
|
13173
13787
|
// ============================================================================
|