@desplega.ai/agent-swarm 2.7.6 → 2.7.7
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/dist/{acp-adapter-bp91d37f.js → acp-adapter-7b4vk3h5.js} +5 -4
- package/dist/{actions-yj0eeaqh.js → actions-3tt153af.js} +18 -18
- package/dist/{amp-adapter-4pg9h34q.js → amp-adapter-njpts0yr.js} +1 -1
- package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
- package/dist/{app-909e97cr.js → app-nky4378f.js} +3 -3
- package/dist/{assistant-qdyb5fyy.js → assistant-jnb669pa.js} +19 -19
- package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
- package/dist/{boot-reembed-fk0s9z18.js → boot-reembed-9mgvaztt.js} +1 -1
- package/dist/{boot-reembed-gs1rzrnh.js → boot-reembed-qs2djdej.js} +1 -1
- package/dist/{boot-scrub-sweep-4qfd4kfx.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
- package/dist/{boot-seeds-yr00k13d.js → boot-seeds-5cm6ambd.js} +4 -4
- package/dist/{boxd-vdmfssd9.js → boxd-nv507va8.js} +1 -1
- package/dist/{claude-adapter-qta1f1xn.js → claude-adapter-fn287zmm.js} +5 -4
- package/dist/{claude-managed-setup-sc6j4dm0.js → claude-managed-setup-wtdz2mj2.js} +1 -1
- package/dist/{claude-sdk-session-zgrz6yvc.js → claude-sdk-session-x91kqyhm.js} +8288 -8224
- package/dist/{cli-wp9sp1qb.js → cli-08bmz9z9.js} +1 -1
- package/dist/{cli-yy3rhsdw.js → cli-1c6tzt8g.js} +7 -7
- package/dist/{cli-q0k1w5zd.js → cli-35rsp8ye.js} +1 -1
- package/dist/{cli-xnrwazav.js → cli-3fg0bdcv.js} +1 -1
- package/dist/{cli-89knwags.js → cli-5b456bpf.js} +2 -2
- package/dist/{cli-drr5v6av.js → cli-5jnhv72d.js} +1 -1
- package/dist/{cli-dxae2yg7.js → cli-5zx699j9.js} +2 -2
- package/dist/{cli-0at8w34w.js → cli-6g0hb4cp.js} +8 -8
- package/dist/{cli-e3r2neck.js → cli-6pfd54vx.js} +5 -5
- package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
- package/dist/{cli-ekwks2vs.js → cli-72v799xn.js} +1 -1
- package/dist/cli-82j36f2m.js +45 -0
- package/dist/{cli-yx4kfngt.js → cli-8305gbq0.js} +29 -16
- package/dist/{cli-4necwtsv.js → cli-8sckt74b.js} +8 -8
- package/dist/{cli-vvpx3gsx.js → cli-95jzxcp3.js} +1 -1
- package/dist/{cli-hr2e2fft.js → cli-adeegh7d.js} +7 -7
- package/dist/{cli-m48g6p95.js → cli-afkqkrtw.js} +3 -3
- package/dist/{cli-0qatpy8q.js → cli-ayayvfg4.js} +24 -4
- package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
- package/dist/{cli-zm72anhg.js → cli-bfv7vdg6.js} +9 -9
- package/dist/{cli-hgaazk69.js → cli-caj33vcq.js} +5 -5
- package/dist/{cli-3ypk9vk1.js → cli-cb2a8pna.js} +1 -1
- package/dist/{cli-waqbxg08.js → cli-cppkf404.js} +17 -17
- package/dist/{cli-yn4kxs06.js → cli-ddaanqkd.js} +4 -4
- package/dist/{cli-386tyxxg.js → cli-e0kmphd6.js} +2 -2
- package/dist/{cli-54t00gt8.js → cli-e46hg0xw.js} +1 -1
- package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
- package/dist/{cli-436532ck.js → cli-excnnjg2.js} +165 -153
- package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
- package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
- package/dist/{cli-pd7vagab.js → cli-fyt2g4wd.js} +3 -3
- package/dist/{cli-gdhpcbhf.js → cli-g08zdaec.js} +1 -1
- package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
- package/dist/{cli-svj5090z.js → cli-gz6q4eve.js} +3 -3
- package/dist/{cli-8snvgs5r.js → cli-je518txm.js} +10 -10
- package/dist/{cli-rmmmrjkd.js → cli-jtjtabxe.js} +1 -1
- package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
- package/dist/{cli-es6va8jk.js → cli-k1nje3v3.js} +38 -43
- package/dist/{cli-p5gp8pcx.js → cli-k8196sh2.js} +6 -6
- package/dist/{cli-nwj0ee2w.js → cli-k899ehvh.js} +2 -2
- package/dist/{cli-decfb6v8.js → cli-m0da8j35.js} +23 -9
- package/dist/{cli-he9wchme.js → cli-mkk9rnzz.js} +4 -4
- package/dist/{cli-nhrvpaj7.js → cli-n6bv12jn.js} +510 -4
- package/dist/{cli-8bc1w0tw.js → cli-nmzhp1b5.js} +14 -12
- package/dist/{cli-0yv5z2pg.js → cli-p82cnnjq.js} +1 -1
- package/dist/{cli-c60vetmm.js → cli-pc6n7fre.js} +1 -1
- package/dist/{cli-xds5f77d.js → cli-pjctyea3.js} +2 -2
- package/dist/{cli-xbbndvjy.js → cli-pq73h5mz.js} +3 -3
- package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
- package/dist/{cli-xnqgpshy.js → cli-q3tkegby.js} +1 -1
- package/dist/cli-qghe0h66.js +67 -0
- package/dist/{cli-zbbwez0r.js → cli-qh4d4tvk.js} +4 -4
- package/dist/{cli-w6dt9ged.js → cli-rtt8g4v9.js} +1 -1
- package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
- package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
- package/dist/{cli-faxe6thg.js → cli-vz22fyqv.js} +186 -54
- package/dist/{cli-k2gzwsj2.js → cli-wk5yrfwd.js} +160 -26
- package/dist/{cli-shpbx5ee.js → cli-ws19768n.js} +1641 -49
- package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
- package/dist/{cli-kqdg1bsb.js → cli-yndmfxb3.js} +2 -2
- package/dist/{cli-bjb3z51z.js → cli-yyj3j5m4.js} +2 -2
- package/dist/cli.js +21 -21
- package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
- package/dist/{codex-adapter-ayd1fb6h.js → codex-adapter-cf2yxe9m.js} +3 -2
- package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
- package/dist/{codex-session-runner-jesa32xc.js → codex-session-runner-6vfzdgbd.js} +3 -2
- package/dist/{commands-hanewgf2.js → commands-t3mfy7vn.js} +3 -3
- package/dist/{controller-dt1n3c1q.js → controller-q7xrbaz0.js} +3 -3
- package/dist/{controllers-connect-xx8x64n5.js → controllers-connect-c6nm70kg.js} +19 -19
- package/dist/{cursor-adapter-9s4dwwdw.js → cursor-adapter-ftd81ga8.js} +1 -1
- package/dist/{db-3vttcbv4.js → db-dk572x25.js} +3 -3
- package/dist/{db-9dv0g1ab.js → db-vemtpmcc.js} +4 -2
- package/dist/{dispatcher-tx9hd1sv.js → dispatcher-kh7ecxh9.js} +17 -17
- package/dist/{dsh-adapter-007y4ejs.js → dsh-adapter-v8628r89.js} +1 -1
- package/dist/{e2b-we6hs6q5.js → e2b-fr12yccx.js} +1 -1
- package/dist/{e2b-a19d28rz.js → e2b-x6ywzak3.js} +1 -1
- package/dist/{easter-egg-3d-rgbrqb8s.js → easter-egg-3d-nenw4x63.js} +1 -1
- package/dist/{embedding-space-ny1e2940.js → embedding-space-0jepb239.js} +6 -6
- package/dist/{embeddings-2t386t13.js → embeddings-xxt49yxg.js} +1 -1
- package/dist/{execute-pksh0ceh.js → execute-1mhw6e4x.js} +1 -1
- package/dist/{external-channel-phgmhmd7.js → external-channel-55v12e60.js} +2 -2
- package/dist/{fx-adapter-fy8rr921.js → fx-adapter-bs8wqrk5.js} +6 -5
- package/dist/{google-generative-ai-8y3m3k4b.js → google-generative-ai-v94hr7h0.js} +5 -5
- package/dist/{google-vertex-w3v98n7b.js → google-vertex-g7vr6cf8.js} +5 -5
- package/dist/{grok-adapter-q0abvpaj.js → grok-adapter-gpjqmkcq.js} +6 -5
- package/dist/handlers-zxmp1ta4.js +32 -0
- package/dist/{heartbeat-kadyek8p.js → heartbeat-k42svnr0.js} +20 -20
- package/dist/{hook-9k551xnj.js → hook-114m7hap.js} +4 -3
- package/dist/{hook-5zs3msf6.js → hook-6nphweh0.js} +24 -9
- package/dist/{http-mhz9vw1f.js → http-r8cc4wz7.js} +1025 -173
- package/dist/index-a8phxqnk.js +37 -0
- package/dist/{index-vdygbq10.js → index-e3s8wmxy.js} +1 -1
- package/dist/{index-q361cg2f.js → index-fpk4bcd9.js} +1 -1
- package/dist/index-k6h00t4c.js +30 -0
- package/dist/{index-rvr00sg7.js → index-nh3wyqdt.js} +1017 -574
- package/dist/index-xrz7te8h.js +33 -0
- package/dist/{keepalive-w56f32cg.js → keepalive-x060nt9p.js} +3 -3
- package/dist/lead-956sd2yc.js +71 -0
- package/dist/{maintenance-7mdgxcea.js → maintenance-2xxfexhf.js} +1 -1
- package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
- package/dist/{oauth-refresh-sweep-29txy9vk.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
- package/dist/{onboard-kpsrkarj.js → onboard-zc48j237.js} +3 -3
- package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
- package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
- package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
- package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
- package/dist/{opencode-adapter-j25vm3rr.js → opencode-adapter-r690h1cr.js} +4 -3
- package/dist/{otel-impl-6kkct7a8.js → otel-impl-r5xbh81q.js} +4 -4
- package/dist/{pi-mono-adapter-exhkzhn2.js → pi-mono-adapter-c7v8sdec.js} +7 -6
- package/dist/{pricing-refresh-n2x7jh6k.js → pricing-refresh-3pmre11v.js} +3 -3
- package/dist/{rbac-roles-81ypx6es.js → rbac-roles-3vbshwsb.js} +1 -1
- package/dist/{rbac-roles-b5htkjby.js → rbac-roles-910mmkgz.js} +2 -2
- package/dist/{render-v2-qn6jmpk4.js → render-v2-pxykkbab.js} +3 -3
- package/dist/{resume-wd30wx23.js → resume-adda0bwy.js} +6 -6
- package/dist/{runtime-4hsg7h60.js → runtime-fnbm7stk.js} +1 -1
- package/dist/{session-access-5qvfevhk.js → session-access-2fvamrdc.js} +4 -4
- package/dist/{session-source-backfill-n1czavbr.js → session-source-backfill-08xvv0sm.js} +2 -2
- package/dist/{setup-dnb56qy5.js → setup-1aqmvmh1.js} +1 -1
- package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
- package/dist/{virtual-modules-76xxv9fs.js → virtual-modules-d2pjk9j1.js} +5 -5
- package/dist/{work-objects-4n21tfkm.js → work-objects-9s0yrm5j.js} +20 -20
- package/dist/{worker-follow-up-77hvzxww.js → worker-follow-up-3q78fqmr.js} +17 -17
- package/dist/worker-y15hy08h.js +71 -0
- package/openapi.json +197 -4
- package/package.json +5 -5
- package/src/apps/definition.ts +6 -5
- package/src/be/db/page-theme-tokens.ts +12 -0
- package/src/be/db/tasks/read.ts +9 -1
- package/src/be/db/tasks/write.ts +19 -0
- package/src/be/db-client.ts +107 -7
- package/src/be/db.ts +34 -2
- package/src/be/migrations/269_pages_design_system.sql +23 -0
- package/src/be/seed/schedules-seeder.ts +0 -10
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
- package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
- package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +3 -3
- package/src/be/swarm-config-guard.ts +32 -0
- package/src/be/task-auto-title-listener.ts +127 -0
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/profile-sync.ts +151 -16
- package/src/commands/runner.ts +15 -7
- package/src/hooks/hook.ts +20 -9
- package/src/http/index.ts +3 -0
- package/src/http/pages-public.ts +93 -129
- package/src/http/pages.ts +94 -21
- package/src/pages/browser-sdk.ts +48 -21
- package/src/pages/color-literals.ts +115 -0
- package/src/pages/page-head.ts +766 -0
- package/src/pages/version.ts +3 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +1 -0
- package/src/providers/codex-app-server.ts +27 -1
- package/src/providers/pi-mono-extension.ts +30 -15
- package/src/tasks/follow-up-gate.ts +318 -0
- package/src/tasks/worker-follow-up.ts +21 -1
- package/src/tests/codex-app-server.test.ts +59 -0
- package/src/tests/codex-gateway-route.test.ts +50 -1
- package/src/tests/create-page-tool.test.ts +501 -0
- package/src/tests/db-client-busy-retry.test.ts +81 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +2 -0
- package/src/tests/db-tasks-read.test.ts +21 -0
- package/src/tests/fixtures/pages/head-v1.html +56 -0
- package/src/tests/follow-up-gate-shadow.test.ts +513 -0
- package/src/tests/identity-field-budget.test.ts +1 -1
- package/src/tests/internal-ai/credentials.test.ts +62 -0
- package/src/tests/internal-ai/task-title.test.ts +283 -0
- package/src/tests/merge-gate-detect-changes.test.ts +145 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-session-dev-cookie.test.ts +188 -0
- package/src/tests/pages-frame-ancestors.test.ts +28 -1
- package/src/tests/pages-head-v2.test.ts +534 -0
- package/src/tests/pages-http.test.ts +330 -0
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-storage.test.ts +182 -0
- package/src/tests/pages-versioning.test.ts +72 -0
- package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
- package/src/tests/profile-sync.test.ts +11 -15
- package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-registry.test.ts +28 -6
- package/src/tests/seed-schedules.test.ts +61 -8
- package/src/tests/seed-scripts.test.ts +142 -2
- package/src/tests/swarm-diff.test.ts +121 -2
- package/src/tests/task-auto-title-config-guard.test.ts +37 -0
- package/src/tests/task-auto-title-listener.test.ts +165 -0
- package/src/tests/theme-import.test.ts +758 -0
- package/src/tests/themes-page-theme.test.ts +155 -0
- package/src/tests/themes-presets.test.ts +172 -0
- package/src/tests/workflow-script-principal.test.ts +2 -1
- package/src/themes/classics.ts +949 -0
- package/src/themes/import.ts +420 -0
- package/src/themes/page-fields.ts +40 -0
- package/src/themes/page-theme.ts +102 -0
- package/src/themes/presets.ts +561 -0
- package/src/themes/tokens.ts +218 -0
- package/src/tools/app-upsert.ts +1 -1
- package/src/tools/create-page.ts +118 -12
- package/src/tools/utils.ts +33 -0
- package/src/types.ts +45 -3
- package/src/utils/constants.ts +19 -0
- package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
- package/src/utils/internal-ai/credentials.ts +146 -93
- package/src/utils/internal-ai/index.ts +10 -0
- package/src/utils/internal-ai/models.ts +18 -0
- package/src/utils/internal-ai/task-title.ts +282 -0
- package/src/utils/page-session.ts +37 -0
- package/templates/schedules/daily-status-report/content.md +88 -2
- package/templates/schedules/daily-swarm-update-check/content.md +16 -40
- package/templates/skills/apps/SKILL.md +1 -1
- package/templates/skills/apps/content.md +1 -1
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/pages/config.json +1 -1
- package/templates/skills/pages/content.md +539 -86
- package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
- package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
- package/templates/skills/pages/files/examples/report-page.html +125 -124
- package/dist/cli-2pnswgh8.js +0 -189
- package/dist/handlers-by3sfxyx.js +0 -32
- package/dist/index-4f2pvxae.js +0 -37
- package/dist/index-98gz7tmp.js +0 -30
- package/dist/index-k08z2cak.js +0 -33
- package/dist/lead-enq35nqp.js +0 -70
- package/dist/worker-gg1576j3.js +0 -70
- package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
- package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
package/src/be/db-client.ts
CHANGED
|
@@ -177,6 +177,11 @@ export type BusyRetryOptions = {
|
|
|
177
177
|
* Defaults to `DEFAULT_FINAL_ATTEMPT_SPIN_MS`.
|
|
178
178
|
*/
|
|
179
179
|
finalAttemptSpinMs?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Longest gap between spinning probes while an external holder is
|
|
182
|
+
* detected (see `BusyGate`). Defaults to `DEFAULT_MAX_GATE_MS`.
|
|
183
|
+
*/
|
|
184
|
+
maxGateMs?: number;
|
|
180
185
|
};
|
|
181
186
|
|
|
182
187
|
/**
|
|
@@ -247,8 +252,81 @@ function isSqliteBusy(err: unknown): boolean {
|
|
|
247
252
|
return typeof code === "string" && code.startsWith("SQLITE_BUSY");
|
|
248
253
|
}
|
|
249
254
|
|
|
255
|
+
/** Ceiling on the gap between spinning probes while a holder is detected. */
|
|
256
|
+
const DEFAULT_MAX_GATE_MS = 1_000;
|
|
257
|
+
/** A quiet stretch this long ends a BUSY episode even without a proven write. */
|
|
258
|
+
const GATE_DECAY_MS = 5_000;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Process-wide memory of a cross-process write-lock holder.
|
|
262
|
+
*
|
|
263
|
+
* Every retrying writer used to spin `attemptSpinMs` (100ms, or 250ms for the
|
|
264
|
+
* last attempt) SYNCHRONOUSLY inside the FIFO lock on every attempt. With N
|
|
265
|
+
* writers queued behind a holder that lasts minutes, one round of attempts
|
|
266
|
+
* costs N x 100ms of blocked event loop, and each attempt waits for that
|
|
267
|
+
* whole round before its own turn. A 5-minute Litestream snapshot turned
|
|
268
|
+
* the nominal 11s retry budget into 90-105s request hangs, 3s /health and
|
|
269
|
+
* 3s page loads, although only writes were contending.
|
|
270
|
+
*
|
|
271
|
+
* The gate records a BUSY seen by a spinning probe. For the next interval
|
|
272
|
+
* (doubling from `attemptSpinMs` up to `maxGateMs`) every attempt runs with
|
|
273
|
+
* a ZERO busy_timeout: it still tries, so recovery is immediate, but it
|
|
274
|
+
* fails in microseconds instead of blocking the loop, and the async backoff
|
|
275
|
+
* sleeps do the waiting. Once the interval lapses, the next attempt is the
|
|
276
|
+
* lone spinning probe again.
|
|
277
|
+
*/
|
|
278
|
+
class BusyGate {
|
|
279
|
+
private openUntil = 0;
|
|
280
|
+
private streak = 0;
|
|
281
|
+
private lastBusyAt = 0;
|
|
282
|
+
private episodeStartedAt = 0;
|
|
283
|
+
private episodeProbes = 0;
|
|
284
|
+
|
|
285
|
+
constructor(
|
|
286
|
+
private readonly baseMs: number,
|
|
287
|
+
private readonly maxMs: number,
|
|
288
|
+
) {}
|
|
289
|
+
|
|
290
|
+
/** True while attempts must not spin. */
|
|
291
|
+
isOpen(now = performance.now()): boolean {
|
|
292
|
+
return now < this.openUntil;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** A spinning probe hit BUSY: a holder exists. Open the gate and widen it. */
|
|
296
|
+
recordBusy(now = performance.now()): void {
|
|
297
|
+
if (this.streak > 0 && now - this.lastBusyAt > GATE_DECAY_MS) this.endEpisode(now, "decayed");
|
|
298
|
+
if (this.streak === 0) {
|
|
299
|
+
this.episodeStartedAt = now;
|
|
300
|
+
this.episodeProbes = 0;
|
|
301
|
+
console.warn(
|
|
302
|
+
"[db-client] SQLITE_BUSY: another process holds the write lock; spinning at most one probe per gate interval until it releases",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
this.streak += 1;
|
|
306
|
+
this.episodeProbes += 1;
|
|
307
|
+
this.lastBusyAt = now;
|
|
308
|
+
const interval = Math.min(this.maxMs, this.baseMs * 2 ** Math.min(this.streak - 1, 20));
|
|
309
|
+
this.openUntil = now + interval;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** A write got through after BUSY: the holder is gone. */
|
|
313
|
+
recordRecovered(now = performance.now()): void {
|
|
314
|
+
if (this.streak === 0) return;
|
|
315
|
+
this.endEpisode(now, "released");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private endEpisode(now: number, how: "released" | "decayed"): void {
|
|
319
|
+
console.warn(
|
|
320
|
+
`[db-client] write lock ${how} after ${Math.round(now - this.episodeStartedAt)}ms (${this.episodeProbes} spinning probes)`,
|
|
321
|
+
);
|
|
322
|
+
this.streak = 0;
|
|
323
|
+
this.openUntil = 0;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
250
327
|
class BunSqliteClient implements DbClient {
|
|
251
328
|
private readonly lock = new FifoLock();
|
|
329
|
+
private readonly gate: BusyGate;
|
|
252
330
|
private savepointSeq = 0;
|
|
253
331
|
/** Lazily captured ambient busy_timeout, restored after each short-spin attempt. */
|
|
254
332
|
private restoreBusyTimeoutMs: number | null = null;
|
|
@@ -256,7 +334,12 @@ class BunSqliteClient implements DbClient {
|
|
|
256
334
|
constructor(
|
|
257
335
|
private readonly getDatabase: () => Database,
|
|
258
336
|
private readonly busyRetry: BusyRetryOptions = DEFAULT_BUSY_RETRY,
|
|
259
|
-
) {
|
|
337
|
+
) {
|
|
338
|
+
this.gate = new BusyGate(
|
|
339
|
+
busyRetry.attemptSpinMs,
|
|
340
|
+
Math.max(busyRetry.attemptSpinMs, busyRetry.maxGateMs ?? DEFAULT_MAX_GATE_MS),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
260
343
|
|
|
261
344
|
query<T>(sql: string, params: DbParam[] = []): Promise<T[]> {
|
|
262
345
|
return this.execute(sql, (db) => db.query<T, DbParam[]>(sql).all(...params));
|
|
@@ -352,6 +435,7 @@ class BunSqliteClient implements DbClient {
|
|
|
352
435
|
// not. The short spin keeps the driver's synchronous busy wait off
|
|
353
436
|
// the event loop; the real waiting is the caller's async backoff.
|
|
354
437
|
this.shortSpin(db, () => db.run("BEGIN IMMEDIATE"), finalAttempt);
|
|
438
|
+
this.gate.recordRecovered();
|
|
355
439
|
} catch (err) {
|
|
356
440
|
if (isSqliteBusy(err)) return { ok: false, busyBegin: err };
|
|
357
441
|
throw err;
|
|
@@ -366,7 +450,7 @@ class BunSqliteClient implements DbClient {
|
|
|
366
450
|
// is exact. The open transaction pins the connection, so these
|
|
367
451
|
// retries hold the FIFO lock and their sleeps block queued work;
|
|
368
452
|
// WAL-mode COMMIT BUSY is rare enough for that to be acceptable.
|
|
369
|
-
await this.retryHoldingLock(() => this.shortSpin(db, () => db.run("COMMIT")));
|
|
453
|
+
await this.retryHoldingLock(() => this.shortSpin(db, () => db.run("COMMIT"), false, false));
|
|
370
454
|
}
|
|
371
455
|
for (const hook of ctx.afterCommit) this.scheduleHook(hook);
|
|
372
456
|
return { ok: true, value: result };
|
|
@@ -478,6 +562,7 @@ class BunSqliteClient implements DbClient {
|
|
|
478
562
|
return timed(() => op(db));
|
|
479
563
|
}
|
|
480
564
|
let sleptMs = 0;
|
|
565
|
+
let sawBusy = false;
|
|
481
566
|
for (let attemptIndex = 0; ; attemptIndex++) {
|
|
482
567
|
const next = this.busyRetry.backoffMs[attemptIndex];
|
|
483
568
|
const canRetryAfter = next !== undefined && sleptMs + next <= this.busyRetry.maxWaitMs;
|
|
@@ -488,9 +573,14 @@ class BunSqliteClient implements DbClient {
|
|
|
488
573
|
// exact. The final attempt spins `finalAttemptSpinMs` (see
|
|
489
574
|
// transaction()).
|
|
490
575
|
const db = this.getDatabase();
|
|
491
|
-
|
|
576
|
+
const result = timed(() => this.shortSpin(db, () => op(db), !canRetryAfter), canRetryAfter);
|
|
577
|
+
// A statement that was BUSY earlier in this call and now landed is a
|
|
578
|
+
// write that got through: the external holder is gone.
|
|
579
|
+
if (sawBusy) this.gate.recordRecovered();
|
|
580
|
+
return result;
|
|
492
581
|
} catch (err) {
|
|
493
582
|
if (!isSqliteBusy(err) || !canRetryAfter || next === undefined) throw err;
|
|
583
|
+
sawBusy = true;
|
|
494
584
|
backoff = next;
|
|
495
585
|
sleptMs += next;
|
|
496
586
|
} finally {
|
|
@@ -509,17 +599,27 @@ class BunSqliteClient implements DbClient {
|
|
|
509
599
|
* lock is held, so the set/attempt/restore sequence cannot interleave with
|
|
510
600
|
* other client work.
|
|
511
601
|
*/
|
|
512
|
-
private shortSpin<T>(db: Database, attempt: () => T, finalAttempt = false): T {
|
|
602
|
+
private shortSpin<T>(db: Database, attempt: () => T, finalAttempt = false, gateable = true): T {
|
|
513
603
|
if (this.restoreBusyTimeoutMs === null) {
|
|
514
604
|
const row = db.query<{ timeout: number }, []>("PRAGMA busy_timeout").get();
|
|
515
605
|
this.restoreBusyTimeoutMs = row?.timeout ?? 5000;
|
|
516
606
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
607
|
+
// While a holder is known (BusyGate) the attempt runs without a spin: it
|
|
608
|
+
// still tries, so a released lock is taken at once, but a held one costs
|
|
609
|
+
// microseconds of event loop instead of a full spin per queued writer.
|
|
610
|
+
// Only the spinning probe that follows a lapsed gate reports BUSY back.
|
|
611
|
+
const gated = gateable && this.gate.isOpen();
|
|
612
|
+
const spinMs = gated
|
|
613
|
+
? 0
|
|
614
|
+
: finalAttempt
|
|
615
|
+
? (this.busyRetry.finalAttemptSpinMs ?? DEFAULT_FINAL_ATTEMPT_SPIN_MS)
|
|
616
|
+
: this.busyRetry.attemptSpinMs;
|
|
520
617
|
db.exec(`PRAGMA busy_timeout = ${spinMs}`);
|
|
521
618
|
try {
|
|
522
619
|
return attempt();
|
|
620
|
+
} catch (err) {
|
|
621
|
+
if (gateable && !gated && isSqliteBusy(err)) this.gate.recordBusy();
|
|
622
|
+
throw err;
|
|
523
623
|
} finally {
|
|
524
624
|
db.exec(`PRAGMA busy_timeout = ${this.restoreBusyTimeoutMs}`);
|
|
525
625
|
}
|
package/src/be/db.ts
CHANGED
|
@@ -198,6 +198,7 @@ import {
|
|
|
198
198
|
} from "./db/context-versions";
|
|
199
199
|
import { clearAgentModelProfileOnOverrideChange } from "./db/model-profiles";
|
|
200
200
|
import { pageSearchSql } from "./db/page-search";
|
|
201
|
+
import { parsePageThemeTokens } from "./db/page-theme-tokens";
|
|
201
202
|
import { getDb, getDbClient } from "./db/runtime";
|
|
202
203
|
import {
|
|
203
204
|
assertAssetKeyMovesAllowed,
|
|
@@ -413,6 +414,7 @@ export {
|
|
|
413
414
|
recordTaskProviderIfUnset,
|
|
414
415
|
resetOrphanedInProgressTasksForAgent,
|
|
415
416
|
resumeTask,
|
|
417
|
+
setTaskTitleIfUnset,
|
|
416
418
|
settleSupersededTaskDependents,
|
|
417
419
|
startTask,
|
|
418
420
|
supersedeTask,
|
|
@@ -8619,6 +8621,9 @@ type PageRow = {
|
|
|
8619
8621
|
createdAt: string;
|
|
8620
8622
|
updatedAt: string;
|
|
8621
8623
|
view_count: number;
|
|
8624
|
+
theme: string | null;
|
|
8625
|
+
theme_tokens: string | null;
|
|
8626
|
+
design_version: number;
|
|
8622
8627
|
};
|
|
8623
8628
|
|
|
8624
8629
|
function rowToPage(row: PageRow): Page {
|
|
@@ -8636,6 +8641,9 @@ function rowToPage(row: PageRow): Page {
|
|
|
8636
8641
|
needsCredentials: row.needsCredentials
|
|
8637
8642
|
? (JSON.parse(row.needsCredentials) as string[])
|
|
8638
8643
|
: undefined,
|
|
8644
|
+
theme: row.theme ?? null,
|
|
8645
|
+
themeTokens: parsePageThemeTokens(row.theme_tokens),
|
|
8646
|
+
designVersion: typeof row.design_version === "number" ? row.design_version : 1,
|
|
8639
8647
|
viewCount: typeof row.view_count === "number" ? row.view_count : 0,
|
|
8640
8648
|
createdAt: normalizeDateRequired(row.createdAt),
|
|
8641
8649
|
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
@@ -8653,13 +8661,17 @@ export async function createPage(data: {
|
|
|
8653
8661
|
passwordHash?: string;
|
|
8654
8662
|
body: string;
|
|
8655
8663
|
needsCredentials?: string[];
|
|
8664
|
+
theme?: string | null;
|
|
8665
|
+
themeTokens?: Page["themeTokens"];
|
|
8666
|
+
/** Defaults to 2 (the swarm design system). Rows inserted outside this function default to 1. */
|
|
8667
|
+
designVersion?: number;
|
|
8656
8668
|
}): Promise<Page> {
|
|
8657
8669
|
// Match the historical SQL default ID shape while making the value
|
|
8658
8670
|
// available before insert so the default namespace can include it.
|
|
8659
8671
|
const id = crypto.randomUUID().replace(/-/g, "");
|
|
8660
8672
|
const row = await getDbClient().get<PageRow>(
|
|
8661
|
-
`INSERT INTO pages (id, "key", agentId, slug, title, description, contentType, authMode, passwordHash, body, needsCredentials)
|
|
8662
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
8673
|
+
`INSERT INTO pages (id, "key", agentId, slug, title, description, contentType, authMode, passwordHash, body, needsCredentials, theme, theme_tokens, design_version)
|
|
8674
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
8663
8675
|
[
|
|
8664
8676
|
id,
|
|
8665
8677
|
normalizeAssetKey(data.key ?? defaultAssetKey("page", id)),
|
|
@@ -8672,6 +8684,9 @@ export async function createPage(data: {
|
|
|
8672
8684
|
data.passwordHash ?? null,
|
|
8673
8685
|
data.body,
|
|
8674
8686
|
data.needsCredentials ? JSON.stringify(data.needsCredentials) : null,
|
|
8687
|
+
data.theme ?? null,
|
|
8688
|
+
data.themeTokens ? JSON.stringify(data.themeTokens) : null,
|
|
8689
|
+
data.designVersion ?? 2,
|
|
8675
8690
|
],
|
|
8676
8691
|
);
|
|
8677
8692
|
if (!row) throw new Error("Failed to create page");
|
|
@@ -8858,6 +8873,11 @@ export async function updatePage(
|
|
|
8858
8873
|
body?: string;
|
|
8859
8874
|
needsCredentials?: string[] | null;
|
|
8860
8875
|
slug?: string;
|
|
8876
|
+
/** undefined keeps the stored value, null clears it. */
|
|
8877
|
+
theme?: string | null;
|
|
8878
|
+
/** undefined keeps the stored value, null clears it. */
|
|
8879
|
+
themeTokens?: Page["themeTokens"];
|
|
8880
|
+
designVersion?: number;
|
|
8861
8881
|
},
|
|
8862
8882
|
): Promise<Page | null> {
|
|
8863
8883
|
const updates: string[] = [];
|
|
@@ -8898,6 +8918,18 @@ export async function updatePage(
|
|
|
8898
8918
|
updates.push("slug = ?");
|
|
8899
8919
|
params.push(data.slug);
|
|
8900
8920
|
}
|
|
8921
|
+
if (data.theme !== undefined) {
|
|
8922
|
+
updates.push("theme = ?");
|
|
8923
|
+
params.push(data.theme);
|
|
8924
|
+
}
|
|
8925
|
+
if (data.themeTokens !== undefined) {
|
|
8926
|
+
updates.push("theme_tokens = ?");
|
|
8927
|
+
params.push(data.themeTokens ? JSON.stringify(data.themeTokens) : null);
|
|
8928
|
+
}
|
|
8929
|
+
if (data.designVersion !== undefined) {
|
|
8930
|
+
updates.push("design_version = ?");
|
|
8931
|
+
params.push(data.designVersion);
|
|
8932
|
+
}
|
|
8901
8933
|
if (updates.length === 0) return getPage(id);
|
|
8902
8934
|
updates.push("updatedAt = ?");
|
|
8903
8935
|
params.push(new Date().toISOString());
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Pages design system v2: three columns that say how an HTML page is styled.
|
|
2
|
+
--
|
|
3
|
+
-- design_version Which design system serves the page. 1 is the original
|
|
4
|
+
-- look (Tailwind v3 Play CDN, `--swarm-*` hex tokens) and stays
|
|
5
|
+
-- frozen. 2 is the swarm design system (theme tokens, `swarm-*`
|
|
6
|
+
-- primitives, Tailwind v4). The DEFAULT makes every existing row
|
|
7
|
+
-- read 1, so old pages keep their look. New pages get 2 from
|
|
8
|
+
-- createPage(), which inserts the value explicitly.
|
|
9
|
+
-- theme Preset id from the dashboard theme registry (for example
|
|
10
|
+
-- `paper`). NULL pins nothing: the page follows the viewer's
|
|
11
|
+
-- theme, then falls back to `hive`. Design version 2 only.
|
|
12
|
+
-- theme_tokens JSON `{"light": {...}, "dark": {...}}` of per-page overrides,
|
|
13
|
+
-- keyed by page token names (`--color-primary`, `--radius-lg`).
|
|
14
|
+
-- Stored normalized by importThemeTokens() (src/themes/import.ts).
|
|
15
|
+
-- NULL means no overrides. Design version 2 only.
|
|
16
|
+
--
|
|
17
|
+
-- Both theme columns are NULL for every existing row. Neither one has a CHECK:
|
|
18
|
+
-- the preset ids live in code (src/themes/presets.ts), so the write paths
|
|
19
|
+
-- validate them, and a stale id read from the table is ignored at render time.
|
|
20
|
+
|
|
21
|
+
ALTER TABLE pages ADD COLUMN theme TEXT;
|
|
22
|
+
ALTER TABLE pages ADD COLUMN theme_tokens TEXT;
|
|
23
|
+
ALTER TABLE pages ADD COLUMN design_version INTEGER NOT NULL DEFAULT 1;
|
|
@@ -29,12 +29,6 @@ import dailySwarmUpdateCheckConfig from "../../../templates/schedules/daily-swar
|
|
|
29
29
|
import dailySwarmUpdateCheckContent from "../../../templates/schedules/daily-swarm-update-check/content.md" with {
|
|
30
30
|
type: "text",
|
|
31
31
|
};
|
|
32
|
-
import dailyWorkflowHealthAuditConfig from "../../../templates/schedules/daily-workflow-health-audit/config.json" with {
|
|
33
|
-
type: "text",
|
|
34
|
-
};
|
|
35
|
-
import dailyWorkflowHealthAuditContent from "../../../templates/schedules/daily-workflow-health-audit/content.md" with {
|
|
36
|
-
type: "text",
|
|
37
|
-
};
|
|
38
32
|
import dreamingDailyConfig from "../../../templates/schedules/dreaming-daily/config.json" with {
|
|
39
33
|
type: "text",
|
|
40
34
|
};
|
|
@@ -153,10 +147,6 @@ const BUILT_IN_SCHEDULE_SOURCES: readonly ScheduleTemplateSource[] = [
|
|
|
153
147
|
config: asText(dailySwarmUpdateCheckConfig),
|
|
154
148
|
content: asText(dailySwarmUpdateCheckContent),
|
|
155
149
|
},
|
|
156
|
-
{
|
|
157
|
-
config: asText(dailyWorkflowHealthAuditConfig),
|
|
158
|
-
content: asText(dailyWorkflowHealthAuditContent),
|
|
159
|
-
},
|
|
160
150
|
{ config: asText(dreamingDailyConfig), content: asText(dreamingDailyContent) },
|
|
161
151
|
{ config: asText(gtmWeeklyReviewConfig), content: asText(gtmWeeklyReviewContent) },
|
|
162
152
|
{
|
|
@@ -745,6 +745,8 @@ export async function publishCatalogReportPage(report: CatalogReport, ctx: any):
|
|
|
745
745
|
description: report.description,
|
|
746
746
|
contentType: "text/html",
|
|
747
747
|
authMode: "authed",
|
|
748
|
+
// The report ships its own palette, written for the v1 page head.
|
|
749
|
+
designVersion: 1,
|
|
748
750
|
body: renderCatalogReportPage(report),
|
|
749
751
|
});
|
|
750
752
|
const payload = response?.data ?? response;
|
|
@@ -745,6 +745,8 @@ export async function publishCatalogReportPage(report: CatalogReport, ctx: any):
|
|
|
745
745
|
description: report.description,
|
|
746
746
|
contentType: "text/html",
|
|
747
747
|
authMode: "authed",
|
|
748
|
+
// The report ships its own palette, written for the v1 page head.
|
|
749
|
+
designVersion: 1,
|
|
748
750
|
body: renderCatalogReportPage(report),
|
|
749
751
|
});
|
|
750
752
|
const payload = response?.data ?? response;
|
|
@@ -522,6 +522,8 @@ export default async function memoryEval(args: any, ctx: any) {
|
|
|
522
522
|
description: `3-axis memory quality dashboard: carry-forward ${report.axis1.score}%, preferences usefulness ${report.axis2.avgUsefulness}, freshness ${report.axis3.freshScore}%. ${history.length} data points.`,
|
|
523
523
|
contentType: "text/html",
|
|
524
524
|
authMode: "authed",
|
|
525
|
+
// The dashboard ships its own palette, written for the v1 page head.
|
|
526
|
+
designVersion: 1,
|
|
525
527
|
body: html,
|
|
526
528
|
});
|
|
527
529
|
const payload = response?.data ?? response;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const argsSchema = z.object({
|
|
4
|
+
dryRun: z
|
|
5
|
+
.boolean()
|
|
6
|
+
.optional()
|
|
7
|
+
.describe("Compute the verdict but do not notify the lead when an update is available"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const RELEASES_URL = "https://api.github.com/repos/desplega-ai/agent-swarm/releases/latest";
|
|
11
|
+
|
|
12
|
+
/** Parse `major.minor.patch` (a leading `v` and a pre-release/build suffix are ignored). */
|
|
13
|
+
export function parseVersion(value: unknown): [number, number, number] | null {
|
|
14
|
+
if (typeof value !== "string") return null;
|
|
15
|
+
const m = value
|
|
16
|
+
.trim()
|
|
17
|
+
.replace(/^v/i, "")
|
|
18
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
19
|
+
if (!m) return null;
|
|
20
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Numeric compare of two parsed versions: negative, zero or positive. Never a string compare. */
|
|
24
|
+
export function compareVersions(a: [number, number, number], b: [number, number, number]): number {
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
const diff = (a[i] as number) - (b[i] as number);
|
|
27
|
+
if (diff !== 0) return diff;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function errorText(error: unknown): string {
|
|
33
|
+
return String((error as any)?.message ?? error).slice(0, 200);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The version the API server is running, from its own unauthenticated `/health` route. */
|
|
37
|
+
async function runningVersion(ctx: any): Promise<string | null> {
|
|
38
|
+
try {
|
|
39
|
+
const base = ctx.stdlib.Redacted.value(ctx.swarm.config.mcpBaseUrl).replace(/\/+$/, "");
|
|
40
|
+
const res: any = await ctx.stdlib.fetch(base + "/health", { retries: 1, timeoutMs: 10_000 });
|
|
41
|
+
if (!res.ok) return null;
|
|
42
|
+
const body: any = await res.json();
|
|
43
|
+
return typeof body?.version === "string" && body.version.trim() ? body.version.trim() : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function resolveLeadId(ctx: any): Promise<string | null> {
|
|
50
|
+
const res: any = await ctx.swarm.swarm_get({});
|
|
51
|
+
const data = res?.data ?? res;
|
|
52
|
+
const agents: any[] = Array.isArray(data?.agents) ? data.agents : Array.isArray(data) ? data : [];
|
|
53
|
+
const lead = agents.find((agent) => agent?.isLead === true || agent?.isLead === 1);
|
|
54
|
+
return typeof lead?.id === "string" ? lead.id : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Compare the running agent-swarm version with the latest published GitHub
|
|
59
|
+
* release and, only when a newer release exists, hand the lead one task to
|
|
60
|
+
* tell the operator through its admin delivery channel.
|
|
61
|
+
*/
|
|
62
|
+
export default async function swarmUpdateCheck(args: any, ctx: any) {
|
|
63
|
+
const parsed = argsSchema.safeParse(args ?? {});
|
|
64
|
+
if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
|
|
65
|
+
const dryRun = parsed.data.dryRun === true;
|
|
66
|
+
|
|
67
|
+
const running = await runningVersion(ctx);
|
|
68
|
+
const runningParsed = parseVersion(running);
|
|
69
|
+
if (!running || !runningParsed) {
|
|
70
|
+
// Unknown is not "up to date": say so and stop, never guess a version.
|
|
71
|
+
return {
|
|
72
|
+
checked: false,
|
|
73
|
+
status: "unknown",
|
|
74
|
+
running,
|
|
75
|
+
message: `Unable to determine running version: ${running ? `unparseable "${running}"` : "not reported by /health"}.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let release: any;
|
|
80
|
+
try {
|
|
81
|
+
// One attempt: a failed GitHub call is reported, never retried in a loop.
|
|
82
|
+
const res: any = await ctx.stdlib.fetch(RELEASES_URL, {
|
|
83
|
+
retries: 1,
|
|
84
|
+
timeoutMs: 15_000,
|
|
85
|
+
headers: { Accept: "application/vnd.github+json", "User-Agent": "agent-swarm-update-check" },
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
88
|
+
release = await res.json();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
checked: false,
|
|
92
|
+
status: "github-unreachable",
|
|
93
|
+
running,
|
|
94
|
+
message: `Could not reach GitHub releases API: ${errorText(error)}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const latest = typeof release?.tag_name === "string" ? release.tag_name.replace(/^v/i, "") : null;
|
|
99
|
+
const latestParsed = parseVersion(latest);
|
|
100
|
+
if (!latest || !latestParsed) {
|
|
101
|
+
return {
|
|
102
|
+
checked: false,
|
|
103
|
+
status: "github-unreachable",
|
|
104
|
+
running,
|
|
105
|
+
message: `Could not read a version from the latest release tag: ${JSON.stringify(release?.tag_name ?? null)}.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const releaseUrl =
|
|
109
|
+
typeof release?.html_url === "string"
|
|
110
|
+
? release.html_url
|
|
111
|
+
: `https://github.com/desplega-ai/agent-swarm/releases/tag/v${latest}`;
|
|
112
|
+
|
|
113
|
+
if (compareVersions(latestParsed, runningParsed) <= 0) {
|
|
114
|
+
return {
|
|
115
|
+
checked: true,
|
|
116
|
+
status: "up-to-date",
|
|
117
|
+
running,
|
|
118
|
+
latest,
|
|
119
|
+
message: `Up to date: running ${running}, latest published ${latest}.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const message = `Update available: running ${running}, latest ${latest} (${releaseUrl})`;
|
|
124
|
+
if (dryRun) {
|
|
125
|
+
return { checked: true, status: "update-available", running, latest, releaseUrl, notified: false, message };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const leadId = await resolveLeadId(ctx);
|
|
129
|
+
// A detected update that reaches nobody is a failed run, not a quiet success.
|
|
130
|
+
if (!leadId) throw new Error(`${message}, but the swarm has no lead to notify.`);
|
|
131
|
+
const sent: any = await ctx.swarm.task_send({
|
|
132
|
+
agentId: leadId,
|
|
133
|
+
routingReason: "skill",
|
|
134
|
+
routingNote: "Lead owns the operator's admin delivery channel for release notices.",
|
|
135
|
+
tags: ["swarm-update-check"],
|
|
136
|
+
task: [
|
|
137
|
+
`A newer agent-swarm release is published: running ${running}, latest ${latest}.`,
|
|
138
|
+
`Release: ${releaseUrl}`,
|
|
139
|
+
"",
|
|
140
|
+
"Post one short message to your configured admin delivery channel (the in-app fallback if none is configured) naming the running version, the new version and the release URL. Then complete with that message as the output.",
|
|
141
|
+
"Do not attempt the upgrade yourself: this notice only detects and reports.",
|
|
142
|
+
].join("\n"),
|
|
143
|
+
});
|
|
144
|
+
const taskId = sent?.data?.task?.id ?? null;
|
|
145
|
+
if (sent?.success === false || sent?.data?.success === false || !taskId) {
|
|
146
|
+
const reason = sent?.data?.message ?? sent?.data?.error ?? `status ${sent?.status}`;
|
|
147
|
+
throw new Error(`${message}, but notifying the lead failed: ${errorText(reason)}`);
|
|
148
|
+
}
|
|
149
|
+
return { checked: true, status: "update-available", running, latest, releaseUrl, notified: true, leadTaskId: taskId, message };
|
|
150
|
+
}
|
|
@@ -60,6 +60,7 @@ import starHistoryRefreshSrc from "./catalog/star-history-refresh.ts" with { typ
|
|
|
60
60
|
// @ts-expect-error Bun text imports synthesize a default string for this helper.
|
|
61
61
|
import starHistoryRendererSrc from "./catalog/star-history-renderer.ts" with { type: "text" };
|
|
62
62
|
import swarmOverviewSrc from "./catalog/swarm-overview.ts" with { type: "text" };
|
|
63
|
+
import swarmUpdateCheckSrc from "./catalog/swarm-update-check.ts" with { type: "text" };
|
|
63
64
|
import taskContextGatheringSrc from "./catalog/task-context-gathering.ts" with { type: "text" };
|
|
64
65
|
import taskFailureAuditSrc from "./catalog/task-failure-audit.ts" with { type: "text" };
|
|
65
66
|
import textDiffSrc from "./catalog/text-diff.ts" with { type: "text" };
|
|
@@ -217,6 +218,14 @@ export const SEED_SCRIPTS: SeedScript[] = [
|
|
|
217
218
|
"Task onboarding: one call instead of task_get plus multiple memory_search calls. Pass the task description split into 2-4 natural-language queries.",
|
|
218
219
|
source: asText(taskContextGatheringSrc),
|
|
219
220
|
},
|
|
221
|
+
{
|
|
222
|
+
name: "swarm-update-check",
|
|
223
|
+
description:
|
|
224
|
+
"Compare the running agent-swarm version (API /health) with the latest GitHub release of desplega-ai/agent-swarm as numeric major.minor.patch, and hand the lead one notify task only when a newer release exists.",
|
|
225
|
+
intent:
|
|
226
|
+
"Daily zero-config upgrade watchdog behind the daily-swarm-update-check schedule; dryRun reports the verdict without notifying.",
|
|
227
|
+
source: asText(swarmUpdateCheckSrc),
|
|
228
|
+
},
|
|
220
229
|
{
|
|
221
230
|
name: "schedule-health",
|
|
222
231
|
description:
|