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
15 changes: 14 additions & 1 deletion src/core/event-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ export interface HookEvent {
* Optional — absent when no workflow is active.
*/
workflow_id?: string;
/**
* Loom spine link — id of the Loom task this event belongs to, when the
* session was launched by Loom (LOOM_TASK_ID set). Lets Loom attribute token
* savings to a task exactly. Optional — absent outside Loom, so standalone
* token-pilot events stay byte-identical to before.
*/
task_id?: string;
event:
| "denied"
| "allowed"
Expand Down Expand Up @@ -235,8 +242,14 @@ export async function appendEvent(
event.workflow_id ??
process.env.TOKEN_PILOT_WORKFLOW_ID ??
process.env.CLAUDE_CODE_WORKFLOW_ID ??
process.env.LOOM_WORKFLOW_ID ??
undefined;
const tagged = wf ? { ...event, workflow_id: wf } : event;
// Loom spine: tag the task id when the session was launched by Loom.
// Env-driven so call sites stay unchanged; absent outside Loom → no field.
const taskId = event.task_id ?? process.env.LOOM_TASK_ID ?? undefined;
let tagged = event;
if (wf) tagged = { ...tagged, workflow_id: wf };
if (taskId) tagged = { ...tagged, task_id: taskId };
await ensureLogDir(projectRoot);
await rotateIfNeeded(projectRoot);
const line = JSON.stringify(tagged) + "\n";
Expand Down
53 changes: 53 additions & 0 deletions tests/core/event-log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,59 @@ describe("appendEvent / loadEvents", () => {
});
});

// ─── appendEvent — Loom spine tagging (task_id / workflow_id from env) ─────────

describe("appendEvent — Loom spine tagging", () => {
const base: HookEvent = {
ts: 1_700_000_000_000,
session_id: "s1",
agent_type: null,
agent_id: null,
event: "denied",
file: "src/big.ts",
lines: 500,
estTokens: 2000,
summaryTokens: 400,
savedTokens: 1600,
};

it("tags task_id from LOOM_TASK_ID when set", async () => {
const project = await makeTmp();
tmpDirs.push(project);
process.env.LOOM_TASK_ID = "tj-xyz";
try {
await appendEvent(project, { ...base });
} finally {
delete process.env.LOOM_TASK_ID;
}
const events = await loadEvents(project);
expect(events[0].task_id).toBe("tj-xyz");
});

it("tags workflow_id from LOOM_WORKFLOW_ID when set", async () => {
const project = await makeTmp();
tmpDirs.push(project);
process.env.LOOM_WORKFLOW_ID = "wf-1";
try {
await appendEvent(project, { ...base });
} finally {
delete process.env.LOOM_WORKFLOW_ID;
}
const events = await loadEvents(project);
expect(events[0].workflow_id).toBe("wf-1");
});

it("adds no task_id field when LOOM_TASK_ID is unset (back-compat)", async () => {
const project = await makeTmp();
tmpDirs.push(project);
delete process.env.LOOM_TASK_ID;
await appendEvent(project, { ...base });
const events = await loadEvents(project);
expect(events[0].task_id).toBeUndefined();
expect("task_id" in events[0]).toBe(false);
});
});

// ─── applyRetention ──────────────────────────────────────────────────────────

describe("applyRetention", () => {
Expand Down