strom-research 1.19.0-beta.2 → 1.19.0-beta.4

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/CHANGELOG.cs.md CHANGED
@@ -7,7 +7,7 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
7
7
 
8
8
  ## 1.19.0
9
9
 
10
- *Připravuje se: bety do 1.19.0-beta.2. Oddíl bude úplný s vydáním.*
10
+ *Připravuje se: bety do 1.19.0-beta.4. Oddíl bude úplný s vydáním.*
11
11
 
12
12
  - **Na archiv, který zmlkl, se čeká, místo opakovaných dotazů.** Když archiv přestane odpovídat, strom ho nechá na
13
13
  pokoji hodinu — a když týž den mlčí dál, dvě a pak čtyři hodiny — a agent se dozví, do kdy: mezitím dělá
@@ -22,6 +22,10 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
22
22
  - **Ostřejší část snímku se opravdu čte.** Když z archivu přišla část snímku v lepším rozlišení, agent i jeho čtenáři
23
23
  se na to místo dívají v ní — i když vyznačené místo kousek přesahuje, zbytek se řekne —, ne na celý zmenšený snímek;
24
24
  ostřejší část, která místo pokrývá jen zčásti, se uvede i s tím, jak se na ni podívat.
25
+ - **Dokument se přečte spolehlivěji.** Čtenář zapisuje čtení do své zprávy průběžně, a čtení dvou čtenářů, kde četl
26
+ jen jeden, to tak řekne — nikdy, že se shodují.
27
+ - **Propojení s telefonem čeká na výzkum, neříká se jako chyba.** Než se po spuštění počítače spustí výzkum, `strom
28
+ phone` i nastavení řeknou, že propojení je zapnuté a jeho server se znovu spustí se spuštěním výzkumu.
25
29
 
26
30
  ## 1.18.0
27
31
 
package/CHANGELOG.de.md CHANGED
@@ -7,7 +7,7 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
7
7
 
8
8
  ## 1.19.0
9
9
 
10
- *In Vorbereitung: die Betas bis 1.19.0-beta.2. Der Abschnitt ist mit dem Release vollständig.*
10
+ *In Vorbereitung: die Betas bis 1.19.0-beta.4. Der Abschnitt ist mit dem Release vollständig.*
11
11
 
12
12
  - **Auf ein verstummtes Archiv wird gewartet, statt es immer wieder zu fragen.** Antwortet ein Archiv nicht mehr,
13
13
  lässt strom es eine Stunde in Ruhe — zwei, dann vier, wenn es am selben Tag still bleibt — und der Agent erfährt, bis
@@ -25,6 +25,11 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
25
25
  sehen der Agent und seine Leser diese Stelle darin an — auch wenn die markierte Stelle etwas darüber hinausreicht,
26
26
  der Rest wird genannt — statt des ganzen, verkleinerten Scans; ein schärferer Teil, der die Stelle nur zum Teil
27
27
  zeigt, wird mit dem Weg genannt, ihn anzusehen.
28
+ - **Ein Dokument wird verlässlicher gelesen.** Ein Leser schreibt seine Lesung laufend in seinen Bericht, und eine
29
+ Lesung zweier Leser, bei der nur einer las, sagt das so — nie, dass sie übereinstimmen.
30
+ - **Die Verbindung mit dem Telefon wartet auf die Forschung, nicht als Fehler genannt.** Bevor die Forschung nach dem
31
+ Neustart des Computers startet, sagen `strom phone` und die Einstellungen, dass die Verbindung an ist und ihr Server
32
+ mit der Forschung wieder startet.
28
33
 
29
34
  ## 1.18.0
30
35
 
package/CHANGELOG.md CHANGED
@@ -7,7 +7,7 @@ The changes of each version in short. The full notes and the install lines are o
7
7
 
8
8
  ## 1.19.0
9
9
 
10
- *In preparation: the betas up to 1.19.0-beta.2. The section is complete with the release.*
10
+ *In preparation: the betas up to 1.19.0-beta.4. The section is complete with the release.*
11
11
 
12
12
  - **An archive that went silent is waited for, not asked again and again.** When an archive stops answering, strom
13
13
  leaves it alone for an hour — two, then four when it stays silent the same day — and the agent hears until when:
@@ -24,6 +24,10 @@ The changes of each version in short. The full notes and the install lines are o
24
24
  agent and its readers look at that place in it — also when the place they mark reaches a little past it, the rest
25
25
  said — instead of the whole scan, reduced; a sharper part that holds only some of the place is named with the way
26
26
  to look at it.
