@desplega.ai/agent-swarm 1.110.0 → 1.112.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 +1 -1
- package/dist/{actions-837ktebk.js → actions-eckb4g8r.js} +6 -6
- package/dist/{app-85h97pg2.js → app-b05sgm02.js} +3 -3
- package/dist/{assistant-yhrta0c5.js → assistant-7255sbyw.js} +9 -9
- package/dist/{boot-reembed-5sd1t9r4.js → boot-reembed-y1bt3ttn.js} +4 -4
- package/dist/{boot-reembed-8xmfv2pq.js → boot-reembed-yv7awg4y.js} +3 -3
- package/dist/{boot-scrub-logs-fdw8wbtq.js → boot-scrub-logs-ddyaeqar.js} +2 -2
- package/dist/{claude-managed-setup-yhbcx0bz.js → claude-managed-setup-cm05wtek.js} +1 -1
- package/dist/{cli-8ps5wsk9.js → cli-0c3n0eba.js} +1 -1
- package/dist/{cli-kna0r84e.js → cli-15598a4k.js} +599 -51
- package/dist/{cli-8s0swp2t.js → cli-30d2ct3k.js} +3 -3
- package/dist/{cli-2h86bsfe.js → cli-3r5y6ayj.js} +2 -2
- package/dist/{cli-6ttdv68b.js → cli-52tbq17n.js} +1 -1
- package/dist/{cli-k6ssh5dk.js → cli-56s4ehzw.js} +1 -1
- package/dist/{cli-m3enfr8q.js → cli-6wscj9z0.js} +4 -4
- package/dist/{cli-5kav4x1a.js → cli-709t753c.js} +7 -2
- package/dist/{cli-g44ffxk5.js → cli-76d1vtvq.js} +11 -2
- package/dist/{cli-gb7dqnna.js → cli-7xkwgkrs.js} +1 -1
- package/dist/{cli-kg9v8h1w.js → cli-8f0dwca2.js} +1 -1
- package/dist/{cli-zw3cmjh6.js → cli-94vzr3nq.js} +4 -4
- package/dist/{cli-d8vmtyyr.js → cli-951w7c79.js} +1 -1
- package/dist/{cli-dhph6q98.js → cli-9p0qhead.js} +1 -1
- package/dist/{cli-vfyhrk9k.js → cli-a5e8n5hg.js} +1 -1
- package/dist/{cli-w6zp4qcx.js → cli-ceewqajw.js} +37 -39
- package/dist/{cli-r19ttke3.js → cli-h9jx5mag.js} +4 -4
- package/dist/{cli-4trfcacj.js → cli-hm27prrc.js} +5 -5
- package/dist/{cli-gtt1f14h.js → cli-pt7c7qxz.js} +3 -3
- package/dist/{cli-sjm0e1pa.js → cli-r1btc895.js} +2 -2
- package/dist/{cli-3baz70yp.js → cli-rp8pca98.js} +1 -1
- package/dist/{cli-dy01kwxk.js → cli-s9pj3c7z.js} +2 -2
- package/dist/{cli-0pzg1f8z.js → cli-tch0ypp6.js} +71 -24
- package/dist/{cli-3t97r3y1.js → cli-tg0f2wmz.js} +5 -5
- package/dist/{cli-amncg1dq.js → cli-tk9vyqzh.js} +66 -9
- package/dist/{cli-cwety9qq.js → cli-tp7e60s3.js} +4 -2
- package/dist/cli.js +11 -11
- package/dist/{codex-adapter-ebs12jef.js → codex-adapter-5cmy858w.js} +3 -3
- package/dist/{codex-login-4sywkz6v.js → codex-login-0h18cy3e.js} +1 -1
- package/dist/{codex-session-runner-3bx0b1hn.js → codex-session-runner-46q8mrba.js} +3 -3
- package/dist/{commands-3fh75nzv.js → commands-14g7taf4.js} +2 -2
- package/dist/{db-kwfxkhpw.js → db-3tge4jrn.js} +2 -2
- package/dist/{handlers-jj8rzetb.js → handlers-sy64egg6.js} +9 -9
- package/dist/{hook-1b45zzz1.js → hook-rnkzw8vp.js} +3 -3
- package/dist/{http-71964f7a.js → http-xx9xptcs.js} +234 -37
- package/dist/{index-e01wmtj0.js → index-3wr6v2mm.js} +7 -7
- package/dist/{index-464v93y6.js → index-brma88d4.js} +9 -9
- package/dist/{index-c35g752k.js → index-ha8e9xn2.js} +7 -11
- package/dist/{index-zm0x6vmf.js → index-zfx5p959.js} +8 -8
- package/dist/{keepalive-2gxqjgrx.js → keepalive-rbz05zad.js} +4 -4
- package/dist/{lead-twm4zp8y.js → lead-s790v7r0.js} +20 -20
- package/dist/{maintenance-1htsnyz6.js → maintenance-420rmadb.js} +4 -4
- package/dist/{onboard-1y1pdzt5.js → onboard-jgqamqnw.js} +2 -2
- package/dist/{otel-impl-pw9tnc80.js → otel-impl-ch3n3783.js} +1 -1
- package/dist/{pi-mono-adapter-ayr3akk8.js → pi-mono-adapter-kxjxpfgy.js} +2 -2
- package/dist/{pricing-refresh-tqvvrk7a.js → pricing-refresh-rmtrby6h.js} +4 -4
- package/dist/{seed-pricing-f23sehzq.js → seed-pricing-1szjzfj1.js} +3 -3
- package/dist/{setup-jtw9rz9t.js → setup-8jfzkyzv.js} +2 -2
- package/dist/{worker-jxrmzwb9.js → worker-40vq0pm8.js} +20 -20
- package/openapi.json +1010 -2
- package/package.json +1 -1
- package/src/be/memory/constants.ts +7 -0
- package/src/be/memory/graph-expansion.ts +167 -0
- package/src/be/memory/link-resolver.ts +81 -17
- package/src/be/memory/links-store.ts +208 -0
- package/src/be/memory/providers/sqlite-store.ts +2 -2
- package/src/be/memory/raters/retrieval.ts +2 -1
- package/src/be/memory/types.ts +1 -1
- package/src/be/memory/usefulness-stats.ts +259 -0
- package/src/be/migrations/107_memory_usefulness_indexes.sql +12 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/smart-recall.ts +1 -5
- package/src/commands/runner.ts +154 -8
- package/src/github/handlers.ts +128 -25
- package/src/github/mentions-aliases.test.ts +4 -3
- package/src/http/codex-oauth-keep-warm.ts +107 -0
- package/src/http/index.ts +2 -0
- package/src/http/memory.ts +73 -6
- package/src/providers/codex-adapter.ts +66 -2
- package/src/providers/codex-oauth/auth-json.ts +18 -9
- package/src/providers/codex-oauth/flow.ts +1 -1
- package/src/providers/codex-oauth/storage.ts +88 -6
- package/src/providers/codex-oauth/types.ts +1 -1
- package/src/scripts-runtime/sdk-allowlist.ts +2 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server.ts +4 -0
- package/src/slack/message-text.ts +25 -0
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +113 -11
- package/src/tests/codex-adapter.test.ts +27 -0
- package/src/tests/codex-oauth-keep-warm.test.ts +268 -0
- package/src/tests/codex-oauth-refresh-lock.test.ts +4 -4
- package/src/tests/codex-oauth-storage.test.ts +149 -6
- package/src/tests/codex-oauth.test.ts +12 -0
- package/src/tests/create-page-tool.test.ts +7 -0
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +124 -0
- package/src/tests/github-event-filter.test.ts +3 -2
- package/src/tests/github-handlers-inline-comments.test.ts +114 -24
- package/src/tests/jira-webhook-lifecycle.test.ts +7 -0
- package/src/tests/memory-edit.test.ts +95 -0
- package/src/tests/memory-get-tool.test.ts +138 -0
- package/src/tests/memory-graph-expansion.test.ts +421 -0
- package/src/tests/memory-http-recall-gating.test.ts +13 -0
- package/src/tests/memory-link-resolver.test.ts +228 -2
- package/src/tests/memory-usefulness-endpoint.test.ts +392 -0
- package/src/tests/runner-repo-autostash.test.ts +168 -1
- package/src/tests/script-workflows-runtime-e2e.test.ts +1 -0
- package/src/tests/slack-delete.test.ts +157 -0
- package/src/tests/slack-message-text.test.ts +37 -1
- package/src/tests/slack-update.test.ts +167 -0
- package/src/tests/slack-upload-file.test.ts +135 -0
- package/src/tests/tool-annotations.test.ts +3 -0
- package/src/tools/inject-learning.ts +1 -0
- package/src/tools/memory-edit.ts +3 -2
- package/src/tools/memory-get.ts +66 -1
- package/src/tools/memory-search.ts +13 -3
- package/src/tools/slack-delete.ts +102 -0
- package/src/tools/slack-reply.ts +11 -3
- package/src/tools/slack-start-thread.ts +2 -0
- package/src/tools/slack-update.ts +127 -0
- package/src/tools/slack-upload-file.ts +11 -1
- package/src/tools/tool-config.ts +3 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windowed usefulness analytics over the memory measurement tables.
|
|
3
|
+
*
|
|
4
|
+
* Answers "is memory useful, per source and per retrieval arm" by reading the
|
|
5
|
+
* plumbing that Phases 1–2 of the memory-enhancements track already write:
|
|
6
|
+
* - `memory_retrieval` — one row per memory surfaced to a task
|
|
7
|
+
* (051 base + 096 contextKey/intent/eventType + 097 retrievalId/rank +
|
|
8
|
+
* 100 retrievalSource).
|
|
9
|
+
* - `memory_rating` — append-only RatingEvent audit (051 + 096
|
|
10
|
+
* contextKey); `source='implicit-citation'` rows are the citation signal.
|
|
11
|
+
* - `agent_memory.alpha/beta` — Beta-Binomial usefulness posteriors (051).
|
|
12
|
+
*
|
|
13
|
+
* Served by `GET /api/memory/usefulness` (src/http/memory.ts) as a sibling of
|
|
14
|
+
* the cheap `/api/memory/health` probe. Server-side only (owns SQL via
|
|
15
|
+
* `getDb()`); plan: thoughts/taras/plans/2026-07-02-memory-retrieval-v2-graph-and-measurement.md
|
|
16
|
+
* Phase 1.
|
|
17
|
+
*/
|
|
18
|
+
import { getDb } from "@/be/db";
|
|
19
|
+
|
|
20
|
+
// ─── Shapes ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface UsefulnessVolume {
|
|
23
|
+
/** memory_retrieval rows inside the window. */
|
|
24
|
+
retrievals: number;
|
|
25
|
+
/** Distinct memories surfaced inside the window. */
|
|
26
|
+
distinctMemories: number;
|
|
27
|
+
/** Distinct retrievalId groups (one per search/get call; pre-097 rows have NULL and are not counted). */
|
|
28
|
+
retrievalGroups: number;
|
|
29
|
+
/** Window rows split by eventType (CHECK: 'search' | 'get'). */
|
|
30
|
+
byEventType: { search: number; get: number };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UsefulnessArmStats {
|
|
34
|
+
/** memory_retrieval.retrievalSource — 'vec' | 'fts' | 'hybrid' | 'fallback' ('graph' after DES-639a); NULL = pre-100 legacy rows. */
|
|
35
|
+
retrievalSource: string | null;
|
|
36
|
+
retrievals: number;
|
|
37
|
+
distinctMemories: number;
|
|
38
|
+
/** Window rows whose (taskId, memoryId) got a positive implicit-citation rating. */
|
|
39
|
+
citedRetrievals: number;
|
|
40
|
+
/** citedRetrievals / retrievals. */
|
|
41
|
+
citationRate: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface UsefulnessSourceCitation {
|
|
45
|
+
/** agent_memory.source — 'manual' | 'file_index' | 'session_summary' | 'task_completion'. */
|
|
46
|
+
source: string;
|
|
47
|
+
/** implicit-citation rating rows inside the window. */
|
|
48
|
+
ratings: number;
|
|
49
|
+
/** Ratings with signal > 0 (memory was cited in task evidence). */
|
|
50
|
+
positive: number;
|
|
51
|
+
/** positive / ratings — a true rate in [0, 1] (implicit-citation signals are ±1). */
|
|
52
|
+
citationRate: number;
|
|
53
|
+
/** AVG(signal) over the window's implicit-citation ratings — in [-1, 1]. */
|
|
54
|
+
avgSignal: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface UsefulnessPosteriorStats {
|
|
58
|
+
totalMemories: number;
|
|
59
|
+
/** Memories whose posterior moved off the Beta(1,1) prior. */
|
|
60
|
+
movedFromPrior: number;
|
|
61
|
+
/** AVG(alpha / (alpha + beta)) across all memories; null when the store is empty. */
|
|
62
|
+
avgPosteriorMean: number | null;
|
|
63
|
+
/** Same average restricted to moved memories; null when none moved. */
|
|
64
|
+
avgPosteriorMeanMoved: number | null;
|
|
65
|
+
/** Memories with posterior mean above `threshold`. */
|
|
66
|
+
aboveThreshold: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface UsefulnessSanity {
|
|
70
|
+
/** All-time memory_retrieval row count — "is anything flowing". */
|
|
71
|
+
totalRetrievalRows: number;
|
|
72
|
+
/** All-time memory_rating row count. */
|
|
73
|
+
totalRatingRows: number;
|
|
74
|
+
/** All-time memory_rating rows grouped by rater source. */
|
|
75
|
+
ratingsBySource: { source: string; count: number }[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface UsefulnessStats {
|
|
79
|
+
windowDays: number;
|
|
80
|
+
threshold: number;
|
|
81
|
+
/** ISO cutoff — rows strictly newer than this are inside the window. */
|
|
82
|
+
cutoff: string;
|
|
83
|
+
volume: UsefulnessVolume;
|
|
84
|
+
byArm: UsefulnessArmStats[];
|
|
85
|
+
citationBySource: UsefulnessSourceCitation[];
|
|
86
|
+
posterior: UsefulnessPosteriorStats;
|
|
87
|
+
sanity: UsefulnessSanity;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface UsefulnessStatsOptions {
|
|
91
|
+
/** Window size in days (default 30). */
|
|
92
|
+
days?: number;
|
|
93
|
+
/** Posterior-mean threshold for the aboveThreshold count (default 0.6). */
|
|
94
|
+
threshold?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Query ───────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export function getUsefulnessStats(options: UsefulnessStatsOptions = {}): UsefulnessStats {
|
|
100
|
+
const days = options.days ?? 30;
|
|
101
|
+
const threshold = options.threshold ?? 0.6;
|
|
102
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
103
|
+
const db = getDb();
|
|
104
|
+
|
|
105
|
+
// Volume — window rows, distinct memories, per-call groups, eventType split.
|
|
106
|
+
// SUM(CASE …) instead of COUNT(*) FILTER for maximum SQLite compatibility.
|
|
107
|
+
const volumeRow = db
|
|
108
|
+
.prepare<
|
|
109
|
+
{
|
|
110
|
+
retrievals: number;
|
|
111
|
+
distinctMemories: number;
|
|
112
|
+
retrievalGroups: number;
|
|
113
|
+
searchEvents: number | null;
|
|
114
|
+
getEvents: number | null;
|
|
115
|
+
},
|
|
116
|
+
[string]
|
|
117
|
+
>(
|
|
118
|
+
`SELECT COUNT(*) AS retrievals,
|
|
119
|
+
COUNT(DISTINCT memoryId) AS distinctMemories,
|
|
120
|
+
COUNT(DISTINCT retrievalId) AS retrievalGroups,
|
|
121
|
+
SUM(CASE WHEN eventType = 'search' THEN 1 ELSE 0 END) AS searchEvents,
|
|
122
|
+
SUM(CASE WHEN eventType = 'get' THEN 1 ELSE 0 END) AS getEvents
|
|
123
|
+
FROM memory_retrieval
|
|
124
|
+
WHERE retrievedAt > ?`,
|
|
125
|
+
)
|
|
126
|
+
.get(cutoff)!;
|
|
127
|
+
|
|
128
|
+
// Per-arm breakdown — retrieval provenance plus "did this surfaced memory
|
|
129
|
+
// get cited in the same task". EXISTS keeps multi-rating (task, memory)
|
|
130
|
+
// pairs from inflating the count. Restricted to search events: memory-get
|
|
131
|
+
// rows carry no retrievalSource and would otherwise pollute the NULL
|
|
132
|
+
// ("legacy") arm. NULL eventType = pre-096 rows, kept (they were searches).
|
|
133
|
+
const armRows = db
|
|
134
|
+
.prepare<
|
|
135
|
+
{
|
|
136
|
+
retrievalSource: string | null;
|
|
137
|
+
retrievals: number;
|
|
138
|
+
distinctMemories: number;
|
|
139
|
+
citedRetrievals: number | null;
|
|
140
|
+
},
|
|
141
|
+
[string]
|
|
142
|
+
>(
|
|
143
|
+
`SELECT mr.retrievalSource AS retrievalSource,
|
|
144
|
+
COUNT(*) AS retrievals,
|
|
145
|
+
COUNT(DISTINCT mr.memoryId) AS distinctMemories,
|
|
146
|
+
SUM(CASE WHEN EXISTS (
|
|
147
|
+
SELECT 1
|
|
148
|
+
FROM memory_rating rt
|
|
149
|
+
WHERE rt.taskId = mr.taskId
|
|
150
|
+
AND rt.memoryId = mr.memoryId
|
|
151
|
+
AND rt.source = 'implicit-citation'
|
|
152
|
+
AND rt.signal > 0
|
|
153
|
+
) THEN 1 ELSE 0 END) AS citedRetrievals
|
|
154
|
+
FROM memory_retrieval mr
|
|
155
|
+
WHERE mr.retrievedAt > ?
|
|
156
|
+
AND (mr.eventType IS NULL OR mr.eventType = 'search')
|
|
157
|
+
GROUP BY mr.retrievalSource
|
|
158
|
+
ORDER BY retrievals DESC`,
|
|
159
|
+
)
|
|
160
|
+
.all(cutoff);
|
|
161
|
+
|
|
162
|
+
// Citation rate per memory-source — the reconstructed R4 §A.2 query.
|
|
163
|
+
const sourceRows = db
|
|
164
|
+
.prepare<
|
|
165
|
+
{
|
|
166
|
+
source: string;
|
|
167
|
+
ratings: number;
|
|
168
|
+
positive: number | null;
|
|
169
|
+
avgSignal: number | null;
|
|
170
|
+
},
|
|
171
|
+
[string]
|
|
172
|
+
>(
|
|
173
|
+
`SELECT am.source AS source,
|
|
174
|
+
COUNT(*) AS ratings,
|
|
175
|
+
SUM(CASE WHEN mr.signal > 0 THEN 1 ELSE 0 END) AS positive,
|
|
176
|
+
AVG(mr.signal) AS avgSignal
|
|
177
|
+
FROM memory_rating mr
|
|
178
|
+
JOIN agent_memory am ON am.id = mr.memoryId
|
|
179
|
+
WHERE mr.source = 'implicit-citation'
|
|
180
|
+
AND mr.createdAt > ?
|
|
181
|
+
GROUP BY am.source
|
|
182
|
+
ORDER BY ratings DESC`,
|
|
183
|
+
)
|
|
184
|
+
.all(cutoff);
|
|
185
|
+
|
|
186
|
+
// Posterior movement — how far the Beta(1,1) priors have drifted.
|
|
187
|
+
const posteriorRow = db
|
|
188
|
+
.prepare<
|
|
189
|
+
{
|
|
190
|
+
totalMemories: number;
|
|
191
|
+
movedFromPrior: number | null;
|
|
192
|
+
avgPosteriorMean: number | null;
|
|
193
|
+
avgPosteriorMeanMoved: number | null;
|
|
194
|
+
aboveThreshold: number | null;
|
|
195
|
+
},
|
|
196
|
+
[number]
|
|
197
|
+
>(
|
|
198
|
+
`SELECT COUNT(*) AS totalMemories,
|
|
199
|
+
SUM(CASE WHEN alpha <> 1.0 OR beta <> 1.0 THEN 1 ELSE 0 END) AS movedFromPrior,
|
|
200
|
+
AVG(alpha / (alpha + beta)) AS avgPosteriorMean,
|
|
201
|
+
AVG(CASE WHEN alpha <> 1.0 OR beta <> 1.0
|
|
202
|
+
THEN alpha / (alpha + beta) END) AS avgPosteriorMeanMoved,
|
|
203
|
+
SUM(CASE WHEN alpha / (alpha + beta) > ? THEN 1 ELSE 0 END) AS aboveThreshold
|
|
204
|
+
FROM agent_memory`,
|
|
205
|
+
)
|
|
206
|
+
.get(threshold)!;
|
|
207
|
+
|
|
208
|
+
// Sanity — all-time totals, unwindowed: "is anything flowing at all".
|
|
209
|
+
const totalRetrievalRows = db
|
|
210
|
+
.prepare<{ n: number }, []>("SELECT COUNT(*) AS n FROM memory_retrieval")
|
|
211
|
+
.get()!.n;
|
|
212
|
+
const ratingsBySource = db
|
|
213
|
+
.prepare<{ source: string; count: number }, []>(
|
|
214
|
+
"SELECT source, COUNT(*) AS count FROM memory_rating GROUP BY source ORDER BY count DESC",
|
|
215
|
+
)
|
|
216
|
+
.all();
|
|
217
|
+
const totalRatingRows = ratingsBySource.reduce((sum, row) => sum + row.count, 0);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
windowDays: days,
|
|
221
|
+
threshold,
|
|
222
|
+
cutoff,
|
|
223
|
+
volume: {
|
|
224
|
+
retrievals: volumeRow.retrievals,
|
|
225
|
+
distinctMemories: volumeRow.distinctMemories,
|
|
226
|
+
retrievalGroups: volumeRow.retrievalGroups,
|
|
227
|
+
byEventType: {
|
|
228
|
+
search: volumeRow.searchEvents ?? 0,
|
|
229
|
+
get: volumeRow.getEvents ?? 0,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
byArm: armRows.map((row) => ({
|
|
233
|
+
retrievalSource: row.retrievalSource,
|
|
234
|
+
retrievals: row.retrievals,
|
|
235
|
+
distinctMemories: row.distinctMemories,
|
|
236
|
+
citedRetrievals: row.citedRetrievals ?? 0,
|
|
237
|
+
citationRate: row.retrievals > 0 ? (row.citedRetrievals ?? 0) / row.retrievals : 0,
|
|
238
|
+
})),
|
|
239
|
+
citationBySource: sourceRows.map((row) => ({
|
|
240
|
+
source: row.source,
|
|
241
|
+
ratings: row.ratings,
|
|
242
|
+
positive: row.positive ?? 0,
|
|
243
|
+
citationRate: row.ratings > 0 ? (row.positive ?? 0) / row.ratings : 0,
|
|
244
|
+
avgSignal: row.avgSignal ?? 0,
|
|
245
|
+
})),
|
|
246
|
+
posterior: {
|
|
247
|
+
totalMemories: posteriorRow.totalMemories,
|
|
248
|
+
movedFromPrior: posteriorRow.movedFromPrior ?? 0,
|
|
249
|
+
avgPosteriorMean: posteriorRow.avgPosteriorMean,
|
|
250
|
+
avgPosteriorMeanMoved: posteriorRow.avgPosteriorMeanMoved,
|
|
251
|
+
aboveThreshold: posteriorRow.aboveThreshold ?? 0,
|
|
252
|
+
},
|
|
253
|
+
sanity: {
|
|
254
|
+
totalRetrievalRows,
|
|
255
|
+
totalRatingRows,
|
|
256
|
+
ratingsBySource,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Supporting indexes for the usefulness readout (GET /api/memory/usefulness)
|
|
2
|
+
-- and the per-arm citation EXISTS probe. Prior to this, the windowed volume
|
|
3
|
+
-- query scanned memory_retrieval (no retrievedAt index) and the citation
|
|
4
|
+
-- EXISTS probed memory_rating via the single-column taskId index only.
|
|
5
|
+
|
|
6
|
+
CREATE INDEX IF NOT EXISTS idx_memret_retrieved_at ON memory_retrieval(retrievedAt);
|
|
7
|
+
|
|
8
|
+
-- Per-arm citation probe: EXISTS (taskId, memoryId, source, signal).
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_memrat_task_memory_source ON memory_rating(taskId, memoryId, source);
|
|
10
|
+
|
|
11
|
+
-- Citation-by-source window: WHERE source = ? AND createdAt > ?.
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_memrat_source_created_at ON memory_rating(source, createdAt);
|
|
@@ -137,6 +137,8 @@ export interface SwarmSdk {
|
|
|
137
137
|
slack_startThread(args: { channelId: string; message: string }): Promise<unknown>;
|
|
138
138
|
slack_uploadFile(args: Record<string, unknown>): Promise<unknown>;
|
|
139
139
|
slack_downloadFile(args: { url: string }): Promise<unknown>;
|
|
140
|
+
slack_delete(args: { channelId: string; messageTs: string }): Promise<unknown>;
|
|
141
|
+
slack_update(args: { channelId: string; messageTs: string; message: string }): Promise<unknown>;
|
|
140
142
|
|
|
141
143
|
// --- write: messaging (internal) ---
|
|
142
144
|
message_post(args: { channel?: string; content: string; to?: string }): Promise<unknown>;
|
|
@@ -15,17 +15,13 @@ export const argsSchema = z.object({
|
|
|
15
15
|
.positive()
|
|
16
16
|
.optional()
|
|
17
17
|
.describe("Max results per query (default 10)"),
|
|
18
|
-
dedupThreshold: z
|
|
19
|
-
.number()
|
|
20
|
-
.optional()
|
|
21
|
-
.describe("Similarity threshold for dedup — memories above this are merged (default 0.92)"),
|
|
22
18
|
});
|
|
23
19
|
|
|
24
20
|
/** Multi-query fan-out memory recall with dedup and composite reranking. */
|
|
25
21
|
export default async function smartRecall(args: any, ctx: any) {
|
|
26
22
|
const parsed = argsSchema.safeParse(args);
|
|
27
23
|
if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
|
|
28
|
-
const { queries, scope = "all", limit = 10
|
|
24
|
+
const { queries, scope = "all", limit = 10 } = parsed.data;
|
|
29
25
|
|
|
30
26
|
const allResults: any[] = [];
|
|
31
27
|
for (const q of queries) {
|
package/src/commands/runner.ts
CHANGED
|
@@ -196,9 +196,111 @@ async function listSwarmAutostashes(clonePath: string, role: string): Promise<Sw
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Continuation task types that must NEVER take the destructive hard-reset path,
|
|
201
|
+
* even though they are not `taskType === "resume"`. Each of these ingress paths
|
|
202
|
+
* creates a NEW task that CONTINUES/cooperates with existing work — often on the
|
|
203
|
+
* same worker/reused clone — rather than starting genuinely fresh coding work:
|
|
204
|
+
* - "follow-up" / "reroute-decision": lead-owned continuations created by
|
|
205
|
+
* src/tasks/worker-follow-up.ts after a task completes/fails or needs
|
|
206
|
+
* re-delegation; they inherit the parent's vcsRepo/branch context via
|
|
207
|
+
* createTaskExtended's parentTaskId inheritance (src/be/db.ts).
|
|
208
|
+
* - "agentmail-reply": AgentMail follow-up on an EXISTING thread
|
|
209
|
+
* (src/agentmail/handlers.ts) — always carries `parentTaskId` pointing at
|
|
210
|
+
* the task it's continuing (as opposed to "agentmail-message", which fires
|
|
211
|
+
* only when no existing task was found for the thread).
|
|
212
|
+
* - "github-comment" / "github-review" / "gitlab-comment" / "gitlab-ci":
|
|
213
|
+
* tracker feedback on an ALREADY-OPEN PR/MR (review comments, review
|
|
214
|
+
* verdicts, CI failures) — the point is to keep working on the existing
|
|
215
|
+
* feature branch, not reset back to the default branch.
|
|
216
|
+
*/
|
|
217
|
+
const CONTINUATION_TASK_TYPES = new Set([
|
|
218
|
+
"resume",
|
|
219
|
+
"follow-up",
|
|
220
|
+
"reroute-decision",
|
|
221
|
+
"agentmail-reply",
|
|
222
|
+
"github-comment",
|
|
223
|
+
"github-review",
|
|
224
|
+
"gitlab-comment",
|
|
225
|
+
"gitlab-ci",
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Task statuses under which a task may have already mutated the shared clone
|
|
230
|
+
* (checked out a feature branch, left uncommitted/unpushed work) — or could
|
|
231
|
+
* still be doing so concurrently, e.g. with `maxTasks > 1` on the same worker.
|
|
232
|
+
* Mirrors the ACTIVE_TASK_STATUSES / getInProgressTasksByContextKey status
|
|
233
|
+
* sets already used server-side (src/agentmail/handlers.ts, src/be/db.ts) for
|
|
234
|
+
* the same "is this task still doing live work" question.
|
|
235
|
+
*/
|
|
236
|
+
const ACTIVE_PARENT_STATUSES = new Set<string>(["pending", "in_progress", "offered", "paused"]);
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A task is a "first kickoff" (safe to hard-reset the base branch) only when it
|
|
240
|
+
* is BOTH (a) not an explicit continuation task-type and (b) not parent-linked
|
|
241
|
+
* to a still-active task.
|
|
242
|
+
*
|
|
243
|
+
* `parentTaskId` alone is NOT a resume signal — send-task/trackers set it on
|
|
244
|
+
* ~every dispatched child task (verified 395/395 real coding tasks carried one;
|
|
245
|
+
* 0 were typed "resume"), so gating on its mere presence made the reset a no-op
|
|
246
|
+
* in production (see git history on this function). But the *opposite*
|
|
247
|
+
* over-correction — treating every non-"resume" task as a safe root kickoff —
|
|
248
|
+
* is also wrong: several ingress paths create parent-linked NON-resume tasks
|
|
249
|
+
* specifically to CONTINUE existing work, often on the same worker/reused
|
|
250
|
+
* clone:
|
|
251
|
+
* - Slack follow-ups (src/slack/handlers.ts) wire `parentTaskId:
|
|
252
|
+
* latestTask?.id` with NO distinguishing taskType at all.
|
|
253
|
+
* - Sibling-awareness (src/tasks/sibling-awareness.ts, applied to every
|
|
254
|
+
* ingress via createTaskWithSiblingAwareness) wires `parentTaskId` to an
|
|
255
|
+
* in-progress same-agent sibling for session continuity — again with no
|
|
256
|
+
* reserved taskType.
|
|
257
|
+
* - AgentMail replies, tracker comments/reviews/CI events — see
|
|
258
|
+
* CONTINUATION_TASK_TYPES.
|
|
259
|
+
* Since the first two carry no reserved taskType, a taskType denylist alone
|
|
260
|
+
* cannot catch them — we ALSO check whether `parentTaskId` currently points at
|
|
261
|
+
* a still-active task (ACTIVE_PARENT_STATUSES). If so, that parent/sibling may
|
|
262
|
+
* hold a checked-out feature branch or unpushed/uncommitted work in the SAME
|
|
263
|
+
* shared clone (especially with `maxTasks > 1`), so a hard reset there would
|
|
264
|
+
* destroy it — take the non-destructive merge path instead.
|
|
265
|
+
*
|
|
266
|
+
* `fetchParentTaskStatus` is injected so this stays a pure, unit-testable
|
|
267
|
+
* function; the real call site fetches `GET /api/tasks/{id}` (same pattern as
|
|
268
|
+
* fetchProviderSessionInfo below).
|
|
269
|
+
*/
|
|
270
|
+
export async function isFirstKickoffTask(
|
|
271
|
+
task?: { taskType?: string; parentTaskId?: string } | null,
|
|
272
|
+
fetchParentTaskStatus?: (parentTaskId: string) => Promise<string | null | undefined>,
|
|
273
|
+
): Promise<boolean> {
|
|
274
|
+
if (task?.taskType && CONTINUATION_TASK_TYPES.has(task.taskType)) return false;
|
|
275
|
+
if (task?.parentTaskId && fetchParentTaskStatus) {
|
|
276
|
+
const parentStatus = await fetchParentTaskStatus(task.parentTaskId);
|
|
277
|
+
if (parentStatus && ACTIVE_PARENT_STATUSES.has(parentStatus)) return false;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Fetch a task's current status by id. Returns null on any failure (not-found, network, etc). */
|
|
283
|
+
async function fetchTaskStatus(
|
|
284
|
+
apiUrl: string,
|
|
285
|
+
apiKey: string,
|
|
286
|
+
taskId: string,
|
|
287
|
+
): Promise<string | null> {
|
|
288
|
+
const headers: Record<string, string> = {};
|
|
289
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
290
|
+
try {
|
|
291
|
+
const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, { headers });
|
|
292
|
+
if (!response.ok) return null;
|
|
293
|
+
const data = (await response.json()) as { status?: string };
|
|
294
|
+
return data.status ?? null;
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
199
300
|
async function refreshExistingRepoForTask(
|
|
200
301
|
repoConfig: { name: string; clonePath: string; defaultBranch: string },
|
|
201
302
|
role: string,
|
|
303
|
+
isFirstKickoff: boolean,
|
|
202
304
|
): Promise<string | null> {
|
|
203
305
|
const { name, clonePath, defaultBranch } = repoConfig;
|
|
204
306
|
const statusResult =
|
|
@@ -219,13 +321,36 @@ async function refreshExistingRepoForTask(
|
|
|
219
321
|
}
|
|
220
322
|
}
|
|
221
323
|
|
|
324
|
+
const fetchSpec = `${defaultBranch}:refs/remotes/origin/${defaultBranch}`;
|
|
325
|
+
const remoteRef = `refs/remotes/origin/${defaultBranch}`;
|
|
326
|
+
|
|
222
327
|
try {
|
|
223
|
-
console.log(`[${role}] Refreshing ${name} from origin/${defaultBranch}...`);
|
|
224
|
-
const fetchSpec = `${defaultBranch}:refs/remotes/origin/${defaultBranch}`;
|
|
225
|
-
const remoteRef = `refs/remotes/origin/${defaultBranch}`;
|
|
226
328
|
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} fetch origin ${fetchSpec}`.quiet();
|
|
227
|
-
|
|
228
|
-
|
|
329
|
+
|
|
330
|
+
// First-kickoff guarantee (see call sites): the clone is REUSED task-to-task,
|
|
331
|
+
// so a leftover feature branch from a prior task can still be checked out here.
|
|
332
|
+
// On a genuine first kickoff (no parentTaskId / not a resume) we force the base
|
|
333
|
+
// branch back to a clean, current state before the caller creates a work branch
|
|
334
|
+
// off it — otherwise a diverged leftover branch would abort the merge below and
|
|
335
|
+
// leave the repo stale (only a warning was emitted). Resumes and follow-up/child
|
|
336
|
+
// tasks (which may legitimately still have that feature branch checked out with
|
|
337
|
+
// in-progress work) must NEVER take this path — merge is the safe default there.
|
|
338
|
+
if (isFirstKickoff) {
|
|
339
|
+
console.log(`[${role}] First kickoff for ${name} — resetting to ${remoteRef}...`);
|
|
340
|
+
try {
|
|
341
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} checkout ${defaultBranch}`.quiet();
|
|
342
|
+
} catch {
|
|
343
|
+
// Local branch may be missing (e.g. deleted by a prior task) — recreate it
|
|
344
|
+
// tracking the remote instead of failing the whole refresh.
|
|
345
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} checkout -B ${defaultBranch} ${remoteRef}`.quiet();
|
|
346
|
+
}
|
|
347
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} reset --hard ${remoteRef}`.quiet();
|
|
348
|
+
console.log(`[${role}] Reset ${name} to ${remoteRef}`);
|
|
349
|
+
} else {
|
|
350
|
+
console.log(`[${role}] Refreshing ${name} from ${remoteRef}...`);
|
|
351
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} merge --no-edit --no-stat ${remoteRef}`.quiet();
|
|
352
|
+
console.log(`[${role}] Refreshed ${name}`);
|
|
353
|
+
}
|
|
229
354
|
return null;
|
|
230
355
|
} catch (err) {
|
|
231
356
|
const errorMsg = scrubSecrets((err as Error).message);
|
|
@@ -278,6 +403,13 @@ export async function ensureRepoForTask(
|
|
|
278
403
|
hooks?: { enabled: boolean } | null;
|
|
279
404
|
},
|
|
280
405
|
role: string,
|
|
406
|
+
/**
|
|
407
|
+
* True only for a genuine first kickoff (a brand-new task, not a resume and
|
|
408
|
+
* not a follow-up/child task continuing prior work). Defaults to false —
|
|
409
|
+
* when in doubt, do NOT force-reset the base branch, since the clone is
|
|
410
|
+
* reused task-to-task and may hold another task's in-progress branch.
|
|
411
|
+
*/
|
|
412
|
+
isFirstKickoff = false,
|
|
281
413
|
): Promise<{
|
|
282
414
|
clonePath: string;
|
|
283
415
|
claudeMd: string | null;
|
|
@@ -308,7 +440,11 @@ export async function ensureRepoForTask(
|
|
|
308
440
|
console.log(`[${role}] Cloned ${name}`);
|
|
309
441
|
} else {
|
|
310
442
|
console.log(`[${role}] Repo ${name} already cloned at ${clonePath}`);
|
|
311
|
-
warning = await refreshExistingRepoForTask(
|
|
443
|
+
warning = await refreshExistingRepoForTask(
|
|
444
|
+
{ name, clonePath, defaultBranch },
|
|
445
|
+
role,
|
|
446
|
+
isFirstKickoff,
|
|
447
|
+
);
|
|
312
448
|
}
|
|
313
449
|
|
|
314
450
|
await installRepoHooksForTask(repoConfig, role);
|
|
@@ -4794,7 +4930,9 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4794
4930
|
clonePath: `/workspace/personal/repos/${task.vcsRepo.split("/").pop() || task.vcsRepo}`,
|
|
4795
4931
|
defaultBranch: "main",
|
|
4796
4932
|
};
|
|
4797
|
-
|
|
4933
|
+
// This is the paused-task-resume-after-restart path — never a first
|
|
4934
|
+
// kickoff, so never force-reset the base branch here.
|
|
4935
|
+
const repoContext = await ensureRepoForTask(effectiveConfig, role, false);
|
|
4798
4936
|
if (repoContext?.clonePath) {
|
|
4799
4937
|
resumeCwd = repoContext.clonePath;
|
|
4800
4938
|
}
|
|
@@ -5187,7 +5325,15 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
5187
5325
|
clonePath: `/workspace/personal/repos/${taskVcsRepo.split("/").pop() || taskVcsRepo}`,
|
|
5188
5326
|
defaultBranch: "main",
|
|
5189
5327
|
};
|
|
5190
|
-
|
|
5328
|
+
// First kickoff = a genuinely new root task: not a continuation
|
|
5329
|
+
// task-type, and not parent-linked to a still-active task (Slack
|
|
5330
|
+
// follow-ups / sibling-awareness wire parentTaskId with no
|
|
5331
|
+
// reserved taskType). See isFirstKickoffTask() for the full
|
|
5332
|
+
// reasoning and CONTINUATION_TASK_TYPES for the denylist.
|
|
5333
|
+
const isFirstKickoff = await isFirstKickoffTask(taskObj, (parentTaskId) =>
|
|
5334
|
+
fetchTaskStatus(apiUrl, apiKey, parentTaskId),
|
|
5335
|
+
);
|
|
5336
|
+
const repoResult = await ensureRepoForTask(effectiveConfig, role, isFirstKickoff);
|
|
5191
5337
|
currentRepoContext = {
|
|
5192
5338
|
...repoResult,
|
|
5193
5339
|
guidelines: repoConfig?.guidelines ?? null,
|