@juli-os/ledger 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@ The L1 layer of the juli open-source ladder: **the append-only ledger of record
8
8
  |---|---|---|
9
9
  | Workflow lifecycle | `createLedgerStore(path)` → `createWorkflow / getWorkflow / listWorkflows / setWorkflowStatus / createStep / insertStepsAt …` | Durable jobs + steps + statuses in SQLite, with dynamic mid-flight step insertion (single transaction, tail renumbered) |
10
10
  | Conversation threading | `latestByMessageId(msgId)` / `recentBySender(from, since)` | Reply-chain matching for email bots and support inboxes — message-id causal anchor + sender/time-window candidate narrowing |
11
- | Case files | `emptyCaseFile() / loadCaseFile / saveCaseFile` | A per-job audit dossier: original → extract → executions → draft → decisions → artifacts |
11
+ | Case files | `emptyCaseFile(now) / loadCaseFile / saveCaseFile` | A per-job audit dossier: original → extract → executions → draft → decisions → artifacts |
12
12
  | Usage & cost | `createUsageStore(path)` → `record / stats / timeline / byLocalDay`, `priceUsage()` | Token accounting per model / day / session with built-in pricing |
13
13
 
14
14
  ## Peel boundary
package/dist/index.js CHANGED
@@ -110,7 +110,7 @@ var parseCaseFile = (raw, now) => {
110
110
  decisions: pick(arr("decisions"), (d) => "action" in d),
111
111
  references: arr("references").filter((x) => typeof x === "string"),
112
112
  artifacts: pick(arr("artifacts"), (a) => "id" in a),
113
- updatedAt: str2("updated_at") || base.updatedAt
113
+ updatedAt: str2("updatedAt") || str2("updated_at") || base.updatedAt
114
114
  };
115
115
  };
116
116
  var serializeCaseFile = (cf) => JSON.stringify(cf);
