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