mixdog 1.0.4 → 1.0.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/README.md +117 -434
- package/package.json +18 -17
- package/scripts/gate-local.mjs +93 -0
- package/scripts/lib/gate-scheduler.mjs +24 -0
- package/scripts/lib/run-node-tests.mjs +5 -1
- package/scripts/release-paths.mjs +22 -1
- package/scripts/test-direct.mjs +2 -1
- package/scripts/test.mjs +3 -1
- package/src/defaults/agents.json +0 -12
- package/src/defaults/mixdog-config.template.json +1 -2
- package/src/defaults/skills/docx/SKILL.md +7 -4
- package/src/defaults/skills/docx/references/html.md +46 -0
- package/src/defaults/skills/docx/references/native-authoring.md +18 -4
- package/src/defaults/skills/pdf/SKILL.md +10 -2
- package/src/defaults/skills/pdf/references/html.md +64 -0
- package/src/defaults/skills/pptx/references/html.md +1 -1
- package/src/defaults/skills/xlsx/SKILL.md +7 -3
- package/src/defaults/skills/xlsx/references/html.md +40 -0
- package/src/defaults/skills/xlsx/references/report-design.md +21 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +6 -5
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +50 -9
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +28 -1
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +34 -8
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/transcript-restore/agent-envelope.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -1
- package/src/runtime/attachments/pdf-extract.mjs +7 -3
- package/src/runtime/computer-bridge/client.mjs +1 -1
- package/src/runtime/memory/index.mjs +4 -15
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +0 -2
- package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +3 -11
- package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +1 -3
- package/src/runtime/memory/lib/cycle-scheduler/scheduled-enqueue.mjs +1 -11
- package/src/runtime/memory/lib/cycle-scheduler/tick-loop.mjs +0 -2
- package/src/runtime/memory/lib/cycle-scheduler.mjs +4 -13
- package/src/runtime/memory/lib/cycle-signatures.mjs +0 -6
- package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +1 -1
- package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +2 -2
- package/src/runtime/memory/lib/embedding-reindex.mjs +1 -1
- package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
- package/src/runtime/memory/lib/memory-action-handlers/cycle-actions.mjs +7 -52
- package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +6 -22
- package/src/runtime/memory/lib/memory-action-handlers/cycle-rebuild-action.mjs +10 -17
- package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -3
- package/src/runtime/memory/lib/memory-action-handlers/manage-actions.mjs +2 -4
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -4
- package/src/runtime/memory/lib/memory-cycle-requests.mjs +1 -1
- package/src/runtime/memory/lib/{memory-cycle2-shared.mjs → memory-cycle-shared.mjs} +14 -16
- package/src/runtime/memory/lib/memory-cycle.mjs +3 -2
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-embed.mjs +14 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +8 -11
- package/src/runtime/memory/lib/memory-schema/entries.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +2 -6
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +1 -1
- package/src/runtime/memory/lib/query-hybrid-search.mjs +1 -1
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -6
- package/src/runtime/memory/lib/recall-format.mjs +1 -1
- package/src/runtime/office/authoring/docx-html-build.mjs +375 -0
- package/src/runtime/office/authoring/docx-html-measure.mjs +561 -0
- package/src/runtime/office/authoring/docx-html-runner.mjs +102 -0
- package/src/runtime/office/authoring/html-browser.mjs +61 -0
- package/src/runtime/office/authoring/html-document-author.mjs +181 -0
- package/src/runtime/office/authoring/html-source-drift.mjs +36 -0
- package/src/runtime/office/authoring/pdf-author-action.mjs +266 -0
- package/src/runtime/office/authoring/pdf-html-charts.mjs +379 -0
- package/src/runtime/office/authoring/pdf-html-runner.mjs +317 -0
- package/src/runtime/office/authoring/pptx-author-action.mjs +44 -22
- package/src/runtime/office/authoring/pptx-author-session.mjs +12 -6
- package/src/runtime/office/authoring/pptx-html-build.mjs +23 -4
- package/src/runtime/office/authoring/pptx-html-measure.mjs +38 -44
- package/src/runtime/office/authoring/pptx-html-runner.mjs +18 -3
- package/src/runtime/office/authoring/pptx-receipt.mjs +39 -31
- package/src/runtime/office/authoring/pptx-review-artifacts.mjs +9 -1
- package/src/runtime/office/authoring/pptx-script-runner.mjs +6 -2
- package/src/runtime/office/authoring/xlsx-html-build.mjs +365 -0
- package/src/runtime/office/authoring/xlsx-html-measure.mjs +257 -0
- package/src/runtime/office/authoring/xlsx-html-runner.mjs +139 -0
- package/src/runtime/office/capabilities-catalog.mjs +8 -0
- package/src/runtime/office/capabilities-signatures.mjs +18 -6
- package/src/runtime/office/capabilities.mjs +26 -1
- package/src/runtime/office/com/office-com-host.ps1 +133 -17
- package/src/runtime/office/core/office-actions-batch.mjs +16 -1
- package/src/runtime/office/core/office-actions-inspect.mjs +3 -2
- package/src/runtime/office/core/office-actions-lifecycle.mjs +6 -2
- package/src/runtime/office/core/office-actions-render.mjs +4 -1
- package/src/runtime/office/core/office-finalize/review-stage.mjs +15 -2
- package/src/runtime/office/core/office-qa/design-review-stage.mjs +9 -0
- package/src/runtime/office/core/office-session-dispatch.mjs +3 -0
- package/src/runtime/office/core/office-sessionless-actions.mjs +7 -2
- package/src/runtime/office/core/office-sessions.mjs +19 -2
- package/src/runtime/office/pdf/pdf-writer.mjs +1 -1
- package/src/runtime/office/portable/docx-formatting.mjs +21 -0
- package/src/runtime/office/portable/ooxml-validator.mjs +22 -1
- package/src/runtime/office/portable/portable-chart.mjs +10 -2
- package/src/runtime/office/portable/portable-docx-edits.mjs +12 -6
- package/src/runtime/office/portable/portable-docx-operations.mjs +75 -1
- package/src/runtime/office/portable/portable-docx-xml.mjs +49 -6
- package/src/runtime/office/portable/portable-docx.mjs +2 -0
- package/src/runtime/office/portable/portable-snapshot-docx.mjs +2 -0
- package/src/runtime/office/portable/portable-validation.mjs +6 -3
- package/src/runtime/office/portable/portable-xlsx-cell-values.mjs +87 -0
- package/src/runtime/office/portable/portable-xlsx-charts.mjs +55 -13
- package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +1 -1
- package/src/runtime/office/portable/xlsx-contract.mjs +82 -3
- package/src/runtime/office/quality/assurance-checklist.mjs +2 -2
- package/src/runtime/office/quality/assurance-rendered.mjs +27 -1
- package/src/runtime/office/quality/assurance-structure-docx.mjs +44 -0
- package/src/runtime/office/quality/assurance-structure-pptx.mjs +24 -6
- package/src/runtime/office/quality/assurance-structure-xlsx.mjs +50 -1
- package/src/runtime/office/quality/design-review-critique.mjs +3 -2
- package/src/runtime/office/quality/document-brief.mjs +44 -0
- package/src/runtime/office/quality/inline-audit.mjs +7 -2
- package/src/runtime/office/quality/quality-pipeline.mjs +9 -1
- package/src/runtime/office/tool-defs.mjs +2 -2
- package/src/runtime/shared/llm/quota-value-estimate.mjs +317 -0
- package/src/runtime/shared/llm/usage-ledger-quota.mjs +266 -63
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +35 -0
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +10 -1
- package/src/runtime/shared/provider-accounts.mjs +11 -1
- package/src/runtime/shared/statusline/statusline-segments.mjs +4 -4
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/wait-until.test-support.mjs +33 -0
- package/src/session-runtime/boot/apis.mjs +1 -1
- package/src/session-runtime/services/channel-admin.mjs +4 -3
- package/src/session-runtime/services/usage-stats-model.mjs +22 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +6 -1
- package/src/session-runtime/turn/session-ops.mjs +15 -9
- package/src/session-runtime/usage-stats-api.mjs +34 -2
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/local-session-runtime.mjs +488 -0
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-runtime-inline-host.mjs +1 -1
- package/src/tui/hooks/useSession.mjs +1 -1
- package/src/tui/session/session-api/commands.mjs +1 -1
- package/src/tui/session/session-api/integrations.mjs +3 -0
- package/src/tui/session/session-flow/auto-clear.mjs +6 -7
- package/src/tui/session/tool-card-results.mjs +1 -1
- package/src/tui/session/turn.mjs +5 -0
- package/src/tui/session-local.mjs +3 -488
- package/src/tui/session.mjs +1 -1
- package/src/ui/statusline-agents.mjs +0 -1
- package/src/rules/agent/41-cycle2-agent.md +0 -24
- package/src/runtime/memory/lib/cycle-scheduler/cycle2-runs.mjs +0 -120
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +0 -110
- package/src/runtime/memory/lib/memory-cycle2-quarantine.mjs +0 -99
- package/src/runtime/memory/lib/memory-cycle2-review.mjs +0 -240
- package/src/runtime/memory/lib/memory-cycle2.mjs +0 -191
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { normalizeUsageMeasurement } from './usage-measurement.mjs';
|
|
16
16
|
import { usageRollupDayKey } from './usage-rollup.mjs';
|
|
17
|
+
import { estimateOutsideQuota, estimateQuotaValue, outsideSpans } from './quota-value-estimate.mjs';
|
|
17
18
|
|
|
18
19
|
export const QUOTA_SCHEMA = `
|
|
19
20
|
CREATE TABLE IF NOT EXISTS quota_samples (
|
|
@@ -38,9 +39,13 @@ const SLOT_SIZES = [30, 60, 120, 180, 360, 720, 1440, 2880, 10080, 20160, 43200]
|
|
|
38
39
|
const MAX_SLOTS = 48;
|
|
39
40
|
const MAX_POINTS = 4000;
|
|
40
41
|
// The window history is read a page at a time, and a page reads only the
|
|
41
|
-
// records of its own windows.
|
|
42
|
+
// records of its own windows and of the earlier windows their values continue.
|
|
42
43
|
const HISTORY_PAGE_SIZE = 10;
|
|
43
44
|
const SERIES_LIMIT = 6;
|
|
45
|
+
// Each window's value continues from the windows just before it, back until
|
|
46
|
+
// they hold this many quota points (at most this many windows).
|
|
47
|
+
const PRIOR_POINTS = 100;
|
|
48
|
+
const PRIOR_WINDOWS = 24;
|
|
44
49
|
// The by-model chart's series key for usage from outside Mixdog. Every
|
|
45
50
|
// recorded model has a name, so the empty key cannot collide.
|
|
46
51
|
const OUTSIDE_KEY = '';
|
|
@@ -361,9 +366,19 @@ function legacyAccountOwner(db, provider) {
|
|
|
361
366
|
* account belong to the legacy owner only, or to every account while no
|
|
362
367
|
* record names one.
|
|
363
368
|
*/
|
|
364
|
-
function readQuotaEvents(db, { provider, account, fromMs, toMs }) {
|
|
369
|
+
function readQuotaEvents(db, { provider, account, label = '', fromMs, toMs }) {
|
|
365
370
|
const owner = legacyAccountOwner(db, provider);
|
|
366
371
|
const unlabeled = owner === null || owner === account ? '' : account;
|
|
372
|
+
// Anthropic's scoped weekly labels are model-family limits, not the whole
|
|
373
|
+
// subscription. An unrecognized scope must never borrow every model's cost.
|
|
374
|
+
const scope =
|
|
375
|
+
provider === 'anthropic-oauth' && label.startsWith('7D ')
|
|
376
|
+
? `claude-${label
|
|
377
|
+
.slice(3)
|
|
378
|
+
.trim()
|
|
379
|
+
.toLowerCase()
|
|
380
|
+
.replace(/[\s_]+/g, '-')}`
|
|
381
|
+
: '';
|
|
367
382
|
const rows = db
|
|
368
383
|
.prepare(`
|
|
369
384
|
WITH routes AS (
|
|
@@ -371,13 +386,13 @@ function readQuotaEvents(db, { provider, account, fromMs, toMs }) {
|
|
|
371
386
|
FROM usage_routes
|
|
372
387
|
WHERE json_extract(signature,'$[0]')=? AND COALESCE(json_extract(signature,'$[8]'),'') IN (?,?)
|
|
373
388
|
)
|
|
374
|
-
SELECT
|
|
389
|
+
SELECT e.ts,e.day,r.model,r.rank,COUNT(*) AS turns,
|
|
375
390
|
SUM(e.input) AS input,SUM(e.output) AS output,SUM(e.cache_read) AS cacheRead,
|
|
376
391
|
SUM(e.cache_write) AS cacheWrite,SUM(e.cost_usd) AS costUsd,COUNT(e.cost_usd) AS priced
|
|
377
392
|
FROM usage_events e JOIN routes r ON r.id=e.route
|
|
378
393
|
WHERE e.ts>=? AND e.ts<=?
|
|
379
|
-
GROUP BY
|
|
380
|
-
ORDER BY
|
|
394
|
+
GROUP BY e.ts,e.day,r.model,r.rank
|
|
395
|
+
ORDER BY e.ts`)
|
|
381
396
|
.all(provider, account, unlabeled, fromMs, toMs);
|
|
382
397
|
const best = new Map();
|
|
383
398
|
const ranks = db.prepare(`SELECT day,model,MIN(rank) AS rank FROM daily
|
|
@@ -386,6 +401,8 @@ function readQuotaEvents(db, { provider, account, fromMs, toMs }) {
|
|
|
386
401
|
best.set(`${row.day}\u0000${row.model}`, row.rank);
|
|
387
402
|
}
|
|
388
403
|
return rows.flatMap((row) => {
|
|
404
|
+
const model = String(row.model).toLowerCase();
|
|
405
|
+
if (scope && model !== scope && !model.startsWith(`${scope}-`)) return [];
|
|
389
406
|
const day = String(row.day);
|
|
390
407
|
if (best.get(`${day.slice(0, 4)}-${day.slice(4, 6)}-${day.slice(6)}\u0000${row.model}`) !== row.rank) return [];
|
|
391
408
|
const usage = normalizeUsageMeasurement(provider, {
|
|
@@ -398,7 +415,7 @@ function readQuotaEvents(db, { provider, account, fromMs, toMs }) {
|
|
|
398
415
|
costKnownTurns: row.priced,
|
|
399
416
|
});
|
|
400
417
|
const event = {
|
|
401
|
-
ts: row.
|
|
418
|
+
ts: row.ts,
|
|
402
419
|
model: row.model,
|
|
403
420
|
turns: row.turns,
|
|
404
421
|
input: usage.input || 0,
|
|
@@ -414,58 +431,211 @@ function readQuotaEvents(db, { provider, account, fromMs, toMs }) {
|
|
|
414
431
|
});
|
|
415
432
|
}
|
|
416
433
|
|
|
434
|
+
/** quota_samples stores readings rounded to two decimal places. Respect the
|
|
435
|
+
* precision actually present: an integer-only series supplies whole-percent
|
|
436
|
+
* readings, not invented tenths. Preserve finer reported readings when present. */
|
|
437
|
+
function quotaReadingResolution(rows) {
|
|
438
|
+
const cents = rows.map((row) => Math.round(row.usedPct * 100));
|
|
439
|
+
if (cents.some((value) => value % 10 !== 0)) return 0.01;
|
|
440
|
+
return cents.some((value) => value % 100 !== 0) ? 0.1 : 1;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** The windows just before `instance`, oldest first: the calibration its
|
|
444
|
+
* value continues from. Defined per window, so every view of one window
|
|
445
|
+
* computes the same value. `listed` is every non-idle window, oldest first. */
|
|
446
|
+
function earlierWindows(listed, instance) {
|
|
447
|
+
const earlier = [];
|
|
448
|
+
let points = 0;
|
|
449
|
+
for (let index = listed.length - 1; index >= 0; index -= 1) {
|
|
450
|
+
if (points >= PRIOR_POINTS || earlier.length >= PRIOR_WINDOWS) break;
|
|
451
|
+
const candidate = listed[index];
|
|
452
|
+
if (candidate.startMs >= instance.startMs) continue;
|
|
453
|
+
earlier.unshift(candidate);
|
|
454
|
+
points += candidate.peak;
|
|
455
|
+
}
|
|
456
|
+
return earlier;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Every window `shown` reads records for: itself and its calibration. */
|
|
460
|
+
function calibratedWindows(listed, shown) {
|
|
461
|
+
return [...new Set([...shown.flatMap((instance) => earlierWindows(listed, instance)), ...shown])];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** One window's meter rises, each with the records behind it. */
|
|
465
|
+
function windowIntervals(instance, events, mixed) {
|
|
466
|
+
const resolution = quotaReadingResolution(instance.rows);
|
|
467
|
+
const intervals = [];
|
|
468
|
+
let fromMs = instance.startMs;
|
|
469
|
+
let fromPct = 0;
|
|
470
|
+
for (const [rowIndex, row] of instance.rows.entries()) {
|
|
471
|
+
const delta = row.usedPct - fromPct;
|
|
472
|
+
if (delta > 0) {
|
|
473
|
+
const start = firstAfter(events, fromMs);
|
|
474
|
+
const end = firstAfter(events, row.ts);
|
|
475
|
+
let cost = 0;
|
|
476
|
+
let tokens = 0;
|
|
477
|
+
let turns = 0;
|
|
478
|
+
let priced = 0;
|
|
479
|
+
for (let index = start; index < end; index += 1) {
|
|
480
|
+
cost += events[index].costUsd;
|
|
481
|
+
tokens += events[index].tokens;
|
|
482
|
+
turns += events[index].turns;
|
|
483
|
+
priced += events[index].costKnownTurns;
|
|
484
|
+
}
|
|
485
|
+
let weight = 'turns';
|
|
486
|
+
let total = turns;
|
|
487
|
+
if (cost > 0) {
|
|
488
|
+
weight = 'costUsd';
|
|
489
|
+
total = cost;
|
|
490
|
+
} else if (tokens > 0) {
|
|
491
|
+
weight = 'tokens';
|
|
492
|
+
total = tokens;
|
|
493
|
+
}
|
|
494
|
+
intervals.push({
|
|
495
|
+
fromMs,
|
|
496
|
+
toMs: row.ts,
|
|
497
|
+
points: delta,
|
|
498
|
+
costUsd: cost,
|
|
499
|
+
start,
|
|
500
|
+
end,
|
|
501
|
+
weight,
|
|
502
|
+
total,
|
|
503
|
+
resolution,
|
|
504
|
+
// Long reading gaps still carry cost and quota evidence. Only an
|
|
505
|
+
// inferred opening, a saturated meter, or the first rise from zero
|
|
506
|
+
// lowers its measurement quality: the first request of a window can
|
|
507
|
+
// show a whole step of a rounded-up meter, so that rise only bounds
|
|
508
|
+
// its use within one step.
|
|
509
|
+
measured: rowIndex > 0 && fromPct > 0 && row.usedPct < 100,
|
|
510
|
+
priced: turns > 0 && priced === turns,
|
|
511
|
+
mixed: mixed.some((span) => span.fromMs < row.ts && span.toMs > fromMs),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
fromMs = row.ts;
|
|
515
|
+
fromPct = row.usedPct;
|
|
516
|
+
}
|
|
517
|
+
return intervals;
|
|
518
|
+
}
|
|
519
|
+
|
|
417
520
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
521
|
+
* Keep observed quota and recorded dollars intact. Continuously estimate
|
|
522
|
+
* probable outside use from priced history before allocating the remainder
|
|
523
|
+
* over Mixdog's requests; recent observations outweigh older ones. Each
|
|
524
|
+
* window continues from its earlier windows in `listed` (every non-idle
|
|
525
|
+
* window, oldest first), whose records `events` must cover. Intervals
|
|
526
|
+
* overlapping `mixed` spans never calibrate the rate: their outside share is
|
|
527
|
+
* the rise that Mixdog's own dollars at that rate do not cover.
|
|
421
528
|
*/
|
|
422
|
-
function allocateQuota(instances, events) {
|
|
529
|
+
function allocateQuota(instances, events, mixed = [], listed = []) {
|
|
423
530
|
const shares = new Float64Array(events.length);
|
|
424
531
|
const outside = [];
|
|
532
|
+
const calibration = new Map();
|
|
533
|
+
const calibrationOf = (instance) => {
|
|
534
|
+
if (!calibration.has(instance)) {
|
|
535
|
+
calibration.set(
|
|
536
|
+
instance,
|
|
537
|
+
windowIntervals(instance, events, mixed).filter((interval) => !interval.mixed)
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return calibration.get(instance);
|
|
541
|
+
};
|
|
425
542
|
for (const instance of instances) {
|
|
426
|
-
instance.attributed = 0;
|
|
427
|
-
instance.attributedCost = 0;
|
|
428
543
|
instance.outside = 0;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (cost > 0) {
|
|
447
|
-
weight = 'costUsd';
|
|
448
|
-
total = cost;
|
|
449
|
-
} else if (tokens > 0) {
|
|
450
|
-
weight = 'tokens';
|
|
451
|
-
total = tokens;
|
|
544
|
+
instance.outsideCostUsd = 0;
|
|
545
|
+
instance.outsideUnpriced = 0;
|
|
546
|
+
instance.intervals = windowIntervals(instance, events, mixed);
|
|
547
|
+
instance.estimate = estimateQuotaValue(
|
|
548
|
+
instance.intervals.filter((interval) => !interval.mixed),
|
|
549
|
+
earlierWindows(listed, instance).flatMap(calibrationOf)
|
|
550
|
+
);
|
|
551
|
+
for (const interval of instance.intervals) {
|
|
552
|
+
const { start, end, total, weight, points } = interval;
|
|
553
|
+
let external = points;
|
|
554
|
+
if (total > 0) {
|
|
555
|
+
external = 0;
|
|
556
|
+
if (interval.measured && interval.priced) {
|
|
557
|
+
external =
|
|
558
|
+
interval.mixed && instance.estimate
|
|
559
|
+
? Math.min(points, Math.max(0, points - interval.costUsd / instance.estimate.costPerPercent))
|
|
560
|
+
: estimateOutsideQuota(interval, instance.estimate);
|
|
452
561
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
} else {
|
|
458
|
-
outside.push({ fromMs, toMs: row.ts, points: delta });
|
|
459
|
-
instance.outside += delta;
|
|
562
|
+
}
|
|
563
|
+
if (total > 0) {
|
|
564
|
+
for (let index = start; index < end; index += 1) {
|
|
565
|
+
shares[index] += ((points - external) * events[index][weight]) / total;
|
|
460
566
|
}
|
|
461
567
|
}
|
|
462
|
-
|
|
463
|
-
|
|
568
|
+
if (external > 0) {
|
|
569
|
+
const costUsd = instance.estimate ? external * instance.estimate.costPerPercent : null;
|
|
570
|
+
outside.push({ fromMs: interval.fromMs, toMs: interval.toMs, points: external, costUsd });
|
|
571
|
+
instance.outside += external;
|
|
572
|
+
if (costUsd === null) instance.outsideUnpriced += external;
|
|
573
|
+
else instance.outsideCostUsd += costUsd;
|
|
574
|
+
}
|
|
464
575
|
}
|
|
465
576
|
}
|
|
466
577
|
return { shares, outside };
|
|
467
578
|
}
|
|
468
579
|
|
|
580
|
+
/**
|
|
581
|
+
* The weekly all-model limit's five-hour companion restarts from zero every
|
|
582
|
+
* few hours and moves in quarter-size steps, so it exposes outside use that a
|
|
583
|
+
* whole-percent weekly reading hides. Each week is judged by its own
|
|
584
|
+
* five-hour windows, so a plan change between weeks cannot make every later
|
|
585
|
+
* five-hour window look mixed. Scoped weekly limits share no five-hour meter.
|
|
586
|
+
*/
|
|
587
|
+
function finerOutsideSpans(db, selection, events, weeks, now) {
|
|
588
|
+
if (selection.label !== '7D' || !events.length || !weeks.length) return [];
|
|
589
|
+
const prefix = [0];
|
|
590
|
+
for (const event of events) prefix.push(prefix.at(-1) + event.costUsd);
|
|
591
|
+
const costBetween = (from, to) => prefix[firstAfter(events, to)] - prefix[firstAfter(events, from)];
|
|
592
|
+
const fives = quotaInstances(readQuotaRows(db, { ...selection, label: '5H' }), '5H', now).filter(
|
|
593
|
+
(instance) => !instance.idle
|
|
594
|
+
);
|
|
595
|
+
return weeks.flatMap((week) =>
|
|
596
|
+
outsideSpans(
|
|
597
|
+
fives
|
|
598
|
+
.filter((five) => five.startMs >= week.startMs && five.startMs < week.endMs)
|
|
599
|
+
.map((five) => ({ startMs: five.startMs, readings: five.rows })),
|
|
600
|
+
costBetween
|
|
601
|
+
)
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function intervalShare(interval, fromMs, toMs) {
|
|
606
|
+
if (interval.toMs <= interval.fromMs) return interval.toMs >= fromMs && interval.toMs <= toMs ? 1 : 0;
|
|
607
|
+
return (
|
|
608
|
+
Math.max(0, Math.min(interval.toMs, toMs) - Math.max(interval.fromMs, fromMs)) / (interval.toMs - interval.fromMs)
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** A period spanning different capacities averages by observed quota, not
|
|
613
|
+
* dollars. A missing calibration must not silently stand in as zero. */
|
|
614
|
+
function periodQuotaRate(instances, fromMs, toMs) {
|
|
615
|
+
let value = 0;
|
|
616
|
+
let points = 0;
|
|
617
|
+
for (const instance of instances) {
|
|
618
|
+
for (const interval of instance.intervals) {
|
|
619
|
+
const part = interval.points * intervalShare(interval, fromMs, toMs);
|
|
620
|
+
if (!(part > 0)) continue;
|
|
621
|
+
if (!instance.estimate) return null;
|
|
622
|
+
value += part * instance.estimate.costPerPercent;
|
|
623
|
+
points += part;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return points > 0 ? round(value / points, 6) : null;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function quotaCostFields(costUsd, outsideCostUsd, unpriced = false) {
|
|
630
|
+
const extra = outsideCostUsd === null ? null : round(outsideCostUsd, 6);
|
|
631
|
+
const recorded = round(costUsd, 6);
|
|
632
|
+
return {
|
|
633
|
+
costUsd: recorded,
|
|
634
|
+
outsideCostUsd: extra,
|
|
635
|
+
estimatedTotalCostUsd: extra === null || unpriced ? null : round(recorded + extra, 6),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
469
639
|
const USAGE_FIELDS = [
|
|
470
640
|
'turns',
|
|
471
641
|
'input',
|
|
@@ -575,22 +745,26 @@ function modelSeries(slots, instances, keys, now) {
|
|
|
575
745
|
/** Outside usage is spread over the time its interval covers; returns its total within the domain. */
|
|
576
746
|
function spreadOutsideUsage(outside, slots, slotAt, domainFrom, domainTo) {
|
|
577
747
|
let total = 0;
|
|
748
|
+
const add = (slot, interval, share) => {
|
|
749
|
+
const part = interval.points * share;
|
|
750
|
+
slot.outside += part;
|
|
751
|
+
if (interval.costUsd === null) slot.outsideUnpriced += part;
|
|
752
|
+
else slot.outsideCostUsd += interval.costUsd * share;
|
|
753
|
+
total += part;
|
|
754
|
+
};
|
|
578
755
|
for (const interval of outside) {
|
|
579
756
|
const from = Math.max(interval.fromMs, domainFrom);
|
|
580
757
|
const to = Math.min(interval.toMs, domainTo);
|
|
581
758
|
if (interval.toMs <= interval.fromMs) {
|
|
582
759
|
if (interval.toMs < domainFrom || interval.toMs > domainTo) continue;
|
|
583
|
-
slots[slotAt(interval.toMs)]
|
|
584
|
-
total += interval.points;
|
|
760
|
+
add(slots[slotAt(interval.toMs)], interval, 1);
|
|
585
761
|
continue;
|
|
586
762
|
}
|
|
587
763
|
for (let index = slotAt(from); index < slots.length && slots[index].fromMs < to; index += 1) {
|
|
588
764
|
const slot = slots[index];
|
|
589
765
|
const overlap = Math.min(to, slot.toMs) - Math.max(from, slot.fromMs);
|
|
590
766
|
if (overlap <= 0) continue;
|
|
591
|
-
|
|
592
|
-
slot.outside += part;
|
|
593
|
-
total += part;
|
|
767
|
+
add(slot, interval, overlap / (interval.toMs - interval.fromMs));
|
|
594
768
|
}
|
|
595
769
|
}
|
|
596
770
|
return total;
|
|
@@ -602,10 +776,12 @@ function historyRow(instance, events) {
|
|
|
602
776
|
let tokens = 0;
|
|
603
777
|
let turns = 0;
|
|
604
778
|
let costUsd = 0;
|
|
779
|
+
let unpriced = 0;
|
|
605
780
|
for (let index = start; index < end; index += 1) {
|
|
606
781
|
tokens += events[index].tokens;
|
|
607
782
|
turns += events[index].turns;
|
|
608
783
|
costUsd += events[index].costUsd;
|
|
784
|
+
unpriced += events[index].turns - events[index].costKnownTurns;
|
|
609
785
|
}
|
|
610
786
|
return {
|
|
611
787
|
key: instance.key,
|
|
@@ -618,8 +794,13 @@ function historyRow(instance, events) {
|
|
|
618
794
|
current: instance.current,
|
|
619
795
|
tokens,
|
|
620
796
|
turns,
|
|
621
|
-
|
|
622
|
-
|
|
797
|
+
...quotaCostFields(
|
|
798
|
+
costUsd,
|
|
799
|
+
instance.estimate && !instance.outsideUnpriced ? instance.outsideCostUsd : null,
|
|
800
|
+
unpriced > 0
|
|
801
|
+
),
|
|
802
|
+
costPerPercent: instance.estimate ? round(instance.estimate.costPerPercent, 6) : null,
|
|
803
|
+
estimateSamples: instance.estimate?.samples ?? 0,
|
|
623
804
|
outside: round(instance.outside),
|
|
624
805
|
};
|
|
625
806
|
}
|
|
@@ -667,13 +848,20 @@ export function readQuotaHistory(
|
|
|
667
848
|
const domainTo = Math.max(period.toMs, domainFrom + MINUTE);
|
|
668
849
|
const overlapping = (instance) => instance.endMs > domainFrom && instance.startMs < domainTo;
|
|
669
850
|
const shown = instances.filter(overlapping);
|
|
851
|
+
const calibrated = calibratedWindows(listed, shown);
|
|
670
852
|
const events = readQuotaEvents(db, {
|
|
671
853
|
provider: selection.provider,
|
|
672
854
|
account: selection.account,
|
|
673
|
-
|
|
855
|
+
label: selection.label,
|
|
856
|
+
fromMs: Math.min(domainFrom, ...calibrated.map((instance) => instance.startMs)),
|
|
674
857
|
toMs: Math.max(domainTo, ...shown.map((instance) => instance.endMs)),
|
|
675
858
|
});
|
|
676
|
-
const { shares, outside } = allocateQuota(
|
|
859
|
+
const { shares, outside } = allocateQuota(
|
|
860
|
+
shown,
|
|
861
|
+
events,
|
|
862
|
+
finerOutsideSpans(db, selection, events, calibrated, now),
|
|
863
|
+
listed
|
|
864
|
+
);
|
|
677
865
|
const points = quotaPoints(shown);
|
|
678
866
|
|
|
679
867
|
const slotMs =
|
|
@@ -688,17 +876,17 @@ export function readQuotaHistory(
|
|
|
688
876
|
usage: emptyUsage(),
|
|
689
877
|
models: new Map(),
|
|
690
878
|
outside: 0,
|
|
879
|
+
outsideCostUsd: 0,
|
|
880
|
+
outsideUnpriced: 0,
|
|
691
881
|
});
|
|
692
882
|
}
|
|
693
883
|
const slotAt = (time) => Math.min(slots.length - 1, Math.max(0, Math.floor((time - domainFrom) / slotMs)));
|
|
694
884
|
const totals = emptyUsage();
|
|
695
885
|
const models = new Map();
|
|
696
|
-
let attributedCost = 0;
|
|
697
886
|
events.forEach((event, index) => {
|
|
698
887
|
if (event.ts < domainFrom || event.ts > domainTo) return;
|
|
699
888
|
const share = shares[index];
|
|
700
889
|
addUsage(totals, event, share);
|
|
701
|
-
if (share > 0) attributedCost += event.costUsd;
|
|
702
890
|
const model = models.get(event.model) || emptyUsage();
|
|
703
891
|
models.set(event.model, model);
|
|
704
892
|
addUsage(model, event, share);
|
|
@@ -709,6 +897,11 @@ export function readQuotaHistory(
|
|
|
709
897
|
addUsage(slotModel, event, share);
|
|
710
898
|
});
|
|
711
899
|
const outsideTotal = spreadOutsideUsage(outside, slots, slotAt, domainFrom, domainTo);
|
|
900
|
+
const costPerPercent = periodQuotaRate(shown, domainFrom, domainTo);
|
|
901
|
+
const outsideCostUsd =
|
|
902
|
+
costPerPercent === null || slots.some((slot) => slot.outsideUnpriced > 0)
|
|
903
|
+
? null
|
|
904
|
+
: slots.reduce((sum, slot) => sum + slot.outsideCostUsd, 0);
|
|
712
905
|
|
|
713
906
|
const exportedModels = [...models].map(([model, usage]) => ({ model, ...exportUsage(usage) })).sort(byConsumption);
|
|
714
907
|
const seriesKeys = exportedModels
|
|
@@ -743,8 +936,9 @@ export function readQuotaHistory(
|
|
|
743
936
|
summary: {
|
|
744
937
|
consumed: round(totals.consumed + outsideTotal),
|
|
745
938
|
outside: round(outsideTotal),
|
|
746
|
-
|
|
747
|
-
costPerPercent
|
|
939
|
+
...quotaCostFields(totals.costUsd, outsideCostUsd, totals.costKnownTurns < totals.turns),
|
|
940
|
+
costPerPercent,
|
|
941
|
+
estimateSamples: shown.reduce((sum, instance) => sum + (instance.estimate?.samples ?? 0), 0),
|
|
748
942
|
maxedOut: peaks.filter(
|
|
749
943
|
(instance) =>
|
|
750
944
|
instance.exhaustedAt !== null && instance.exhaustedAt >= domainFrom && instance.exhaustedAt <= domainTo
|
|
@@ -764,7 +958,13 @@ export function readQuotaHistory(
|
|
|
764
958
|
outside: round(slot.outside),
|
|
765
959
|
tokens: tokensOf(slot.usage),
|
|
766
960
|
turns: slot.usage.turns,
|
|
767
|
-
|
|
961
|
+
...quotaCostFields(
|
|
962
|
+
slot.usage.costUsd,
|
|
963
|
+
periodQuotaRate(shown, slot.fromMs, slot.toMs) === null || slot.outsideUnpriced > 0
|
|
964
|
+
? null
|
|
965
|
+
: slot.outsideCostUsd,
|
|
966
|
+
slot.usage.costKnownTurns < slot.usage.turns
|
|
967
|
+
),
|
|
768
968
|
models: [...slot.models]
|
|
769
969
|
.map(([model, usage]) => ({
|
|
770
970
|
model,
|
|
@@ -795,20 +995,23 @@ export function readQuotaWindows(
|
|
|
795
995
|
const selection = pickSelection(listQuotaSeries(db), { provider, account, label, inUse });
|
|
796
996
|
const base = { generatedAt: now, selection, page: 0, pageCount: 0, total: 0, windows: [] };
|
|
797
997
|
if (!selection) return base;
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
998
|
+
const chronological = quotaInstances(readQuotaRows(db, selection), selection.label, now).filter(
|
|
999
|
+
(instance) => !instance.idle
|
|
1000
|
+
);
|
|
1001
|
+
const listed = chronological.toReversed();
|
|
801
1002
|
const pageCount = Math.ceil(listed.length / HISTORY_PAGE_SIZE);
|
|
802
1003
|
if (!pageCount) return base;
|
|
803
1004
|
const index = Math.min(Math.max(0, Math.floor(page) || 0), pageCount - 1);
|
|
804
1005
|
const shown = listed.slice(index * HISTORY_PAGE_SIZE, (index + 1) * HISTORY_PAGE_SIZE);
|
|
1006
|
+
const calibrated = calibratedWindows(chronological, shown);
|
|
805
1007
|
const events = readQuotaEvents(db, {
|
|
806
1008
|
provider: selection.provider,
|
|
807
1009
|
account: selection.account,
|
|
808
|
-
|
|
1010
|
+
label: selection.label,
|
|
1011
|
+
fromMs: Math.min(...calibrated.map((instance) => instance.startMs)),
|
|
809
1012
|
toMs: Math.max(...shown.map((instance) => instance.endMs)),
|
|
810
1013
|
});
|
|
811
|
-
allocateQuota(shown, events);
|
|
1014
|
+
allocateQuota(shown, events, finerOutsideSpans(db, selection, events, calibrated, now), chronological);
|
|
812
1015
|
return {
|
|
813
1016
|
...base,
|
|
814
1017
|
page: index,
|
|
@@ -252,6 +252,41 @@ function mergeLegacyDays(db, { days, hourly, hourlyDay, fromDay, toDay }) {
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* One session's lifetime totals over every id its spend was recorded under.
|
|
257
|
+
* Compaction requests run under their own provider session id but are
|
|
258
|
+
* recorded under the source session, so this is the whole conversation
|
|
259
|
+
* across compactions. The same route choice as a rollup applies: a gateway
|
|
260
|
+
* summary never doubles a detail record.
|
|
261
|
+
*/
|
|
262
|
+
export function readSessionUsage(db, sessionIds) {
|
|
263
|
+
const total = empty();
|
|
264
|
+
const rows = db
|
|
265
|
+
.prepare(`
|
|
266
|
+
WITH mine AS (
|
|
267
|
+
SELECT day,route,COUNT(*) AS turns,SUM(input) AS input,SUM(output) AS output,
|
|
268
|
+
SUM(cache_read) AS cache_read,SUM(cache_write) AS cache_write,
|
|
269
|
+
SUM(cost_usd) AS cost_usd,SUM(duration_ms) AS duration_ms
|
|
270
|
+
FROM usage_events WHERE session IN (
|
|
271
|
+
SELECT id FROM usage_sessions WHERE value IN (SELECT value FROM json_each(?)))
|
|
272
|
+
GROUP BY day,route
|
|
273
|
+
), attributed AS (
|
|
274
|
+
SELECT printf('%04d-%02d-%02d',m.day/10000,(m.day/100)%100,m.day%100) AS day,
|
|
275
|
+
json_extract(r.signature,'$[0]') AS provider,
|
|
276
|
+
json_extract(r.signature,'$[1]') AS model,
|
|
277
|
+
json_extract(r.signature,'$[4]') AS cost_source,
|
|
278
|
+
json_extract(r.signature,'$[7]') AS rank,
|
|
279
|
+
m.turns,m.input,m.output,m.cache_read,m.cache_write,m.cost_usd,m.duration_ms
|
|
280
|
+
FROM mine m JOIN usage_routes r ON r.id=m.route
|
|
281
|
+
)
|
|
282
|
+
SELECT a.* FROM attributed a JOIN (${best}) b USING(day,provider,model,rank)
|
|
283
|
+
`)
|
|
284
|
+
.all(JSON.stringify(sessionIds), '0000-01-01', '9999-12-31');
|
|
285
|
+
for (const row of rows) add(total, row);
|
|
286
|
+
const { sessions, sessionsComplete, ...usage } = total;
|
|
287
|
+
return usage;
|
|
288
|
+
}
|
|
289
|
+
|
|
255
290
|
/** Read cached amounts; group retained attribution separately for distinct sessions. */
|
|
256
291
|
export function rollupUsage(
|
|
257
292
|
db,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { DatabaseSync } from 'node:sqlite';
|
|
8
8
|
import { serveWorkerRequests } from '../worker-requests.mjs';
|
|
9
9
|
import { UsageLedger } from './usage-ledger.mjs';
|
|
10
|
-
import { rollupUsage } from './usage-ledger-rollup.mjs';
|
|
10
|
+
import { readSessionUsage, rollupUsage } from './usage-ledger-rollup.mjs';
|
|
11
11
|
import { readQuotaHistory, readQuotaWindows } from './usage-ledger-quota.mjs';
|
|
12
12
|
import { applyUsageRepairIfUnchanged, selectUsageRepairCandidates } from './usage-ledger-repair.mjs';
|
|
13
13
|
import { unpricedCountAfter, unpricedPendingCounts } from './usage-pricing-refresh.mjs';
|
|
@@ -59,6 +59,7 @@ function recordBatch(ledger, rows) {
|
|
|
59
59
|
|
|
60
60
|
const operations = {
|
|
61
61
|
rollup: ({ path, options }) => read(path, (db) => rollupUsage(db, options)),
|
|
62
|
+
sessionUsage: ({ path, sessionIds }) => read(path, (db) => readSessionUsage(db, sessionIds)),
|
|
62
63
|
record: ({ path, rows }) => write(path, (ledger) => recordBatch(ledger, rows)),
|
|
63
64
|
recordQuota: ({ path, samples }) => write(path, (ledger) => ledger.recordQuota(samples)),
|
|
64
65
|
quotaHistory: ({ path, options }) => read(path, (db) => readQuotaHistory(db, options)),
|
|
@@ -11,7 +11,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
11
11
|
import { resolvePluginData } from '../plugin-paths.mjs';
|
|
12
12
|
import { usageRollupDayKey, isConversationUsageSource, num } from './usage-rollup.mjs';
|
|
13
13
|
import { priceUsage } from './cost.mjs';
|
|
14
|
-
import { rollupUsage } from './usage-ledger-rollup.mjs';
|
|
14
|
+
import { readSessionUsage, rollupUsage } from './usage-ledger-rollup.mjs';
|
|
15
15
|
import { QUOTA_SCHEMA, readQuotaHistory, readQuotaWindows, recordQuotaSamples } from './usage-ledger-quota.mjs';
|
|
16
16
|
import { SHARE_ENV } from 'node:worker_threads';
|
|
17
17
|
import { createWorkerRequestClient } from '../worker-requests.mjs';
|
|
@@ -67,6 +67,9 @@ const COMPACT_SCHEMA = `
|
|
|
67
67
|
-- table made a week of real records take ~400 ms instead of ~100 ms.
|
|
68
68
|
CREATE INDEX IF NOT EXISTS usage_events_time_usage ON usage_events(
|
|
69
69
|
ts,route,day,session,input,output,cache_read,cache_write,cost_usd,duration_ms);
|
|
70
|
+
-- One session's lifetime totals (the /context footer) read that session's
|
|
71
|
+
-- records only instead of scanning every record ever written.
|
|
72
|
+
CREATE INDEX IF NOT EXISTS usage_events_session ON usage_events(session);
|
|
70
73
|
`;
|
|
71
74
|
|
|
72
75
|
// Preserve the old read surface. Only storage changes: every field, including
|
|
@@ -542,6 +545,12 @@ export class UsageLedger {
|
|
|
542
545
|
return this.workerRequest('quotaWindows', { options });
|
|
543
546
|
}
|
|
544
547
|
|
|
548
|
+
/** Every request one session made under any of its ids, compactions included, read on the ledger worker. */
|
|
549
|
+
sessionUsageAsync(sessionIds) {
|
|
550
|
+
if (this.path === ':memory:') return Promise.resolve().then(() => readSessionUsage(this.db, sessionIds));
|
|
551
|
+
return this.workerRequest('sessionUsage', { sessionIds });
|
|
552
|
+
}
|
|
553
|
+
|
|
545
554
|
preserveLegacyDays(days) {
|
|
546
555
|
const insert = this.db.prepare('INSERT OR IGNORE INTO legacy_days VALUES (?,?)');
|
|
547
556
|
this.db.exec('BEGIN IMMEDIATE');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { updateJsonAtomicSync } from './atomic-file.mjs';
|
|
@@ -95,6 +95,16 @@ function update(provider, mutate) {
|
|
|
95
95
|
return normalizeLabels(result.providers[provider]);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The provider-side session one account sends under: account-bound
|
|
100
|
+
* connection and delta state stays apart per account without changing the
|
|
101
|
+
* visible Mixdog session. Usage recorded before spend kept the visible id
|
|
102
|
+
* carries this id, so a session's past spend is found through it as well.
|
|
103
|
+
*/
|
|
104
|
+
export function accountScopedSessionId(provider, accountId, sessionId) {
|
|
105
|
+
return `account-${createHash('sha256').update(`${provider}:${accountId}:${sessionId}`).digest('hex')}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
export function providerAccountPath(provider, id, dataDir = resolvePluginData()) {
|
|
99
109
|
requireAccountProvider(provider);
|
|
100
110
|
requireAccountId(id);
|
|
@@ -64,8 +64,8 @@ export function shellJobsStatus({ clientHostPid, sessionId } = {}) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Memory cycle L2 segment source. The daemon-hosted memory runtime writes
|
|
67
|
-
// data/memory-cycle-state.json on every cycle start/finish
|
|
68
|
-
//
|
|
67
|
+
// data/memory-cycle-state.json on every cycle start/finish through the
|
|
68
|
+
// cycle-scheduler health ledger — no HTTP call from the statusline path. Cached at
|
|
69
69
|
// the same 1s cadence as the shell segment. Shows a single unified "Memory"
|
|
70
70
|
// segment: running (spinner + elapsed) or backlog warning (yellow count).
|
|
71
71
|
let _memoryCycleSegmentCache = { at: 0, value: null };
|
|
@@ -102,10 +102,10 @@ async function refreshMemoryCycleStatus() {
|
|
|
102
102
|
// drop the spinner immediately instead of waiting out the 10-minute
|
|
103
103
|
// window. Pid-less markers (older daemons) keep the time-based guard.
|
|
104
104
|
const ownerAlive = !Number(running?.pid) || isPidAlive(Number(running.pid));
|
|
105
|
-
if (fresh && ownerAlive && running?.cycle && Number(running.started_at) > 0) {
|
|
105
|
+
if (fresh && ownerAlive && running?.cycle === 'cycle1' && Number(running.started_at) > 0) {
|
|
106
106
|
value = { kind: 'running', startedAt: Number(running.started_at) };
|
|
107
107
|
} else if (fresh) {
|
|
108
|
-
const pending =
|
|
108
|
+
const pending = Number(backlog?.unchunked) || 0;
|
|
109
109
|
if (pending > MEMORY_CYCLE_BACKLOG_WARN) value = { kind: 'backlog', pending };
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -252,7 +252,7 @@ export function isTranscriptCancelledStatusText(text) {
|
|
|
252
252
|
// The flexible Result separator covers legacy rows and command bodies whose
|
|
253
253
|
// embedded newlines lost their quote prefix during persistence.
|
|
254
254
|
// `lenientWrapper: false` drops the shape-only wrapper guess, matching the TUI
|
|
255
|
-
// rule in session-
|
|
255
|
+
// rule in local-session-runtime.mjs: a directly typed or pasted prompt that merely
|
|
256
256
|
// LOOKS like "instruction + Result: + quoted body" is real chat and must stay
|
|
257
257
|
// visible. Only runtime-injected rows may be hidden on the loose shape.
|
|
258
258
|
export function isInternalTranscriptDisplayText(text, { lenientWrapper = true } = {}) {
|