@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 +32 -22
- package/modules/economy/cost.js +84 -19
- package/modules/lifecycle/publish-reconciler.js +7 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/go-live.js +142 -48
- package/scripts/gds/run-unit-tests.js +8 -1
- package/scripts/gds/session-cost.js +16 -0
- package/scripts/gds/session-digest-build.js +261 -0
- package/scripts/gds/ship.js +7 -198
- package/scripts/gds/update-channel.js +12 -0
- package/src/module-api.js +5 -1
- package/tests/go_live.mjs +65 -32
- package/tests/publish_reconciler.mjs +38 -0
- package/tests/session_cost.mjs +64 -0
- package/tests/session_cost_row_raise.mjs +139 -0
- package/tests/session_records.mjs +72 -0
- package/tests/update_channel.mjs +33 -0
|
@@ -405,3 +405,75 @@ test('a failed/empty subagent spawn digests to 0 tokens despite 1 assistant turn
|
|
|
405
405
|
console.log('');
|
|
406
406
|
console.log(`Session-records (6D.1) tests: ${passed} passed, ${failed} failed`);
|
|
407
407
|
if (failed > 0) process.exit(1);
|
|
408
|
+
|
|
409
|
+
// ---------- task 1003217: one API call is many transcript lines ----------
|
|
410
|
+
//
|
|
411
|
+
// Claude Code writes a separate line per content block of one assistant message
|
|
412
|
+
// (thinking / text / tool_use) and repeats the SAME message.usage on each. The
|
|
413
|
+
// digest must price the call ONCE. Measured 2.22x inflation on real transcripts
|
|
414
|
+
// before this fix.
|
|
415
|
+
function splitCallFixture() {
|
|
416
|
+
const usage = {
|
|
417
|
+
input_tokens: 2,
|
|
418
|
+
output_tokens: 799,
|
|
419
|
+
cache_read_input_tokens: 63182,
|
|
420
|
+
cache_creation_input_tokens: 3227,
|
|
421
|
+
};
|
|
422
|
+
const mk = (content) => ({
|
|
423
|
+
type: 'assistant',
|
|
424
|
+
timestamp: '2026-08-21T19:23:47.000Z',
|
|
425
|
+
message: { id: 'msg_01SPLIT', model: 'claude-opus-5', usage, content },
|
|
426
|
+
});
|
|
427
|
+
return [
|
|
428
|
+
mk([{ type: 'thinking', thinking: 'weighing it up' }]),
|
|
429
|
+
mk([{ type: 'text', text: 'Here is the plan.' }]),
|
|
430
|
+
mk([{ type: 'tool_use', name: 'Bash', input: { command: 'ls' } }]),
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
test('1003217 buildSessionDigest prices one split assistant message ONCE, not per line', () => {
|
|
435
|
+
const d = buildSessionDigest(splitCallFixture());
|
|
436
|
+
// 3 transcript lines, 1 API call.
|
|
437
|
+
assert.equal(d.metrics.assistant_turns, 3, 'assistant_turns stays the LINE count for existing consumers');
|
|
438
|
+
assert.equal(d.metrics.api_calls, 1, 'api_calls is the honest round-trip count');
|
|
439
|
+
assert.equal(d.metrics.tokens.output, 799, 'output counted once, not 3x');
|
|
440
|
+
assert.equal(d.metrics.tokens.cache_read, 63182, 'cache_read counted once, not 3x');
|
|
441
|
+
assert.equal(d.metrics.tokens.cache_creation, 3227);
|
|
442
|
+
assert.equal(d.metrics.tokens.input, 2);
|
|
443
|
+
assert.equal(d.totalTokens, 2 + 799 + 63182 + 3227);
|
|
444
|
+
assert.deepEqual(d.metrics.model_usage['claude-opus-5'], {
|
|
445
|
+
input: 2, output: 799, cache_read: 63182, cache_creation: 3227,
|
|
446
|
+
}, 'model_usage — the cost-plus reward basis (ADR 0054) — must not be inflated');
|
|
447
|
+
// The tool_use on the third line is still seen.
|
|
448
|
+
assert.equal(d.metrics.tool_use_count, 1);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('1003217 the detail rows still sum to the true usage (repeats zeroed + flagged)', () => {
|
|
452
|
+
const d = buildSessionDigest(splitCallFixture());
|
|
453
|
+
const asst = d.detail.filter((r) => r.role === 'assistant');
|
|
454
|
+
assert.equal(asst.length, 3, 'every line keeps its detail row');
|
|
455
|
+
const sum = (k) => asst.reduce((a, r) => a + (r[k] || 0), 0);
|
|
456
|
+
assert.equal(sum('cache_read'), 63182, 'summing detail must not double-count');
|
|
457
|
+
assert.equal(sum('out'), 799);
|
|
458
|
+
assert.equal(asst.filter((r) => r.dup === 1).length, 2, 'the two repeat lines are flagged dup');
|
|
459
|
+
assert.equal(asst[0].dup, undefined, 'the first line of the call is not a dup');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('1003217 distinct message ids are still counted separately', () => {
|
|
463
|
+
const usage = { input_tokens: 0, output_tokens: 10, cache_read_input_tokens: 100, cache_creation_input_tokens: 0 };
|
|
464
|
+
const mk = (id) => ({
|
|
465
|
+
type: 'assistant',
|
|
466
|
+
timestamp: '2026-08-21T19:23:47.000Z',
|
|
467
|
+
message: { id, model: 'claude-opus-5', usage, content: [{ type: 'text', text: 'x' }] },
|
|
468
|
+
});
|
|
469
|
+
const d = buildSessionDigest([mk('msg_A'), mk('msg_B'), mk('msg_C')]);
|
|
470
|
+
assert.equal(d.metrics.api_calls, 3);
|
|
471
|
+
assert.equal(d.metrics.tokens.cache_read, 300, 'three real calls = three times the usage');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('1003217 a transcript with no message.id degrades to the old per-line behaviour', () => {
|
|
475
|
+
const d = buildSessionDigest(fixture()); // fixture() messages carry no id
|
|
476
|
+
assert.equal(d.metrics.tokens.input, 300, 'unchanged from the pre-fix expectation');
|
|
477
|
+
assert.equal(d.metrics.tokens.output, 130);
|
|
478
|
+
assert.equal(d.metrics.api_calls, 2, 'with no id to dedup on, every line is its own call');
|
|
479
|
+
});
|
package/tests/update_channel.mjs
CHANGED
|
@@ -184,3 +184,36 @@ t('loadSubscriptions: an entry without a dir is dropped (cannot be upgraded)', (
|
|
|
184
184
|
|
|
185
185
|
console.log(`\nupdate_channel: ${passed} passed, ${failed} failed`);
|
|
186
186
|
process.exit(failed ? 1 : 0);
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Deploy topology survives normalization (task 1003219)
|
|
190
|
+
//
|
|
191
|
+
// The sweep can only hand a bump to go-live.js — and so inherit its backup, its
|
|
192
|
+
// pull-deploy-timer guard and its independent box-side read-back — if the roster's
|
|
193
|
+
// topology actually REACHES the entry. The first cut of this dropped the new fields
|
|
194
|
+
// on the floor, so an instance that named them looked enrolled while the sweep
|
|
195
|
+
// quietly kept using the weaker direct path. A normalizer that silently discards a
|
|
196
|
+
// field is indistinguishable from a feature that does not work.
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
test('deploy topology is carried through loadSubscriptions', () => {
|
|
200
|
+
const entry = {
|
|
201
|
+
slug: 's', dir: '/i', service: 'x.service', healthUrl: 'http://h',
|
|
202
|
+
versionUrl: 'http://v', deployTimer: 'd.timer', deployService: 'd.service',
|
|
203
|
+
backupDb: 'db', backupDir: '~/b', registryPackage: '@scope/core',
|
|
204
|
+
};
|
|
205
|
+
const fsImpl = { readFileSync: () => JSON.stringify({ instances: [entry] }) };
|
|
206
|
+
const { instances } = loadSubscriptions({ instanceDir: '/x', fsImpl });
|
|
207
|
+
assert.equal(instances.length, 1);
|
|
208
|
+
for (const k of ['versionUrl', 'deployTimer', 'deployService', 'backupDb', 'backupDir', 'registryPackage']) {
|
|
209
|
+
assert.equal(instances[0][k], entry[k], `${k} must survive normalization`);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('an entry that names no topology reports null, not undefined', () => {
|
|
214
|
+
const fsImpl = { readFileSync: () => JSON.stringify({ instances: [{ slug: 's', dir: '/i' }] }) };
|
|
215
|
+
const { instances } = loadSubscriptions({ instanceDir: '/x', fsImpl });
|
|
216
|
+
for (const k of ['versionUrl', 'deployTimer', 'deployService', 'backupDb', 'backupDir']) {
|
|
217
|
+
assert.equal(instances[0][k], null, `${k} should be an explicit null`);
|
|
218
|
+
}
|
|
219
|
+
});
|