tryharness 0.7.23 → 0.7.26
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/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- package/package.json +3 -3
package/lib/context.mjs
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
openAtlas,
|
|
6
|
+
treeFiles,
|
|
7
|
+
addFiles,
|
|
8
|
+
embedMissing,
|
|
9
|
+
searchFiles,
|
|
10
|
+
ATLAS_VERSION,
|
|
11
|
+
} from "./atlas.mjs";
|
|
5
12
|
import { readTrail, needs, trailFromTranscript } from "./trails.mjs";
|
|
6
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
featureRows,
|
|
15
|
+
swarmPrior,
|
|
16
|
+
baselineScore,
|
|
17
|
+
train,
|
|
18
|
+
modelScore,
|
|
19
|
+
recallAt,
|
|
20
|
+
loadModel,
|
|
21
|
+
saveModel,
|
|
22
|
+
codeFiles,
|
|
23
|
+
} from "./learn.mjs";
|
|
7
24
|
import { repositoryOf } from "./worktrees.mjs";
|
|
8
25
|
import { createMediaIndex } from "./media.mjs";
|
|
9
26
|
|
|
@@ -28,48 +45,127 @@ const MEDIA_FLOOR = 0.7;
|
|
|
28
45
|
const MEDIA_IN_PACK = 2;
|
|
29
46
|
const MIN_TRAINING_RUNS = 30;
|
|
30
47
|
|
|
31
|
-
const git = (cwd, args) => {
|
|
32
|
-
const
|
|
48
|
+
const git = (cwd, args) => {
|
|
49
|
+
const out = spawnSync("git", args, {
|
|
50
|
+
cwd,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
maxBuffer: 1 << 28,
|
|
53
|
+
});
|
|
54
|
+
return out.status === 0 ? out.stdout : "";
|
|
55
|
+
};
|
|
56
|
+
const commitBefore = (root, at) =>
|
|
57
|
+
git(root, [
|
|
58
|
+
"rev-list",
|
|
59
|
+
"-1",
|
|
60
|
+
`--before=${new Date(at).toISOString()}`,
|
|
61
|
+
"HEAD",
|
|
62
|
+
]).trim() || null;
|
|
33
63
|
// Files changed in the two weeks before a commit, by how often; kept per commit, since a commit's history is fixed.
|
|
34
64
|
const recentByCommit = new Map();
|
|
35
65
|
export function recentChanges(root, commit = "HEAD") {
|
|
36
66
|
const sha = git(root, ["rev-parse", commit]).trim();
|
|
37
67
|
if (recentByCommit.has(sha)) return recentByCommit.get(sha);
|
|
38
68
|
const counts = new Map();
|
|
39
|
-
const since = new Date(
|
|
40
|
-
|
|
69
|
+
const since = new Date(
|
|
70
|
+
(Number(git(root, ["show", "-s", "--format=%ct", sha]).trim()) -
|
|
71
|
+
14 * 86_400) *
|
|
72
|
+
1000
|
|
73
|
+
).toISOString();
|
|
74
|
+
for (const file of git(root, [
|
|
75
|
+
"log",
|
|
76
|
+
`--since=${since}`,
|
|
77
|
+
"--name-only",
|
|
78
|
+
"--format=",
|
|
79
|
+
sha,
|
|
80
|
+
]).split("\n"))
|
|
81
|
+
if (file) counts.set(file, (counts.get(file) ?? 0) + 1);
|
|
41
82
|
if (recentByCommit.size > 500) recentByCommit.clear();
|
|
42
83
|
recentByCommit.set(sha, counts);
|
|
43
84
|
return counts;
|
|
44
85
|
}
|
|
45
86
|
const days = (ms) => Math.round(ms / 86_400_000);
|
|
46
|
-
const AGENT_NAMES = {
|
|
87
|
+
const AGENT_NAMES = {
|
|
88
|
+
"claude-code": "Claude Code",
|
|
89
|
+
codex: "Codex",
|
|
90
|
+
pi: "Pi",
|
|
91
|
+
opencode: "OpenCode",
|
|
92
|
+
};
|
|
47
93
|
|
|
48
94
|
/**
|
|
49
95
|
* Ranks a snapshot's files for a request with every signal the engine has: [{ path, best, score, x }] best first.
|
|
50
96
|
* history: earlier runs in this repository ({ at, edited, read }); model: the learned ranker or null for the default.
|
|
51
97
|
*/
|
|
52
|
-
export async function rankFiles({
|
|
53
|
-
|
|
98
|
+
export async function rankFiles({
|
|
99
|
+
atlas,
|
|
100
|
+
inference,
|
|
101
|
+
query,
|
|
102
|
+
scope,
|
|
103
|
+
history = [],
|
|
104
|
+
at = Date.now(),
|
|
105
|
+
recent = new Map(),
|
|
106
|
+
model = null,
|
|
107
|
+
vectors = null,
|
|
108
|
+
lane = "handoff",
|
|
109
|
+
wordLimit,
|
|
110
|
+
}) {
|
|
111
|
+
const candidates = await searchFiles(atlas, inference, query, scope, {
|
|
112
|
+
vectors,
|
|
113
|
+
lane,
|
|
114
|
+
wordLimit,
|
|
115
|
+
});
|
|
54
116
|
const topWords = Math.max(...candidates.map((c) => c.lexical), 0) || 1;
|
|
55
117
|
const hybrid = (c) => c.dense + 0.25 * (c.lexical / topWords) + 0.2 * c.named;
|
|
56
118
|
const byHybrid = [...candidates].sort((a, b) => hybrid(b) - hybrid(a));
|
|
57
119
|
const prior = swarmPrior(history, at);
|
|
58
120
|
// No cross-encoder pass: on the owner's own sessions it moved recall by under one point (context-recall v3).
|
|
59
|
-
const byDense = [...candidates]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
121
|
+
const byDense = [...candidates]
|
|
122
|
+
.sort((a, b) => b.dense - a.dense)
|
|
123
|
+
.slice(0, POOL)
|
|
124
|
+
.map((c) => c.path);
|
|
125
|
+
const byWords = [...candidates]
|
|
126
|
+
.sort((a, b) => b.lexical - a.lexical)
|
|
127
|
+
.slice(0, POOL)
|
|
128
|
+
.map((c) => c.path);
|
|
129
|
+
const pool = new Set([
|
|
130
|
+
...byDense,
|
|
131
|
+
...byWords,
|
|
132
|
+
...byHybrid.slice(0, POOL).map((c) => c.path),
|
|
133
|
+
...candidates.filter((c) => c.named > 0).map((c) => c.path),
|
|
134
|
+
...[...prior.edits.entries()]
|
|
135
|
+
.sort((a, b) => b[1] - a[1])
|
|
136
|
+
.slice(0, POOL)
|
|
137
|
+
.map(([file]) => file),
|
|
138
|
+
]);
|
|
63
139
|
const best = new Map(candidates.map((c) => [c.path, c.best]));
|
|
64
|
-
const scored = featureRows(
|
|
65
|
-
.
|
|
140
|
+
const scored = featureRows(
|
|
141
|
+
candidates.filter((c) => pool.has(c.path)),
|
|
142
|
+
{ prior, recent }
|
|
143
|
+
).map((row) => ({
|
|
144
|
+
...row,
|
|
145
|
+
best: best.get(row.path),
|
|
146
|
+
score: model ? modelScore(model, row.x) : baselineScore(row.x),
|
|
147
|
+
}));
|
|
66
148
|
return scored.sort((a, b) => b.score - a.score);
|
|
67
149
|
}
|
|
68
150
|
|
|
69
|
-
export function createContextEngine({
|
|
151
|
+
export function createContextEngine({
|
|
152
|
+
store,
|
|
153
|
+
inference,
|
|
154
|
+
home,
|
|
155
|
+
known,
|
|
156
|
+
mediaFolders = () => [],
|
|
157
|
+
mediaReady = () => false,
|
|
158
|
+
log = () => {},
|
|
159
|
+
notify = () => {},
|
|
160
|
+
}) {
|
|
70
161
|
const atlas = openAtlas(path.join(home, "atlas.db"));
|
|
71
162
|
// Recordings and screenshots in folders the person added (media.mjs), once the image and audio part is here.
|
|
72
|
-
const media = createMediaIndex({
|
|
163
|
+
const media = createMediaIndex({
|
|
164
|
+
atlas,
|
|
165
|
+
inference,
|
|
166
|
+
folders: mediaFolders,
|
|
167
|
+
log,
|
|
168
|
+
});
|
|
73
169
|
const modelPath = path.join(home, "ranker.json");
|
|
74
170
|
let model = loadModel(modelPath);
|
|
75
171
|
let refreshing = null;
|
|
@@ -79,8 +175,12 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
79
175
|
/** The vectors for a scope's pieces, read from the atlas once and kept until packs stop for a while. */
|
|
80
176
|
const vectorsFor = (scope) => {
|
|
81
177
|
const wanted = new Set();
|
|
82
|
-
for (const row of scope)
|
|
83
|
-
|
|
178
|
+
for (const row of scope)
|
|
179
|
+
for (const hash of atlas.blob(row.blob) ?? [])
|
|
180
|
+
if (!cached.has(hash)) wanted.add(hash);
|
|
181
|
+
if (wanted.size)
|
|
182
|
+
for (const [hash, vector] of atlas.vectors(wanted, ATLAS_VERSION))
|
|
183
|
+
cached.set(hash, vector);
|
|
84
184
|
clearTimeout(dropCache);
|
|
85
185
|
dropCache = setTimeout(() => cached.clear(), VECTORS_IDLE_MS);
|
|
86
186
|
dropCache.unref?.();
|
|
@@ -91,12 +191,25 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
91
191
|
const placeOf = (folder) => {
|
|
92
192
|
const repo = repositoryOf(folder);
|
|
93
193
|
if (!repo) return null;
|
|
94
|
-
const common = git(repo.root, [
|
|
95
|
-
|
|
194
|
+
const common = git(repo.root, [
|
|
195
|
+
"rev-parse",
|
|
196
|
+
"--path-format=absolute",
|
|
197
|
+
"--git-common-dir",
|
|
198
|
+
]).trim();
|
|
199
|
+
const root =
|
|
200
|
+
common && path.basename(common) === ".git"
|
|
201
|
+
? path.dirname(common)
|
|
202
|
+
: repo.root;
|
|
96
203
|
return { root, top: repo.root };
|
|
97
204
|
};
|
|
98
205
|
|
|
99
|
-
const roots = () => [
|
|
206
|
+
const roots = () => [
|
|
207
|
+
...new Set(
|
|
208
|
+
known()
|
|
209
|
+
.map((place) => placeOf(place.path)?.root)
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
),
|
|
212
|
+
];
|
|
100
213
|
|
|
101
214
|
/** Brings one checkout's files into the atlas; returns its files. Pieces are embedded later on the background lane. */
|
|
102
215
|
const indexTree = (top, commit = null) => {
|
|
@@ -108,7 +221,9 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
108
221
|
/** Records every coding session that ran in a known repository: what it was asked and what it needed. */
|
|
109
222
|
const recordRuns = async () => {
|
|
110
223
|
const rootList = roots();
|
|
111
|
-
const since = new Date(
|
|
224
|
+
const since = new Date(
|
|
225
|
+
Date.now() - HISTORY_DAYS * 86_400_000
|
|
226
|
+
).toISOString();
|
|
112
227
|
const trees = new Map();
|
|
113
228
|
let added = 0;
|
|
114
229
|
for (const session of store.sessionSources({ since })) {
|
|
@@ -116,16 +231,50 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
116
231
|
if (!place || !rootList.includes(place.root)) continue;
|
|
117
232
|
// A session whose source file the agent has since deleted is read from the relay's own copy, once.
|
|
118
233
|
let mtime = 0;
|
|
119
|
-
try {
|
|
234
|
+
try {
|
|
235
|
+
mtime = fs.statSync(session.source).mtimeMs;
|
|
236
|
+
} catch {}
|
|
120
237
|
if (atlas.runSeen(session.agent, session.session_id) === mtime) continue;
|
|
121
|
-
const trail = mtime
|
|
122
|
-
|
|
238
|
+
const trail = mtime
|
|
239
|
+
? await readTrail(session.agent, session.source)
|
|
240
|
+
: trailFromTranscript(
|
|
241
|
+
session.agent,
|
|
242
|
+
store.sessionTranscript(session.agent, session.session_id),
|
|
243
|
+
{
|
|
244
|
+
cwd: session.cwd,
|
|
245
|
+
startedAt: Date.parse(session.started_at) || null,
|
|
246
|
+
}
|
|
247
|
+
);
|
|
123
248
|
if (!trail?.brief || trail.startedAt === null) continue;
|
|
124
249
|
const commit = commitBefore(place.root, trail.startedAt);
|
|
125
250
|
if (!commit) continue;
|
|
126
|
-
if (!trees.has(commit))
|
|
127
|
-
|
|
128
|
-
|
|
251
|
+
if (!trees.has(commit))
|
|
252
|
+
trees.set(
|
|
253
|
+
commit,
|
|
254
|
+
new Set(treeFiles(place.root, commit).map((row) => row.path))
|
|
255
|
+
);
|
|
256
|
+
const need = needs(
|
|
257
|
+
{
|
|
258
|
+
...trail,
|
|
259
|
+
cwd:
|
|
260
|
+
trail.cwd && placeOf(trail.cwd)?.top === place.top
|
|
261
|
+
? path.join(place.root, path.relative(place.top, trail.cwd))
|
|
262
|
+
: trail.cwd,
|
|
263
|
+
},
|
|
264
|
+
place.root,
|
|
265
|
+
trees.get(commit)
|
|
266
|
+
);
|
|
267
|
+
atlas.putRun({
|
|
268
|
+
agent: session.agent,
|
|
269
|
+
sessionId: session.session_id,
|
|
270
|
+
root: place.root,
|
|
271
|
+
at: trail.startedAt,
|
|
272
|
+
brief: trail.brief,
|
|
273
|
+
edited: need.edited,
|
|
274
|
+
read: need.read,
|
|
275
|
+
before: need.beforeFirstEdit,
|
|
276
|
+
sourceMtime: mtime,
|
|
277
|
+
});
|
|
129
278
|
added += 1;
|
|
130
279
|
}
|
|
131
280
|
return added;
|
|
@@ -135,8 +284,17 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
135
284
|
for (;;) {
|
|
136
285
|
const rows = atlas.runsToEmbed(ATLAS_VERSION, 16);
|
|
137
286
|
if (!rows.length) return;
|
|
138
|
-
const vectors = await inference.embed(
|
|
139
|
-
|
|
287
|
+
const vectors = await inference.embed(
|
|
288
|
+
rows.map((row) => ({
|
|
289
|
+
kind: "document",
|
|
290
|
+
title: "none",
|
|
291
|
+
text: row.brief,
|
|
292
|
+
})),
|
|
293
|
+
{ lane }
|
|
294
|
+
);
|
|
295
|
+
rows.forEach((row, i) =>
|
|
296
|
+
atlas.putRunVector(row.agent, row.session_id, ATLAS_VERSION, vectors[i])
|
|
297
|
+
);
|
|
140
298
|
}
|
|
141
299
|
};
|
|
142
300
|
|
|
@@ -147,7 +305,8 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
147
305
|
// A first pass over a repository waits for the charger; small catch-ups run on battery too unless it is low.
|
|
148
306
|
if (power.paused()) return { skipped: "power" };
|
|
149
307
|
const first = !atlas.roots().length;
|
|
150
|
-
if (first && !power.onCharger())
|
|
308
|
+
if (first && !power.onCharger())
|
|
309
|
+
return { skipped: "waiting for the charger" };
|
|
151
310
|
let files = 0;
|
|
152
311
|
for (const root of roots()) {
|
|
153
312
|
const rows = indexTree(root);
|
|
@@ -155,15 +314,30 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
155
314
|
files += rows.length;
|
|
156
315
|
}
|
|
157
316
|
const runs = await recordRuns();
|
|
158
|
-
const pieces = await embedMissing(atlas, inference, {
|
|
317
|
+
const pieces = await embedMissing(atlas, inference, {
|
|
318
|
+
lane: "background",
|
|
319
|
+
});
|
|
159
320
|
await embedRuns("background");
|
|
160
|
-
const recorded =
|
|
321
|
+
const recorded =
|
|
322
|
+
mediaReady() && mediaFolders().length && power.onCharger()
|
|
323
|
+
? await media.refresh({ lane: "background" })
|
|
324
|
+
: null;
|
|
161
325
|
return { files, runs, pieces, ...(recorded ? { media: recorded } : {}) };
|
|
162
|
-
})()
|
|
326
|
+
})()
|
|
327
|
+
.catch((error) => {
|
|
328
|
+
log(`context engine: ${error.message}`);
|
|
329
|
+
return { error: error.message };
|
|
330
|
+
})
|
|
331
|
+
.finally(() => {
|
|
332
|
+
refreshing = null;
|
|
333
|
+
});
|
|
163
334
|
return refreshing;
|
|
164
335
|
};
|
|
165
336
|
|
|
166
|
-
const historyFor = (root) =>
|
|
337
|
+
const historyFor = (root) =>
|
|
338
|
+
atlas
|
|
339
|
+
.runs(root)
|
|
340
|
+
.map((run) => ({ at: run.at, edited: run.edited, read: run.read }));
|
|
167
341
|
|
|
168
342
|
/**
|
|
169
343
|
* The context pack for a brief going to a local agent working in folder: { files, earlier, text }. Files are the most
|
|
@@ -177,10 +351,21 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
177
351
|
// A few changed pieces are embedded now; a repository the relay has not indexed yet is searched by words and names
|
|
178
352
|
// this time while the background pass embeds it.
|
|
179
353
|
const missing = atlas.missing(ATLAS_VERSION);
|
|
180
|
-
if (missing && missing <= 400)
|
|
354
|
+
if (missing && missing <= 400)
|
|
355
|
+
await embedMissing(atlas, inference, { lane });
|
|
181
356
|
else if (missing) void refresh();
|
|
182
357
|
const history = historyFor(place.root);
|
|
183
|
-
const ranked = await rankFiles({
|
|
358
|
+
const ranked = await rankFiles({
|
|
359
|
+
atlas,
|
|
360
|
+
inference,
|
|
361
|
+
query: String(brief).slice(0, 4_000),
|
|
362
|
+
scope,
|
|
363
|
+
history,
|
|
364
|
+
recent: recentChanges(place.top),
|
|
365
|
+
model,
|
|
366
|
+
lane,
|
|
367
|
+
vectors: vectorsFor(scope),
|
|
368
|
+
});
|
|
184
369
|
const prior = swarmPrior(history, Date.now());
|
|
185
370
|
const files = ranked.slice(0, PACK_FILES).map((row) => {
|
|
186
371
|
const piece = row.best ? atlas.piece(row.best) : null;
|
|
@@ -188,30 +373,86 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
188
373
|
if (row.x.named) why.push("named in the request");
|
|
189
374
|
if (row.x.lexical > 0.5) why.push("shares the request's words");
|
|
190
375
|
if (prior.edits.has(row.path)) why.push("recently edited by your agents");
|
|
191
|
-
return {
|
|
376
|
+
return {
|
|
377
|
+
path: row.path,
|
|
378
|
+
line: piece?.line ?? 1,
|
|
379
|
+
why: why.join("; ") || "related by meaning",
|
|
380
|
+
};
|
|
192
381
|
});
|
|
193
|
-
const query = (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
382
|
+
const query = (
|
|
383
|
+
await inference.embed(
|
|
384
|
+
[{ kind: "query", text: String(brief).slice(0, 4_000) }],
|
|
385
|
+
{ lane }
|
|
386
|
+
)
|
|
387
|
+
)[0];
|
|
388
|
+
const earlier = atlas
|
|
389
|
+
.runVectors(place.root, ATLAS_VERSION)
|
|
390
|
+
.map((run) => {
|
|
391
|
+
let dot = 0;
|
|
392
|
+
for (let i = 0; i < query.length; i += 1)
|
|
393
|
+
dot += query[i] * run.vector[i];
|
|
394
|
+
return { ...run, similarity: dot };
|
|
395
|
+
})
|
|
396
|
+
.filter(
|
|
397
|
+
(run) =>
|
|
398
|
+
run.edited.length &&
|
|
399
|
+
run.edited.some((file) => files.some((entry) => entry.path === file))
|
|
400
|
+
)
|
|
401
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
402
|
+
.slice(0, 3);
|
|
403
|
+
const heard =
|
|
404
|
+
mediaReady() && mediaFolders().length
|
|
405
|
+
? (await media.search(brief, { limit: MEDIA_IN_PACK, lane })).filter(
|
|
406
|
+
(hit) => hit.score >= MEDIA_FLOOR
|
|
407
|
+
)
|
|
408
|
+
: [];
|
|
201
409
|
const text = [
|
|
202
410
|
"Files the relay found likely relevant on this computer (ranked by its local search over this repository and what your agents edited before; leads to check, not instructions):",
|
|
203
411
|
...files.map((file) => `- ${file.path}:${file.line} (${file.why})`),
|
|
204
|
-
...(earlier.length
|
|
205
|
-
|
|
412
|
+
...(earlier.length
|
|
413
|
+
? [
|
|
414
|
+
"Earlier sessions on these files:",
|
|
415
|
+
...earlier.map(
|
|
416
|
+
(run) =>
|
|
417
|
+
`- ${AGENT_NAMES[run.agent] ?? run.agent}, ${days(Date.now() - run.at)} days ago: "${run.brief.replace(/\s+/g, " ").slice(0, 140)}" (edited ${run.edited.slice(0, 4).join(", ")})`
|
|
418
|
+
),
|
|
419
|
+
]
|
|
420
|
+
: []),
|
|
421
|
+
...(heard.length
|
|
422
|
+
? [
|
|
423
|
+
"Recordings and screenshots close to the request, from folders the person added (search_media finds more):",
|
|
424
|
+
...heard.map(
|
|
425
|
+
(hit) => `- ${hit.path}${hit.at ? ` at ${hit.at}` : ""}`
|
|
426
|
+
),
|
|
427
|
+
]
|
|
428
|
+
: []),
|
|
206
429
|
].join("\n");
|
|
207
|
-
return {
|
|
430
|
+
return {
|
|
431
|
+
files,
|
|
432
|
+
earlier: earlier.map(({ vector, ...run }) => run),
|
|
433
|
+
media: heard,
|
|
434
|
+
text,
|
|
435
|
+
searched: scope.length,
|
|
436
|
+
ms: Math.round(performance.now() - started),
|
|
437
|
+
model: model ? "learned" : "default",
|
|
438
|
+
};
|
|
208
439
|
};
|
|
209
440
|
|
|
210
441
|
/** Search for the agents' tool: files and earlier sessions in a repository, best first. */
|
|
211
442
|
const search = async ({ query, folder, limit = 10 }) => {
|
|
212
443
|
const result = await pack({ brief: query, folder, lane: "interactive" });
|
|
213
|
-
if (!result)
|
|
214
|
-
|
|
444
|
+
if (!result)
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
error: "that folder is not in a git repository the relay indexes",
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
ok: true,
|
|
451
|
+
files: result.files.slice(0, limit),
|
|
452
|
+
earlier: result.earlier,
|
|
453
|
+
model: result.model,
|
|
454
|
+
ms: result.ms,
|
|
455
|
+
};
|
|
215
456
|
};
|
|
216
457
|
|
|
217
458
|
/**
|
|
@@ -220,56 +461,126 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
220
461
|
*/
|
|
221
462
|
const learn = async ({ force = false } = {}) => {
|
|
222
463
|
const last = Number(atlas.meta("learned_at") || 0);
|
|
223
|
-
if (
|
|
464
|
+
if (
|
|
465
|
+
!force &&
|
|
466
|
+
(Date.now() - last < LEARN_EVERY_MS ||
|
|
467
|
+
!inference.power.onCharger() ||
|
|
468
|
+
inference.power.paused())
|
|
469
|
+
)
|
|
470
|
+
return null;
|
|
224
471
|
await refresh();
|
|
225
472
|
const report = { at: new Date().toISOString(), repositories: [] };
|
|
226
473
|
const rows = [];
|
|
227
474
|
for (const root of roots()) {
|
|
228
|
-
const runs = atlas
|
|
475
|
+
const runs = atlas
|
|
476
|
+
.runs(root, Date.now() - HISTORY_DAYS * 86_400_000)
|
|
477
|
+
.filter((run) => run.edited.length);
|
|
229
478
|
if (!runs.length) continue;
|
|
230
479
|
const snapshots = new Map();
|
|
231
480
|
for (const run of runs) {
|
|
232
481
|
const commit = commitBefore(root, run.at);
|
|
233
482
|
if (!commit) continue;
|
|
234
|
-
if (!snapshots.has(commit))
|
|
483
|
+
if (!snapshots.has(commit))
|
|
484
|
+
snapshots.set(commit, indexTree(root, commit));
|
|
235
485
|
rows.push({ root, run, commit });
|
|
236
486
|
}
|
|
237
|
-
report.repositories.push({
|
|
487
|
+
report.repositories.push({
|
|
488
|
+
root,
|
|
489
|
+
runs: runs.length,
|
|
490
|
+
snapshots: snapshots.size,
|
|
491
|
+
});
|
|
238
492
|
}
|
|
239
493
|
await embedMissing(atlas, inference, { lane: "background" });
|
|
240
494
|
rows.sort((a, b) => a.run.at - b.run.at);
|
|
241
|
-
if (rows.length < MIN_TRAINING_RUNS) {
|
|
495
|
+
if (rows.length < MIN_TRAINING_RUNS) {
|
|
496
|
+
atlas.meta("learned_at", Date.now());
|
|
497
|
+
return { ...report, kept: false, why: `only ${rows.length} runs so far` };
|
|
498
|
+
}
|
|
242
499
|
const featured = [];
|
|
243
500
|
for (const { root, run, commit } of rows) {
|
|
244
501
|
const history = historyFor(root).filter((other) => other.at < run.at);
|
|
245
|
-
const ranked = await rankFiles({
|
|
502
|
+
const ranked = await rankFiles({
|
|
503
|
+
atlas,
|
|
504
|
+
inference,
|
|
505
|
+
query: run.brief,
|
|
506
|
+
scope: treeFiles(root, commit),
|
|
507
|
+
history,
|
|
508
|
+
at: run.at,
|
|
509
|
+
recent: recentChanges(root, commit),
|
|
510
|
+
model: null,
|
|
511
|
+
lane: "background",
|
|
512
|
+
});
|
|
246
513
|
featured.push({ run, ranked });
|
|
247
514
|
}
|
|
248
515
|
const cut = Math.floor(featured.length * (1 - HELD_OUT_SHARE));
|
|
249
|
-
const label = (run, row) =>
|
|
250
|
-
|
|
516
|
+
const label = (run, row) =>
|
|
517
|
+
codeFiles(run.edited).includes(row.path) ? 1 : 0;
|
|
518
|
+
const trainRows = featured
|
|
519
|
+
.slice(0, cut)
|
|
520
|
+
.flatMap(({ run, ranked }) =>
|
|
521
|
+
ranked.map((row) => ({ x: row.x, y: label(run, row) }))
|
|
522
|
+
);
|
|
251
523
|
const candidate = train(trainRows);
|
|
252
|
-
const heldOut = featured
|
|
253
|
-
|
|
254
|
-
|
|
524
|
+
const heldOut = featured
|
|
525
|
+
.slice(cut)
|
|
526
|
+
.filter(({ run }) => codeFiles(run.edited).length);
|
|
527
|
+
const recall = (score) =>
|
|
528
|
+
heldOut.reduce(
|
|
529
|
+
(sum, { run, ranked }) =>
|
|
530
|
+
sum +
|
|
531
|
+
recallAt(
|
|
532
|
+
[...ranked]
|
|
533
|
+
.sort((a, b) => score(b.x) - score(a.x))
|
|
534
|
+
.map((row) => row.path),
|
|
535
|
+
codeFiles(run.edited),
|
|
536
|
+
10
|
|
537
|
+
),
|
|
538
|
+
0
|
|
539
|
+
) / heldOut.length;
|
|
540
|
+
const result = {
|
|
541
|
+
default: recall(baselineScore),
|
|
542
|
+
current: model ? recall((x) => modelScore(model, x)) : null,
|
|
543
|
+
candidate: recall((x) => modelScore(candidate, x)),
|
|
544
|
+
};
|
|
255
545
|
const bar = Math.max(result.default, result.current ?? 0);
|
|
256
546
|
const kept = result.candidate > bar;
|
|
257
547
|
if (kept) {
|
|
258
548
|
// Retrained on every run before it ships; the held-out score is the one reported.
|
|
259
|
-
model = {
|
|
549
|
+
model = {
|
|
550
|
+
...train(
|
|
551
|
+
featured.flatMap(({ run, ranked }) =>
|
|
552
|
+
ranked.map((row) => ({ x: row.x, y: label(run, row) }))
|
|
553
|
+
)
|
|
554
|
+
),
|
|
555
|
+
trainedAt: report.at,
|
|
556
|
+
heldOut: result,
|
|
557
|
+
runs: featured.length,
|
|
558
|
+
};
|
|
260
559
|
saveModel(modelPath, model);
|
|
261
560
|
}
|
|
262
561
|
atlas.meta("learned_at", Date.now());
|
|
263
|
-
const full = {
|
|
562
|
+
const full = {
|
|
563
|
+
...report,
|
|
564
|
+
runs: featured.length,
|
|
565
|
+
heldOutRuns: heldOut.length,
|
|
566
|
+
recallAt10: result,
|
|
567
|
+
kept,
|
|
568
|
+
};
|
|
264
569
|
atlas.meta("last_learning", JSON.stringify(full));
|
|
265
|
-
if (kept)
|
|
570
|
+
if (kept)
|
|
571
|
+
notify(
|
|
572
|
+
`Learned from ${featured.length} runs overnight: the files your agents went on to change are now in their briefs' top ten ${Math.round(result.candidate * 100)}% of the time (was ${Math.round(bar * 100)}%).`
|
|
573
|
+
);
|
|
266
574
|
return full;
|
|
267
575
|
};
|
|
268
576
|
|
|
269
577
|
return {
|
|
270
578
|
atlas,
|
|
271
579
|
/** A folder's files as the atlas holds them now ([{ path, blob }]), indexed first; [] outside a repository. */
|
|
272
|
-
scope: (folder) => {
|
|
580
|
+
scope: (folder) => {
|
|
581
|
+
const place = folder && placeOf(folder);
|
|
582
|
+
return place ? indexTree(place.top) : [];
|
|
583
|
+
},
|
|
273
584
|
pack,
|
|
274
585
|
search,
|
|
275
586
|
/** Recordings' minutes and screenshots closest to a question, for an agent's search_media. */
|
|
@@ -277,14 +588,41 @@ export function createContextEngine({ store, inference, home, known, mediaFolder
|
|
|
277
588
|
refreshMedia: () => media.refresh({ lane: "background" }),
|
|
278
589
|
refresh,
|
|
279
590
|
learn,
|
|
280
|
-
report: () => {
|
|
281
|
-
|
|
591
|
+
report: () => {
|
|
592
|
+
try {
|
|
593
|
+
return JSON.parse(atlas.meta("last_learning") || "null");
|
|
594
|
+
} catch {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
status: () => ({
|
|
599
|
+
roots: atlas.roots(),
|
|
600
|
+
pieces: { waiting: atlas.missing(ATLAS_VERSION) },
|
|
601
|
+
media: media.status(),
|
|
602
|
+
model: model
|
|
603
|
+
? {
|
|
604
|
+
trainedAt: model.trainedAt,
|
|
605
|
+
heldOut: model.heldOut,
|
|
606
|
+
runs: model.runs,
|
|
607
|
+
}
|
|
608
|
+
: null,
|
|
609
|
+
device: inference.device(),
|
|
610
|
+
}),
|
|
282
611
|
start() {
|
|
283
|
-
const tick = () => {
|
|
612
|
+
const tick = () => {
|
|
613
|
+
void refresh()
|
|
614
|
+
.then(() => learn())
|
|
615
|
+
.catch((error) => log(`context engine: ${error.message}`));
|
|
616
|
+
};
|
|
284
617
|
setTimeout(tick, 60_000).unref?.();
|
|
285
618
|
timer = setInterval(tick, REFRESH_MS);
|
|
286
619
|
timer.unref?.();
|
|
287
620
|
},
|
|
288
|
-
stop() {
|
|
621
|
+
stop() {
|
|
622
|
+
clearInterval(timer);
|
|
623
|
+
clearTimeout(dropCache);
|
|
624
|
+
cached.clear();
|
|
625
|
+
atlas.close();
|
|
626
|
+
},
|
|
289
627
|
};
|
|
290
628
|
}
|
package/lib/control-client.mjs
CHANGED
|
@@ -19,8 +19,11 @@ export function daemonRequest(message, timeoutMs = 5_000) {
|
|
|
19
19
|
buffer += chunk;
|
|
20
20
|
const newline = buffer.indexOf("\n");
|
|
21
21
|
if (newline < 0) return;
|
|
22
|
-
try {
|
|
23
|
-
|
|
22
|
+
try {
|
|
23
|
+
finish(null, JSON.parse(buffer.slice(0, newline)));
|
|
24
|
+
} catch {
|
|
25
|
+
finish(new Error("daemon returned invalid JSON"));
|
|
26
|
+
}
|
|
24
27
|
});
|
|
25
28
|
socket.on("timeout", () => finish(new Error("daemon request timed out")));
|
|
26
29
|
socket.on("error", (error) => finish(error));
|