27
+ - **A document read more surely.** A reader writes its reading into its report as it goes, and a reading of two readers where
28
+ only one read says so, never that they agree.
29
+ - **The phone connection waits for the research, not said as an error.** Before the research starts after the computer
30
+ started again, `strom phone` and the settings say the connection is on and its server starts again with the research.
27
31
 
28
32
  ## 1.18.0
29
33
 
@@ -1298,6 +1298,7 @@
1298
1298
  "ui.phone.window.no": "Zrušit",
1299
1299
  "ui.phone.on": "propojení s telefonem: zapnuto, v místní síti na {urls}",
1300
1300
  "ui.phone.starting": "propojení s telefonem: zapnuto, jeho server se spouští",
1301
+ "ui.phone.waiting": "propojení s telefonem: zapnuto, jeho server se znovu spustí se spuštěním výzkumu",
1301
1302
  "ui.phone.error": "propojení s telefonem: zapnuto, ale telefon se k němu nedostane: {why}",
1302
1303
  "ui.phone.why.no-lan": "počítač není v žádné místní síti (Wi-Fi ani kabel)",
1303
1304
  "ui.phone.why.port-busy": "porty 47800–47819 mají obsazené jiné programy",
@@ -1298,6 +1298,7 @@
1298
1298
  "ui.phone.window.no": "Abbrechen",
1299
1299
  "ui.phone.on": "Verbindung mit dem Telefon: an, im lokalen Netz unter {urls}",
1300
1300
  "ui.phone.starting": "Verbindung mit dem Telefon: an, ihr Server startet",
1301
+ "ui.phone.waiting": "Verbindung mit dem Telefon: an, ihr Server startet wieder, sobald die Forschung startet",
1301
1302
  "ui.phone.error": "Verbindung mit dem Telefon: an, aber ein Telefon erreicht sie nicht: {why}",
1302
1303
  "ui.phone.why.no-lan": "der Computer ist in keinem lokalen Netz (weder WLAN noch Kabel)",
1303
1304
  "ui.phone.why.port-busy": "die Ports 47800–47819 belegen andere Programme",
@@ -180,7 +180,7 @@ async function phoneStep(ctx, run, lang) {
180
180
  await pause(ctx, lang);
181
181
  return;
182
182
  }
183
- const i = await ctx.choose(t(s.state === "on" ? "ui.phone.on" : "ui.phone.error", { urls: s.addresses.map((a) => a.url).join(", "), why: t(`ui.phone.why.${s.error?.code ?? "bind-failed"}`) }), [{ label: t("ui.phone.menu.off") }, ...s.phones.map((p) => ({ label: t("ui.phone.menu.remove", { name: p.name || p.id, date: p.pairedAt.slice(0, 10) }) }))], 0, { back: t("ui.browse.back") });
183
+ const i = await ctx.choose(t(s.state === "on" ? "ui.phone.on" : s.error?.waiting ? "ui.phone.waiting" : "ui.phone.error", { urls: s.addresses.map((a) => a.url).join(", "), why: t(`ui.phone.why.${s.error?.code ?? "bind-failed"}`) }), [{ label: t("ui.phone.menu.off") }, ...s.phones.map((p) => ({ label: t("ui.phone.menu.remove", { name: p.name || p.id, date: p.pairedAt.slice(0, 10) }) }))], 0, { back: t("ui.browse.back") });
184
184
  if (i === undefined)
185
185
  return;
186
186
  if (i === 0)
