@desplega.ai/agent-swarm 1.161.0 → 1.162.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 +3 -2
- package/dist/{acp-adapter-xx489vvb.js → acp-adapter-74m73ae9.js} +1 -1
- package/dist/{actions-ykjkn0m9.js → actions-4sr14gnt.js} +34 -31
- package/dist/{anthropic-messages-5qx80j9k.js → anthropic-messages-8xxrzky9.js} +22 -26
- package/dist/{app-0hdqa4k1.js → app-pt68fbwp.js} +13 -11
- package/dist/{artifact-3v5t9jtg.js → artifact-fey7dvj9.js} +3 -2
- package/dist/{assistant-am2g5g8a.js → assistant-cv3jytks.js} +37 -34
- package/dist/{azure-openai-responses-fszbkp0e.js → azure-openai-responses-f5cqsyj8.js} +15 -10
- package/dist/{boot-reembed-xy9t7avg.js → boot-reembed-4ht4gqth.js} +13 -11
- package/dist/{boot-reembed-gt5gjkt4.js → boot-reembed-nxpcvpj9.js} +12 -10
- package/dist/{boot-scrub-logs-9x0985y5.js → boot-scrub-logs-k93kyw8k.js} +9 -7
- package/dist/{boot-seeds-trv0d4vg.js → boot-seeds-1skcrs0e.js} +13 -10
- package/dist/{claude-adapter-jb6e1e54.js → claude-adapter-r5g0tzge.js} +23 -20
- package/dist/{claude-managed-setup-d1q9hv2b.js → claude-managed-setup-pkyrgshb.js} +3 -2
- package/dist/{claude-sdk-session-zga700s2.js → claude-sdk-session-jxyxnrwd.js} +5565 -5512
- package/dist/{cli-we0q7t0e.js → cli-2zy95vhm.js} +137 -64
- package/dist/{cli-35pj2yva.js → cli-4cwxn7gp.js} +38 -11
- package/dist/{cli-ad4qxm6j.js → cli-4dcpaefp.js} +2 -2
- package/dist/{cli-82c1pacn.js → cli-4x0csdjq.js} +71 -37
- package/dist/{cli-sqm3r5ps.js → cli-5bcd7sfy.js} +14 -18
- package/dist/{cli-d56tgd45.js → cli-5k6k8ta3.js} +2 -2
- package/dist/{cli-1er0rhz8.js → cli-5xhpb3kv.js} +15 -15
- package/dist/{cli-yrg7c73k.js → cli-7af8yhav.js} +10 -183
- package/dist/{cli-r6fsxc76.js → cli-7tre70zp.js} +513 -961
- package/dist/{cli-bh7jed3k.js → cli-8ev9xqnx.js} +34 -18
- package/dist/cli-bsky8g7w.js +449 -0
- package/dist/{cli-hn04t9db.js → cli-cebmgwsc.js} +14 -10
- package/dist/{cli-cfkrztza.js → cli-cvatsam7.js} +2 -2
- package/dist/{cli-2658f6at.js → cli-cvk6yzw5.js} +6 -8
- package/dist/{cli-ptr8h7kp.js → cli-eg0b3p0k.js} +5 -218
- package/dist/{cli-zb6xbk61.js → cli-g739zqzt.js} +4 -4
- package/dist/{cli-dv6cdjs2.js → cli-gd4favx9.js} +28 -7
- package/dist/{cli-5p3x4zna.js → cli-gpqpmns7.js} +2 -2
- package/dist/{cli-5nbmpy8w.js → cli-hb5c2x5y.js} +1 -1
- package/dist/{cli-jpf7e1zq.js → cli-hmsw1gpy.js} +3 -3
- package/dist/{cli-1cz0c18q.js → cli-jf7wmv1r.js} +2521 -733
- package/dist/{cli-qwsnmjdd.js → cli-jxs2ygaw.js} +168 -61
- package/dist/cli-kdkwd3yr.js +615 -0
- package/dist/{cli-hgmnf38m.js → cli-kh04eqwe.js} +1 -1
- package/dist/{cli-0383bp7f.js → cli-kp42yek0.js} +425 -90
- package/dist/{cli-ybgv4kw2.js → cli-mbssg1xv.js} +9 -2
- package/dist/{cli-jsp14b6c.js → cli-mzfth0bp.js} +38 -1
- package/dist/{cli-8g6mc14k.js → cli-nq7yqw7b.js} +16 -5
- package/dist/{cli-ee8bc46j.js → cli-nt8hecg9.js} +33 -32
- package/dist/{cli-9s68w752.js → cli-p0r8sxj5.js} +1 -1
- package/dist/{cli-58e8yr0d.js → cli-pkd6s2wy.js} +4 -2
- package/dist/{cli-17xpf7cm.js → cli-q24re23n.js} +6 -4
- package/dist/cli-qjjvzwzd.js +222 -0
- package/dist/cli-qrgt5bqf.js +189 -0
- package/dist/{cli-et8m9xsv.js → cli-rfjw9jw2.js} +22 -10
- package/dist/{cli-798vkbks.js → cli-rj0gqwwp.js} +2 -2
- package/dist/{cli-121ph74b.js → cli-rnyzn44p.js} +1 -1
- package/dist/{cli-vnnz5ncg.js → cli-s3hwpt9v.js} +95 -16
- package/dist/{cli-7119ja3w.js → cli-sptrj5f5.js} +2 -2
- package/dist/{cli-a8c82p2t.js → cli-sqvfa090.js} +13 -6
- package/dist/{cli-phfg8vy8.js → cli-swfmp1fx.js} +26 -26
- package/dist/{cli-nsdxtcbg.js → cli-tcen9p5w.js} +8 -5
- package/dist/{cli-9m03pzga.js → cli-vc9bp061.js} +5 -5
- package/dist/{cli-rtx9dygp.js → cli-vxe6qa4h.js} +1 -1
- package/dist/{cli-ecfxwnyb.js → cli-vy3ed88x.js} +1 -1
- package/dist/{cli-0c7ata9h.js → cli-vyqs9ya8.js} +6 -6
- package/dist/cli-wbvnn6tk.js +861 -0
- package/dist/{cli-kgd3p0bm.js → cli-wjgs5wxe.js} +6 -6
- package/dist/cli-wxkkd0e0.js +33 -0
- package/dist/{cli-akfj32vj.js → cli-xqpk3rp7.js} +5 -4
- package/dist/cli-zp38xvtm.js +9487 -0
- package/dist/{cli-3zc55ctx.js → cli-zvhd3f64.js} +2 -2
- package/dist/{cli-ze1d3wzv.js → cli-zxqx25nz.js} +5 -5
- package/dist/cli.js +32 -25
- package/dist/{cloudflare-workers-ai-system-one-k6fbxw9d.js → cloudflare-workers-ai-system-one-k8ddqjkq.js} +8 -6
- package/dist/{codex-adapter-04v97vep.js → codex-adapter-esvmnhn7.js} +13 -13
- package/dist/{codex-hook-20me6byw.js → codex-hook-m73jmqgx.js} +19 -3
- package/dist/{codex-session-runner-ax69mqq3.js → codex-session-runner-ban9excr.js} +13 -13
- package/dist/{commands-18gy08n1.js → commands-75413p2n.js} +9 -7
- package/dist/{db-d2pvjpyt.js → db-7f5pecwg.js} +11 -7
- package/dist/{dispatcher-8wbwyzjg.js → dispatcher-fh9at8nz.js} +33 -30
- package/dist/{dsh-adapter-ysqk9snv.js → dsh-adapter-wpze65d4.js} +3 -3
- package/dist/{e2b-esmt1r16.js → e2b-gjgdjp91.js} +10 -9
- package/dist/{pi-logo-animation-e9a0t68z.js → easter-egg-3d-3w5c2n4c.js} +122 -68
- package/dist/{execute-cmagrbrz.js → execute-f50wg9nh.js} +29 -6
- package/dist/{google-generative-ai-k6n5nbpp.js → google-generative-ai-jxxpnegy.js} +2 -2
- package/dist/{google-vertex-44xxtcvp.js → google-vertex-x7q1angm.js} +5 -5
- package/dist/{handlers-6b69m5kp.js → handlers-w13787bb.js} +36 -33
- package/dist/{hook-srnkt0hb.js → hook-46g1jrvx.js} +28 -15
- package/dist/{hook-k3mydf2n.js → hook-4tpts2ry.js} +20 -17
- package/dist/{http-hrf80ca1.js → http-19pzbfrt.js} +964 -176
- package/dist/{index-9rxdvj6w.js → index-8fd4dm4h.js} +34 -31
- package/dist/{index-1asbaj62.js → index-dqmp15bq.js} +35 -32
- package/dist/{index-45evq4ee.js → index-qb4q1y3e.js} +33 -30
- package/dist/{index-k4nj18qs.js → index-s75wc4wd.js} +12 -10
- package/dist/{index-xmevk2b9.js → index-scptvdxg.js} +27 -25
- package/dist/{keepalive-sfdz6ksp.js → keepalive-xa5ts9cd.js} +11 -9
- package/dist/{lead-ej91e9hw.js → lead-qp4xkvjq.js} +45 -41
- package/dist/{maintenance-80dnd7ke.js → maintenance-rm5885hm.js} +13 -11
- package/dist/{mistral-conversations-pc3ntnxt.js → mistral-conversations-rw7fbt22.js} +4 -4
- package/dist/{node-a5e00qhp.js → node-8zkdq42h.js} +834 -833
- package/dist/{oauth-refresh-sweep-m1etk2mx.js → oauth-refresh-sweep-1csdfebf.js} +10 -8
- package/dist/{onboard-0q8sdfqc.js → onboard-r6b78cs4.js} +10 -10
- package/dist/{openai-codex-responses-n7n683p2.js → openai-codex-responses-efvrgrq3.js} +4 -4
- package/dist/{openai-completions-jpaj5g2b.js → openai-completions-bbbrrz4z.js} +12 -8
- package/dist/{openai-responses-9a3nme9j.js → openai-responses-9jjyrcm1.js} +15 -10
- package/dist/{opencode-adapter-4wcgmvgf.js → opencode-adapter-kb3g6pkf.js} +2 -2
- package/dist/{otel-impl-hnqm9mxq.js → otel-impl-0y9knpky.js} +5 -5
- package/dist/{pi-mono-adapter-d6sqm9wr.js → pi-mono-adapter-z6s1bme2.js} +100 -19
- package/dist/{pricing-refresh-y60z64pm.js → pricing-refresh-mth26tgp.js} +11 -9
- package/dist/{rbac-roles-8pa0754b.js → rbac-roles-kwntb23q.js} +13 -9
- package/dist/rbac-roles-vpk4ebm8.js +45 -0
- package/dist/{registry-v5wytg0s.js → registry-km6t1jd2.js} +1 -1
- package/dist/{render-v2-418s1vj7.js → render-v2-hcm7h00y.js} +13 -11
- package/dist/{runtime-3pknc168.js → runtime-vfjsfsc5.js} +4 -4
- package/dist/{setup-yghdkj4x.js → setup-hbyzd65h.js} +2 -2
- package/dist/{virtual-modules-jcceevt5.js → virtual-modules-9bv7ry91.js} +9 -8
- package/dist/{work-objects-j1sz20v9.js → work-objects-m09sxnhe.js} +36 -33
- package/dist/{worker-d4gcvkjt.js → worker-0h35rvk4.js} +45 -41
- package/openapi.json +491 -12
- package/package.json +13 -6
- package/src/be/api-drain.ts +184 -0
- package/src/be/db/tasks/read.ts +31 -6
- package/src/be/db/tasks/write.ts +5 -4
- package/src/be/db.ts +42 -18
- package/src/be/memory/link-resolver.ts +29 -3
- package/src/be/migrations/192_favorites_agent_fs_path.sql +36 -0
- package/src/be/onboarding.ts +7 -1
- package/src/be/seed-prompt-templates.ts +1 -0
- package/src/be/swarm-config-guard.ts +35 -16
- package/src/be/task-citations.ts +11 -5
- package/src/be/users.ts +3 -0
- package/src/cli.tsx +3 -2
- package/src/comb/agent-fs.ts +39 -0
- package/src/comb/config.ts +37 -0
- package/src/comb/markers.ts +37 -0
- package/src/comb/review-batch.ts +486 -0
- package/src/comb/templates.ts +88 -0
- package/src/commands/context-preamble.ts +4 -1
- package/src/commands/hook.ts +2 -1
- package/src/commands/runner.ts +262 -68
- package/src/commands/templates.ts +10 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/fs/agent-fs-provider.ts +106 -2
- package/src/heartbeat/heartbeat.ts +4 -0
- package/src/hooks/codex-hook.ts +25 -1
- package/src/hooks/hook.ts +13 -0
- package/src/hooks/pr-body-guard.ts +983 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/assets.ts +5 -17
- package/src/http/comb.ts +131 -0
- package/src/http/core.ts +2 -1
- package/src/http/fs.ts +9 -17
- package/src/http/index.ts +23 -9
- package/src/http/onboarding.ts +21 -2
- package/src/http/poll.ts +30 -3
- package/src/http/realtime.ts +62 -0
- package/src/http/request-principal.ts +22 -0
- package/src/http/status.ts +34 -2
- package/src/kv-reserved-namespaces.ts +10 -5
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +123 -0
- package/src/rbac/legacy-policy.ts +1 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/realtime/auth.ts +63 -1
- package/src/realtime/tickets.ts +52 -0
- package/src/realtime/transport.ts +51 -22
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -1
- package/src/telemetry-context.ts +175 -0
- package/src/telemetry-contract/EVENTS.md +81 -0
- package/src/telemetry-contract/SOURCE.json +9 -0
- package/src/telemetry-contract/catalog.json +1335 -0
- package/src/telemetry-contract/index.ts +60 -0
- package/src/telemetry-contract/types.gen.ts +1971 -0
- package/src/telemetry-identity.ts +129 -0
- package/src/telemetry-snapshot.ts +211 -0
- package/src/telemetry-trigger.ts +125 -0
- package/src/telemetry-types.typetest.ts +40 -0
- package/src/telemetry.ts +271 -67
- package/src/tests/api-drain-e2e.test.ts +249 -0
- package/src/tests/api-drain-worker-e2e.test.ts +216 -0
- package/src/tests/api-drain.test.ts +351 -0
- package/src/tests/comb-config-guard.test.ts +38 -0
- package/src/tests/comb-links.test.ts +162 -0
- package/src/tests/comb-markers.test.ts +68 -0
- package/src/tests/comb-review-batch.test.ts +770 -0
- package/src/tests/comb-status.test.ts +152 -0
- package/src/tests/db-task-read-contract.test.ts +2 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/favorites-agent-fs-path.test.ts +226 -0
- package/src/tests/memory-link-resolver.test.ts +34 -0
- package/src/tests/onboarding.test.ts +108 -0
- package/src/tests/pi-mono-adapter.test.ts +265 -2
- package/src/tests/pr-body-leaks.test.ts +789 -0
- package/src/tests/preload.ts +8 -0
- package/src/tests/rbac-engine.test.ts +19 -0
- package/src/tests/realtime-room-auth.test.ts +104 -1
- package/src/tests/realtime-tickets.test.ts +74 -0
- package/src/tests/realtime-transport.test.ts +131 -4
- package/src/tests/runner-api-drain.test.ts +301 -0
- package/src/tests/runner-fallback-output.test.ts +61 -0
- package/src/tests/runner-fenced-json-output.test.ts +178 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/send-task-requested-by.test.ts +133 -15
- package/src/tests/task-lifecycle-telemetry.test.ts +8 -3
- package/src/tests/telemetry-context.test.ts +314 -0
- package/src/tests/telemetry-init.test.ts +51 -19
- package/src/tests/telemetry-snapshot.test.ts +229 -0
- package/src/tests/telemetry-trigger.test.ts +200 -0
- package/src/tests/template-registry-helpers.ts +1 -0
- package/src/tests/workflow-agent-task-retry.test.ts +363 -0
- package/src/tests/workflow-definition-telemetry.test.ts +4 -4
- package/src/tools/poll-task.ts +20 -1
- package/src/tools/send-task.ts +9 -1
- package/src/tools/task-action.ts +11 -0
- package/src/types.ts +2 -1
- package/src/utils/api-drain.ts +24 -0
- package/src/utils/constants.ts +57 -16
- package/src/utils/pr-body-leaks.ts +172 -0
- package/src/utils/task-attachment-links.ts +14 -8
- package/src/workflows/recovery.ts +26 -1
- package/src/workflows/resume.ts +31 -4
- package/src/workflows/task-step-routing.ts +75 -2
- package/tsconfig.json +3 -0
- package/dist/cli-tjf4cn8z.js +0 -9119
- package/dist/cli-tsk3nz8a.js +0 -582
- package/dist/{cli-t6v2ah3r.js → cli-twrh04ap.js} +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.162.0",
|
|
4
4
|
"description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-swarm.dev",
|
|
@@ -82,6 +82,13 @@
|
|
|
82
82
|
"check:ai-toolbox-skills": "bun scripts/sync-ai-toolbox-skills.ts --check",
|
|
83
83
|
"check:skill-sources": "bun scripts/check-skill-sources.ts",
|
|
84
84
|
"check:operator-skill": "bun scripts/check-operator-skill.ts",
|
|
85
|
+
"sync:telemetry-contract": "bun scripts/sync-telemetry-contract.ts",
|
|
86
|
+
"check:telemetry-contract": "bun scripts/sync-telemetry-contract.ts --check",
|
|
87
|
+
"build:telemetry-docs": "bun scripts/gen-telemetry-docs.ts",
|
|
88
|
+
"check:telemetry-docs": "bun scripts/gen-telemetry-docs.ts --check",
|
|
89
|
+
"build:claude-mod": "bun scripts/build-claude-mod.ts",
|
|
90
|
+
"check:claude-mod": "bun scripts/build-claude-mod.ts --check",
|
|
91
|
+
"test:claude-mod": "bash scripts/test-claude-mod.sh",
|
|
85
92
|
"build:script-types": "bun scripts/bundle-script-types.ts",
|
|
86
93
|
"build:extension-types": "bun scripts/bundle-extension-types.ts",
|
|
87
94
|
"build:extension-schema": "bun scripts/build-extension-manifest-schema.ts",
|
|
@@ -187,17 +194,17 @@
|
|
|
187
194
|
"bun": ">=1.3.12"
|
|
188
195
|
},
|
|
189
196
|
"dependencies": {
|
|
190
|
-
"@agentclientprotocol/sdk": "1.
|
|
197
|
+
"@agentclientprotocol/sdk": "1.7.0",
|
|
191
198
|
"@ai-sdk/openai": "^3.0.41",
|
|
192
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
199
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.289",
|
|
193
200
|
"@anthropic-ai/sdk": "^0.131.0",
|
|
194
201
|
"@asteasolutions/zod-to-openapi": "^8.0.0",
|
|
195
202
|
"@aws-sdk/client-bedrock": "3.1048.0",
|
|
196
203
|
"@desplega.ai/business-use": "^0.4.2",
|
|
197
204
|
"@desplega.ai/localtunnel": "^2.2.0",
|
|
198
|
-
"@earendil-works/pi-agent-core": "^1.0.
|
|
199
|
-
"@earendil-works/pi-ai": "^1.0.
|
|
200
|
-
"@earendil-works/pi-coding-agent": "^1.0.
|
|
205
|
+
"@earendil-works/pi-agent-core": "^1.0.2",
|
|
206
|
+
"@earendil-works/pi-ai": "^1.0.2",
|
|
207
|
+
"@earendil-works/pi-coding-agent": "^1.0.2",
|
|
201
208
|
"@inkjs/ui": "^2.0.0",
|
|
202
209
|
"@linear/sdk": "^77.0.0",
|
|
203
210
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API drain: what the API does between SIGTERM and closing its HTTP server.
|
|
3
|
+
*
|
|
4
|
+
* On a deploy the orchestrator stops the API and the workers together, and the
|
|
5
|
+
* workers' own SIGTERM handoff (`POST /api/tasks/:id/supersede`) lands after the
|
|
6
|
+
* API is gone, so every in-flight task falls through to the heartbeat sweep.
|
|
7
|
+
* Draining flips that order: the API keeps serving, dispatches no new work,
|
|
8
|
+
* marks every response with `X-Swarm-Draining: 1` (see `src/utils/api-drain.ts`),
|
|
9
|
+
* and waits, bounded, until the tasks it saw in flight have been handed off.
|
|
10
|
+
*
|
|
11
|
+
* The state is process-local. A new API process starts not draining.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { API_DRAIN_MAX_MS_LIMIT } from "../utils/api-drain";
|
|
15
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
16
|
+
import { getDbClient } from "./db";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default cap on the wait for worker handoffs: `0`, the drain is off. It is
|
|
20
|
+
* opt-in because it makes every live worker supersede its in-flight tasks on
|
|
21
|
+
* any API SIGTERM, including an API-only restart where those tasks would
|
|
22
|
+
* otherwise keep running. A deploy that stops the API and the workers together
|
|
23
|
+
* sets `API_DRAIN_MAX_MS` (e.g. `30000`) to turn it on.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_API_DRAIN_MAX_MS = 0;
|
|
26
|
+
const DRAIN_CHECK_INTERVAL_MS = 500;
|
|
27
|
+
/**
|
|
28
|
+
* A worker pings every few seconds. One silent for longer than this is not
|
|
29
|
+
* expected to hand anything off; the heartbeat sweep owns its tasks.
|
|
30
|
+
*/
|
|
31
|
+
const LIVE_WORKER_WINDOW_MS = 30_000;
|
|
32
|
+
|
|
33
|
+
let drainStartedAt: number | null = null;
|
|
34
|
+
|
|
35
|
+
export function isApiDraining(): boolean {
|
|
36
|
+
return drainStartedAt !== null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Enter the draining state. Returns false when already draining. */
|
|
40
|
+
export function beginApiDrain(now: number = Date.now()): boolean {
|
|
41
|
+
if (drainStartedAt !== null) return false;
|
|
42
|
+
drainStartedAt = now;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Test seam: the state is module-level, so tests reset it between cases. */
|
|
47
|
+
export function resetApiDrainForTesting(): void {
|
|
48
|
+
drainStartedAt = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `API_DRAIN_MAX_MS`, capped at `API_DRAIN_MAX_MS_LIMIT`. Unset, empty, or
|
|
53
|
+
* invalid means `DEFAULT_API_DRAIN_MAX_MS` (`0`, the drain is off).
|
|
54
|
+
*/
|
|
55
|
+
export function resolveApiDrainMaxMs(
|
|
56
|
+
env: Record<string, string | undefined> = process.env,
|
|
57
|
+
): number {
|
|
58
|
+
const raw = env.API_DRAIN_MAX_MS;
|
|
59
|
+
if (raw === undefined || raw.trim() === "") return DEFAULT_API_DRAIN_MAX_MS;
|
|
60
|
+
const parsed = Number(raw);
|
|
61
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_API_DRAIN_MAX_MS;
|
|
62
|
+
return Math.min(Math.floor(parsed), API_DRAIN_MAX_MS_LIMIT);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `in_progress` tasks held by an agent that pinged recently, i.e. one that can still hand off. */
|
|
66
|
+
export async function listLiveInFlightTaskIds(now: number = Date.now()): Promise<string[]> {
|
|
67
|
+
const cutoff = new Date(now - LIVE_WORKER_WINDOW_MS).toISOString();
|
|
68
|
+
const rows = await getDbClient().query<{ id: string }>(
|
|
69
|
+
`SELECT t.id AS id
|
|
70
|
+
FROM agent_tasks t
|
|
71
|
+
JOIN agents a ON a.id = t.agentId
|
|
72
|
+
WHERE t.status = 'in_progress' AND a.status != 'offline' AND a.lastUpdatedAt >= ?`,
|
|
73
|
+
[cutoff],
|
|
74
|
+
);
|
|
75
|
+
return rows.map((row) => row.id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** How many of `taskIds` are still `in_progress`. */
|
|
79
|
+
export async function countStillInProgress(taskIds: string[]): Promise<number> {
|
|
80
|
+
if (taskIds.length === 0) return 0;
|
|
81
|
+
const row = await getDbClient().get<{ n: number }>(
|
|
82
|
+
`SELECT count(*) AS n FROM agent_tasks
|
|
83
|
+
WHERE status = 'in_progress' AND id IN (SELECT value FROM json_each(?))`,
|
|
84
|
+
[JSON.stringify(taskIds)],
|
|
85
|
+
);
|
|
86
|
+
return row?.n ?? 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ApiDrainDeps {
|
|
90
|
+
env?: Record<string, string | undefined>;
|
|
91
|
+
now?: () => number;
|
|
92
|
+
sleep?: (ms: number) => Promise<void>;
|
|
93
|
+
listInFlight?: () => Promise<string[]>;
|
|
94
|
+
countRemaining?: (taskIds: string[]) => Promise<number>;
|
|
95
|
+
log?: (message: string) => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ApiDrainOutcome {
|
|
99
|
+
/** False when the cap is `0` (the default): the API did not enter the draining state. */
|
|
100
|
+
enabled: boolean;
|
|
101
|
+
/** Live in-flight tasks seen when the drain began. */
|
|
102
|
+
inFlight: number;
|
|
103
|
+
/** Of those, still `in_progress` when the wait ended. */
|
|
104
|
+
remaining: number;
|
|
105
|
+
waitedMs: number;
|
|
106
|
+
timedOut: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Enter the draining state, then wait until the live in-flight tasks are handed
|
|
111
|
+
* off or the cap passes. Never throws: a failed read ends the wait, because a
|
|
112
|
+
* shutdown that cannot finish is worse than one that skips the drain.
|
|
113
|
+
*/
|
|
114
|
+
export async function drainApi(deps: ApiDrainDeps = {}): Promise<ApiDrainOutcome> {
|
|
115
|
+
const now = deps.now ?? Date.now;
|
|
116
|
+
const sleep = deps.sleep ?? ((ms: number) => Bun.sleep(ms));
|
|
117
|
+
const log = deps.log ?? ((message: string) => console.log(message));
|
|
118
|
+
const listInFlight = deps.listInFlight ?? (() => listLiveInFlightTaskIds(now()));
|
|
119
|
+
const countRemaining = deps.countRemaining ?? countStillInProgress;
|
|
120
|
+
const maxMs = resolveApiDrainMaxMs(deps.env);
|
|
121
|
+
const startedAt = now();
|
|
122
|
+
|
|
123
|
+
if (maxMs === 0) {
|
|
124
|
+
log("[drain] off (API_DRAIN_MAX_MS is 0 or unset): closing without waiting for handoffs");
|
|
125
|
+
return { enabled: false, inFlight: 0, remaining: 0, waitedMs: 0, timedOut: false };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
beginApiDrain(startedAt);
|
|
129
|
+
|
|
130
|
+
let taskIds: string[];
|
|
131
|
+
try {
|
|
132
|
+
taskIds = await listInFlight();
|
|
133
|
+
} catch (err) {
|
|
134
|
+
log(`[drain] could not read in-flight tasks, closing without waiting: ${describe(err)}`);
|
|
135
|
+
return {
|
|
136
|
+
enabled: true,
|
|
137
|
+
inFlight: 0,
|
|
138
|
+
remaining: 0,
|
|
139
|
+
waitedMs: now() - startedAt,
|
|
140
|
+
timedOut: false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (taskIds.length === 0) {
|
|
145
|
+
log("[drain] draining: no in-flight tasks on live workers");
|
|
146
|
+
return {
|
|
147
|
+
enabled: true,
|
|
148
|
+
inFlight: 0,
|
|
149
|
+
remaining: 0,
|
|
150
|
+
waitedMs: now() - startedAt,
|
|
151
|
+
timedOut: false,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log(
|
|
156
|
+
`[drain] draining: waiting up to ${maxMs}ms for ${taskIds.length} in-flight task(s) to be handed off`,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const deadline = startedAt + maxMs;
|
|
160
|
+
let remaining = taskIds.length;
|
|
161
|
+
while (true) {
|
|
162
|
+
try {
|
|
163
|
+
remaining = await countRemaining(taskIds);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
log(`[drain] could not re-read in-flight tasks, closing now: ${describe(err)}`);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (remaining === 0 || now() >= deadline) break;
|
|
169
|
+
await sleep(Math.min(DRAIN_CHECK_INTERVAL_MS, Math.max(0, deadline - now())));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const waitedMs = now() - startedAt;
|
|
173
|
+
const timedOut = remaining > 0;
|
|
174
|
+
log(
|
|
175
|
+
timedOut
|
|
176
|
+
? `[drain] timed out after ${waitedMs}ms with ${remaining} of ${taskIds.length} task(s) still in_progress`
|
|
177
|
+
: `[drain] all ${taskIds.length} in-flight task(s) handed off after ${waitedMs}ms`,
|
|
178
|
+
);
|
|
179
|
+
return { enabled: true, inFlight: taskIds.length, remaining, waitedMs, timedOut };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function describe(err: unknown): string {
|
|
183
|
+
return scrubSecrets(err instanceof Error ? err.message : String(err));
|
|
184
|
+
}
|
package/src/be/db/tasks/read.ts
CHANGED
|
@@ -1161,7 +1161,9 @@ export type ExistingTrackerContextWork = {
|
|
|
1161
1161
|
|
|
1162
1162
|
const LINEAR_TRACKER_CONTEXT_KEY_PREFIX = "task:trackers:linear:";
|
|
1163
1163
|
|
|
1164
|
-
function isLinearTrackerContextKey(
|
|
1164
|
+
export function isLinearTrackerContextKey(
|
|
1165
|
+
contextKey: string | null | undefined,
|
|
1166
|
+
): contextKey is string {
|
|
1165
1167
|
return !!contextKey && contextKey.startsWith(LINEAR_TRACKER_CONTEXT_KEY_PREFIX);
|
|
1166
1168
|
}
|
|
1167
1169
|
|
|
@@ -1170,28 +1172,51 @@ function isLinearTrackerContextKey(contextKey: string | null | undefined): conte
|
|
|
1170
1172
|
*
|
|
1171
1173
|
* Active means any non-terminal task. A completed task with persisted VCS PR/MR
|
|
1172
1174
|
* metadata is also treated as existing work because the task can be complete
|
|
1173
|
-
* while the PR is still awaiting review/merge.
|
|
1175
|
+
* while the PR is still awaiting review/merge. Delegation excludes the tracker
|
|
1176
|
+
* root and all its descendants from both lookups.
|
|
1174
1177
|
*/
|
|
1175
1178
|
export async function findExistingLinearTrackerContextWork(
|
|
1176
1179
|
contextKey: string | null | undefined,
|
|
1180
|
+
delegatingParentTaskId?: string,
|
|
1177
1181
|
): Promise<ExistingTrackerContextWork | null> {
|
|
1178
1182
|
if (!isLinearTrackerContextKey(contextKey)) return null;
|
|
1179
1183
|
|
|
1184
|
+
const lineageCte = `WITH RECURSIVE
|
|
1185
|
+
ancestors(id, parentTaskId) AS (
|
|
1186
|
+
SELECT id, parentTaskId FROM agent_tasks WHERE id = ? AND contextKey = ?
|
|
1187
|
+
UNION
|
|
1188
|
+
SELECT parent.id, parent.parentTaskId FROM agent_tasks parent
|
|
1189
|
+
JOIN ancestors child ON parent.id = child.parentTaskId
|
|
1190
|
+
WHERE parent.contextKey = ?
|
|
1191
|
+
),
|
|
1192
|
+
lineage(id) AS (
|
|
1193
|
+
SELECT id FROM ancestors
|
|
1194
|
+
WHERE parentTaskId IS NULL OR parentTaskId NOT IN (SELECT id FROM ancestors)
|
|
1195
|
+
UNION
|
|
1196
|
+
SELECT child.id FROM agent_tasks child
|
|
1197
|
+
JOIN lineage parent ON child.parentTaskId = parent.id
|
|
1198
|
+
)`;
|
|
1199
|
+
const lineageParams = [delegatingParentTaskId ?? null, contextKey, contextKey];
|
|
1200
|
+
|
|
1180
1201
|
const activeRow = await getDbClient().get<AgentTaskRow>(
|
|
1181
|
-
|
|
1202
|
+
`${lineageCte}
|
|
1203
|
+
SELECT * FROM agent_tasks
|
|
1182
1204
|
WHERE contextKey = ?
|
|
1205
|
+
AND id NOT IN (SELECT id FROM lineage)
|
|
1183
1206
|
AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
1184
1207
|
ORDER BY lastUpdatedAt DESC
|
|
1185
1208
|
LIMIT 1`,
|
|
1186
|
-
[contextKey],
|
|
1209
|
+
[...lineageParams, contextKey],
|
|
1187
1210
|
);
|
|
1188
1211
|
if (activeRow) {
|
|
1189
1212
|
return { task: rowToAgentTask(activeRow), reason: "active_task" };
|
|
1190
1213
|
}
|
|
1191
1214
|
|
|
1192
1215
|
const linkedPrRow = await getDbClient().get<AgentTaskRow>(
|
|
1193
|
-
|
|
1216
|
+
`${lineageCte}
|
|
1217
|
+
SELECT * FROM agent_tasks
|
|
1194
1218
|
WHERE contextKey = ?
|
|
1219
|
+
AND id NOT IN (SELECT id FROM lineage)
|
|
1195
1220
|
AND status = 'completed'
|
|
1196
1221
|
AND vcsProvider IS NOT NULL
|
|
1197
1222
|
AND vcsRepo IS NOT NULL
|
|
@@ -1199,7 +1224,7 @@ export async function findExistingLinearTrackerContextWork(
|
|
|
1199
1224
|
AND vcsUrl IS NOT NULL
|
|
1200
1225
|
ORDER BY lastUpdatedAt DESC
|
|
1201
1226
|
LIMIT 1`,
|
|
1202
|
-
[contextKey],
|
|
1227
|
+
[...lineageParams, contextKey],
|
|
1203
1228
|
);
|
|
1204
1229
|
if (linkedPrRow) {
|
|
1205
1230
|
return { task: rowToAgentTask(linkedPrRow), reason: "linked_open_pr" };
|
package/src/be/db/tasks/write.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pkg from "../../../../package.json";
|
|
2
2
|
import { defaultAssetKey } from "../../../assets/key";
|
|
3
|
-
import type {
|
|
3
|
+
import type { TaskTelemetryEvent, TaskTelemetryInput } from "../../../telemetry-trigger";
|
|
4
4
|
import type {
|
|
5
5
|
Agent,
|
|
6
6
|
AgentLog,
|
|
@@ -42,10 +42,11 @@ type TaskWriteDependencies = {
|
|
|
42
42
|
agentId: string | null,
|
|
43
43
|
sourceTexts: Array<string | null | undefined>,
|
|
44
44
|
) => Promise<TaskAttachment[]>;
|
|
45
|
-
emitTaskLifecycleTelemetryAfterCommit: (
|
|
46
|
-
event:
|
|
47
|
-
props:
|
|
45
|
+
emitTaskLifecycleTelemetryAfterCommit: <S extends TaskTelemetryEvent>(
|
|
46
|
+
event: S,
|
|
47
|
+
props: TaskTelemetryInput<S>,
|
|
48
48
|
verify?: (task: AgentTask | null) => boolean,
|
|
49
|
+
actorUserId?: string | null,
|
|
49
50
|
) => void;
|
|
50
51
|
taskContextForTelemetry: (task: AgentTask) => {
|
|
51
52
|
provider?: ProviderName;
|
package/src/be/db.ts
CHANGED
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
import { realtimeBus } from "../realtime/bus";
|
|
11
11
|
import { slackChannelFromContextKey } from "../tasks/slack-routing";
|
|
12
12
|
import { _resolveIntegrationType, emitIntegrationConnected, telemetry } from "../telemetry";
|
|
13
|
+
import { scheduleOrgDomainRecompute } from "../telemetry-identity";
|
|
14
|
+
import {
|
|
15
|
+
emitTaskTelemetry,
|
|
16
|
+
type TaskTelemetryEvent,
|
|
17
|
+
type TaskTelemetryInput,
|
|
18
|
+
} from "../telemetry-trigger";
|
|
13
19
|
import type {
|
|
14
20
|
ActiveSession,
|
|
15
21
|
Agent,
|
|
@@ -281,6 +287,7 @@ export {
|
|
|
281
287
|
getTasksCount,
|
|
282
288
|
hasNonTerminalRerouteDecisionChild,
|
|
283
289
|
hasNonTerminalResumeChild,
|
|
290
|
+
isLinearTrackerContextKey,
|
|
284
291
|
markFinalizedSlackRelaysDelivered,
|
|
285
292
|
markSlackRelayAttempted,
|
|
286
293
|
markSlackRelayDelivered,
|
|
@@ -331,7 +338,6 @@ configureTaskReadDependencies({
|
|
|
331
338
|
previewText: (text, maxChars) => previewText(text, maxChars),
|
|
332
339
|
});
|
|
333
340
|
|
|
334
|
-
type TaskTelemetryProps = Parameters<typeof telemetry.taskEvent>[1];
|
|
335
341
|
type TaskTelemetryContext = {
|
|
336
342
|
provider?: ProviderName;
|
|
337
343
|
harnessVariant?: string;
|
|
@@ -347,23 +353,27 @@ function assetKeyPrefixPattern(input: string): string {
|
|
|
347
353
|
return literalPrefixPattern(normalizeAssetKey(input));
|
|
348
354
|
}
|
|
349
355
|
|
|
350
|
-
function emitTaskLifecycleTelemetryAfterCommit(
|
|
351
|
-
event:
|
|
352
|
-
props:
|
|
356
|
+
function emitTaskLifecycleTelemetryAfterCommit<S extends TaskTelemetryEvent>(
|
|
357
|
+
event: S,
|
|
358
|
+
props: TaskTelemetryInput<S>,
|
|
353
359
|
verify?: (task: AgentTask | null) => boolean,
|
|
360
|
+
actorUserId?: string | null,
|
|
354
361
|
): void {
|
|
355
362
|
// afterCommit (not queueMicrotask): under an async client transaction,
|
|
356
363
|
// microtasks drain before COMMIT, so the verify read could observe
|
|
357
364
|
// uncommitted state. afterCommit runs strictly post-COMMIT/ROLLBACK.
|
|
358
365
|
getDbClient().afterCommit(() => {
|
|
366
|
+
// `trigger_surface` is resolved (one read up the parent chain) and
|
|
367
|
+
// `source` renamed to `task_source` inside emitTaskTelemetry, so every
|
|
368
|
+
// lifecycle event of a task reports the same root surface.
|
|
359
369
|
if (!verify) {
|
|
360
|
-
|
|
370
|
+
void emitTaskTelemetry(event, props, actorUserId);
|
|
361
371
|
return;
|
|
362
372
|
}
|
|
363
|
-
getTaskById(props.taskId)
|
|
373
|
+
getTaskById((props as { taskId: string }).taskId)
|
|
364
374
|
.then((task) => {
|
|
365
375
|
if (!verify(task)) return;
|
|
366
|
-
|
|
376
|
+
return emitTaskTelemetry(event, props, actorUserId);
|
|
367
377
|
})
|
|
368
378
|
.catch((err) =>
|
|
369
379
|
console.error(
|
|
@@ -2896,6 +2906,8 @@ export async function createTaskExtended(
|
|
|
2896
2906
|
priority: row.priority,
|
|
2897
2907
|
},
|
|
2898
2908
|
(task) => task !== null,
|
|
2909
|
+
// The human who asked (a pseudonymous user_ref), null for system tasks.
|
|
2910
|
+
row.requestedByUserId,
|
|
2899
2911
|
);
|
|
2900
2912
|
|
|
2901
2913
|
getDbClient().afterCommit(() => {
|
|
@@ -7352,12 +7364,17 @@ export async function createWorkflow(
|
|
|
7352
7364
|
if (!row) throw new Error("Failed to create workflow");
|
|
7353
7365
|
const workflow = rowToWorkflow(row);
|
|
7354
7366
|
// afterCommit: a caller's transaction that rolls back must not report the workflow.
|
|
7367
|
+
const requestUserId = getCurrentRequestUserId();
|
|
7355
7368
|
getDbClient().afterCommit(() =>
|
|
7356
|
-
telemetry.workflow(
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7369
|
+
telemetry.workflow(
|
|
7370
|
+
"created",
|
|
7371
|
+
{
|
|
7372
|
+
workflowId: workflow.id,
|
|
7373
|
+
nodeCount: workflow.definition.nodes.length,
|
|
7374
|
+
...(source ? { via: source } : {}),
|
|
7375
|
+
},
|
|
7376
|
+
{ userId: requestUserId ?? null },
|
|
7377
|
+
),
|
|
7361
7378
|
);
|
|
7362
7379
|
return workflow;
|
|
7363
7380
|
}
|
|
@@ -7578,11 +7595,13 @@ async function deleteWorkflowRows(id: string, source?: "api" | "mcp"): Promise<b
|
|
|
7578
7595
|
const deleted = result.changes > 0;
|
|
7579
7596
|
if (deleted) {
|
|
7580
7597
|
// afterCommit: a caller's transaction that rolls back must not report the delete.
|
|
7598
|
+
const requestUserId = getCurrentRequestUserId();
|
|
7581
7599
|
getDbClient().afterCommit(() =>
|
|
7582
|
-
telemetry.workflow(
|
|
7583
|
-
|
|
7584
|
-
...(source ? { source } : {}),
|
|
7585
|
-
|
|
7600
|
+
telemetry.workflow(
|
|
7601
|
+
"deleted",
|
|
7602
|
+
{ workflowId: id, ...(source ? { via: source } : {}) },
|
|
7603
|
+
{ userId: requestUserId ?? null },
|
|
7604
|
+
),
|
|
7586
7605
|
);
|
|
7587
7606
|
}
|
|
7588
7607
|
return deleted;
|
|
@@ -7669,6 +7688,7 @@ export async function getWorkflowRun(id: string): Promise<WorkflowRun | null> {
|
|
|
7669
7688
|
|
|
7670
7689
|
function emitWorkflowTerminalTelemetry(run: WorkflowRun): void {
|
|
7671
7690
|
if (run.status !== "completed" && run.status !== "failed") return;
|
|
7691
|
+
const status = run.status;
|
|
7672
7692
|
|
|
7673
7693
|
// afterCommit (not queueMicrotask): under an async client transaction,
|
|
7674
7694
|
// microtasks drain before COMMIT, so the verify read below could observe
|
|
@@ -7678,9 +7698,9 @@ function emitWorkflowTerminalTelemetry(run: WorkflowRun): void {
|
|
|
7678
7698
|
// crashing the process as an unhandled rejection.
|
|
7679
7699
|
getDbClient().afterCommit(async () => {
|
|
7680
7700
|
const latest = await getWorkflowRun(run.id);
|
|
7681
|
-
if (!latest || latest.status !==
|
|
7701
|
+
if (!latest || latest.status !== status) return;
|
|
7682
7702
|
const steps = await getWorkflowRunStepsByRunId(run.id);
|
|
7683
|
-
telemetry.workflow(
|
|
7703
|
+
telemetry.workflow(status, {
|
|
7684
7704
|
workflowId: run.workflowId,
|
|
7685
7705
|
durationMs: run.startedAt ? Date.now() - new Date(run.startedAt).getTime() : undefined,
|
|
7686
7706
|
stepsCompleted: steps.filter((step) => step.status === "completed").length,
|
|
@@ -12046,6 +12066,8 @@ export async function createUser(data: {
|
|
|
12046
12066
|
],
|
|
12047
12067
|
);
|
|
12048
12068
|
if (!row) throw new Error("Failed to create user");
|
|
12069
|
+
// The org's email domain may come from this user (debounced, post-commit).
|
|
12070
|
+
getDbClient().afterCommit(scheduleOrgDomainRecompute);
|
|
12049
12071
|
return rowToUser(row);
|
|
12050
12072
|
}
|
|
12051
12073
|
|
|
@@ -12118,6 +12140,7 @@ export async function updateUser(
|
|
|
12118
12140
|
`UPDATE users SET ${sets.join(", ")} WHERE id = ? RETURNING *`,
|
|
12119
12141
|
params,
|
|
12120
12142
|
);
|
|
12143
|
+
if (row) getDbClient().afterCommit(scheduleOrgDomainRecompute);
|
|
12121
12144
|
return row ? rowToUser(row) : null;
|
|
12122
12145
|
}
|
|
12123
12146
|
|
|
@@ -12227,6 +12250,7 @@ export async function deleteUser(id: string, replacementUserId?: string): Promis
|
|
|
12227
12250
|
await reclassifyTaskHumanFree(reclassifySeedIds);
|
|
12228
12251
|
|
|
12229
12252
|
const result = await tx.run("DELETE FROM users WHERE id = ?", [id]);
|
|
12253
|
+
if (result.changes > 0) getDbClient().afterCommit(scheduleOrgDomainRecompute);
|
|
12230
12254
|
return result.changes > 0;
|
|
12231
12255
|
});
|
|
12232
12256
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Phase 1: capture layer only — no traversal tools, no reranker integration.
|
|
7
7
|
*/
|
|
8
8
|
import { getDbClient } from "@/be/db";
|
|
9
|
+
import { getConfiguredAppUrls } from "@/utils/constants";
|
|
9
10
|
|
|
10
11
|
export type LinkType =
|
|
11
12
|
| "wikilink"
|
|
@@ -41,8 +42,8 @@ const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
|
41
42
|
const PR_HASH_RE = /(?:^|[\s(])#(\d{1,5})(?=[\s,.)!?]|$)/gm;
|
|
42
43
|
const PR_PREFIX_RE = /\bPR\s*#(\d{1,5})\b/gi;
|
|
43
44
|
const GITHUB_PR_URL_RE = /https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/g;
|
|
44
|
-
const
|
|
45
|
-
|
|
45
|
+
const AGENT_FS_LIVE_HOSTS = String.raw`agent-fs|live\.agent-fs\.dev`;
|
|
46
|
+
const AGENT_FS_FILE_ROUTE = String.raw`\/file\/~\/([a-f0-9-]+)\/([a-f0-9-]+)\/([\w/.%-]+)`;
|
|
46
47
|
const AGENT_UI_PAGE_RE = /(?:app\.[^/]+|localhost:\d+)\/pages\/([a-f0-9-]+)/g;
|
|
47
48
|
const AGENT_UI_TASK_RE = /(?:app\.[^/]+|localhost:\d+)\/tasks\/([a-f0-9-]+)/g;
|
|
48
49
|
|
|
@@ -111,9 +112,34 @@ const prMatcher: Matcher = (content) => {
|
|
|
111
112
|
return results;
|
|
112
113
|
};
|
|
113
114
|
|
|
115
|
+
function escapeRegExp(value: string): string {
|
|
116
|
+
return value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* agent-fs file links: the live UI, and the dashboard (Comb) at a configured
|
|
121
|
+
* `APP_URL` / `DASHBOARD_URL` or `localhost:<port>`. Both use the same
|
|
122
|
+
* `/file/~/<org>/<drive>/<path>` route and path encoding, so they resolve to
|
|
123
|
+
* the same target. Built per call: a config reload can change the app URLs.
|
|
124
|
+
* A host must start the host name (`evil-swarm.example.com` does not match
|
|
125
|
+
* `swarm.example.com`). Hosts are case-insensitive.
|
|
126
|
+
*/
|
|
127
|
+
function agentFsPathRegex(): RegExp {
|
|
128
|
+
const appHosts = getConfiguredAppUrls().flatMap((value) => {
|
|
129
|
+
try {
|
|
130
|
+
const url = new URL(value);
|
|
131
|
+
return [escapeRegExp(`${url.host}${url.pathname.replace(/\/+$/, "")}`)];
|
|
132
|
+
} catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const hosts = [AGENT_FS_LIVE_HOSTS, ...appHosts, String.raw`localhost:\d+`].join("|");
|
|
137
|
+
return new RegExp(`(?<![\\w.-])(?:${hosts})${AGENT_FS_FILE_ROUTE}`, "gi");
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
const agentFsMatcher: Matcher = (content) => {
|
|
115
141
|
const results: MatcherResult[] = [];
|
|
116
|
-
for (const match of content.matchAll(
|
|
142
|
+
for (const match of content.matchAll(agentFsPathRegex())) {
|
|
117
143
|
const orgId = match[1]!;
|
|
118
144
|
const driveId = match[2]!;
|
|
119
145
|
const path = match[3]!;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Comb pins: an agent-fs file or folder can be a favorite. The item id is
|
|
2
|
+
-- `<orgId>/<driveId>/<path>` (a folder path ends with "/"). SQLite cannot
|
|
3
|
+
-- change a CHECK in place, so rebuild the table with the 116 shape plus the
|
|
4
|
+
-- new item type. Every row is copied unchanged.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE user_favorites_new (
|
|
7
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
8
|
+
favoriteScope TEXT NOT NULL,
|
|
9
|
+
userId TEXT REFERENCES users(id) ON DELETE CASCADE,
|
|
10
|
+
itemType TEXT NOT NULL CHECK (itemType IN ('page','workflow','schedule','agent-fs-path')),
|
|
11
|
+
itemId TEXT NOT NULL,
|
|
12
|
+
createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
13
|
+
lastUpdatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
14
|
+
created_by TEXT,
|
|
15
|
+
updated_by TEXT,
|
|
16
|
+
UNIQUE (favoriteScope, itemType, itemId)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
INSERT INTO user_favorites_new (
|
|
20
|
+
id, favoriteScope, userId, itemType, itemId,
|
|
21
|
+
createdAt, lastUpdatedAt, created_by, updated_by
|
|
22
|
+
)
|
|
23
|
+
SELECT
|
|
24
|
+
id, favoriteScope, userId, itemType, itemId,
|
|
25
|
+
createdAt, lastUpdatedAt, created_by, updated_by
|
|
26
|
+
FROM user_favorites;
|
|
27
|
+
|
|
28
|
+
DROP TABLE user_favorites;
|
|
29
|
+
ALTER TABLE user_favorites_new RENAME TO user_favorites;
|
|
30
|
+
|
|
31
|
+
CREATE INDEX idx_user_favorites_scope_type
|
|
32
|
+
ON user_favorites(favoriteScope, itemType);
|
|
33
|
+
CREATE INDEX idx_user_favorites_user_type
|
|
34
|
+
ON user_favorites(userId, itemType);
|
|
35
|
+
CREATE INDEX idx_user_favorites_item
|
|
36
|
+
ON user_favorites(itemType, itemId);
|
package/src/be/onboarding.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { telemetry } from "../telemetry";
|
|
2
2
|
import { ProviderNameSchema } from "../types";
|
|
3
|
+
import { getCurrentRequestUserId } from "../utils/request-auth-context";
|
|
3
4
|
import { z } from "../utils/zod-openapi";
|
|
4
5
|
import { getDbClient, getSwarmConfigs, getTaskById, upsertSwarmConfig } from "./db";
|
|
5
6
|
import { validateConfigValue } from "./swarm-config-guard";
|
|
@@ -23,6 +24,7 @@ export const OnboardingErrorClassSchema = z.enum([
|
|
|
23
24
|
"timeout",
|
|
24
25
|
"dimension",
|
|
25
26
|
"model",
|
|
27
|
+
"endpoint",
|
|
26
28
|
"not_enabled",
|
|
27
29
|
"expired",
|
|
28
30
|
"unknown",
|
|
@@ -48,6 +50,7 @@ export const OnboardingMemoryPresetSchema = z.enum([
|
|
|
48
50
|
"openai",
|
|
49
51
|
"openrouter",
|
|
50
52
|
"vercel",
|
|
53
|
+
"azure",
|
|
51
54
|
"custom",
|
|
52
55
|
"existing",
|
|
53
56
|
]);
|
|
@@ -359,8 +362,11 @@ function addTelemetry<Event extends OnboardingTelemetryEvent>(
|
|
|
359
362
|
}
|
|
360
363
|
|
|
361
364
|
function queueTelemetry(events: PendingTelemetry[]): void {
|
|
365
|
+
// Captured now: the request's user is ambient state, and the post-commit hook
|
|
366
|
+
// may run after it has moved on. Null for an operator (API key) request.
|
|
367
|
+
const actor = { userId: getCurrentRequestUserId() ?? null };
|
|
362
368
|
for (const item of events) {
|
|
363
|
-
getDbClient().afterCommit(() => telemetry.onboarding(item.event, item.properties));
|
|
369
|
+
getDbClient().afterCommit(() => telemetry.onboarding(item.event, item.properties, actor));
|
|
364
370
|
}
|
|
365
371
|
}
|
|
366
372
|
|
|
@@ -15,6 +15,7 @@ import { getPromptTemplates, resetPromptTemplateToDefault, upsertPromptTemplate
|
|
|
15
15
|
// handler imports, but runner/session templates are only loaded by the worker. Importing them
|
|
16
16
|
// here ensures all templates are available for the render endpoint and seeded to the DB.
|
|
17
17
|
import "../prompts/session-templates";
|
|
18
|
+
import "../comb/templates";
|
|
18
19
|
import "../commands/templates";
|
|
19
20
|
import "../tools/templates";
|
|
20
21
|
|