Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,28 @@ configuration, and `claude-haiku-4-5-20251001`. The check required Claude
Code's native `Skill` tool to invoke `meta-mode` and return an exact marker;
the remote fixture and temporary credentials were removed afterward.

## Recover recent context

`recall` includes a history reader in `recall/scripts/history.mjs`; it also works
when only the skill directory is installed. List session metadata for the active
workspace before selecting a session to read:

```bash
node <recall-directory>/scripts/history.mjs list --harness codex --workspace <workspace> --exclude <current-session-id>
node <recall-directory>/scripts/history.mjs read --harness codex --workspace <workspace> --session <session-id> --query parser
```

The reader supports Codex, Claude Code, OpenCode, and pi, with configured storage
roots, session exclusions, branch selection where available, and bounded text
output. OpenCode exports are sanitized by default; explicitly use `--local-text`
for private local recovery because sanitization can remove all message text.
See [history sources](./skills/recall/references/history-sources.md) for supported
formats and limits. Tests use disposable sessions; no user transcript is bundled.

The [authoring playbook](./skills/meta-mode/playbooks/authoring-a-skill.md) gives
`automate-me` and `reflect` a concrete draft, description review, and validation
workflow even when no native skill creator or repository validator is installed.

## Repository layout

- `skills/` contains the canonical, harness-neutral skill files.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"install-skills": "node scripts/install.mjs",
"optimize-context": "node scripts/optimize-context.mjs",
"reconcile-context": "node scripts/reconcile-context.mjs",
"test": "node scripts/validate.mjs && node --test scripts/install.test.mjs scripts/context.test.mjs scripts/model-config.test.mjs scripts/audit-context.test.mjs scripts/worktree-audit.test.mjs scripts/sync-upstream.test.mjs scripts/runtime.test.mjs scripts/environment.test.mjs scripts/skill-integrity.test.mjs scripts/version-integrity.test.mjs scripts/agent-format.test.mjs"
"test": "node scripts/validate.mjs && node --test scripts/install.test.mjs scripts/context.test.mjs scripts/model-config.test.mjs scripts/audit-context.test.mjs scripts/worktree-audit.test.mjs scripts/sync-upstream.test.mjs scripts/runtime.test.mjs scripts/environment.test.mjs scripts/skill-integrity.test.mjs scripts/version-integrity.test.mjs scripts/agent-format.test.mjs scripts/history.test.mjs"
},
"bin": {
"mstack": "scripts/install.mjs"
Expand Down
6 changes: 3 additions & 3 deletions profiles/upstream-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
},
"automate-me": {
"source": "c80a6024fa6bebfa6875975c74293415fa6f729087377998751dc1343c865e75",
"target": "b87d5af4db107ca6e3c2019f41875e36aeeda32b8748d8d92fbe133091529bb5"
"target": "6ab6c4c4a4b50a7cb182149408bd1758229d4386fbb8b1a66cf6927847b02c27"
},
"blast-radius": {
"source": "20cb2945f5fe62055166f745107274af5f19428ca033e53a9b8bc69650d3ded1",
Expand Down Expand Up @@ -209,11 +209,11 @@
},
"recall": {
"source": "d265b0e5b6dc5b89c0407f3387d24462c988e1990a37d5359735137908128c5a",
"target": "fbf2ae86fe5f3aeff6a6454b1a0f0899eebb5311a37ba44a3a0088fc2523ca77"
"target": "be67c9e064a2b6d2ec52e915c19f6507f9f389a112e9961eb93b3c793b565e70"
},
"reflect": {
"source": "6a4d4ccaace9ce88c3e268fcbedf3cfd40e527162a5e5aad6de8edbaf5e2b83f",
"target": "008d424f5a4570c0452852322591a50a75c4e90828f85bbb8662b5dabcc26e47"
"target": "5478382b85614482fdf8799c9b0113d0c12c6595ee1003aa62c91f6288b91089"
},
"setup-mstack": {
"source": "a11137437e12831c73f228f449bdfefab487bf7e2c69a4b306189e8fb1e6ab3e",
Expand Down
247 changes: 247 additions & 0 deletions scripts/history.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import assert from "node:assert/strict";
import { cpSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, symlinkSync, utimesSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
import { pathToFileURL } from "node:url";
import test from "node:test";
import { history } from "../skills/recall/scripts/history.mjs";

function fixture(t) {
const root = mkdtempSync(join(tmpdir(), "mstack-history-"));
t.after(() => rmSync(root, { recursive: true, force: true }));
const workspace = join(root, "project with spaces");
mkdirSync(workspace);
return { root, workspace };
}

function jsonl(path, rows) {
mkdirSync(resolve(path, ".."), { recursive: true });
writeFileSync(path, rows.map((row) => typeof row === "string" ? row : JSON.stringify(row)).join("\n") + "\n");
}

test("Codex lists metadata only, honors CODEX_HOME and excludes unrelated/current/child sessions", async (t) => {
const { root, workspace } = fixture(t);
const store = join(root, "codex");
const meta = (id, cwd = workspace, source = "cli") => ({ type: "session_meta", payload: { id, cwd, source } });
jsonl(join(store, "sessions", "wanted.jsonl"), [meta("wanted"), { type: "response_item", payload: { type: "message", role: "user", content: [{ type: "input_text", text: "Fix parser" }] } }]);
jsonl(join(store, "archived_sessions", "other.jsonl"), [meta("other", join(root, "unrelated")), "PRIVATE INVALID BODY"]);
jsonl(join(store, "sessions", "current.jsonl"), [meta("current")]);
jsonl(join(store, "sessions", "child.jsonl"), [meta("child", workspace, { subagent: { thread_spawn: {} } })]);
const options = { harness: "codex", workspace, env: { CODEX_HOME: store, CODEX_THREAD_ID: "current" } };
const listing = await history({ ...options, command: "list" });
assert.deepEqual(listing.sessions.map((session) => session.id), ["wanted"]);
assert.doesNotMatch(JSON.stringify(listing), /Fix parser|PRIVATE/);
assert.deepEqual(listing.warnings, []);
const reading = await history({ ...options, command: "read", session: "wanted" });
assert.deepEqual(reading.messages.map(({ role, text }) => ({ role, text })), [{ role: "user", text: "Fix parser" }]);
await assert.rejects(history({ ...options, command: "read", session: "other" }), /not found in the requested workspace/);
});

test("Claude scopes its project directory and follows the selected conversation branch", async (t) => {
const { root, workspace } = fixture(t);
const store = join(root, "claude");
const slug = workspace.replace(/[^a-zA-Z0-9]/g, "-");
const row = (uuid, parentUuid, text) => ({ type: "user", sessionId: "claude-session", cwd: workspace, uuid, parentUuid, message: { role: "user", content: text } });
jsonl(join(store, "projects", slug, "claude-session.jsonl"), [
{ type: "file-history-snapshot" }, row("a", null, "Start"), row("discarded", "a", "Old approach"), row("chosen", "a", "New approach"),
]);
jsonl(join(store, "projects", "unrelated", "private.jsonl"), ["PRIVATE INVALID RECORD"]);
const options = { harness: "claude", workspace, env: { CLAUDE_CONFIG_DIR: store }, command: "read", session: "claude-session" };
const reading = await history(options);
assert.deepEqual(reading.messages.map((message) => message.text), ["Start", "New approach"]);
assert.deepEqual((await history({ ...options, leaf: "discarded" })).messages.map((message) => message.text), ["Start", "Old approach"]);
});

test("history discovery releases file handles before callers move and delete the store", async (t) => {
const { root, workspace } = fixture(t);
const headers = [
{ type: "session_meta", payload: { id: "matching", cwd: workspace } },
{ type: "session_meta", payload: { id: "unrelated", cwd: join(root, "other") } },
"MALFORMED HEADER",
];
for (let index = 0; index < headers.length; index++) {
const store = join(root, `store-${index}`);
const moved = join(root, `moved-${index}`);
jsonl(join(store, "archived_sessions", "session.jsonl"), [headers[index], "Unneeded body".repeat(1000)]);
const result = await history({ harness: "codex", workspace, root: store, env: {}, command: "list" });
assert.equal(result.sessions.length, index === 0 ? 1 : 0);
renameSync(store, moved);
rmSync(moved, { recursive: true, force: true });
assert.equal(existsSync(moved), false);
}
});

test("discovery sorts by modification time, deduplicates newest records and honors the time window", async (t) => {
const { root, workspace } = fixture(t);
const meta = { type: "session_meta", payload: { id: "same", cwd: workspace } };
const old = join(root, "archived_sessions", "same.jsonl");
const newest = join(root, "sessions", "same.jsonl");
const message = (text) => ({ type: "event_msg", payload: { type: "agent_message", message: text } });
jsonl(old, [meta, message("Archived response")]);
jsonl(newest, [meta, message("Current response")]);
utimesSync(old, new Date("2026-01-01"), new Date("2026-01-01"));
const options = { harness: "codex", workspace, root, env: {}, since: "2025-01-01" };
assert.equal((await history({ ...options, command: "list" })).sessions.length, 1);
assert.equal((await history({ ...options, command: "read", session: "same" })).messages[0].text, "Current response");
assert.deepEqual((await history({ ...options, command: "list", since: "2100-01-01" })).sessions, []);
});

test("pi reads version 3 parent trees, excludes tool results and bounds matching output", async (t) => {
const { root, workspace } = fixture(t);
const store = join(root, "pi");
const message = (id, parentId, role, text) => ({ type: "message", id, parentId, message: { role, content: [{ type: "text", text }] } });
jsonl(join(store, "branch.jsonl"), [
{ type: "session", version: 3, id: "pi-session", cwd: workspace },
message("a", null, "user", "Parser question"), message("b", "a", "assistant", "Discarded answer"),
message("c", "a", "assistant", "Parser fix"), message("d", "c", "toolResult", "PRIVATE TOOL OUTPUT"),
]);
const options = { harness: "pi", workspace, env: { PI_CODING_AGENT_SESSION_DIR: store }, command: "read", session: "pi-session" };
assert.deepEqual((await history(options)).messages.map((message) => message.text), ["Parser question", "Parser fix"]);
const bounded = await history({ ...options, query: "parser", limit: 1, maxChars: 6 });
assert.deepEqual(bounded.messages.map((message) => message.text), ["Parser"]);
assert.equal(bounded.truncated, true);
const newest = await history({ ...options, maxChars: 12 });
assert.deepEqual(newest.messages.map((message) => message.text), ["Pa", "Parser fix"]);
assert.equal(newest.truncated, true);
await assert.rejects(history({ ...options, leaf: "missing" }), /Unknown leaf/);
});

test("pi default storage uses only the requested workspace slug and keeps branch summaries", async (t) => {
const { root, workspace } = fixture(t);
const slug = `--${workspace.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
jsonl(join(root, "sessions", slug, "session.jsonl"), [
{ type: "session", version: 3, id: "summary", cwd: workspace },
{ type: "branch_summary", id: "s", parentId: null, summary: "The first attempt was reverted." },
]);
jsonl(join(root, "sessions", "unrelated", "private.jsonl"), ["PRIVATE INVALID HEADER"]);
const result = await history({ harness: "pi", workspace, env: { PI_CODING_AGENT_DIR: root }, command: "read", session: "summary" });
assert.deepEqual(result.messages, [{ role: "summary", text: "The first attempt was reverted." }]);
assert.deepEqual(result.warnings, []);
});

test("OpenCode filters list metadata before exporting one sanitized session", async (t) => {
const { root, workspace } = fixture(t);
const calls = [];
const runOpenCode = (args, cwd) => {
calls.push({ args, cwd });
if (args[0] === "session") return [
{ id: "other", directory: join(root, "other"), updated: Date.now() },
{ id: "wanted", directory: workspace, updated: Date.now() },
{ id: "child", directory: workspace, parentID: "wanted", updated: Date.now() },
];
return { info: { id: "wanted", directory: "[redacted:session-directory:wanted]" }, messages: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Sanitized answer" }, { type: "tool", text: "Do not emit" }] }] };
};
const options = { harness: "opencode", workspace, runOpenCode, env: {} };
const listing = await history({ ...options, command: "list" });
assert.deepEqual(listing.sessions.map((session) => session.id), ["wanted"]);
assert.equal(calls.length, 1);
const reading = await history({ ...options, command: "read", session: "wanted" });
assert.deepEqual(calls.at(-1), { args: ["export", "wanted", "--sanitize", "--pure"], cwd: workspace });
assert.equal(reading.messages[0].text, "Sanitized answer");
await assert.rejects(history({ ...options, command: "read", session: "other" }), /not found/);
assert.equal(calls.filter((call) => call.args[0] === "export").length, 1);
for (const directory of [join(root, "other"), "[redacted:session-directory:other]"]) {
const wrongExport = (args, cwd) => args[0] === "session" ? runOpenCode(args, cwd)
: { info: { id: "wanted", directory }, messages: [] };
await assert.rejects(history({ ...options, runOpenCode: wrongExport, command: "read", session: "wanted" }), /does not match/);
}
});

test("missing stores and malformed records produce controlled diagnostics without raw content", async (t) => {
const { root, workspace } = fixture(t);
const options = { harness: "codex", workspace, root: join(root, "missing"), command: "list", env: {} };
assert.deepEqual((await history(options)).sessions, []);
jsonl(join(options.root, "sessions", "broken.jsonl"), ["PRIVATE INVALID HEADER"]);
jsonl(join(options.root, "sessions", "also-broken.jsonl"), ["ANOTHER PRIVATE INVALID HEADER"]);
const broken = await history(options);
assert.equal(broken.warnings.length, 1);
assert.doesNotMatch(JSON.stringify(broken), /PRIVATE/);
jsonl(join(options.root, "sessions", "good.jsonl"), [{ type: "session_meta", payload: { id: "good", cwd: workspace } }, "PRIVATE INVALID BODY"]);
const reading = await history({ ...options, command: "read", session: "good" });
assert.deepEqual(reading.messages, []);
assert.equal(reading.warnings.length, 2, "keep distinct diagnostics but emit each only once");
assert.ok(reading.warnings.some((warning) => /malformed record/.test(warning)));
assert.doesNotMatch(JSON.stringify(reading), /PRIVATE/);
});

test("OpenCode warns when sanitizer removes text and requires an explicit local-text read to recover it", async (t) => {
const { workspace } = fixture(t);
const runOpenCode = (args) => args[0] === "session" ? [{ id: "session", directory: workspace, updated: Date.now() }]
: { info: { id: "session", directory: args.includes("--sanitize") ? "[redacted:session-directory:session]" : workspace },
messages: [{ info: { role: "user" }, parts: [{ type: "text", text: args.includes("--sanitize") ? "[redacted:text:part]" : "Local fixture prompt" }] }] };
const options = { harness: "opencode", workspace, runOpenCode, command: "read", session: "session" };
const sanitized = await history(options);
assert.equal(sanitized.sanitized, true);
assert.match(sanitized.warnings[0], /redacted message text/);
const local = await history({ ...options, localText: true });
assert.equal(local.sanitized, false);
assert.equal(local.messages[0].text, "Local fixture prompt");
assert.deepEqual(local.warnings, []);
await assert.rejects(history({ ...options, command: "list", localText: true }), /only for OpenCode read/);
await assert.rejects(history({ ...options, harness: "codex", localText: true }), /only for OpenCode read/);
});

test("the copied recall skill runs without repository scripts and validates CLI arguments", (t) => {
const { root, workspace } = fixture(t);
const installed = join(root, "installed-recall");
cpSync(resolve("skills/recall"), installed, { recursive: true });
const script = join(installed, "scripts", "history.mjs");
const args = [script, "list", "--harness", "codex", "--workspace", workspace, "--root", join(root, "empty")];
const result = spawnSync(process.execPath, args, { encoding: "utf8", cwd: root });
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout).sessions, []);
for (const suffix of [["--root"], ["--limit", "0"], ["--since", "invalid"], ["--unknown", "value"], ["--local-text"]]) {
const failed = spawnSync(process.execPath, [...args, ...suffix], { encoding: "utf8", cwd: root });
assert.notEqual(failed.status, 0);
assert.doesNotMatch(failed.stderr, /at file:/);
}
});

test("the recall CLI runs through a linked directory while imports stay silent", (t) => {
const { root, workspace } = fixture(t);
const installed = join(root, "installed-recall");
const linked = join(root, "linked-recall");
cpSync(resolve("skills/recall"), installed, { recursive: true });
symlinkSync(installed, linked, process.platform === "win32" ? "junction" : "dir");
const script = join(linked, "scripts", "history.mjs");
const args = [script, "list", "--harness", "codex", "--workspace", workspace, "--root", join(root, "empty")];
for (const flags of [[], ["--preserve-symlinks-main"]]) {
const result = spawnSync(process.execPath, [...flags, ...args], { encoding: "utf8", cwd: root });
assert.equal(result.status, 0, result.stderr);
assert.notEqual(result.stdout.trim(), "", "the CLI must run when its entry path has a symlink alias");
assert.deepEqual(JSON.parse(result.stdout).sessions, []);
}
const importing = spawnSync(process.execPath, ["--input-type=module", "--eval", `await import(${JSON.stringify(pathToFileURL(script).href)})`], { encoding: "utf8", cwd: root });
assert.equal(importing.status, 0, importing.stderr);
assert.equal(importing.stdout, "");
assert.equal(importing.stderr, "");
const stdinImport = spawnSync(process.execPath, ["--input-type=module", "-"], {
input: `await import(${JSON.stringify(pathToFileURL(script).href)})`, encoding: "utf8", cwd: root,
});
assert.equal(stdinImport.status, 0, stdinImport.stderr);
assert.equal(stdinImport.stdout, "");
assert.equal(stdinImport.stderr, "");
});

test("the recall CLI rejects duplicate single-value options and switches but permits repeated exclusions", (t) => {
const { root, workspace } = fixture(t);
const store = join(root, "codex");
for (const id of ["first", "second"]) {
jsonl(join(store, "sessions", `${id}.jsonl`), [{ type: "session_meta", payload: { id, cwd: workspace } }]);
}
const args = [resolve("skills/recall/scripts/history.mjs"), "list", "--harness", "codex", "--workspace", workspace, "--root", store];
for (const [flag, suffix] of [
["--workspace", ["--workspace", join(root, "other-workspace")]],
["--limit", ["--limit", "1", "--limit", "2"]],
["--local-text", ["--local-text", "--local-text"]],
]) {
const result = spawnSync(process.execPath, [...args, ...suffix], { encoding: "utf8", cwd: root });
assert.notEqual(result.status, 0);
assert.equal(result.stdout, "");
assert.match(result.stderr, new RegExp(`Duplicate option: ${flag}`));
}
const excluded = spawnSync(process.execPath, [...args, "--exclude", "first", "--exclude", "second"], { encoding: "utf8", cwd: root });
assert.equal(excluded.status, 0, excluded.stderr);
assert.deepEqual(JSON.parse(excluded.stdout).sessions, []);
});
Loading