sandoichi 0.4.2 → 0.5.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.
@@ -8,6 +8,7 @@ import { exposeMcpResult, recoverStoredArtifact } from './artifact-store.mjs';
8
8
  import { optimizeToolOutput } from './core.mjs';
9
9
  import { ARTIFACT_TOOL_NAME } from './result-disclosure.mjs';
10
10
  import { PLUGIN_VERSION } from './version.mjs';
11
+ import { createSliceBridge, isSliceTool, SLICE_TOOLS, SliceRpcError } from './slice.mjs';
11
12
 
12
13
  const TOOL = {
13
14
  name: 'prepare_tool_output',
@@ -20,14 +21,16 @@ const TOOL = {
20
21
  };
21
22
  const ARTIFACT_TOOL = {
22
23
  name: ARTIFACT_TOOL_NAME,
23
- description: 'Recover a bounded redacted byte or line range from an artifact created in this MCP session.',
24
+ description: 'Recover bounded redacted content from an artifact created in this MCP session. Copy artifact.handle exactly into ref (for example, sando:sha256:0123456789abcdef). Omit range fields to select the full artifact; the response remains bounded by maxBytes (default 65536). Otherwise use either 0-based byte offsets or a 1-based inclusive line range, and omit fields for the unused mode.',
24
25
  inputSchema: {
25
26
  type: 'object', additionalProperties: false, required: ['ref'],
26
27
  properties: {
27
28
  ref: { type: 'string', pattern: '^sando:sha256:[a-f0-9]{16,64}$' },
28
- startByte: { type: 'integer', minimum: 0 }, endByte: { type: 'integer', minimum: 0 },
29
- startLine: { type: 'integer', minimum: 1 }, endLine: { type: 'integer', minimum: 1 },
30
- maxBytes: { type: 'integer', minimum: 1, maximum: 1048576 },
29
+ startByte: { type: 'integer', minimum: 0, description: '0-based inclusive byte offset.' },
30
+ endByte: { type: 'integer', minimum: 0, description: '0-based exclusive byte offset.' },
31
+ startLine: { type: 'integer', minimum: 1, description: '1-based inclusive line number.' },
32
+ endLine: { type: 'integer', minimum: 1, description: '1-based inclusive line number.' },
33
+ maxBytes: { type: 'integer', minimum: 1, maximum: 1048576, description: 'Maximum output bytes; omit for the default.' },
31
34
  },
32
35
  },
33
36
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
@@ -35,38 +38,70 @@ const ARTIFACT_TOOL = {
35
38
  const TOOLS = [TOOL, ARTIFACT_TOOL];
36
39
 
37
40
  function response(id, result) { return { jsonrpc: '2.0', id, result }; }
38
- function error(id, code, message) { return { jsonrpc: '2.0', id: id ?? null, error: { code, message } }; }
41
+ function error(id, code, message, data) { return { jsonrpc: '2.0', id: id ?? null, error: { code, message, ...(data === undefined ? {} : { data }) } }; }
42
+ function requestKey(id) { return `${typeof id}:${JSON.stringify(id)}`; }
39
43
 
40
- function dispatch(message) {
44
+ async function dispatch(message, bridge, active) {
41
45
  if (!message || message.jsonrpc !== '2.0' || typeof message.method !== 'string') return error(message?.id, -32600, 'Invalid Request');
46
+ if (message.method === 'notifications/cancelled') {
47
+ active.get(requestKey(message.params?.requestId))?.abort();
48
+ return null;
49
+ }
42
50
  if (message.id === undefined) return null;
43
51
  if (message.method === 'initialize') return response(message.id, {
44
52
  protocolVersion: message.params?.protocolVersion || '2025-06-18', capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'sando', version: PLUGIN_VERSION },
45
53
  });
46
54
  if (message.method === 'ping') return response(message.id, {});
47
- if (message.method === 'tools/list') return response(message.id, { tools: TOOLS });
55
+ const tools = [...TOOLS, ...SLICE_TOOLS()];
56
+ if (message.method === 'tools/list') return response(message.id, { tools });
48
57
  if (message.method === 'tools/call') {
49
- if (!TOOLS.some((tool) => tool.name === message.params?.name)) return error(message.id, -32602, 'Unknown tool');
58
+ if (!tools.some((tool) => tool.name === message.params?.name)) return error(message.id, -32602, 'Unknown tool');
59
+ const controller = new AbortController();
60
+ active.set(requestKey(message.id), controller);
50
61
  try {
62
+ if (isSliceTool(message.params.name)) {
63
+ return response(message.id, await bridge.call(message.params.name, message.params.arguments, { signal: controller.signal }));
64
+ }
51
65
  const result = message.params.name === TOOL.name
52
66
  ? optimizeToolOutput(message.params.arguments)
53
67
  : recoverStoredArtifact(message.params.arguments);
54
68
  const exposed = message.params.name === TOOL.name ? exposeMcpResult(result) : result;
55
69
  return response(message.id, { content: [{ type: 'text', text: exposed.inline ?? exposed.content }], structuredContent: exposed, isError: false });
56
70
  } catch (cause) {
71
+ if (cause instanceof SliceRpcError) return error(message.id, cause.code, cause.message, cause.data);
57
72
  return response(message.id, { content: [{ type: 'text', text: cause instanceof Error ? cause.message : 'invalid tool input' }], isError: true });
58
- }
73
+ } finally { active.delete(requestKey(message.id)); }
59
74
  }
60
75
  return error(message.id, -32601, 'Method not found');
61
76
  }
62
77
 
63
78
  export function startMcpServer() {
64
79
  const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
80
+ const bridge = createSliceBridge();
81
+ const active = new Map();
82
+ const pending = new Set();
65
83
  lines.on('line', (line) => {
66
- let output;
67
- try { output = dispatch(JSON.parse(line)); } catch { output = error(null, -32700, 'Parse error'); }
68
- if (output) process.stdout.write(`${JSON.stringify(output)}\n`);
84
+ let message;
85
+ try { message = JSON.parse(line); } catch { process.stdout.write(`${JSON.stringify(error(null, -32700, 'Parse error'))}\n`); return; }
86
+ const task = dispatch(message, bridge, active).then((output) => {
87
+ if (output) process.stdout.write(`${JSON.stringify(output)}\n`);
88
+ }).catch(() => process.stdout.write(`${JSON.stringify(error(message?.id, -32603, 'Internal error'))}\n`));
89
+ pending.add(task);
90
+ void task.finally(() => pending.delete(task));
69
91
  });
92
+ lines.once('close', async () => { await Promise.allSettled(pending); bridge.close(); });
93
+ let stopping = false;
94
+ const stop = async () => {
95
+ if (stopping) return;
96
+ stopping = true;
97
+ lines.close();
98
+ for (const controller of active.values()) controller.abort();
99
+ bridge.close();
100
+ await Promise.allSettled(pending);
101
+ process.exit(0);
102
+ };
103
+ process.once('SIGTERM', stop);
104
+ process.once('SIGINT', stop);
70
105
  }
71
106
 
72
107
  if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) startMcpServer();
package/src/metrics.mjs CHANGED
@@ -204,13 +204,14 @@ function providerSavings(providerUsage) {
204
204
  const value = (names) => names.map((name) => providerUsage[name]).find((candidate) => candidate !== undefined);
205
205
  const baseline = value(['baselineInputTokens', 'baseline_input_tokens']);
206
206
  const optimized = value(['optimizedInputTokens', 'optimized_input_tokens']);
207
+ const reported = value(['reportedSavingsTokens', 'reported_savings_tokens']);
208
+ if (baseline === undefined && optimized === undefined && reported === undefined) return null;
207
209
  if (baseline !== undefined || optimized !== undefined) {
208
210
  integer(baseline, 'baselineInputTokens');
209
211
  integer(optimized, 'optimizedInputTokens');
210
- return baseline - optimized;
212
+ return null;
211
213
  }
212
- const reported = value(['reportedSavingsTokens', 'reported_savings_tokens']);
213
- if (reported !== undefined) return integer(reported, 'reportedSavingsTokens', { min: -Number.MAX_SAFE_INTEGER });
214
+ integer(reported, 'reportedSavingsTokens', { min: -Number.MAX_SAFE_INTEGER });
214
215
  return null;
215
216
  }
216
217
 
@@ -11,6 +11,8 @@ const LOCK_WAIT_MS = 10;
11
11
  const LOCK_ATTEMPTS = 250;
12
12
  const STALE_LOCK_MS = 30_000;
13
13
  const COST_SCOPES = new Set(['session', 'event']);
14
+ const COST_SOURCES = new Set(['host-reported', 'provider-reported']);
15
+ const AGGREGATIONS = new Set(['session']);
14
16
 
15
17
  export const PROVIDER_USAGE_SCHEMA = SCHEMA;
16
18
  export const PROVIDER_USAGE_VERSION = VERSION;
@@ -71,11 +73,12 @@ function reportedCost(value) {
71
73
  function attachReportedCost(records, totalCostUsd) {
72
74
  if (!records.length || !usd(totalCostUsd)) return records;
73
75
  return records.map((item, index) => index === records.length - 1
74
- ? { ...item, totalCostUsd, costScope: 'session' } : item);
76
+ ? { ...item, totalCostUsd, costScope: 'session', costSource: 'host-reported' } : item);
75
77
  }
76
78
 
77
79
  function usageRecord({ host, source, sourceKey, sessionId, turnId, at, inputTokens, cachedInputTokens = 0,
78
- cacheWriteInputTokens = 0, outputTokens, reasoningOutputTokens = 0, totalCostUsd, arm, experimentId, workloadId }) {
80
+ cacheWriteInputTokens = 0, outputTokens, reasoningOutputTokens = 0, totalCostUsd, costSource,
81
+ aggregation, turnCount, arm, experimentId, workloadId, identityAt }) {
79
82
  if (!text(host) || !text(source) || !text(sourceKey)
80
83
  || (sessionId !== null && !text(sessionId)) || (turnId !== null && !text(turnId))
81
84
  || !text(at) || !counter(inputTokens) || !counter(cachedInputTokens)
@@ -83,8 +86,10 @@ function usageRecord({ host, source, sourceKey, sessionId, turnId, at, inputToke
83
86
  || !counter(outputTokens) || !counter(reasoningOutputTokens) || reasoningOutputTokens > outputTokens) return null;
84
87
  const totalTokens = safeSum(inputTokens, outputTokens);
85
88
  if (totalTokens === null) return null;
86
- const identity = JSON.stringify({ host, source, sourceKey, at, inputTokens, cachedInputTokens,
87
- cacheWriteInputTokens, outputTokens, reasoningOutputTokens, totalTokens, arm, experimentId, workloadId });
89
+ const identity = JSON.stringify({ host, source, sourceKey,
90
+ ...(identityAt === undefined ? { sessionId, turnId } : { at: identityAt }),
91
+ inputTokens, cachedInputTokens, cacheWriteInputTokens, outputTokens, reasoningOutputTokens,
92
+ totalTokens, aggregation, turnCount, arm, experimentId, workloadId });
88
93
  const result = {
89
94
  eventKey: `usage:${host}:${sha256(identity)}`,
90
95
  schema: SCHEMA, version: VERSION, host, source,
@@ -93,15 +98,18 @@ function usageRecord({ host, source, sourceKey, sessionId, turnId, at, inputToke
93
98
  reasoningOutputTokens, totalTokens,
94
99
  };
95
100
  if (totalCostUsd !== undefined && usd(totalCostUsd)) result.totalCostUsd = totalCostUsd;
101
+ if (costSource !== undefined) result.costSource = costSource;
102
+ if (aggregation !== undefined) result.aggregation = aggregation;
103
+ if (turnCount !== undefined) result.turnCount = turnCount;
96
104
  if (arm !== undefined) result.arm = arm;
97
105
  if (experimentId !== undefined) result.experimentId = experimentId;
98
106
  if (workloadId !== undefined) result.workloadId = workloadId;
99
107
  return result;
100
108
  }
101
109
 
102
- function claudeRecord(value, index, { sessionId = null, turnId = null, now, arm, experimentId, workloadId } = {}) {
103
- if (value.type !== 'assistant' || !record(value.message?.usage)) return null;
104
- const usage = value.message.usage;
110
+ function claudeUsageRecord(usage, { source, sourceKey, sessionId = null, turnId = null, at,
111
+ arm, experimentId, workloadId, aggregation, turnCount, identityAt } = {}) {
112
+ if (!record(usage)) return null;
105
113
  const inputTokens = usage.input_tokens;
106
114
  const cachedInputTokens = optionalCounter(usage.cache_read_input_tokens);
107
115
  const cacheWriteInputTokens = optionalCounter(usage.cache_creation_input_tokens);
@@ -112,13 +120,37 @@ function claudeRecord(value, index, { sessionId = null, turnId = null, now, arm,
112
120
  const totalTokens = usage.total_tokens === undefined ? safeSum(totalInputTokens, outputTokens) : usage.total_tokens;
113
121
  if (!counter(totalTokens) || totalTokens !== totalInputTokens + outputTokens) return null;
114
122
  return usageRecord({
115
- host: 'claude', source: 'claude-transcript', sourceKey: value.uuid ?? value.request_id ?? value.timestamp ?? String(index),
116
- sessionId, turnId: value.turn_id ?? turnId, at: isoDate(value.timestamp, now),
123
+ host: 'claude', source, sourceKey, sessionId, turnId, at,
117
124
  inputTokens: totalInputTokens, cachedInputTokens, cacheWriteInputTokens, outputTokens,
125
+ aggregation, turnCount, identityAt,
118
126
  arm, experimentId, workloadId,
119
127
  });
120
128
  }
121
129
 
130
+ function claudeRecord(value, index, { sessionId = null, turnId = null, now, arm, experimentId, workloadId } = {}) {
131
+ if (value.type !== 'assistant' || !record(value.message?.usage)) return null;
132
+ const identityAt = value.timestamp === undefined ? undefined : isoDate(value.timestamp, now);
133
+ return claudeUsageRecord(value.message.usage, {
134
+ source: 'claude-transcript', sourceKey: value.message.id ?? value.uuid ?? value.request_id ?? value.timestamp ?? String(index),
135
+ sessionId, turnId: value.turn_id ?? turnId, at: isoDate(value.timestamp, now), identityAt,
136
+ arm, experimentId, workloadId,
137
+ });
138
+ }
139
+
140
+ function claudeResultRecord(value, entries, { sessionId = null, arm, experimentId, workloadId, now } = {}) {
141
+ const identityTimestamp = value.timestamp ?? entries.slice().reverse().map((entry) => entry.value.timestamp).find(text);
142
+ const at = identityTimestamp ?? now;
143
+ const messages = new Set(entries
144
+ .filter(({ value: entry }) => entry.type === 'assistant' && record(entry.message?.usage))
145
+ .map(({ value: entry }, index) => entry.message.id ?? entry.uuid ?? entry.request_id ?? String(index)));
146
+ const turnCount = counter(value.num_turns ?? value.numTurns) ? value.num_turns ?? value.numTurns : messages.size || undefined;
147
+ return claudeUsageRecord(value.usage, {
148
+ source: 'claude-result', sourceKey: sessionId === null ? value.uuid ?? value.request_id ?? 'final' : 'session',
149
+ sessionId, turnId: null, at: isoDate(at, now), identityAt: identityTimestamp === undefined ? undefined : isoDate(identityTimestamp, now),
150
+ aggregation: 'session', turnCount, arm, experimentId, workloadId,
151
+ });
152
+ }
153
+
122
154
  function codexRecord(value, index, { sessionId = null, turnId = null, now, arm, experimentId, workloadId } = {}) {
123
155
  const usage = value.type === 'turn.completed'
124
156
  ? value.usage
@@ -135,9 +167,10 @@ function codexRecord(value, index, { sessionId = null, turnId = null, now, arm,
135
167
  || !counter(outputTokens) || reasoningOutputTokens === null) return null;
136
168
  const totalTokens = usage.total_tokens === undefined ? safeSum(inputTokens, outputTokens) : usage.total_tokens;
137
169
  if (!counter(totalTokens) || totalTokens !== inputTokens + outputTokens) return null;
170
+ const identityAt = value.timestamp === undefined ? undefined : isoDate(value.timestamp, now);
138
171
  return usageRecord({
139
172
  host: 'codex', source: 'codex-transcript', sourceKey: value.turn_id ?? value.id ?? value.timestamp ?? String(index),
140
- sessionId, turnId: value.turn_id ?? value.id ?? (value.timestamp ? `at:${value.timestamp}` : turnId), at: isoDate(value.timestamp, now),
173
+ sessionId, turnId: value.turn_id ?? value.id ?? (value.timestamp ? `at:${value.timestamp}` : turnId), at: isoDate(value.timestamp, now), identityAt,
141
174
  inputTokens, cachedInputTokens, cacheWriteInputTokens, outputTokens, reasoningOutputTokens,
142
175
  arm, experimentId, workloadId,
143
176
  });
@@ -145,9 +178,18 @@ function codexRecord(value, index, { sessionId = null, turnId = null, now, arm,
145
178
 
146
179
  export function parseClaudeTranscript(textValue, options = {}) {
147
180
  const entries = jsonLines(textValue);
148
- const records = entries.map(({ value }, index) => claudeRecord(value, index, options)).filter(Boolean);
181
+ const unique = new Map();
182
+ entries.forEach(({ value }, index) => {
183
+ const result = claudeRecord(value, index, options);
184
+ if (!result) return;
185
+ const messageId = text(value.message?.id);
186
+ const key = messageId ? `message:${messageId}` : result.eventKey;
187
+ unique.set(key, result);
188
+ });
189
+ const resultEntry = entries.findLast(({ value }) => value.type === 'result' && record(value.usage));
149
190
  const totalCostUsd = options.totalCostUsd ?? entries.slice().reverse().map(({ value }) => reportedCost(value)).find(usd);
150
- return attachReportedCost(records, totalCostUsd);
191
+ const aggregate = resultEntry ? claudeResultRecord(resultEntry.value, entries, options) : null;
192
+ return attachReportedCost(aggregate ? [aggregate] : [...unique.values()], totalCostUsd);
151
193
  }
152
194
 
153
195
  export function parseCodexTranscript(textValue, options = {}) {
@@ -188,6 +230,9 @@ function validateUsage(value) {
188
230
  || (value.experimentId !== undefined && !text(value.experimentId))
189
231
  || (value.workloadId !== undefined && !text(value.workloadId))
190
232
  || (value.totalCostUsd !== undefined && !usd(value.totalCostUsd))
233
+ || (value.costSource !== undefined && !COST_SOURCES.has(value.costSource))
234
+ || (value.aggregation !== undefined && !AGGREGATIONS.has(value.aggregation))
235
+ || (value.turnCount !== undefined && !counter(value.turnCount))
191
236
  || (value.costScope !== undefined && (!COST_SCOPES.has(value.costScope) || value.totalCostUsd === undefined))) {
192
237
  throw new Error('provider usage record is invalid');
193
238
  }
@@ -251,11 +296,19 @@ export function appendProviderUsage({ storagePath = defaultProviderUsagePath(),
251
296
  const state = readProviderUsage(filePath);
252
297
  const existing = new Map(state.records.map((item, index) => [item.eventKey, index]));
253
298
  for (const item of records) {
254
- const index = existing.get(item.eventKey);
299
+ const index = item.aggregation === 'session'
300
+ ? item.sessionId === null
301
+ ? existing.get(item.eventKey) ?? -1
302
+ : state.records.findIndex((candidate) => candidate.aggregation === 'session'
303
+ && candidate.host === item.host && candidate.source === item.source && candidate.sessionId === item.sessionId)
304
+ : existing.get(item.eventKey);
255
305
  if (index === undefined) {
256
306
  existing.set(item.eventKey, state.records.length);
257
307
  state.records.push(item);
258
- } else if (item.costScope === 'session') {
308
+ } else if (index < 0) {
309
+ existing.set(item.eventKey, state.records.length);
310
+ state.records.push(item);
311
+ } else if (item.aggregation === 'session' || item.costScope === 'session') {
259
312
  state.records[index] = item;
260
313
  }
261
314
  }
@@ -278,9 +331,20 @@ export function collectProviderUsage({ host, transcriptPath, sessionId = null, t
278
331
  }
279
332
 
280
333
  export function buildProviderUsageReport(state, { sessionId, pricing } = {}) {
281
- const records = validateState(state).records.filter((item) => sessionId === undefined || item.sessionId === sessionId);
282
- const sessions = new Set(records.map((item) => `${item.host}\0${item.sessionId ?? '<unknown>'}`));
283
- const turns = new Set(records.map((item, index) => `${item.host}\0${item.sessionId ?? '<unknown>'}\0${item.turnId ?? `record:${index}`}`));
334
+ const selected = validateState(state).records.filter((item) => sessionId === undefined || item.sessionId === sessionId);
335
+ const aggregateSessions = new Set(selected
336
+ .filter((item) => item.aggregation === 'session' && item.sessionId !== null)
337
+ .map(sessionKey));
338
+ const records = selected.filter((item) => item.aggregation === 'session'
339
+ || item.sessionId === null || !aggregateSessions.has(sessionKey(item)));
340
+ const sessions = new Set(records.map(sessionKey));
341
+ const turns = new Set(records.map(turnKey));
342
+ const aggregateRecords = records.filter((item) => item.aggregation === 'session');
343
+ const aggregateCounts = aggregateRecords.map((item) => item.turnCount);
344
+ const nonAggregateTurns = new Set(records.filter((item) => item.aggregation !== 'session').map(turnKey));
345
+ const turnCount = aggregateRecords.length
346
+ ? aggregateCounts.every(counter) ? safeSum(...aggregateCounts, nonAggregateTurns.size) : null
347
+ : turns.size;
284
348
  const sum = (field) => records.reduce((total, item) => total + item[field], 0);
285
349
  const weightedCostUnits = records.reduce((total, item) => total + computeWeightedUsage(item, pricing).costUnits, 0);
286
350
  const freshInputTokens = records.reduce((total, item) => total + item.inputTokens - item.cachedInputTokens - item.cacheWriteInputTokens, 0);
@@ -289,7 +353,7 @@ export function buildProviderUsageReport(state, { sessionId, pricing } = {}) {
289
353
  const effectiveRate = totalCostUsd !== null && totalTokens(records) > 0
290
354
  ? totalCostUsd / totalTokens(records) * 1_000_000 : null;
291
355
  const cost = {
292
- status: billing.complete ? 'provider-reported' : 'unavailable',
356
+ status: billing.complete ? billing.source : billing.partial ? billing.source : 'unavailable',
293
357
  coverage: billing.complete ? 'complete' : billing.partial ? 'partial' : 'none',
294
358
  totalCostUsd,
295
359
  effectiveRateUsdPerMillionTokens: effectiveRate,
@@ -299,18 +363,28 @@ export function buildProviderUsageReport(state, { sessionId, pricing } = {}) {
299
363
  inputTokens: sum('inputTokens'), cachedInputTokens: sum('cachedInputTokens'),
300
364
  cacheWriteInputTokens: sum('cacheWriteInputTokens'), freshInputTokens,
301
365
  outputTokens: sum('outputTokens'), reasoningOutputTokens: sum('reasoningOutputTokens'), totalTokens: sum('totalTokens'),
302
- turnCount: turns.size, weightedCostUnits,
366
+ turnCount, weightedCostUnits,
303
367
  weightedCost: { source: 'weighted-estimate', costUnits: weightedCostUnits },
304
368
  cost,
305
369
  totalCostUsd,
306
- providerReportedCostUsd: totalCostUsd,
370
+ providerReportedCostUsd: billing.source === 'provider-reported' && billing.complete ? totalCostUsd : null,
307
371
  sessionBlendedEffectiveRateUsdPerMillionTokens: effectiveRate,
308
372
  costSource: cost.status,
309
373
  };
310
374
  }
311
375
 
312
376
  function billingKey(item) {
313
- return `${item.host}\0${item.sessionId ?? '<unknown>'}`;
377
+ return sessionKey(item);
378
+ }
379
+
380
+ function sessionKey(item) {
381
+ return item.sessionId === null
382
+ ? `${item.host}\0unknown:${item.eventKey}`
383
+ : `${item.host}\0${item.sessionId}`;
384
+ }
385
+
386
+ function turnKey(item, index) {
387
+ return `${sessionKey(item)}\0${item.turnId ?? `record:${index}`}`;
314
388
  }
315
389
 
316
390
  function reportedCostSummary(records) {
@@ -321,18 +395,24 @@ function reportedCostSummary(records) {
321
395
  groups.set(billingKey(item), entries);
322
396
  });
323
397
  const totals = [];
398
+ const sources = new Set();
324
399
  for (const entries of groups.values()) {
325
400
  const sessionCosts = entries.filter((item) => item.costScope === 'session' && usd(item.totalCostUsd));
326
401
  if (sessionCosts.length) {
327
402
  const latest = sessionCosts.reduce((left, right) => right.at >= left.at ? right : left);
328
403
  totals.push(latest.totalCostUsd);
404
+ sources.add(latest.costSource ?? 'unknown');
329
405
  continue;
330
406
  }
331
- if (entries.every((item) => usd(item.totalCostUsd))) totals.push(sumUsd(entries.map((item) => item.totalCostUsd)));
407
+ if (entries.every((item) => usd(item.totalCostUsd))) {
408
+ totals.push(sumUsd(entries.map((item) => item.totalCostUsd)));
409
+ entries.forEach((item) => sources.add(item.costSource ?? 'unknown'));
410
+ }
332
411
  }
333
412
  const complete = groups.size > 0 && totals.length === groups.size && !totals.includes(null);
334
413
  const totalCostUsd = complete ? sumUsd(totals) : null;
335
- return { complete: complete && totalCostUsd !== null, partial: totals.length > 0, totalCostUsd };
414
+ const source = sources.size === 1 ? [...sources][0] : sources.size ? 'mixed' : null;
415
+ return { complete: complete && totalCostUsd !== null, partial: totals.length > 0, totalCostUsd, source: source ?? 'unknown' };
336
416
  }
337
417
 
338
418
  function totalTokens(records) {
package/src/proxy.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import net from 'node:net';
2
2
  import http from 'node:http';
3
+ import path from 'node:path';
3
4
  import tls from 'node:tls';
4
5
  import { brotliDecompressSync, gunzipSync, inflateSync, zstdDecompressSync } from 'node:zlib';
5
6
 
@@ -31,6 +32,7 @@ function recordProxyTelemetry({ env, provider, transformed, beforeText, afterTex
31
32
  incrementCounter({
32
33
  statePaths,
33
34
  day: todayUtc(),
35
+ pluginVersion: PLUGIN_VERSION,
34
36
  event: 'proxy_summary',
35
37
  provider: telemetryProvider(provider),
36
38
  mode: 'enforce',
@@ -52,7 +54,7 @@ function recordProxyFailure({ env, provider, failureStage }) {
52
54
  const statePaths = defaultTelemetryStatePaths(env);
53
55
  const day = todayUtc();
54
56
  recordFailure({
55
- statePaths, day, event: 'proxy_failure_summary',
57
+ statePaths, day, pluginVersion: PLUGIN_VERSION, event: 'proxy_failure_summary',
56
58
  provider: telemetryProvider(provider), failureStage,
57
59
  });
58
60
  closeFinishedDays({ statePaths, configPath, day, pluginVersion: PLUGIN_VERSION });
@@ -408,13 +410,18 @@ async function observeSemanticCandidates({ provider, candidates, semanticCompact
408
410
  export async function createProviderProxy({
409
411
  upstream, host = '127.0.0.1', port = 0, policy = {}, maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
410
412
  semanticCompactor, metricsPath, contextCapturePath, contextCaptureHost, contextSessionKey,
411
- f1TelemetryPublisher = publishF1Telemetry, transformProviderRequests = true,
413
+ f1TelemetryPublisher = publishF1Telemetry, transformProviderRequests = false,
414
+ historyArchiveRoot,
412
415
  env = process.env,
413
416
  } = {}) {
414
417
  const upstreamUrl = assertUpstream(upstream);
415
418
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new TypeError('port is invalid');
416
419
  if (!Number.isInteger(maxBodyBytes) || maxBodyBytes < 1024) throw new TypeError('maxBodyBytes is invalid');
417
420
  if (typeof transformProviderRequests !== 'boolean') throw new TypeError('transformProviderRequests is invalid');
421
+ if (transformProviderRequests && policy?.strategies?.recoverableArchive === true
422
+ && (typeof historyArchiveRoot !== 'string' || !path.isAbsolute(historyArchiveRoot))) {
423
+ throw new TypeError('historyArchiveRoot must be an absolute path');
424
+ }
418
425
  let lastStats = null;
419
426
  let lastRequestAt = null;
420
427
  const capturedContextSessions = new Set();
@@ -497,17 +504,17 @@ export async function createProviderProxy({
497
504
  const now = Date.now();
498
505
  const idleMs = lastRequestAt === null ? null : now - lastRequestAt;
499
506
  lastRequestAt = now;
500
- const transformed = transformProviderRequest({ provider, body: parsed, policy, idleMs });
507
+ const transformed = transformProviderRequest({ provider, body: parsed, policy, idleMs, historyArchiveRoot });
501
508
  if (transformed.changed) body = Buffer.from(JSON.stringify(transformed.body));
502
509
  const mechanicalContextTrimmedBytes = Math.max(0, rawBody.length - body.length);
503
510
  recordProxyTelemetry({
504
511
  env, provider, transformed,
505
512
  beforeText: rawBody.toString('utf8'), afterText: body.toString('utf8'),
506
513
  });
507
- lastStats = { provider, ...transformed.stats, mechanicalContextTrimmedBytes, changed: transformed.changed, reasons: transformed.reasons };
514
+ lastStats = { provider, ...transformed.stats, mechanicalContextTrimmedBytes, changed: transformed.changed, reasons: transformed.reasons, disclosures: transformed.disclosures };
508
515
  recordProvider = provider;
509
516
  recordModel = typeof parsed?.model === 'string' ? parsed.model : null;
510
- recordStats = { ...transformed.stats, mechanicalContextTrimmedBytes };
517
+ recordStats = { ...transformed.stats, mechanicalContextTrimmedBytes, disclosures: transformed.disclosures };
511
518
  if (typeof semanticCompactor === 'function') {
512
519
  const candidates = listSemanticCandidates({ provider, body: transformed.body });
513
520
  const stats = createSemanticStats(candidates);
@@ -51,7 +51,7 @@ function bytes(value, name) {
51
51
  }
52
52
 
53
53
  export function buildResultDisclosure({
54
- toolName, route, reason, inline, redactedText, inputBytes, redactedBytes, artifact,
54
+ toolName, route, reason, inline, redactedText, inputBytes, redactedBytes, artifact, elidedRange,
55
55
  } = {}) {
56
56
  if (typeof toolName !== 'string' || !toolName || typeof route !== 'string' || !route
57
57
  || typeof reason !== 'string' || !reason || typeof inline !== 'string' || typeof redactedText !== 'string') {
@@ -79,6 +79,11 @@ export function buildResultDisclosure({
79
79
  const recovery = !artifact && reason === 'artifact-admission-limit'
80
80
  ? { mode: 'unavailable', bounded: true }
81
81
  : undefined;
82
+ const recoveryCommand = artifact
83
+ ? (elidedRange && Number.isInteger(elidedRange.startLine) && Number.isInteger(elidedRange.endLine)
84
+ ? `sando artifact get --ref ${artifact.ref} --start-line ${elidedRange.startLine} --end-line ${elidedRange.endLine}`
85
+ : `sando artifact get --ref ${artifact.ref} --max-bytes 65536`)
86
+ : undefined;
82
87
  return {
83
88
  schema: RESULT_DISCLOSURE_SCHEMA,
84
89
  version: RESULT_DISCLOSURE_VERSION,
@@ -96,9 +101,10 @@ export function buildResultDisclosure({
96
101
  bytes: artifact.bytes,
97
102
  recovery: {
98
103
  tool: ARTIFACT_TOOL_NAME,
99
- command: `sando artifact get --ref ${artifact.ref} --max-bytes 65536`,
104
+ command: recoveryCommand,
100
105
  bounded: true,
101
106
  },
107
+ ...(elidedRange ? { elidedRange } : {}),
102
108
  } : null,
103
109
  };
104
110
  }