feat(ai-tools): omp shared + OpenCode/omp sessions, tokens, subagents; Grok hooks; resume by id

- 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).
This commit is contained in:
2026-10-19 16:49:59 +07:00
parent 0434ffc750
commit f8d07dab61
33 changed files with 2007 additions and 103 deletions
+233
View File
@@ -0,0 +1,233 @@
/**
* @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");
});
});