@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 +1 -1
- package/dist/index.js +73 -64
- package/dist/juli-ledger.js +73 -64
- package/dist/platform/shared/json.d.ts +0 -2
- package/package.json +1 -1
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
|
|
228
|
-
const jsonValue = JSON.stringify(
|
|
229
|
-
return `%"
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
559
|
-
const
|
|
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,
|
|
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:
|
|
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
|
-
/**
|
|
631
|
-
*
|
|
632
|
-
*
|
|
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
|
|
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,
|
|
662
|
-
return { id, runId, content, status: "submitted", createdAt:
|
|
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,
|
|
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:
|
|
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
|
|
887
|
+
const p2 = findPrice(key);
|
|
885
888
|
a = {
|
|
886
889
|
model: key,
|
|
887
|
-
label:
|
|
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
|
-
|
|
904
|
-
|
|
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.
|
|
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
|
|
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
|
|
1047
|
-
totalInputTokens: n(total
|
|
1048
|
-
totalOutputTokens: n(total
|
|
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
|
|
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("
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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, (
|
|
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 : (
|
|
1274
|
+
timestamp: typeof v.timestamp === "string" ? v.timestamp : now().toISOString(),
|
|
1266
1275
|
callContext: v.uuid ?? ""
|
|
1267
1276
|
};
|
|
1268
1277
|
} catch {
|
package/dist/juli-ledger.js
CHANGED
|
@@ -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
|
|
186
|
-
const jsonValue = JSON.stringify(
|
|
187
|
-
return `%"
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
517
|
-
const
|
|
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,
|
|
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:
|
|
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
|
-
/**
|
|
589
|
-
*
|
|
590
|
-
*
|
|
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
|
|
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,
|
|
620
|
-
return { id, runId, content, status: "submitted", createdAt:
|
|
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,
|
|
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:
|
|
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
|
|
845
|
+
const p2 = findPrice(key);
|
|
843
846
|
a = {
|
|
844
847
|
model: key,
|
|
845
|
-
label:
|
|
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
|
-
|
|
862
|
-
|
|
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.
|
|
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
|
|
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
|
|
1005
|
-
totalInputTokens: n(total
|
|
1006
|
-
totalOutputTokens: n(total
|
|
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
|
|
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("
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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, (
|
|
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 : (
|
|
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.
|
|
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",
|