@openagentsy/ledger 0.0.0-stage → 0.2.1

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.
Files changed (86) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +20 -2
  3. package/data/pricing/litellm.json +8882 -0
  4. package/data/pricing/models.dev.json +12151 -0
  5. package/data/pricing/openrouter.json +1017 -0
  6. package/dist/db.d.ts +44 -0
  7. package/dist/db.d.ts.map +1 -0
  8. package/dist/db.js +97 -0
  9. package/dist/db.js.map +1 -0
  10. package/dist/export.d.ts +71 -0
  11. package/dist/export.d.ts.map +1 -0
  12. package/dist/export.js +78 -0
  13. package/dist/export.js.map +1 -0
  14. package/dist/footprint.d.ts +24 -0
  15. package/dist/footprint.d.ts.map +1 -0
  16. package/dist/footprint.js +33 -0
  17. package/dist/footprint.js.map +1 -0
  18. package/dist/git.d.ts +44 -0
  19. package/dist/git.d.ts.map +1 -0
  20. package/dist/git.js +126 -0
  21. package/dist/git.js.map +1 -0
  22. package/dist/git.test.d.ts +2 -0
  23. package/dist/git.test.d.ts.map +1 -0
  24. package/dist/git.test.js +101 -0
  25. package/dist/git.test.js.map +1 -0
  26. package/dist/index.d.ts +19 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +19 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/ingest.d.ts +133 -0
  31. package/dist/ingest.d.ts.map +1 -0
  32. package/dist/ingest.js +324 -0
  33. package/dist/ingest.js.map +1 -0
  34. package/dist/ingest.test.d.ts +2 -0
  35. package/dist/ingest.test.d.ts.map +1 -0
  36. package/dist/ingest.test.js +245 -0
  37. package/dist/ingest.test.js.map +1 -0
  38. package/dist/outcomes.d.ts +81 -0
  39. package/dist/outcomes.d.ts.map +1 -0
  40. package/dist/outcomes.js +85 -0
  41. package/dist/outcomes.js.map +1 -0
  42. package/dist/outcomes.test.d.ts +2 -0
  43. package/dist/outcomes.test.d.ts.map +1 -0
  44. package/dist/outcomes.test.js +160 -0
  45. package/dist/outcomes.test.js.map +1 -0
  46. package/dist/policy.d.ts +48 -0
  47. package/dist/policy.d.ts.map +1 -0
  48. package/dist/policy.js +99 -0
  49. package/dist/policy.js.map +1 -0
  50. package/dist/policy.test.d.ts +2 -0
  51. package/dist/policy.test.d.ts.map +1 -0
  52. package/dist/policy.test.js +126 -0
  53. package/dist/policy.test.js.map +1 -0
  54. package/dist/pricing.d.ts +92 -0
  55. package/dist/pricing.d.ts.map +1 -0
  56. package/dist/pricing.js +341 -0
  57. package/dist/pricing.js.map +1 -0
  58. package/dist/pricing.test.d.ts +2 -0
  59. package/dist/pricing.test.d.ts.map +1 -0
  60. package/dist/pricing.test.js +167 -0
  61. package/dist/pricing.test.js.map +1 -0
  62. package/dist/query.d.ts +134 -0
  63. package/dist/query.d.ts.map +1 -0
  64. package/dist/query.js +247 -0
  65. package/dist/query.js.map +1 -0
  66. package/dist/query.test.d.ts +2 -0
  67. package/dist/query.test.d.ts.map +1 -0
  68. package/dist/query.test.js +242 -0
  69. package/dist/query.test.js.map +1 -0
  70. package/dist/schema.d.ts +33 -0
  71. package/dist/schema.d.ts.map +1 -0
  72. package/dist/schema.js +139 -0
  73. package/dist/schema.js.map +1 -0
  74. package/dist/schema.test.d.ts +2 -0
  75. package/dist/schema.test.d.ts.map +1 -0
  76. package/dist/schema.test.js +129 -0
  77. package/dist/schema.test.js.map +1 -0
  78. package/dist/waste.d.ts +116 -0
  79. package/dist/waste.d.ts.map +1 -0
  80. package/dist/waste.js +553 -0
  81. package/dist/waste.js.map +1 -0
  82. package/dist/waste.test.d.ts +2 -0
  83. package/dist/waste.test.d.ts.map +1 -0
  84. package/dist/waste.test.js +278 -0
  85. package/dist/waste.test.js.map +1 -0
  86. package/package.json +52 -4
