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 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 recording's minute or a
72
- screenshot from your added folders when one is close) and the thread's task frame: what you
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`, `search_media`,
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 ~/Recordings # a folder of recordings and screenshots agents can search
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 of recordings and screenshots for search_media and briefs.
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 recordings and screenshots there in the background${response.downloading ? ", once the image and audio part of the search model has downloaded (555 MB)" : ""}.`
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.media?.folders || [])
489
- console.log(`recordings and screenshots: ${folder}`);
490
- if (current.media?.folders?.length)
491
- console.log(`media files read: ${current.media.files}`);
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 add|remove <folder> a folder of recordings and screenshots agents can search by what was said or shown\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`
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 media (hash text primary key, path text not null, kind text not null, start_ms integer not null, end_ms integer not null, mtime real not null);
140
- create index if not exists media_path on media(path);
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
- /** Recordings' windows and images (media.mjs): one row each, its vector beside the pieces'. */
331
- mediaSeen: (file) =>
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 { createMediaIndex } from "./media.mjs";
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
- // A recording's minute or a screenshot joins a brief only this close to the request: on the authored meeting every right
42
- // answer scored 0.68 or more and no question about something never said passed 0.675 (eval media-search); 0.70 keeps a
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
- mediaFolders = () => [],
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
- // Recordings and screenshots in folders the person added (media.mjs), once the image and audio part is here.
163
- const media = createMediaIndex({
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: mediaFolders,
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
- const recorded =
322
- mediaReady() && mediaFolders().length && power.onCharger()
323
- ? await media.refresh({ lane: "background" })
324
- : null;
325
- return { files, runs, pieces, ...(recorded ? { media: recorded } : {}) };
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 heard =
404
- mediaReady() && mediaFolders().length
405
- ? (await media.search(brief, { limit: MEDIA_IN_PACK, lane })).filter(
406
- (hit) => hit.score >= MEDIA_FLOOR
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
- ...(heard.length
432
+ ...(found.length
422
433
  ? [
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
- ),
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
- media: heard,
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
- /** Recordings' minutes and screenshots closest to a question, for an agent's search_media. */
587
- searchMedia: (query, options) => media.search(query, options),
588
- refreshMedia: () => media.refresh({ lane: "background" }),
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
- media: media.status(),
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
  };