package/dist/cli/ui.js CHANGED
@@ -1198,6 +1198,7 @@ export const UI = {
1198
1198
  "ui.phone.window.no": "Cancel",
1199
1199
  "ui.phone.on": "phone connection: on, on the local network at {urls}",
1200
1200
  "ui.phone.starting": "phone connection: on, its server is starting",
1201
+ "ui.phone.waiting": "phone connection: on, its server starts again when the research starts",
1201
1202
  "ui.phone.error": "phone connection: on, but a phone cannot reach it: {why}",
1202
1203
  "ui.phone.why.no-lan": "this computer is on no local network (Wi-Fi or a cable)",
1203
1204
  "ui.phone.why.port-busy": "the ports 47800–47819 are taken by other programs",
@@ -246,7 +246,7 @@ async function readWith(ctx, tree, s, opts, only) {
246
246
  }
247
247
  const team = readers(ctx, tree, shared, "document", "1", opts, undefined, { stem: s.id });
248
248
  const views = s.pages.map((p) => viewsOf(tree, shared, p, team.viewMax));
249
- s = { ...s, groups: groupsOf(views), state: "reading", since: now(), pid: process.pid, ...(team.model ? { model: team.model } : {}) };
249
+ s = { ...s, groups: groupsOf(views), state: "reading", since: now(), pid: process.pid, agent: team.agent, ...(team.model ? { model: team.model } : {}) };
250
250
  saveState(tree.root, s);
251
251
  const stop = new AbortController();
252
252
  const look = setInterval(() => {
@@ -13,7 +13,9 @@ function stateLines(lang, s) {
13
13
  ? ui(lang, "ui.phone.on", { urls: s.addresses.map((a) => a.url).join(", ") })
14
14
  : ui(lang, "ui.phone.starting")
15
15
  : s.state === "error"
16
- ? ui(lang, "ui.phone.error", { why: ui(lang, `ui.phone.why.${s.error?.code ?? "bind-failed"}`) })
16
+ ? s.error?.waiting
17
+ ? ui(lang, "ui.phone.waiting")
18
+ : ui(lang, "ui.phone.error", { why: ui(lang, `ui.phone.why.${s.error?.code ?? "bind-failed"}`) })
17
19
  : s.state === "confirm"
18
20
  ? ui(lang, "ui.phone.confirm")
19
21
  : s.state === "declined"
@@ -189,5 +189,5 @@ export function readers(ctx, tree, shared, kind, first, opts, reportsAt, own = {
189
189
  fs.appendFileSync(report, r.text.trim() + "\n");
190
190
  return { report, text: fs.readFileSync(report, "utf8"), outcome: r.outcome, ...(r.metrics.costUsd !== undefined ? { costUsd: r.metrics.costUsd } : {}), ...(r.metrics.costPartial ? { costPartial: true } : {}), ...(r.resumeAt ? { resumeAt: r.resumeAt } : {}) };
191
191
  };
192
- return { model, viewMax: sizes.read, parallel, stem, reportsDir, read, cost: () => cost, runs: () => runs, partial: () => partial, failed, progress };
192
+ return { agent: agentId, model, viewMax: sizes.read, parallel, stem, reportsDir, read, cost: () => cost, runs: () => runs, partial: () => partial, failed, progress };
193
193
  }
@@ -1219,19 +1219,27 @@ export function buildResult(tree, s) {
1219
1219
  return out;
1220
1220
  };
1221
1221
  let entries = entriesOf(firsts, false);
1222
+ const firstRead = entries.length > 0;
1222
1223
  const blindRan = blinds.some((r) => r);
1223
- // the first reader failed, the blind one read: its entries, untranslated
1224
+ // the first reader failed, the blind one read: its entries, untranslated — never held against themselves
1224
1225
  if (!entries.length && blindRan)
1225
1226
  entries = entriesOf(blinds, false);
1226
1227
  let agree = null;
1227
- if (blindRan && s.readers > 1 && entries.length) {
1228
+ if (blindRan && s.readers > 1 && firstRead) {
1228
1229
  const blindEntries = entriesOf(blinds, true);
1229
1230
  const c = compareReadings(entries, blindEntries);
1230
1231
  entries = c.entries;
1231
1232
  agree = c.agree;
1232
1233
  }
1233
1234
  entries = withTree(tree, entries);
1234
- const ran = (firsts.some((r) => r?.outcome === "ok") ? 1 : 0) + (blinds.some((r) => r?.outcome === "ok") ? 1 : 0);
1235
+ // a reader ran when it ended well and its report says something (an answer in prose only is no reading)
1236
+ const said = (r) => {
1237
+ if (r?.outcome !== "ok")
1238
+ return false;
1239
+ const rep = read(r);
1240
+ return !!rep && (rep.entries.length > 0 || rep.done || rep.doc.legible !== undefined);
1241
+ };
1242
+ const ran = (firsts.some(said) ? 1 : 0) + (blinds.some(said) ? 1 : 0);
1235
1243
  const usd = s.reports.reduce((n, r) => n + (r.usd ?? 0), 0);
1236
1244
  const partial = s.reports.some((r) => r.outcome !== undefined && (r.usd === undefined || r.partial));
1237
1245
  return {
@@ -27,6 +27,8 @@ import { EXIT } from "./errors.js";
27
27
  import { forApp, refusal } from "./receive.js";
28
28
  import { stromLauncher } from "./self.js";
29
29
  import { normId } from "./records.js";
30
+ import { metricsDir } from "./metrics.js";
31
+ import { readJournal } from "./readstats.js";
30
32
  /** The readings the app may ask, unless the person set others (read.app). */
31
33
  export const APP_READ_DEFAULT = { hour: 20, day: 60 };
32
34
  /** Readings of the app waiting beside the one being read. */
@@ -213,12 +215,12 @@ export class ReadingsSeen {
213
215
  };
214
216
  }
215
217
  }
216
- /** /status reads: those at work, and those ended in the last week. */
217
- export function statusReads(seen, now = Date.now()) {
218
+ /** /status reads: those at work (with the seconds each is expected to take: expectSecs), and those ended in the last week. */
219
+ export function statusReads(seen, agentNow, now = Date.now()) {
218
220
  return seen
219
221
  .all()
220
- .map((r) => r.read)
221
- .filter((r) => !FINAL_STATES.includes(r.state) || Date.parse(r.since) > now - STATUS_DAYS * DAY_MS);
222
+ .filter((r) => !FINAL_STATES.includes(r.read.state) || Date.parse(r.read.since) > now - STATUS_DAYS * DAY_MS)
223
+ .map((r) => (r.state && !FINAL_STATES.includes(r.read.state) ? { ...r.read, expectSecs: expectOf(seen.root, r.state, agentNow) } : r.read));
222
224
  }
223
225
  /**
224
226
  * GET /reads: the last 90 days, and every reading no source of the research is of. inSource: a source of this tree —
@@ -268,6 +270,7 @@ export function readAccepts(tree, settings, env, seen, now = Date.now()) {
268
270
  max: READ_MAX_BYTES,
269
271
  pages: DOC_PAGES,
270
272
  langs: readLangs(),
273
+ ...(not ? {} : { expectSecs: expectSecs(tree.root, settings.agent(tree.config).value) }),
271
274
  };
272
275
  }
273
276
  function notReady(tree, settings, env, seen, now) {
@@ -296,6 +299,55 @@ function notReady(tree, settings, env, seen, now) {
296
299
  }
297
300
  return undefined;
298
301
  }
302
+ // ── how long a reading takes ────────────────────────────────────────────────────────────────────────────────────
303
+ /** A reading's seconds before any of this agent's readers ended here (measured 2026-10-20 and 21): Grok's take minutes. */
304
+ export const READ_EXPECT_DEFAULT = { grok: 240 };
305
+ export const READ_EXPECT_OTHERS = 60;
306
+ /** The readers of documents a reading's time is learned from: the last so many of the agent that read to an end. */
307
+ const EXPECT_LAST = 9;
308
+ const expectSeen = new Map();
309
+ /** How long this agent's readers of documents took here (.strom/metrics/readers.jsonl, nothing of the tree), oldest first. */
310
+ function readerSecs(root, agent) {
311
+ const file = path.join(metricsDir(root), "readers.jsonl");
312
+ let key = "-";
313
+ try {
314
+ const st = fs.statSync(file);
315
+ key = `${st.mtimeMs}:${st.size}`;
316
+ }
317
+ catch {
318
+ return [];
319
+ }
320
+ let had = expectSeen.get(file);
321
+ if (had?.key !== key) {
322
+ const secs = new Map();
323
+ for (const r of readJournal(file)) {
324
+ const ms = typeof r.wallMs === "number" ? r.wallMs : typeof r.ms === "number" ? r.ms : NaN;
325
+ if (r.kind !== "document" || r.outcome !== "ok" || typeof r.agent !== "string" || !(ms >= 0))
326
+ continue;
327
+ secs.set(r.agent, [...(secs.get(r.agent) ?? []), ms / 1000].slice(-EXPECT_LAST));
328
+ }
329
+ had = { key, secs };
330
+ expectSeen.set(file, had);
331
+ }
332
+ return had.secs.get(agent) ?? [];
333
+ }
334
+ /**
335
+ * The seconds one reading is expected to take with this agent (`rounds`: its readers one after another, a long
336
+ * document's parts beyond the readers at work at once): the median of its last readers of documents here, else what
337
+ * the agent's readers took when measured — for the app to say a reading takes minutes, never a promise.
338
+ */
339
+ export function expectSecs(root, agent, rounds = 1) {
340
+ const secs = readerSecs(root, agent).sort((a, b) => a - b);
341
+ const mid = secs.length >> 1;
342
+ const one = secs.length ? (secs.length % 2 ? secs[mid] : (secs[mid - 1] + secs[mid]) / 2) : (READ_EXPECT_DEFAULT[agent] ?? READ_EXPECT_OTHERS);
343
+ return Math.max(1, Math.round(one * Math.max(1, rounds)));
344
+ }
345
+ /** A reading at work or queued: how long it is expected to take from its start (since, when it is reading). */
346
+ function expectOf(root, s, agentNow) {
347
+ // each part of the document read by the first reader and the blind one, as many at once as the readers asked, three at least
348
+ const jobs = Math.max(1, s.groups.length) * ((s.only === "blind" ? 0 : 1) + (s.readers > 1 ? 1 : 0));
349
+ return expectSecs(root, s.agent ?? agentNow, Math.ceil(jobs / Math.max(3, s.readers)));
350
+ }
299
351
  const BODY_MAX = 16 * 1024;
300
352
  /** The names of files sent to be read kept until they are asked (the last ones). */
301
353
  const NAMES_KEPT = 200;
@@ -394,7 +446,10 @@ export class AppReadings {
394
446
  return this.json(res, n.code, n.body);
395
447
  }
396
448
  const r = resultForApp(got.result);
397
- this.json(res, 200, { ...r, pages: r.pages.map((p) => ({ ...p, image: `read/${id}/page/${p.n}.jpg` })) });
449
+ // at work or queued: how long it is expected to take from its start
450
+ const s = FINAL_STATES.includes(got.result.state) ? undefined : loadState(this.b.root, id);
451
+ const expect = s ? { expectSecs: expectOf(this.b.root, s, this.b.settings().agent(got.tree.config).value) } : {};
452
+ this.json(res, 200, { ...r, ...expect, pages: r.pages.map((p) => ({ ...p, image: `read/${id}/page/${p.n}.jpg` })) });
398
453
  }
399
454
  /** GET /read/<id>/page/<n>.jpg: the page upright as a JPEG of 2000 px at most (a HEIC's, a PDF's page too). */
400
455
  page(res, id, rest) {
package/dist/core/live.js CHANGED
@@ -1211,7 +1211,7 @@ function status(root, env, version, seen) {
1211
1211
  ...(intake ? { lastIntake: intake } : {}),
1212
1212
  accepts: accepts(tree, settings, env, seen),
1213
1213
  // the documents the app asked to be read and the research's own readings: at work, and ended in the last week
1214
- ...(archive || !seen ? {} : { reads: statusReads(seen) }),
1214
+ ...(archive || !seen ? {} : { reads: statusReads(seen, settings.agent(tree.config).value) }),
1215
1215
  inbox: { trees: inboxTrees(root, env), material: materialWaiting(tree) },
1216
1216
  sends: recentSends(root, tree),
1217
1217
  batches: batchStatus(tree),
@@ -511,7 +511,7 @@ export function phoneState(env) {
511
511
  // just turned on: its server on the way
512
512
  if (c.since && Date.now() - Date.parse(c.since) < 15_000)
513
513
  return { ...base, state: "on", since: c.since, phones };
514
- return { ...base, state: "error", since: c.since ?? null, error: { code: "bind-failed", detail: run?.ended ? `the server ended: ${run.ended.reason}` : "the server does not run" }, phones };
514
+ return { ...base, state: "error", since: c.since ?? null, error: { code: "bind-failed", detail: run?.ended ? `the server ended: ${run.ended.reason}` : "the server does not run", waiting: true }, phones };
515
515
  }
516
516
  const a = readAsk(env);
517
517
  if (asking(a))
@@ -226,11 +226,15 @@ export function parseReport(text) {
226
226
  }
227
227
  return out;
228
228
  }
229
- /** Claude Code settings for a reader: it may read these views and write its report — nothing else. */
229
+ /**
230
+ * Claude Code settings for a reader: it may read these views and read and write its report — nothing else. Its report
231
+ * read too: Claude Code edits no file it has not read (found live 2026-10-21: every edit refused, the report kept only
232
+ * from the answer, a reader that answered in prose lost).
233
+ */
230
234
  export function readerSettings(views, report, toPermission) {
231
235
  return {
232
236
  permissions: {
233
- allow: [...views.map((v) => `Read(${toPermission(v)})`), `Write(${toPermission(report)})`, `Edit(${toPermission(report)})`],
237
+ allow: [...views.map((v) => `Read(${toPermission(v)})`), `Read(${toPermission(report)})`, `Write(${toPermission(report)})`, `Edit(${toPermission(report)})`],
234
238
  deny: ["Bash", "WebFetch", "WebSearch"],
235
239
  },
236
240
  };
package/dist/core/tree.js CHANGED
@@ -27,7 +27,7 @@ import { lastBackup } from "./backup.js";
27
27
  export const TREE_FILE = "strom.json";
28
28
  // between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
29
29
  // at a release the release's own (package.json)
30
- export const VERSION = "1.19.0-beta.2";
30
+ export const VERSION = "1.19.0-beta.4";
31
31
  const GITIGNORE = `# Strom Research — local state and large files are not versioned
32
32
  .strom/
33
33
  /media/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strom-research",
3
- "version": "1.19.0-beta.2",
3
+ "version": "1.19.0-beta.4",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {