test / m2.test.ts
test / m2.test.ts
// Tests for M2: automatic start, source verification, staleness reminders, the health line,
// hand edits of ledger.md, and questions about the user's projects.
import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { test } from "node:test";
const HOME = fs.mkdtempSync(path.join(os.tmpdir(), "throughline-m2-"));
process.env.THROUGHLINE_HOME = HOME;
import { loadChatState } from "../src/chat";
import { FULL_OPEN, HEADER_OPEN } from "../src/digest";
import { normalizeUrl, urlInSource } from "../src/history";
import { listProjects, loadLedger, loadMeta, markdownFile, parseMarkdown, readEvents, renderMarkdown, saveMeta } from "../src/ledger";
import { AUTO_START_NOTE } from "../src/preprocessor";
import { Session } from "./helpers";
const search = (q: string) => ({ name: "web_search", args: { query: q, goal: q }, result: "status: ok\nresults:\n1. Something\n https://example.org/only-in-search" });
const read = (url: string, result = "status: ok\nPage text") => ({ name: "read_webpage", args: { url, goal: "g" }, result });
async function researchTurn(s: Session, text: string, calls: Array<{ name: string; args?: Record<string, unknown>; result?: string }>) {
const out = await s.send(text);
s.toolRound(calls);
s.reply("Here is what I found.");
return out;
}
test("a project starts on its own after two research-heavy turns, with both requests as the goal, word for word", async () => {
const s = new Session("2000000000001");
// Memory and clock lookups do not count as work.
await researchTurn(s, "Good morning.", [{ name: "read_memory" }, { name: "get_current_time" }, search("weather")]);
await researchTurn(s, "Compare the default context lengths of three inference servers.", [search("a"), search("b"), read("https://example.org/a")]);
const second = await researchTurn(s, "Now add their licenses.", [search("c"), read("https://example.org/b"), read("https://example.org/c")]);
assert.equal(second.text, "Now add their licenses.", "nothing added before the start");
assert.equal(loadChatState("2000000000001"), null, "nothing written before the start");
const third = await s.send("Continue.");
const slug = loadChatState("2000000000001")!.project!;
const l = loadLedger(slug)!;
assert.equal(l.goal, "Compare the default context lengths of three inference servers. Now add their licenses.");
assert.equal(l.goalSource, "user_message");
assert.equal(l.autoStarted, true);
assert.ok(third.text.includes(FULL_OPEN));
assert.ok(third.text.includes("Goal (taken from the user's message; the user can change it with /tl goal)"));
assert.ok(third.text.includes(AUTO_START_NOTE));
assert.match(third.statuses[0], /started compare-default-context-lengths-three automatically \| \/tl off to stop/);
assert.equal((await s.tools()).length, 4);
// /tl off straight away removes the empty project and keeps the chat from starting another.
s.reply("OK.");
const off = await s.send("/tl off");
assert.match(off.text, /started automatically and was still empty, so it was removed/);
assert.ok(!listProjects().includes(slug));
await researchTurn(s, "More.", [search("d"), search("e"), search("f")]);
await researchTurn(s, "And more.", [search("g"), search("h"), search("i")]);
const after = await s.send("Again.");
assert.equal(after.text, "Again.");
assert.equal(loadChatState("2000000000001")!.project, undefined);
});
test("auto-start is off in settings when the user turns it off", async () => {
const s = new Session("2000000000002");
s.config = { autoStart: false };
await researchTurn(s, "One.", [search("a"), search("b"), search("c")]);
await researchTurn(s, "Two.", [search("d"), search("e"), search("f")]);
const out = await s.send("Three.");
assert.equal(out.text, "Three.");
});
test("sources are checked against pages actually read in the chat", async () => {
assert.equal(normalizeUrl("https://www.Example.org/a/b/?x=1#frag"), "example.org/a/b");
assert.equal(urlInSource("GitHub releases page (github.com/ollama/ollama/releases)"), "github.com/ollama/ollama/releases");
assert.equal(urlInSource("user"), null);
const s = new Session("2000000000003");
await s.send("/tl start Find two facts.");
s.reply("Started.");
await s.send("Go.");
s.toolRound([search("facts"), read("https://example.org/read-ok"), read("https://example.org/failed", "status: captcha\nSolve it")]);
await s.call("ledger_record", { kind: "finding", text: "Fact one.", source: "https://example.org/read-ok/" });
await s.call("ledger_record", { kind: "finding", text: "Fact two.", source: "https://example.org/only-in-search" });
await s.call("ledger_record", { kind: "finding", text: "Fact three.", source: "https://example.org/failed" });
await s.call("ledger_record", { kind: "finding", text: "Fact four.", source: "turn 1" });
await s.call("ledger_record", { kind: "finding", text: "Fact five." });
s.reply("Recorded.");
const next = await s.send("Next.");
const slug = loadChatState("2000000000003")!.project!;
const v = Object.fromEntries(loadLedger(slug)!.findings.map((f) => [f.id, f.verification]));
assert.deepEqual(v, { F1: "verified", F2: "not_read", F3: "not_read", F4: "verified", F5: "unverified" });
assert.match(next.text, /Health: .*F2, F3 cite a page or turn not found in this chat \(read it, or correct the source\); F5 has no source/);
// Reading the page later upgrades the finding.
s.toolRound([read("https://example.org/only-in-search")]);
s.reply("Read it.");
await s.send("And?");
assert.equal(loadLedger(slug)!.findings[1].verification, "verified");
// Another chat on the same project never judges this chat's findings.
const s2 = new Session("2000000000004");
await s2.send(`/tl open ${slug}`);
await s2.send("Hi.");
assert.equal(loadLedger(slug)!.findings[2].verification, "not_read");
});
test("a reminder comes after two turns of tool use without a ledger update, and not every turn", async () => {
const s = new Session("2000000000005");
await s.send("/tl start Track a slow task.");
s.reply("Started.");
await s.send("Plan it.");
await s.call("ledger_plan", { action: "add", steps: ["First", "Second"], status: "doing" });
s.reply("Planned.");
await researchTurn(s, "Work.", [search("a")]); // the plan change above resets the count
await researchTurn(s, "Work more.", [search("b")]);
const third = await s.send("Keep going.");
assert.match(third.text, /Reminder: the last 2 turns used tools but the ledger was not updated/);
assert.match(third.statuses[0], /reminder sent/);
s.toolRound([search("c")]);
s.reply("Done.");
const fourth = await s.send("Next.");
assert.ok(!fourth.text.includes("Reminder:"), "not repeated on the next turn");
await s.call("ledger_record", { kind: "finding", text: "A result.", source: "user" });
s.reply("Recorded.");
const fifth = await s.send("Then?");
assert.ok(!fifth.text.includes("Reminder:"));
assert.equal(loadChatState("2000000000005")!.staleTurns, 0, "a ledger update resets the count");
});
test("the health line reports plan problems, and a finished step with nothing recorded once", async () => {
const s = new Session("2000000000006");
await s.send("/tl start Check health reporting.");
s.reply("Started.");
await s.send("Plan.");
await s.call("ledger_plan", { action: "add", steps: ["Alpha", "Beta", "Gamma"] });
s.reply("Planned.");
const noneDoing = await s.send("Go.");
assert.match(noneDoing.text, /Health: no step is marked doing\./);
await s.call("ledger_plan", { action: "update", step: 1, status: "doing" });
await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
s.reply("Working.");
const two = await s.send("Go on.");
assert.match(two.text, /steps 1 and 2 are all marked doing \(keep one\)/);
await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
s.reply("Done with one.");
const once = await s.send("Next.");
assert.match(once.text, /step 1 was marked done with nothing recorded during it/);
s.reply("Noted.");
const later = await s.send("Next again.");
assert.ok(!later.text.includes("step 1 was marked done"), "reported once");
});
test("a hand edit of ledger.md is applied, logged as the user's, and shown to the model", async () => {
const s = new Session("2000000000007");
await s.send("/tl start Edit me by hand.");
s.reply("Started.");
await s.send("Plan.");
s.toolRound([read("https://example.org/source")]);
await s.call("ledger_plan", { action: "add", steps: ["Collect", "Write"], status: "doing" });
await s.call("ledger_record", { kind: "finding", text: "Keep this one.", source: "https://example.org/source" });
await s.call("ledger_record", { kind: "finding", text: "Delete this one.", source: "user" });
await s.call("ledger_record", { kind: "question", text: "Which format?" });
s.reply("Done.");
await s.send("Status?");
const slug = loadChatState("2000000000007")!.project!;
assert.equal(loadLedger(slug)!.findings[0].verification, "verified");
const file = markdownFile(slug);
const md = fs.readFileSync(file, "utf8");
assert.deepEqual(parseMarkdown(md).plan.map((p) => p.status), ["doing", "todo"], "the rendering reads back");
const edited = md
.replace("Edit me by hand.", "Edit me by hand, carefully.")
.replace("1. [doing] Collect", "1. [done] Collect\n note: finished by hand")
.replace(/- F2: Delete this one\.\n source: user\n status: verified\n/, "- A finding I typed myself.\n source: user\n")
.replace("- Q1: Which format?", "- Q1: Which format? Markdown.");
assert.notEqual(edited, md);
fs.writeFileSync(file, edited);
s.reply("OK.");
const next = await s.send("Carry on.");
assert.ok(next.text.includes(FULL_OPEN), "a hand edit brings the full state");
assert.match(next.text, /Notice: the user edited the ledger by hand \(goal, plan, findings, questions\)/);
assert.match(next.statuses[0], /your edit applied/);
const l = loadLedger(slug)!;
assert.equal(l.goal, "Edit me by hand, carefully.");
assert.deepEqual(l.plan.map((p) => [p.status, p.note]), [["done", "finished by hand"], ["todo", ""]]);
assert.deepEqual(l.findings.map((f) => [f.id, f.text, f.verification]), [["F1", "Keep this one.", "verified"], ["F3", "A finding I typed myself.", "verified"]]);
assert.equal(l.questions[0].text, "Which format? Markdown.");
const edits = readEvents(slug, 0).filter((e) => e.op === "hand_edit");
assert.equal(edits.length, 1);
assert.equal(edits[0].actor, "user");
// An edit that cannot be read is kept aside, and the ledger is untouched.
fs.writeFileSync(file, fs.readFileSync(file, "utf8").replace("## Plan\n\n", "## Plan\n\nthis is not a step\n"));
s.reply("OK.");
const bad = await s.send("Again.");
assert.match(bad.text, /Notice: the user's edit to ledger\.md could not be read \(a plan line could not be read: "this is not a step"\)/);
assert.ok(fs.readdirSync(path.dirname(file)).some((f) => f.startsWith("ledger.edit-not-applied-")));
assert.equal(loadLedger(slug)!.plan.length, 2);
assert.ok(!fs.readFileSync(file, "utf8").includes("this is not a step"), "the file is restored from the ledger");
// Whitespace-only changes are not edits.
fs.writeFileSync(file, fs.readFileSync(file, "utf8") + "\n\n");
s.reply("OK.");
const ws = await s.send("Fine.");
assert.ok(!ws.text.includes("Notice:"));
});
test("a ledger.md from before this version is not mistaken for a hand edit", async () => {
const s = new Session("2000000000008");
await s.send("/tl start Upgrade check.");
const slug = loadChatState("2000000000008")!.project!;
const meta = loadMeta(slug);
delete meta.renderedHash;
saveMeta(slug, meta);
fs.writeFileSync(markdownFile(slug), "# old format\n\nGoal: something\n");
s.reply("OK.");
const out = await s.send("Hello.");
assert.ok(!out.text.includes("Notice:"));
assert.equal(fs.readFileSync(markdownFile(slug), "utf8"), renderMarkdown(loadLedger(slug)!));
});
test("asking about projects attaches the list, in any chat", async () => {
const s = new Session("2000000000009");
const asked = await s.send("What's the status of my projects?");
assert.match(asked.text, /\[throughLine: the user's projects, for answering this message\.\]\n/);
assert.match(asked.text, /edit-me-hand: Edit me by hand, carefully\. \| 1 of 2 steps done \| 2 findings/);
assert.match(asked.text, /upgrade-check: Upgrade check\. \| no plan yet \| 0 findings/);
assert.match(asked.text, /track-slow-task: Track a slow task\. \| now on step 1: First \| 1 finding \|/);
assert.ok(!asked.text.includes("compare-default-context"), "the removed false start is not listed");
assert.match(asked.statuses[0], /project list attached/);
assert.equal(loadChatState("2000000000009"), null, "asking does not start or bind anything");
const plain = await s.send("Tell me about Lisbon.");
assert.equal(plain.text, "Tell me about Lisbon.");
const p = new Session("2000000000010");
await p.send("/tl start A project of its own.");
p.reply("OK.");
const inProject = await p.send("How are the projects going?");
assert.ok(inProject.text.includes(FULL_OPEN) || inProject.text.includes(HEADER_OPEN));
assert.match(inProject.text, /project-its-own \(this chat\)/);
});
test("an edit saved by an editor that escapes Markdown or drops indentation still reads back", () => {
const md = [
"# x", "", "## Goal", "", "Find versions\\.", "", "## Constraints", "", "\\- Official sources only.", "",
"## Plan", "", "1\\. \\[done\\] First step", "note: finished early", "2. \\[todo] Second step", "",
"## Findings", "", "\\- F1: A fact.", "source: https://example.org/a", "status: verified", "",
"## Decisions", "", "(none)", "", "## Open questions", "", "(none)", "",
].join("\r\n");
const p = parseMarkdown("\uFEFF" + md);
assert.equal(p.goal, "Find versions.");
assert.deepEqual(p.constraints, ["Official sources only."]);
assert.deepEqual(p.plan.map((s) => [s.id, s.status, s.text, s.note]), [[1, "done", "First step", "finished early"], [2, "todo", "Second step", ""]]);
assert.deepEqual(p.findings, [{ id: "F1", text: "A fact.", source: "https://example.org/a" }]);
});
test("the state keeps the model inside the goal, and says so once the plan is done", async () => {
const s = new Session("2000000000011");
await s.send("/tl start List three things.");
s.reply("OK.");
const first = await s.send("Go.");
assert.match(first.text, /Stay within the goal: add steps beyond it only if the user asks/);
await s.call("ledger_plan", { action: "add", steps: ["Only step"], status: "doing" });
const done = await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
assert.match(done, /next: All steps are done\. Give the user the result and stop\. Add steps beyond the goal only if the user asks\./);
s.reply("Done.");
const header = await s.send("Continue.");
assert.ok(header.text.includes(HEADER_OPEN));
assert.match(header.text, /Stay within the goal/);
});
test("the full state counts hidden plan steps by position", () => {
const { renderFull } = require("../src/digest");
const l: any = { name: "x", goal: "g", goalSource: "command", constraints: [], findings: [], decisions: [], questions: [],
plan: [1, 2, 3, 4, 5, 6, 7, 8].map((id) => ({ id, text: `S${id}`, status: "done", note: "" })) };
const all = renderFull(l, 600);
assert.match(all, /\(3 earlier steps, 3 done\)\n 4\. S4 \(done\)/);
assert.ok(!all.includes("later step"));
l.plan[1].status = "doing"; l.plan.slice(2).forEach((s: any) => (s.status = "todo"));
const mid = renderFull(l, 600);
assert.match(mid, / 1\. S1 \(done\)\n 2\. S2 \(doing\)/);
assert.match(mid, / 6\. S6 \(todo\)\n \(2 later steps\)/);
});
test("a copy opened before the model's latest work keeps that work when saved", async () => {
const s = new Session("2000000000012");
await s.send("/tl start Keep concurrent work.");
s.reply("OK.");
await s.send("Go.");
s.toolRound([read("https://example.org/one"), read("https://example.org/two")]);
await s.call("ledger_record", { kind: "finding", text: "First fact.", source: "https://example.org/one" });
const slug = loadChatState("2000000000012")!.project!;
const file = markdownFile(slug);
const opened = fs.readFileSync(file, "utf8"); // the user opens the file in an editor here
await s.call("ledger_record", { kind: "finding", text: "Second fact, recorded while the file was open.", source: "https://example.org/two" });
await s.call("ledger_plan", { action: "add", steps: ["A step added meanwhile"], status: "doing" });
fs.writeFileSync(file, opened.replace("## Constraints\n\n(none)", "## Constraints\n\n- Official sources only."));
s.reply("OK.");
const next = await s.send("Continue.");
assert.match(next.text, /Notice: the user edited the ledger by hand \(constraints\)/);
const l = loadLedger(slug)!;
assert.deepEqual(l.constraints, ["Official sources only."]);
assert.deepEqual(l.findings.map((f) => f.id), ["F1", "F2"], "the model's later finding survives");
assert.deepEqual(l.plan.map((p) => p.text), ["A step added meanwhile"]);
// A deliberate deletion from an up-to-date copy is applied.
const fresh = fs.readFileSync(file, "utf8");
fs.writeFileSync(file, fresh.replace(/- F1: First fact\.\n source: \S+\n status: \S+\n/, ""));
s.reply("OK.");
await s.send("And now?");
assert.deepEqual(loadLedger(slug)!.findings.map((f) => f.id), ["F2"]);
});
test("steps added after finishing the plan in the same reply are flagged as beyond the request", async () => {
const s = new Session("2000000000031");
await s.send("/tl start Compare two tools.");
s.reply("OK.");
await s.send("Research the first tool.");
await s.call("ledger_plan", { action: "add", steps: ["First tool"], status: "doing" });
await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
const added = await s.call("ledger_plan", { action: "add", steps: ["Second tool", "Third tool"] });
assert.match(added, /next: The plan was finished earlier in this reply/);
const started = await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
assert.match(started, /next: The plan was finished earlier in this reply/);
s.reply("Here is the first tool. Next could be the second and third.");
await s.send("Now the second tool.");
const next = await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
assert.doesNotMatch(next, /finished earlier in this reply/, "a new message that asks for it starts it normally");
assert.match(next, /next: Continue with the current step/);
});
test("adding steps to an unfinished plan is not flagged", async () => {
const s = new Session("2000000000032");
await s.send("/tl start Compare tools.");
s.reply("OK.");
await s.send("Go.");
await s.call("ledger_plan", { action: "add", steps: ["One", "Two"], status: "doing" });
await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
const more = await s.call("ledger_plan", { action: "add", steps: ["Three"] });
assert.doesNotMatch(more, /finished earlier in this reply/);
});
test("starting another step in the reply that finished one asks whether the user wanted it", async () => {
const s = new Session("2000000000033");
await s.send("/tl start Compare three tools.");
s.reply("OK.");
await s.send("Research the first tool.");
await s.call("ledger_plan", { action: "add", steps: ["First", "Second", "Third"], status: "doing" });
await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
const next = await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
assert.match(next, /next: A step was finished earlier in this reply\. If the user's latest message asked only for that step/);
assert.match(next, /automatic note from context management telling you to continue is not a new request/);
s.reply("First tool done.");
await s.send("Now the second.");
const asked = await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
assert.match(asked, /next: Continue with the current step/);
});
test("a page reached through a redirect counts as read at the address the tool reported", async () => {
const { readUrls, turnsOf } = require("../src/history");
const { Chat } = require("@lmstudio/sdk");
const s = new Session("2000000000034");
await s.send("Read the server page.");
s.toolRound([{ name: "read_webpage", args: { url: "https://docs.example.org/serving/openai_server.html" }, result: '[{"type":"text","text":"status: ok\\ntitle: Online Serving\\nurl: https://docs.example.org/serving/online/\\n----- page text"}]' }]);
const urls: Set<string> = readUrls(turnsOf(s.history));
assert.ok(urls.has("docs.example.org/serving/openai_server.html"));
assert.ok(urls.has("docs.example.org/serving/online"), "the redirect target counts too");
void Chat;
});
// Tests for M2: automatic start, source verification, staleness reminders, the health line,
// hand edits of ledger.md, and questions about the user's projects.
import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { test } from "node:test";
const HOME = fs.mkdtempSync(path.join(os.tmpdir(), "throughline-m2-"));
process.env.THROUGHLINE_HOME = HOME;
import { loadChatState } from "../src/chat";
import { FULL_OPEN, HEADER_OPEN } from "../src/digest";
import { normalizeUrl, urlInSource } from "../src/history";
import { listProjects, loadLedger, loadMeta, markdownFile, parseMarkdown, readEvents, renderMarkdown, saveMeta } from "../src/ledger";
import { AUTO_START_NOTE } from "../src/preprocessor";
import { Session } from "./helpers";
const search = (q: string) => ({ name: "web_search", args: { query: q, goal: q }, result: "status: ok\nresults:\n1. Something\n https://example.org/only-in-search" });
const read = (url: string, result = "status: ok\nPage text") => ({ name: "read_webpage", args: { url, goal: "g" }, result });
async function researchTurn(s: Session, text: string, calls: Array<{ name: string; args?: Record<string, unknown>; result?: string }>) {
const out = await s.send(text);
s.toolRound(calls);
s.reply("Here is what I found.");
return out;
}
test("a project starts on its own after two research-heavy turns, with both requests as the goal, word for word", async () => {
const s = new Session("2000000000001");
// Memory and clock lookups do not count as work.
await researchTurn(s, "Good morning.", [{ name: "read_memory" }, { name: "get_current_time" }, search("weather")]);
await researchTurn(s, "Compare the default context lengths of three inference servers.", [search("a"), search("b"), read("https://example.org/a")]);
const second = await researchTurn(s, "Now add their licenses.", [search("c"), read("https://example.org/b"), read("https://example.org/c")]);
assert.equal(second.text, "Now add their licenses.", "nothing added before the start");
assert.equal(loadChatState("2000000000001"), null, "nothing written before the start");
const third = await s.send("Continue.");
const slug = loadChatState("2000000000001")!.project!;
const l = loadLedger(slug)!;
assert.equal(l.goal, "Compare the default context lengths of three inference servers. Now add their licenses.");
assert.equal(l.goalSource, "user_message");
assert.equal(l.autoStarted, true);
assert.ok(third.text.includes(FULL_OPEN));
assert.ok(third.text.includes("Goal (taken from the user's message; the user can change it with /tl goal)"));
assert.ok(third.text.includes(AUTO_START_NOTE));
assert.match(third.statuses[0], /started compare-default-context-lengths-three automatically \| \/tl off to stop/);
assert.equal((await s.tools()).length, 4);
// /tl off straight away removes the empty project and keeps the chat from starting another.
s.reply("OK.");
const off = await s.send("/tl off");
assert.match(off.text, /started automatically and was still empty, so it was removed/);
assert.ok(!listProjects().includes(slug));
await researchTurn(s, "More.", [search("d"), search("e"), search("f")]);
await researchTurn(s, "And more.", [search("g"), search("h"), search("i")]);
const after = await s.send("Again.");
assert.equal(after.text, "Again.");
assert.equal(loadChatState("2000000000001")!.project, undefined);
});
test("auto-start is off in settings when the user turns it off", async () => {
const s = new Session("2000000000002");
s.config = { autoStart: false };
await researchTurn(s, "One.", [search("a"), search("b"), search("c")]);
await researchTurn(s, "Two.", [search("d"), search("e"), search("f")]);
const out = await s.send("Three.");
assert.equal(out.text, "Three.");
});
test("sources are checked against pages actually read in the chat", async () => {
assert.equal(normalizeUrl("https://www.Example.org/a/b/?x=1#frag"), "example.org/a/b");
assert.equal(urlInSource("GitHub releases page (github.com/ollama/ollama/releases)"), "github.com/ollama/ollama/releases");
assert.equal(urlInSource("user"), null);
const s = new Session("2000000000003");
await s.send("/tl start Find two facts.");
s.reply("Started.");
await s.send("Go.");
s.toolRound([search("facts"), read("https://example.org/read-ok"), read("https://example.org/failed", "status: captcha\nSolve it")]);
await s.call("ledger_record", { kind: "finding", text: "Fact one.", source: "https://example.org/read-ok/" });
await s.call("ledger_record", { kind: "finding", text: "Fact two.", source: "https://example.org/only-in-search" });
await s.call("ledger_record", { kind: "finding", text: "Fact three.", source: "https://example.org/failed" });
await s.call("ledger_record", { kind: "finding", text: "Fact four.", source: "turn 1" });
await s.call("ledger_record", { kind: "finding", text: "Fact five." });
s.reply("Recorded.");
const next = await s.send("Next.");
const slug = loadChatState("2000000000003")!.project!;
const v = Object.fromEntries(loadLedger(slug)!.findings.map((f) => [f.id, f.verification]));
assert.deepEqual(v, { F1: "verified", F2: "not_read", F3: "not_read", F4: "verified", F5: "unverified" });
assert.match(next.text, /Health: .*F2, F3 cite a page or turn not found in this chat \(read it, or correct the source\); F5 has no source/);
// Reading the page later upgrades the finding.
s.toolRound([read("https://example.org/only-in-search")]);
s.reply("Read it.");
await s.send("And?");
assert.equal(loadLedger(slug)!.findings[1].verification, "verified");
// Another chat on the same project never judges this chat's findings.
const s2 = new Session("2000000000004");
await s2.send(`/tl open ${slug}`);
await s2.send("Hi.");
assert.equal(loadLedger(slug)!.findings[2].verification, "not_read");
});
test("a reminder comes after two turns of tool use without a ledger update, and not every turn", async () => {
const s = new Session("2000000000005");
await s.send("/tl start Track a slow task.");
s.reply("Started.");
await s.send("Plan it.");
await s.call("ledger_plan", { action: "add", steps: ["First", "Second"], status: "doing" });
s.reply("Planned.");
await researchTurn(s, "Work.", [search("a")]); // the plan change above resets the count
await researchTurn(s, "Work more.", [search("b")]);
const third = await s.send("Keep going.");
assert.match(third.text, /Reminder: the last 2 turns used tools but the ledger was not updated/);
assert.match(third.statuses[0], /reminder sent/);
s.toolRound([search("c")]);
s.reply("Done.");
const fourth = await s.send("Next.");
assert.ok(!fourth.text.includes("Reminder:"), "not repeated on the next turn");
await s.call("ledger_record", { kind: "finding", text: "A result.", source: "user" });
s.reply("Recorded.");
const fifth = await s.send("Then?");
assert.ok(!fifth.text.includes("Reminder:"));
assert.equal(loadChatState("2000000000005")!.staleTurns, 0, "a ledger update resets the count");
});
test("the health line reports plan problems, and a finished step with nothing recorded once", async () => {
const s = new Session("2000000000006");
await s.send("/tl start Check health reporting.");
s.reply("Started.");
await s.send("Plan.");
await s.call("ledger_plan", { action: "add", steps: ["Alpha", "Beta", "Gamma"] });
s.reply("Planned.");
const noneDoing = await s.send("Go.");
assert.match(noneDoing.text, /Health: no step is marked doing\./);
await s.call("ledger_plan", { action: "update", step: 1, status: "doing" });
await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
s.reply("Working.");
const two = await s.send("Go on.");
assert.match(two.text, /steps 1 and 2 are all marked doing \(keep one\)/);
await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
s.reply("Done with one.");
const once = await s.send("Next.");
assert.match(once.text, /step 1 was marked done with nothing recorded during it/);
s.reply("Noted.");
const later = await s.send("Next again.");
assert.ok(!later.text.includes("step 1 was marked done"), "reported once");
});
test("a hand edit of ledger.md is applied, logged as the user's, and shown to the model", async () => {
const s = new Session("2000000000007");
await s.send("/tl start Edit me by hand.");
s.reply("Started.");
await s.send("Plan.");
s.toolRound([read("https://example.org/source")]);
await s.call("ledger_plan", { action: "add", steps: ["Collect", "Write"], status: "doing" });
await s.call("ledger_record", { kind: "finding", text: "Keep this one.", source: "https://example.org/source" });
await s.call("ledger_record", { kind: "finding", text: "Delete this one.", source: "user" });
await s.call("ledger_record", { kind: "question", text: "Which format?" });
s.reply("Done.");
await s.send("Status?");
const slug = loadChatState("2000000000007")!.project!;
assert.equal(loadLedger(slug)!.findings[0].verification, "verified");
const file = markdownFile(slug);
const md = fs.readFileSync(file, "utf8");
assert.deepEqual(parseMarkdown(md).plan.map((p) => p.status), ["doing", "todo"], "the rendering reads back");
const edited = md
.replace("Edit me by hand.", "Edit me by hand, carefully.")
.replace("1. [doing] Collect", "1. [done] Collect\n note: finished by hand")
.replace(/- F2: Delete this one\.\n source: user\n status: verified\n/, "- A finding I typed myself.\n source: user\n")
.replace("- Q1: Which format?", "- Q1: Which format? Markdown.");
assert.notEqual(edited, md);
fs.writeFileSync(file, edited);
s.reply("OK.");
const next = await s.send("Carry on.");
assert.ok(next.text.includes(FULL_OPEN), "a hand edit brings the full state");
assert.match(next.text, /Notice: the user edited the ledger by hand \(goal, plan, findings, questions\)/);
assert.match(next.statuses[0], /your edit applied/);
const l = loadLedger(slug)!;
assert.equal(l.goal, "Edit me by hand, carefully.");
assert.deepEqual(l.plan.map((p) => [p.status, p.note]), [["done", "finished by hand"], ["todo", ""]]);
assert.deepEqual(l.findings.map((f) => [f.id, f.text, f.verification]), [["F1", "Keep this one.", "verified"], ["F3", "A finding I typed myself.", "verified"]]);
assert.equal(l.questions[0].text, "Which format? Markdown.");
const edits = readEvents(slug, 0).filter((e) => e.op === "hand_edit");
assert.equal(edits.length, 1);
assert.equal(edits[0].actor, "user");
// An edit that cannot be read is kept aside, and the ledger is untouched.
fs.writeFileSync(file, fs.readFileSync(file, "utf8").replace("## Plan\n\n", "## Plan\n\nthis is not a step\n"));
s.reply("OK.");
const bad = await s.send("Again.");
assert.match(bad.text, /Notice: the user's edit to ledger\.md could not be read \(a plan line could not be read: "this is not a step"\)/);
assert.ok(fs.readdirSync(path.dirname(file)).some((f) => f.startsWith("ledger.edit-not-applied-")));
assert.equal(loadLedger(slug)!.plan.length, 2);
assert.ok(!fs.readFileSync(file, "utf8").includes("this is not a step"), "the file is restored from the ledger");
// Whitespace-only changes are not edits.
fs.writeFileSync(file, fs.readFileSync(file, "utf8") + "\n\n");
s.reply("OK.");
const ws = await s.send("Fine.");
assert.ok(!ws.text.includes("Notice:"));
});
test("a ledger.md from before this version is not mistaken for a hand edit", async () => {
const s = new Session("2000000000008");
await s.send("/tl start Upgrade check.");
const slug = loadChatState("2000000000008")!.project!;
const meta = loadMeta(slug);
delete meta.renderedHash;
saveMeta(slug, meta);
fs.writeFileSync(markdownFile(slug), "# old format\n\nGoal: something\n");
s.reply("OK.");
const out = await s.send("Hello.");
assert.ok(!out.text.includes("Notice:"));
assert.equal(fs.readFileSync(markdownFile(slug), "utf8"), renderMarkdown(loadLedger(slug)!));
});
test("asking about projects attaches the list, in any chat", async () => {
const s = new Session("2000000000009");
const asked = await s.send("What's the status of my projects?");
assert.match(asked.text, /\[throughLine: the user's projects, for answering this message\.\]\n/);
assert.match(asked.text, /edit-me-hand: Edit me by hand, carefully\. \| 1 of 2 steps done \| 2 findings/);
assert.match(asked.text, /upgrade-check: Upgrade check\. \| no plan yet \| 0 findings/);
assert.match(asked.text, /track-slow-task: Track a slow task\. \| now on step 1: First \| 1 finding \|/);
assert.ok(!asked.text.includes("compare-default-context"), "the removed false start is not listed");
assert.match(asked.statuses[0], /project list attached/);
assert.equal(loadChatState("2000000000009"), null, "asking does not start or bind anything");
const plain = await s.send("Tell me about Lisbon.");
assert.equal(plain.text, "Tell me about Lisbon.");
const p = new Session("2000000000010");
await p.send("/tl start A project of its own.");
p.reply("OK.");
const inProject = await p.send("How are the projects going?");
assert.ok(inProject.text.includes(FULL_OPEN) || inProject.text.includes(HEADER_OPEN));
assert.match(inProject.text, /project-its-own \(this chat\)/);
});
test("an edit saved by an editor that escapes Markdown or drops indentation still reads back", () => {
const md = [
"# x", "", "## Goal", "", "Find versions\\.", "", "## Constraints", "", "\\- Official sources only.", "",
"## Plan", "", "1\\. \\[done\\] First step", "note: finished early", "2. \\[todo] Second step", "",
"## Findings", "", "\\- F1: A fact.", "source: https://example.org/a", "status: verified", "",
"## Decisions", "", "(none)", "", "## Open questions", "", "(none)", "",
].join("\r\n");
const p = parseMarkdown("\uFEFF" + md);
assert.equal(p.goal, "Find versions.");
assert.deepEqual(p.constraints, ["Official sources only."]);
assert.deepEqual(p.plan.map((s) => [s.id, s.status, s.text, s.note]), [[1, "done", "First step", "finished early"], [2, "todo", "Second step", ""]]);
assert.deepEqual(p.findings, [{ id: "F1", text: "A fact.", source: "https://example.org/a" }]);
});
test("the state keeps the model inside the goal, and says so once the plan is done", async () => {
const s = new Session("2000000000011");
await s.send("/tl start List three things.");
s.reply("OK.");
const first = await s.send("Go.");
assert.match(first.text, /Stay within the goal: add steps beyond it only if the user asks/);
await s.call("ledger_plan", { action: "add", steps: ["Only step"], status: "doing" });
const done = await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
assert.match(done, /next: All steps are done\. Give the user the result and stop\. Add steps beyond the goal only if the user asks\./);
s.reply("Done.");
const header = await s.send("Continue.");
assert.ok(header.text.includes(HEADER_OPEN));
assert.match(header.text, /Stay within the goal/);
});
test("the full state counts hidden plan steps by position", () => {
const { renderFull } = require("../src/digest");
const l: any = { name: "x", goal: "g", goalSource: "command", constraints: [], findings: [], decisions: [], questions: [],
plan: [1, 2, 3, 4, 5, 6, 7, 8].map((id) => ({ id, text: `S${id}`, status: "done", note: "" })) };
const all = renderFull(l, 600);
assert.match(all, /\(3 earlier steps, 3 done\)\n 4\. S4 \(done\)/);
assert.ok(!all.includes("later step"));
l.plan[1].status = "doing"; l.plan.slice(2).forEach((s: any) => (s.status = "todo"));
const mid = renderFull(l, 600);
assert.match(mid, / 1\. S1 \(done\)\n 2\. S2 \(doing\)/);
assert.match(mid, / 6\. S6 \(todo\)\n \(2 later steps\)/);
});
test("a copy opened before the model's latest work keeps that work when saved", async () => {
const s = new Session("2000000000012");
await s.send("/tl start Keep concurrent work.");
s.reply("OK.");
await s.send("Go.");
s.toolRound([read("https://example.org/one"), read("https://example.org/two")]);
await s.call("ledger_record", { kind: "finding", text: "First fact.", source: "https://example.org/one" });
const slug = loadChatState("2000000000012")!.project!;
const file = markdownFile(slug);
const opened = fs.readFileSync(file, "utf8"); // the user opens the file in an editor here
await s.call("ledger_record", { kind: "finding", text: "Second fact, recorded while the file was open.", source: "https://example.org/two" });
await s.call("ledger_plan", { action: "add", steps: ["A step added meanwhile"], status: "doing" });
fs.writeFileSync(file, opened.replace("## Constraints\n\n(none)", "## Constraints\n\n- Official sources only."));
s.reply("OK.");
const next = await s.send("Continue.");
assert.match(next.text, /Notice: the user edited the ledger by hand \(constraints\)/);
const l = loadLedger(slug)!;
assert.deepEqual(l.constraints, ["Official sources only."]);
assert.deepEqual(l.findings.map((f) => f.id), ["F1", "F2"], "the model's later finding survives");
assert.deepEqual(l.plan.map((p) => p.text), ["A step added meanwhile"]);
// A deliberate deletion from an up-to-date copy is applied.
const fresh = fs.readFileSync(file, "utf8");
fs.writeFileSync(file, fresh.replace(/- F1: First fact\.\n source: \S+\n status: \S+\n/, ""));
s.reply("OK.");
await s.send("And now?");
assert.deepEqual(loadLedger(slug)!.findings.map((f) => f.id), ["F2"]);
});
test("steps added after finishing the plan in the same reply are flagged as beyond the request", async () => {
const s = new Session("2000000000031");
await s.send("/tl start Compare two tools.");
s.reply("OK.");
await s.send("Research the first tool.");
await s.call("ledger_plan", { action: "add", steps: ["First tool"], status: "doing" });
await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
const added = await s.call("ledger_plan", { action: "add", steps: ["Second tool", "Third tool"] });
assert.match(added, /next: The plan was finished earlier in this reply/);
const started = await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
assert.match(started, /next: The plan was finished earlier in this reply/);
s.reply("Here is the first tool. Next could be the second and third.");
await s.send("Now the second tool.");
const next = await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
assert.doesNotMatch(next, /finished earlier in this reply/, "a new message that asks for it starts it normally");
assert.match(next, /next: Continue with the current step/);
});
test("adding steps to an unfinished plan is not flagged", async () => {
const s = new Session("2000000000032");
await s.send("/tl start Compare tools.");
s.reply("OK.");
await s.send("Go.");
await s.call("ledger_plan", { action: "add", steps: ["One", "Two"], status: "doing" });
await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
const more = await s.call("ledger_plan", { action: "add", steps: ["Three"] });
assert.doesNotMatch(more, /finished earlier in this reply/);
});
test("starting another step in the reply that finished one asks whether the user wanted it", async () => {
const s = new Session("2000000000033");
await s.send("/tl start Compare three tools.");
s.reply("OK.");
await s.send("Research the first tool.");
await s.call("ledger_plan", { action: "add", steps: ["First", "Second", "Third"], status: "doing" });
await s.call("ledger_plan", { action: "update", step: 1, status: "done" });
const next = await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
assert.match(next, /next: A step was finished earlier in this reply\. If the user's latest message asked only for that step/);
assert.match(next, /automatic note from context management telling you to continue is not a new request/);
s.reply("First tool done.");
await s.send("Now the second.");
const asked = await s.call("ledger_plan", { action: "update", step: 2, status: "doing" });
assert.match(asked, /next: Continue with the current step/);
});
test("a page reached through a redirect counts as read at the address the tool reported", async () => {
const { readUrls, turnsOf } = require("../src/history");
const { Chat } = require("@lmstudio/sdk");
const s = new Session("2000000000034");
await s.send("Read the server page.");
s.toolRound([{ name: "read_webpage", args: { url: "https://docs.example.org/serving/openai_server.html" }, result: '[{"type":"text","text":"status: ok\\ntitle: Online Serving\\nurl: https://docs.example.org/serving/online/\\n----- page text"}]' }]);
const urls: Set<string> = readUrls(turnsOf(s.history));
assert.ok(urls.has("docs.example.org/serving/openai_server.html"));
assert.ok(urls.has("docs.example.org/serving/online"), "the redirect target counts too");
void Chat;
});