symbiot 0.54.0 → 0.54.1
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.md +4 -0
- package/agents.mjs +7 -6
- package/lanes.mjs +2 -2
- package/package.json +1 -1
- package/server.mjs +2 -1
- package/tasks.mjs +2 -1
- package/work.mjs +26 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
|
|
4
4
|
|
|
5
|
+
## 0.54.1
|
|
6
|
+
|
|
7
|
+
- **No more lag from a big agent log.** An agent's log keeps every run and everything it reads, screenshots included; one reached 144 MB, and Symbiot reread all of it on almost every refresh, so the app stalled for 5–28 seconds at a time and ate a core. Now it reads only the newest run, from the end of the file, and keeps it until the file changes. A log past 8 MB starts afresh at the next run, with the old one kept as `agent.log.old`.
|
|
8
|
+
|
|
5
9
|
## 0.54.0
|
|
6
10
|
|
|
7
11
|
- **Show Symbiot a screenshot.** Paste one into "Talk to Symbiot" (Ctrl+V) or drop it on the bar: it shows as a thumbnail you can remove, goes with what you say, and the AI you connected sees it (Claude, OpenAI, Gemini, or a local model that reads images). If Symbiot hands the work to an agent, the agent gets the screenshot too. Up to 4 at a time; they're kept a week, readable by you only.
|
package/agents.mjs
CHANGED
|
@@ -5,10 +5,10 @@ import { spawn } from "node:child_process";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join, dirname, resolve, relative } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, writeSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, writeSync, unlinkSync, readdirSync, statSync, renameSync } from "node:fs";
|
|
9
9
|
import { randomBytes, createHash } from "node:crypto";
|
|
10
10
|
import { CONFIG_DIR, loadConfig, saveConfig, loadTasks, TASK_MAX, clipWords, sameTask, taskWords, sh, hasCmd } from "./core.mjs";
|
|
11
|
-
import { parseRun, lastRunText } from "./work.mjs";
|
|
11
|
+
import { parseRun, lastRunText, readRunLog } from "./work.mjs";
|
|
12
12
|
import { parseFacts } from "./handover.mjs";
|
|
13
13
|
import { scanRoots, repoPathMap } from "./scan.mjs";
|
|
14
14
|
import { knowledgeFolders } from "./knowledge.mjs";
|
|
@@ -273,7 +273,7 @@ function earlierRuns() {
|
|
|
273
273
|
const busy = runningHandoff(r.path), ask = agentQuestions(r.path, r.name), wait = waitingFor(r.path), facts = busy ? [] : factsOf(r.path);
|
|
274
274
|
if (!busy && !ask.questions.length && !wait && !facts.length) continue;
|
|
275
275
|
const log = join(r.path, ".symbiot", "agent.log"), startedAt = (busy && busy.startedAt) || Number(r.startedAt) || Date.now();
|
|
276
|
-
let tail = "", work = null, progress = null, end = Date.now(); try { ({ tail, work, progress } = workOf(r.path,
|
|
276
|
+
let tail = "", work = null, progress = null, end = Date.now(); try { ({ tail, work, progress } = workOf(r.path, readRunLog(log))); if (!busy) end = statSync(log).mtimeMs; } catch {}
|
|
277
277
|
out.push({ id: "earlier-" + digest(r.path).slice(0, 8), name: String(r.name || r.path.split("/").pop()), path: r.path, status: busy ? "running" : "done", earlier: true, elapsed: Math.max(0, end - startedAt), exitCode: null, tail, work, progress, changed: agentChanges(r.path, startedAt), ask, held: heldTasks(r.path), waiting: wait, remember: facts.length ? facts : null, fromHeld: false });
|
|
278
278
|
}
|
|
279
279
|
return out;
|
|
@@ -576,10 +576,12 @@ function _findOrcaCli() {
|
|
|
576
576
|
// ---- background jobs --------------------------------------------------------
|
|
577
577
|
// Run a shell command as a tracked, logged background job that shows up live in
|
|
578
578
|
// the Agents tab. Shared by the agent handoff and the local-model setup.
|
|
579
|
+
const LOG_KEEP = 8 * 1024 * 1024;
|
|
579
580
|
function track(name, cmd, cwd, onExit, env) {
|
|
580
581
|
try {
|
|
581
582
|
const dir = join(cwd, ".symbiot"); mkdirSync(dir, { recursive: true });
|
|
582
583
|
const logp = join(dir, "agent.log");
|
|
584
|
+
try { if (statSync(logp).size > LOG_KEEP) renameSync(logp, logp + ".old"); } catch {} // past 8 MB: kept as agent.log.old, and a fresh one starts
|
|
583
585
|
let fd = "ignore"; try { fd = openSync(logp, "a"); writeSync(fd, `\n=== ${name} ${new Date().toISOString()} ===\n$ ${cmd}\n`); } catch {}
|
|
584
586
|
const entry = { id: randomBytes(4).toString("hex"), name, path: cwd, log: logp, startedAt: Date.now(), status: "running", exitCode: null, endedAt: null };
|
|
585
587
|
const child = spawn(cmd, { shell: true, cwd, detached: true, stdio: ["ignore", fd === "ignore" ? "ignore" : fd, fd === "ignore" ? "ignore" : fd], ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
@@ -623,7 +625,7 @@ const QUESTIONS_MAX = 5;
|
|
|
623
625
|
// the next moves up. The rest are a click away.
|
|
624
626
|
const OPTIONS_SHOWN = 2, IDEAS_SHOWN = 2;
|
|
625
627
|
const qKey = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ");
|
|
626
|
-
const readSymbiot = (path, f) => { try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
628
|
+
const readSymbiot = (path, f) => { if (f === "agent.log") return readRunLog(join(path, ".symbiot", f)); try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
627
629
|
// "## Questions" → "### question", context lines, "- option" bullets;
|
|
628
630
|
// "## Suggestions" (or Ideas / Follow-ups) → "- idea" bullets. Forgiving: a bare
|
|
629
631
|
// bullet under Questions is a question with no options.
|
|
@@ -880,8 +882,7 @@ function workOf(path, log) {
|
|
|
880
882
|
function agentsList() {
|
|
881
883
|
const seen = new Set();
|
|
882
884
|
return HANDOFFS.map((e) => {
|
|
883
|
-
|
|
884
|
-
const { tail, work, progress } = workOf(e.path, log);
|
|
885
|
+
const { tail, work, progress } = workOf(e.path, readRunLog(e.log));
|
|
885
886
|
const first = !seen.has(e.path); seen.add(e.path);
|
|
886
887
|
const facts = first && e.status !== "running" ? factsOf(e.path) : [];
|
|
887
888
|
return { id: e.id, name: e.name, path: e.path, status: e.status, elapsed: (e.endedAt || Date.now()) - e.startedAt, exitCode: e.exitCode, tail, work, progress, changed: agentChanges(e.path, e.startedAt), ask: first ? agentQuestions(e.path, e.name) : null, held: first ? heldTasks(e.path) : null, waiting: first && e.status !== "running" ? waitingFor(e.path) : null, remember: facts.length ? facts : null, fromHeld: !!e.fromHeld };
|
package/lanes.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { runHandoff, runningHandoff, waitingFor, agentQuestions } from "./agents
|
|
|
23
23
|
import { addTask, pushTasks } from "./tasks.mjs";
|
|
24
24
|
import { actNow } from "./mind.mjs";
|
|
25
25
|
import { repoPathMap } from "./scan.mjs";
|
|
26
|
-
import { parseRun, lastRunText } from "./work.mjs";
|
|
26
|
+
import { parseRun, lastRunText, readRunLog } from "./work.mjs";
|
|
27
27
|
import { OPS, parseHandoffs } from "./handover.mjs";
|
|
28
28
|
|
|
29
29
|
const LEDGER = join(CONFIG_DIR, "lanes.json");
|
|
@@ -33,7 +33,7 @@ const MAX_KEEP = 200, MAX_CHAIN = 4, TAIL = 4000;
|
|
|
33
33
|
function loadLedger() { try { const d = JSON.parse(readFileSync(LEDGER, "utf8")); return { handoffs: Array.isArray(d.handoffs) ? d.handoffs : [] }; } catch { return { handoffs: [] }; } }
|
|
34
34
|
// It can name accounts and what's in them: yours only (0600).
|
|
35
35
|
function saveLedger(d) { try { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(LEDGER, JSON.stringify({ handoffs: d.handoffs.slice(-MAX_KEEP) }, null, 1), { mode: 0o600 }); try { chmodSync(LEDGER, 0o600); } catch {} return true; } catch { return false; } }
|
|
36
|
-
const readSym = (path, f) => { try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
36
|
+
const readSym = (path, f) => { if (f === "agent.log") return readRunLog(join(path, ".symbiot", f)); try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
37
37
|
const firstLine = (t) => String(t || "").split("\n").find((l) => l.trim()) || "";
|
|
38
38
|
|
|
39
39
|
// A folder's lane: the repo it is, or ops for a run of its own.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.54.
|
|
3
|
+
"version": "0.54.1",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { gunzipSync } from "node:zlib";
|
|
10
|
+
import { readRunLog } from "./work.mjs";
|
|
10
11
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
11
12
|
import { createServer } from "node:http";
|
|
12
13
|
import { randomBytes } from "node:crypto";
|
|
@@ -413,7 +414,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
413
414
|
// giving up and sending you to a terminal.
|
|
414
415
|
const attempt = (n) => track("symbiot update", inst, homedir(), (code) => {
|
|
415
416
|
if (code !== 0) {
|
|
416
|
-
|
|
417
|
+
const tail = readRunLog(join(homedir(), ".symbiot", "agent.log")).slice(-3000);
|
|
417
418
|
if (n < 5 && /E404|ETARGET|notarget|No matching version|is not in this registry/i.test(tail)) { UPDATING = { target, retrying: true, attempt: n + 1, at: Date.now() }; setTimeout(() => attempt(n + 1), 30000).unref(); }
|
|
418
419
|
else UPDATING = null;
|
|
419
420
|
return;
|
package/tasks.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { handoverRules, ONLY_YOU, HANDBACK } from "./handover.mjs";
|
|
|
11
11
|
import { userStyleLine } from "./adapt.mjs";
|
|
12
12
|
import { QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, handoffCmd, runningHandoff, writeTasks, droppedTasks, startHeldTasks, connectorsLine } from "./agents.mjs";
|
|
13
13
|
import { gitDefaultBranch } from "./drift.mjs";
|
|
14
|
+
import { readRunLog } from "./work.mjs";
|
|
14
15
|
import { repoPathMap, openWork, detectRepo } from "./scan.mjs";
|
|
15
16
|
|
|
16
17
|
// ---- tasks: a persistent checklist (stored by core.mjs) -------------------
|
|
@@ -342,7 +343,7 @@ function shipWithBump(path, texts, opts) {
|
|
|
342
343
|
// What the last run in a folder said when it finished: agent.log after the last
|
|
343
344
|
// run's "=== name time ===" and "$ command" lines.
|
|
344
345
|
function runSummary(path) {
|
|
345
|
-
|
|
346
|
+
const log = readRunLog(join(path, ".symbiot", "agent.log")); if (!log) return "";
|
|
346
347
|
const at = log.lastIndexOf("\n=== "), rest = at < 0 ? log : log.slice(log.indexOf("\n", at + 1) + 1);
|
|
347
348
|
return rest.replace(/^\$ .*\n?/, "").trim().slice(-8000);
|
|
348
349
|
}
|
package/work.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// time. Another agent (Codex, Aider, Gemini) writes plain text: its last lines
|
|
9
9
|
// come through as what it said. Pure: text in, data out.
|
|
10
10
|
|
|
11
|
+
import { statSync, openSync, readSync, closeSync } from "node:fs";
|
|
12
|
+
|
|
11
13
|
const MAX_STEPS = 60;
|
|
12
14
|
const short = (s, n = 80) => { s = String(s ?? "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1) + "…" : s; };
|
|
13
15
|
const base = (p) => String(p || "").split("/").filter(Boolean).slice(-2).join("/");
|
|
@@ -103,6 +105,29 @@ function parseRun(text) {
|
|
|
103
105
|
return out;
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
// The newest run of an agent.log, from its "=== " header on, read from the end of the
|
|
109
|
+
// file. A log keeps every run and everything they read (one reached 144 MB: 96 runs and
|
|
110
|
+
// the screenshots they looked at), and every view only needs the newest, so it never
|
|
111
|
+
// reads the rest; and it's kept until the file changes, so a view that asks again
|
|
112
|
+
// costs nothing. Past RUN_CAP of one run, it's that run's last RUN_CAP.
|
|
113
|
+
const RUN_CAP = 16 * 1024 * 1024, RUN_CACHE = new Map();
|
|
114
|
+
function readRunLog(file) {
|
|
115
|
+
let st; try { st = statSync(file); } catch { return ""; }
|
|
116
|
+
const hit = RUN_CACHE.get(file); if (hit && hit.size === st.size && hit.mtime === st.mtimeMs) return hit.text;
|
|
117
|
+
let fd; try { fd = openSync(file, "r"); } catch { return ""; }
|
|
118
|
+
let buf = Buffer.alloc(0), at = -1;
|
|
119
|
+
try {
|
|
120
|
+
for (let end = st.size; end > 0 && buf.length < RUN_CAP;) {
|
|
121
|
+
const start = Math.max(0, end - (1 << 20)), b = Buffer.alloc(end - start);
|
|
122
|
+
readSync(fd, b, 0, b.length, start); buf = Buffer.concat([b, buf]); end = start;
|
|
123
|
+
at = buf.lastIndexOf("\n=== "); if (at >= 0) break;
|
|
124
|
+
}
|
|
125
|
+
} catch { buf = Buffer.alloc(0); } finally { try { closeSync(fd); } catch {} }
|
|
126
|
+
const text = (at >= 0 ? buf.subarray(at) : buf).toString("utf8");
|
|
127
|
+
RUN_CACHE.delete(file); RUN_CACHE.set(file, { size: st.size, mtime: st.mtimeMs, text });
|
|
128
|
+
if (RUN_CACHE.size > 40) RUN_CACHE.delete(RUN_CACHE.keys().next().value);
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
106
131
|
// The newest run's part of an agent.log (after its header and command line).
|
|
107
132
|
function lastRunText(log) {
|
|
108
133
|
const s = String(log || ""), at = s.lastIndexOf("\n=== ");
|
|
@@ -111,4 +136,4 @@ function lastRunText(log) {
|
|
|
111
136
|
// What the agent said last: its final answer, from either kind of log.
|
|
112
137
|
function finalOf(log) { return parseRun(lastRunText(log)).final; }
|
|
113
138
|
|
|
114
|
-
export { parseRun, lastRunText, finalOf, describe, testsIn };
|
|
139
|
+
export { parseRun, lastRunText, finalOf, describe, testsIn, readRunLog };
|