@juli-os/ledger 0.1.4 → 0.1.6

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/dist/index.js CHANGED
@@ -224,10 +224,12 @@ 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);
232
+ var SEQ_PARK_BASE = 1e9;
231
233
  var SEND_STEP_LIKE_CUTOFF = "2026-09-19";
232
234
  var rowToWorkflow = (r) => ({
233
235
  id: str(r["id"]),
@@ -267,15 +269,23 @@ var rowToEvent = (r) => ({
267
269
  });
268
270
  var pruneImpl = (db, retentionDays, nowIso) => {
269
271
  if (retentionDays <= 0) return { events: 0, workflows: 0 };
270
- const cutoff = new Date(new Date(nowIso).getTime() - retentionDays * 864e5).toISOString();
272
+ const cutoff = rfc3339(new Date(new Date(nowIso).getTime() - retentionDays * 864e5));
271
273
  const ev = db.prepare("DELETE FROM events WHERE ts < ?").run(cutoff);
272
274
  const wfRows = db.prepare(
273
275
  `SELECT id FROM workflows WHERE status IN (${sqlInList(PRUNE_WORKFLOW_STATUSES)}) AND updated_at < ?`
274
276
  ).all(cutoff);
275
277
  let wfCount = 0;
276
278
  for (const r of wfRows) {
277
- db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
278
- db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
279
+ db.exec("BEGIN");
280
+ try {
281
+ db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
282
+ db.prepare("DELETE FROM relays WHERE workflow_id=?").run(r.id);
283
+ db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
284
+ db.exec("COMMIT");
285
+ } catch (e) {
286
+ db.exec("ROLLBACK");
287
+ throw e;
288
+ }
279
289
  wfCount++;
280
290
  }
281
291
  return { events: Number(ev.changes), workflows: wfCount };
@@ -320,7 +330,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
320
330
  r.status AS status, r.created_at AS created_at FROM runs r
321
331
  JOIN workflows w ON w.id = r.workflow_id
322
332
  WHERE r.session=? AND r.status IN ('running','idle') AND w.status='running'
323
- ORDER BY r.created_at DESC LIMIT 1`
333
+ ORDER BY r.created_at DESC, r.rowid DESC LIMIT 1`
324
334
  ).get(session);
325
335
  if (!r) return null;
326
336
  return {
@@ -343,8 +353,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
343
353
  return getWorkflowById(id);
344
354
  },
345
355
  getWorkflow(id) {
346
- const r = db.prepare("SELECT * FROM workflows WHERE id=?").get(id);
347
- return r ? rowToWorkflow(r) : null;
356
+ return getWorkflowById(id);
348
357
  },
349
358
  listWorkflows(limit = 100, status) {
350
359
  if (status !== void 0 && status !== "") {
@@ -419,12 +428,16 @@ var createLedgerStore = (dbPath, deps = {}) => {
419
428
  `INSERT INTO workflow_steps (id, workflow_id, seq, kind, title, status, input, updated_at)
420
429
  VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)`
421
430
  );
422
- const shift = db.prepare(
423
- "UPDATE workflow_steps SET seq = seq + ? WHERE workflow_id = ? AND seq >= ?"
431
+ const park = db.prepare(
432
+ `UPDATE workflow_steps SET seq = seq + ${SEQ_PARK_BASE} WHERE workflow_id = ? AND seq >= ?`
433
+ );
434
+ const restore = db.prepare(
435
+ `UPDATE workflow_steps SET seq = seq - ${SEQ_PARK_BASE} + ? WHERE workflow_id = ? AND seq >= ${SEQ_PARK_BASE}`
424
436
  );
425
437
  db.exec("BEGIN");
426
438
  try {
427
- shift.run(steps.length, workflowId, afterSeq + 1);
439
+ park.run(workflowId, afterSeq + 1);
440
+ restore.run(steps.length, workflowId);
428
441
  steps.forEach((st, i) => {
429
442
  insert.run(
430
443
  ids.newId("st"),
@@ -443,8 +456,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
443
456
  }
444
457
  },
445
458
  getStep(id) {
446
- const r = db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE id=?`).get(id);
447
- return r ? rowToStep(r) : null;
459
+ return getStepById(id);
448
460
  },
449
461
  stepsForWorkflow(id) {
450
462
  return db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE workflow_id=? ORDER BY seq`).all(id).map(rowToStep);
@@ -474,7 +486,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
474
486
  },
475
487
  pendingRelays() {
476
488
  return db.prepare(`SELECT id, workflow_id, step_id, session, text, marker, created_at, attempts,
477
- COALESCE(last_error,'') AS last_error FROM relays WHERE delivered_at IS NULL ORDER BY created_at`).all().map((r) => ({
489
+ COALESCE(last_error,'') AS last_error FROM relays WHERE delivered_at IS NULL ORDER BY created_at, rowid`).all().map((r) => ({
478
490
  id: str(r["id"]),
479
491
  workflowId: str(r["workflow_id"]),
480
492
  stepId: str(r["step_id"]),
@@ -515,25 +527,21 @@ var createLedgerStore = (dbPath, deps = {}) => {
515
527
  ).all(status, limit).map(rowToStep);
516
528
  },
517
529
  runningAgentStepForSession(session) {
518
- const r = db.prepare(
519
- `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
530
+ const proj = `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
520
531
  COALESCE(st.title,'') AS title, st.status AS status,
521
532
  COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
522
533
  COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
523
534
  FROM workflow_steps st
524
- JOIN workflows w ON w.id = st.workflow_id
535
+ JOIN workflows w ON w.id = st.workflow_id`;
536
+ const r = db.prepare(
537
+ `${proj}
525
538
  JOIN runs r ON r.workflow_id = st.workflow_id
526
539
  WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND r.session=? AND w.status='running'
527
540
  ORDER BY st.updated_at DESC LIMIT 1`
528
541
  ).get(session);
529
542
  if (r) return rowToStep(r);
530
543
  const r2 = db.prepare(
531
- `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
532
- COALESCE(st.title,'') AS title, st.status AS status,
533
- COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
534
- COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
535
- FROM workflow_steps st
536
- JOIN workflows w ON w.id = st.workflow_id
544
+ `${proj}
537
545
  WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND w.status='running'
538
546
  AND (w.session=? OR COALESCE(json_extract(w.meta,'$.assigned_session'),'')=?)
539
547
  ORDER BY st.updated_at DESC LIMIT 1`
@@ -542,7 +550,8 @@ var createLedgerStore = (dbPath, deps = {}) => {
542
550
  },
543
551
  sessionHasOpenWork(session, excludeWorkflowId) {
544
552
  const anchor = "(session=?1 OR COALESCE(json_extract(meta,'$.assigned_session'),'')=?1 OR id IN (SELECT workflow_id FROM runs WHERE session=?1))";
545
- 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}`;
553
+ 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}`;
546
555
  const row = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
547
556
  return Number(row["n"]) > 0;
548
557
  },
@@ -617,7 +626,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
617
626
  /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
618
627
  * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
619
628
  * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
620
- * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 dedupPattern
629
+ * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
621
630
  * 成对转义(message-id 几乎必含 `_`)。 */
622
631
  recordSentMessageId(messageId, meta) {
623
632
  if (messageId === "") return;
@@ -631,8 +640,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
631
640
  `SELECT 1 FROM events WHERE type = 'email_sent' AND entity_type = 'email' AND entity_id = ? LIMIT 1`
632
641
  ).get(messageId);
633
642
  if (registered !== void 0) return true;
634
- const jsonValue = JSON.stringify(messageId).slice(1, -1);
635
- const pattern = `%"message_id":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
643
+ const pattern = jsonFieldPattern("message_id", messageId);
636
644
  const rows = db.prepare(
637
645
  `SELECT output FROM workflow_steps WHERE kind = 'send' AND status = 'completed'
638
646
  AND updated_at < ? AND output LIKE ? ESCAPE '\\'`
@@ -706,7 +714,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
706
714
  const placeholders = statuses.map(() => "?").join(",");
707
715
  const r = db.prepare(
708
716
  `SELECT id, run_id, content, status, created_at FROM prompts
709
- WHERE run_id=? AND status IN (${placeholders}) ORDER BY created_at DESC LIMIT 1`
717
+ WHERE run_id=? AND status IN (${placeholders}) ORDER BY created_at DESC, rowid DESC LIMIT 1`
710
718
  ).get(run.id, ...statuses);
711
719
  if (!r) return null;
712
720
  return {
@@ -868,18 +876,18 @@ var priceUsage = (rows) => {
868
876
  const key = r.model === "" ? "\uFF08\u672A\u8BB0\u5F55\u6A21\u578B\uFF09" : r.model;
869
877
  let a = accs.get(key);
870
878
  if (a === void 0) {
871
- const p = findPrice(key);
879
+ const p2 = findPrice(key);
872
880
  a = {
873
881
  model: key,
874
- label: p?.label ?? key,
882
+ label: p2?.label ?? key,
875
883
  free: false,
876
- priced: p !== null,
877
884
  calls: 0,
878
885
  inputTokens: 0,
879
886
  cachedTokens: 0,
880
887
  outputTokens: 0,
881
888
  tiersHit: /* @__PURE__ */ new Set(),
882
- cost: { cache: 0, input: 0, output: 0 }
889
+ cost: { cache: 0, input: 0, output: 0 },
890
+ price: p2
883
891
  };
884
892
  accs.set(key, a);
885
893
  }
@@ -887,8 +895,9 @@ var priceUsage = (rows) => {
887
895
  a.inputTokens += r.inputTokens;
888
896
  a.cachedTokens += r.cachedTokens;
889
897
  a.outputTokens += r.outputTokens;
890
- if (!a.priced) continue;
891
- const t = tierOf(findPrice(key), r);
898
+ const p = a.price;
899
+ if (p === null) continue;
900
+ const t = tierOf(p, r);
892
901
  a.tiersHit.add(t.note);
893
902
  if (t.input === 0 && t.output === 0) {
894
903
  a.free = true;
@@ -904,7 +913,7 @@ var priceUsage = (rows) => {
904
913
  model: a.model,
905
914
  label: a.label,
906
915
  free: a.free,
907
- priced: a.priced,
916
+ priced: a.price !== null,
908
917
  calls: a.calls,
909
918
  inputTokens: a.inputTokens,
910
919
  cachedTokens: a.cachedTokens,
@@ -968,11 +977,15 @@ CREATE TABLE IF NOT EXISTS usage_meta (
968
977
  );
969
978
  `;
970
979
  var n = (v) => typeof v === "number" ? v : Number(v ?? 0);
971
- var windowUsageRows = (db, tmuxSession, windows) => {
972
- if (windows.length === 0) return [];
980
+ var windowConds = (tmuxSession, windows) => {
973
981
  const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
974
982
  const params = [tmuxSession];
975
983
  for (const [a, b] of windows) params.push(a, b);
984
+ return { conds, params };
985
+ };
986
+ var windowUsageRows = (db, tmuxSession, windows) => {
987
+ if (windows.length === 0) return [];
988
+ const { conds, params } = windowConds(tmuxSession, windows);
976
989
  return db.prepare(
977
990
  `SELECT model, input_tokens AS i, output_tokens AS o, cached_tokens AS k
978
991
  FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
@@ -1064,9 +1077,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1064
1077
  },
1065
1078
  perSessionWindows(tmuxSession, windows) {
1066
1079
  if (windows.length === 0) return { count: 0, inputTokens: 0, outputTokens: 0 };
1067
- const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
1068
- const params = [tmuxSession];
1069
- for (const [a, b] of windows) params.push(a, b);
1080
+ const { conds, params } = windowConds(tmuxSession, windows);
1070
1081
  const r = db.prepare(
1071
1082
  `SELECT COUNT(*) AS c, COALESCE(SUM(input_tokens),0) AS i, COALESCE(SUM(output_tokens),0) AS o
1072
1083
  FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
@@ -1147,9 +1158,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1147
1158
  VALUES (?, ?, '', '') ON CONFLICT(claude_session_id) DO NOTHING`).run(claudeSessionId, tmuxSession);
1148
1159
  },
1149
1160
  ingestProjectTranscripts(claudeProjectsDir) {
1150
- const mapped = new Set(
1151
- db.prepare("SELECT claude_session_id FROM claude_sessions").all().map((r) => String(r["claude_session_id"]))
1152
- );
1153
1161
  let count = 0;
1154
1162
  try {
1155
1163
  const projects = readdirSync(claudeProjectsDir);
@@ -1201,27 +1209,34 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1201
1209
  if (lastNL < 0) return 0;
1202
1210
  const complete = text.slice(0, lastNL + 1);
1203
1211
  let count = 0;
1204
- for (const line of complete.split("\n")) {
1205
- if (line === "") continue;
1206
- const rec = parseAssistantLine(line);
1207
- if (!rec) continue;
1212
+ const newOffset = offset + Buffer.byteLength(complete, "utf8");
1213
+ db.exec("BEGIN");
1214
+ try {
1208
1215
  const sess = tmuxOf(db, claudeId);
1209
- db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
1210
- VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
1211
- `turn=${String(rec.callContext).slice(0, 40)}`,
1212
- rec.model,
1213
- rec.inputTokens,
1214
- rec.outputTokens,
1215
- rec.cacheRead,
1216
- rec.timestamp,
1217
- sess
1218
- );
1219
- count++;
1216
+ for (const line of complete.split("\n")) {
1217
+ if (line === "") continue;
1218
+ const rec = parseAssistantLine(line);
1219
+ if (!rec) continue;
1220
+ db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
1221
+ VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
1222
+ `turn=${String(rec.callContext).slice(0, 40)}`,
1223
+ rec.model,
1224
+ rec.inputTokens,
1225
+ rec.outputTokens,
1226
+ rec.cacheRead,
1227
+ rec.timestamp,
1228
+ sess
1229
+ );
1230
+ count++;
1231
+ }
1232
+ db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
1233
+ 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());
1235
+ db.exec("COMMIT");
1236
+ } catch (e) {
1237
+ db.exec("ROLLBACK");
1238
+ throw e;
1220
1239
  }
1221
- const newOffset = offset + Buffer.byteLength(complete, "utf8");
1222
- db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
1223
- VALUES (?, ?, ?)
1224
- 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());
1225
1240
  return count;
1226
1241
  } finally {
1227
1242
  closeSync(fd);
@@ -182,10 +182,12 @@ 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);
190
+ var SEQ_PARK_BASE = 1e9;
189
191
  var SEND_STEP_LIKE_CUTOFF = "2026-09-19";
190
192
  var rowToWorkflow = (r) => ({
191
193
  id: str(r["id"]),
@@ -225,15 +227,23 @@ var rowToEvent = (r) => ({
225
227
  });
226
228
  var pruneImpl = (db, retentionDays, nowIso) => {
227
229
  if (retentionDays <= 0) return { events: 0, workflows: 0 };
228
- const cutoff = new Date(new Date(nowIso).getTime() - retentionDays * 864e5).toISOString();
230
+ const cutoff = rfc3339(new Date(new Date(nowIso).getTime() - retentionDays * 864e5));
229
231
  const ev = db.prepare("DELETE FROM events WHERE ts < ?").run(cutoff);
230
232
  const wfRows = db.prepare(
231
233
  `SELECT id FROM workflows WHERE status IN (${sqlInList(PRUNE_WORKFLOW_STATUSES)}) AND updated_at < ?`
232
234
  ).all(cutoff);
233
235
  let wfCount = 0;
234
236
  for (const r of wfRows) {
235
- db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
236
- db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
237
+ db.exec("BEGIN");
238
+ try {
239
+ db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
240
+ db.prepare("DELETE FROM relays WHERE workflow_id=?").run(r.id);
241
+ db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
242
+ db.exec("COMMIT");
243
+ } catch (e) {
244
+ db.exec("ROLLBACK");
245
+ throw e;
246
+ }
237
247
  wfCount++;
238
248
  }
239
249
  return { events: Number(ev.changes), workflows: wfCount };
@@ -278,7 +288,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
278
288
  r.status AS status, r.created_at AS created_at FROM runs r
279
289
  JOIN workflows w ON w.id = r.workflow_id
280
290
  WHERE r.session=? AND r.status IN ('running','idle') AND w.status='running'
281
- ORDER BY r.created_at DESC LIMIT 1`
291
+ ORDER BY r.created_at DESC, r.rowid DESC LIMIT 1`
282
292
  ).get(session);
283
293
  if (!r) return null;
284
294
  return {
@@ -301,8 +311,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
301
311
  return getWorkflowById(id);
302
312
  },
303
313
  getWorkflow(id) {
304
- const r = db.prepare("SELECT * FROM workflows WHERE id=?").get(id);
305
- return r ? rowToWorkflow(r) : null;
314
+ return getWorkflowById(id);
306
315
  },
307
316
  listWorkflows(limit = 100, status) {
308
317
  if (status !== void 0 && status !== "") {
@@ -377,12 +386,16 @@ var createLedgerStore = (dbPath, deps = {}) => {
377
386
  `INSERT INTO workflow_steps (id, workflow_id, seq, kind, title, status, input, updated_at)
378
387
  VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)`
379
388
  );
380
- const shift = db.prepare(
381
- "UPDATE workflow_steps SET seq = seq + ? WHERE workflow_id = ? AND seq >= ?"
389
+ const park = db.prepare(
390
+ `UPDATE workflow_steps SET seq = seq + ${SEQ_PARK_BASE} WHERE workflow_id = ? AND seq >= ?`
391
+ );
392
+ const restore = db.prepare(
393
+ `UPDATE workflow_steps SET seq = seq - ${SEQ_PARK_BASE} + ? WHERE workflow_id = ? AND seq >= ${SEQ_PARK_BASE}`
382
394
  );
383
395
  db.exec("BEGIN");
384
396
  try {
385
- shift.run(steps.length, workflowId, afterSeq + 1);
397
+ park.run(workflowId, afterSeq + 1);
398
+ restore.run(steps.length, workflowId);
386
399
  steps.forEach((st, i) => {
387
400
  insert.run(
388
401
  ids.newId("st"),
@@ -401,8 +414,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
401
414
  }
402
415
  },
403
416
  getStep(id) {
404
- const r = db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE id=?`).get(id);
405
- return r ? rowToStep(r) : null;
417
+ return getStepById(id);
406
418
  },
407
419
  stepsForWorkflow(id) {
408
420
  return db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE workflow_id=? ORDER BY seq`).all(id).map(rowToStep);
@@ -432,7 +444,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
432
444
  },
433
445
  pendingRelays() {
434
446
  return db.prepare(`SELECT id, workflow_id, step_id, session, text, marker, created_at, attempts,
435
- COALESCE(last_error,'') AS last_error FROM relays WHERE delivered_at IS NULL ORDER BY created_at`).all().map((r) => ({
447
+ COALESCE(last_error,'') AS last_error FROM relays WHERE delivered_at IS NULL ORDER BY created_at, rowid`).all().map((r) => ({
436
448
  id: str(r["id"]),
437
449
  workflowId: str(r["workflow_id"]),
438
450
  stepId: str(r["step_id"]),
@@ -473,25 +485,21 @@ var createLedgerStore = (dbPath, deps = {}) => {
473
485
  ).all(status, limit).map(rowToStep);
474
486
  },
475
487
  runningAgentStepForSession(session) {
476
- const r = db.prepare(
477
- `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
488
+ const proj = `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
478
489
  COALESCE(st.title,'') AS title, st.status AS status,
479
490
  COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
480
491
  COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
481
492
  FROM workflow_steps st
482
- JOIN workflows w ON w.id = st.workflow_id
493
+ JOIN workflows w ON w.id = st.workflow_id`;
494
+ const r = db.prepare(
495
+ `${proj}
483
496
  JOIN runs r ON r.workflow_id = st.workflow_id
484
497
  WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND r.session=? AND w.status='running'
485
498
  ORDER BY st.updated_at DESC LIMIT 1`
486
499
  ).get(session);
487
500
  if (r) return rowToStep(r);
488
501
  const r2 = db.prepare(
489
- `SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
490
- COALESCE(st.title,'') AS title, st.status AS status,
491
- COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
492
- COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
493
- FROM workflow_steps st
494
- JOIN workflows w ON w.id = st.workflow_id
502
+ `${proj}
495
503
  WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND w.status='running'
496
504
  AND (w.session=? OR COALESCE(json_extract(w.meta,'$.assigned_session'),'')=?)
497
505
  ORDER BY st.updated_at DESC LIMIT 1`
@@ -500,7 +508,8 @@ var createLedgerStore = (dbPath, deps = {}) => {
500
508
  },
501
509
  sessionHasOpenWork(session, excludeWorkflowId) {
502
510
  const anchor = "(session=?1 OR COALESCE(json_extract(meta,'$.assigned_session'),'')=?1 OR id IN (SELECT workflow_id FROM runs WHERE session=?1))";
503
- 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}`;
511
+ 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}`;
504
513
  const row = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
505
514
  return Number(row["n"]) > 0;
506
515
  },
@@ -575,7 +584,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
575
584
  /** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
576
585
  * 发送记录——completed send 步的 output.message_id(email_sent 事件同源
577
586
  * 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
578
- * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 dedupPattern
587
+ * LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
579
588
  * 成对转义(message-id 几乎必含 `_`)。 */
580
589
  recordSentMessageId(messageId, meta) {
581
590
  if (messageId === "") return;
@@ -589,8 +598,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
589
598
  `SELECT 1 FROM events WHERE type = 'email_sent' AND entity_type = 'email' AND entity_id = ? LIMIT 1`
590
599
  ).get(messageId);
591
600
  if (registered !== void 0) return true;
592
- const jsonValue = JSON.stringify(messageId).slice(1, -1);
593
- const pattern = `%"message_id":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
601
+ const pattern = jsonFieldPattern("message_id", messageId);
594
602
  const rows = db.prepare(
595
603
  `SELECT output FROM workflow_steps WHERE kind = 'send' AND status = 'completed'
596
604
  AND updated_at < ? AND output LIKE ? ESCAPE '\\'`
@@ -664,7 +672,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
664
672
  const placeholders = statuses.map(() => "?").join(",");
665
673
  const r = db.prepare(
666
674
  `SELECT id, run_id, content, status, created_at FROM prompts
667
- WHERE run_id=? AND status IN (${placeholders}) ORDER BY created_at DESC LIMIT 1`
675
+ WHERE run_id=? AND status IN (${placeholders}) ORDER BY created_at DESC, rowid DESC LIMIT 1`
668
676
  ).get(run.id, ...statuses);
669
677
  if (!r) return null;
670
678
  return {
@@ -826,18 +834,18 @@ var priceUsage = (rows) => {
826
834
  const key = r.model === "" ? "\uFF08\u672A\u8BB0\u5F55\u6A21\u578B\uFF09" : r.model;
827
835
  let a = accs.get(key);
828
836
  if (a === void 0) {
829
- const p = findPrice(key);
837
+ const p2 = findPrice(key);
830
838
  a = {
831
839
  model: key,
832
- label: p?.label ?? key,
840
+ label: p2?.label ?? key,
833
841
  free: false,
834
- priced: p !== null,
835
842
  calls: 0,
836
843
  inputTokens: 0,
837
844
  cachedTokens: 0,
838
845
  outputTokens: 0,
839
846
  tiersHit: /* @__PURE__ */ new Set(),
840
- cost: { cache: 0, input: 0, output: 0 }
847
+ cost: { cache: 0, input: 0, output: 0 },
848
+ price: p2
841
849
  };
842
850
  accs.set(key, a);
843
851
  }
@@ -845,8 +853,9 @@ var priceUsage = (rows) => {
845
853
  a.inputTokens += r.inputTokens;
846
854
  a.cachedTokens += r.cachedTokens;
847
855
  a.outputTokens += r.outputTokens;
848
- if (!a.priced) continue;
849
- const t = tierOf(findPrice(key), r);
856
+ const p = a.price;
857
+ if (p === null) continue;
858
+ const t = tierOf(p, r);
850
859
  a.tiersHit.add(t.note);
851
860
  if (t.input === 0 && t.output === 0) {
852
861
  a.free = true;
@@ -862,7 +871,7 @@ var priceUsage = (rows) => {
862
871
  model: a.model,
863
872
  label: a.label,
864
873
  free: a.free,
865
- priced: a.priced,
874
+ priced: a.price !== null,
866
875
  calls: a.calls,
867
876
  inputTokens: a.inputTokens,
868
877
  cachedTokens: a.cachedTokens,
@@ -926,11 +935,15 @@ CREATE TABLE IF NOT EXISTS usage_meta (
926
935
  );
927
936
  `;
928
937
  var n = (v) => typeof v === "number" ? v : Number(v ?? 0);
929
- var windowUsageRows = (db, tmuxSession, windows) => {
930
- if (windows.length === 0) return [];
938
+ var windowConds = (tmuxSession, windows) => {
931
939
  const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
932
940
  const params = [tmuxSession];
933
941
  for (const [a, b] of windows) params.push(a, b);
942
+ return { conds, params };
943
+ };
944
+ var windowUsageRows = (db, tmuxSession, windows) => {
945
+ if (windows.length === 0) return [];
946
+ const { conds, params } = windowConds(tmuxSession, windows);
934
947
  return db.prepare(
935
948
  `SELECT model, input_tokens AS i, output_tokens AS o, cached_tokens AS k
936
949
  FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
@@ -1022,9 +1035,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1022
1035
  },
1023
1036
  perSessionWindows(tmuxSession, windows) {
1024
1037
  if (windows.length === 0) return { count: 0, inputTokens: 0, outputTokens: 0 };
1025
- const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
1026
- const params = [tmuxSession];
1027
- for (const [a, b] of windows) params.push(a, b);
1038
+ const { conds, params } = windowConds(tmuxSession, windows);
1028
1039
  const r = db.prepare(
1029
1040
  `SELECT COUNT(*) AS c, COALESCE(SUM(input_tokens),0) AS i, COALESCE(SUM(output_tokens),0) AS o
1030
1041
  FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
@@ -1105,9 +1116,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
1105
1116
  VALUES (?, ?, '', '') ON CONFLICT(claude_session_id) DO NOTHING`).run(claudeSessionId, tmuxSession);
1106
1117
  },
1107
1118
  ingestProjectTranscripts(claudeProjectsDir) {
1108
- const mapped = new Set(
1109
- db.prepare("SELECT claude_session_id FROM claude_sessions").all().map((r) => String(r["claude_session_id"]))
1110
- );
1111
1119
  let count = 0;
1112
1120
  try {
1113
1121
  const projects = readdirSync(claudeProjectsDir);
@@ -1159,27 +1167,34 @@ var ingestFile = (db, claudeId, transcriptPath) => {
1159
1167
  if (lastNL < 0) return 0;
1160
1168
  const complete = text.slice(0, lastNL + 1);
1161
1169
  let count = 0;
1162
- for (const line of complete.split("\n")) {
1163
- if (line === "") continue;
1164
- const rec = parseAssistantLine(line);
1165
- if (!rec) continue;
1170
+ const newOffset = offset + Buffer.byteLength(complete, "utf8");
1171
+ db.exec("BEGIN");
1172
+ try {
1166
1173
  const sess = tmuxOf(db, claudeId);
1167
- db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
1168
- VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
1169
- `turn=${String(rec.callContext).slice(0, 40)}`,
1170
- rec.model,
1171
- rec.inputTokens,
1172
- rec.outputTokens,
1173
- rec.cacheRead,
1174
- rec.timestamp,
1175
- sess
1176
- );
1177
- count++;
1174
+ for (const line of complete.split("\n")) {
1175
+ if (line === "") continue;
1176
+ const rec = parseAssistantLine(line);
1177
+ if (!rec) continue;
1178
+ db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
1179
+ VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
1180
+ `turn=${String(rec.callContext).slice(0, 40)}`,
1181
+ rec.model,
1182
+ rec.inputTokens,
1183
+ rec.outputTokens,
1184
+ rec.cacheRead,
1185
+ rec.timestamp,
1186
+ sess
1187
+ );
1188
+ count++;
1189
+ }
1190
+ db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
1191
+ 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());
1193
+ db.exec("COMMIT");
1194
+ } catch (e) {
1195
+ db.exec("ROLLBACK");
1196
+ throw e;
1178
1197
  }
1179
- const newOffset = offset + Buffer.byteLength(complete, "utf8");
1180
- db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
1181
- VALUES (?, ?, ?)
1182
- 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());
1183
1198
  return count;
1184
1199
  } finally {
1185
1200
  closeSync(fd);
@@ -1220,7 +1235,7 @@ function demo() {
1220
1235
  const w1 = store.createWorkflow({ kind: "case", title: "Client inquiry: website redesign", meta: { from: "client@acme.com", message_id: "demo-001" } });
1221
1236
  store.createStep({ workflowId: w1.id, seq: 1, kind: "agent", title: "Understand the request" });
1222
1237
  store.createStep({ workflowId: w1.id, seq: 2, kind: "agent", title: "Draft a proposal" });
1223
- store.setWorkflowStatus(w1.id, "awaiting_approval");
1238
+ store.setWorkflowStatus(w1.id, "running");
1224
1239
  const w2 = store.createWorkflow({ kind: "case", title: "Monthly report automation", meta: { from: "ops@acme.com" } });
1225
1240
  store.setWorkflowStatus(w2.id, "completed");
1226
1241
  const cf = { ...emptyCaseFile(/* @__PURE__ */ new Date()), original: { from: "client@acme.com", subject: "Website redesign" } };
@@ -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.4",
3
+ "version": "0.1.6",
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",