sandoichi 0.4.2 → 0.6.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.
@@ -33,19 +33,14 @@ function readMetricsSnapshot(metricsPath, { host, sessionId, model } = {}) {
33
33
  const records = scopedRecords(state.records, { host, sessionId });
34
34
  if (!records.length) return undefined;
35
35
  const report = buildMetricsReport({ ...state, records }, { sessionId });
36
- const hasProvider = records.some((item) => item.providerReportedSavingsTokens !== null);
37
- const hasEstimate = records.some((item) => item.providerReportedSavingsTokens === null);
38
- const providerSavings = report.cumulative.providerReportedSavingsTokens;
39
- const source = hasProvider && !hasEstimate && providerSavings !== null
40
- ? 'provider-reported' : 'estimate';
36
+ const source = 'estimate';
41
37
  const estimatedInputTokens = records.reduce((total, item) => total + item.estimatedInputTokens, 0);
42
38
  const latest = [...records].sort((left, right) => left.at.localeCompare(right.at)).at(-1);
43
39
  return {
44
40
  updatedAt: latestAt(records), source,
45
41
  model: model ?? latest?.model,
46
42
  estimatedInputTokens,
47
- savedTokens: source === 'provider-reported'
48
- ? providerSavings : report.cumulative.estimatedTransformSavingsTokens,
43
+ savedTokens: report.cumulative.estimatedTransformSavingsTokens,
49
44
  };
50
45
  } catch {
51
46
  return undefined;
@@ -85,7 +80,8 @@ function compactTokens(value) {
85
80
  export function renderStatusLine({ metrics } = {}) {
86
81
  if (!Number.isSafeInteger(metrics?.savedTokens) || metrics.savedTokens < 0
87
82
  || !Number.isSafeInteger(metrics?.estimatedInputTokens) || metrics.estimatedInputTokens <= 0) return '🥪 —';
88
- const percentage = Math.round(metrics.savedTokens / metrics.estimatedInputTokens * 100);
89
83
  const estimate = metrics.source === 'estimate' ? '~' : '';
90
- return `🥪 saved ${estimate}${compactTokens(metrics.savedTokens)} ctx tok (${percentage}%)`;
84
+ const percentage = metrics.source === 'estimate'
85
+ ? ` (${Math.round(metrics.savedTokens / metrics.estimatedInputTokens * 100)}%)` : '';
86
+ return `🥪 saved ${estimate}${compactTokens(metrics.savedTokens)} ctx tok${percentage}`;
91
87
  }
package/src/telemetry.mjs CHANGED
@@ -29,6 +29,20 @@ const F1_STATUSES = ['complete', 'partial', 'unavailable'];
29
29
  const F1_RATIO_BUCKETS = ['zero', 'lt_1pct', '1_to_10pct', 'gt_10pct', 'unavailable'];
30
30
  const F1_SIZE_BUCKETS = [...BYTE_BUCKETS, 'unavailable'];
31
31
  const F1_INPUT_BUCKETS = [...COUNT_BUCKETS, 'unavailable'];
32
+ // Reduction without coverage reads as better than it is: a day that bounds heavily on the 3% of
33
+ // commands it recognises looks identical to one that bounds everything. These are the reasons the
34
+ // shell classifier already emits, so they are a closed set and carry nothing free-form.
35
+ // Counts alone cannot answer "how much did it reach": 1,276 routed and 43,945 bypassed both land
36
+ // in `gt_100`, and so would the reverse. The ratio is the field that carries the answer.
37
+ export const COVERAGE_RATIO_BUCKETS = ['zero', 'lt_1pct', '1_to_10pct', '10_to_50pct', '50_to_90pct', 'gt_90pct'];
38
+
39
+ export const COVERAGE_REASONS = [
40
+ 'ambiguous-shell', 'compound-feeds-pipeline', 'compound-has-redirect', 'compound-segment-ambiguous',
41
+ 'grep-shape', 'head-shape', 'invalid-input', 'read-shape', 'routing-disabled', 'sed-shape',
42
+ 'tail-unbounded-from-end', 'unsafe-cwd', 'unsafe-grep-pattern', 'unsafe-grep-target',
43
+ 'unsafe-read-target', 'unsupported-shell', 'unsupported-tool', 'other',
44
+ ];
45
+
32
46
  export const FAILURE_STAGES = [
33
47
  'policy', 'input', 'redaction', 'optimization', 'artifact', 'output', 'upstream', 'response',
34
48
  ];
@@ -37,7 +51,7 @@ const SHARED_FIELDS = {
37
51
  schema_version: (value) => value === SCHEMA_VERSION,
38
52
  event: (value) => [
39
53
  'hook_summary', 'proxy_summary', 'active_day', 'hook_failure_summary', 'proxy_failure_summary',
40
- 'f1_footprint', 'f4_gateway',
54
+ 'f1_footprint', 'f4_gateway', 'coverage_summary',
41
55
  ].includes(value),
42
56
  day_utc: (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value),
43
57
  plugin_version: (value) => typeof value === 'string' && /^\d+\.\d+(?:\.\d+)?$/.test(value) && value.length <= MAX_STRING_LENGTH,
@@ -69,6 +83,14 @@ const PROXY_FAILURE_FIELDS = {
69
83
  provider: (value) => PROVIDERS.includes(value),
70
84
  failure_stage: (value) => FAILURE_STAGES.includes(value),
71
85
  };
86
+ const COVERAGE_FIELDS = {
87
+ host: (value) => HOSTS.includes(value),
88
+ routed_bucket: (value) => COUNT_BUCKETS.includes(value),
89
+ bypassed_bucket: (value) => COUNT_BUCKETS.includes(value),
90
+ coverage_ratio_bucket: (value) => COVERAGE_RATIO_BUCKETS.includes(value),
91
+ top_bypass_reason: (value) => COVERAGE_REASONS.includes(value),
92
+ };
93
+
72
94
  const F4_FIELDS = {
73
95
  f4_host: (value) => F4_HOSTS.includes(value),
74
96
  f4_operation: (value) => F4_OPERATIONS.includes(value),
@@ -91,6 +113,7 @@ function fieldsForEvent(eventType) {
91
113
  if (eventType === 'active_day') return ACTIVE_DAY_FIELDS;
92
114
  if (eventType === 'hook_failure_summary') return HOOK_FAILURE_FIELDS;
93
115
  if (eventType === 'f4_gateway') return F4_FIELDS;
116
+ if (eventType === 'coverage_summary') return COVERAGE_FIELDS;
94
117
  return PROXY_FAILURE_FIELDS;
95
118
  }
96
119
 
@@ -259,11 +282,11 @@ const LEASE_MS = 5 * 60 * 1000;
259
282
  const RETRY_DELAYS_MS = [60_000, 300_000, 1_800_000, 7_200_000, 21_600_000];
260
283
  const CHILD_ENV_KEYS = ['HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY', 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE'];
261
284
 
262
- function emptyCounters() { return { schema_version: TELEMETRY_CONFIG_VERSION, counters: {}, active_days: {} }; }
285
+ function emptyCounters() { return { schema_version: TELEMETRY_CONFIG_VERSION, counters: {}, active_days: {}, coverage_days: {} }; }
263
286
  function readCounters(countersPath) {
264
287
  if (!fs.existsSync(countersPath)) return emptyCounters();
265
288
  const state = JSON.parse(fs.readFileSync(countersPath, 'utf8'));
266
- return { ...state, counters: state.counters ?? {}, active_days: state.active_days ?? {} };
289
+ return { ...state, counters: state.counters ?? {}, active_days: state.active_days ?? {}, coverage_days: state.coverage_days ?? {} };
267
290
  }
268
291
 
269
292
  function readQueueRows(queuePath) {
@@ -320,9 +343,10 @@ function publicRow(row) {
320
343
  }
321
344
 
322
345
  function bucketEntry(entry, pluginVersion) {
346
+ const recordedVersion = entry.pluginVersion ?? pluginVersion;
323
347
  if (entry.event === 'hook_summary') {
324
348
  return {
325
- schema_version: SCHEMA_VERSION, event: 'hook_summary', day_utc: entry.day, plugin_version: PLUGIN_VERSION,
349
+ schema_version: SCHEMA_VERSION, event: 'hook_summary', day_utc: entry.day, plugin_version: recordedVersion,
326
350
  host: entry.host, mode: entry.mode,
327
351
  tool_calls_bucket: countBucket(entry.toolCalls ?? 0),
328
352
  capped_outputs_bucket: countBucket(entry.cappedOutputs ?? 0),
@@ -331,41 +355,70 @@ function bucketEntry(entry, pluginVersion) {
331
355
  };
332
356
  }
333
357
  if (entry.event === 'proxy_summary') return {
334
- schema_version: SCHEMA_VERSION, event: 'proxy_summary', day_utc: entry.day, plugin_version: PLUGIN_VERSION,
358
+ schema_version: SCHEMA_VERSION, event: 'proxy_summary', day_utc: entry.day, plugin_version: recordedVersion,
335
359
  provider: entry.provider ?? 'unknown', mode: entry.mode ?? 'enforce',
336
360
  rewrites_applied_bucket: countBucket(entry.rewritesApplied ?? 0),
337
361
  rewrites_skipped_cache_bucket: countBucket(entry.rewritesSkippedCache ?? 0),
338
362
  input_tokens_saved_bucket: byteBucket(entry.inputTokensSaved ?? 0),
339
363
  };
364
+ if (entry.event === 'coverage_summary') {
365
+ const reasons = Object.entries(entry)
366
+ .filter(([field, count]) => field.startsWith(REASON_PREFIX) && Number.isInteger(count) && count > 0)
367
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
368
+ const leading = reasons[0]?.[0].slice(REASON_PREFIX.length);
369
+ const routed = entry.routed ?? 0;
370
+ const bypassed = entry.bypassed ?? 0;
371
+ return {
372
+ schema_version: SCHEMA_VERSION, event: 'coverage_summary', day_utc: entry.day, plugin_version: recordedVersion,
373
+ host: entry.host,
374
+ routed_bucket: countBucket(routed),
375
+ bypassed_bucket: countBucket(bypassed),
376
+ coverage_ratio_bucket: coverageRatioBucket(routed, routed + bypassed),
377
+ // A reason this build does not know travels as `other`, never as free text.
378
+ top_bypass_reason: COVERAGE_REASONS.includes(leading) ? leading : 'other',
379
+ };
380
+ }
340
381
  if (entry.event === 'hook_failure_summary') return {
341
- schema_version: SCHEMA_VERSION, event: 'hook_failure_summary', day_utc: entry.day, plugin_version: PLUGIN_VERSION,
382
+ schema_version: SCHEMA_VERSION, event: 'hook_failure_summary', day_utc: entry.day, plugin_version: recordedVersion,
342
383
  host: entry.host, failure_stage: entry.failureStage,
343
384
  };
344
385
  return {
345
- schema_version: SCHEMA_VERSION, event: 'proxy_failure_summary', day_utc: entry.day, plugin_version: PLUGIN_VERSION,
386
+ schema_version: SCHEMA_VERSION, event: 'proxy_failure_summary', day_utc: entry.day, plugin_version: recordedVersion,
346
387
  provider: entry.provider, failure_stage: entry.failureStage,
347
388
  };
348
389
  }
349
390
 
350
391
  /** Accumulates raw per-day counts in memory/on disk; values are only bucketed (and thus
351
392
  * only ever leave the machine) once `closeDay` closes a finished UTC day. */
352
- export function incrementCounter({ statePaths, day, event, host, provider, mode, failureStage, deltas = {} }) {
353
- if (!['hook_summary', 'proxy_summary', 'hook_failure_summary', 'proxy_failure_summary'].includes(event)) {
393
+ export function incrementCounter({ statePaths, day, pluginVersion = PLUGIN_VERSION, event, host, provider, mode, failureStage, deltas = {} }) {
394
+ if (!['hook_summary', 'proxy_summary', 'hook_failure_summary', 'proxy_failure_summary', 'coverage_summary'].includes(event)) {
354
395
  throw new Error('incrementCounter: invalid event');
355
396
  }
356
397
  const isProxy = event.startsWith('proxy_');
357
398
  const dimension = isProxy ? provider : host;
358
- const key = event.includes('failure')
359
- ? [day, event, dimension, failureStage ?? ''].join('|')
360
- : [day, event, dimension, mode ?? ''].join('|');
399
+ if (!SHARED_FIELDS.plugin_version(pluginVersion)) throw new Error('incrementCounter: invalid plugin version');
400
+ const suffix = event.includes('failure') ? failureStage ?? '' : mode ?? '';
401
+ const key = [day, pluginVersion, event, dimension, suffix].join('|');
402
+ const legacyKey = [day, event, dimension, suffix].join('|');
361
403
  ensureDirectory(path.dirname(statePaths.counters));
362
404
  withLock(`${statePaths.counters}.lock`, () => {
363
405
  const state = readCounters(statePaths.counters);
364
- const existing = state.counters[key] ?? {
365
- day, event, ...(isProxy ? { provider: dimension } : { host: dimension }),
406
+ const legacy = state.counters[legacyKey];
407
+ let existing = state.counters[key];
408
+ if (legacy && !legacy.pluginVersion) {
409
+ if (existing) {
410
+ for (const [field, value] of Object.entries(legacy)) {
411
+ if (Number.isInteger(value) && value >= 0) existing[field] = (existing[field] ?? 0) + value;
412
+ }
413
+ } else existing = legacy;
414
+ delete state.counters[legacyKey];
415
+ }
416
+ existing ??= {
417
+ day, pluginVersion, event, ...(isProxy ? { provider: dimension } : { host: dimension }),
366
418
  ...(event.endsWith('_summary') && !event.includes('failure') ? { mode: mode ?? null } : {}),
367
419
  ...(event.includes('failure') ? { failureStage } : {}),
368
420
  };
421
+ existing.pluginVersion = pluginVersion;
369
422
  for (const [field, value] of Object.entries(deltas)) {
370
423
  if (!Number.isInteger(value) || value < 0) throw new Error(`incrementCounter: invalid delta ${field}`);
371
424
  existing[field] = (existing[field] ?? 0) + value;
@@ -375,23 +428,52 @@ export function incrementCounter({ statePaths, day, event, host, provider, mode,
375
428
  });
376
429
  }
377
430
 
378
- export function recordFailure({ statePaths, day, event, host, provider, failureStage }) {
431
+ export function recordFailure({ statePaths, day, pluginVersion = PLUGIN_VERSION, event, host, provider, failureStage }) {
379
432
  incrementCounter({
380
- statePaths, day, event, host, provider, failureStage, deltas: { count: 1 },
433
+ statePaths, day, pluginVersion, event, host, provider, failureStage, deltas: { count: 1 },
381
434
  });
382
435
  }
383
436
 
384
437
  /** Queues a single non-aggregate activity marker for this UTC day and host. */
385
- export function recordActiveDay({ statePaths, day, pluginVersion, host }) {
438
+ export const REASON_PREFIX = 'reason:';
439
+
440
+ export function coverageRatioBucket(routed, total) {
441
+ if (!Number.isInteger(routed) || !Number.isInteger(total) || routed < 0 || total < routed) {
442
+ throw new Error('coverageRatioBucket: invalid counts');
443
+ }
444
+ if (total === 0 || routed === 0) return 'zero';
445
+ const ratio = routed / total;
446
+ if (ratio < 0.01) return 'lt_1pct';
447
+ if (ratio < 0.1) return '1_to_10pct';
448
+ if (ratio < 0.5) return '10_to_50pct';
449
+ if (ratio < 0.9) return '50_to_90pct';
450
+ return 'gt_90pct';
451
+ }
452
+
453
+ /** One call per classified shell command. Counters close into a single row per day, next to the
454
+ * reduction they qualify: a large saving on a small share of commands should not read the same as
455
+ * a large saving on all of them. */
456
+ export function recordCoverage({ statePaths, day, pluginVersion = PLUGIN_VERSION, host, routed, reason }) {
457
+ const deltas = routed ? { routed: 1 } : { bypassed: 1 };
458
+ if (!routed) {
459
+ const label = COVERAGE_REASONS.includes(reason) ? reason : 'other';
460
+ deltas[`${REASON_PREFIX}${label}`] = 1;
461
+ }
462
+ incrementCounter({ statePaths, day, pluginVersion, event: 'coverage_summary', host, deltas });
463
+ }
464
+
465
+ export function recordActiveDay({ statePaths, day, pluginVersion = PLUGIN_VERSION, host }) {
386
466
  const marker = {
387
- schema_version: SCHEMA_VERSION, event: 'active_day', day_utc: day, plugin_version: PLUGIN_VERSION, host,
467
+ schema_version: SCHEMA_VERSION, event: 'active_day', day_utc: day, plugin_version: pluginVersion, host,
388
468
  };
389
469
  const validatedMarker = validateEvent(marker);
390
- const activeDayKey = `${day}|${host}`;
470
+ const activeDayKey = `${day}|${pluginVersion}|${host}`;
471
+ const legacyActiveDayKey = `${day}|${host}`;
391
472
  ensureDirectory(path.dirname(statePaths.counters));
392
473
  withLock(`${statePaths.counters}.lock`, () => {
393
474
  const state = readCounters(statePaths.counters);
394
475
  const activeDays = state.active_days;
476
+ if (Object.hasOwn(activeDays, legacyActiveDayKey)) delete activeDays[legacyActiveDayKey];
395
477
  const cutoff = Date.parse(`${day}T00:00:00Z`) - (ACTIVE_DAY_RETENTION_DAYS - 1) * 86_400_000;
396
478
  for (const [key, recordedDay] of Object.entries(activeDays)) {
397
479
  if (Date.parse(`${recordedDay}T00:00:00Z`) < cutoff) delete activeDays[key];