@@ -240,7 +240,7 @@ var rowToWorkflow = (r) => ({
240
240
  session: str(r["session"]),
241
241
  project: str(r["project"]),
242
242
  path: str(r["path"]),
243
- threadId: r["thread_id"] === null || r["thread_id"] === void 0 ? null : str(r["thread_id"]),
243
+ threadId: r["thread_id"] === null ? null : str(r["thread_id"]),
244
244
  meta: asRecord(safeParse(str(r["meta"]))),
245
245
  createdAt: str(r["created_at"]),
246
246
  updatedAt: str(r["updated_at"])
@@ -280,6 +280,8 @@ var pruneImpl = (db, retentionDays, nowIso) => {
280
280
  try {
281
281
  db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
282
282
  db.prepare("DELETE FROM relays WHERE workflow_id=?").run(r.id);
283
+ db.prepare("DELETE FROM prompts WHERE run_id IN (SELECT id FROM runs WHERE workflow_id=?)").run(r.id);
284
+ db.prepare("DELETE FROM runs WHERE workflow_id=?").run(r.id);
283
285
  db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
284
286
  db.exec("COMMIT");
285
287
  } catch (e) {
@@ -327,7 +329,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
327
329
  const activeRunOf = (session) => {
328
330
  const r = db.prepare(
329
331
  `SELECT r.id AS id, r.workflow_id AS workflow_id, r.session AS session,
330
- r.status AS status, r.created_at AS created_at FROM runs r
332
+ r.created_at AS created_at FROM runs r
331
333
  JOIN workflows w ON w.id = r.workflow_id
332
334
  WHERE r.session=? AND r.status IN ('running','idle') AND w.status='running'
333
335
  ORDER BY r.created_at DESC, r.rowid DESC LIMIT 1`
@@ -551,8 +553,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
551
553
  sessionHasOpenWork(session, excludeWorkflowId) {
552
554
  const anchor = "(session=?1 OR COALESCE(json_extract(meta,'$.assigned_session'),'')=?1 OR id IN (SELECT workflow_id FROM runs WHERE session=?1))";
553
555
  const openFilter = `status IN (${sqlInList(ACTIVE_WORKFLOW_STATUSES)})`;
554
- const sql = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? `SELECT COUNT(*) AS n FROM workflows WHERE ${openFilter} AND ${anchor} AND id != ?2` : `SELECT COUNT(*) AS n FROM workflows WHERE ${openFilter} AND ${anchor}`;
555
- const row = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
556
+ const exclude = excludeWorkflowId !== void 0 && excludeWorkflowId !== "";
557
+ const sql = `SELECT COUNT(*) AS n FROM workflows WHERE ${openFilter} AND ${anchor}${exclude ? " AND id != ?2" : ""}`;
558
+ const row = exclude ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
556
559
  return Number(row["n"]) > 0;
557
560
  },
558
561
  appendEvent(entityType, entityId, type, payload = {}) {
@@ -593,14 +596,15 @@ var createLedgerStore = (dbPath, deps = {}) => {
593
596
  },
594
597
  createRun(workflowId, session) {
595
598
  const id = ids.newId("run");
599
+ const ts = now();
596
600
  db.prepare(`INSERT INTO runs (id, workflow_id, session, status, created_at)
597
- VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, now());
601
+ VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, ts);
598
602
  return {
599
603
  id,
600
604
  workflowId,
601
605
  session,
602
606
  status: "running",
603
- createdAt: now()
607
+ createdAt: ts
604
608
  };
605
609
  },
606
610
  activeRunForSession: (session) => activeRunOf(session),
@@ -623,17 +627,20 @@ var createLedgerStore = (dbPath, deps = {}) => {
623
627
  ).get(dedupPattern(key));
624
628
  return r !== void 0;
625
629
  },
626
- /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
627
- * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
628
- * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
629
- * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
630
- * 成对转义(message-id 几乎必含 `_`)。 */
630
+ /** 全量出站邮件登记(2026-09-20 幽灵单缺口):send 步之外,通知邮件
631
+ * (emailNotifier)等所有出站信的 message_id 也落 email_sent 事件——
632
+ * 发给轮询邮箱自身的告警信不再被当新来信建单([juli] 待审批 幽灵单)。 */
631
633
  recordSentMessageId(messageId, meta) {
632
634
  if (messageId === "") return;
633
635
  db.prepare(
634
636
  `INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES ('email', ?, 'email_sent', ?, ?)`
635
637
  ).run(messageId, JSON.stringify(meta ?? {}), now());
636
638
  },
639
+ /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
640
+ * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
641
+ * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
642
+ * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
643
+ * 成对转义(message-id 几乎必含 `_`)。 */
637
644
  sentMessageIdExists(messageId) {
638
645
  if (messageId === "") return false;
639
646
  const registered = db.prepare(
@@ -652,9 +659,10 @@ var createLedgerStore = (dbPath, deps = {}) => {
652
659
  },
653
660
  createPrompt(runId, content, source) {
654
661
  const id = ids.newId("pr");
662
+ const ts = now();
655
663
  db.prepare(`INSERT INTO prompts (id, run_id, content, source, status, created_at)
656
- VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source, now());
657
- return { id, runId, content, status: "submitted", createdAt: now() };
664
+ VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source, ts);
665
+ return { id, runId, content, status: "submitted", createdAt: ts };
658
666
  },
659
667
  prune(retentionDays) {
660
668
  return pruneImpl(db, retentionDays, now());
@@ -698,9 +706,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
698
706
  if (arr === void 0) members.set(root, [w.id]);
699
707
  else arr.push(w.id);
700
708
  }
701
- const views = [...members.entries()].map(([taskId, ids2]) => {
709
+ const views = [...members.entries()].map(([taskId, memberIds]) => {
702
710
  const card = byId.get(taskId);
703
- return { taskId, title: card.title, status: card.status, workflowIds: ids2 };
711
+ return { taskId, title: card.title, status: card.status, workflowIds: memberIds };
704
712
  }).sort((a, b) => {
705
713
  const ca = byId.get(a.taskId)?.createdAt ?? "";
706
714
  const cb = byId.get(b.taskId)?.createdAt ?? "";
@@ -1043,9 +1051,9 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1043
1051
  outputTokens: n(r["o"])
1044
1052
  }));
1045
1053
  return {
1046
- totalCalls: n(total?.["c"]),
1047
- totalInputTokens: n(total?.["i"]),
1048
- totalOutputTokens: n(total?.["o"]),
1054
+ totalCalls: n(total["c"]),
1055
+ totalInputTokens: n(total["i"]),
1056
+ totalOutputTokens: n(total["o"]),
1049
1057
  byModel
1050
1058
  };
1051
1059
  },
@@ -1088,7 +1096,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1088
1096
  return windowUsageRows(db, tmuxSession, windows);
1089
1097
  },
1090
1098
  sessionWindowsPriced(sessions, windows) {
1091
- if (sessions.length === 0 || windows.length === 0) return priceUsage([]);
1092
1099
  return priceUsage(sessions.flatMap((s) => windowUsageRows(db, s, windows)));
1093
1100
  },
1094
1101
  byLocalDay(days = 7) {
@@ -1122,10 +1129,17 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1122
1129
  return mapped > 0 && total > 0 && filled * 2 < total;
1123
1130
  },
1124
1131
  resetClaudeIngest() {
1125
- db.exec("DELETE FROM prompt_usage WHERE source='claude_code'");
1126
- db.exec("DELETE FROM claude_ingest_offset");
1127
- db.prepare(`INSERT INTO usage_meta (key, value) VALUES ('last_claude_replay_at', ?)
1128
- ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(now().toISOString());
1132
+ db.exec("BEGIN");
1133
+ try {
1134
+ db.exec("DELETE FROM prompt_usage WHERE source='claude_code'");
1135
+ db.exec("DELETE FROM claude_ingest_offset");
1136
+ db.prepare(`INSERT INTO usage_meta (key, value) VALUES ('last_claude_replay_at', ?)
1137
+ ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(now().toISOString());
1138
+ db.exec("COMMIT");
1139
+ } catch (e) {
1140
+ db.exec("ROLLBACK");
1141
+ throw e;
1142
+ }
1129
1143
  },
1130
1144
  recent(limit = 100) {
1131
1145
  return db.prepare("SELECT * FROM prompt_usage ORDER BY id DESC LIMIT ?").all(limit).map((r) => ({
@@ -1171,7 +1185,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1171
1185
  }
1172
1186
  for (const f of files) {
1173
1187
  const stem = f.replace(/\.jsonl$/, "");
1174
- count += ingestFile(db, stem, join(dir, f));
1188
+ count += ingestFile(db, stem, join(dir, f), now);
1175
1189
  }
1176
1190
  }
1177
1191
  } catch {
@@ -1185,7 +1199,7 @@ var tmuxOf = (db, claudeId) => {
1185
1199
  const r = db.prepare("SELECT tmux_session FROM claude_sessions WHERE claude_session_id=?").get(claudeId);
1186
1200
  return r?.["tmux_session"] ?? "";
1187
1201
  };
1188
- var ingestFile = (db, claudeId, transcriptPath) => {
1202
+ var ingestFile = (db, claudeId, transcriptPath, now) => {
1189
1203
  let fd;
1190
1204
  try {
1191
1205
  fd = openSync(transcriptPath, "r");
@@ -1215,7 +1229,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1215
1229
  const sess = tmuxOf(db, claudeId);
1216
1230
  for (const line of complete.split("\n")) {
1217
1231
  if (line === "") continue;
1218
- const rec = parseAssistantLine(line);
1232
+ const rec = parseAssistantLine(line, now);
1219
1233
  if (!rec) continue;
1220
1234
  db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
1221
1235
  VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
@@ -1231,7 +1245,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1231
1245
  }
1232
1246
  db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
1233
1247
  VALUES (?, ?, ?)
1234
- ON CONFLICT(claude_session_id) DO UPDATE SET byte_offset=excluded.byte_offset, last_ingested_at=excluded.last_ingested_at`).run(claudeId, newOffset, (/* @__PURE__ */ new Date()).toISOString());
1248
+ ON CONFLICT(claude_session_id) DO UPDATE SET byte_offset=excluded.byte_offset, last_ingested_at=excluded.last_ingested_at`).run(claudeId, newOffset, now().toISOString());
1235
1249
  db.exec("COMMIT");
1236
1250
  } catch (e) {
1237
1251
  db.exec("ROLLBACK");
@@ -1242,7 +1256,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1242
1256
  closeSync(fd);
1243
1257
  }
1244
1258
  };
1245
- var parseAssistantLine = (line) => {
1259
+ var parseAssistantLine = (line, now) => {
1246
1260
  try {
1247
1261
  const v = JSON.parse(line);
1248
1262
  if (v.type !== "assistant") return null;
@@ -1257,7 +1271,7 @@ var parseAssistantLine = (line) => {
1257
1271
  inputTokens: input,
1258
1272
  outputTokens: output,
1259
1273
  cacheRead,
1260
- timestamp: typeof v.timestamp === "string" ? v.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1274
+ timestamp: typeof v.timestamp === "string" ? v.timestamp : now().toISOString(),
1261
1275
  callContext: v.uuid ?? ""
1262
1276
  };
1263
1277
  } catch {
@@ -105,7 +105,7 @@ var parseCaseFile = (raw, now) => {
105
105
  decisions: pick(arr("decisions"), (d) => "action" in d),
106
106
  references: arr("references").filter((x) => typeof x === "string"),
107
107
  artifacts: pick(arr("artifacts"), (a) => "id" in a),
108
- updatedAt: str2("updated_at") || base.updatedAt
108
+ updatedAt: str2("updatedAt") || str2("updated_at") || base.updatedAt
109
109
  };
110
110
  };
111
111
  var serializeCaseFile = (cf) => JSON.stringify(cf);
@@ -198,7 +198,7 @@ var rowToWorkflow = (r) => ({
198
198
  session: str(r["session"]),
199
199
  project: str(r["project"]),
200
200
  path: str(r["path"]),
201
- threadId: r["thread_id"] === null || r["thread_id"] === void 0 ? null : str(r["thread_id"]),
201
+ threadId: r["thread_id"] === null ? null : str(r["thread_id"]),
202
202
  meta: asRecord(safeParse(str(r["meta"]))),
203
203
  createdAt: str(r["created_at"]),
204
204
  updatedAt: str(r["updated_at"])
@@ -238,6 +238,8 @@ var pruneImpl = (db, retentionDays, nowIso) => {
238
238
  try {
239
239
  db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
240
240
  db.prepare("DELETE FROM relays WHERE workflow_id=?").run(r.id);
241
+ db.prepare("DELETE FROM prompts WHERE run_id IN (SELECT id FROM runs WHERE workflow_id=?)").run(r.id);
242
+ db.prepare("DELETE FROM runs WHERE workflow_id=?").run(r.id);
241
243
  db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
242
244
  db.exec("COMMIT");
243
245
  } catch (e) {
@@ -285,7 +287,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
285
287
  const activeRunOf = (session) => {
286
288
  const r = db.prepare(
287
289
  `SELECT r.id AS id, r.workflow_id AS workflow_id, r.session AS session,
288
- r.status AS status, r.created_at AS created_at FROM runs r
290
+ r.created_at AS created_at FROM runs r
289
291
  JOIN workflows w ON w.id = r.workflow_id
290
292
  WHERE r.session=? AND r.status IN ('running','idle') AND w.status='running'
291
293
  ORDER BY r.created_at DESC, r.rowid DESC LIMIT 1`
@@ -509,8 +511,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
509
511
  sessionHasOpenWork(session, excludeWorkflowId) {
510
512
  const anchor = "(session=?1 OR COALESCE(json_extract(meta,'$.assigned_session'),'')=?1 OR id IN (SELECT workflow_id FROM runs WHERE session=?1))";
511
513
  const openFilter = `status IN (${sqlInList(ACTIVE_WORKFLOW_STATUSES)})`;
512
- const sql = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? `SELECT COUNT(*) AS n FROM workflows WHERE ${openFilter} AND ${anchor} AND id != ?2` : `SELECT COUNT(*) AS n FROM workflows WHERE ${openFilter} AND ${anchor}`;
513
- const row = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
514
+ const exclude = excludeWorkflowId !== void 0 && excludeWorkflowId !== "";
515
+ const sql = `SELECT COUNT(*) AS n FROM workflows WHERE ${openFilter} AND ${anchor}${exclude ? " AND id != ?2" : ""}`;
516
+ const row = exclude ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
514
517
  return Number(row["n"]) > 0;
515
518
  },
516
519
  appendEvent(entityType, entityId, type, payload = {}) {
@@ -551,14 +554,15 @@ var createLedgerStore = (dbPath, deps = {}) => {
551
554
  },
552
555
  createRun(workflowId, session) {
553
556
  const id = ids.newId("run");
557
+ const ts = now();
554
558
  db.prepare(`INSERT INTO runs (id, workflow_id, session, status, created_at)
555
- VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, now());
559
+ VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, ts);
556
560
  return {
557
561
  id,
558
562
  workflowId,
559
563
  session,
560
564
  status: "running",
561
- createdAt: now()
565
+ createdAt: ts
562
566
  };
563
567
  },
564
568
  activeRunForSession: (session) => activeRunOf(session),
@@ -581,17 +585,20 @@ var createLedgerStore = (dbPath, deps = {}) => {
581
585
  ).get(dedupPattern(key));
582
586
  return r !== void 0;
583
587
  },
584
- /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
585
- * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
586
- * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
587
- * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
588
- * 成对转义(message-id 几乎必含 `_`)。 */
588
+ /** 全量出站邮件登记(2026-09-20 幽灵单缺口):send 步之外,通知邮件
589
+ * (emailNotifier)等所有出站信的 message_id 也落 email_sent 事件——
590
+ * 发给轮询邮箱自身的告警信不再被当新来信建单([juli] 待审批 幽灵单)。 */
589
591
  recordSentMessageId(messageId, meta) {
590
592
  if (messageId === "") return;
591
593
  db.prepare(
592
594
  `INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES ('email', ?, 'email_sent', ?, ?)`
593
595
  ).run(messageId, JSON.stringify(meta ?? {}), now());
594
596
  },
597
+ /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
598
+ * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
599
+ * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
600
+ * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
601
+ * 成对转义(message-id 几乎必含 `_`)。 */
595
602
  sentMessageIdExists(messageId) {
596
603
  if (messageId === "") return false;
597
604
  const registered = db.prepare(
@@ -610,9 +617,10 @@ var createLedgerStore = (dbPath, deps = {}) => {
610
617
  },
611
618
  createPrompt(runId, content, source) {
612
619
  const id = ids.newId("pr");
620
+ const ts = now();
613
621
  db.prepare(`INSERT INTO prompts (id, run_id, content, source, status, created_at)
614
- VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source, now());
615
- return { id, runId, content, status: "submitted", createdAt: now() };
622
+ VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source, ts);
623
+ return { id, runId, content, status: "submitted", createdAt: ts };
616
624
  },
617
625
  prune(retentionDays) {
618
626
  return pruneImpl(db, retentionDays, now());
@@ -656,9 +664,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
656
664
  if (arr === void 0) members.set(root, [w.id]);
657
665
  else arr.push(w.id);
658
666
  }
659
- const views = [...members.entries()].map(([taskId, ids2]) => {
667
+ const views = [...members.entries()].map(([taskId, memberIds]) => {
660
668
  const card = byId.get(taskId);
661
- return { taskId, title: card.title, status: card.status, workflowIds: ids2 };
669
+ return { taskId, title: card.title, status: card.status, workflowIds: memberIds };
662
670
  }).sort((a, b) => {
663
671
  const ca = byId.get(a.taskId)?.createdAt ?? "";
664
672
  const cb = byId.get(b.taskId)?.createdAt ?? "";
@@ -1001,9 +1009,9 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1001
1009
  outputTokens: n(r["o"])
1002
1010
  }));
1003
1011
  return {
1004
- totalCalls: n(total?.["c"]),
1005
- totalInputTokens: n(total?.["i"]),
1006
- totalOutputTokens: n(total?.["o"]),
1012
+ totalCalls: n(total["c"]),
1013
+ totalInputTokens: n(total["i"]),
1014
+ totalOutputTokens: n(total["o"]),
1007
1015
  byModel
1008
1016
  };
1009
1017
  },
@@ -1046,7 +1054,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1046
1054
  return windowUsageRows(db, tmuxSession, windows);
1047
1055
  },
1048
1056
  sessionWindowsPriced(sessions, windows) {
1049
- if (sessions.length === 0 || windows.length === 0) return priceUsage([]);
1050
1057
  return priceUsage(sessions.flatMap((s) => windowUsageRows(db, s, windows)));
1051
1058
  },
1052
1059
  byLocalDay(days = 7) {
@@ -1080,10 +1087,17 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1080
1087
  return mapped > 0 && total > 0 && filled * 2 < total;
1081
1088
  },
1082
1089
  resetClaudeIngest() {
1083
- db.exec("DELETE FROM prompt_usage WHERE source='claude_code'");
1084
- db.exec("DELETE FROM claude_ingest_offset");
1085
- db.prepare(`INSERT INTO usage_meta (key, value) VALUES ('last_claude_replay_at', ?)
1086
- ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(now().toISOString());
1090
+ db.exec("BEGIN");
1091
+ try {
1092
+ db.exec("DELETE FROM prompt_usage WHERE source='claude_code'");
1093
+ db.exec("DELETE FROM claude_ingest_offset");
1094
+ db.prepare(`INSERT INTO usage_meta (key, value) VALUES ('last_claude_replay_at', ?)
1095
+ ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(now().toISOString());
1096
+ db.exec("COMMIT");
1097
+ } catch (e) {
1098
+ db.exec("ROLLBACK");
1099
+ throw e;
1100
+ }
1087
1101
  },
1088
1102
  recent(limit = 100) {
1089
1103
  return db.prepare("SELECT * FROM prompt_usage ORDER BY id DESC LIMIT ?").all(limit).map((r) => ({
@@ -1129,7 +1143,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1129
1143
  }
1130
1144
  for (const f of files) {
1131
1145
  const stem = f.replace(/\.jsonl$/, "");
1132
- count += ingestFile(db, stem, join(dir2, f));
1146
+ count += ingestFile(db, stem, join(dir2, f), now);
1133
1147
  }
1134
1148
  }
1135
1149
  } catch {
@@ -1143,7 +1157,7 @@ var tmuxOf = (db, claudeId) => {
1143
1157
  const r = db.prepare("SELECT tmux_session FROM claude_sessions WHERE claude_session_id=?").get(claudeId);
1144
1158
  return r?.["tmux_session"] ?? "";
1145
1159
  };
1146
- var ingestFile = (db, claudeId, transcriptPath) => {
1160
+ var ingestFile = (db, claudeId, transcriptPath, now) => {
1147
1161
  let fd;
1148
1162
  try {
1149
1163
  fd = openSync(transcriptPath, "r");
@@ -1173,7 +1187,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1173
1187
  const sess = tmuxOf(db, claudeId);
1174
1188
  for (const line of complete.split("\n")) {
1175
1189
  if (line === "") continue;
1176
- const rec = parseAssistantLine(line);
1190
+ const rec = parseAssistantLine(line, now);
1177
1191
  if (!rec) continue;
1178
1192
  db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
1179
1193
  VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
@@ -1189,7 +1203,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1189
1203
  }
1190
1204
  db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
1191
1205
  VALUES (?, ?, ?)
1192
- ON CONFLICT(claude_session_id) DO UPDATE SET byte_offset=excluded.byte_offset, last_ingested_at=excluded.last_ingested_at`).run(claudeId, newOffset, (/* @__PURE__ */ new Date()).toISOString());
1206
+ ON CONFLICT(claude_session_id) DO UPDATE SET byte_offset=excluded.byte_offset, last_ingested_at=excluded.last_ingested_at`).run(claudeId, newOffset, now().toISOString());
1193
1207
  db.exec("COMMIT");
1194
1208
  } catch (e) {
1195
1209
  db.exec("ROLLBACK");
@@ -1200,7 +1214,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1200
1214
  closeSync(fd);
1201
1215
  }
1202
1216
  };
1203
- var parseAssistantLine = (line) => {
1217
+ var parseAssistantLine = (line, now) => {
1204
1218
  try {
1205
1219
  const v = JSON.parse(line);
1206
1220
  if (v.type !== "assistant") return null;
@@ -1215,7 +1229,7 @@ var parseAssistantLine = (line) => {
1215
1229
  inputTokens: input,
1216
1230
  outputTokens: output,
1217
1231
  cacheRead,
1218
- timestamp: typeof v.timestamp === "string" ? v.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1232
+ timestamp: typeof v.timestamp === "string" ? v.timestamp : now().toISOString(),
1219
1233
  callContext: v.uuid ?? ""
1220
1234
  };
1221
1235
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juli-os/ledger",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "juli-os L1 layer: the ledger of record for agent work — workflows, steps, case files, and token usage in one zero-dependency SQLite layer.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",