@@ -0,0 +1,33 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ /**
3
+ * The ledger's schema.
4
+ *
5
+ * Numbers only: token counts, model ids, timestamps, task ids and commit
6
+ * shas. A prompt, a completion or a line of code never reaches this
7
+ * database — the transcripts stay in OpenCode's own storage, and the ledger
8
+ * only ever holds the counters.
9
+ */
10
+ /** The role keys the resolver hands the ledger (§6.1). */
11
+ export declare const ROLE_KEYS: readonly ["lead", "scout", "researcher", "builder", "reviewer", "advisor", "archivist", "offload"];
12
+ export type RoleKey = (typeof ROLE_KEYS)[number];
13
+ /** The waste signals of §6.4. */
14
+ export declare const WASTE_KINDS: readonly ["retry", "tool_error_loop", "interrupted", "undo", "reverted_code", "cache_miss", "no_progress"];
15
+ export type WasteKind = (typeof WASTE_KINDS)[number];
16
+ /** The outcome kinds of §6.5. */
17
+ export declare const OUTCOME_KINDS: readonly ["task_completed", "task_scrapped", "commit", "pr_merged"];
18
+ export type OutcomeKind = (typeof OUTCOME_KINDS)[number];
19
+ /**
20
+ * The tables.
21
+ *
22
+ * `usage` carries the resolved role: the `role` column is the role key the
23
+ * adapter resolved, and `role_raw` holds the unresolved agent id of a
24
+ * backfilled row, which no per-role report counts.
25
+ *
26
+ * The two REV3 tables are schema only for now: their writers land in 2f.
27
+ * `policy_firing` carries the audit columns of REV3 §21.3 next to the
28
+ * classification and the detail, so the trace survives.
29
+ */
30
+ export declare const SCHEMA_SQL: string;
31
+ /** Bring the database up to the schema. Opening twice changes nothing. */
32
+ export declare function applySchema(db: Database): void;
33
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AAEH,0DAA0D;AAC1D,eAAO,MAAM,SAAS,oGASZ,CAAC;AAEX,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD,iCAAiC;AACjC,eAAO,MAAM,WAAW,4GAQd,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,iCAAiC;AACjC,eAAO,MAAM,aAAa,qEAAsE,CAAC;AAEjG,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAOzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,QAkEtB,CAAC;AAuBF,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAG9C"}
package/dist/schema.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The ledger's schema.
3
+ *
4
+ * Numbers only: token counts, model ids, timestamps, task ids and commit
5
+ * shas. A prompt, a completion or a line of code never reaches this
6
+ * database — the transcripts stay in OpenCode's own storage, and the ledger
7
+ * only ever holds the counters.
8
+ */
9
+ /** The role keys the resolver hands the ledger (§6.1). */
10
+ export const ROLE_KEYS = [
11
+ 'lead',
12
+ 'scout',
13
+ 'researcher',
14
+ 'builder',
15
+ 'reviewer',
16
+ 'advisor',
17
+ 'archivist',
18
+ 'offload',
19
+ ];
20
+ /** The waste signals of §6.4. */
21
+ export const WASTE_KINDS = [
22
+ 'retry',
23
+ 'tool_error_loop',
24
+ 'interrupted',
25
+ 'undo',
26
+ 'reverted_code',
27
+ 'cache_miss',
28
+ 'no_progress',
29
+ ];
30
+ /** The outcome kinds of §6.5. */
31
+ export const OUTCOME_KINDS = ['task_completed', 'task_scrapped', 'commit', 'pr_merged'];
32
+ /** The list a CHECK constraint takes. */
33
+ function kindList(kinds) {
34
+ return kinds.map((kind) => `'${kind}'`).join(',');
35
+ }
36
+ /**
37
+ * The tables.
38
+ *
39
+ * `usage` carries the resolved role: the `role` column is the role key the
40
+ * adapter resolved, and `role_raw` holds the unresolved agent id of a
41
+ * backfilled row, which no per-role report counts.
42
+ *
43
+ * The two REV3 tables are schema only for now: their writers land in 2f.
44
+ * `policy_firing` carries the audit columns of REV3 §21.3 next to the
45
+ * classification and the detail, so the trace survives.
46
+ */
47
+ export const SCHEMA_SQL = `
48
+ CREATE TABLE IF NOT EXISTS usage (
49
+ message_id TEXT PRIMARY KEY, -- dedup key (OpenCode message id)
50
+ session_id TEXT NOT NULL,
51
+ root_session_id TEXT NOT NULL, -- Lead session; subagents roll up here
52
+ role TEXT, -- RESOLVED role key
53
+ role_raw TEXT, -- unresolved agent id, backfilled rows only
54
+ provider TEXT NOT NULL,
55
+ model TEXT NOT NULL,
56
+ input INTEGER NOT NULL DEFAULT 0,
57
+ output INTEGER NOT NULL DEFAULT 0,
58
+ reasoning INTEGER NOT NULL DEFAULT 0,
59
+ cache_read INTEGER NOT NULL DEFAULT 0,
60
+ cache_write INTEGER NOT NULL DEFAULT 0,
61
+ cost_reported REAL, -- from OpenCode message.cost, if present
62
+ cost_estimated REAL, -- from pricing chain; NULL = unpriced (never 0)
63
+ latency_ms INTEGER,
64
+ task_id TEXT, -- in-progress task at the time, if any
65
+ ts TEXT NOT NULL
66
+ );
67
+ CREATE INDEX IF NOT EXISTS usage_root ON usage(root_session_id);
68
+ CREATE INDEX IF NOT EXISTS usage_task ON usage(task_id);
69
+
70
+ CREATE TABLE IF NOT EXISTS waste (
71
+ id INTEGER PRIMARY KEY, root_session_id TEXT, message_id TEXT,
72
+ kind TEXT CHECK (kind IN (${kindList(WASTE_KINDS)})),
73
+ tokens INTEGER NOT NULL, detail TEXT, ts TEXT NOT NULL
74
+ );
75
+
76
+ CREATE TABLE IF NOT EXISTS outcome (
77
+ id INTEGER PRIMARY KEY, root_session_id TEXT, task_id TEXT, commit_sha TEXT,
78
+ kind TEXT CHECK (kind IN (${kindList(OUTCOME_KINDS)})), ts TEXT NOT NULL
79
+ );
80
+
81
+ CREATE TABLE IF NOT EXISTS footprint (
82
+ message_id TEXT PRIMARY KEY REFERENCES usage(message_id),
83
+ energy_lo REAL, energy_hi REAL, water_lo REAL, water_hi REAL,
84
+ co2_lo REAL, co2_hi REAL, method_version TEXT NOT NULL
85
+ );
86
+
87
+ CREATE TABLE IF NOT EXISTS policy_firing (
88
+ id TEXT PRIMARY KEY,
89
+ session_id TEXT NOT NULL,
90
+ policy_id TEXT NOT NULL,
91
+ policy_version TEXT,
92
+ layer INTEGER,
93
+ action TEXT NOT NULL,
94
+ severity TEXT,
95
+ reason TEXT,
96
+ finding_id TEXT,
97
+ classification TEXT,
98
+ detail TEXT,
99
+ origin TEXT, -- 'first-party' | 'external' (§21)
100
+ ts TEXT NOT NULL
101
+ );
102
+ CREATE INDEX IF NOT EXISTS idx_policy_firing_session ON policy_firing(session_id, ts);
103
+ CREATE INDEX IF NOT EXISTS idx_policy_firing_policy ON policy_firing(policy_id, ts);
104
+
105
+ CREATE TABLE IF NOT EXISTS safety_metric (
106
+ id INTEGER PRIMARY KEY,
107
+ ts TEXT NOT NULL,
108
+ session_id TEXT,
109
+ metric TEXT NOT NULL,
110
+ value REAL,
111
+ cohort TEXT
112
+ );
113
+ `;
114
+ /**
115
+ * The columns the policy_firing table gained after it was first written.
116
+ *
117
+ * A ledger that was written before a column existed still has to take the
118
+ * rows of the policies that fire now, so the column is added on open. The
119
+ * check makes it happen once: a column that is there is not added again.
120
+ */
121
+ const POLICY_FIRING_COLUMNS = [
122
+ { name: 'origin', ddl: 'ALTER TABLE policy_firing ADD COLUMN origin TEXT' },
123
+ ];
124
+ /** Add the columns a ledger that was written before does not have. */
125
+ function migratePolicyFiring(db) {
126
+ const columns = db.query('PRAGMA table_info(policy_firing)').all();
127
+ const names = new Set(columns.map((column) => column.name));
128
+ for (const column of POLICY_FIRING_COLUMNS) {
129
+ if (names.has(column.name))
130
+ continue;
131
+ db.exec(column.ddl);
132
+ }
133
+ }
134
+ /** Bring the database up to the schema. Opening twice changes nothing. */
135
+ export function applySchema(db) {
136
+ db.exec(SCHEMA_SQL);
137
+ migratePolicyFiring(db);
138
+ }
139
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,MAAM;IACN,OAAO;IACP,YAAY;IACZ,SAAS;IACT,UAAU;IACV,SAAS;IACT,WAAW;IACX,SAAS;CACD,CAAC;AAIX,iCAAiC;AACjC,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,OAAO;IACP,iBAAiB;IACjB,aAAa;IACb,MAAM;IACN,eAAe;IACf,YAAY;IACZ,aAAa;CACL,CAAC;AAIX,iCAAiC;AACjC,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,gBAAgB,EAAE,eAAe,EAAE,QAAQ,EAAE,WAAW,CAAU,CAAC;AAIjG,yCAAyC;AACzC,SAAS,QAAQ,CAAC,KAAwB;IACxC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;8BAyBI,QAAQ,CAAC,WAAW,CAAC;;;;;;8BAMrB,QAAQ,CAAC,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCpD,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,qBAAqB,GAA6C;IACtE,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,kDAAkD,EAAE;CAC5E,CAAC;AAEF,sEAAsE;AACtE,SAAS,mBAAmB,CAAC,EAAY;IACvC,MAAM,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC,GAAG,EAAwB,CAAC;IACzF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,KAAK,MAAM,MAAM,IAAI,qBAAqB,EAAE,CAAC;QAC3C,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,SAAS;QACrC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,EAAY;IACtC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACpB,mBAAmB,CAAC,EAAE,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schema.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.test.d.ts","sourceRoot":"","sources":["../src/schema.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,129 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { BUSY_RETRIES, closeLedger, isBusyError, openLedger, withBusyRetry } from './db.js';
6
+ import { applySchema } from './schema.js';
7
+ let root;
8
+ beforeEach(async () => {
9
+ root = await mkdtemp(join(tmpdir(), 'openagentsy-ledger-'));
10
+ });
11
+ afterEach(async () => {
12
+ await rm(root, { recursive: true, force: true });
13
+ });
14
+ const INSERT = `INSERT INTO usage (message_id, session_id, root_session_id, provider, model, ts)
15
+ VALUES ('msg_lock','ses','root','p','m','2026-10-26T00:00:00.000Z')`;
16
+ describe('the ledger schema', () => {
17
+ it('holds the six tables of the plan', () => {
18
+ const db = openLedger(root);
19
+ try {
20
+ const tables = db
21
+ .query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
22
+ .all();
23
+ expect(tables.map((row) => row.name).sort()).toEqual([
24
+ 'footprint',
25
+ 'outcome',
26
+ 'policy_firing',
27
+ 'safety_metric',
28
+ 'usage',
29
+ 'waste',
30
+ ]);
31
+ }
32
+ finally {
33
+ closeLedger(db);
34
+ }
35
+ });
36
+ it('is the same after it is applied twice', () => {
37
+ const db = openLedger(root);
38
+ try {
39
+ const read = () => db
40
+ .query("SELECT name, sql FROM sqlite_master WHERE type IN ('table','index') ORDER BY name")
41
+ .all();
42
+ const before = read();
43
+ applySchema(db);
44
+ expect(read()).toEqual(before);
45
+ }
46
+ finally {
47
+ closeLedger(db);
48
+ }
49
+ });
50
+ it('opens in WAL mode with a busy timeout', () => {
51
+ const db = openLedger(root);
52
+ try {
53
+ const journal = db.query('PRAGMA journal_mode').get();
54
+ expect(journal.journal_mode.toLowerCase()).toBe('wal');
55
+ const busy = db.query('PRAGMA busy_timeout').get();
56
+ expect(busy.timeout).toBe(250);
57
+ }
58
+ finally {
59
+ closeLedger(db);
60
+ }
61
+ });
62
+ });
63
+ describe('the busy retry', () => {
64
+ it('tries five times and then gives up', () => {
65
+ let attempts = 0;
66
+ expect(() => withBusyRetry(() => {
67
+ attempts += 1;
68
+ throw new Error('SQLITE_BUSY: database is locked');
69
+ })).toThrow(/SQLITE_BUSY/);
70
+ expect(attempts).toBe(BUSY_RETRIES + 1);
71
+ });
72
+ it('returns the value of the first attempt that works', () => {
73
+ let attempts = 0;
74
+ const value = withBusyRetry(() => {
75
+ attempts += 1;
76
+ if (attempts < 3)
77
+ throw new Error('database is locked');
78
+ return 'written';
79
+ });
80
+ expect(value).toBe('written');
81
+ expect(attempts).toBe(3);
82
+ });
83
+ it('does not retry an error that is not a busy one', () => {
84
+ let attempts = 0;
85
+ expect(() => withBusyRetry(() => {
86
+ attempts += 1;
87
+ throw new Error('no such table: usage');
88
+ })).toThrow(/no such table/);
89
+ expect(attempts).toBe(1);
90
+ });
91
+ it('gives up on a locked database and writes once the lock is gone', () => {
92
+ const writer = openLedger(root);
93
+ const other = openLedger(root);
94
+ try {
95
+ other.exec('PRAGMA busy_timeout = 0');
96
+ writer.exec('BEGIN EXCLUSIVE');
97
+ // The error has to be the busy one, and the loop has to have tried
98
+ // again: a retry that never retries would pass a plain toThrow.
99
+ let caught;
100
+ let attempts = 0;
101
+ try {
102
+ withBusyRetry(() => {
103
+ attempts += 1;
104
+ return other.query(INSERT).run();
105
+ });
106
+ }
107
+ catch (error) {
108
+ caught = error;
109
+ }
110
+ expect(caught).toBeDefined();
111
+ expect(isBusyError(caught)).toBe(true);
112
+ expect(String(caught.message)).toMatch(/locked|SQLITE_BUSY/);
113
+ expect(attempts).toBeGreaterThan(1);
114
+ expect(attempts).toBe(BUSY_RETRIES + 1);
115
+ writer.exec('COMMIT');
116
+ let written = 0;
117
+ expect(() => withBusyRetry(() => {
118
+ written += 1;
119
+ return other.query(INSERT).run();
120
+ })).not.toThrow();
121
+ expect(written).toBe(1);
122
+ }
123
+ finally {
124
+ closeLedger(writer);
125
+ closeLedger(other);
126
+ }
127
+ });
128
+ });
129
+ //# sourceMappingURL=schema.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.test.js","sourceRoot":"","sources":["../src/schema.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import type { WasteKind } from './schema.js';
3
+ /**
4
+ * The waste signals of §6.4 — and who finds them.
5
+ *
6
+ * The seven kinds of §6.4 split in two, and the split is by what the
7
+ * detector needs to see:
8
+ *
9
+ * - `cache_miss`, `no_progress` and `reverted_code` are computable from what
10
+ * is already on disk — OpenCode's own storage, the board's records and the
11
+ * repository — so this module finds them. They need no live event.
12
+ * - `retry`, `tool_error_loop`, `interrupted` and `undo` are properties of a
13
+ * tool call while it happens: they need the event bridge of the plugin, so
14
+ * they are recorded by the bridge through the same `recordWaste` this
15
+ * module writes with. `HOOK_WASTE_KINDS` names them, so the two halves can
16
+ * be counted apart.
17
+ *
18
+ * The full list of the seven is `WASTE_KINDS` in the schema, and the CHECK
19
+ * constraint on the `waste` table holds all of them.
20
+ */
21
+ /** The kinds this module can find on its own. */
22
+ export declare const DETECTED_WASTE_KINDS: readonly ["cache_miss", "no_progress", "reverted_code"];
23
+ /** The kinds the event bridge finds, because they live in the tool call. */
24
+ export declare const HOOK_WASTE_KINDS: readonly ["retry", "tool_error_loop", "interrupted", "undo"];
25
+ /** The ratio below which a turn counts as a cache miss (§6.4). */
26
+ export declare const CACHE_MISS_RATIO = 0.5;
27
+ /** How long a revert may take before it is not a revert any more (§6.4). */
28
+ export declare const REVERT_WINDOW_DAYS = 14;
29
+ /** A waste signal, ready to be written. */
30
+ export type WasteCandidate = {
31
+ kind: WasteKind;
32
+ tokens: number;
33
+ ts: string;
34
+ rootSessionId?: string | undefined;
35
+ messageId?: string | undefined;
36
+ detail?: string | undefined;
37
+ };
38
+ /**
39
+ * Write the waste signals, and skip the ones that are already there.
40
+ *
41
+ * A detector is a command: it runs again and again over the same history,
42
+ * so it must not count the same turn twice. The dedup key is the kind and
43
+ * the message for a message-bound signal, and the kind, the time and the
44
+ * detail for one that has no message.
45
+ */
46
+ export declare function recordWasteRows(db: Database, rows: WasteCandidate[]): number;
47
+ /** What the cache-miss detector needs to know. */
48
+ export type CacheMissOptions = {
49
+ /** Where OpenCode keeps its storage. */
50
+ dataDir?: string | undefined;
51
+ /** Only the turns that happened after this instant. */
52
+ since?: string | undefined;
53
+ /** The sessions to read; by default the sessions the ledger knows. */
54
+ sessions?: string[] | undefined;
55
+ /** The ratio below which a turn counts as a miss. */
56
+ threshold?: number | undefined;
57
+ };
58
+ /**
59
+ * The cache misses of §6.4.
60
+ *
61
+ * A turn with no new user parts since the previous assistant message is a
62
+ * retry or a continuation: the prefix was unchanged by definition, so a low
63
+ * cache ratio is the model's problem and not the user's. The detection reads
64
+ * the message list of a session: the pair is the previous assistant message
65
+ * and the one after it, and a user message between them ends the pair. A
66
+ * user message that carried no text part does not end it — nothing new came
67
+ * from the user — but only when the storage records parts at all.
68
+ *
69
+ * The tokens counted are the uncached input of the turn — the part of the
70
+ * prefix the model had to read again.
71
+ */
72
+ export declare function detectCacheMiss(db: Database, options?: CacheMissOptions): WasteCandidate[];
73
+ /** What the no-progress detector needs to know. */
74
+ export type NoProgressOptions = {
75
+ /** The agent id the Lead writes its records under. */
76
+ leadAgent?: string | undefined;
77
+ /** The role key the Lead's usage rows carry. */
78
+ leadRole?: string | undefined;
79
+ /** Only the rounds that closed after this instant. */
80
+ since?: string | undefined;
81
+ };
82
+ /**
83
+ * The rounds of the Lead that went nowhere (§6.4, §8.7).
84
+ *
85
+ * A round that dispatched nothing, consumed no results and changed nothing
86
+ * on the board cannot claim progress, so it is written down as waste. The
87
+ * tokens counted are the round's tokens — the Lead's own.
88
+ */
89
+ export declare function detectNoProgress(db: Database, boardDb: Database, options?: NoProgressOptions): WasteCandidate[];
90
+ /**
91
+ * The no-progress rounds, read from the board at a path.
92
+ *
93
+ * The board is opened read-only and closed again here, so a caller does not
94
+ * have to know how the board is stored.
95
+ */
96
+ export declare function detectNoProgressAtPath(db: Database, boardPath: string, options?: NoProgressOptions): WasteCandidate[];
97
+ /** What the revert scanner needs to know. */
98
+ export type RevertOptions = {
99
+ /** The tokens the builders of a task spent, for the share. */
100
+ tokensForTask?: ((taskId: string) => number) | undefined;
101
+ /** How long a revert may take. */
102
+ windowDays?: number | undefined;
103
+ };
104
+ /**
105
+ * The reverted code of §6.4.
106
+ *
107
+ * A commit that carries a `Refs:` trailer and whose lines are deleted again
108
+ * within the window is code that was written and then taken back. The tokens
109
+ * counted are the builder tokens of the task, times the share of the added
110
+ * lines that were lost.
111
+ *
112
+ * This is a command and not a daemon: it walks the history of a repository
113
+ * once, when a person or the Lead asks for it.
114
+ */
115
+ export declare function scanReverts(gitDir: string, since: string, options?: RevertOptions): WasteCandidate[];
116
+ //# sourceMappingURL=waste.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waste.d.ts","sourceRoot":"","sources":["../src/waste.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAatC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;;;;;;;;;;;GAiBG;AAEH,iDAAiD;AACjD,eAAO,MAAM,oBAAoB,yDAA0D,CAAC;AAE5F,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,8DAA+D,CAAC;AAE7F,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,2CAA2C;AAC3C,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B,CAAC;AAWF;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,MAAM,CAe5E;AAuID,kDAAkD;AAClD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAChC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,GAAE,gBAAqB,GAAG,cAAc,EAAE,CAuB9F;AAqHD,mDAAmD;AACnD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B,CAAC;AAgHF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE,QAAQ,EACjB,OAAO,GAAE,iBAAsB,GAC9B,cAAc,EAAE,CAclB;AAgCD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,iBAAsB,GAC9B,cAAc,EAAE,CAQlB;AAWD,6CAA6C;AAC7C,MAAM,MAAM,aAAa,GAAG;IAC1B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,CAAC;IACzD,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,aAAkB,GAC1B,cAAc,EAAE,CAsBlB"}