tryharness 0.7.28 → 0.7.29
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 +5 -4
- package/cli.mjs +32 -7
- package/lib/atlas.mjs +155 -31
- package/lib/context.mjs +44 -32
- package/lib/daemon.mjs +193 -34
- package/lib/desktop.mjs +24 -1
- package/lib/library.mjs +693 -0
- package/lib/mcp-stdio.mjs +12 -6
- package/lib/readers.mjs +459 -0
- package/lib/service.mjs +21 -1
- package/lib/snippets.mjs +114 -7
- package/lib/store.mjs +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,10 +68,11 @@ on low battery and when the machine is hot.
|
|
|
68
68
|
What they do:
|
|
69
69
|
|
|
70
70
|
- **Briefs:** every brief a local coding agent gets carries a context pack (the files in its
|
|
71
|
-
repository most likely needed, earlier sessions on them, and a
|
|
72
|
-
|
|
71
|
+
repository most likely needed, earlier sessions on them, and a page, passage, picture or
|
|
72
|
+
recording's minute from your added folders or the thread's files when one stands out) and the
|
|
73
|
+
thread's task frame: what you
|
|
73
74
|
named resolved to file and line, the repository's state, other threads changing the same
|
|
74
|
-
repository, and each run's receipt. Agents can call `search_code`, `
|
|
75
|
+
repository, and each run's receipt. Agents can call `search_code`, `search_files`,
|
|
75
76
|
`compare_screens` (two screenshots of UI work, before and after: what changed and which text)
|
|
76
77
|
and `search_screen` (what you saw in tabs shared with Harness, asked of your own Harness window
|
|
77
78
|
sealed over the `tryharness link` line, so Harness's server carries it unread; your Allow in
|
|
@@ -91,7 +92,7 @@ What they do:
|
|
|
91
92
|
```sh
|
|
92
93
|
npx tryharness local # what is installed, where it runs, what is indexed, the last learning
|
|
93
94
|
npx tryharness local learn # start the learner now
|
|
94
|
-
npx tryharness local add ~/
|
|
95
|
+
npx tryharness local add ~/Documents # a folder agents can search by what is in its files
|
|
95
96
|
npx tryharness watch # watches and what each last read
|
|
96
97
|
npx tryharness watch add "keeper heartbeat" --run "cat state/heartbeat.json" --age completedAt --above 900 --every 5m
|
|
97
98
|
```
|
package/cli.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "./lib/localmodels.mjs";
|
|
30
30
|
import {
|
|
31
31
|
installService,
|
|
32
|
+
launcherMissing,
|
|
32
33
|
serviceInfo,
|
|
33
34
|
uninstallService,
|
|
34
35
|
} from "./lib/service.mjs";
|
|
@@ -300,6 +301,11 @@ async function connect() {
|
|
|
300
301
|
spin.start("Getting the newest desktop app");
|
|
301
302
|
const shown = await desktop();
|
|
302
303
|
spin.stop(shown.text, shown.ok ? 0 : 2);
|
|
304
|
+
// The app brings the daemon's launcher: the daemon starts through it from now on (service.mjs).
|
|
305
|
+
if (launcherMissing()) {
|
|
306
|
+
installService({ runtimeRoot });
|
|
307
|
+
await waitForDaemon().catch(() => {});
|
|
308
|
+
}
|
|
303
309
|
}
|
|
304
310
|
// The relay's core local models (localmodels.mjs) and the server that runs them come down with setup, so agents get
|
|
305
311
|
// context packs, code search, the handoff firewall and images checked before they leave from the first task. Each file
|
|
@@ -361,6 +367,10 @@ async function connect() {
|
|
|
361
367
|
);
|
|
362
368
|
}
|
|
363
369
|
ui.info("History indexes locally in the background");
|
|
370
|
+
if (!readJson(paths.config, {}).libraryOff)
|
|
371
|
+
ui.info(
|
|
372
|
+
`Your agents on this computer can search what is in your Desktop, Documents and Downloads. It stays on this computer${process.platform === "darwin" ? "; your Mac asks you once, when Harness first reads them" : ""}.\n${pc.dim("Change it: npx tryharness local off")}`
|
|
373
|
+
);
|
|
364
374
|
ui.info(
|
|
365
375
|
`Your coding agents start the tasks you ask for in folders they have worked in, up to 10 a day each, without a Start. Tasks another agent wrote wait for you.\n${pc.dim("Change it: npx tryharness board standing")}`
|
|
366
376
|
);
|
|
@@ -426,9 +436,20 @@ async function status() {
|
|
|
426
436
|
|
|
427
437
|
// The relay's local models: each one's job, licence, size and whether it is here, where they run, what the context engine
|
|
428
438
|
// has indexed, and what the overnight learner last did. `tryharness local learn` starts the learner now; `local add
|
|
429
|
-
// <folder>` and `local remove <folder>` name folders
|
|
439
|
+
// <folder>` and `local remove <folder>` name folders whose files agents search (search_files) and briefs draw on.
|
|
430
440
|
async function local() {
|
|
431
441
|
const root = path.join(paths.home, "local");
|
|
442
|
+
if (argv[0] === "on" || argv[0] === "off") {
|
|
443
|
+
const response = await daemonRequest({ command: `local.${argv[0]}` });
|
|
444
|
+
if (!response?.ok)
|
|
445
|
+
throw new Error(response?.error || "the daemon did not answer");
|
|
446
|
+
console.log(
|
|
447
|
+
argv[0] === "on"
|
|
448
|
+
? `Your agents on this computer can search what is in ${response.folders.join(", ") || "your folders"}.`
|
|
449
|
+
: "Your folders are no longer searched, and what was read from them is forgotten. Your threads' files still are."
|
|
450
|
+
);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
432
453
|
if (argv[0] === "add" || argv[0] === "remove") {
|
|
433
454
|
const response = await daemonRequest({
|
|
434
455
|
command: `local.${argv[0]}`,
|
|
@@ -438,7 +459,7 @@ async function local() {
|
|
|
438
459
|
throw new Error(response?.error || "the daemon did not answer");
|
|
439
460
|
console.log(
|
|
440
461
|
argv[0] === "add"
|
|
441
|
-
? `Added. The relay reads the
|
|
462
|
+
? `Added. The relay reads the files there in the background: documents and PDFs now, pictures, recordings and videos${response.downloading ? " once the image and audio part of the search model has downloaded (555 MB)" : " too"}.`
|
|
442
463
|
: "Removed."
|
|
443
464
|
);
|
|
444
465
|
return;
|
|
@@ -485,10 +506,14 @@ async function local() {
|
|
|
485
506
|
? `ranking: learned ${current.model.trainedAt}, held-out recall at 10 ${JSON.stringify(current.model.heldOut)}`
|
|
486
507
|
: "ranking: the default until the learner has enough runs"
|
|
487
508
|
);
|
|
488
|
-
for (const folder of current.
|
|
489
|
-
console.log(`
|
|
490
|
-
if (current.
|
|
491
|
-
console.log(
|
|
509
|
+
for (const folder of current.library?.folders || [])
|
|
510
|
+
console.log(`searched by what is in it: ${folder}`);
|
|
511
|
+
if (current.library?.files)
|
|
512
|
+
console.log(
|
|
513
|
+
`files read: ${current.library.files}${current.library.waiting ? ` (${current.library.waiting} parts waiting for the model)` : ""}`
|
|
514
|
+
);
|
|
515
|
+
for (const skip of (current.library?.skipped || []).slice(0, 5))
|
|
516
|
+
console.log(`not read: ${skip.path} (${skip.reason})`);
|
|
492
517
|
if (current.learning)
|
|
493
518
|
console.log(
|
|
494
519
|
`last learning: ${current.learning.at} · ${current.learning.kept ? "kept a new model" : `kept the old one${current.learning.why ? ` (${current.learning.why})` : ""}`}`
|
|
@@ -1752,7 +1777,7 @@ async function t3() {
|
|
|
1752
1777
|
|
|
1753
1778
|
function help() {
|
|
1754
1779
|
console.log(
|
|
1755
|
-
`tryharness ${VERSION}\n\nCommands:\n tryharness set up everything and start the daemon\n tryharness desktop open the Harness desktop app (setup opens it too; --no-desktop skips it)\n tryharness status show daemon, bridge, agent, and index health\n tryharness doctor diagnose the complete connection\n tryharness local [learn] the relay's local models, where they run, what is indexed and the last learning\n tryharness local
|
|
1780
|
+
`tryharness ${VERSION}\n\nCommands:\n tryharness set up everything and start the daemon\n tryharness desktop open the Harness desktop app (setup opens it too; --no-desktop skips it)\n tryharness status show daemon, bridge, agent, and index health\n tryharness doctor diagnose the complete connection\n tryharness local [learn] the relay's local models, where they run, what is indexed and the last learning\n tryharness local on|off search over your Desktop, Documents and Downloads by what is in the files (on by default)\n tryharness local add|remove <folder> another folder to search the same way\n tryharness watch [add <name> --run <command> [--every 5m] [--field f | --age f | --count text] [--above n] [--below n] | start <id> | remove <id>]\n watches the relay checks and says out loud only when they are off\n tryharness sync [--agent] rescan local coding history in the background\n tryharness task <id> --follow\n follow a delegated agent run in this terminal\n tryharness board [--all] list open work on the machine's task board\n tryharness board post --for <agent> --objective <text> [--file <brief.md>]\n tryharness board show <id>\n tryharness board post --for codex --objective <text> --run --workspace <dir>\n the daemon runs it and reports back on the task\n tryharness board run <id> authorize a run an agent asked for\n tryharness board release <id>\n tryharness board agents when each agent last read the board\n tryharness board --watch follow the board in this terminal\n tryharness notify on|off desktop notifications for what waits on you\n tryharness board send <task> <message> send a held reply to another person's agent\n tryharness board snippet [<id> share|keep] code an agent elsewhere asked to see; nothing leaves until you share it\n tryharness board auto <task> on|off auto-send replies on that thread\n tryharness board end <task> --outcome <text>\n tryharness board context <task> <query> search everything written about a task\n tryharness goal set --objective <text> [--team a,b] [--workspace <dir>] a goal your agents work together in one room\n tryharness goal [list | show <id> | stop <id> | resume <id>]\n tryharness room <goal> | room post <goal> <text> [--ask agent,agent]\n tryharness swarm rounds [<thread>] what the coordinator saw, chose and cost each round\n tryharness swarm health [<thread>] who decided each day's rounds: Harness, a backup, or no one\n tryharness swarm ceiling <thread> <dollars|off> what a thread may spend of your Harness credits\n tryharness swarm coordinator [harness|local] who coordinates your threads\n tryharness connect <handle/agent> --about <text> ask another person's agent to work an issue with yours, sealed to their computer\n tryharness peers the keys of other people's computers this one has worked with, to compare\n tryharness agents how Harness wakes each of your agents, and set up the rest\n tryharness agents setup <agent> | agents test <agent>\n tryharness agents cards what each of your agents is told about the others\n tryharness agents note <agent> <text> [--clear] your own note on an agent's card, told to every other agent\n tryharness agents models [<agent>] [--refresh] the models each coding agent lists and the one it runs on\n tryharness agents model <agent> <model> [--effort <level>] | auto | default the model it runs on; auto lets Harness pick per run\n tryharness agents routing "<text>" [--clear] how Auto picks models for your swarm\n tryharness mcp grok-bot print the MCP snippet that puts Grok Bot on the board\n tryharness link connect the browser you use Harness in to this computer, end to end (--print shows the link)\n tryharness phone pair show a QR code that pairs your phone with this computer\n tryharness phone list | revoke <id>\n tryharness t3 approve Harness in T3 Code once, so your messages go into its threads (t3 status | t3 forget)\n tryharness logs [--follow]\n tryharness disconnect\n`
|
|
1756
1781
|
);
|
|
1757
1782
|
}
|
|
1758
1783
|
|
package/lib/atlas.mjs
CHANGED
|
@@ -136,9 +136,13 @@ export function openAtlas(file) {
|
|
|
136
136
|
create index if not exists runs_root_at on runs(root, at);
|
|
137
137
|
create table if not exists run_vectors (agent text not null, session_id text not null, version text not null, vector blob not null, primary key(agent, session_id, version));
|
|
138
138
|
create table if not exists meta (key text primary key, value text not null);
|
|
139
|
-
create table if not exists
|
|
140
|
-
|
|
139
|
+
create table if not exists library (file text not null, vec text not null, source text not null, name text not null,
|
|
140
|
+
kind text not null, modality text not null, loc real not null, loc_end real, mtime real not null, embed integer not null,
|
|
141
|
+
primary key(file, vec));
|
|
142
|
+
create index if not exists library_source on library(source);
|
|
143
|
+
create virtual table if not exists library_fts using fts5(file unindexed, vec unindexed, name, text);
|
|
141
144
|
`);
|
|
145
|
+
migrateMedia(db);
|
|
142
146
|
// A blob's pieces never change, so lookups are kept (a few MB for a large repository and its recent versions).
|
|
143
147
|
const blobPieces = new Map();
|
|
144
148
|
const run = (sql, ...values) =>
|
|
@@ -327,35 +331,8 @@ export function openAtlas(file) {
|
|
|
327
331
|
edited: JSON.parse(row.edited),
|
|
328
332
|
vector: fromBlob(row.vector),
|
|
329
333
|
})),
|
|
330
|
-
/**
|
|
331
|
-
|
|
332
|
-
get("select max(mtime) as mtime from media where path = ?", file)
|
|
333
|
-
?.mtime ?? null,
|
|
334
|
-
mediaPaths: () =>
|
|
335
|
-
all("select distinct path from media").map((row) => row.path),
|
|
336
|
-
dropMedia: (file) => run("delete from media where path = ?", file),
|
|
337
|
-
putMedia: (row) =>
|
|
338
|
-
run(
|
|
339
|
-
"insert or replace into media(hash, path, kind, start_ms, end_ms, mtime) values(?, ?, ?, ?, ?, ?)",
|
|
340
|
-
row.hash,
|
|
341
|
-
row.path,
|
|
342
|
-
row.kind,
|
|
343
|
-
row.startMs,
|
|
344
|
-
row.endMs,
|
|
345
|
-
row.mtime
|
|
346
|
-
),
|
|
347
|
-
mediaVectors: (version) =>
|
|
348
|
-
all(
|
|
349
|
-
"select m.hash, m.path, m.kind, m.start_ms, m.end_ms, v.vector from media m join piece_vectors v on v.hash = m.hash and v.version = ?",
|
|
350
|
-
version
|
|
351
|
-
).map((row) => ({
|
|
352
|
-
hash: row.hash,
|
|
353
|
-
path: row.path,
|
|
354
|
-
kind: row.kind,
|
|
355
|
-
startMs: Number(row.start_ms),
|
|
356
|
-
endMs: Number(row.end_ms),
|
|
357
|
-
vector: fromBlob(row.vector),
|
|
358
|
-
})),
|
|
334
|
+
/** The library (library.mjs): parts of the person's files and the threads' files, their vectors beside the pieces'. */
|
|
335
|
+
library: libraryTables(run, all, get),
|
|
359
336
|
/** Full-text matches: { hash, rank } best first (bm25, lower is better), over every indexed piece. */
|
|
360
337
|
words(terms, limit = 400) {
|
|
361
338
|
if (!terms.length) return [];
|
|
@@ -371,6 +348,153 @@ export function openAtlas(file) {
|
|
|
371
348
|
};
|
|
372
349
|
}
|
|
373
350
|
|
|
351
|
+
// Recordings and screenshots indexed before the library keep their vectors: their part keys are the same.
|
|
352
|
+
function migrateMedia(db) {
|
|
353
|
+
const old = db.get(
|
|
354
|
+
"select name from sqlite_master where type = 'table' and name = 'media'"
|
|
355
|
+
);
|
|
356
|
+
if (!old) return;
|
|
357
|
+
db.exec(`
|
|
358
|
+
insert or ignore into library(file, vec, source, name, kind, modality, loc, loc_end, mtime, embed)
|
|
359
|
+
select path, hash, 'folders', path, kind, kind, start_ms, end_ms, mtime, 1 from media;
|
|
360
|
+
drop table media;
|
|
361
|
+
`);
|
|
362
|
+
for (const row of db.all("select distinct file from library")) {
|
|
363
|
+
const name = path.basename(row.file);
|
|
364
|
+
db.run("update library set name = ? where file = ?", [name, row.file]);
|
|
365
|
+
db.run(
|
|
366
|
+
"insert into library_fts(file, vec, name, text) values(?, ?, ?, '')",
|
|
367
|
+
[row.file, "", name]
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const LIBRARY_COLUMNS =
|
|
373
|
+
"file, vec, source, name, kind, modality, loc, loc_end, mtime, embed";
|
|
374
|
+
const libraryRow = (row) => ({
|
|
375
|
+
file: row.file,
|
|
376
|
+
vec: row.vec,
|
|
377
|
+
source: row.source,
|
|
378
|
+
name: row.name,
|
|
379
|
+
kind: row.kind,
|
|
380
|
+
modality: row.modality,
|
|
381
|
+
loc: Number(row.loc),
|
|
382
|
+
locEnd: row.loc_end === null ? null : Number(row.loc_end),
|
|
383
|
+
mtime: Number(row.mtime),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
function libraryTables(run, all, get) {
|
|
387
|
+
return {
|
|
388
|
+
/** Every file in one source ("folders", or a thread's id) with when it was read: Map(file, mtime). */
|
|
389
|
+
files(source) {
|
|
390
|
+
const rows = all(
|
|
391
|
+
"select file, max(mtime) as mtime from library where source = ? group by file",
|
|
392
|
+
source
|
|
393
|
+
);
|
|
394
|
+
return new Map(rows.map((row) => [row.file, Number(row.mtime)]));
|
|
395
|
+
},
|
|
396
|
+
sources: () =>
|
|
397
|
+
all("select distinct source from library").map((row) => row.source),
|
|
398
|
+
drop(file) {
|
|
399
|
+
run("delete from library where file = ?", file);
|
|
400
|
+
run("delete from library_fts where file = ?", file);
|
|
401
|
+
},
|
|
402
|
+
/** A file's parts: rows of LIBRARY_COLUMNS plus text (the words it is found by). One row of words names the file. */
|
|
403
|
+
put(rows) {
|
|
404
|
+
for (const row of rows) {
|
|
405
|
+
run(
|
|
406
|
+
`insert or replace into library(${LIBRARY_COLUMNS}) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
407
|
+
row.file,
|
|
408
|
+
row.vec,
|
|
409
|
+
row.source,
|
|
410
|
+
row.name,
|
|
411
|
+
row.kind,
|
|
412
|
+
row.modality,
|
|
413
|
+
row.loc,
|
|
414
|
+
row.locEnd ?? null,
|
|
415
|
+
row.mtime,
|
|
416
|
+
row.embed ? 1 : 0
|
|
417
|
+
);
|
|
418
|
+
if (row.text)
|
|
419
|
+
run(
|
|
420
|
+
"insert into library_fts(file, vec, name, text) values(?, ?, '', ?)",
|
|
421
|
+
row.file,
|
|
422
|
+
row.vec,
|
|
423
|
+
row.text
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const first = rows[0];
|
|
427
|
+
if (first)
|
|
428
|
+
run(
|
|
429
|
+
"insert into library_fts(file, vec, name, text) values(?, '', ?, '')",
|
|
430
|
+
first.file,
|
|
431
|
+
first.name.replace(/[._-]/g, " ")
|
|
432
|
+
);
|
|
433
|
+
},
|
|
434
|
+
/** Parts still to embed: the newest files first. */
|
|
435
|
+
toEmbed: (version, modalities, limit = 32) =>
|
|
436
|
+
all(
|
|
437
|
+
`select l.*, f.text from library l
|
|
438
|
+
left join piece_vectors v on v.hash = l.vec and v.version = ?
|
|
439
|
+
left join library_fts f on f.file = l.file and f.vec = l.vec
|
|
440
|
+
where l.embed = 1 and v.hash is null and l.modality in (${modalities.map(() => "?").join(",")})
|
|
441
|
+
group by l.file, l.vec order by l.mtime desc limit ?`,
|
|
442
|
+
version,
|
|
443
|
+
...modalities,
|
|
444
|
+
limit
|
|
445
|
+
).map((row) => ({ ...libraryRow(row), text: row.text ?? "" })),
|
|
446
|
+
waiting: (version) =>
|
|
447
|
+
Number(
|
|
448
|
+
get(
|
|
449
|
+
`select count(*) as n from library l left join piece_vectors v on v.hash = l.vec and v.version = ?
|
|
450
|
+
where l.embed = 1 and v.hash is null`,
|
|
451
|
+
version
|
|
452
|
+
).n
|
|
453
|
+
),
|
|
454
|
+
/** Every part with a vector: { ...row, vector }. */
|
|
455
|
+
vectors: (version) =>
|
|
456
|
+
all(
|
|
457
|
+
`select l.*, v.vector from library l join piece_vectors v on v.hash = l.vec and v.version = ?`,
|
|
458
|
+
version
|
|
459
|
+
).map((row) => ({ ...libraryRow(row), vector: fromBlob(row.vector) })),
|
|
460
|
+
/** The files holding a word or phrase (FTS5 syntax): Map(file, [vec of each matching part, best first]). */
|
|
461
|
+
holding(match, limit = 2_000) {
|
|
462
|
+
const out = new Map();
|
|
463
|
+
for (const row of all(
|
|
464
|
+
"select file, vec from library_fts where library_fts match ? order by bm25(library_fts, 0, 0, 3.0, 1.0) limit ?",
|
|
465
|
+
match,
|
|
466
|
+
limit
|
|
467
|
+
)) {
|
|
468
|
+
if (!out.has(row.file)) out.set(row.file, []);
|
|
469
|
+
if (row.vec) out.get(row.file).push(row.vec);
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
},
|
|
473
|
+
part(file, vec) {
|
|
474
|
+
const row = get(
|
|
475
|
+
`select ${LIBRARY_COLUMNS} from library where file = ? and vec = ?`,
|
|
476
|
+
file,
|
|
477
|
+
vec
|
|
478
|
+
);
|
|
479
|
+
if (!row) return null;
|
|
480
|
+
const words = get(
|
|
481
|
+
"select text from library_fts where file = ? and vec = ?",
|
|
482
|
+
file,
|
|
483
|
+
vec
|
|
484
|
+
);
|
|
485
|
+
return { ...libraryRow(row), text: words?.text ?? "" };
|
|
486
|
+
},
|
|
487
|
+
first(file) {
|
|
488
|
+
const row = get(
|
|
489
|
+
`select ${LIBRARY_COLUMNS} from library where file = ? order by loc limit 1`,
|
|
490
|
+
file
|
|
491
|
+
);
|
|
492
|
+
return row ? { ...libraryRow(row), text: "" } : null;
|
|
493
|
+
},
|
|
494
|
+
count: () => Number(get("select count(distinct file) as n from library").n),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
374
498
|
const git = (cwd, args, input) =>
|
|
375
499
|
spawnSync("git", args, {
|
|
376
500
|
cwd,
|
package/lib/context.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
codeFiles,
|
|
23
23
|
} from "./learn.mjs";
|
|
24
24
|
import { repositoryOf } from "./worktrees.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import { createLibrary } from "./library.mjs";
|
|
26
26
|
|
|
27
27
|
// The context engine. It keeps the atlas of the code the swarm works in, remembers what every coding session on this
|
|
28
28
|
// computer needed (its trail: what it read before its first edit and what it edited, from any agent), and turns that
|
|
@@ -38,11 +38,8 @@ const LEARN_EVERY_MS = 20 * 3_600_000;
|
|
|
38
38
|
// Vectors of the pieces packs search stay in memory while packs are being made, and go after this long without one.
|
|
39
39
|
const VECTORS_IDLE_MS = 5 * 60_000;
|
|
40
40
|
const HELD_OUT_SHARE = 0.2;
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
// margin and 16 of 20 right answers. An agent's own search_media has no floor.
|
|
44
|
-
const MEDIA_FLOOR = 0.7;
|
|
45
|
-
const MEDIA_IN_PACK = 2;
|
|
41
|
+
// Files from the library join a brief only when they stand out for the request (library.mjs); search_files finds more.
|
|
42
|
+
const FILES_IN_PACK = 3;
|
|
46
43
|
const MIN_TRAINING_RUNS = 30;
|
|
47
44
|
|
|
48
45
|
const git = (cwd, args) => {
|
|
@@ -83,6 +80,9 @@ export function recentChanges(root, commit = "HEAD") {
|
|
|
83
80
|
recentByCommit.set(sha, counts);
|
|
84
81
|
return counts;
|
|
85
82
|
}
|
|
83
|
+
// A file in a brief: where it is and the place in it.
|
|
84
|
+
const fileLine = (hit) =>
|
|
85
|
+
`- ${hit.thread ? `${hit.name} (this thread's file ${hit.file})` : hit.file}${hit.place ? `, ${hit.place}` : ""}`;
|
|
86
86
|
const days = (ms) => Math.round(ms / 86_400_000);
|
|
87
87
|
const AGENT_NAMES = {
|
|
88
88
|
"claude-code": "Claude Code",
|
|
@@ -153,17 +153,26 @@ export function createContextEngine({
|
|
|
153
153
|
inference,
|
|
154
154
|
home,
|
|
155
155
|
known,
|
|
156
|
-
|
|
156
|
+
libraryFolders = () => [],
|
|
157
|
+
threadFiles = () => [],
|
|
157
158
|
mediaReady = () => false,
|
|
159
|
+
wantMedia = () => {},
|
|
160
|
+
ocr = null,
|
|
161
|
+
mask,
|
|
158
162
|
log = () => {},
|
|
159
163
|
notify = () => {},
|
|
160
164
|
}) {
|
|
161
165
|
const atlas = openAtlas(path.join(home, "atlas.db"));
|
|
162
|
-
//
|
|
163
|
-
const
|
|
166
|
+
// The person's added folders and the threads' files, searchable by what is in them (library.mjs).
|
|
167
|
+
const library = createLibrary({
|
|
164
168
|
atlas,
|
|
165
169
|
inference,
|
|
166
|
-
folders:
|
|
170
|
+
folders: libraryFolders,
|
|
171
|
+
threadFiles,
|
|
172
|
+
mediaReady,
|
|
173
|
+
wantMedia,
|
|
174
|
+
ocr,
|
|
175
|
+
mask,
|
|
167
176
|
log,
|
|
168
177
|
});
|
|
169
178
|
const modelPath = path.join(home, "ranker.json");
|
|
@@ -318,11 +327,12 @@ export function createContextEngine({
|
|
|
318
327
|
lane: "background",
|
|
319
328
|
});
|
|
320
329
|
await embedRuns("background");
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
:
|
|
325
|
-
|
|
330
|
+
// The first pass over the person's folders waits for the charger too; after it, changes are read as they come.
|
|
331
|
+
const read =
|
|
332
|
+
!library.status().folderFiles && !power.onCharger()
|
|
333
|
+
? { skipped: "waiting for the charger" }
|
|
334
|
+
: await library.refresh({ lane: "background" });
|
|
335
|
+
return { files, runs, pieces, library: read };
|
|
326
336
|
})()
|
|
327
337
|
.catch((error) => {
|
|
328
338
|
log(`context engine: ${error.message}`);
|
|
@@ -343,7 +353,7 @@ export function createContextEngine({
|
|
|
343
353
|
* The context pack for a brief going to a local agent working in folder: { files, earlier, text }. Files are the most
|
|
344
354
|
* likely needed, each with the line to start at and why; earlier runs are sessions whose request was close to this one.
|
|
345
355
|
*/
|
|
346
|
-
const pack = async ({ brief, folder, lane = "handoff" }) => {
|
|
356
|
+
const pack = async ({ brief, folder, thread = null, lane = "handoff" }) => {
|
|
347
357
|
const started = performance.now();
|
|
348
358
|
const place = folder && placeOf(folder);
|
|
349
359
|
if (!place || !String(brief).trim()) return null;
|
|
@@ -400,12 +410,13 @@ export function createContextEngine({
|
|
|
400
410
|
)
|
|
401
411
|
.sort((a, b) => b.similarity - a.similarity)
|
|
402
412
|
.slice(0, 3);
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
413
|
+
const found = await library.search(brief, {
|
|
414
|
+
folders: true,
|
|
415
|
+
threads: thread ? [thread] : [],
|
|
416
|
+
brief: true,
|
|
417
|
+
limit: FILES_IN_PACK,
|
|
418
|
+
lane,
|
|
419
|
+
});
|
|
409
420
|
const text = [
|
|
410
421
|
"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):",
|
|
411
422
|
...files.map((file) => `- ${file.path}:${file.line} (${file.why})`),
|
|
@@ -418,19 +429,17 @@ export function createContextEngine({
|
|
|
418
429
|
),
|
|
419
430
|
]
|
|
420
431
|
: []),
|
|
421
|
-
...(
|
|
432
|
+
...(found.length
|
|
422
433
|
? [
|
|
423
|
-
"
|
|
424
|
-
...
|
|
425
|
-
(hit) => `- ${hit.path}${hit.at ? ` at ${hit.at}` : ""}`
|
|
426
|
-
),
|
|
434
|
+
"Files close to the request, from folders the person added and this thread's files (search_files finds more):",
|
|
435
|
+
...found.map(fileLine),
|
|
427
436
|
]
|
|
428
437
|
: []),
|
|
429
438
|
].join("\n");
|
|
430
439
|
return {
|
|
431
440
|
files,
|
|
432
441
|
earlier: earlier.map(({ vector, ...run }) => run),
|
|
433
|
-
|
|
442
|
+
library: found,
|
|
434
443
|
text,
|
|
435
444
|
searched: scope.length,
|
|
436
445
|
ms: Math.round(performance.now() - started),
|
|
@@ -583,9 +592,11 @@ export function createContextEngine({
|
|
|
583
592
|
},
|
|
584
593
|
pack,
|
|
585
594
|
search,
|
|
586
|
-
/**
|
|
587
|
-
|
|
588
|
-
|
|
595
|
+
/** The person's files and the threads' files closest to a question (library.mjs search). */
|
|
596
|
+
searchFiles: (query, options) => library.search(query, options),
|
|
597
|
+
refreshFiles: () => library.refresh({ lane: "background" }),
|
|
598
|
+
/** One part of a file, read fresh for sharing. */
|
|
599
|
+
fileContent: (file, vec) => library.content(file, vec),
|
|
589
600
|
refresh,
|
|
590
601
|
learn,
|
|
591
602
|
report: () => {
|
|
@@ -598,7 +609,7 @@ export function createContextEngine({
|
|
|
598
609
|
status: () => ({
|
|
599
610
|
roots: atlas.roots(),
|
|
600
611
|
pieces: { waiting: atlas.missing(ATLAS_VERSION) },
|
|
601
|
-
|
|
612
|
+
library: library.status(),
|
|
602
613
|
model: model
|
|
603
614
|
? {
|
|
604
615
|
trainedAt: model.trainedAt,
|
|
@@ -622,6 +633,7 @@ export function createContextEngine({
|
|
|
622
633
|
clearInterval(timer);
|
|
623
634
|
clearTimeout(dropCache);
|
|
624
635
|
cached.clear();
|
|
636
|
+
library.stop();
|
|
625
637
|
atlas.close();
|
|
626
638
|
},
|
|
627
639
|
};
|