segreant 0.1.0 → 0.2.0
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/LICENSING.md +5 -3
- package/README.md +57 -19
- package/bin/segreant-statusline.mjs +20 -0
- package/bin/segreant.mjs +19 -8
- package/dist/alerts/detect.js +13 -0
- package/dist/billing/epistemic.js +4 -0
- package/dist/billing/importer.js +29 -10
- package/dist/billing/reconcile.js +122 -32
- package/dist/billing/scope.js +7 -0
- package/dist/budget/guard.js +33 -4
- package/dist/cli/allocCmd.js +73 -4
- package/dist/cli/askModelPrices.js +59 -0
- package/dist/cli/backupCmd.js +2 -1
- package/dist/cli/basisLines.js +45 -0
- package/dist/cli/billingCmd.js +68 -9
- package/dist/cli/capitalCmd.js +2 -1
- package/dist/cli/connectCmd.js +50 -9
- package/dist/cli/egressCmd.js +15 -3
- package/dist/cli/evidenceCmd.js +6 -3
- package/dist/cli/flags.js +68 -0
- package/dist/cli/homeCmd.js +117 -0
- package/dist/cli/importCmd.js +210 -54
- package/dist/cli/launchCmd.js +2 -1
- package/dist/cli/opsCmd.js +49 -2
- package/dist/cli/planCmd.js +181 -0
- package/dist/cli/pricingDeclareCmd.js +178 -0
- package/dist/cli/quotaCmd.js +190 -0
- package/dist/cli/runCmd.js +203 -51
- package/dist/cli/showCmd.js +37 -11
- package/dist/cli/teamCmd.js +5 -5
- package/dist/cli/ui.js +24 -8
- package/dist/cli/valueCmd.js +189 -14
- package/dist/cli.js +102 -21
- package/dist/config.js +95 -6
- package/dist/connect/antigravity.js +310 -0
- package/dist/connect/claudeCode.js +43 -3
- package/dist/connect/codex.js +38 -1
- package/dist/connect/importShared.js +58 -5
- package/dist/connect/opencode.js +37 -9
- package/dist/connect/protobuf.js +126 -0
- package/dist/connect/registry.js +46 -0
- package/dist/connect/repoRoot.js +112 -0
- package/dist/cost/basis.js +168 -0
- package/dist/cost/declarations.js +37 -0
- package/dist/cost/exactPricing.js +4 -1
- package/dist/cost/firstSight.js +98 -0
- package/dist/cost/overrides.js +230 -0
- package/dist/cost/pricing.js +228 -15
- package/dist/cost/reprice.js +130 -0
- package/dist/dashboard/claim-support.js +11 -8
- package/dist/dashboard/contracts.js +23 -3
- package/dist/dashboard/routes.js +980 -112
- package/dist/dashboard/sessions.js +345 -0
- package/dist/dashboard/settings.js +2 -1
- package/dist/dashboard/value-cache.js +105 -0
- package/dist/dashboard/web/app/components/claimInspector.js +6 -5
- package/dist/dashboard/web/app/components/drawer.js +105 -35
- package/dist/dashboard/web/app/components/ruleFields.js +144 -0
- package/dist/dashboard/web/app/components/spine.js +54 -24
- package/dist/dashboard/web/app/components/workflow.js +82 -0
- package/dist/dashboard/web/app/core/actions.js +734 -23
- package/dist/dashboard/web/app/core/api.js +37 -1
- package/dist/dashboard/web/app/core/chain.js +11 -4
- package/dist/dashboard/web/app/core/claimLayers.js +179 -51
- package/dist/dashboard/web/app/core/claimTypes.js +30 -0
- package/dist/dashboard/web/app/core/fmt.js +19 -2
- package/dist/dashboard/web/app/core/generated-contract.js +23 -3
- package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
- package/dist/dashboard/web/app/core/generated-types.js +1 -1
- package/dist/dashboard/web/app/core/griffin.js +10 -0
- package/dist/dashboard/web/app/core/jobs.js +12 -0
- package/dist/dashboard/web/app/core/period.js +35 -0
- package/dist/dashboard/web/app/core/periodWords.js +10 -0
- package/dist/dashboard/web/app/core/periods.js +32 -0
- package/dist/dashboard/web/app/core/pricingActions.js +264 -0
- package/dist/dashboard/web/app/core/privacyText.js +78 -0
- package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
- package/dist/dashboard/web/app/core/registry.js +79 -46
- package/dist/dashboard/web/app/core/slug.js +9 -0
- package/dist/dashboard/web/app/core/theme.js +48 -0
- package/dist/dashboard/web/app/core/view.js +69 -0
- package/dist/dashboard/web/app/main.js +197 -15
- package/dist/dashboard/web/app/views/allocation.js +249 -61
- package/dist/dashboard/web/app/views/control.js +57 -16
- package/dist/dashboard/web/app/views/data.js +4 -4
- package/dist/dashboard/web/app/views/evidence.js +336 -87
- package/dist/dashboard/web/app/views/privacy.js +13 -0
- package/dist/dashboard/web/app/views/spend.js +30 -15
- package/dist/dashboard/web/app/views/statement.js +189 -0
- package/dist/dashboard/web/app/views/system.js +44 -4
- package/dist/dashboard/web/app/views/value.js +105 -59
- package/dist/dashboard/web/app/views/workbench.js +312 -0
- package/dist/dashboard/web/classic.html +16 -4
- package/dist/dashboard/web/index.html +1 -1
- package/dist/dashboard/web/styles/app.css +536 -23
- package/dist/demo/seed.js +153 -14
- package/dist/economics/ledger.js +1 -1
- package/dist/egress/policy.js +129 -5
- package/dist/export/allocationCsv.js +70 -0
- package/dist/export/billingCsv.js +6 -2
- package/dist/export/csv.js +10 -2
- package/dist/export/economic.js +6 -2
- package/dist/export/statementCsv.js +104 -0
- package/dist/git/correlate.js +86 -9
- package/dist/git/quality.js +107 -24
- package/dist/git/repoScope.js +207 -0
- package/dist/git/survivalStore.js +186 -0
- package/dist/guide.js +9 -4
- package/dist/plans/detect.js +147 -0
- package/dist/proxy/server.js +134 -14
- package/dist/proxy/usage.js +9 -4
- package/dist/quota/limits.js +138 -0
- package/dist/quota/statusline.js +68 -0
- package/dist/quota/view.js +122 -0
- package/dist/scan/knownApps.js +6 -2
- package/dist/scan/scan.js +9 -0
- package/dist/store/allocation.js +14 -0
- package/dist/store/db.js +259 -34
- package/dist/store/realization.js +78 -14
- package/dist/store/schema.js +59 -1
- package/dist/store/scopeEvidence.js +67 -0
- package/dist/util/pool.js +19 -0
- package/dist/util/time.js +7 -0
- package/dist/value/realization.js +321 -30
- package/dist/value/report.js +28 -8
- package/dist/value/selfReported.js +10 -9
- package/docs/ARCHITECTURE.md +8 -4
- package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
- package/docs/CLI-JSON-CONTRACT.md +38 -0
- package/docs/DATA-BOUNDARIES.md +66 -6
- package/docs/FAQ.md +43 -9
- package/docs/GETTING-STARTED.md +19 -12
- package/docs/GUIDE.md +81 -18
- package/docs/INTEGRATIONS.md +9 -9
- package/docs/METHODOLOGY.md +3 -2
- package/docs/NEUTRALITY.md +1 -1
- package/docs/PRIVACY.md +51 -0
- package/docs/README.md +1 -0
- package/docs/RELEASE-GATE.md +52 -7
- package/docs/THREAT-MODEL.md +2 -1
- package/package.json +3 -2
- package/pricing/models.json +1366 -60
package/dist/config.js
CHANGED
|
@@ -37,6 +37,10 @@ function validNullableUsd(value) {
|
|
|
37
37
|
export function validateBudgetConfig(value) {
|
|
38
38
|
if (!isRecord(value))
|
|
39
39
|
throw new ConfigValidationError('budget must be an object');
|
|
40
|
+
const allowed = ['dailyUsd', 'dailySoftUsd', 'sessionUsd', 'runawayWindowSec', 'runawayMaxUsd', 'capIncludesImported'];
|
|
41
|
+
if (Object.keys(value).some((key) => !allowed.includes(key))) {
|
|
42
|
+
throw new ConfigValidationError('budget contains an unknown field; check the configured cap names');
|
|
43
|
+
}
|
|
40
44
|
for (const key of ['dailyUsd', 'dailySoftUsd', 'sessionUsd', 'runawayMaxUsd']) {
|
|
41
45
|
if (!validNullableUsd(value[key])) {
|
|
42
46
|
throw new ConfigValidationError(`budget.${key} must be null or a finite non-negative USD value`);
|
|
@@ -133,6 +137,10 @@ function exactUsdCap(value, label) {
|
|
|
133
137
|
}
|
|
134
138
|
const CAP_CACHE = new WeakMap();
|
|
135
139
|
export function exactBudgetCaps(cfg) {
|
|
140
|
+
// A caller can supply or mutate the live proxy config without loadConfig.
|
|
141
|
+
// Validate before consulting the cache: a new unknown key must never reuse
|
|
142
|
+
// previously valid, unlimited caps.
|
|
143
|
+
validateBudgetConfig(cfg);
|
|
136
144
|
const source = CAP_KEYS.map((key) => cfg[key]);
|
|
137
145
|
const cached = CAP_CACHE.get(cfg);
|
|
138
146
|
if (cached && cached.source.length === source.length
|
|
@@ -187,6 +195,32 @@ function validateFeatureOverrides(value) {
|
|
|
187
195
|
throw new ConfigValidationError(`features.${key} must be boolean`);
|
|
188
196
|
}
|
|
189
197
|
}
|
|
198
|
+
export const PLAN_SOURCES = ['claude-code', 'codex'];
|
|
199
|
+
/** Plan prices are bounded and shaped exactly; anything else stops the load. */
|
|
200
|
+
export function validatePlansConfig(value) {
|
|
201
|
+
if (!isRecord(value))
|
|
202
|
+
throw new ConfigValidationError('plans must be an object of tool -> { monthlyUsd, plan, setAt }');
|
|
203
|
+
for (const [source, price] of Object.entries(value)) {
|
|
204
|
+
if (!PLAN_SOURCES.includes(source)) {
|
|
205
|
+
throw new ConfigValidationError(`plans.${source}: unknown tool (expected one of ${PLAN_SOURCES.join(', ')})`);
|
|
206
|
+
}
|
|
207
|
+
if (!isRecord(price))
|
|
208
|
+
throw new ConfigValidationError(`plans.${source} must be an object`);
|
|
209
|
+
const usdValue = price.monthlyUsd;
|
|
210
|
+
if (typeof usdValue !== 'number' || !Number.isFinite(usdValue) || usdValue < 0 || usdValue > 100_000) {
|
|
211
|
+
throw new ConfigValidationError(`plans.${source}.monthlyUsd must be a number from 0 to 100000`);
|
|
212
|
+
}
|
|
213
|
+
if (price.plan !== null && (typeof price.plan !== 'string' || price.plan.length > 40)) {
|
|
214
|
+
throw new ConfigValidationError(`plans.${source}.plan must be null or a short name`);
|
|
215
|
+
}
|
|
216
|
+
if (typeof price.setAt !== 'string' || Number.isNaN(Date.parse(price.setAt))) {
|
|
217
|
+
throw new ConfigValidationError(`plans.${source}.setAt must be a date`);
|
|
218
|
+
}
|
|
219
|
+
const extra = Object.keys(price).filter((k) => !['monthlyUsd', 'plan', 'setAt'].includes(k));
|
|
220
|
+
if (extra.length > 0)
|
|
221
|
+
throw new ConfigValidationError(`plans.${source}: unexpected field ${extra[0]}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
190
224
|
export const DEFAULT_CONFIG = {
|
|
191
225
|
features: { ...FEATURE_DEFAULTS },
|
|
192
226
|
port: 8090,
|
|
@@ -238,6 +272,7 @@ export const DEFAULT_CONFIG = {
|
|
|
238
272
|
manifestUrl: null,
|
|
239
273
|
maxAgeDays: 30,
|
|
240
274
|
autoRefresh: false,
|
|
275
|
+
lookupUnknownModels: true,
|
|
241
276
|
},
|
|
242
277
|
perUser: {
|
|
243
278
|
enabled: false,
|
|
@@ -252,6 +287,7 @@ export const DEFAULT_CONFIG = {
|
|
|
252
287
|
retentionDays: 180,
|
|
253
288
|
metadataOnly: false,
|
|
254
289
|
proposalRetentionDays: 30,
|
|
290
|
+
plans: {},
|
|
255
291
|
};
|
|
256
292
|
/**
|
|
257
293
|
* The environment overrides. `SEGREANT_*` is the only family the product reads.
|
|
@@ -306,6 +342,23 @@ export class DemoReadOnlyError extends Error {
|
|
|
306
342
|
export function isDemo() {
|
|
307
343
|
return envOverride('DEMO') === '1';
|
|
308
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Raised when a command would write real usage into the sample store. In demo
|
|
347
|
+
* mode the open store is demo.db, so an import there would sit inside totals
|
|
348
|
+
* labelled as synthetic data while claiming to be isolated.
|
|
349
|
+
*/
|
|
350
|
+
export class DemoWriteBlockedError extends Error {
|
|
351
|
+
code = 'DEMO_WRITE_BLOCKED';
|
|
352
|
+
constructor(what) {
|
|
353
|
+
super(`Demo mode holds sample data only, so ${what} was not written. Run it without --demo to bring in your own usage; the sample data stays in demo.db.`);
|
|
354
|
+
this.name = 'DemoWriteBlockedError';
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/** Fail closed before any import writes into a store that is demo data. */
|
|
358
|
+
export function refuseDemoWrite(what) {
|
|
359
|
+
if (isDemo())
|
|
360
|
+
throw new DemoWriteBlockedError(what);
|
|
361
|
+
}
|
|
309
362
|
/** Remove the demo database (and its WAL/SHM sidecars) for a clean re-seed. */
|
|
310
363
|
export function unlinkDemoDb() {
|
|
311
364
|
for (const suffix of ['', '-wal', '-shm']) {
|
|
@@ -385,17 +438,21 @@ function sanitizePort(value, fallback) {
|
|
|
385
438
|
* an array of exact rule shapes, and boolean enabled flags.
|
|
386
439
|
*/
|
|
387
440
|
function sanitizeEgressConfig(value) {
|
|
388
|
-
|
|
441
|
+
// Absent is the default; present but malformed fails closed.
|
|
442
|
+
if (value === undefined)
|
|
389
443
|
return { mode: 'local_locked', rules: [] };
|
|
444
|
+
const invalid = { mode: 'invalid', rules: [] };
|
|
445
|
+
if (!isRecord(value))
|
|
446
|
+
return invalid;
|
|
390
447
|
if (value.mode !== 'local_locked' && value.mode !== 'controlled_cloud') {
|
|
391
|
-
return
|
|
448
|
+
return invalid;
|
|
392
449
|
}
|
|
393
450
|
const keys = Object.keys(value).sort();
|
|
394
451
|
if (keys.length !== 2 || keys[0] !== 'mode' || keys[1] !== 'rules') {
|
|
395
|
-
return
|
|
452
|
+
return invalid;
|
|
396
453
|
}
|
|
397
454
|
if (!Array.isArray(value.rules) || !value.rules.every((rule) => validateEgressRule(rule).length === 0)) {
|
|
398
|
-
return
|
|
455
|
+
return invalid;
|
|
399
456
|
}
|
|
400
457
|
return {
|
|
401
458
|
mode: value.mode,
|
|
@@ -411,13 +468,20 @@ export function loadConfig() {
|
|
|
411
468
|
else {
|
|
412
469
|
let raw;
|
|
413
470
|
try {
|
|
414
|
-
|
|
471
|
+
// Windows PowerShell 5.1 writes UTF-8 with a byte-order mark
|
|
472
|
+
// (`Set-Content -Encoding utf8`). The mark is not content, so it is
|
|
473
|
+
// skipped rather than reported as a parse error.
|
|
474
|
+
const text = readFileSync(path, 'utf8');
|
|
475
|
+
raw = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
|
|
415
476
|
}
|
|
416
477
|
catch (error) {
|
|
417
478
|
throw new ConfigValidationError(`cannot parse ${path}; repair or restore ${path}.bak before starting Segreant (${error instanceof Error ? error.message : String(error)})`);
|
|
418
479
|
}
|
|
419
480
|
if (!isRecord(raw))
|
|
420
481
|
throw new ConfigValidationError(`configuration root in ${path} must be an object`);
|
|
482
|
+
if (Object.keys(raw).some((key) => !Object.hasOwn(DEFAULT_CONFIG, key))) {
|
|
483
|
+
throw new ConfigValidationError('configuration contains an unknown field; check the configured section names');
|
|
484
|
+
}
|
|
421
485
|
if (Object.hasOwn(raw, 'features'))
|
|
422
486
|
validateFeatureOverrides(raw.features);
|
|
423
487
|
cfg = deepMerge(DEFAULT_CONFIG, raw);
|
|
@@ -431,8 +495,12 @@ export function loadConfig() {
|
|
|
431
495
|
port: sanitizePort(cfg.port, DEFAULT_CONFIG.port),
|
|
432
496
|
dashboardPort: sanitizePort(cfg.dashboardPort, DEFAULT_CONFIG.dashboardPort),
|
|
433
497
|
};
|
|
498
|
+
if (typeof cfg.pricing.lookupUnknownModels !== 'boolean') {
|
|
499
|
+
throw new ConfigValidationError('pricing.lookupUnknownModels must be true or false');
|
|
500
|
+
}
|
|
434
501
|
validateBudgetConfig(cfg.budget);
|
|
435
502
|
validateFeaturesConfig(cfg.features);
|
|
503
|
+
validatePlansConfig(cfg.plans);
|
|
436
504
|
return isDemo() ? withDemoDefaults(cfg) : cfg;
|
|
437
505
|
}
|
|
438
506
|
function configLockOwner(path) {
|
|
@@ -513,6 +581,27 @@ function assertConfigMutationLock(lock) {
|
|
|
513
581
|
throw new ConfigValidationError('config mutation lock ownership cannot be proven');
|
|
514
582
|
}
|
|
515
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* `invalid` is how a malformed egress section loads; it is never written. A
|
|
586
|
+
* write that does not touch egress (a cap, a plan price) keeps the section
|
|
587
|
+
* exactly as it is on disk, so the user's rules survive to be repaired. Writing
|
|
588
|
+
* the marker replaced the section with `{"mode":"invalid","rules":[]}`, and the
|
|
589
|
+
* rules were gone once the next write rotated the backup (independent review,
|
|
590
|
+
* 2026-10-18). A write that repairs egress sets a real mode and is written as is.
|
|
591
|
+
*/
|
|
592
|
+
function withPersistedEgress(config, path) {
|
|
593
|
+
if (config.egress.mode !== 'invalid')
|
|
594
|
+
return config;
|
|
595
|
+
let onDisk;
|
|
596
|
+
try {
|
|
597
|
+
const text = readFileSync(path, 'utf8');
|
|
598
|
+
onDisk = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text)['egress'];
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
throw new ConfigValidationError(`the egress section of ${path} is malformed and could not be kept as it is; repair it before changing other settings (${error instanceof Error ? error.message : String(error)})`);
|
|
602
|
+
}
|
|
603
|
+
return { ...config, egress: onDisk };
|
|
604
|
+
}
|
|
516
605
|
function persistConfigUnlocked(config) {
|
|
517
606
|
ensureHome();
|
|
518
607
|
validateBudgetConfig(config.budget);
|
|
@@ -520,7 +609,7 @@ function persistConfigUnlocked(config) {
|
|
|
520
609
|
const path = configPath();
|
|
521
610
|
const tempPath = `${path}.tmp-${randomUUID()}`;
|
|
522
611
|
const backupPath = `${path}.bak`;
|
|
523
|
-
const text = JSON.stringify(config, null, 2) + '\n';
|
|
612
|
+
const text = JSON.stringify(withPersistedEgress(config, path), null, 2) + '\n';
|
|
524
613
|
let descriptor = null;
|
|
525
614
|
try {
|
|
526
615
|
// Preserve the previous known-good config before replacing it. The target
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native metering for Google Antigravity (the IDE agent and the `agy` CLI).
|
|
3
|
+
*
|
|
4
|
+
* Antigravity keeps one SQLite file per conversation under
|
|
5
|
+
* ~/.gemini/antigravity/conversations and ~/.gemini/antigravity-cli/conversations.
|
|
6
|
+
* Each model call is a row of `gen_metadata` holding a protobuf
|
|
7
|
+
* `CortexStepGeneratorMetadata`; its `chat_model` (field 1) carries the call's
|
|
8
|
+
* `ModelUsageStats` (field 4) and the model that answered (`response_model`,
|
|
9
|
+
* field 19). Field numbers follow the published Windsurf/Antigravity schema
|
|
10
|
+
* (`exa.cortex_pb` and `exa.codeium_common_pb`, as mirrored in open-source
|
|
11
|
+
* Antigravity tooling); nothing is guessed from values.
|
|
12
|
+
*
|
|
13
|
+
* ModelUsageStats: 2 input_tokens · 3 output_tokens · 4 cache_write_tokens ·
|
|
14
|
+
* 5 cache_read_tokens · 9 thinking_output_tokens
|
|
15
|
+
*
|
|
16
|
+
* The same usage is repeated in each step's metadata and in `retry_infos`; only
|
|
17
|
+
* the generator's own usage is counted, once. The call's time is its first
|
|
18
|
+
* step's `created_at`; the workspace folder comes from the trajectory metadata.
|
|
19
|
+
*
|
|
20
|
+
* Read-only: each database is opened read-only, so Antigravity keeps writing.
|
|
21
|
+
* Antigravity prices nothing locally, so every row is list cost from the rate
|
|
22
|
+
* card; a model the card does not carry (Gemini 3.x today) gets the labelled
|
|
23
|
+
* fallback estimate, never an invented price.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
26
|
+
import { homedir } from 'node:os';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
30
|
+
import { computeCost } from '../cost/pricing.js';
|
|
31
|
+
import { emptyImportSummary, recordInsert, createRepoResolver, noteRelabel, markImportTruncated, fileStampForImport, noteFileImported, noteFileUnchanged, truncationMark, } from './importShared.js';
|
|
32
|
+
import { pbFields, pbMessage, pbString, pbTimestampMs, pbUint, pbUints } from './protobuf.js';
|
|
33
|
+
import { RESOURCE_LIMITS } from '../util/resource-limits.js';
|
|
34
|
+
/** ~/.gemini, where Antigravity keeps its data. null = not installed. */
|
|
35
|
+
export function defaultAntigravityRoot() {
|
|
36
|
+
const root = join(homedir(), '.gemini');
|
|
37
|
+
return ['antigravity', 'antigravity-cli'].some((d) => existsSync(join(root, d, 'conversations'))) ? root : null;
|
|
38
|
+
}
|
|
39
|
+
function measureRead(profile, read) {
|
|
40
|
+
if (profile === undefined)
|
|
41
|
+
return read();
|
|
42
|
+
const start = performance.now();
|
|
43
|
+
try {
|
|
44
|
+
return read();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
profile.readMs += performance.now() - start;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function providerFor(model) {
|
|
51
|
+
const m = model.toLowerCase();
|
|
52
|
+
if (m.startsWith('claude'))
|
|
53
|
+
return 'anthropic';
|
|
54
|
+
if (/^(gpt|o\d)/.test(m))
|
|
55
|
+
return 'openai';
|
|
56
|
+
return 'google';
|
|
57
|
+
}
|
|
58
|
+
function workspacePath(uri) {
|
|
59
|
+
if (uri === null || !uri.startsWith('file://'))
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
return fileURLToPath(uri);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Every model call in one conversation database. Pure over the database's rows. */
|
|
69
|
+
export function readAntigravityConversation(db, fallbackId, profile) {
|
|
70
|
+
const tableNames = new Set(measureRead(profile, () => db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all()).map((t) => t.name));
|
|
71
|
+
if (!tableNames.has('gen_metadata'))
|
|
72
|
+
return [];
|
|
73
|
+
let trajectoryId = fallbackId;
|
|
74
|
+
let cwd = null;
|
|
75
|
+
let startedMs = null;
|
|
76
|
+
if (tableNames.has('trajectory_metadata_blob')) {
|
|
77
|
+
const meta = measureRead(profile, () => db.prepare('SELECT data FROM trajectory_metadata_blob LIMIT 1').get());
|
|
78
|
+
if (meta?.data) {
|
|
79
|
+
const decodeStart = profile === undefined ? 0 : performance.now();
|
|
80
|
+
const f = pbFields(meta.data);
|
|
81
|
+
const workspace = pbMessage(f, 1);
|
|
82
|
+
cwd = workspace === null ? null : workspacePath(pbString(workspace, 1));
|
|
83
|
+
startedMs = pbTimestampMs(pbMessage(f, 2));
|
|
84
|
+
trajectoryId = pbString(f, 3) ?? trajectoryId;
|
|
85
|
+
if (profile !== undefined)
|
|
86
|
+
profile.decodeMs += performance.now() - decodeStart;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const stepTimes = new Map();
|
|
90
|
+
if (tableNames.has('steps')) {
|
|
91
|
+
const steps = measureRead(profile, () => db.prepare('SELECT idx, metadata FROM steps').all());
|
|
92
|
+
if (profile !== undefined)
|
|
93
|
+
profile.stepRows += steps.length;
|
|
94
|
+
const decodeStart = profile === undefined ? 0 : performance.now();
|
|
95
|
+
for (const s of steps) {
|
|
96
|
+
if (!s.metadata)
|
|
97
|
+
continue;
|
|
98
|
+
const t = pbTimestampMs(pbMessage(pbFields(s.metadata), 1));
|
|
99
|
+
if (t !== null)
|
|
100
|
+
stepTimes.set(s.idx, t);
|
|
101
|
+
}
|
|
102
|
+
if (profile !== undefined)
|
|
103
|
+
profile.decodeMs += performance.now() - decodeStart;
|
|
104
|
+
}
|
|
105
|
+
const calls = [];
|
|
106
|
+
const generators = measureRead(profile, () => db.prepare('SELECT idx, data FROM gen_metadata ORDER BY idx').all());
|
|
107
|
+
if (profile !== undefined)
|
|
108
|
+
profile.generatorRows += generators.length;
|
|
109
|
+
const decodeStart = profile === undefined ? 0 : performance.now();
|
|
110
|
+
for (const g of generators) {
|
|
111
|
+
if (!g.data)
|
|
112
|
+
continue;
|
|
113
|
+
const gen = pbFields(g.data);
|
|
114
|
+
const chat = pbMessage(gen, 1);
|
|
115
|
+
if (chat === null)
|
|
116
|
+
continue; // an injected response: no model call
|
|
117
|
+
const usage = pbMessage(chat, 4);
|
|
118
|
+
if (usage === null)
|
|
119
|
+
continue;
|
|
120
|
+
const inputTokens = pbUint(usage, 2);
|
|
121
|
+
const outputTokens = pbUint(usage, 3);
|
|
122
|
+
const cacheWriteTokens = pbUint(usage, 4);
|
|
123
|
+
const cacheReadTokens = pbUint(usage, 5);
|
|
124
|
+
if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
|
|
125
|
+
continue;
|
|
126
|
+
const model = pbString(chat, 19) || `antigravity-model-${pbUint(chat, 3)}`;
|
|
127
|
+
const firstStep = pbUints(gen, 2)[0];
|
|
128
|
+
const ts = (firstStep !== undefined ? stepTimes.get(firstStep) : undefined) ?? startedMs;
|
|
129
|
+
if (ts === null || ts === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
calls.push({
|
|
132
|
+
requestId: `antigravity:${trajectoryId}:${g.idx}`,
|
|
133
|
+
sessionId: trajectoryId,
|
|
134
|
+
tsEpochMs: ts,
|
|
135
|
+
model,
|
|
136
|
+
provider: providerFor(model),
|
|
137
|
+
cwd,
|
|
138
|
+
inputTokens,
|
|
139
|
+
outputTokens,
|
|
140
|
+
cacheWriteTokens,
|
|
141
|
+
cacheReadTokens,
|
|
142
|
+
reasoningTokens: pbUint(usage, 9),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (profile !== undefined) {
|
|
146
|
+
profile.decodeMs += performance.now() - decodeStart;
|
|
147
|
+
profile.decodedCalls += calls.length;
|
|
148
|
+
}
|
|
149
|
+
return calls;
|
|
150
|
+
}
|
|
151
|
+
function conversationFiles(root) {
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const sub of ['antigravity', 'antigravity-cli']) {
|
|
154
|
+
const dir = join(root, sub, 'conversations');
|
|
155
|
+
let names = [];
|
|
156
|
+
try {
|
|
157
|
+
names = readdirSync(dir);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
for (const n of names)
|
|
163
|
+
if (n.endsWith('.db'))
|
|
164
|
+
out.push(join(dir, n));
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
/** Imports in periodic commits rather than one commit per row (see Store.importBatch). */
|
|
169
|
+
export async function importAntigravity(store, opts = {}) {
|
|
170
|
+
const batch = store.importBatch();
|
|
171
|
+
try {
|
|
172
|
+
return await importAntigravityRows(store, opts);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
batch.end();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function importAntigravityRows(store, opts) {
|
|
179
|
+
const started = performance.now();
|
|
180
|
+
const profile = opts.onProfile === undefined ? undefined : {
|
|
181
|
+
filesRead: 0, stepRows: 0, generatorRows: 0, decodedCalls: 0, eligibleCalls: 0,
|
|
182
|
+
insertedRows: 0, readMs: 0, decodeMs: 0, filterMs: 0,
|
|
183
|
+
attributionMs: 0, pricingMs: 0, persistMs: 0, totalMs: 0,
|
|
184
|
+
};
|
|
185
|
+
const root = opts.root ?? defaultAntigravityRoot();
|
|
186
|
+
const source = opts.source ?? 'antigravity';
|
|
187
|
+
const sinceMs = opts.sinceMs ?? 0;
|
|
188
|
+
if (root === null) {
|
|
189
|
+
const empty = emptyImportSummary(0);
|
|
190
|
+
if (profile !== undefined) {
|
|
191
|
+
profile.totalMs = performance.now() - started;
|
|
192
|
+
opts.onProfile?.(profile);
|
|
193
|
+
}
|
|
194
|
+
return empty;
|
|
195
|
+
}
|
|
196
|
+
const files = conversationFiles(root);
|
|
197
|
+
const summary = emptyImportSummary(files.length);
|
|
198
|
+
const resolveProject = createRepoResolver();
|
|
199
|
+
let rows = 0;
|
|
200
|
+
for (const file of files) {
|
|
201
|
+
// A live conversation writes to its WAL first; the main file's size and time
|
|
202
|
+
// do not move until a checkpoint, so such a file is always read again.
|
|
203
|
+
let walLive = false;
|
|
204
|
+
try {
|
|
205
|
+
walLive = statSync(`${file}-wal`).size > 0;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
walLive = false;
|
|
209
|
+
}
|
|
210
|
+
const stamp = fileStampForImport(store, source, file, walLive ? { ...opts, rescan: true } : opts);
|
|
211
|
+
if (stamp === 'deferred') {
|
|
212
|
+
summary.filesDeferred = (summary.filesDeferred ?? 0) + 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (stamp === 'unchanged') {
|
|
216
|
+
noteFileUnchanged(store, source, file, summary);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const truncationsBefore = truncationMark(summary);
|
|
220
|
+
let db;
|
|
221
|
+
try {
|
|
222
|
+
db = new DatabaseSync(file, { readOnly: true });
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
continue; // locked or not a database: skipped, never a crash
|
|
226
|
+
}
|
|
227
|
+
if (profile !== undefined)
|
|
228
|
+
profile.filesRead += 1;
|
|
229
|
+
let calls = [];
|
|
230
|
+
try {
|
|
231
|
+
calls = readAntigravityConversation(db, file.replace(/^.*[\\/]/, '').replace(/\.db$/, ''), profile);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
calls = [];
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
db.close();
|
|
238
|
+
}
|
|
239
|
+
for (const c of calls) {
|
|
240
|
+
const filterStart = profile === undefined ? 0 : performance.now();
|
|
241
|
+
const tooOld = c.tsEpochMs < sinceMs;
|
|
242
|
+
if (profile !== undefined)
|
|
243
|
+
profile.filterMs += performance.now() - filterStart;
|
|
244
|
+
if (tooOld)
|
|
245
|
+
continue;
|
|
246
|
+
if (++rows > RESOURCE_LIMITS.importRows) {
|
|
247
|
+
markImportTruncated(summary, 'rows');
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
if (profile !== undefined)
|
|
251
|
+
profile.eligibleCalls += 1;
|
|
252
|
+
const attributionStart = profile === undefined ? 0 : performance.now();
|
|
253
|
+
const attribution = await resolveProject(c.cwd, 'antigravity');
|
|
254
|
+
if (profile !== undefined)
|
|
255
|
+
profile.attributionMs += performance.now() - attributionStart;
|
|
256
|
+
noteRelabel(summary, attribution);
|
|
257
|
+
// The rate card's lookup follows the model across providers (rateFor), so a
|
|
258
|
+
// Gemini row finds the Google table through any named provider.
|
|
259
|
+
const pricingStart = profile === undefined ? 0 : performance.now();
|
|
260
|
+
const cost = computeCost(c.provider === 'anthropic' ? 'anthropic' : 'openai', c.model, {
|
|
261
|
+
inputTokens: c.inputTokens,
|
|
262
|
+
outputTokens: c.outputTokens,
|
|
263
|
+
cacheWriteTokens: c.cacheWriteTokens,
|
|
264
|
+
cacheReadTokens: c.cacheReadTokens,
|
|
265
|
+
});
|
|
266
|
+
if (profile !== undefined)
|
|
267
|
+
profile.pricingMs += performance.now() - pricingStart;
|
|
268
|
+
const row = {
|
|
269
|
+
requestId: c.requestId,
|
|
270
|
+
sessionId: c.sessionId,
|
|
271
|
+
tsEpochMs: c.tsEpochMs,
|
|
272
|
+
provider: c.provider,
|
|
273
|
+
model: c.model,
|
|
274
|
+
project: attribution.project,
|
|
275
|
+
attributionBasis: attribution.basis,
|
|
276
|
+
taskWeight: 1,
|
|
277
|
+
inputTokens: c.inputTokens,
|
|
278
|
+
outputTokens: c.outputTokens,
|
|
279
|
+
cacheWriteTokens: c.cacheWriteTokens,
|
|
280
|
+
cacheReadTokens: c.cacheReadTokens,
|
|
281
|
+
reasoningTokens: c.reasoningTokens,
|
|
282
|
+
costUsd: cost.costUsd,
|
|
283
|
+
economicAmount: cost.exact?.total,
|
|
284
|
+
estimated: cost.estimated,
|
|
285
|
+
pricing: cost.pricing,
|
|
286
|
+
streamed: true,
|
|
287
|
+
statusCode: 200,
|
|
288
|
+
durationMs: null,
|
|
289
|
+
user: null,
|
|
290
|
+
source,
|
|
291
|
+
cwd: c.cwd,
|
|
292
|
+
};
|
|
293
|
+
const persistStart = profile === undefined ? 0 : performance.now();
|
|
294
|
+
store.upsertSession(c.sessionId, attribution.project, source, c.tsEpochMs);
|
|
295
|
+
recordInsert(store, summary, row, cost.estimated);
|
|
296
|
+
if (profile !== undefined)
|
|
297
|
+
profile.persistMs += performance.now() - persistStart;
|
|
298
|
+
}
|
|
299
|
+
const cursorStart = profile === undefined ? 0 : performance.now();
|
|
300
|
+
noteFileImported(store, source, file, stamp, opts, truncationsBefore, summary);
|
|
301
|
+
if (profile !== undefined)
|
|
302
|
+
profile.persistMs += performance.now() - cursorStart;
|
|
303
|
+
}
|
|
304
|
+
if (profile !== undefined) {
|
|
305
|
+
profile.insertedRows = summary.inserted;
|
|
306
|
+
profile.totalMs = performance.now() - started;
|
|
307
|
+
opts.onProfile?.(profile);
|
|
308
|
+
}
|
|
309
|
+
return summary;
|
|
310
|
+
}
|
|
@@ -24,10 +24,11 @@ import { createReadStream } from 'node:fs';
|
|
|
24
24
|
import { createInterface } from 'node:readline';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { homedir } from 'node:os';
|
|
27
|
-
import { computeCost } from '../cost/pricing.js';
|
|
27
|
+
import { cacheWriteDuration, computeCost } from '../cost/pricing.js';
|
|
28
28
|
import { projectKeyWithBasis } from '../value/characterization.js';
|
|
29
|
-
import { emptyImportSummary, recordInsert, createRepoResolver, noteRelabel, boundedJsonlFiles, markImportTruncated, fileStampForImport, noteFileImported, noteFileUnchanged, truncationMark, } from './importShared.js';
|
|
29
|
+
import { emptyImportSummary, recordInsert, createRepoResolver, noteRelabel, boundedJsonlFiles, markImportTruncated, fileStampForImport, noteFileImported, noteFileUnchanged, truncationMark, commitObservationsInLine, } from './importShared.js';
|
|
30
30
|
import { RESOURCE_LIMITS } from '../util/resource-limits.js';
|
|
31
|
+
import { claudeLimitEvent } from '../quota/limits.js';
|
|
31
32
|
/** Default transcript root: ~/.claude/projects (override for tests / unusual installs). */
|
|
32
33
|
export function defaultClaudeCodeRoot() {
|
|
33
34
|
return join(homedir(), '.claude', 'projects');
|
|
@@ -73,7 +74,10 @@ export function parseTranscriptLine(line) {
|
|
|
73
74
|
outputTokens: u.output_tokens ?? 0,
|
|
74
75
|
cacheWriteTokens: cacheWrite,
|
|
75
76
|
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
76
|
-
|
|
77
|
+
...(cacheWrite > 0 ? (() => {
|
|
78
|
+
const d = cacheWriteDuration(ttl5m, ttl1h);
|
|
79
|
+
return { ...(d.ttl === undefined ? {} : { cacheWriteTtl: d.ttl }), cacheWriteTtlObserved: d.observed };
|
|
80
|
+
})() : {}),
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
/**
|
|
@@ -106,6 +110,10 @@ async function importClaudeCodeRows(store, opts) {
|
|
|
106
110
|
const resolveProject = createRepoResolver();
|
|
107
111
|
for (const file of files) {
|
|
108
112
|
const stamp = fileStampForImport(store, source, file, opts);
|
|
113
|
+
if (stamp === 'deferred') {
|
|
114
|
+
summary.filesDeferred = (summary.filesDeferred ?? 0) + 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
109
117
|
if (stamp === 'unchanged') {
|
|
110
118
|
noteFileUnchanged(store, source, file, summary);
|
|
111
119
|
continue;
|
|
@@ -115,6 +123,30 @@ async function importClaudeCodeRows(store, opts) {
|
|
|
115
123
|
noteFileImported(store, source, file, stamp, opts, truncationsBefore, summary);
|
|
116
124
|
}
|
|
117
125
|
return summary;
|
|
126
|
+
// A tool result that shows git creating a commit names the session that made
|
|
127
|
+
// it. Each transcript line carries its own session id, time and directory.
|
|
128
|
+
function recordCommitsIn(line) {
|
|
129
|
+
const commits = commitObservationsInLine(line);
|
|
130
|
+
if (commits.length === 0)
|
|
131
|
+
return;
|
|
132
|
+
const sessionId = /"sessionId":"([^"]+)"/.exec(line)?.[1];
|
|
133
|
+
const ts = Date.parse(/"timestamp":"([^"]+)"/.exec(line)?.[1] ?? '');
|
|
134
|
+
if (!sessionId || !Number.isFinite(ts) || ts < sinceMs)
|
|
135
|
+
return;
|
|
136
|
+
let cwd = null;
|
|
137
|
+
const rawCwd = /"cwd":"((?:[^"\\]|\\.)*)"/.exec(line)?.[1];
|
|
138
|
+
if (rawCwd !== undefined) {
|
|
139
|
+
try {
|
|
140
|
+
cwd = JSON.parse(`"${rawCwd}"`);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
cwd = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const c of commits) {
|
|
147
|
+
store.recordObservedCommit({ source, sessionId, shortSha: c.shortSha, branch: c.branch, subject: c.subject, tsEpochMs: ts, cwd });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
118
150
|
async function importTranscriptFile(file) {
|
|
119
151
|
// One API request = many transcript lines; first wins. A copy in ANOTHER file
|
|
120
152
|
// (a resumed session) goes to the store, which recognises the same charge as a
|
|
@@ -126,6 +158,13 @@ async function importClaudeCodeRows(store, opts) {
|
|
|
126
158
|
markImportTruncated(summary, 'lines');
|
|
127
159
|
continue;
|
|
128
160
|
}
|
|
161
|
+
recordCommitsIn(line);
|
|
162
|
+
if (line.includes('"isApiErrorMessage":true')) {
|
|
163
|
+
const ts = Date.parse(/"timestamp":"([^"]+)"/.exec(line)?.[1] ?? '');
|
|
164
|
+
const limit = Number.isFinite(ts) && ts >= sinceMs ? claudeLimitEvent(line, ts) : null;
|
|
165
|
+
if (limit !== null)
|
|
166
|
+
store.recordQuotaEvent(limit);
|
|
167
|
+
}
|
|
129
168
|
const ev = parseTranscriptLine(line);
|
|
130
169
|
if (!ev || ev.tsEpochMs < sinceMs)
|
|
131
170
|
continue;
|
|
@@ -158,6 +197,7 @@ async function importClaudeCodeRows(store, opts) {
|
|
|
158
197
|
outputTokens: ev.outputTokens,
|
|
159
198
|
cacheWriteTokens: ev.cacheWriteTokens,
|
|
160
199
|
cacheReadTokens: ev.cacheReadTokens,
|
|
200
|
+
...(ev.cacheWriteTtl === undefined || !ev.cacheWriteTtlObserved ? {} : { cacheWriteTtl: ev.cacheWriteTtl }),
|
|
161
201
|
reasoningTokens: 0,
|
|
162
202
|
costUsd: cost.costUsd,
|
|
163
203
|
economicAmount: cost.exact?.total,
|
package/dist/connect/codex.js
CHANGED
|
@@ -25,8 +25,9 @@ import { join } from 'node:path';
|
|
|
25
25
|
import { homedir } from 'node:os';
|
|
26
26
|
import { computeCost, unpricedPricingEvidence } from '../cost/pricing.js';
|
|
27
27
|
import { projectKeyWithBasis } from '../value/characterization.js';
|
|
28
|
-
import { emptyImportSummary, recordInsert, createRepoResolver, noteRelabel, boundedJsonlFiles, markImportTruncated, fileStampForImport, noteFileImported, noteFileUnchanged, truncationMark, } from './importShared.js';
|
|
28
|
+
import { emptyImportSummary, recordInsert, createRepoResolver, noteRelabel, boundedJsonlFiles, markImportTruncated, fileStampForImport, noteFileImported, noteFileUnchanged, truncationMark, commitObservationsInLine, } from './importShared.js';
|
|
29
29
|
import { RESOURCE_LIMITS } from '../util/resource-limits.js';
|
|
30
|
+
import { codexRateLimitEvents } from '../quota/limits.js';
|
|
30
31
|
/** Codex home: ~/.codex (override with CODEX_HOME). null = not installed. */
|
|
31
32
|
export function defaultCodexRoot() {
|
|
32
33
|
const home = process.env.CODEX_HOME ?? join(homedir(), '.codex');
|
|
@@ -94,6 +95,7 @@ export async function parseCodexRollout(file, options = {}) {
|
|
|
94
95
|
let emittedRows = 0;
|
|
95
96
|
const maxRows = options.maxRows ?? RESOURCE_LIMITS.importRows;
|
|
96
97
|
const projFromCwd = (cwd) => projectKeyWithBasis(cwd, 'codex');
|
|
98
|
+
const lastMeter = new Map();
|
|
97
99
|
for await (const line of rl) {
|
|
98
100
|
if (Buffer.byteLength(line, 'utf8') > RESOURCE_LIMITS.transcriptLineBytes) {
|
|
99
101
|
options.onTruncatedLine?.();
|
|
@@ -107,6 +109,17 @@ export async function parseCodexRollout(file, options = {}) {
|
|
|
107
109
|
continue; // torn tail of a live session — next import gets it whole
|
|
108
110
|
}
|
|
109
111
|
const p = o.payload ?? {};
|
|
112
|
+
// A fork or subagent file opens with its parent's history replayed; its own
|
|
113
|
+
// turns begin after its first token count. Commits in the replay are the
|
|
114
|
+
// parent's, so they are not credited to this thread.
|
|
115
|
+
if (options.onCommit && o.type !== 'session_meta' && o.type !== 'turn_context' && !(forked && firstCount)) {
|
|
116
|
+
const thread = forkId ?? sessionId;
|
|
117
|
+
const ts = Date.parse(o.timestamp ?? '');
|
|
118
|
+
if (thread && Number.isFinite(ts)) {
|
|
119
|
+
for (const c of commitObservationsInLine(line))
|
|
120
|
+
options.onCommit({ sessionId: thread, ...c, tsEpochMs: ts, cwd });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
110
123
|
if (o.type === 'session_meta') {
|
|
111
124
|
// Only the first session_meta names this file's thread; later ones are
|
|
112
125
|
// the parent's context replayed into a subagent or fork.
|
|
@@ -134,6 +147,18 @@ export async function parseCodexRollout(file, options = {}) {
|
|
|
134
147
|
}
|
|
135
148
|
continue;
|
|
136
149
|
}
|
|
150
|
+
if (o.type === 'event_msg' && p.type === 'token_count' && options.onRateLimit && p.rate_limits !== undefined) {
|
|
151
|
+
const ts = Date.parse(o.timestamp ?? '');
|
|
152
|
+
if (Number.isFinite(ts)) {
|
|
153
|
+
for (const e of codexRateLimitEvents(p.rate_limits, ts)) {
|
|
154
|
+
const key = JSON.stringify([e.usedPercent, e.resetsAtMs]);
|
|
155
|
+
if (lastMeter.get(e.kind) === key)
|
|
156
|
+
continue;
|
|
157
|
+
lastMeter.set(e.kind, key);
|
|
158
|
+
options.onRateLimit(e);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
137
162
|
if (o.type === 'event_msg' && p.type === 'token_count') {
|
|
138
163
|
const info = p.info;
|
|
139
164
|
const tot = totalsFrom(info?.total_token_usage);
|
|
@@ -220,6 +245,10 @@ async function importCodexRows(store, opts) {
|
|
|
220
245
|
const resolveProject = createRepoResolver();
|
|
221
246
|
for (const file of files) {
|
|
222
247
|
const stamp = fileStampForImport(store, source, file, opts);
|
|
248
|
+
if (stamp === 'deferred') {
|
|
249
|
+
summary.filesDeferred = (summary.filesDeferred ?? 0) + 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
223
252
|
if (stamp === 'unchanged') {
|
|
224
253
|
noteFileUnchanged(store, source, file, summary);
|
|
225
254
|
continue;
|
|
@@ -229,6 +258,14 @@ async function importCodexRows(store, opts) {
|
|
|
229
258
|
await parseCodexRollout(file, {
|
|
230
259
|
onTruncatedLine: () => markImportTruncated(summary, 'lines'),
|
|
231
260
|
onTruncatedRow: () => markImportTruncated(summary, 'rows'),
|
|
261
|
+
onCommit: (c) => {
|
|
262
|
+
if (c.tsEpochMs >= sinceMs)
|
|
263
|
+
store.recordObservedCommit({ source, ...c });
|
|
264
|
+
},
|
|
265
|
+
onRateLimit: (e) => {
|
|
266
|
+
if (e.tsEpochMs >= sinceMs)
|
|
267
|
+
store.recordQuotaEvent(e);
|
|
268
|
+
},
|
|
232
269
|
onRow: async (ev) => {
|
|
233
270
|
if (ev.tsEpochMs < sinceMs)
|
|
234
271
|
return;
|