@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 +78 -63
- package/dist/juli-ledger.js +79 -64
- package/dist/platform/shared/json.d.ts +0 -2
- package/package.json +1 -1
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
|
|
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);
|
|
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)
|
|
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.
|
|
278
|
-
|
|
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
|
-
|
|
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
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 编码取实际存储字节,%/_/\ 照
|
|
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
|
|
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
|
|
879
|
+
const p2 = findPrice(key);
|
|
872
880
|
a = {
|
|
873
881
|
model: key,
|
|
874
|
-
label:
|
|
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
|
-
|
|
891
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
rec
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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);
|
package/dist/juli-ledger.js
CHANGED
|
@@ -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
|
|
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);
|
|
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)
|
|
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.
|
|
236
|
-
|
|
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
|
-
|
|
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
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 编码取实际存储字节,%/_/\ 照
|
|
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
|
|
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
|
|
837
|
+
const p2 = findPrice(key);
|
|
830
838
|
a = {
|
|
831
839
|
model: key,
|
|
832
|
-
label:
|
|
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
|
-
|
|
849
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
rec
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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, "
|
|
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.
|
|
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",
|