@juli-os/ledger 0.1.5 → 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);
@@ -224,10 +224,11 @@ CREATE INDEX IF NOT EXISTS idx_runs_session ON runs(session, status);
224
224
  `;
225
225
  var str = (v) => typeof v === "string" ? v : v === null || v === void 0 ? "" : String(v);
226
226
  var num = (v) => typeof v === "number" ? v : Number(v ?? 0);
227
- var dedupPattern = (key) => {
228
- const jsonValue = JSON.stringify(key).slice(1, -1);
229
- return `%"dedup_key":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
227
+ var jsonFieldPattern = (field, value) => {
228
+ const jsonValue = JSON.stringify(value).slice(1, -1);
229
+ return `%"${field}":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
230
230
  };
231
+ var dedupPattern = (key) => jsonFieldPattern("dedup_key", key);
231
232
  var SEQ_PARK_BASE = 1e9;
232
233
  var SEND_STEP_LIKE_CUTOFF = "2026-09-19";
233
234
  var rowToWorkflow = (r) => ({
@@ -239,7 +240,7 @@ var rowToWorkflow = (r) => ({
239
240
  session: str(r["session"]),
240
241
  project: str(r["project"]),
241
242
  path: str(r["path"]),
242
- 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"]),
243
244
  meta: asRecord(safeParse(str(r["meta"]))),
244
245
  createdAt: str(r["created_at"]),
245
246
  updatedAt: str(r["updated_at"])
@@ -279,6 +280,8 @@ var pruneImpl = (db, retentionDays, nowIso) => {
279
280
  try {
280
281
  db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
281
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);
282
285
  db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
283
286
  db.exec("COMMIT");
284
287
  } catch (e) {
@@ -326,7 +329,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
326
329
  const activeRunOf = (session) => {
327
330
  const r = db.prepare(
328
331
  `SELECT r.id AS id, r.workflow_id AS workflow_id, r.session AS session,
329
- r.status AS status, r.created_at AS created_at FROM runs r
332
+ r.created_at AS created_at FROM runs r
330
333
  JOIN workflows w ON w.id = r.workflow_id
331
334
  WHERE r.session=? AND r.status IN ('running','idle') AND w.status='running'
332
335
  ORDER BY r.created_at DESC, r.rowid DESC LIMIT 1`
@@ -352,8 +355,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
352
355
  return getWorkflowById(id);
353
356
  },
354
357
  getWorkflow(id) {
355
- const r = db.prepare("SELECT * FROM workflows WHERE id=?").get(id);
356
- return r ? rowToWorkflow(r) : null;
358
+ return getWorkflowById(id);
357
359
  },
358
360
  listWorkflows(limit = 100, status) {
359
361
  if (status !== void 0 && status !== "") {
@@ -456,8 +458,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
456
458
  }
457
459
  },
458
460
  getStep(id) {
459
- const r = db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE id=?`).get(id);
460
- return r ? rowToStep(r) : null;
461
+ return getStepById(id);
461
462
  },
