strom-research 1.18.0-beta.11 → 1.18.0-beta.12
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 +11 -9
- package/CHANGELOG.de.md +13 -11
- package/CHANGELOG.md +10 -7
- package/dist/commands/read.js +3 -1
- package/dist/commands/readers.js +1 -0
- package/dist/core/document.js +130 -11
- package/dist/core/live-read.js +3 -6
- package/dist/core/readsync.js +9 -3
- package/dist/core/tree.js +1 -1
- package/dist/runners/antigravity.js +35 -0
- package/dist/runners/codex.js +18 -6
- package/dist/runners/grok.js +9 -1
- package/dist/runners/opencode.js +43 -3
- package/package.json +1 -1
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.18.0
|
|
9
9
|
|
|
10
|
-
*Připravuje se: bety do 1.18.0-beta.
|
|
10
|
+
*Připravuje se: bety do 1.18.0-beta.12. Oddíl bude úplný s vydáním.*
|
|
11
11
|
|
|
12
12
|
- **Fotky z telefonu nastojato.** Fotka otočená podle údaje EXIF se čte tak, jak se má dívat, nastojato; místa na ní
|
|
13
13
|
vyznačená (výřezy, poloviny dvoustrany, uložená část) jsou místa obrázku, jak se ukazuje. Fotka sama zůstává, jak
|
|
@@ -16,14 +16,16 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
16
16
|
na macOS, Windows i Linuxu — a originál zůstává, jak přišel. Fotka HEIC jde do aplikace Strom jako výřez JPEG.
|
|
17
17
|
Kde obrázek na počítači nic nepřevede, řekne to jedna věta i s tím, co by pomohlo; `strom tidy` převedené obrázky
|
|
18
18
|
po 30 dnech nebo nad 500 MB uklidí.
|
|
19
|
-
- **Jeden dokument přečtený naráz.** `strom document read` přečte fotku, sken nebo PDF jednoho dokumentu — zápis
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
- **Jeden dokument přečtený naráz.** `strom document read` přečte fotku, sken nebo PDF jednoho dokumentu — zápis z
|
|
20
|
+
matriky, dopis, listinu, náhrobek, rub fotky: přepis slovo od slova, překlad, význam starých slov a zkratek a osoby,
|
|
21
|
+
které jmenuje, i s tím, kdo ze stromu to může být. Nic se nedomýšlí: jméno, které v dokumentu není, vypadne — i
|
|
22
|
+
příjmení, nikdy se nebere od otce ani z jiné rodiny —, co nejde přečíst, je označené; druhý čtenář může číst naslepo.
|
|
23
|
+
Dokument na úředním formuláři, jako rodný list, si své osoby ponechá — dítě i oba rodiče s jejich podílem —, a dokument
|
|
24
|
+
přečtený dřív je po aktualizaci ukáže, aniž by se četl znovu.
|
|
25
|
+
Do stromu se z toho nic nezapíše samo. Dokument už přečtený to o sobě řekne, takže se omylem nečte podruhé. Dokument
|
|
26
|
+
čtený znovu se ukazuje pod názvem svého souboru, nikdy pod kódem. Dokument poslaný z aplikace Strom najde agenta
|
|
27
|
+
stejně jako terminál — i nainstalovaného do vlastní složky, přes npm nebo Homebrew — místo hlášení, že agent není
|
|
28
|
+
nastavený. Čte ho každý agent, se kterým výzkum pracuje, s modelem a úsilím nastaveným pro výzkum.
|
|
27
29
|
- **Fotky z telefonu.** `strom phone on`, které zapíná jen člověk, pustí k počítači telefon v domácí síti (Wi-Fi
|
|
28
30
|
nebo kabel — nikdy mobilní síť ani VPN). Telefon se jednou spáruje přes QR kód; jeho stránka pak fotí nebo vybírá
|
|
29
31
|
z galerie a posílá fotky celé do stromu výzkumu, s poznámkou a osobou, kterou ukazují — stejně jako „Přidat
|
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.18.0
|
|
9
9
|
|
|
10
|
-
*In Vorbereitung: die Betas bis 1.18.0-beta.
|
|
10
|
+
*In Vorbereitung: die Betas bis 1.18.0-beta.12. Der Abschnitt ist mit dem Release vollständig.*
|
|
11
11
|
|
|
12
12
|
- **Handyfotos aufrecht.** Ein Foto, das nach seiner EXIF-Ausrichtung gedreht ist, wird gelesen, wie es gesehen
|
|
13
13
|
werden soll, aufrecht; die darauf markierten Stellen (Ausschnitte, die Hälften einer Doppelseite, ein gespeicherter
|
|
@@ -16,16 +16,18 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
16
16
|
umgewandelt — unter macOS, Windows und Linux —, das Original bleibt, wie es kam. Ein HEIC-Foto geht als
|
|
17
17
|
JPEG-Ausschnitt in die Strom-App. Wo nichts auf dem Computer ein Bild umwandelt, sagt es ein Satz, auch was helfen
|
|
18
18
|
würde; `strom tidy` räumt umgewandelte Bilder nach 30 Tagen oder über 500 MB weg.
|
|
19
|
-
- **Ein Dokument auf einmal gelesen.** `strom document read` liest ein Foto, einen Scan oder eine PDF eines
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
- **Ein Dokument auf einmal gelesen.** `strom document read` liest ein Foto, einen Scan oder eine PDF eines Dokuments —
|
|
20
|
+
einen Matrikeleintrag, einen Brief, eine Urkunde, einen Grabstein, die Rückseite eines Fotos: die Abschrift Wort für
|
|
21
|
+
Wort, ihre Übersetzung, was alte Wörter und Abkürzungen bedeuten, und die genannten Personen, samt wem im Stammbaum
|
|
22
|
+
sie entsprechen können. Nichts wird ergänzt: ein Name, der nicht im Dokument steht, fällt weg — auch ein Nachname, nie
|
|
23
|
+
vom Vater oder aus einer anderen Familie genommen —, Unleserliches wird markiert; ein zweiter Leser kann blind lesen.
|
|
24
|
+
Ein Dokument auf einem amtlichen Formular, etwa eine Geburtsurkunde, behält seine Personen — das Kind und beide
|
|
25
|
+
Eltern mit ihrem Anteil —, und ein früher gelesenes Dokument zeigt sie nach dem Update, ohne erneut gelesen zu werden.
|
|
26
|
+
In den Stammbaum gelangt nichts von selbst. Ein schon gelesenes Dokument sagt es, so wird es nicht versehentlich ein
|
|
27
|
+
zweites Mal gelesen. Ein erneut gelesenes Dokument wird unter dem Namen seiner Datei gezeigt, nie unter einem Code.
|
|
28
|
+
Ein aus der Strom-App gesendetes Dokument findet den Agenten wie das Terminal — auch einen in einem eigenen Ordner,
|
|
29
|
+
über npm oder Homebrew installierten —, statt zu melden, es sei kein Agent eingerichtet. Jeder Agent, mit dem die
|
|
30
|
+
Forschung arbeitet, liest es, mit dem Modell und dem Aufwand, die für die Forschung eingestellt sind.
|
|
29
31
|
- **Fotos vom Handy.** `strom phone on`, das nur ein Mensch einschaltet, lässt ein Handy im Heimnetz (WLAN oder
|
|
30
32
|
Kabel — nie ein Mobilfunknetz oder ein VPN) zu diesem Computer. Das Handy wird einmal per QR-Code gekoppelt; seine
|
|
31
33
|
Seite fotografiert dann oder wählt aus der Galerie und schickt die Fotos ganz in einen Stammbaum der Forschung, mit
|
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.18.0
|
|
9
9
|
|
|
10
|
-
*In preparation: the betas up to 1.18.0-beta.
|
|
10
|
+
*In preparation: the betas up to 1.18.0-beta.12. The section is complete with the release.*
|
|
11
11
|
|
|
12
12
|
- **Phone photos upright.** A photo turned by its EXIF orientation is read as it is meant to be seen, upright; the
|
|
13
13
|
places marked on it (excerpts, the halves of a spread, a cut-out part) are of the picture as it is shown. The
|
|
@@ -18,12 +18,15 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
18
18
|
help; `strom tidy` clears the converted pictures away after 30 days or above 500 MB.
|
|
19
19
|
- **One document read at once.** `strom document read` reads a photo, a scan or a PDF of one document — a register
|
|
20
20
|
entry, a letter, a deed, a gravestone, the back of a photo: the transcript word for word, its translation, what its
|
|
21
|
-
old words and abbreviations mean, and the people it names, with who of the tree they may be. Nothing is filled in:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
document
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
old words and abbreviations mean, and the people it names, with who of the tree they may be. Nothing is filled in: a
|
|
22
|
+
name the document does not have is left out — a surname too, never taken from a father or another family —, what
|
|
23
|
+
cannot be read is marked; a second reader can read it blind. A
|
|
24
|
+
document on a printed form, as a birth certificate, keeps its people — the child and both parents with their part in
|
|
25
|
+
it —, and a document read before shows them after the update with nothing read again. Nothing of it enters the tree by itself. A document read
|
|
26
|
+
already says so, so it is not read a second time by mistake. A document read again is shown under the name of its
|
|
27
|
+
file, never a code. A document sent from the Strom app finds the agent as the terminal does — one installed in a
|
|
28
|
+
folder of its own, by npm or Homebrew too — instead of saying no agent is set up. Every agent the research works with
|
|
29
|
+
reads it, with the model and effort set for the research.
|
|
27
30
|
- **Photos from a phone.** `strom phone on`, turned on by the person alone, lets a phone on the home network (Wi-Fi
|
|
28
31
|
or a cable — never a mobile network or a VPN) reach this computer. The phone is paired once by its QR code; its
|
|
29
32
|
page then takes photos or picks them from the gallery and sends them whole into a tree of the research, with a
|
package/dist/commands/read.js
CHANGED
|
@@ -440,7 +440,8 @@ register({
|
|
|
440
440
|
const own = path.join(work, `${stem}-${k + 1}`);
|
|
441
441
|
fs.mkdirSync(own, { recursive: true });
|
|
442
442
|
const settingsFile = path.join(own, "reader-settings.json");
|
|
443
|
-
|
|
443
|
+
const seen = group.flatMap((g) => [g.view, ...(g.parts ?? []).map((p) => p.view)]);
|
|
444
|
+
fs.writeFileSync(settingsFile, JSON.stringify(readerSettings(seen, report, permissionPath), null, 2));
|
|
444
445
|
const context = typeof opts.context === "string" && opts.context.trim() ? `Context: ${opts.context.trim().replace(/\s*\n\s*/g, " ")}\n` : "";
|
|
445
446
|
// a blind reading says so in its head: its "unclear" over a place the researcher looked at before is a check
|
|
446
447
|
const head = `# Reading ${label} · batch ${k + 1}${fetching ? "" : ` of ${groups.length}`}\nQuestion: ${question.replace(/\s*\n\s*/g, " ")}\n${context}${opts.blind ? "Blind: yes\n" : ""}\n${NEXT_BLOCK}\n`;
|
|
@@ -459,6 +460,7 @@ register({
|
|
|
459
460
|
env: { ...withoutAgentMarks(env), STROM_NONINTERACTIVE: "1", STROM_READER: "1" },
|
|
460
461
|
settingsFile,
|
|
461
462
|
reader: true,
|
|
463
|
+
readerFiles: { views: seen, report },
|
|
462
464
|
// without the user's own add-ons (skills, plugins, MCP servers) unless they said otherwise
|
|
463
465
|
...(ctx.settings.agentAddons(tree.config) ? {} : { clean: true }),
|
|
464
466
|
timeoutMs: minutes * 60_000,
|
package/dist/commands/readers.js
CHANGED
|
@@ -135,6 +135,7 @@ export function readers(ctx, tree, shared, kind, first, opts, reportsAt, own = {
|
|
|
135
135
|
env: { ...withoutAgentMarks(env), STROM_NONINTERACTIVE: "1", STROM_READER: "1" },
|
|
136
136
|
settingsFile,
|
|
137
137
|
reader: true,
|
|
138
|
+
readerFiles: { views, report },
|
|
138
139
|
// without the user's own add-ons (skills, plugins, MCP servers) unless they said otherwise
|
|
139
140
|
...(ctx.settings.agentAddons(tree.config) ? {} : { clean: true }),
|
|
140
141
|
timeoutMs: limit.minutes * 60_000,
|
package/dist/core/document.js
CHANGED
|
@@ -113,6 +113,7 @@ term: <a word or abbreviation as written> = <what it means, in ${into}> (term |
|
|
|
113
113
|
residence=<…> — one line each person named; ref p1, p2, … in this entry; role: ${ROLES.join(", ")};
|
|
114
114
|
given and surname only turn the written name into today's form ("Joannes" → "Jan"): never a name the transcript
|
|
115
115
|
does not have — no surname the entry does not write by the person (a child's is not taken from its father's);
|
|
116
|
+
on a form (a certificate) whose fields hold a name apart, written= the words of its fields as written there;
|
|
116
117
|
a name you could not read stays out; leave out what the entry does not say
|
|
117
118
|
event: <BIRT | BAPM | MARR | DEAT | BURI | RESI | EVEN> | date=<as on the record, in numbers: 3.4.1846, or a year> |
|
|
118
119
|
place=<the place as written> | persons=<ref>:<principal|father|mother|godparent|witness|groom|bride|deceased|other>,…
|
|
@@ -280,6 +281,9 @@ export function parseDocReport(text) {
|
|
|
280
281
|
/** A kind a reader wrote in its own words: the nearest of the list. */
|
|
281
282
|
function kindOf(said) {
|
|
282
283
|
const f = foldText(said);
|
|
284
|
+
// a certificate first: a birth certificate is no register of births ("rodný list", "Geburtsurkunde", "odpis aktu")
|
|
285
|
+
if (/certif|urkunde|zaswiadcz|odpis|extrait|auszug|vypis|(?:rodn|oddac|umrtn)\p{L}*\s+list/u.test(f))
|
|
286
|
+
return "certificate";
|
|
283
287
|
if (/bapt|krest|tauf|chrzest|christen/u.test(f))
|
|
284
288
|
return "register.baptism";
|
|
285
289
|
if (/birth|narozen|geburt|urodzen/u.test(f))
|
|
@@ -327,6 +331,29 @@ function find(whole, part) {
|
|
|
327
331
|
}
|
|
328
332
|
return -1;
|
|
329
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Where the words of `part` stand in `whole` apart — a form's fields, the surname in one and the given names in
|
|
336
|
+
* another —: each at a place of its own, after the word before where it can be; undefined when one is not there
|
|
337
|
+
* (a word not read, "[...]", is never found apart).
|
|
338
|
+
*/
|
|
339
|
+
function findApart(whole, part) {
|
|
340
|
+
if (!part.length || part.some((w) => w.illegible))
|
|
341
|
+
return undefined;
|
|
342
|
+
const used = new Set();
|
|
343
|
+
const out = [];
|
|
344
|
+
let from = 0;
|
|
345
|
+
for (const w of part) {
|
|
346
|
+
let i = whole.findIndex((x, k) => k >= from && !used.has(k) && x.key === w.key);
|
|
347
|
+
if (i < 0)
|
|
348
|
+
i = whole.findIndex((x, k) => !used.has(k) && x.key === w.key);
|
|
349
|
+
if (i < 0)
|
|
350
|
+
return undefined;
|
|
351
|
+
used.add(i);
|
|
352
|
+
out.push(whole[i]);
|
|
353
|
+
from = i + 1;
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
330
357
|
/** A name's word in letters alone (folded): Latin and old spellings as today's (ph f, th t, w v, y i, j i, ae e). */
|
|
331
358
|
function spelled(key) {
|
|
332
359
|
return key
|
|
@@ -458,7 +485,8 @@ export function gedcomDate(text) {
|
|
|
458
485
|
}
|
|
459
486
|
/**
|
|
460
487
|
* A reader's entry as strom gives it on: each person's written name must stand in the transcript (folded — no
|
|
461
|
-
* accents, no case — as words in a row
|
|
488
|
+
* accents, no case — as words in a row; on a form, `form`, each word in a field of its own), else the person is left
|
|
489
|
+
* out; its surname must be a form of one of those
|
|
462
490
|
* words in the form of the person's sex, else it is left out (a surname of another person, of the parish's head, of
|
|
463
491
|
* nowhere, a woman's form for a man written in a man's); its given name in
|
|
464
492
|
* today's form must be of the letters of the others, else the name is as written and the reader's form a lead
|
|
@@ -466,7 +494,7 @@ export function gedcomDate(text) {
|
|
|
466
494
|
* transcript marks "[?]" is uncertain, one it marks "[...]" all through is left out (the reader cannot have read it);
|
|
467
495
|
* a date of "[...]" has no GEDCOM; references to people left out go too. What was left out is said (checks).
|
|
468
496
|
*/
|
|
469
|
-
export function checkEntry(raw, id, blind = false) {
|
|
497
|
+
export function checkEntry(raw, id, blind = false, form = false) {
|
|
470
498
|
const checks = [];
|
|
471
499
|
const text = words(raw.transcript);
|
|
472
500
|
const persons = [];
|
|
@@ -478,11 +506,13 @@ export function checkEntry(raw, id, blind = false) {
|
|
|
478
506
|
}
|
|
479
507
|
const w = words(written);
|
|
480
508
|
const at = find(text, w);
|
|
481
|
-
|
|
509
|
+
// a form (a certificate) holds a name in its fields apart — the surname in one, the given names in another —:
|
|
510
|
+
// its words each written there are the name; elsewhere a name stands in a row as written
|
|
511
|
+
const span = at >= 0 ? text.slice(at, at + w.length) : form ? findApart(text, w) : undefined;
|
|
512
|
+
if (!span) {
|
|
482
513
|
checks.push({ entry: id, ref: p.ref, what: written, why: "not-in-transcript" });
|
|
483
514
|
continue;
|
|
484
515
|
}
|
|
485
|
-
const span = text.slice(at, at + w.length);
|
|
486
516
|
const role = (ROLES.includes(p.role) ? p.role : "other");
|
|
487
517
|
const person = { ref: p.ref, role, written: span.map((x) => x.text).join(" ") };
|
|
488
518
|
if (p.roleText && !blind)
|
|
@@ -630,6 +660,46 @@ export function wordDiff(a, b) {
|
|
|
630
660
|
return out;
|
|
631
661
|
}
|
|
632
662
|
const same = (a, b) => foldText(a ?? "") === foldText(b ?? "");
|
|
663
|
+
/**
|
|
664
|
+
* The people of one reader paired with the other's by the name written, the best match first: the whole name alike,
|
|
665
|
+
* else the most of its words shared, a word of the surname weighing more. A person two match equally well (a father
|
|
666
|
+
* and his son of one given name, two of the same name) is paired with none — missing on one side rather than another
|
|
667
|
+
* one's. Index in `mine` → index in `theirs`.
|
|
668
|
+
*/
|
|
669
|
+
function pairByName(mine, theirs) {
|
|
670
|
+
const keys = (p) => words(p.written).filter((x) => !x.illegible).map((x) => x.key);
|
|
671
|
+
const surname = (p) => words(p.surname ?? "").map((x) => x.key);
|
|
672
|
+
const WHOLE = 1000;
|
|
673
|
+
const score = (p, q) => {
|
|
674
|
+
const a = keys(p);
|
|
675
|
+
const b = keys(q);
|
|
676
|
+
if (a.length && a.join(" ") === b.join(" "))
|
|
677
|
+
return WHOLE;
|
|
678
|
+
const other = new Set(b);
|
|
679
|
+
const sur = new Set([...surname(p), ...surname(q)]);
|
|
680
|
+
let n = 0;
|
|
681
|
+
for (const k of new Set(a))
|
|
682
|
+
if (other.has(k))
|
|
683
|
+
n += sur.has(k) ? 2 : 1;
|
|
684
|
+
return n;
|
|
685
|
+
};
|
|
686
|
+
let open = mine.flatMap((p, i) => theirs.map((q, j) => ({ i, j, s: score(p, q) }))).filter((x) => x.s > 0);
|
|
687
|
+
const out = new Map();
|
|
688
|
+
while (open.length) {
|
|
689
|
+
const best = Math.max(...open.map((x) => x.s));
|
|
690
|
+
const top = open.filter((x) => x.s === best);
|
|
691
|
+
const done = { i: new Set(), j: new Set() };
|
|
692
|
+
for (const x of top) {
|
|
693
|
+
// equally good for either side: a tie, neither paired
|
|
694
|
+
if (!top.some((y) => y !== x && (y.i === x.i || y.j === x.j)))
|
|
695
|
+
out.set(x.i, x.j);
|
|
696
|
+
done.i.add(x.i);
|
|
697
|
+
done.j.add(x.j);
|
|
698
|
+
}
|
|
699
|
+
open = open.filter((x) => !done.i.has(x.i) && !done.j.has(x.j));
|
|
700
|
+
}
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
633
703
|
/**
|
|
634
704
|
* The first reader's entries against the blind reader's: paired by page and order; the transcripts word by word, the
|
|
635
705
|
* people by role and order (given name, surname), the events by type and order (date, place). Where they differ is a
|
|
@@ -663,20 +733,38 @@ export function compareReadings(first, blind) {
|
|
|
663
733
|
};
|
|
664
734
|
const theirs = groups(b.persons);
|
|
665
735
|
const seen = new Map();
|
|
736
|
+
const fields = (p, q) => {
|
|
737
|
+
for (const f of ["given", "surname"])
|
|
738
|
+
if (!same(p[f], q[f]))
|
|
739
|
+
variants.push({ field: `${p.ref}.${f}`, at: p[f] ?? "", other: q[f] ?? "" });
|
|
740
|
+
};
|
|
741
|
+
const alone = [];
|
|
666
742
|
for (const p of e.persons) {
|
|
667
743
|
const i = seen.get(p.role) ?? 0;
|
|
668
744
|
seen.set(p.role, i + 1);
|
|
669
745
|
const q = theirs.get(p.role)?.[i];
|
|
670
|
-
if (
|
|
746
|
+
if (q)
|
|
747
|
+
fields(p, q);
|
|
748
|
+
else
|
|
749
|
+
alone.push(p);
|
|
750
|
+
}
|
|
751
|
+
// a person the blind reader gave another role (other / writer): the same one by the name written, the role a
|
|
752
|
+
// variant as its other fields
|
|
753
|
+
const left = [...theirs].flatMap(([role, qs]) => qs.slice(seen.get(role) ?? 0));
|
|
754
|
+
const pairs = pairByName(alone, left);
|
|
755
|
+
for (const [i, p] of alone.entries()) {
|
|
756
|
+
const j = pairs.get(i);
|
|
757
|
+
if (j === undefined) {
|
|
671
758
|
variants.push({ field: `${p.ref}`, at: [p.given, p.surname].filter(Boolean).join(" ") || p.written, other: "" });
|
|
672
759
|
continue;
|
|
673
760
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
761
|
+
const q = left[j];
|
|
762
|
+
variants.push({ field: `${p.ref}.role`, at: p.role, other: q.role });
|
|
763
|
+
fields(p, q);
|
|
677
764
|
}
|
|
678
|
-
|
|
679
|
-
|
|
765
|
+
const taken = new Set(pairs.values());
|
|
766
|
+
for (const [j, q] of left.entries())
|
|
767
|
+
if (!taken.has(j))
|
|
680
768
|
variants.push({ field: "person", at: "", other: [q.given, q.surname].filter(Boolean).join(" ") || q.written });
|
|
681
769
|
const evs = new Map();
|
|
682
770
|
for (const ev of e.events) {
|
|
@@ -881,6 +969,37 @@ export function alive(pid) {
|
|
|
881
969
|
}
|
|
882
970
|
}
|
|
883
971
|
const LEGIBLE_ORDER = { yes: 0, partly: 1, no: 2 };
|
|
972
|
+
/** A variant of a person's role (`p2.role`): no words of the transcript. */
|
|
973
|
+
const ROLE_VARIANT = /^[\p{L}\p{N}_-]+\.role$/u;
|
|
974
|
+
/**
|
|
975
|
+
* The result as the Strom app is given it (GET /read/<id>): a variant of a person's role is no words of the
|
|
976
|
+
* transcript, and the app (3.14) marks each variant where its words stand in the transcript and counts it as a
|
|
977
|
+
* difference — a role code ("other") stood inside "Mother". So the role the blind reader gave goes to the person
|
|
978
|
+
* (`blindRole`) and out of `variants`; the result kept and `strom document show` keep it as a variant.
|
|
979
|
+
*/
|
|
980
|
+
export function resultForApp(r) {
|
|
981
|
+
return {
|
|
982
|
+
...r,
|
|
983
|
+
entries: r.entries.map((e) => {
|
|
984
|
+
const roles = new Map();
|
|
985
|
+
for (const v of e.variants ?? [])
|
|
986
|
+
if (ROLE_VARIANT.test(v.field))
|
|
987
|
+
roles.set(v.field.slice(0, -".role".length), v.other);
|
|
988
|
+
if (!roles.size)
|
|
989
|
+
return e;
|
|
990
|
+
const variants = (e.variants ?? []).filter((v) => !ROLE_VARIANT.test(v.field));
|
|
991
|
+
const { variants: _, ...rest } = e;
|
|
992
|
+
return {
|
|
993
|
+
...rest,
|
|
994
|
+
...(variants.length ? { variants } : {}),
|
|
995
|
+
persons: e.persons.map((p) => {
|
|
996
|
+
const other = roles.get(p.ref);
|
|
997
|
+
return other && ROLES.includes(other) ? { ...p, blindRole: other } : p;
|
|
998
|
+
}),
|
|
999
|
+
};
|
|
1000
|
+
}),
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
884
1003
|
/**
|
|
885
1004
|
* The result of a reading as it stands (while its readers write too: what they wrote so far): the first reader's
|
|
886
1005
|
* entries checked and matched with the tree, the blind reader's held against them.
|
|
@@ -921,7 +1040,7 @@ export function buildResult(tree, s) {
|
|
|
921
1040
|
}
|
|
922
1041
|
for (const e of rep?.entries ?? []) {
|
|
923
1042
|
const id = `e${out.length + 1}`;
|
|
924
|
-
const c = checkEntry(e, id, blind);
|
|
1043
|
+
const c = checkEntry(e, id, blind, rep?.doc.kind === "certificate");
|
|
925
1044
|
if (!blind)
|
|
926
1045
|
checks.push(...c.checks);
|
|
927
1046
|
// a region of a part asked (--region): of the whole page
|
package/dist/core/live-read.js
CHANGED
|
@@ -15,7 +15,7 @@ import path from "node:path";
|
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
16
|
import { newerTree, Tree } from "./tree.js";
|
|
17
17
|
import { actsAsArchive } from "./state.js";
|
|
18
|
-
import { APP_READ_FROM, alive, backBefore, buildResult, DOC_PAGES, FINAL_STATES, loadState, parseRegion, READ_ID, readsDir, reportPath, resultPath, saveState, shownName, statePath } from "./document.js";
|
|
18
|
+
import { APP_READ_FROM, alive, backBefore, buildResult, DOC_PAGES, FINAL_STATES, loadState, parseRegion, READ_ID, readsDir, reportPath, resultForApp, resultPath, saveState, shownName, statePath } from "./document.js";
|
|
19
19
|
import { knownOriginal } from "./originals.js";
|
|
20
20
|
import { readableTypes } from "./readable.js";
|
|
21
21
|
import { makeView } from "./views.js";
|
|
@@ -33,8 +33,6 @@ export const APP_READ_DEFAULT = { hour: 20, day: 60 };
|
|
|
33
33
|
export const READ_QUEUE_MAX = 3;
|
|
34
34
|
/** The largest file read (a page is read at 2000 px at most: a larger file adds nothing). */
|
|
35
35
|
export const READ_MAX_BYTES = 50 * 1024 * 1024;
|
|
36
|
-
/** The agents whose reader of documents was tried (the others wait for a live test: why agent.untested). */
|
|
37
|
-
export const READ_TESTED = ["claude", "script"];
|
|
38
36
|
/** The longest side of a page's picture for the app. */
|
|
39
37
|
const PAGE_PX = 2000;
|
|
40
38
|
const DAY_MS = 24 * 3600_000;
|
|
@@ -283,8 +281,6 @@ function notReady(tree, settings, env, seen, now) {
|
|
|
283
281
|
const known = AGENTS.find((a) => a.id === agent);
|
|
284
282
|
if (agent !== "script" && (!known || !findAgent(known.command, env)))
|
|
285
283
|
return { why: "agent.none" };
|
|
286
|
-
if (!READ_TESTED.includes(agent))
|
|
287
|
-
return { why: "agent.untested" };
|
|
288
284
|
// the last reading that ended: its agent out of its limit, or not signed in — until another reads
|
|
289
285
|
const last = (seen ?? new ReadingsSeen(tree.root, env)).all().find((r) => r.state && FINAL_STATES.includes(r.state.state))?.state;
|
|
290
286
|
if (last?.state === "failed") {
|
|
@@ -397,7 +393,8 @@ export class AppReadings {
|
|
|
397
393
|
const n = this.none(id);
|
|
398
394
|
return this.json(res, n.code, n.body);
|
|
399
395
|
}
|
|
400
|
-
|
|
396
|
+
const r = resultForApp(got.result);
|
|
397
|
+
this.json(res, 200, { ...r, pages: r.pages.map((p) => ({ ...p, image: `read/${id}/page/${p.n}.jpg` })) });
|
|
401
398
|
}
|
|
402
399
|
/** 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). */
|
|
403
400
|
page(res, id, rest) {
|
package/dist/core/readsync.js
CHANGED
|
@@ -13,10 +13,17 @@ import { shownToFile } from "../image/image.js";
|
|
|
13
13
|
import { inputPath, mimeOf, storeShared } from "./media.js";
|
|
14
14
|
import { create, update } from "./records.js";
|
|
15
15
|
import { foldText } from "./text.js";
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* The reading's result as the research keeps it: read here, from its readers' reports as the app is shown it (GET
|
|
18
|
+
* /read/<id>: a newer strom's checks on reports read before), else its finished result (in git — read on another
|
|
19
|
+
* computer).
|
|
20
|
+
*/
|
|
17
21
|
export function readingResult(tree, id) {
|
|
18
22
|
if (!READ_ID.test(id))
|
|
19
23
|
return undefined;
|
|
24
|
+
const s = loadState(tree.root, id);
|
|
25
|
+
if (s?.state === "done")
|
|
26
|
+
return buildResult(tree, s);
|
|
20
27
|
try {
|
|
21
28
|
const file = resultPath(tree.root, id);
|
|
22
29
|
if (fs.existsSync(file))
|
|
@@ -25,8 +32,7 @@ export function readingResult(tree, id) {
|
|
|
25
32
|
catch {
|
|
26
33
|
// an unreadable result is none
|
|
27
34
|
}
|
|
28
|
-
|
|
29
|
-
return s?.state === "done" ? buildResult(tree, s) : undefined;
|
|
35
|
+
return undefined;
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
32
38
|
* The readings of an input (Input.read) as an agent is told of them: each one's ID and state here (its state on this
|
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.18.0-beta.
|
|
30
|
+
export const VERSION = "1.18.0-beta.12";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|
|
@@ -21,6 +21,7 @@ import path from "node:path";
|
|
|
21
21
|
import { addResponses, heardResponse, runJsonLines, webUse } from "./jsonl.js";
|
|
22
22
|
import { usageNumber, tellUsage, tellWeb } from "./runner.js";
|
|
23
23
|
import { effortArgs } from "../agents/effort.js";
|
|
24
|
+
import { DOC_NEXT } from "../core/document.js";
|
|
24
25
|
/** Command-line arguments of a headless run (exported for tests); `first` is its first message. */
|
|
25
26
|
export function antigravityArgs(opts, first) {
|
|
26
27
|
return [
|
|
@@ -36,6 +37,30 @@ export function antigravityArgs(opts, first) {
|
|
|
36
37
|
...(opts.extraArgs ?? []),
|
|
37
38
|
];
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* A reader in agy: its views in its workspace (--add-dir: read without asking); never a skipped permission, whatever the
|
|
42
|
+
* level. agy refuses every file it would write in a headless run, its own folder's too (only rules in the person's
|
|
43
|
+
* settings would allow one, and strom writes none there): its report is its answer, which strom adds to the report
|
|
44
|
+
* (found live 2026-10-20 — the views refused outside its folder, then the report).
|
|
45
|
+
*/
|
|
46
|
+
export function antigravityReaderArgs(opts, files, first) {
|
|
47
|
+
const dirs = [...new Set(files.views.map((v) => path.dirname(path.resolve(v))))];
|
|
48
|
+
return antigravityArgs({ ...opts, permissions: "auto" }, first).flatMap((a, i, all) => (i === all.indexOf("stream-json") ? [a, ...dirs.flatMap((d) => ["--add-dir", d])] : [a]));
|
|
49
|
+
}
|
|
50
|
+
/** What a reader in agy is told beside its task: the report as its answer (exported for tests). */
|
|
51
|
+
export function antigravityReaderNote(report) {
|
|
52
|
+
return (`\n\nHERE: this agent cannot write files. Do not edit ${report}: give every block you would add to it, in order, ` +
|
|
53
|
+
"as your final answer — the blocks alone, nothing before or after them — and strom adds them to the report.");
|
|
54
|
+
}
|
|
55
|
+
/** A reader's answer added to its report at its last line (DOC_NEXT), when it added nothing itself (exported for tests). */
|
|
56
|
+
export function addReaderAnswer(report, answer, next) {
|
|
57
|
+
const text = fs.existsSync(report) ? fs.readFileSync(report, "utf8") : "";
|
|
58
|
+
const at = text.lastIndexOf(next);
|
|
59
|
+
if (at < 0 || !answer.trim() || text.slice(0, at).includes("## Document"))
|
|
60
|
+
return false;
|
|
61
|
+
fs.writeFileSync(report, `${text.slice(0, at)}${answer.trim()}\n${text.slice(at)}`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
39
64
|
/** The first message of a run whose brief is in a file (exported for tests). */
|
|
40
65
|
export function antigravityFirstMessage(briefFile) {
|
|
41
66
|
return (`Your brief for this strom session is the file ${briefFile}. Read that whole file first, every line of it ` +
|
|
@@ -94,6 +119,16 @@ export const antigravityRunner = {
|
|
|
94
119
|
resume: (id, message, timeoutMs) => ({ args: antigravityGoOnArgs({ ...opts, timeoutMs }, id, message), input: "" }),
|
|
95
120
|
};
|
|
96
121
|
const web = { seen: new Set(), on: (w) => tellWeb(opts, w) };
|
|
122
|
+
const reader = opts.readerFiles;
|
|
123
|
+
if (reader) {
|
|
124
|
+
// a reader: its report is its answer, and it is never told to go on through strom
|
|
125
|
+
const args = antigravityReaderArgs(opts, reader, first + antigravityReaderNote(reader.report));
|
|
126
|
+
return runJsonLines("agy", args, opts.env, { ...opts, prompt: "" }, undefined, (msg, heard) => readEvent(msg, heard, said, opts.onProgress, opts.onUsage, web), { totalsAtEnd: true }).then((r) => {
|
|
127
|
+
if (r.outcome === "ok")
|
|
128
|
+
addReaderAnswer(reader.report, r.text, DOC_NEXT);
|
|
129
|
+
return r;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
97
132
|
return runJsonLines("agy", antigravityArgs(opts, first), opts.env, { ...opts, prompt: "" }, undefined, (msg, heard) => readEvent(msg, heard, said, opts.onProgress, opts.onUsage, web), { totalsAtEnd: true, goOn });
|
|
98
133
|
},
|
|
99
134
|
};
|
package/dist/runners/codex.js
CHANGED
|
@@ -18,6 +18,14 @@ import { isolated } from "../core/paths.js";
|
|
|
18
18
|
export function codexWritable(root, shared) {
|
|
19
19
|
return ["-c", `sandbox_workspace_write.writable_roots=${JSON.stringify([path.join(root, ".git"), ...(shared ? [shared] : [])])}`];
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* A reader's sandbox: the folder of its report the one place it writes besides the folder it works in (found live: the
|
|
23
|
+
* report in the tree's notes/readings was refused, "writing outside of the project"), and no network — it reads the
|
|
24
|
+
* views it is given, it fetches nothing. Whatever the person's level: a reader opens its views and writes its report.
|
|
25
|
+
*/
|
|
26
|
+
export function codexReaderWritable(files) {
|
|
27
|
+
return ["-c", `sandbox_workspace_write.writable_roots=${JSON.stringify([path.dirname(path.resolve(files.report))])}`];
|
|
28
|
+
}
|
|
21
29
|
/**
|
|
22
30
|
* The folder Codex works in, trusted for this process alone in an isolated installation: a session Codex starts in a
|
|
23
31
|
* folder its config does not trust it records as trusted in ~/.codex/config.toml (`[projects."<folder>"]`, found
|
|
@@ -47,9 +55,11 @@ export function codexTrust(env, cwd) {
|
|
|
47
55
|
export const CODEX_CLEAN = ["-c", "features.plugins=false", "-c", "features.apps=false", "-c", "features.hooks=false", "-c", "features.memories=false"];
|
|
48
56
|
/** Command-line arguments of a headless run (exported for tests). */
|
|
49
57
|
export function codexArgs(opts) {
|
|
50
|
-
const sandbox = opts.
|
|
51
|
-
? ["--
|
|
52
|
-
:
|
|
58
|
+
const sandbox = opts.readerFiles
|
|
59
|
+
? ["--sandbox", "workspace-write", ...codexReaderWritable(opts.readerFiles)]
|
|
60
|
+
: opts.permissions === "full"
|
|
61
|
+
? ["--dangerously-bypass-approvals-and-sandbox"]
|
|
62
|
+
: ["--sandbox", "workspace-write", "-c", "sandbox_workspace_write.network_access=true", ...codexWritable(opts.cwd, opts.shared)];
|
|
53
63
|
return ["exec", "--json", "--skip-git-repo-check", ...sandbox, ...codexTrust(opts.env ?? {}, opts.cwd), ...(opts.clean ? CODEX_CLEAN : []), ...(opts.model ? ["--model", opts.model] : []), ...effortArgs("codex", opts.effort), ...(opts.extraArgs ?? []), "-"];
|
|
54
64
|
}
|
|
55
65
|
/**
|
|
@@ -57,9 +67,11 @@ export function codexArgs(opts) {
|
|
|
57
67
|
* --sandbox or --add-dir: the same sandbox is set through its configuration.
|
|
58
68
|
*/
|
|
59
69
|
export function codexResumeArgs(id, opts) {
|
|
60
|
-
const sandbox = opts.
|
|
61
|
-
? ["
|
|
62
|
-
:
|
|
70
|
+
const sandbox = opts.readerFiles
|
|
71
|
+
? ["-c", 'sandbox_mode="workspace-write"', ...codexReaderWritable(opts.readerFiles)]
|
|
72
|
+
: opts.permissions === "full"
|
|
73
|
+
? ["--dangerously-bypass-approvals-and-sandbox"]
|
|
74
|
+
: ["-c", 'sandbox_mode="workspace-write"', "-c", "sandbox_workspace_write.network_access=true", ...codexWritable(opts.cwd, opts.shared)];
|
|
63
75
|
return ["exec", "resume", "--json", "--skip-git-repo-check", ...sandbox, ...codexTrust(opts.env ?? {}, opts.cwd), ...(opts.clean ? CODEX_CLEAN : []), ...(opts.model ? ["--model", opts.model] : []), ...effortArgs("codex", opts.effort), id, "-"];
|
|
64
76
|
}
|
|
65
77
|
/**
|
package/dist/runners/grok.js
CHANGED
|
@@ -35,6 +35,14 @@ export function grokTrust(env, reader) {
|
|
|
35
35
|
return { args: [], env: {} };
|
|
36
36
|
return isolated(env) ? { args: [], env: { GROK_FOLDER_TRUST: "0" } } : { args: ["--trust"], env: {} };
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A reader's report, the one file it may write: dontAsk refuses an edit outside the folder it works in, and a reader
|
|
40
|
+
* works beside its views, its report in the tree (found live 2026-10-20: the report refused, the reading lost).
|
|
41
|
+
*/
|
|
42
|
+
export function grokReaderAllow(files) {
|
|
43
|
+
const report = path.resolve(files.report).replace(/\\/g, "/");
|
|
44
|
+
return ["--allow", `Edit(${report})`, "--allow", `Write(${report})`];
|
|
45
|
+
}
|
|
38
46
|
/** Command-line arguments of a headless run (exported for tests). */
|
|
39
47
|
export function grokArgs(opts, prompt, session) {
|
|
40
48
|
return [
|
|
@@ -42,7 +50,7 @@ export function grokArgs(opts, prompt, session) {
|
|
|
42
50
|
...prompt,
|
|
43
51
|
"--output-format",
|
|
44
52
|
"streaming-json",
|
|
45
|
-
...(opts.permissions === "full" ? ["--always-approve"] : ["--permission-mode", "dontAsk"]),
|
|
53
|
+
...(opts.readerFiles ? ["--permission-mode", "dontAsk", ...grokReaderAllow(opts.readerFiles)] : opts.permissions === "full" ? ["--always-approve"] : ["--permission-mode", "dontAsk"]),
|
|
46
54
|
...session,
|
|
47
55
|
...(opts.model ? ["--model", opts.model] : []),
|
|
48
56
|
...effortArgs("grok", opts.effort),
|
package/dist/runners/opencode.js
CHANGED
|
@@ -7,11 +7,48 @@
|
|
|
7
7
|
// (everything the rules do not deny). Events are JSON lines: text, tool_use,
|
|
8
8
|
// step_start, step_finish (tokens and cost of a step), error; each names its
|
|
9
9
|
// session (sessionID), which `--session` resumes.
|
|
10
|
+
import path from "node:path";
|
|
10
11
|
import { runJsonLines, webUse } from "./jsonl.js";
|
|
11
12
|
import { OPENCODE_RUN_AGENT } from "../agents/files.js";
|
|
12
13
|
import { usageNumber, tellUsage, tellWeb } from "./runner.js";
|
|
13
14
|
/** OpenCode's own web tools (OpenCode 2: webfetch with its url, websearch with its query), counted once each call is done. */
|
|
14
15
|
export const OPENCODE_WEB = { page: ["webfetch"], search: ["websearch"] };
|
|
16
|
+
/** OpenCode's agent of a reader, given for its run alone (OPENCODE_CONFIG_CONTENT): no tree's config is where it works. */
|
|
17
|
+
export const OPENCODE_READER_AGENT = "strom-reader";
|
|
18
|
+
/** A reader's message: its task comes on stdin, joined with it. */
|
|
19
|
+
const READER_KICKOFF = "Your task is above: open the pages it names and write your report as it says.";
|
|
20
|
+
/**
|
|
21
|
+
* A reader's agent (exported for tests): it opens its views and edits its report — every other file, the shell, the
|
|
22
|
+
* web, subagents and questions refused (refused, never asked: OpenCode 2 ends a headless run at a question). Paths with
|
|
23
|
+
* forward slashes, as OpenCode matches them.
|
|
24
|
+
*/
|
|
25
|
+
export function opencodeReaderConfig(files) {
|
|
26
|
+
const abs = (p) => path.resolve(p).replace(/\\/g, "/");
|
|
27
|
+
const only = (paths) => ({ "*": "deny", ...Object.fromEntries(paths.map((p) => [p, "allow"])) });
|
|
28
|
+
const report = abs(files.report);
|
|
29
|
+
const views = files.views.map(abs);
|
|
30
|
+
return {
|
|
31
|
+
agent: {
|
|
32
|
+
[OPENCODE_READER_AGENT]: {
|
|
33
|
+
mode: "primary",
|
|
34
|
+
description: "a reader of strom: opens the views it is given, writes its report — nothing else",
|
|
35
|
+
permission: {
|
|
36
|
+
read: only([...views, report]),
|
|
37
|
+
edit: only([report]),
|
|
38
|
+
external_directory: only([...new Set([...views, report].map((f) => `${path.posix.dirname(f)}/*`))]),
|
|
39
|
+
...Object.fromEntries(["bash", "webfetch", "websearch", "task", "question", "skill", "todowrite", "glob", "grep", "list"].map((t) => [t, "deny"])),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Command-line arguments of a reader (exported for tests): a server of its own (--standalone — never the person's
|
|
47
|
+
* background service, which another installation's settings may have started: found live, its port taken), its agent.
|
|
48
|
+
*/
|
|
49
|
+
export function opencodeReaderArgs(opts) {
|
|
50
|
+
return ["run", "--standalone", "--format", "json", "--agent", OPENCODE_READER_AGENT, ...(opts.model ? ["--model", opts.model] : []), ...(opts.extraArgs ?? []), READER_KICKOFF];
|
|
51
|
+
}
|
|
15
52
|
/** Command-line arguments of a headless run (exported for tests). */
|
|
16
53
|
export function opencodeArgs(opts) {
|
|
17
54
|
const level = opts.permissions === "full" ? ["--auto"] : ["--agent", OPENCODE_RUN_AGENT];
|
|
@@ -21,14 +58,17 @@ export const opencodeRunner = {
|
|
|
21
58
|
id: "opencode",
|
|
22
59
|
command: "opencode",
|
|
23
60
|
run(opts) {
|
|
24
|
-
|
|
61
|
+
// a reader: its own agent and rules, given for this run (no tree's opencode.json where it works)
|
|
62
|
+
const reader = opts.readerFiles;
|
|
63
|
+
const env = reader ? { ...opts.env, OPENCODE_CONFIG_CONTENT: JSON.stringify(opencodeReaderConfig(reader)) } : opts.env;
|
|
64
|
+
const args = reader ? opencodeReaderArgs(opts) : opencodeArgs({ ...opts, kickoff: "Your brief is above. Work only through `strom` in this folder, as it says." });
|
|
25
65
|
const tokens = { input: 0, output: 0, cacheRead: 0 };
|
|
26
66
|
let main;
|
|
27
67
|
// the web tools' calls passed on (a call is said again as its state changes)
|
|
28
68
|
const webSeen = new Set();
|
|
29
69
|
// Resumed: its session by id, the message as the prompt.
|
|
30
|
-
const resume = (id, message) => ({ args: [...opencodeArgs({ ...opts, kickoff: message }).slice(0, -1), "--session", id, message], input: "" });
|
|
31
|
-
return runJsonLines("opencode", args,
|
|
70
|
+
const resume = (id, message) => ({ args: [...(reader ? opencodeReaderArgs(opts) : opencodeArgs({ ...opts, kickoff: message })).slice(0, -1), "--session", id, message], input: "" });
|
|
71
|
+
return runJsonLines("opencode", args, env, opts, resume, (msg, heard) => {
|
|
32
72
|
const part = (msg.part ?? {});
|
|
33
73
|
// the run's own session is the first one named; another one is a subagent's (its scan reader) — the run's own
|
|
34
74
|
// is the one resumed at the time limit, never the subagent's that spoke last
|