f8d07dab61
- omp-usage.js / opencode-usage.js: poll each CLI's own session store into sessions (tool omp/opencode), token_usage per model and subagent agents; transcript routes read them (omp journals, OpenCode's opencode.db). - Grok runs the Claude hooks in ~/.claude/settings.json: its hooks are now recognised (hookEventName + workspaceRoot), tagged tool=grok, and their transcript_path dropped instead of being parsed as a Claude transcript. - Catalog `resumeById`: omp/Grok/Cursor `--resume <id>`, OpenCode `--session <id>`; session ids may carry `_`. The client offers Resume for any tool that resumes by id. - Kimi Code and GitHub Copilot leave the catalog (their session stores could not be verified).
234 lines
8.5 KiB
JavaScript
234 lines
8.5 KiB
JavaScript
/**
|
|
* @file Tests server/lib/omp-usage.js and the omp branch of the transcript
|
|
* routes — Oh My Pi sessions from omp's session journals: a session created
|
|
* as `tool = 'omp'` with its title, cwd, model and per-model tokens (its
|
|
* subagents' included), a subagent row per subagent journal, a journal read on
|
|
* from where it stopped, `session_exit` settling it to completed, a session
|
|
* the user deleted staying deleted, a non-omp row with the same id never
|
|
* touched, and the journal mapped to the Claude transcript shape. The omp dir
|
|
* and the DB are temp dirs; the journals are synthetic, in the shape omp
|
|
* 18.12 writes.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const os = require("node:os");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-omp-usage-"));
|
|
process.env.DASHBOARD_DB_PATH = path.join(ROOT, "dashboard.db");
|
|
process.env.CLAUDE_HOME = path.join(ROOT, "claude");
|
|
process.env.AGENTHUB_OMP_DIR = path.join(ROOT, "omp");
|
|
|
|
const { describe, it, before, after } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { db } = require("../db");
|
|
const omp = require("../lib/omp-usage");
|
|
const { createApp, startServer } = require("../index");
|
|
|
|
const ID = "01a15366-27e2-734a-9247-04d5c65c1636";
|
|
const STAMP = "2026-10-19T09-02-46-242Z";
|
|
const CWD = path.join(fs.realpathSync(ROOT), "work");
|
|
fs.mkdirSync(CWD, { recursive: true });
|
|
const DIR = path.join(process.env.AGENTHUB_OMP_DIR, "agent", "sessions", "-work");
|
|
const MAIN = path.join(DIR, `${STAMP}_${ID}.jsonl`);
|
|
const SUB = path.join(DIR, `${STAMP}_${ID}`, "Pinger.jsonl");
|
|
|
|
let server;
|
|
let BASE;
|
|
after(() => {
|
|
server?.close();
|
|
fs.rmSync(ROOT, { recursive: true, force: true });
|
|
});
|
|
|
|
const line = (o) => JSON.stringify(o) + "\n";
|
|
const title = (t) =>
|
|
line({ type: "title", v: 1, title: t, updatedAt: "2026-10-19T09:02:46.242Z", pad: " ".repeat(40) });
|
|
const header = (id = ID) =>
|
|
line({ type: "session", version: 3, id, timestamp: "2026-10-19T09:02:46.242Z", cwd: CWD });
|
|
const user = (text, attribution = "user") =>
|
|
line({
|
|
type: "message",
|
|
id: "u1",
|
|
timestamp: "2026-10-19T09:02:46.400Z",
|
|
message: { role: "user", content: [{ type: "text", text }], attribution },
|
|
});
|
|
const assistant = (content, usage, model = "claude-sonnet-4-5") =>
|
|
line({
|
|
type: "message",
|
|
id: "a1",
|
|
timestamp: "2026-10-19T09:02:47.000Z",
|
|
message: { role: "assistant", content, provider: "anthropic", model, usage },
|
|
});
|
|
const usage = (input, output, cacheRead = 0, cacheWrite = 0) => ({
|
|
input,
|
|
output,
|
|
cacheRead,
|
|
cacheWrite,
|
|
totalTokens: input + output + cacheRead + cacheWrite,
|
|
});
|
|
const exit = line({ type: "custom", customType: "session_exit", data: { reason: "dispose" } });
|
|
|
|
const sessionRow = () => db.prepare("SELECT * FROM sessions WHERE id = ?").get(ID);
|
|
const tokens = () =>
|
|
db
|
|
.prepare(
|
|
"SELECT model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens FROM token_usage WHERE session_id = ? ORDER BY model"
|
|
)
|
|
.all(ID);
|
|
|
|
before(async () => {
|
|
fs.mkdirSync(path.dirname(SUB), { recursive: true });
|
|
fs.writeFileSync(
|
|
MAIN,
|
|
title("") +
|
|
header() +
|
|
line({ type: "model_change", model: "anthropic/claude-sonnet-4-5" }) +
|
|
user("Fix the login bug\nin auth.ts") +
|
|
assistant(
|
|
[
|
|
{ type: "thinking", thinking: "Look first" },
|
|
{ type: "toolCall", id: "t1", name: "task", arguments: { tasks: [{ task: "ping" }] } },
|
|
],
|
|
usage(100, 20, 1000, 50)
|
|
) +
|
|
line({
|
|
type: "message",
|
|
timestamp: "2026-10-19T09:02:48.000Z",
|
|
message: {
|
|
role: "toolResult",
|
|
toolCallId: "t1",
|
|
toolName: "task",
|
|
content: [{ type: "text", text: "Spawned agent `Pinger`." }],
|
|
},
|
|
})
|
|
);
|
|
fs.writeFileSync(
|
|
SUB,
|
|
header("01a15366-0000-7000-8000-000000000001") +
|
|
user("Complete assignment: ping", "agent") +
|
|
assistant([{ type: "text", text: "pong" }], usage(10, 5), "claude-haiku-4-5")
|
|
);
|
|
server = await startServer(createApp(), 0);
|
|
BASE = `http://127.0.0.1:${server.address().port}`;
|
|
});
|
|
|
|
describe("omp usage ingest", () => {
|
|
it("creates the session with its prompt, cwd, model and every journal's tokens", async () => {
|
|
const r = await omp.ingestOmpUsage();
|
|
assert.equal(r.files, 2);
|
|
const s = sessionRow();
|
|
assert.equal(s.tool, "omp");
|
|
assert.equal(s.name, "Fix the login bug");
|
|
assert.equal(s.cwd, CWD);
|
|
assert.equal(s.model, "claude-sonnet-4-5");
|
|
assert.equal(s.status, "active"); // written just now, no session_exit
|
|
assert.deepEqual(tokens(), [
|
|
{
|
|
model: "claude-haiku-4-5",
|
|
input_tokens: 10,
|
|
output_tokens: 5,
|
|
cache_read_tokens: 0,
|
|
cache_write_tokens: 0,
|
|
},
|
|
{
|
|
model: "claude-sonnet-4-5",
|
|
input_tokens: 100,
|
|
output_tokens: 20,
|
|
cache_read_tokens: 1000,
|
|
cache_write_tokens: 50,
|
|
},
|
|
]);
|
|
const agents = db.prepare("SELECT * FROM agents WHERE session_id = ?").all(ID);
|
|
assert.equal(agents.length, 1);
|
|
assert.equal(agents[0].id, `omp-${ID}-Pinger`);
|
|
assert.equal(agents[0].name, "Pinger");
|
|
assert.equal(agents[0].type, "subagent");
|
|
assert.equal(agents[0].task, "Complete assignment: ping");
|
|
});
|
|
|
|
it("leaves an unchanged journal unread", async () => {
|
|
const r = await omp.ingestOmpUsage();
|
|
assert.equal(r.files, 0);
|
|
});
|
|
|
|
it("reads only what was appended, takes the title, and settles on session_exit", async () => {
|
|
fs.appendFileSync(MAIN, assistant([{ type: "text", text: "Done" }], usage(5, 7)) + exit);
|
|
// The title slot is rewritten in place, same width.
|
|
const text = fs.readFileSync(MAIN, "utf8").split("\n");
|
|
const slot = { type: "title", v: 1, title: "Login fix", updatedAt: "x", pad: "" };
|
|
slot.pad = " ".repeat(Math.max(0, text[0].length - JSON.stringify(slot).length));
|
|
text[0] = JSON.stringify(slot);
|
|
fs.writeFileSync(MAIN, text.join("\n"));
|
|
const r = await omp.ingestOmpUsage();
|
|
assert.equal(r.files, 1);
|
|
const s = sessionRow();
|
|
assert.equal(s.name, "Login fix");
|
|
assert.equal(s.status, "completed");
|
|
assert.ok(s.ended_at);
|
|
const sonnet = tokens().find((t) => t.model === "claude-sonnet-4-5");
|
|
assert.equal(sonnet.input_tokens, 105);
|
|
assert.equal(sonnet.output_tokens, 27);
|
|
const agent = db.prepare("SELECT status FROM agents WHERE id = ?").get(`omp-${ID}-Pinger`);
|
|
assert.equal(agent.status, "completed");
|
|
});
|
|
|
|
it("maps the journal to the transcript shape, and lists the subagent's", async () => {
|
|
const list = await (await fetch(`${BASE}/api/sessions/${ID}/transcripts`)).json();
|
|
assert.deepEqual(
|
|
list.transcripts.map((t) => [t.id, t.type, t.db_agent_id]),
|
|
[
|
|
["main", "main", null],
|
|
["Pinger", "subagent", `omp-${ID}-Pinger`],
|
|
]
|
|
);
|
|
const body = await (await fetch(`${BASE}/api/sessions/${ID}/transcript`)).json();
|
|
assert.deepEqual(
|
|
body.messages.map((m) => [m.type, m.sender, m.content.map((c) => c.type).join("+")]),
|
|
[
|
|
["user", "user", "text"],
|
|
["assistant", "assistant", "thinking+tool_use"],
|
|
["user", "tool", "tool_result"],
|
|
["assistant", "assistant", "text"],
|
|
]
|
|
);
|
|
assert.deepEqual(body.messages[1].content[1], {
|
|
type: "tool_use",
|
|
name: "task",
|
|
id: "t1",
|
|
input: { tasks: [{ task: "ping" }] },
|
|
});
|
|
const sub = await (
|
|
await fetch(`${BASE}/api/sessions/${ID}/transcript?agent_id=Pinger`)
|
|
).json();
|
|
assert.deepEqual(
|
|
sub.messages.map((m) => [m.sender, m.content[0].text]),
|
|
[
|
|
["orchestrator", "Complete assignment: ping"],
|
|
["assistant", "pong"],
|
|
]
|
|
);
|
|
});
|
|
|
|
it("keeps a deleted session deleted until omp writes again", async () => {
|
|
db.prepare("DELETE FROM sessions WHERE id = ?").run(ID);
|
|
await omp.ingestOmpUsage();
|
|
assert.equal(sessionRow(), undefined);
|
|
fs.appendFileSync(MAIN, user("one more"));
|
|
await omp.ingestOmpUsage();
|
|
assert.equal(sessionRow()?.tool, "omp");
|
|
});
|
|
|
|
it("never touches another tool's session with the same id", async () => {
|
|
const other = "01a15366-27e2-734a-9247-04d5c65c9999";
|
|
db.prepare("INSERT INTO sessions (id, name, status, tool) VALUES (?, 'mine', 'completed', 'codex')").run(
|
|
other
|
|
);
|
|
fs.writeFileSync(path.join(DIR, `${STAMP}_${other}.jsonl`), title("") + header(other) + user("x"));
|
|
await omp.ingestOmpUsage();
|
|
const s = db.prepare("SELECT * FROM sessions WHERE id = ?").get(other);
|
|
assert.equal(s.tool, "codex");
|
|
assert.equal(s.name, "mine");
|
|
});
|
|
});
|