462
463
  stepsForWorkflow(id) {
463
464
  return db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE workflow_id=? ORDER BY seq`).all(id).map(rowToStep);
@@ -528,25 +529,21 @@ var createLedgerStore = (dbPath, deps = {}) => {
528
529
  ).all(status, limit).map(rowToStep);
529
530
  },
530
531
  runningAgentStepForSession(session) {
531
- const r = db.prepare(
532
- `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
532
+ const proj = `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
533
533
  COALESCE(st.title,'') AS title, st.status AS status,
534
534
  COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
535
535
  COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
536
536
  FROM workflow_steps st
537
- JOIN workflows w ON w.id = st.workflow_id
537
+ JOIN workflows w ON w.id = st.workflow_id`;
538
+ const r = db.prepare(
539
+ `${proj}
538
540
  JOIN runs r ON r.workflow_id = st.workflow_id
539
541
  WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND r.session=? AND w.status='running'
540
542
  ORDER BY st.updated_at DESC LIMIT 1`
541
543
  ).get(session);
542
544
  if (r) return rowToStep(r);
543
545
  const r2 = db.prepare(
544
- `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
545
- COALESCE(st.title,'') AS title, st.status AS status,
546
- COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
547
- COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
548
- FROM workflow_steps st
549
- JOIN workflows w ON w.id = st.workflow_id
546
+ `${proj}
550
547
  WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND w.status='running'
551
548
  AND (w.session=? OR COALESCE(json_extract(w.meta,'$.assigned_session'),'')=?)
552
549
  ORDER BY st.updated_at DESC LIMIT 1`
@@ -555,8 +552,10 @@ var createLedgerStore = (dbPath, deps = {}) => {
555
552
  },
556
553
  sessionHasOpenWork(session, excludeWorkflowId) {
557
554
  const anchor = "(session=?1 OR COALESCE(json_extract(meta,'$.assigned_session'),'')=?1 OR id IN (SELECT workflow_id FROM runs WHERE session=?1))";
558
- const sql = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? `SELECT COUNT(*) AS n FROM workflows WHERE status IN ('queued','running') AND ${anchor} AND id != ?2` : `SELECT COUNT(*) AS n FROM workflows WHERE status IN ('queued','running') AND ${anchor}`;
559
- const row = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
555
+ const openFilter = `status IN (${sqlInList(ACTIVE_WORKFLOW_STATUSES)})`;
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);
560
559
  return Number(row["n"]) > 0;
561
560
  },
562
561
  appendEvent(entityType, entityId, type, payload = {}) {
@@ -597,14 +596,15 @@ var createLedgerStore = (dbPath, deps = {}) => {
597
596
  },
598
597
  createRun(workflowId, session) {
599
598
  const id = ids.newId("run");
599
+ const ts = now();
600
600
  db.prepare(`INSERT INTO runs (id, workflow_id, session, status, created_at)
601
- VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, now());
601
+ VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, ts);
602
602
  return {
603
603
  id,
604
604
  workflowId,
605
605
  session,
606
606
  status: "running",
607
- createdAt: now()
607
+ createdAt: ts
608
608
  };
609
609
  },
610
610
  activeRunForSession: (session) => activeRunOf(session),
@@ -627,25 +627,27 @@ var createLedgerStore = (dbPath, deps = {}) => {
627
627
  ).get(dedupPattern(key));
628
628
  return r !== void 0;
629
629
  },
630
- /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
631
- * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
632
- * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
633
- * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 dedupPattern
634
- * 成对转义(message-id 几乎必含 `_`)。 */
630
+ /** 全量出站邮件登记(2026-09-20 幽灵单缺口):send 步之外,通知邮件
631
+ * (emailNotifier)等所有出站信的 message_id 也落 email_sent 事件——
632
+ * 发给轮询邮箱自身的告警信不再被当新来信建单([juli] 待审批 幽灵单)。 */
635
633
  recordSentMessageId(messageId, meta) {
636
634
  if (messageId === "") return;
637
635
  db.prepare(
638
636
  `INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES ('email', ?, 'email_sent', ?, ?)`
639
637
  ).run(messageId, JSON.stringify(meta ?? {}), now());
640
638
  },
639
+ /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
640
+ * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
641
+ * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
642
+ * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
643
+ * 成对转义(message-id 几乎必含 `_`)。 */
641
644
  sentMessageIdExists(messageId) {
642
645
  if (messageId === "") return false;
643
646
  const registered = db.prepare(
644
647
  `SELECT 1 FROM events WHERE type = 'email_sent' AND entity_type = 'email' AND entity_id = ? LIMIT 1`
645
648
  ).get(messageId);
646
649
  if (registered !== void 0) return true;
647
- const jsonValue = JSON.stringify(messageId).slice(1, -1);
648
- const pattern = `%"message_id":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
650
+ const pattern = jsonFieldPattern("message_id", messageId);
649
651
  const rows = db.prepare(
650
652
  `SELECT output FROM workflow_steps WHERE kind = 'send' AND status = 'completed'
651
653
  AND updated_at < ? AND output LIKE ? ESCAPE '\\'`
@@ -657,9 +659,10 @@ var createLedgerStore = (dbPath, deps = {}) => {
657
659
  },
658
660
  createPrompt(runId, content, source) {
659
661
  const id = ids.newId("pr");
662
+ const ts = now();
660
663
  db.prepare(`INSERT INTO prompts (id, run_id, content, source, status, created_at)
661
- VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source, now());
662
- 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 };
663
666
  },
664
667
  prune(retentionDays) {
665
668
  return pruneImpl(db, retentionDays, now());
@@ -703,9 +706,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
703
706
  if (arr === void 0) members.set(root, [w.id]);
704
707
  else arr.push(w.id);
705
708
  }
