@desplega.ai/agent-swarm 2.7.2 → 2.7.3
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 +6 -0
- package/dist/{acp-adapter-624yge7a.js → acp-adapter-pdzw76ja.js} +1 -1
- package/dist/{actions-rd9xn281.js → actions-a90cfpca.js} +21 -21
- package/dist/{amp-adapter-k27hc00p.js → amp-adapter-4vtywcjk.js} +2 -2
- package/dist/{app-sjm3xc7g.js → app-8k68gygk.js} +3 -3
- package/dist/{assistant-6jbzs8wa.js → assistant-ck01q2yg.js} +32 -21
- package/dist/{boot-reembed-e2348eqc.js → boot-reembed-33spavpd.js} +1 -1
- package/dist/{boot-reembed-w53pd5xt.js → boot-reembed-xzge83a2.js} +1 -1
- package/dist/{boot-scrub-sweep-cpy4yg97.js → boot-scrub-sweep-mxvb8dr3.js} +3 -3
- package/dist/{boot-seeds-f6j4t6k6.js → boot-seeds-rpm8bb5r.js} +4 -4
- package/dist/{claude-adapter-v3ac8esd.js → claude-adapter-q12bcfj9.js} +3 -3
- package/dist/{claude-managed-setup-yyrkqvtm.js → claude-managed-setup-11vb6q89.js} +1 -1
- package/dist/{claude-sdk-session-85j69v6d.js → claude-sdk-session-gkv1107j.js} +10325 -10243
- package/dist/{cli-rs59z1qx.js → cli-07pcqwaj.js} +1 -1
- package/dist/{cli-6vq3ymqm.js → cli-0tkm519n.js} +1 -1
- package/dist/{cli-s7cm1927.js → cli-0xzbk52h.js} +4 -4
- package/dist/{cli-g0zc1b40.js → cli-0zae6bs6.js} +2 -2
- package/dist/{cli-p8yrsmfn.js → cli-10kjx0dm.js} +1 -1
- package/dist/{cli-7xsq63pe.js → cli-157ykn37.js} +10 -9
- package/dist/{cli-vjcj9k2p.js → cli-2h0rfmqk.js} +1 -1
- package/dist/{cli-9wasax9f.js → cli-35nzvqaf.js} +1 -1
- package/dist/{cli-e1p7dqe1.js → cli-4aydmmdy.js} +24 -3
- package/dist/{cli-942pp3bc.js → cli-54ax65rn.js} +3 -3
- package/dist/{cli-10cp92df.js → cli-5ke7gxwr.js} +6 -6
- package/dist/{cli-femw4q81.js → cli-6151a7dr.js} +2 -2
- package/dist/{cli-3cywb44q.js → cli-77eq8txs.js} +85 -3
- package/dist/{cli-atpfk4yd.js → cli-7cwrwb5x.js} +5 -5
- package/dist/{cli-bfbpvccx.js → cli-8a1k6mat.js} +61 -49
- package/dist/{cli-c18941tx.js → cli-8ch3b2r7.js} +3 -3
- package/dist/{cli-rs2s1t05.js → cli-9fk56y8c.js} +4 -4
- package/dist/{cli-0wx63vvw.js → cli-9km88w11.js} +1 -1
- package/dist/{cli-qy0z3mfj.js → cli-9vttw9nz.js} +56 -46
- package/dist/{cli-mzgppb7q.js → cli-a0w50zw9.js} +4 -4
- package/dist/{cli-z8a4mfz1.js → cli-aqp59jmv.js} +4 -4
- package/dist/{cli-5d8wd50e.js → cli-bs0fwb23.js} +1 -1
- package/dist/{cli-fa4kxxx3.js → cli-ct5920jb.js} +2 -2
- package/dist/{cli-0awbtmv2.js → cli-dek8hqjc.js} +10 -10
- package/dist/{cli-qp6adcmw.js → cli-e24w2mvk.js} +1 -1
- package/dist/{cli-t3npd8hm.js → cli-ghfhn1qc.js} +1 -1
- package/dist/{cli-jmcf8cnz.js → cli-ja369zab.js} +1 -1
- package/dist/{cli-wtjg6je3.js → cli-jhh7tbx6.js} +1 -1
- package/dist/{cli-e19bgatr.js → cli-jmk31131.js} +2 -2
- package/dist/{cli-2cj8fqv1.js → cli-kjcdzea8.js} +1 -1
- package/dist/{cli-1xpr5992.js → cli-kjha1q9k.js} +3 -3
- package/dist/{cli-9jfty8px.js → cli-ktcdczth.js} +538 -30
- package/dist/{cli-exd52qn4.js → cli-mcmpbpcc.js} +2 -2
- package/dist/{cli-2m2p3snc.js → cli-mmf43vcp.js} +6 -6
- package/dist/{cli-b2rhbaty.js → cli-pnm99spe.js} +10 -10
- package/dist/{cli-9jn9cj0q.js → cli-v9jk2k63.js} +1 -1
- package/dist/{cli-mt7bxjfd.js → cli-vhgcwppn.js} +1 -1
- package/dist/{cli-qa4hgda3.js → cli-vvz64zc5.js} +14 -13
- package/dist/{cli-txr0z97c.js → cli-wmhajw68.js} +171 -32
- package/dist/{cli-hzrmv8pg.js → cli-x38rmhap.js} +3 -3
- package/dist/{cli-hq9hd4hp.js → cli-xz6qjdqj.js} +1 -1
- package/dist/{cli-g9ypd5en.js → cli-z52gq55b.js} +4 -0
- package/dist/{cli-2qbmjfta.js → cli-z6219sd3.js} +4 -4
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-zm1bkdde.js → codex-adapter-46kbvvkx.js} +1 -1
- package/dist/{codex-hook-tkag7fxq.js → codex-hook-tm9ecrd5.js} +1 -1
- package/dist/{codex-session-runner-8k02mqw2.js → codex-session-runner-qy93hdrv.js} +1 -1
- package/dist/{commands-nw1dn4av.js → commands-hwdhacfx.js} +2 -2
- package/dist/{controller-3kkkmps0.js → controller-hjza5p1f.js} +3 -3
- package/dist/{controllers-connect-n2c4167r.js → controllers-connect-7m25cay2.js} +20 -20
- package/dist/{cursor-adapter-h5028emw.js → cursor-adapter-1qh7ps8w.js} +1 -1
- package/dist/{db-76y2msnf.js → db-w23k9301.js} +2 -2
- package/dist/{db-8rk0p8j1.js → db-wm8by383.js} +3 -3
- package/dist/{dispatcher-xp4wsx30.js → dispatcher-en25ebgc.js} +18 -18
- package/dist/{dsh-adapter-vyrr31bn.js → dsh-adapter-zkme32vd.js} +1 -1
- package/dist/{e2b-eesfj689.js → e2b-9z9mvt2p.js} +1 -1
- package/dist/{e2b-03nha1wz.js → e2b-yhvn70es.js} +1 -1
- package/dist/{embedding-space-ejefyep1.js → embedding-space-rgf4aypt.js} +6 -6
- package/dist/{embeddings-w297xaw0.js → embeddings-z9cmxhx1.js} +1 -1
- package/dist/{external-channel-mf9vp1fe.js → external-channel-g6vv85zv.js} +2 -2
- package/dist/{grok-adapter-mpzyyprz.js → grok-adapter-f6n1tpq3.js} +2 -2
- package/dist/handlers-ss5948rq.js +42 -0
- package/dist/{heartbeat-qnaycjzs.js → heartbeat-5xcdzz45.js} +21 -21
- package/dist/{hook-h6sqfzxr.js → hook-6423rme3.js} +2 -2
- package/dist/{hook-4zqdgea4.js → hook-8dk1csdd.js} +4 -4
- package/dist/{http-65ts210h.js → http-6mfh2ww9.js} +255 -60
- package/dist/{index-sqe7f6mj.js → index-0js6bgff.js} +1 -1
- package/dist/{index-farm1zsn.js → index-0ng1dd1t.js} +10937 -9527
- package/dist/{index-k3ewf1jg.js → index-5eyn1wc5.js} +18 -18
- package/dist/{index-vp4wa3qn.js → index-71gnebsn.js} +18 -18
- package/dist/{index-hdfad00b.js → index-q5gzpjdd.js} +12 -10
- package/dist/index-z3b1vfjx.js +44 -0
- package/dist/{keepalive-52k6skha.js → keepalive-cymckeg6.js} +3 -3
- package/dist/{lead-y9rj0gr1.js → lead-baknj2dh.js} +29 -29
- package/dist/{maintenance-f86wqfq6.js → maintenance-dfrnsx3n.js} +1 -1
- package/dist/{oauth-refresh-sweep-vddv9ky7.js → oauth-refresh-sweep-5dztk8x6.js} +2 -2
- package/dist/{onboard-8z2qk6gq.js → onboard-h36vja6y.js} +11 -6
- package/dist/{otel-impl-epet9j18.js → otel-impl-jerq8vz8.js} +1 -1
- package/dist/{pricing-refresh-6wm42a51.js → pricing-refresh-t3nx5krn.js} +3 -3
- package/dist/{rbac-roles-2kfcs1sc.js → rbac-roles-dw1awawd.js} +2 -2
- package/dist/{rbac-roles-13a0vj6d.js → rbac-roles-km0fc3e8.js} +1 -1
- package/dist/{registry-ajhexe44.js → registry-hzfh268v.js} +1 -1
- package/dist/{render-v2-cy8zsfsf.js → render-v2-65x4ayj8.js} +3 -3
- package/dist/{resume-c783qe6b.js → resume-61znk3hj.js} +6 -6
- package/dist/{session-access-s4tv0az8.js → session-access-4yc9htrg.js} +4 -4
- package/dist/{session-source-backfill-qzapz0ne.js → session-source-backfill-kr5bdjtp.js} +2 -2
- package/dist/{setup-st7htx1k.js → setup-9h61yr2p.js} +1 -1
- package/dist/{work-objects-ftj4zw70.js → work-objects-x7rb53qn.js} +27 -22
- package/dist/{worker-859fk25k.js → worker-7f3khgn0.js} +29 -29
- package/openapi.json +134 -3
- package/package.json +5 -5
- package/src/be/db/agents.ts +16 -1
- package/src/be/db/runtime.ts +25 -15
- package/src/be/db/session-summaries.ts +120 -0
- package/src/be/db/tasks/read.ts +7 -4
- package/src/be/db/tasks/search-index.ts +159 -0
- package/src/be/db/tasks/write.ts +3 -0
- package/src/be/db-queries/slack-inbound.ts +77 -0
- package/src/be/db.ts +27 -8
- package/src/be/mcp-overflow-sweep.ts +39 -0
- package/src/be/migrations/253_session_summaries.sql +300 -0
- package/src/be/migrations/254_query_audit_indexes.sql +25 -0
- package/src/be/migrations/256_agent_slack_icon.sql +2 -0
- package/src/be/migrations/257_agent_tasks_fts.sql +55 -0
- package/src/comb/config.ts +6 -2
- package/src/comb/requester.ts +69 -0
- package/src/comb/review-batch.ts +24 -2
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/onboard/compose-generator.ts +7 -2
- package/src/commands/onboard/env-generator.ts +8 -3
- package/src/commands/runner.ts +41 -24
- package/src/fs/agent-fs-provider.ts +9 -0
- package/src/heartbeat/heartbeat.ts +13 -6
- package/src/http/agents.ts +10 -2
- package/src/http/index.ts +15 -0
- package/src/http/integrations.ts +20 -0
- package/src/http/list-total-cache.ts +62 -0
- package/src/http/sessions.ts +9 -2
- package/src/http/slack-inbound.ts +1 -0
- package/src/http/status.ts +4 -1
- package/src/http/tasks.ts +12 -2
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/queue-stall-alarm.ts +4 -1
- package/src/slack/actions.ts +2 -2
- package/src/slack/app-icon.ts +50 -0
- package/src/slack/app.ts +13 -1
- package/src/slack/assistant.ts +22 -1
- package/src/slack/event-dedup.test.ts +11 -0
- package/src/slack/event-dedup.ts +8 -4
- package/src/slack/inbound-dispatch.ts +58 -0
- package/src/slack/render-v2.ts +7 -8
- package/src/slack/responses.ts +21 -8
- package/src/slack/socket-ack.ts +237 -0
- package/src/slack/thread-buffer.ts +3 -5
- package/src/slack/watcher.ts +2 -2
- package/src/slack/work-objects.ts +5 -1
- package/src/tests/agent-fs-release-notes.test.ts +40 -0
- package/src/tests/agent-slack-icon.test.ts +85 -0
- package/src/tests/approval-cancellation-migration.test.ts +2 -0
- package/src/tests/asset-key-migration.test.ts +26 -0
- package/src/tests/comb-default.test.ts +159 -0
- package/src/tests/comb-review-batch.test.ts +192 -4
- package/src/tests/comb-status.test.ts +65 -2
- package/src/tests/db-agents.test.ts +13 -0
- package/src/tests/db-tasks-read.test.ts +10 -15
- package/src/tests/deploy-one-box.test.ts +161 -0
- package/src/tests/deploy-stubs.ts +68 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/e2e-codex-auth-chain.test.ts +18 -1
- package/src/tests/fixtures/deploy-stubs/bun +3 -0
- package/src/tests/fixtures/deploy-stubs/bunx +27 -0
- package/src/tests/fixtures/deploy-stubs/curl +26 -0
- package/src/tests/fixtures/deploy-stubs/docker +122 -0
- package/src/tests/fixtures/onboard/content.compose.yml.txt +5 -5
- package/src/tests/fixtures/onboard/dev.compose.yml.txt +4 -4
- package/src/tests/fixtures/onboard/full-all.compose.yml.txt +12 -12
- package/src/tests/fixtures/onboard/full.compose.yml.txt +12 -12
- package/src/tests/fixtures/onboard/research.compose.yml.txt +4 -4
- package/src/tests/fixtures/onboard/solo.compose.yml.txt +2 -2
- package/src/tests/heartbeat-expiry.test.ts +151 -25
- package/src/tests/http-api-integration.test.ts +31 -0
- package/src/tests/human-free-reclassify.test.ts +6 -0
- package/src/tests/install-sh-bind.test.ts +242 -0
- package/src/tests/install-sh-tunnel.test.ts +89 -0
- package/src/tests/kv-storage.test.ts +28 -0
- package/src/tests/list-endpoint-slimming.test.ts +3 -0
- package/src/tests/list-total-cache.test.ts +63 -0
- package/src/tests/mcp-overflow-sweep.test.ts +84 -0
- package/src/tests/onboard-compose.test.ts +17 -3
- package/src/tests/onboard-env.test.ts +13 -0
- package/src/tests/onboarding.test.ts +1 -1
- package/src/tests/preload.ts +1 -0
- package/src/tests/query-audit-plans.test.ts +314 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/session-summaries.test.ts +384 -0
- package/src/tests/sessions-tree.test.ts +5 -1
- package/src/tests/slack-app-icon.test.ts +90 -0
- package/src/tests/slack-external-channel-egress.test.ts +1 -1
- package/src/tests/slack-inbound-receipts.test.ts +34 -0
- package/src/tests/slack-inline-output.test.ts +29 -2
- package/src/tests/slack-render-v2-query-plans.test.ts +3 -2
- package/src/tests/slack-render-v2.test.ts +35 -0
- package/src/tests/slack-socket-ack.test.ts +553 -0
- package/src/tests/task-lineage.test.ts +3 -0
- package/src/tests/task-search-filter.test.ts +4 -4
- package/src/tests/task-search-fts.test.ts +320 -0
- package/src/tests/update-profile-auth.test.ts +28 -0
- package/src/tools/get-swarm.ts +1 -0
- package/src/tools/get-tasks.ts +6 -1
- package/src/tools/join-swarm.ts +1 -0
- package/src/tools/my-agent-info.ts +1 -0
- package/src/tools/slack-post.ts +2 -2
- package/src/tools/slack-reply.ts +2 -2
- package/src/tools/slack-start-thread.ts +2 -2
- package/src/tools/slack-update.ts +1 -1
- package/src/tools/update-profile.ts +20 -4
- package/src/tools/utils.ts +3 -4
- package/src/types.ts +47 -0
- package/src/utils/constants.ts +127 -5
- package/templates/official/lead/HEARTBEAT.md +4 -2
- package/templates/official/lead/config.json +2 -2
- package/templates/skills/swarm-docs/content.md +2 -0
- package/dist/handlers-7x8sywgr.js +0 -42
- package/dist/index-mc30r1hs.js +0 -44
package/src/be/db/tasks/read.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { parseProviderMeta } from "../../../utils/provider-metadata";
|
|
|
24
24
|
import { scrubSecrets } from "../../../utils/secret-scrubber";
|
|
25
25
|
import { getAgentById } from "../agents";
|
|
26
26
|
import { getDbClient } from "../runtime";
|
|
27
|
+
import { taskSearchCondition } from "./search-index";
|
|
27
28
|
|
|
28
29
|
type TaskReadDependencies = {
|
|
29
30
|
checkDependencies: (taskId: string) => Promise<{ ready: boolean; blockedBy: string[] }>;
|
|
@@ -906,8 +907,9 @@ export async function getAllTasks(
|
|
|
906
907
|
}
|
|
907
908
|
|
|
908
909
|
if (filters?.search) {
|
|
909
|
-
|
|
910
|
-
|
|
910
|
+
const search = await taskSearchCondition(filters.search, "page");
|
|
911
|
+
conditions.push(search.sql);
|
|
912
|
+
params.push(...search.params);
|
|
911
913
|
}
|
|
912
914
|
|
|
913
915
|
// New filters
|
|
@@ -1079,8 +1081,9 @@ export async function getTasksCount(
|
|
|
1079
1081
|
}
|
|
1080
1082
|
|
|
1081
1083
|
if (filters?.search) {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
+
const search = await taskSearchCondition(filters.search, "count");
|
|
1085
|
+
conditions.push(search.sql);
|
|
1086
|
+
params.push(...search.params);
|
|
1084
1087
|
}
|
|
1085
1088
|
|
|
1086
1089
|
if (filters?.unassigned) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { getDbClient } from "../runtime";
|
|
2
|
+
|
|
3
|
+
/** `agent_tasks_fts_state.indexedThrough` once every row is indexed (migration 256). */
|
|
4
|
+
export const TASK_FTS_COMPLETE = Number.MAX_SAFE_INTEGER;
|
|
5
|
+
|
|
6
|
+
// At or below this many candidate rows, a page reads them by rowid and sorts
|
|
7
|
+
// them. Above it, the page walks the lastUpdatedAt index and tests each row
|
|
8
|
+
// against the index instead: sorting thousands of hits means reading each
|
|
9
|
+
// row's record, and a long task body spills over many overflow pages.
|
|
10
|
+
const ROWID_SET_MAX = 500;
|
|
11
|
+
|
|
12
|
+
// Rows per backfill transaction. A task averages about 16 KB of text, so a
|
|
13
|
+
// batch holds the write lock and the event loop for tens of milliseconds.
|
|
14
|
+
const BACKFILL_BATCH_SIZE = 100;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The agent_tasks_fts MATCH expression for free text: the whole input as one
|
|
18
|
+
* FTS5 phrase, so user text cannot inject operators (NEAR, AND, -, ^, column
|
|
19
|
+
* filters), with a prefix match on its last token. Returns null when the input
|
|
20
|
+
* has no indexable token (the phrase would match nothing).
|
|
21
|
+
*/
|
|
22
|
+
export function buildTaskFtsMatch(search: string): string | null {
|
|
23
|
+
if (!/[\p{L}\p{N}]/u.test(search)) return null;
|
|
24
|
+
return `"${search.replaceAll('"', '""')}"*`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The WHERE term for a task `search`: a token and phrase match on the task
|
|
29
|
+
* text and title through agent_tasks_fts, or a prefix of the task id. Until
|
|
30
|
+
* the post-boot backfill has indexed every row it is the old substring LIKE,
|
|
31
|
+
* so results never miss rows the index has not reached yet.
|
|
32
|
+
*
|
|
33
|
+
* `purpose` picks the plan. A page with few candidates reads them by rowid; a
|
|
34
|
+
* page with many walks its ORDER BY index and tests membership (`+rowid`
|
|
35
|
+
* keeps the planner off the rowid set), stopping at the page size. A count
|
|
36
|
+
* has no LIMIT to stop at, so it always drives from the candidate set.
|
|
37
|
+
*/
|
|
38
|
+
export async function taskSearchCondition(
|
|
39
|
+
search: string,
|
|
40
|
+
purpose: "page" | "count",
|
|
41
|
+
): Promise<{ sql: string; params: string[] }> {
|
|
42
|
+
const client = getDbClient();
|
|
43
|
+
const state = await client.get<{ indexedThrough: number }>(
|
|
44
|
+
"SELECT indexedThrough FROM agent_tasks_fts_state WHERE id = 1",
|
|
45
|
+
);
|
|
46
|
+
if (!state || state.indexedThrough < TASK_FTS_COMPLETE) {
|
|
47
|
+
const pattern = `%${search}%`;
|
|
48
|
+
return {
|
|
49
|
+
sql: "(task LIKE ? OR title LIKE ? OR id LIKE ?)",
|
|
50
|
+
params: [pattern, pattern, pattern],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const match = buildTaskFtsMatch(search);
|
|
55
|
+
// Task ids are lowercase UUIDs; a prefix is a range on the primary key.
|
|
56
|
+
const idLow = search.trim().toLowerCase();
|
|
57
|
+
const idRange = idLow ? [idLow, `${idLow}\uffff`] : null;
|
|
58
|
+
if (!match && !idRange) return { sql: "0", params: [] };
|
|
59
|
+
|
|
60
|
+
const arms: string[] = [];
|
|
61
|
+
const armParams: string[] = [];
|
|
62
|
+
if (match) {
|
|
63
|
+
arms.push("SELECT rowid AS rid FROM agent_tasks_fts WHERE agent_tasks_fts MATCH ?");
|
|
64
|
+
armParams.push(match);
|
|
65
|
+
}
|
|
66
|
+
if (idRange) {
|
|
67
|
+
arms.push("SELECT rowid AS rid FROM agent_tasks WHERE id >= ? AND id < ?");
|
|
68
|
+
armParams.push(...idRange);
|
|
69
|
+
}
|
|
70
|
+
const candidates = await client.query<{ rid: number }>(
|
|
71
|
+
`${arms.join(" UNION ")} LIMIT ${ROWID_SET_MAX + 1}`,
|
|
72
|
+
armParams,
|
|
73
|
+
);
|
|
74
|
+
if (candidates.length === 0) return { sql: "0", params: [] };
|
|
75
|
+
if (candidates.length <= ROWID_SET_MAX) {
|
|
76
|
+
return {
|
|
77
|
+
sql: `agent_tasks.rowid IN (${candidates.map((row) => row.rid).join(", ")})`,
|
|
78
|
+
params: [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const rowid = purpose === "page" ? "+agent_tasks.rowid" : "agent_tasks.rowid";
|
|
83
|
+
const terms: string[] = [];
|
|
84
|
+
const params: string[] = [];
|
|
85
|
+
if (match) {
|
|
86
|
+
terms.push(`${rowid} IN (SELECT rowid FROM agent_tasks_fts WHERE agent_tasks_fts MATCH ?)`);
|
|
87
|
+
params.push(match);
|
|
88
|
+
}
|
|
89
|
+
if (idRange) {
|
|
90
|
+
terms.push("(agent_tasks.id >= ? AND agent_tasks.id < ?)");
|
|
91
|
+
params.push(...idRange);
|
|
92
|
+
}
|
|
93
|
+
return { sql: `(${terms.join(" OR ")})`, params };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Index the rows migration 256 left out, in rowid batches. Each batch and the
|
|
98
|
+
* `indexedThrough` mark that admits it to the triggers commit together, so a
|
|
99
|
+
* concurrent write is indexed exactly once: by its trigger when its row is
|
|
100
|
+
* already covered, else by a later batch. The last batch sets the mark to
|
|
101
|
+
* TASK_FTS_COMPLETE, which switches search to the index. Resumable after a
|
|
102
|
+
* restart; a no-op once complete.
|
|
103
|
+
*/
|
|
104
|
+
export async function backfillTaskSearchIndex(
|
|
105
|
+
batchSize = BACKFILL_BATCH_SIZE,
|
|
106
|
+
): Promise<{ indexed: number; ms: number }> {
|
|
107
|
+
const client = getDbClient();
|
|
108
|
+
const startedAt = performance.now();
|
|
109
|
+
let indexed = 0;
|
|
110
|
+
while (!stopRequested) {
|
|
111
|
+
const done = await client.transaction(async (tx) => {
|
|
112
|
+
const state = await tx.get<{ indexedThrough: number }>(
|
|
113
|
+
"SELECT indexedThrough FROM agent_tasks_fts_state WHERE id = 1",
|
|
114
|
+
);
|
|
115
|
+
if (!state || state.indexedThrough >= TASK_FTS_COMPLETE) return true;
|
|
116
|
+
const last = await tx.get<{ rid: number }>(
|
|
117
|
+
"SELECT rowid AS rid FROM agent_tasks WHERE rowid > ? ORDER BY rowid LIMIT 1 OFFSET ?",
|
|
118
|
+
[state.indexedThrough, batchSize - 1],
|
|
119
|
+
);
|
|
120
|
+
const through = last?.rid ?? TASK_FTS_COMPLETE;
|
|
121
|
+
const result = await tx.run(
|
|
122
|
+
`INSERT INTO agent_tasks_fts(rowid, task, title)
|
|
123
|
+
SELECT rowid, task, title FROM agent_tasks WHERE rowid > ? AND rowid <= ?`,
|
|
124
|
+
[state.indexedThrough, through],
|
|
125
|
+
);
|
|
126
|
+
indexed += result.changes;
|
|
127
|
+
await tx.run("UPDATE agent_tasks_fts_state SET indexedThrough = ? WHERE id = 1", [through]);
|
|
128
|
+
return through >= TASK_FTS_COMPLETE;
|
|
129
|
+
});
|
|
130
|
+
if (done) break;
|
|
131
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
132
|
+
}
|
|
133
|
+
return { indexed, ms: Math.round(performance.now() - startedAt) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let backfill: Promise<void> | null = null;
|
|
137
|
+
let stopRequested = false;
|
|
138
|
+
|
|
139
|
+
/** Start the backfill once per process, off the boot path. */
|
|
140
|
+
export function startTaskSearchBackfill(): Promise<void> {
|
|
141
|
+
stopRequested = false;
|
|
142
|
+
backfill ??= backfillTaskSearchIndex()
|
|
143
|
+
.then(({ indexed, ms }) => {
|
|
144
|
+
if (indexed > 0) console.log(`[task-fts] backfill indexed=${indexed} ms=${ms}`);
|
|
145
|
+
})
|
|
146
|
+
.catch((err) => {
|
|
147
|
+
console.error("[task-fts] backfill failed:", (err as Error).message);
|
|
148
|
+
})
|
|
149
|
+
.finally(() => {
|
|
150
|
+
backfill = null;
|
|
151
|
+
});
|
|
152
|
+
return backfill;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Stop after the current batch, before SQLite closes. The next start resumes. */
|
|
156
|
+
export async function stopTaskSearchBackfill(): Promise<void> {
|
|
157
|
+
stopRequested = true;
|
|
158
|
+
await backfill;
|
|
159
|
+
}
|
package/src/be/db/tasks/write.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { emitTaskStarted } from "../../task-lifecycle-events";
|
|
|
19
19
|
import { publishWorkerNudge } from "../../worker-nudges";
|
|
20
20
|
import { getAgentById, isAgentEligibleForTask } from "../agents";
|
|
21
21
|
import { getDbClient } from "../runtime";
|
|
22
|
+
import { dropSessionDescendants } from "../session-summaries";
|
|
22
23
|
import {
|
|
23
24
|
classifyTaskHumanFree,
|
|
24
25
|
reclassifyTaskHumanFree,
|
|
@@ -1138,6 +1139,8 @@ export async function deleteTask(id: string): Promise<boolean> {
|
|
|
1138
1139
|
const childIds = (
|
|
1139
1140
|
await tx.query<{ id: string }>("SELECT id FROM agent_tasks WHERE parentTaskId = ?", [id])
|
|
1140
1141
|
).map((row) => row.id);
|
|
1142
|
+
// Its descendants leave the session too; the delete trigger handles the rest.
|
|
1143
|
+
await dropSessionDescendants(id);
|
|
1141
1144
|
const result = await tx.run("DELETE FROM agent_tasks WHERE id = ?", [id]);
|
|
1142
1145
|
await reclassifyTaskHumanFree(childIds);
|
|
1143
1146
|
return result.changes > 0;
|
|
@@ -236,6 +236,83 @@ export async function admitSlackInboundReceipt(
|
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
export interface RecordSlackInboundInlineInput {
|
|
240
|
+
dedupKey: string;
|
|
241
|
+
transport: SlackInboundTransport;
|
|
242
|
+
kind: SlackInboundKind;
|
|
243
|
+
payloadType: string;
|
|
244
|
+
apiAppId?: string | null;
|
|
245
|
+
eventId?: string | null;
|
|
246
|
+
retryNum?: number | null;
|
|
247
|
+
retryReason?: string | null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Remember the dedup key of a delivery that is about to run outside the drain.
|
|
252
|
+
*
|
|
253
|
+
* The row is a tombstone: terminal (`processed`), no payload, never claimable.
|
|
254
|
+
* Retention drops it with the other completed keys, after the window in which
|
|
255
|
+
* Slack still redelivers. It exists so a redelivery is dropped after a restart,
|
|
256
|
+
* when the in-memory `event_id` cache is gone. The key is written before the
|
|
257
|
+
* work runs, so a crash mid-run is never replayed, as for an `uncertain`
|
|
258
|
+
* receipt. Returns `duplicate` when the key was already stored, counting the
|
|
259
|
+
* sighting.
|
|
260
|
+
*/
|
|
261
|
+
export async function recordSlackInboundInline(
|
|
262
|
+
input: RecordSlackInboundInlineInput,
|
|
263
|
+
now: Date = new Date(),
|
|
264
|
+
): Promise<"recorded" | "duplicate"> {
|
|
265
|
+
const nowIso = now.toISOString();
|
|
266
|
+
const inserted = await getDbClient().get<{ id: string }>(
|
|
267
|
+
`INSERT INTO slack_inbound_receipts (
|
|
268
|
+
id, dedup_key, transport, kind, payload_type, api_app_id, event_id,
|
|
269
|
+
payload_bytes, retry_num, retry_reason, state, attempts, outcome_code,
|
|
270
|
+
received_at, available_at, completed_at, payload_erased_at, created_at, updated_at
|
|
271
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 'processed', 1, 'ran_inline', ?, ?, ?, ?, ?, ?)
|
|
272
|
+
ON CONFLICT(dedup_key) DO NOTHING
|
|
273
|
+
RETURNING id`,
|
|
274
|
+
[
|
|
275
|
+
crypto.randomUUID(),
|
|
276
|
+
input.dedupKey,
|
|
277
|
+
input.transport,
|
|
278
|
+
input.kind,
|
|
279
|
+
input.payloadType,
|
|
280
|
+
input.apiAppId ?? null,
|
|
281
|
+
input.eventId ?? null,
|
|
282
|
+
input.retryNum ?? null,
|
|
283
|
+
input.retryReason ?? null,
|
|
284
|
+
nowIso,
|
|
285
|
+
nowIso,
|
|
286
|
+
nowIso,
|
|
287
|
+
nowIso,
|
|
288
|
+
nowIso,
|
|
289
|
+
nowIso,
|
|
290
|
+
],
|
|
291
|
+
);
|
|
292
|
+
if (inserted) return "recorded";
|
|
293
|
+
await touchSlackInboundDuplicate(input.dedupKey, now);
|
|
294
|
+
return "duplicate";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Is this key stored? Counts the sighting as a duplicate delivery when it is.
|
|
299
|
+
* One atomic UPDATE, no read-then-write.
|
|
300
|
+
*/
|
|
301
|
+
export async function touchSlackInboundDuplicate(
|
|
302
|
+
dedupKey: string,
|
|
303
|
+
now: Date = new Date(),
|
|
304
|
+
): Promise<boolean> {
|
|
305
|
+
const nowIso = now.toISOString();
|
|
306
|
+
const row = await getDbClient().get<{ id: string }>(
|
|
307
|
+
`UPDATE slack_inbound_receipts
|
|
308
|
+
SET duplicate_count = duplicate_count + 1, last_duplicate_at = ?, updated_at = ?
|
|
309
|
+
WHERE dedup_key = ?
|
|
310
|
+
RETURNING id`,
|
|
311
|
+
[nowIso, nowIso, dedupKey],
|
|
312
|
+
);
|
|
313
|
+
return row !== null && row !== undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
239
316
|
/**
|
|
240
317
|
* Atomically move one claimable pending receipt to `processing`. Without an id
|
|
241
318
|
* the oldest available receipt is claimed. Returns null when nothing is
|
package/src/be/db.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
AgentLogEventType,
|
|
26
26
|
AgentMcpServer,
|
|
27
27
|
AgentSkill,
|
|
28
|
+
AgentSlackIcon,
|
|
28
29
|
AgentStatus,
|
|
29
30
|
AgentTask,
|
|
30
31
|
AgentTaskStatus,
|
|
@@ -127,6 +128,7 @@ import {
|
|
|
127
128
|
CreateTaskOptionsSchema,
|
|
128
129
|
isTerminalTaskStatus,
|
|
129
130
|
type ModelTier,
|
|
131
|
+
OPEN_TASK_STATUSES,
|
|
130
132
|
parseModelTier,
|
|
131
133
|
SERVER_GENERATED_ATTACHMENT_CAPABILITY,
|
|
132
134
|
SessionCostModelBreakdownSchema,
|
|
@@ -2435,17 +2437,23 @@ async function findLatestRoutingCandidate(
|
|
|
2435
2437
|
async function captureRoutingDecisionSnapshot(
|
|
2436
2438
|
options: CreateTaskOptions,
|
|
2437
2439
|
): Promise<RoutingDecisionSnapshot> {
|
|
2440
|
+
// MATERIALIZED pins the plan: a status seek over the few open tasks. Without
|
|
2441
|
+
// it (and with no sqlite_stat1) the planner flattens the CTE back into an OR
|
|
2442
|
+
// over agentId/offeredTo that reads each agent's whole task history.
|
|
2438
2443
|
const workerStatuses = await getDbClient().query<RoutingDecisionWorkerStatus>(
|
|
2439
|
-
`
|
|
2444
|
+
`WITH open_task AS MATERIALIZED (
|
|
2445
|
+
SELECT id, agentId, offeredTo, status FROM agent_tasks
|
|
2446
|
+
WHERE status IN (${OPEN_TASK_STATUSES.map(() => "?").join(", ")})
|
|
2447
|
+
)
|
|
2448
|
+
SELECT agent.id AS agentId, agent.status,
|
|
2440
2449
|
COUNT(task.id) AS openTaskCount,
|
|
2441
2450
|
SUM(CASE WHEN task.status IN ('in_progress', 'reviewing') THEN 1 ELSE 0 END) AS activeTaskCount
|
|
2442
2451
|
FROM agents agent
|
|
2443
|
-
LEFT JOIN
|
|
2444
|
-
(task.agentId = agent.id OR task.offeredTo = agent.id)
|
|
2445
|
-
AND task.status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
2452
|
+
LEFT JOIN open_task task ON task.agentId = agent.id OR task.offeredTo = agent.id
|
|
2446
2453
|
WHERE agent.isLead = 0
|
|
2447
2454
|
GROUP BY agent.id, agent.status
|
|
2448
2455
|
ORDER BY agent.id`,
|
|
2456
|
+
[...OPEN_TASK_STATUSES],
|
|
2449
2457
|
);
|
|
2450
2458
|
|
|
2451
2459
|
const samePr =
|
|
@@ -3744,6 +3752,7 @@ export async function updateAgentProfile(
|
|
|
3744
3752
|
heartbeatMd?: string;
|
|
3745
3753
|
/** `null` resets to the deterministic fallback; omit the key to leave untouched. */
|
|
3746
3754
|
avatar?: AgentAvatar | null;
|
|
3755
|
+
slackIcon?: AgentSlackIcon | null;
|
|
3747
3756
|
},
|
|
3748
3757
|
meta?: VersionMeta,
|
|
3749
3758
|
guard?: {
|
|
@@ -3891,6 +3900,8 @@ export async function updateAgentProfile(
|
|
|
3891
3900
|
// untouched) from "key present with value null" (reset).
|
|
3892
3901
|
const avatarProvided = Object.hasOwn(updates, "avatar");
|
|
3893
3902
|
const avatarJson = updates.avatar ? JSON.stringify(updates.avatar) : null;
|
|
3903
|
+
const slackIconProvided = Object.hasOwn(updates, "slackIcon");
|
|
3904
|
+
const slackIconJson = updates.slackIcon ? JSON.stringify(updates.slackIcon) : null;
|
|
3894
3905
|
|
|
3895
3906
|
const now = new Date().toISOString();
|
|
3896
3907
|
const row = await tx.get<AgentRow>(
|
|
@@ -3906,6 +3917,7 @@ export async function updateAgentProfile(
|
|
|
3906
3917
|
toolsMd = COALESCE(?, toolsMd),
|
|
3907
3918
|
heartbeatMd = COALESCE(?, heartbeatMd),
|
|
3908
3919
|
avatar = CASE WHEN ? = 1 THEN ? ELSE avatar END,
|
|
3920
|
+
slack_icon = CASE WHEN ? = 1 THEN ? ELSE slack_icon END,
|
|
3909
3921
|
lastUpdatedAt = ?
|
|
3910
3922
|
WHERE id = ? RETURNING *`,
|
|
3911
3923
|
[
|
|
@@ -3921,6 +3933,8 @@ export async function updateAgentProfile(
|
|
|
3921
3933
|
writable.heartbeatMd ?? null,
|
|
3922
3934
|
avatarProvided ? 1 : 0,
|
|
3923
3935
|
avatarJson,
|
|
3936
|
+
slackIconProvided ? 1 : 0,
|
|
3937
|
+
slackIconJson,
|
|
3924
3938
|
now,
|
|
3925
3939
|
id,
|
|
3926
3940
|
],
|
|
@@ -8276,6 +8290,10 @@ export interface StuckWorkflowRun {
|
|
|
8276
8290
|
}
|
|
8277
8291
|
|
|
8278
8292
|
export async function getStuckWorkflowRuns(): Promise<StuckWorkflowRun[]> {
|
|
8293
|
+
// CROSS JOIN fixes the join order: the few waiting steps drive, and each
|
|
8294
|
+
// one seeks its task through idx_agent_tasks_workflowRunStepId, so `output`
|
|
8295
|
+
// is read only for matched tasks. Driving from agent_tasks by status reads
|
|
8296
|
+
// nearly every finished task.
|
|
8279
8297
|
return getDbClient().query<StuckWorkflowRun>(
|
|
8280
8298
|
`SELECT
|
|
8281
8299
|
wr.id as runId,
|
|
@@ -8285,10 +8303,11 @@ export async function getStuckWorkflowRuns(): Promise<StuckWorkflowRun[]> {
|
|
|
8285
8303
|
at.status as taskStatus,
|
|
8286
8304
|
at.output as taskOutput,
|
|
8287
8305
|
wr.workflowId
|
|
8288
|
-
FROM
|
|
8289
|
-
JOIN
|
|
8290
|
-
JOIN agent_tasks at ON at.workflowRunStepId = wrs.id
|
|
8291
|
-
WHERE
|
|
8306
|
+
FROM workflow_run_steps wrs
|
|
8307
|
+
CROSS JOIN workflow_runs wr ON wr.id = wrs.runId
|
|
8308
|
+
CROSS JOIN agent_tasks at ON at.workflowRunStepId = wrs.id
|
|
8309
|
+
WHERE wrs.status = 'waiting'
|
|
8310
|
+
AND wr.status = 'waiting'
|
|
8292
8311
|
AND at.status IN ('completed', 'failed', 'cancelled')
|
|
8293
8312
|
ORDER BY at.createdAt ASC, at.rowid ASC`,
|
|
8294
8313
|
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { MCP_OVERFLOW_NAMESPACE } from "../kv-overflow";
|
|
2
|
+
import { sweepExpiredKvPrefix } from "./db";
|
|
3
|
+
|
|
4
|
+
export const MCP_OVERFLOW_SWEEP_INTERVAL_MS = 10 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
let sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Delete expired MCP overflow values across every agent's namespace. Runs on
|
|
10
|
+
* a timer, not on the spill path: the DELETE of multi-MB values and its write
|
|
11
|
+
* lock would otherwise land on a read tool's request.
|
|
12
|
+
*/
|
|
13
|
+
export async function runMcpOverflowSweep(now = Date.now()): Promise<number> {
|
|
14
|
+
try {
|
|
15
|
+
const removed = await sweepExpiredKvPrefix(MCP_OVERFLOW_NAMESPACE, now);
|
|
16
|
+
if (removed > 0) {
|
|
17
|
+
console.debug(`[mcp-overflow] Sweep removed ${removed} expired value(s)`);
|
|
18
|
+
}
|
|
19
|
+
return removed;
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error("[mcp-overflow] Sweep failed:", err);
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function startMcpOverflowSweep(intervalMs = MCP_OVERFLOW_SWEEP_INTERVAL_MS): void {
|
|
27
|
+
if (sweepTimer) return;
|
|
28
|
+
sweepTimer = setInterval(() => {
|
|
29
|
+
void runMcpOverflowSweep();
|
|
30
|
+
}, intervalMs);
|
|
31
|
+
if (typeof sweepTimer?.unref === "function") sweepTimer.unref();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function stopMcpOverflowSweep(): void {
|
|
35
|
+
if (sweepTimer) {
|
|
36
|
+
clearInterval(sweepTimer);
|
|
37
|
+
sweepTimer = null;
|
|
38
|
+
}
|
|
39
|
+
}
|