@bongos/core 1.19.317 → 1.19.319

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/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.317",
6
- "core_contract": "1.19.317",
7
- "source_commit": "47d557d1d8a73aac8e99002c7a282f7bab39a700",
5
+ "core_version": "1.19.319",
6
+ "core_contract": "1.19.319",
7
+ "source_commit": "682a79a31b7882310cc854404a34a8743b0e3804",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-23T22:06:51.573Z",
9
+ "built_at": "2026-08-23T22:19:11.577Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 323,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1436,
14
+ "functional_verbatim": 1438,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1781,
20
- "tree_sha256": "1a2430cfe83da4de17388e4663512e1eac2d35a6269e0f124f26e7004d09f0a8",
19
+ "file_count": 1783,
20
+ "tree_sha256": "7c253e075a3fb04c94cc2fb0df42550178740faa03e84c8e9c1cb7675be2c23f",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2987,7 +2987,7 @@
2987
2987
  {
2988
2988
  "path": "modules/economy/cost.js",
2989
2989
  "mode": "0000644",
2990
- "sha256": "5d125e8b884ebfeb5687ee6748c49bf752d5757114ede98d0cabf2eee4a38b76"
2990
+ "sha256": "01bc7725fe16388971fd4def2544d0de9af6b3a522973df7ef86386737933d40"
2991
2991
  },