706
- const views = [...members.entries()].map(([taskId, ids2]) => {
709
+ const views = [...members.entries()].map(([taskId, memberIds]) => {
707
710
  const card = byId.get(taskId);
708
- return { taskId, title: card.title, status: card.status, workflowIds: ids2 };
711
+ return { taskId, title: card.title, status: card.status, workflowIds: memberIds };
709
712
  }).sort((a, b) => {
710
713
  const ca = byId.get(a.taskId)?.createdAt ?? "";
711
714
  const cb = byId.get(b.taskId)?.createdAt ?? "";
@@ -881,18 +884,18 @@ var priceUsage = (rows) => {
881
884
  const key = r.model === "" ? "\uFF08\u672A\u8BB0\u5F55\u6A21\u578B\uFF09" : r.model;
882
885
  let a = accs.get(key);
883
886
  if (a === void 0) {
884
- const p = findPrice(key);
887
+ const p2 = findPrice(key);
885
888
  a = {
886
889
  model: key,
887
- label: p?.label ?? key,
890
+ label: p2?.label ?? key,
888
891
  free: false,
889
- priced: p !== null,
890
892
  calls: 0,
891
893
  inputTokens: 0,
892
894
  cachedTokens: 0,
893
895
  outputTokens: 0,
894
896
  tiersHit: /* @__PURE__ */ new Set(),
895
- cost: { cache: 0, input: 0, output: 0 }
897
+ cost: { cache: 0, input: 0, output: 0 },
898
+ price: p2
896
899
  };
897
900
  accs.set(key, a);
898
901
  }
@@ -900,8 +903,9 @@ var priceUsage = (rows) => {
900
903
  a.inputTokens += r.inputTokens;
901
904
  a.cachedTokens += r.cachedTokens;
902
905
  a.outputTokens += r.outputTokens;
903
- if (!a.priced) continue;
904
- const t = tierOf(findPrice(key), r);
906
+ const p = a.price;
907
+ if (p === null) continue;
908
+ const t = tierOf(p, r);
905
909
  a.tiersHit.add(t.note);
906
910
  if (t.input === 0 && t.output === 0) {
907
911
  a.free = true;
@@ -917,7 +921,7 @@ var priceUsage = (rows) => {
917
921
  model: a.model,
918
922
  label: a.label,
919
923
  free: a.free,
920
- priced: a.priced,
924
+ priced: a.price !== null,
921
925
  calls: a.calls,
922
926
  inputTokens: a.inputTokens,
923
927
  cachedTokens: a.cachedTokens,
@@ -981,11 +985,15 @@ CREATE TABLE IF NOT EXISTS usage_meta (
981
985
  );
982
986
  `;
983
987
  var n = (v) => typeof v === "number" ? v : Number(v ?? 0);
984
- var windowUsageRows = (db, tmuxSession, windows) => {
985
- if (windows.length === 0) return [];
988
+ var windowConds = (tmuxSession, windows) => {
986
989
  const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
987
990
  const params = [tmuxSession];
988
991
  for (const [a, b] of windows) params.push(a, b);
992
+ return { conds, params };
993
+ };
994
+ var windowUsageRows = (db, tmuxSession, windows) => {
995
+ if (windows.length === 0) return [];
996
+ const { conds, params } = windowConds(tmuxSession, windows);
989
997
  return db.prepare(
990
998
  `SELECT model, input_tokens AS i, output_tokens AS o, cached_tokens AS k
991
999
  FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
@@ -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
  },
@@ -1077,9 +1085,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1077
1085
  },
1078
1086
  perSessionWindows(tmuxSession, windows) {
1079
1087
  if (windows.length === 0) return { count: 0, inputTokens: 0, outputTokens: 0 };
1080
- const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
1081
- const params = [tmuxSession];
1082
- for (const [a, b] of windows) params.push(a, b);
1088
+ const { conds, params } = windowConds(tmuxSession, windows);
1083
1089
  const r = db.prepare(
1084
1090
  `SELECT COUNT(*) AS c, COALESCE(SUM(input_tokens),0) AS i, COALESCE(SUM(output_tokens),0) AS o
1085
1091
  FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
@@ -1090,7 +1096,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1090
1096
  return windowUsageRows(db, tmuxSession, windows);
1091
1097
  },
1092
1098
  sessionWindowsPriced(sessions, windows) {
1093
- if (sessions.length === 0 || windows.length === 0) return priceUsage([]);
1094
1099
  return priceUsage(sessions.flatMap((s) => windowUsageRows(db, s, windows)));
1095
1100
  },
1096
1101
  byLocalDay(days = 7) {
@@ -1124,10 +1129,17 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1124
1129
  return mapped > 0 && total > 0 && filled * 2 < total;
1125
1130
  },
1126
1131
  resetClaudeIngest() {
1127
- db.exec("DELETE FROM prompt_usage WHERE source='claude_code'");
1128
- db.exec("DELETE FROM claude_ingest_offset");
1129
- db.prepare(`INSERT INTO usage_meta (key, value) VALUES ('last_claude_replay_at', ?)
1130
- 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
+ }
1131
1143
  },
1132
1144
  recent(limit = 100) {
1133
1145
  return db.prepare("SELECT * FROM prompt_usage ORDER BY id DESC LIMIT ?").all(limit).map((r) => ({
@@ -1160,9 +1172,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1160
1172
  VALUES (?, ?, '', '') ON CONFLICT(claude_session_id) DO NOTHING`).run(claudeSessionId, tmuxSession);
1161
1173
  },
1162
1174
  ingestProjectTranscripts(claudeProjectsDir) {
1163
- const mapped = new Set(
1164
- db.prepare("SELECT claude_session_id FROM claude_sessions").all().map((r) => String(r["claude_session_id"]))
1165
- );
1166
1175
  let count = 0;
1167
1176
  try {
1168
1177
  const projects = readdirSync(claudeProjectsDir);
@@ -1176,7 +1185,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1176
1185
  }
1177
1186
  for (const f of files) {
1178
1187
  const stem = f.replace(/\.jsonl$/, "");
1179
- count += ingestFile(db, stem, join(dir, f));
1188
+ count += ingestFile(db, stem, join(dir, f), now);
1180
1189
  }
1181
1190
  }
1182
1191
  } catch {
@@ -1190,7 +1199,7 @@ var tmuxOf = (db, claudeId) => {
1190
1199
  const r = db.prepare("SELECT tmux_session FROM claude_sessions WHERE claude_session_id=?").get(claudeId);
1191
1200
  return r?.["tmux_session"] ?? "";
1192
1201
  };
1193
- var ingestFile = (db, claudeId, transcriptPath) => {
1202
+ var ingestFile = (db, claudeId, transcriptPath, now) => {
1194
1203
  let fd;
1195
1204
  try {
1196
1205
  fd = openSync(transcriptPath, "r");
@@ -1217,11 +1226,11 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1217
1226
  const newOffset = offset + Buffer.byteLength(complete, "utf8");
1218
1227
  db.exec("BEGIN");
1219
1228
  try {
1229
+ const sess = tmuxOf(db, claudeId);
1220
1230
  for (const line of complete.split("\n")) {
1221
1231
  if (line === "") continue;
1222
- const rec = parseAssistantLine(line);
1232
+ const rec = parseAssistantLine(line, now);
1223
1233
  if (!rec) continue;
1224
- const sess = tmuxOf(db, claudeId);
1225
1234
  db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
1226
1235
  VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
1227
1236
  `turn=${String(rec.callContext).slice(0, 40)}`,
@@ -1236,7 +1245,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1236
1245
  }
1237
1246
  db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
1238
1247
  VALUES (?, ?, ?)
1239
- 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());
1240
1249
  db.exec("COMMIT");
1241
1250
  } catch (e) {
1242
1251
  db.exec("ROLLBACK");
@@ -1247,7 +1256,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1247
1256
  closeSync(fd);
1248
1257
  }
1249
1258
  };
1250
- var parseAssistantLine = (line) => {
1259
+ var parseAssistantLine = (line, now) => {
1251
1260
  try {
1252
1261
  const v = JSON.parse(line);
1253
1262
  if (v.type !== "assistant") return null;
@@ -1262,7 +1271,7 @@ var parseAssistantLine = (line) => {
1262
1271
  inputTokens: input,
1263
1272
  outputTokens: output,
1264
1273
  cacheRead,
1265
- timestamp: typeof v.timestamp === "string" ? v.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1274
+ timestamp: typeof v.timestamp === "string" ? v.timestamp : now().toISOString(),
1266
1275
  callContext: v.uuid ?? ""
1267
1276
  };
1268
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);
@@ -182,10 +182,11 @@ CREATE INDEX IF NOT EXISTS idx_runs_session ON runs(session, status);
182
182
  `;
183
183
  var str = (v) => typeof v === "string" ? v : v === null || v === void 0 ? "" : String(v);
184
184
  var num = (v) => typeof v === "number" ? v : Number(v ?? 0);
185
- var dedupPattern = (key) => {
186
- const jsonValue = JSON.stringify(key).slice(1, -1);
187
- return `%"dedup_key":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
185
+ var jsonFieldPattern = (field, value) => {
186
+ const jsonValue = JSON.stringify(value).slice(1, -1);
187
+ return `%"${field}":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
188
188
  };
189
+ var dedupPattern = (key) => jsonFieldPattern("dedup_key", key);
189
190
  var SEQ_PARK_BASE = 1e9;
190
191
  var SEND_STEP_LIKE_CUTOFF = "2026-09-19";
191
192
  var rowToWorkflow = (r) => ({
@@ -197,7 +198,7 @@ var rowToWorkflow = (r) => ({
197
198
  session: str(r["session"]),
198
199
  project: str(r["project"]),
199
200
  path: str(r["path"]),
200
- 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"]),
201
202
  meta: asRecord(safeParse(str(r["meta"]))),
202
203
  createdAt: str(r["created_at"]),
203
204
  updatedAt: str(r["updated_at"])
@@ -237,6 +238,8 @@ var pruneImpl = (db, retentionDays, nowIso) => {
237
238
  try {
238
239
  db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
239
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);
240
243
  db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
241
244
  db.exec("COMMIT");
242
245
  } catch (e) {
@@ -284,7 +287,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
284
287
  const activeRunOf = (session) => {
285
288
  const r = db.prepare(
286
289
  `SELECT r.id AS id, r.workflow_id AS workflow_id, r.session AS session,
287
- r.status AS status, r.created_at AS created_at FROM runs r
290
+ r.created_at AS created_at FROM runs r
288
291
  JOIN workflows w ON w.id = r.workflow_id
289
292
  WHERE r.session=? AND r.status IN ('running','idle') AND w.status='running'
290
293
  ORDER BY r.created_at DESC, r.rowid DESC LIMIT 1`
@@ -310,8 +313,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
310
313
  return getWorkflowById(id);
311
314
  },
312
315
  getWorkflow(id) {
313
- const r = db.prepare("SELECT * FROM workflows WHERE id=?").get(id);
314
- return r ? rowToWorkflow(r) : null;
316
+ return getWorkflowById(id);
315
317
  },
316
318
  listWorkflows(limit = 100, status) {
317
319
  if (status !== void 0 && status !== "") {
@@ -414,8 +416,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
414
416
  }
415
417
  },
416
418
  getStep(id) {
417
- const r = db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE id=?`).get(id);
418
- return r ? rowToStep(r) : null;
419
+ return getStepById(id);
419
420
  },
420
421
  stepsForWorkflow(id) {
421
422
  return db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE workflow_id=? ORDER BY seq`).all(id).map(rowToStep);
@@ -486,25 +487,21 @@ var createLedgerStore = (dbPath, deps = {}) => {
486
487
  ).all(status, limit).map(rowToStep);
487
488
  },
488
489
  runningAgentStepForSession(session) {
489
- const r = db.prepare(
490
- `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
490
+ const proj = `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
491
491
  COALESCE(st.title,'') AS title, st.status AS status,
492
492
  COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
493
493
  COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
494
494
  FROM workflow_steps st
495
- JOIN workflows w ON w.id = st.workflow_id
495
+ JOIN workflows w ON w.id = st.workflow_id`;
496
+ const r = db.prepare(
497
+ `${proj}
496
498
  JOIN runs r ON r.workflow_id = st.workflow_id
497
499
  WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND r.session=? AND w.status='running'
498
500
  ORDER BY st.updated_at DESC LIMIT 1`
499
501
  ).get(session);
500
502
  if (r) return rowToStep(r);
501
503
  const r2 = db.prepare(
502
- `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
503
- COALESCE(st.title,'') AS title, st.status AS status,
504
- COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
505
- COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
506
- FROM workflow_steps st
507
- JOIN workflows w ON w.id = st.workflow_id
504
+ `${proj}
508
505
  WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND w.status='running'
509
506
  AND (w.session=? OR COALESCE(json_extract(w.meta,'$.assigned_session'),'')=?)
510
507
  ORDER BY st.updated_at DESC LIMIT 1`
@@ -513,8 +510,10 @@ var createLedgerStore = (dbPath, deps = {}) => {
513
510
  },
514
511
  sessionHasOpenWork(session, excludeWorkflowId) {
515
512
  const anchor = "(session=?1 OR COALESCE(json_extract(meta,'$.assigned_session'),'')=?1 OR id IN (SELECT workflow_id FROM runs WHERE session=?1))";
516
- const sql = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? `SELECT COUNT(*) AS n FROM workflows WHERE status IN ('queued','running') AND ${anchor} AND id != ?2` : `SELECT COUNT(*) AS n FROM workflows WHERE status IN ('queued','running') AND ${anchor}`;
517
- const row = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
513
+ const openFilter = `status IN (${sqlInList(ACTIVE_WORKFLOW_STATUSES)})`;
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);
518
517
  return Number(row["n"]) > 0;
519
518
  },
520
519
  appendEvent(entityType, entityId, type, payload = {}) {
@@ -555,14 +554,15 @@ var createLedgerStore = (dbPath, deps = {}) => {
555
554
  },
556
555
  createRun(workflowId, session) {
557
556
  const id = ids.newId("run");
557
+ const ts = now();
558
558
  db.prepare(`INSERT INTO runs (id, workflow_id, session, status, created_at)
559
- VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, now());
559
+ VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, ts);
560
560
  return {
561
561
  id,
562
562
  workflowId,
563
563
  session,
564
564
  status: "running",
565
- createdAt: now()
565
+ createdAt: ts
566
566
  };
567
567
  },
568
568
  activeRunForSession: (session) => activeRunOf(session),
@@ -585,25 +585,27 @@ var createLedgerStore = (dbPath, deps = {}) => {
585
585
  ).get(dedupPattern(key));
586
586
  return r !== void 0;
587
587
  },
588
- /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
589
- * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
590
- * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
591
- * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 dedupPattern
592
- * 成对转义(message-id 几乎必含 `_`)。 */
588
+ /** 全量出站邮件登记(2026-09-20 幽灵单缺口):send 步之外,通知邮件
589
+ * (emailNotifier)等所有出站信的 message_id 也落 email_sent 事件——
590
+ * 发给轮询邮箱自身的告警信不再被当新来信建单([juli] 待审批 幽灵单)。 */
593
591
  recordSentMessageId(messageId, meta) {
594
592
  if (messageId === "") return;
595
593
  db.prepare(
596
594
  `INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES ('email', ?, 'email_sent', ?, ?)`
597
595
  ).run(messageId, JSON.stringify(meta ?? {}), now());
598
596
  },
597
+ /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
598
+ * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
599
+ * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
600
+ * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
601
+ * 成对转义(message-id 几乎必含 `_`)。 */
599
602
  sentMessageIdExists(messageId) {
600
603
  if (messageId === "") return false;
601
604
  const registered = db.prepare(
602
605
  `SELECT 1 FROM events WHERE type = 'email_sent' AND entity_type = 'email' AND entity_id = ? LIMIT 1`
603
606
  ).get(messageId);
604
607
  if (registered !== void 0) return true;
605
- const jsonValue = JSON.stringify(messageId).slice(1, -1);
606
- const pattern = `%"message_id":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
608
+ const pattern = jsonFieldPattern("message_id", messageId);
607
609
  const rows = db.prepare(
608
610
  `SELECT output FROM workflow_steps WHERE kind = 'send' AND status = 'completed'
609
611
  AND updated_at < ? AND output LIKE ? ESCAPE '\\'`
@@ -615,9 +617,10 @@ var createLedgerStore = (dbPath, deps = {}) => {
615
617
  },
616
618
  createPrompt(runId, content, source) {
617
619
  const id = ids.newId("pr");
620
+ const ts = now();
618
621
  db.prepare(`INSERT INTO prompts (id, run_id, content, source, status, created_at)
619
- VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source, now());
620
- 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 };
621
624
  },
622
625
  prune(retentionDays) {
623
626
  return pruneImpl(db, retentionDays, now());
@@ -661,9 +664,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
661
664
  if (arr === void 0) members.set(root, [w.id]);
662
665
  else arr.push(w.id);
663
666
  }
664
- const views = [...members.entries()].map(([taskId, ids2]) => {
667
+ const views = [...members.entries()].map(([taskId, memberIds]) => {
665
668
  const card = byId.get(taskId);
666
- return { taskId, title: card.title, status: card.status, workflowIds: ids2 };
669
+ return { taskId, title: card.title, status: card.status, workflowIds: memberIds };
667
670
  }).sort((a, b) => {
668
671
  const ca = byId.get(a.taskId)?.createdAt ?? "";
669
672
  const cb = byId.get(b.taskId)?.createdAt ?? "";
@@ -839,18 +842,18 @@ var priceUsage = (rows) => {
839
842
  const key = r.model === "" ? "\uFF08\u672A\u8BB0\u5F55\u6A21\u578B\uFF09" : r.model;
840
843
  let a = accs.get(key);
841
844
  if (a === void 0) {
842
- const p = findPrice(key);
845
+ const p2 = findPrice(key);
843
846
  a = {
844
847
  model: key,
845
- label: p?.label ?? key,
848
+ label: p2?.label ?? key,
846
849
  free: false,
847
- priced: p !== null,
848
850
  calls: 0,
849
851
  inputTokens: 0,
850
852
  cachedTokens: 0,
851
853
  outputTokens: 0,
852
854
  tiersHit: /* @__PURE__ */ new Set(),
853
- cost: { cache: 0, input: 0, output: 0 }
855
+ cost: { cache: 0, input: 0, output: 0 },
856
+ price: p2
854
857
  };
855
858
  accs.set(key, a);
856
859
  }
@@ -858,8 +861,9 @@ var priceUsage = (rows) => {
858
861
  a.inputTokens += r.inputTokens;
859
862
  a.cachedTokens += r.cachedTokens;
860
863
  a.outputTokens += r.outputTokens;
861
- if (!a.priced) continue;
862
- const t = tierOf(findPrice(key), r);
864
+ const p = a.price;
865
+ if (p === null) continue;
866
+ const t = tierOf(p, r);
863
867
  a.tiersHit.add(t.note);
864
868
  if (t.input === 0 && t.output === 0) {
865
869
  a.free = true;
@@ -875,7 +879,7 @@ var priceUsage = (rows) => {
875
879
  model: a.model,
876
880
  label: a.label,
877
881
  free: a.free,
878
- priced: a.priced,
882
+ priced: a.price !== null,
879
883
  calls: a.calls,
880
884
  inputTokens: a.inputTokens,
881
885
  cachedTokens: a.cachedTokens,
@@ -939,11 +943,15 @@ CREATE TABLE IF NOT EXISTS usage_meta (
939
943
  );
940
944
  `;
941
945
  var n = (v) => typeof v === "number" ? v : Number(v ?? 0);
942
- var windowUsageRows = (db, tmuxSession, windows) => {
943
- if (windows.length === 0) return [];
946
+ var windowConds = (tmuxSession, windows) => {
944
947
  const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
945
948
  const params = [tmuxSession];
946
949
  for (const [a, b] of windows) params.push(a, b);
950
+ return { conds, params };
951
+ };
952
+ var windowUsageRows = (db, tmuxSession, windows) => {
953
+ if (windows.length === 0) return [];
954
+ const { conds, params } = windowConds(tmuxSession, windows);
947
955
  return db.prepare(
948
956
  `SELECT model, input_tokens AS i, output_tokens AS o, cached_tokens AS k
949
957
  FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
@@ -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
  },
@@ -1035,9 +1043,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1035
1043
  },
1036
1044
  perSessionWindows(tmuxSession, windows) {
1037
1045
  if (windows.length === 0) return { count: 0, inputTokens: 0, outputTokens: 0 };
1038
- const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
1039
- const params = [tmuxSession];
1040
- for (const [a, b] of windows) params.push(a, b);
1046
+ const { conds, params } = windowConds(tmuxSession, windows);
1041
1047
  const r = db.prepare(
1042
1048
  `SELECT COUNT(*) AS c, COALESCE(SUM(input_tokens),0) AS i, COALESCE(SUM(output_tokens),0) AS o
1043
1049
  FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
@@ -1048,7 +1054,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1048
1054
  return windowUsageRows(db, tmuxSession, windows);
1049
1055
  },
1050
1056
  sessionWindowsPriced(sessions, windows) {
1051
- if (sessions.length === 0 || windows.length === 0) return priceUsage([]);
1052
1057
  return priceUsage(sessions.flatMap((s) => windowUsageRows(db, s, windows)));
1053
1058
  },
1054
1059
  byLocalDay(days = 7) {
@@ -1082,10 +1087,17 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1082
1087
  return mapped > 0 && total > 0 && filled * 2 < total;
1083
1088
  },
1084
1089
  resetClaudeIngest() {
1085
- db.exec("DELETE FROM prompt_usage WHERE source='claude_code'");
1086
- db.exec("DELETE FROM claude_ingest_offset");
1087
- db.prepare(`INSERT INTO usage_meta (key, value) VALUES ('last_claude_replay_at', ?)
1088
- 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
+ }
1089
1101
  },
1090
1102
  recent(limit = 100) {
1091
1103
  return db.prepare("SELECT * FROM prompt_usage ORDER BY id DESC LIMIT ?").all(limit).map((r) => ({
@@ -1118,9 +1130,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1118
1130
  VALUES (?, ?, '', '') ON CONFLICT(claude_session_id) DO NOTHING`).run(claudeSessionId, tmuxSession);
1119
1131
  },
1120
1132
  ingestProjectTranscripts(claudeProjectsDir) {
1121
- const mapped = new Set(
1122
- db.prepare("SELECT claude_session_id FROM claude_sessions").all().map((r) => String(r["claude_session_id"]))
1123
- );
1124
1133
  let count = 0;
1125
1134
  try {
1126
1135
  const projects = readdirSync(claudeProjectsDir);
@@ -1134,7 +1143,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1134
1143
  }
1135
1144
  for (const f of files) {
1136
1145
  const stem = f.replace(/\.jsonl$/, "");
1137
- count += ingestFile(db, stem, join(dir2, f));
1146
+ count += ingestFile(db, stem, join(dir2, f), now);
1138
1147
  }
1139
1148
  }
1140
1149
  } catch {
@@ -1148,7 +1157,7 @@ var tmuxOf = (db, claudeId) => {
1148
1157
  const r = db.prepare("SELECT tmux_session FROM claude_sessions WHERE claude_session_id=?").get(claudeId);
1149
1158
  return r?.["tmux_session"] ?? "";
1150
1159
  };
1151
- var ingestFile = (db, claudeId, transcriptPath) => {
1160
+ var ingestFile = (db, claudeId, transcriptPath, now) => {
1152
1161
  let fd;
1153
1162
  try {
1154
1163
  fd = openSync(transcriptPath, "r");
@@ -1175,11 +1184,11 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1175
1184
  const newOffset = offset + Buffer.byteLength(complete, "utf8");
1176
1185
  db.exec("BEGIN");
1177
1186
  try {
1187
+ const sess = tmuxOf(db, claudeId);
1178
1188
  for (const line of complete.split("\n")) {
1179
1189
  if (line === "") continue;
1180
- const rec = parseAssistantLine(line);
1190
+ const rec = parseAssistantLine(line, now);
1181
1191
  if (!rec) continue;
1182
- const sess = tmuxOf(db, claudeId);
1183
1192
  db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
1184
1193
  VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
1185
1194
  `turn=${String(rec.callContext).slice(0, 40)}`,
@@ -1194,7 +1203,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1194
1203
  }
1195
1204
  db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
1196
1205
  VALUES (?, ?, ?)
1197
- 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());
1198
1207
  db.exec("COMMIT");
1199
1208
  } catch (e) {
1200
1209
  db.exec("ROLLBACK");
@@ -1205,7 +1214,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1205
1214
  closeSync(fd);
1206
1215
  }
1207
1216
  };
1208
- var parseAssistantLine = (line) => {
1217
+ var parseAssistantLine = (line, now) => {
1209
1218
  try {
1210
1219
  const v = JSON.parse(line);
1211
1220
  if (v.type !== "assistant") return null;
@@ -1220,7 +1229,7 @@ var parseAssistantLine = (line) => {
1220
1229
  inputTokens: input,
1221
1230
  outputTokens: output,
1222
1231
  cacheRead,
1223
- timestamp: typeof v.timestamp === "string" ? v.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1232
+ timestamp: typeof v.timestamp === "string" ? v.timestamp : now().toISOString(),
1224
1233
  callContext: v.uuid ?? ""
1225
1234
  };
1226
1235
  } catch {
@@ -6,5 +6,3 @@ export type JsonRecord = {
6
6
  };
7
7
  export declare const safeParse: (raw: string | null | undefined) => JsonValue | null;
8
8
  export declare const asRecord: (v: JsonValue | null | undefined) => JsonRecord;
9
- export declare const asString: (v: JsonValue | undefined) => string;
10
- export declare const truncate: (s: string, n: number) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juli-os/ledger",
3
- "version": "0.1.5",
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",