2992
2992
  {
2993
2993
  "path": "modules/economy/credits.js",
@@ -3972,7 +3972,7 @@
3972
3972
  {
3973
3973
  "path": "modules/lifecycle/publish-reconciler.js",
3974
3974
  "mode": "0000644",
3975
- "sha256": "8f08c6d58db41ba247aaf5b04b5a8988365b469e96c0ad04c336f272ef308c4e"
3975
+ "sha256": "28c61d55fe2b492f0fce4c99b60e88f0cbeb542c47c04482558818d65628e0d7"
3976
3976
  },
3977
3977
  {
3978
3978
  "path": "modules/lifecycle/routes/analytics.js",
@@ -4642,12 +4642,12 @@
4642
4642
  {
4643
4643
  "path": "package-lock.json",
4644
4644
  "mode": "0000644",
4645
- "sha256": "1bdb7a2ab4583036c92b3108244d5c30dc8aa6314ba28eb2339f3712deec806a"
4645
+ "sha256": "cb1d669bd4b49b50fc37854b48c9363f3b6a7bccf503cd3a1291dfa87ac072ac"
4646
4646
  },
4647
4647
  {
4648
4648
  "path": "package.json",
4649
4649
  "mode": "0000644",
4650
- "sha256": "2bbe8915c29c3f2cde9ef26aa885ddeb123f32caea804ed3146090147fa68a4d"
4650
+ "sha256": "ca4620fbb1129ae165e0f7bfb9f4bc6d837c027a2d7326eec6d9418718bbf03c"
4651
4651
  },
4652
4652
  {
4653
4653
  "path": "public-docs/index.html",
@@ -5377,7 +5377,7 @@
5377
5377
  {
5378
5378
  "path": "scripts/gds/go-live.js",
5379
5379
  "mode": "0000644",
5380
- "sha256": "fa7803b178d64eda47d4f05032353582f7000359b13caea7d018112f03c9906a"
5380
+ "sha256": "1e14a452c0ae53384facd6a20526a90106cb479c414937ebb7fe180f11c5fa31"
5381
5381
  },
5382
5382
  {
5383
5383
  "path": "scripts/gds/goal.js",
@@ -5672,7 +5672,7 @@
5672
5672
  {
5673
5673
  "path": "scripts/gds/run-unit-tests.js",
5674
5674
  "mode": "0000644",
5675
- "sha256": "e2e3cd6299f63b838ed5a78121eedcb44e76b3cc62b622581cd537c154b1d8ae"
5675
+ "sha256": "c96e50e533f4e20dba91c6a26ac6efd2c6ad33ffcdc856eeb46af21a17e67975"
5676
5676
  },
5677
5677
  {
5678
5678
  "path": "scripts/gds/sandbox-stage.js",
@@ -5832,7 +5832,12 @@
5832
5832
  {
5833
5833
  "path": "scripts/gds/session-cost.js",
5834
5834
  "mode": "0000644",
5835
- "sha256": "52d6c931fa0d7213632a365984ed360cb8df2d369762dbb417d0c0a1b74145a6"
5835
+ "sha256": "fdf3b9d8e0089078da550ac65d0c012583cc21103a668deecf481383a73c9da1"
5836
+ },
5837
+ {
5838
+ "path": "scripts/gds/session-digest-build.js",
5839
+ "mode": "0000644",
5840
+ "sha256": "95e5c21533f67588fd02fae311cef8985d3eee58ad3217fcb6e9998e1f4da965"
5836
5841
  },
5837
5842
  {
5838
5843
  "path": "scripts/gds/session-digest.js",
@@ -5867,7 +5872,7 @@
5867
5872
  {
5868
5873
  "path": "scripts/gds/ship.js",
5869
5874
  "mode": "0000644",
5870
- "sha256": "f3005175abc0ba20e958381df6aee02402267e9956da466386656dd4e86ad8cf"
5875
+ "sha256": "8f2c0ca63abae081cccfa96059ae07b008bd108f85866a248075cb4a1b7e025e"
5871
5876
  },
5872
5877
  {
5873
5878
  "path": "scripts/gds/skill-preflight.js",
@@ -5942,7 +5947,7 @@
5942
5947
  {
5943
5948
  "path": "scripts/gds/update-channel.js",
5944
5949
  "mode": "0000644",
5945
- "sha256": "e4ad34a4a98ad6e7327922c36a60eb54cbb507ca3e26a9ec793f5a9390eacd77"
5950
+ "sha256": "065095ccdcb104ede0312375484f62b23742fa1bc446f9749505c22b71babb5d"
5946
5951
  },
5947
5952
  {
5948
5953
  "path": "scripts/gds/upgrade.js",
@@ -6327,7 +6332,7 @@
6327
6332
  {
6328
6333
  "path": "src/module-api.js",
6329
6334
  "mode": "0000644",
6330
- "sha256": "7301568d229fc28265cf1ad41b4d568923cc18d1f7098ef5403c971ed531839a"
6335
+ "sha256": "c918c65478a9b35e7a10371f117e8a826c063f577331147740f9fa8ffa121161"
6331
6336
  },
6332
6337
  {
6333
6338
  "path": "src/module-loader/catalog.js",
@@ -7252,7 +7257,7 @@
7252
7257
  {
7253
7258
  "path": "tests/go_live.mjs",
7254
7259
  "mode": "0000644",
7255
- "sha256": "360712fee43b00b410e4b3b8ca27e801ae41966ac0c5ce0a4679fa7943aa554b"
7260
+ "sha256": "ce97c74d749ec3574c0683160b3ce5dad8a7af2dcf1ce53fd23e95f5e0ad480d"
7256
7261
  },
7257
7262
  {
7258
7263
  "path": "tests/goal_abuse_matrix.mjs",
@@ -8287,7 +8292,7 @@
8287
8292
  {
8288
8293
  "path": "tests/publish_reconciler.mjs",
8289
8294
  "mode": "0000644",
8290
- "sha256": "09c5e96eadbfd99f26579fbf67b2ccdf39c6c04415a2596e9dbd4644be8d3ebe"
8295
+ "sha256": "5a9ea0bf7b4d776a6f9036b8bd060fef76335533274ed748e9b9d742da6960d3"
8291
8296
  },
8292
8297
  {
8293
8298
  "path": "tests/rank_gate.mjs",
@@ -8507,7 +8512,12 @@
8507
8512
  {
8508
8513
  "path": "tests/session_cost.mjs",
8509
8514
  "mode": "0000644",
8510
- "sha256": "6531fd14dbfc025543c4855c326746c126cfcc94173b9a724e56f74a989a1d4e"
8515
+ "sha256": "5f330f79eee8c39dc589d892ba5fa6835906e7b61ff5fe52aec3285721e84876"
8516
+ },
8517
+ {
8518
+ "path": "tests/session_cost_row_raise.mjs",
8519
+ "mode": "0000644",
8520
+ "sha256": "74a106432222df7d8e845ec409bd2e1d49aff18bf80cdeff032cfe91b1d6dc3f"
8511
8521
  },
8512
8522
  {
8513
8523
  "path": "tests/session_digest.mjs",
@@ -8527,7 +8537,7 @@
8527
8537
  {
8528
8538
  "path": "tests/session_records.mjs",
8529
8539
  "mode": "0000644",
8530
- "sha256": "c33e4f5bb635eb523bc7014fe8cfad343b7ab7753822644ce987cd419520c748"
8540
+ "sha256": "8c62fe0bec07aea4c30f84d296eb7b40c9550606aacf6cb63f9644ca34f12069"
8531
8541
  },
8532
8542
  {
8533
8543
  "path": "tests/session_reward_on_land.mjs",
@@ -8862,7 +8872,7 @@
8862
8872
  {
8863
8873
  "path": "tests/update_channel.mjs",
8864
8874
  "mode": "0000644",
8865
- "sha256": "4e082a36725d91f00d5fc0cc266d32bf607c144994ac88cb2abb3b49149632da"
8875
+ "sha256": "3b0912dcda6888b4daa2cd13bbef0cddddafc425c39a509dc4955ce77ddd720e"
8866
8876
  },
8867
8877
  {
8868
8878
  "path": "tests/upgrade.mjs",
@@ -105,23 +105,51 @@ async function logCost({
105
105
  outputUsd = null,
106
106
  // task #385 / migration 181: which skill invocation incurred this cost.
107
107
  skillName = null,
108
+ // task 1003217 — opt-in MONOTONIC RAISE on (source, source_ref) conflict.
109
+ //
110
+ // Default false, so every existing caller keeps the plain insert-once
111
+ // idempotency it relies on. The session reconciler below sets it true: a
112
+ // long session's digest GROWS on every ship, and a cost row written from an
113
+ // early digest must be allowed to catch up, or the session is billed at its
114
+ // first hour forever (the bug this task fixes).
115
+ //
116
+ // Safe because the update SETS an absolute amount rather than adding to one:
117
+ // re-running with the same figure is a no-op, so there is no double-count
118
+ // path, and the WHERE guard means a row can only ever move UP.
119
+ raiseOnConflict = false,
108
120
  }) {
109
121
  // V3.R11 (#91): builder_id is the per-builder attribution column added by
110
122
  // migration 046. Routes default it to the authed builder, so the only
111
123
  // legitimately-null builder_id is for historical rows or system-injected
112
124
  // costs (infra, domain, etc.) that don't belong to any one person.
125
+ // Monotonic raise (opt-in) vs the historical insert-once. `recorded_at` moves
126
+ // with the amount so the reconciler's "digest is newer than the priced row"
127
+ // predicate settles instead of re-firing every sweep.
128
+ const conflictAction = raiseOnConflict
129
+ ? `DO UPDATE SET
130
+ amount_usd = EXCLUDED.amount_usd,
131
+ description = EXCLUDED.description,
132
+ fresh_input_usd = EXCLUDED.fresh_input_usd,
133
+ cache_read_usd = EXCLUDED.cache_read_usd,
134
+ cache_write_usd = EXCLUDED.cache_write_usd,
135
+ output_usd = EXCLUDED.output_usd,
136
+ recorded_at = EXCLUDED.recorded_at
137
+ WHERE EXCLUDED.amount_usd > cost_log.amount_usd`
138
+ : 'DO NOTHING';
113
139
  const { rows } = await pool.query(
114
140
  `INSERT INTO cost_log (task_id, amount_usd, category, description, source, source_ref, recorded_at, builder_id,
115
141
  fresh_input_usd, cache_read_usd, cache_write_usd, output_usd, skill_name)
116
142
  VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, now()), $8, $9, $10, $11, $12, $13)
117
143
  ON CONFLICT (source, source_ref)
118
144
  WHERE source IS NOT NULL AND source_ref IS NOT NULL
119
- DO NOTHING
145
+ ${conflictAction}
120
146
  RETURNING id, recorded_at`,
121
147
  [taskId, amountUsd, category, description, source, sourceRef, recordedAt, builderId,
122
148
  freshInputUsd, cacheReadUsd, cacheWriteUsd, outputUsd, skillName || null]
123
149
  );
124
- return rows[0] ?? null; // null when ON CONFLICT skipped the insert
150
+ // null when the conflict clause skipped the write (DO NOTHING, or a raise that
151
+ // would not have moved the row upward).
152
+ return rows[0] ?? null;
125
153
  }
126
154
 
127
155
  // task 1366 (System 1) — server-side session-cost reconciler.
@@ -144,25 +172,45 @@ async function logCost({
144
172
  // first crack at posting (avoids racing it). 15m default.
145
173
  // - limit : per-sweep cap (runaway guard; there are normally few).
146
174
  //
147
- // The NOT EXISTS checks BOTH the interactive key (session_id) AND the subagent
148
- // key (session_id || ':sub') — session-cost.js splits a captured session into
149
- // those two rows (task 1344), so a session the client captured even partially
150
- // is skipped entirely (we only have the combined model_usage, so we can't split
151
- // it; better to defer to the client's split than to add a duplicate total).
152
- // Returns { scanned, reconciled, totalUsd, unknownModels } for logging.
175
+ // A session the CLIENT split into interactive + subagent rows (session_id ||
176
+ // ':sub', task 1344) is skipped entirely — we hold only the combined
177
+ // model_usage, so we cannot reproduce that split and should defer to it.
178
+ //
179
+ // task 1003217 — WHY THIS NO LONGER SKIPS EVERY PRICED SESSION. It used to
180
+ // exclude any session that had a cost row at all. But `session_records` is
181
+ // upserted on EVERY ship, so a session that ships twelve tasks has a digest
182
+ // from its FIRST ship, and this sweep priced that partial digest ~15 minutes
183
+ // later and then never looked again — logCost's DO NOTHING silently dropped
184
+ // every subsequent, larger figure. Measured on prod: a 9.3-hour session was
185
+ // billed at its first 56 minutes, and across four audited sessions the ledger
186
+ // captured 14%, 14%, 16% and 54% of real spend. Now a session is revisited
187
+ // while its digest is NEWER than the row we priced from it, and logCost raises
188
+ // the row monotonically. `recorded_at` advances on each raise, so a settled
189
+ // session stops matching and the sweep converges.
190
+ //
191
+ // Historical rows are NOT re-priced downward: llm-pricing.js is explicit that a
192
+ // paid row records what was charged, and ADR 0054 paid credits from it. The
193
+ // raise is upward-only, so pre-fix inflated rows stay as they are — expect a
194
+ // visible step change in cost-per-ship at deploy rather than a rewritten past.
195
+ // Returns { scanned, reconciled, raised, totalUsd, unknownModels } for logging.
153
196
  async function reconcileSessionCosts({ olderThanMinutes = 15, limit = 500 } = {}) {
154
197
  const mins = Number.isFinite(Number(olderThanMinutes)) ? Number(olderThanMinutes) : 15;
155
198
  const lim = Number.isFinite(Number(limit)) ? Number(limit) : 500;
156
199
  const { rows } = await pool.query(
157
- `SELECT sr.session_id, sr.builder_id, sr.model_usage
200
+ `SELECT sr.session_id, sr.builder_id, sr.model_usage,
201
+ c.amount_usd AS prior_usd
158
202
  FROM session_records sr
203
+ LEFT JOIN cost_log c
204
+ ON c.source = 'session:interactive'
205
+ AND c.source_ref = sr.session_id
159
206
  WHERE sr.model_usage IS NOT NULL
160
207
  AND sr.uploaded_at < now() - ($1 || ' minutes')::interval
161
208
  AND NOT EXISTS (
162
- SELECT 1 FROM cost_log c
163
- WHERE c.source_ref = sr.session_id
164
- OR c.source_ref = sr.session_id || ':sub'
209
+ SELECT 1 FROM cost_log s
210
+ WHERE s.source_ref = sr.session_id || ':sub'
165
211
  )
212
+ -- Never priced, or the digest has grown since we priced it.
213
+ AND (c.id IS NULL OR c.recorded_at < sr.uploaded_at)
166
214
  ORDER BY sr.uploaded_at DESC
167
215
  LIMIT $2`,
168
216
  [String(mins), lim]
@@ -170,6 +218,7 @@ async function reconcileSessionCosts({ olderThanMinutes = 15, limit = 500 } = {}
170
218
 
171
219
  const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
172
220
  let reconciled = 0;
221
+ let raised = 0;
173
222
  let totalUsd = 0;
174
223
  const unknownModels = new Set();
175
224
  for (const r of rows) {
@@ -177,34 +226,50 @@ async function reconcileSessionCosts({ olderThanMinutes = 15, limit = 500 } = {}
177
226
  for (const m of comp.unknown_models) unknownModels.add(m);
178
227
  const amount = round4(comp.total_usd);
179
228
  if (!(amount > 0)) continue; // nothing priceable on this row — skip
229
+ const prior = round4(r.prior_usd);
230
+ // A row already at or above this figure needs no write: the digest grew but
231
+ // the price did not (or the stored row predates a pricing fix and is higher,
232
+ // which we deliberately leave alone).
233
+ if (prior > 0 && amount <= prior) continue;
180
234
  const id8 = String(r.session_id).slice(0, 8);
181
235
  const unkNote = comp.unknown_models.length
182
236
  ? ` [unpriced model(s) priced as opus: ${comp.unknown_models.join(',')}]`
183
237
  : '';
238
+ const provenance = prior > 0
239
+ ? `raised from $${prior.toFixed(4)} as the session grew`
240
+ : 'reconciled from digest; client cost-hook did not post';
184
241
  const inserted = await logCost({
185
242
  amountUsd: amount,
186
243
  category: 'api',
187
244
  source: 'session:interactive',
188
- // SAME key the client would use → ON CONFLICT DO NOTHING prevents any
189
- // double-count if the client also posts later.
245
+ // SAME key the client would use, so a client post and this sweep can
246
+ // never both land as separate rows.
190
247
  sourceRef: r.session_id,
191
248
  builderId: r.builder_id,
192
- description: `session ${id8} — $${amount.toFixed(4)} (reconciled from digest; client cost-hook did not post)${unkNote}`.slice(0, 1000),
249
+ // Upward-only: catches a long session up as it ships more tasks, and can
250
+ // never lower a row that has already been paid from.
251
+ raiseOnConflict: true,
252
+ description: `session ${id8} — $${amount.toFixed(4)} (${provenance})${unkNote}`.slice(0, 1000),
193
253
  freshInputUsd: round4(comp.fresh_input_usd),
194
254
  cacheReadUsd: round4(comp.cache_read_usd),
195
255
  cacheWriteUsd: round4(comp.cache_write_usd),
196
256
  outputUsd: round4(comp.output_usd),
197
257
  });
198
- // inserted === null means ON CONFLICT skipped it (client raced in between the
199
- // SELECT and the INSERT) — expected + idempotent, not counted.
258
+ // inserted === null means the conflict clause skipped the write (the client
259
+ // raced in between the SELECT and the INSERT, or the raise would not have
260
+ // moved the row up) — expected + idempotent, not counted.
200
261
  if (inserted) {
201
- reconciled++;
202
- totalUsd += amount;
262
+ if (prior > 0) raised++;
263
+ else reconciled++;
264
+ // Bill only the INCREMENT on a raise, so this figure stays a true
265
+ // "spend recorded by this sweep" rather than re-counting the whole row.
266
+ totalUsd += amount - prior;
203
267
  }
204
268
  }
205
269
  return {
206
270
  scanned: rows.length,
207
271
  reconciled,
272
+ raised,
208
273
  totalUsd: round4(totalUsd),
209
274
  unknownModels: Array.from(unknownModels),
210
275
  };
@@ -804,12 +804,17 @@ async function runCostSweep(deps = {}) {
804
804
  if (typeof reconciler.reconcileSessionCosts !== 'function') return { skipped: 'unsupported' };
805
805
  try {
806
806
  const cost = await reconciler.reconcileSessionCosts();
807
- if (cost && cost.reconciled > 0) {
807
+ // task 1003217: `raised` counts long sessions caught up as their digest grew.
808
+ // It is reported alongside `reconciled` — a sweep that only raised rows did
809
+ // real ledger work and must not log silence.
810
+ const raised = Number(cost && cost.raised) || 0;
811
+ if (cost && (cost.reconciled > 0 || raised > 0)) {
808
812
  const unk = cost.unknownModels && cost.unknownModels.length
809
813
  ? ` [unpriced→opus: ${cost.unknownModels.join(',')}]`
810
814
  : '';
815
+ const raisedNote = raised > 0 ? `, raised ${raised}` : '';
811
816
  logFn(
812
- `[publish-reconciler] session-cost: scanned ${cost.scanned}, reconciled ${cost.reconciled} cost_log row(s) totalling $${Number(cost.totalUsd || 0).toFixed(4)}${unk}`
817
+ `[publish-reconciler] session-cost: scanned ${cost.scanned}, reconciled ${cost.reconciled}${raisedNote} cost_log row(s) totalling $${Number(cost.totalUsd || 0).toFixed(4)}${unk}`
813
818
  );
814
819
  }
815
820
  return cost;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.317",
3
+ "version": "1.19.319",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.317",
9
+ "version": "1.19.319",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.317",
3
+ "version": "1.19.319",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",