mixdog 0.9.175 → 1.0.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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/scripts/context-renewal-live-check.mjs +2 -1
  3. package/scripts/plan-runtime-release-tag.mjs +57 -0
  4. package/src/defaults/skills/xlsx/SKILL.md +1 -0
  5. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +7 -12
  6. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +6 -1
  7. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +2 -0
  8. package/src/runtime/agent/orchestrator/providers/cursor.mjs +20 -2
  9. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +3 -0
  10. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -0
  11. package/src/runtime/agent/orchestrator/runtime-core/goal-tasks.mjs +12 -1
  12. package/src/runtime/agent/orchestrator/session/result-classification.mjs +1 -1
  13. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +7 -3
  14. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -3
  15. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -0
  16. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +39 -3
  17. package/src/runtime/memory/lib/runtime-fetcher.mjs +94 -23
  18. package/src/runtime/office/capabilities-catalog.mjs +2 -0
  19. package/src/runtime/office/capabilities-signatures.mjs +12 -6
  20. package/src/runtime/office/capabilities.mjs +30 -10
  21. package/src/runtime/office/core/office-actions-open.mjs +1 -6
  22. package/src/runtime/office/core/office-sessions.mjs +9 -0
  23. package/src/runtime/office/portable/portable-xlsx-operations.mjs +2 -49
  24. package/src/runtime/office/portable/xlsx-contract.mjs +37 -3
  25. package/src/runtime/office/tool-defs.mjs +2 -2
  26. package/src/runtime/shared/llm/cost.mjs +16 -4
  27. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  28. package/src/runtime/shared/llm/model-catalog.mjs +48 -0
  29. package/src/runtime/shared/llm/model-pricing-rates.mjs +11 -4
  30. package/src/runtime/shared/llm/usage-accounting.mjs +12 -11
  31. package/src/runtime/shared/llm/usage-context.mjs +7 -0
  32. package/src/runtime/shared/llm/usage-ledger-repair.mjs +3 -1
  33. package/src/runtime/shared/llm/usage-ledger.mjs +1 -1
  34. package/src/runtime/shared/tool-card-model/card-status.mjs +7 -5
  35. package/src/runtime/shared/tool-card-model/git-result.mjs +10 -0
  36. package/src/runtime/shared/tool-card-model/terminal-status.mjs +6 -2
  37. package/src/runtime/shared/tool-card-model.golden-cases.mjs +2 -1
  38. package/src/runtime/shared/tool-result-summary.mjs +1 -0
  39. package/src/runtime/shared/tool-status.mjs +35 -2
  40. package/src/runtime/tidy/report.mjs +2 -2
  41. package/src/runtime/tidy/tool.mjs +6 -1
  42. package/src/tui/dist/index.mjs +88 -37
  43. package/src/tui/session/session-api/settings.mjs +16 -10
  44. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -2
@@ -139,6 +139,7 @@ export const CATALOG = {
139
139
  properties: {
140
140
  paragraph: ['style'],
141
141
  font: ['name', 'nameEastAsia', 'size', 'bold', 'italic', 'underline', 'color', 'hidden'],
142
+ documentFont: ['name', 'nameEastAsia', 'size', 'color'],
142
143
  page: [
143
144
  'pageSize',
144
145
  'orientation',
@@ -169,6 +170,7 @@ export const CATALOG = {
169
170
  'headerRows',
170
171
  'keepWithNext',
171
172
  ],
173
+ tableStyle: ['style', 'alignment', 'columnAlignments', 'columnWidths', 'borders', 'shading'],
172
174
  tableCell: [
173
175
  'fillColor',
174
176
  'horizontalAlignment',
@@ -1,8 +1,13 @@
1
1
  // Operation input signatures: which fields each Office operation requires,
2
2
  // accepts, or takes as alternatives, per format.
3
+ import { XLSX_VALIDATION_OPERATORS, XLSX_VALIDATION_TYPES } from './portable/xlsx-contract.mjs';
3
4
 
4
- export function signature(required = [], optional = [], { oneOf = [], propertySets = [], notes = '' } = {}) {
5
- return { required, optional, oneOf, propertySets, notes };
5
+ export function signature(
6
+ required = [],
7
+ optional = [],
8
+ { oneOf = [], propertySets = [], propertyBackends = {}, notes = '' } = {}
9
+ ) {
10
+ return { required, optional, oneOf, propertySets, propertyBackends, notes };
6
11
  }
7
12
 
8
13
  export const COMMON_SIGNATURES = {
@@ -66,9 +71,10 @@ export const FORMAT_SIGNATURES = {
66
71
  'properties.alignment places the table on the page (left, center, right); properties.columnAlignments sets the text of each column (one of left, center, right, justify per column); without it a column of figures (184,200, 2.1%, 2.6억 원) sets right and the rest left. Without style, borders, or shading the table takes a bold header row with a rule under it and hairlines between rows; headerBold:false keeps the header plain. properties.headerRows (default 1) is how many rows the header takes: a two-level header, its group label merged across the columns it spans (merge_table_cells), is headerRows:2, and every header row is set bold on its bottom edge and repeats on a continuation page.',
67
72
  }),
68
73
  set_table_style: signature(['table', 'properties'], [], {
69
- propertySets: ['table'],
74
+ propertySets: ['tableStyle'],
75
+ propertyBackends: { columnWidths: ['microsoft-office-com'] },
70
76
  notes:
71
- 'Replaces the table-level properties; columnAlignments re-aligns the text of every existing cell in each column.',
77
+ 'Replaces table-level properties; columnAlignments re-aligns existing cells. columnWidths is Microsoft Office only. For existing cell fonts or fills use set_table_cell_style, not set_table_style.',
72
78
  }),
73
79
  merge_table_cells: signature(['table', 'row', 'col'], ['rowSpan', 'colSpan'], {
74
80
  notes:
@@ -91,7 +97,7 @@ export const FORMAT_SIGNATURES = {
91
97
  move_paragraph: signature(['paragraph', 'index']),
92
98
  set_paragraph_style: signature(['paragraph', 'style']),
93
99
  set_document_font: signature(['properties'], [], {
94
- propertySets: ['font'],
100
+ propertySets: ['documentFont'],
95
101
  notes:
96
102
  "The document's own face, size, and ink — what every paragraph without its own reads, and what a paragraph added later in Word or by append_text starts from. properties: name, nameEastAsia, size, color.",
97
103
  }),
@@ -325,7 +331,7 @@ export const FORMAT_SIGNATURES = {
325
331
  ['sheet', 'type', 'operator', 'formula2', 'inputMessage', 'errorMessage'],
326
332
  {
327
333
  notes:
328
- 'type is list, whole, decimal, date, time, textLength, or custom; without it a formula naming choices ("a,b,c" or $A$1:$A$9) becomes a list and any other formula a custom rule. A ranged kind takes operator (between by default with formula2) and its bounds. Under protect_sheet the entry cells need set_style properties { locked: false } or nobody can type in them.',
334
+ `type: ${XLSX_VALIDATION_TYPES.join(', ')}. Without type, choices ("a,b,c" or $A$1:$A$9) mean list; other formulas mean custom. Numeric/date/time/textLength rules take operator: ${XLSX_VALIDATION_OPERATORS.join(', ')} (default between). between/notBetween require formula2; other operators use formula1. Example: type:"whole", operator:"greaterThanOrEqual", formula1:"0". Under protect_sheet unlock entry cells with set_style properties:{ locked:false }.`,
329
335
  }
330
336
  ),
331
337
  freeze_panes: signature([], ['sheet', 'row', 'column'], {
@@ -3,6 +3,7 @@ import { plainObject } from './shared/values.mjs';
3
3
  import { FIELD_ALIASES, OPERATION_ALIASES, PROPERTY_ALIASES } from './capabilities-aliases.mjs';
4
4
  import { BACKENDS, CATALOG, COMMON, VIRTUAL_OPERATIONS } from './capabilities-catalog.mjs';
5
5
  import { COMMON_SIGNATURES, FORMAT_SIGNATURES, signature } from './capabilities-signatures.mjs';
6
+ import { normalizeXlsxValidation } from './portable/xlsx-contract.mjs';
6
7
 
7
8
  export { OFFICE_ACTIONS } from './capabilities-catalog.mjs';
8
9
 
@@ -141,6 +142,19 @@ function describeHint(format, backend, operation) {
141
142
  })}.`;
142
143
  }
143
144
 
145
+ function operationProperties(catalog, signatureValue, backend) {
146
+ return Object.fromEntries(
147
+ signatureValue.propertySets
148
+ .filter((name) => catalog.properties[name])
149
+ .map((name) => [
150
+ name,
151
+ catalog.properties[name].filter(
152
+ (field) => !backend || !signatureValue.propertyBackends[field] || signatureValue.propertyBackends[field].includes(backend)
153
+ ),
154
+ ])
155
+ );
156
+ }
157
+
144
158
  function operationDescription(format, backend, catalog, requested) {
145
159
  const knownOperations = catalogOperations(format);
146
160
  const operation = resolveOperationAlias(format, requested);
@@ -152,11 +166,7 @@ function operationDescription(format, backend, catalog, requested) {
152
166
  }
153
167
  const available = operationsForBackend(format, backend);
154
168
  const signatureValue = operationSignature(format, operation);
155
- const properties = Object.fromEntries(
156
- signatureValue.propertySets
157
- .filter((name) => catalog.properties[name])
158
- .map((name) => [name, catalog.properties[name]])
159
- );
169
+ const properties = operationProperties(catalog, signatureValue, backend);
160
170
  return {
161
171
  name: operation,
162
172
  ...(VIRTUAL_OPERATIONS.has(operation) || VIRTUAL_OPERATIONS.has(`${format}:${operation}`) ? { virtual: true } : {}),
@@ -311,10 +321,10 @@ function hoistGeometryProperties(signatureValue, operation, allowed) {
311
321
  delete operation.properties;
312
322
  }
313
323
 
314
- function propertyKeySet(catalog, signatureValue) {
324
+ function propertyKeySet(catalog, signatureValue, backend) {
315
325
  return new Set(
316
- signatureValue.propertySets
317
- .flatMap((set) => catalog.properties?.[set] || [])
326
+ Object.values(operationProperties(catalog, signatureValue, backend))
327
+ .flat()
318
328
  .map((entry) => String(entry).split('.')[0])
319
329
  );
320
330
  }
@@ -474,14 +484,24 @@ function operationContractFaults(batch, operation, index) {
474
484
  if (cellFault) return [cellFault];
475
485
  hoistComposeSheetTable(format, name, operation);
476
486
  hoistGeometryProperties(signatureValue, operation, allowed);
477
- const propertyKeys = propertyKeySet(catalog, signatureValue);
487
+ const propertyKeys = propertyKeySet(catalog, signatureValue, backend);
478
488
  const missing = signatureValue.required.filter((field) => !suppliedField(operation, field, stableTargets));
479
- return [
489
+ const faults = [
480
490
  unknownFieldsFault(batch, name, operation, index, { allowed, propertyKeys, missing }),
481
491
  unknownPropertiesFault(batch, name, operation, index, propertyKeys),
482
492
  ...docxTableAlignmentFaults(format, name, operation, index),
483
493
  requiredInputFault(batch, name, operation, index, signatureValue, stableTargets),
484
494
  ].filter(Boolean);
495
+ if (!faults.length && format === 'xlsx' && name === 'add_validation') {
496
+ try {
497
+ Object.assign(operation, normalizeXlsxValidation(operation));
498
+ } catch (error) {
499
+ faults.push(
500
+ `XLSX operation "${name}" at operation ${index + 1}: ${error.message}. ${describeHint(format, backend, name)}`
501
+ );
502
+ }
503
+ }
504
+ return faults;
485
505
  }
486
506
 
487
507
  export function assertOfficeOperationContracts({ format = '', backend = '', operations = [] } = {}) {
@@ -10,12 +10,7 @@ import {
10
10
  releaseOfficeSession,
11
11
  toolResult,
12
12
  } from './office-core.mjs';
13
- import { createSession, openSession, snapshot } from './office-sessions.mjs';
14
-
15
- function initialOfficeOperations(args) {
16
- if (Array.isArray(args.operations) && args.operations.length) return args.operations;
17
- return Array.isArray(args.design?.operations) ? args.design.operations : [];
18
- }
13
+ import { createSession, initialOfficeOperations, openSession, snapshot } from './office-sessions.mjs';
19
14
 
20
15
  async function discardFailedOfficeSession(session, { action, initialEditSettled }) {
21
16
  if (session.reused) return;
@@ -3,6 +3,7 @@ import { constants as fsConstants } from 'node:fs';
3
3
  import { basename, dirname, extname, isAbsolute, join, resolve } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { randomUUID } from 'node:crypto';
6
+ import { assertOfficeOperationContracts } from '../capabilities.mjs';
6
7
  import {
7
8
  callMicrosoftOffice,
8
9
  detectMicrosoftOffice,
@@ -496,6 +497,11 @@ async function createMicrosoftOfficeSession(creation, args, mode) {
496
497
  });
497
498
  }
498
499
 
500
+ export function initialOfficeOperations(args) {
501
+ if (Array.isArray(args.operations) && args.operations.length) return args.operations;
502
+ return Array.isArray(args.design?.operations) ? args.design.operations : [];
503
+ }
504
+
499
505
  export async function createSession(args, cwd, dataDir) {
500
506
  const requestedPath = String(args.path || args.output || '').trim();
501
507
  if (!requestedPath) throw new Error('create requires path or output');
@@ -506,6 +512,8 @@ export async function createSession(args, cwd, dataDir) {
506
512
  if (format !== inferredFormat)
507
513
  throw new Error(`Office create format ${args.format} does not match target .${fileKind}`);
508
514
  assertCreateContentFields(format, args);
515
+ const initialOperations = initialOfficeOperations(args);
516
+ assertOfficeOperationContracts({ format, operations: initialOperations });
509
517
  // Whether this call brings the file into being decides what a failed create
510
518
  // may clean up afterwards: a file it wrote itself, never one already there.
511
519
  const targetExisted = await exists(target);
@@ -528,6 +536,7 @@ export async function createSession(args, cwd, dataDir) {
528
536
  throw new Error('Office create requires visible, background, or portable mode');
529
537
  }
530
538
  const selected = await selectMode(requestedMode, format, target);
539
+ assertOfficeOperationContracts({ format, backend: selected.backend, operations: initialOperations });
531
540
  if (selected.backend === 'mixdog-ooxml') return createPortableOoxmlSession(creation);
532
541
  return createMicrosoftOfficeSession(creation, args, selected.mode);
533
542
  }
@@ -1,7 +1,7 @@
1
1
  // Worksheet-level operations the portable XLSX backend applies: sheet
2
2
  // management, images, header/footer, conditional formats, validation, sort,
3
3
  // autofit, pivot tables and charts.
4
- import { conditionalFormatKind, listValidationChoices, listValidationFormula } from './xlsx-contract.mjs';
4
+ import { conditionalFormatKind, listValidationChoices, normalizeXlsxValidation } from './xlsx-contract.mjs';
5
5
  import { nextRelationshipId, partRelationshipPath, zipText } from './portable-opc.mjs';
6
6
  import {
7
7
  OFFICE_RELATIONSHIP_BASE,
@@ -45,29 +45,6 @@ function headerFooterFields(text) {
45
45
  .replace(/\{file\}/gi, '&F');
46
46
  }
47
47
 
48
- // Cell validation as both backends express it: the OOXML names here, the Excel
49
- // enumeration in the COM host.
50
- const XLSX_VALIDATION_TYPES = Object.freeze({
51
- list: 'list',
52
- whole: 'whole',
53
- decimal: 'decimal',
54
- date: 'date',
55
- time: 'time',
56
- textlength: 'textLength',
57
- custom: 'custom',
58
- });
59
-
60
- const XLSX_VALIDATION_OPERATORS = Object.freeze({
61
- between: 'between',
62
- notbetween: 'notBetween',
63
- equal: 'equal',
64
- notequal: 'notEqual',
65
- greaterthan: 'greaterThan',
66
- lessthan: 'lessThan',
67
- greaterthanorequal: 'greaterThanOrEqual',
68
- lessthanorequal: 'lessThanOrEqual',
69
- });
70
-
71
48
  export const TABLE_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml';
72
49
 
73
50
  // A built-in table style bands the range from the workbook's own theme, which
@@ -330,18 +307,6 @@ async function expressionConditionalRule(zip, op, priority) {
330
307
 
331
308
  // The comparison a bounded validation applies; a second bound without one
332
309
  // means between.
333
- function validationOperator(op, type) {
334
- const requested = String(op.operator || '').trim();
335
- let operator = '';
336
- if (requested && Object.hasOwn(XLSX_VALIDATION_OPERATORS, requested.toLowerCase())) {
337
- operator = XLSX_VALIDATION_OPERATORS[requested.toLowerCase()];
338
- } else if (op.formula2 != null && !['list', 'custom'].includes(type)) operator = 'between';
339
- if (requested && !operator) {
340
- throw new Error(`add_validation operator must be one of ${Object.keys(XLSX_VALIDATION_OPERATORS).join(', ')}`);
341
- }
342
- return operator;
343
- }
344
-
345
310
  function dataValidationXml(op, { type, operator, reference }) {
346
311
  const formula = (value) => `${xmlEncode(String(value).replace(/^=/, ''))}`;
347
312
  return (
@@ -359,19 +324,7 @@ function dataValidationXml(op, { type, operator, reference }) {
359
324
  /** One data validation over a range, appended to the validations already there. */
360
325
  export function addWorksheetValidation(zip, sheet, xml, op) {
361
326
  const reference = areaReference(parseAreaRange(op.range));
362
- // A list is the common case and what Excel writes through the same
363
- // operation, so it is the default; the other kinds guard a number, a
364
- // date, or a length, and take a second bound. A formula that states a
365
- // rule rather than naming choices is that rule, not a dropdown of one
366
- // entry: "서울,부산" and $A$1:$A$9 are lists, B2>0 is a custom check.
367
- const kind = String(op.type || (listValidationFormula(op.formula1) ? 'list' : 'custom'))
368
- .trim()
369
- .toLowerCase();
370
- const type = Object.hasOwn(XLSX_VALIDATION_TYPES, kind) ? XLSX_VALIDATION_TYPES[kind] : '';
371
- if (!type) {
372
- throw new Error(`add_validation type must be one of ${Object.keys(XLSX_VALIDATION_TYPES).join(', ')}`);
373
- }
374
- const operator = validationOperator(op, type);
327
+ const { type, operator = '' } = normalizeXlsxValidation(op);
375
328
  const existing = worksheetSection(xml, 'dataValidations');
376
329
  const previous = existing ? containerBody(existing[0], 'dataValidations') : '';
377
330
  const count = (previous.match(/<dataValidation\b/g) || []).length + 1;
@@ -246,6 +246,42 @@ export function listValidationChoices(formula1) {
246
246
  return `"${text}"`;
247
247
  }
248
248
 
249
+ export const XLSX_VALIDATION_TYPES = Object.freeze(['list', 'whole', 'decimal', 'date', 'time', 'textLength', 'custom']);
250
+ export const XLSX_VALIDATION_OPERATORS = Object.freeze([
251
+ 'between',
252
+ 'notBetween',
253
+ 'equal',
254
+ 'notEqual',
255
+ 'greaterThan',
256
+ 'lessThan',
257
+ 'greaterThanOrEqual',
258
+ 'lessThanOrEqual',
259
+ ]);
260
+
261
+ // Resolve and validate once, before either Excel COM or the portable writer
262
+ // can interpret an unknown name as a different rule.
263
+ export function normalizeXlsxValidation(operation) {
264
+ if (!String(operation.formula1 ?? '').trim()) throw new Error('add_validation requires non-empty formula1');
265
+ const requestedType = String(operation.type ?? '').trim() || (listValidationFormula(operation.formula1) ? 'list' : 'custom');
266
+ const type = XLSX_VALIDATION_TYPES.find((value) => value.toLowerCase() === requestedType.toLowerCase());
267
+ if (!type) throw new Error(`add_validation type must be one of ${XLSX_VALIDATION_TYPES.join(', ')}`);
268
+ const requestedOperator = String(operation.operator ?? '').trim();
269
+ const bounded = !['list', 'custom'].includes(type);
270
+ const operator = requestedOperator
271
+ ? XLSX_VALIDATION_OPERATORS.find((value) => value.toLowerCase() === requestedOperator.toLowerCase())
272
+ : bounded ? 'between' : '';
273
+ if (requestedOperator && !operator) {
274
+ throw new Error(`add_validation operator must be one of ${XLSX_VALIDATION_OPERATORS.join(', ')}`);
275
+ }
276
+ if (!bounded && requestedOperator) {
277
+ throw new Error(`add_validation operator is unsupported for type "${type}"; omit operator`);
278
+ }
279
+ if (['between', 'notBetween'].includes(operator) && !String(operation.formula2 ?? '').trim()) {
280
+ throw new Error(`add_validation operator "${operator}" requires formula2; for one bound use a comparison operator`);
281
+ }
282
+ return { type, ...(operator ? { operator } : {}) };
283
+ }
284
+
249
285
  // Three ways a sheet marks its numbers: a rule that paints the cells it picks,
250
286
  // a scale that colors every cell by where its value sits, and a bar drawn in
251
287
  // the cell. The first needs a formula and a format; the other two carry their
@@ -394,9 +430,7 @@ function validateXlsxOperation(operation) {
394
430
  // Both backends write the kind named here, so the default is settled once:
395
431
  // a formula that names choices is a dropdown, a formula that states a test
396
432
  // is a custom rule. Writing B2>0 as a list would offer it as one entry.
397
- if (op === 'add_validation' && !String(operation.type ?? '').trim()) {
398
- operation.type = listValidationFormula(operation.formula1) ? 'list' : 'custom';
399
- }
433
+ if (op === 'add_validation') Object.assign(operation, normalizeXlsxValidation(operation));
400
434
  if (op === 'set_style' && operation.cell) parseXlsxCell(operation.cell);
401
435
  if (operation.range && RANGE_OPERATIONS.includes(op)) validateRangeOperation(operation, op);
402
436
  else if (op === 'set_range') throw new Error('XLSX set_range requires range');
@@ -26,7 +26,7 @@ export const TOOL_DEFS = [
26
26
  type: 'string',
27
27
  enum: OFFICE_ACTIONS,
28
28
  description:
29
- 'detect/describe discover; author: PPTX from HTML or pptxgenjs; transactions/recover, begin/diff/commit/rollback checkpoint; create/attach/open start; snapshot/get/query inspect; batch edits; issues/qa/render/validate review; save/finalize/close finish; secure encrypts/decrypts PDF.',
29
+ 'detect/describe discover; author: HTML/pptxgenjs PPTX; transactions/recover/begin/diff/commit/rollback checkpoints; create/attach/open; snapshot/get/query; batch edits; issues/qa/validate review; render exports PDF and page images; save/finalize/close; secure: PDF passwords.',
30
30
  },
31
31
  path: { type: 'string', description: 'Document path; relative paths resolve from the caller project.' },
32
32
  script: { type: 'string', description: 'author: HTML slides or a pptxgenjs script (pptx skill).' },
@@ -75,7 +75,7 @@ export const TOOL_DEFS = [
75
75
  description:
76
76
  'auto defaults to background with Office, otherwise portable. Only explicit attach (alias live) co-edits an open file; visible opens a window; background edits a copy.',
77
77
  },
78
- output: { type: 'string', description: 'Output copy or render destination; defaults beside source.' },
78
+ output: { type: 'string', description: 'Output copy; render: exported PDF path. Defaults beside source.' },
79
79
  target: { type: 'string', description: 'Stable path from snapshot/query, e.g. /body/p[2].' },
80
80
  query: { type: 'string', description: 'Case-insensitive value search; with pdf-layout, where the text sits.' },
81
81
  queryKind: {
@@ -64,7 +64,7 @@ export function priceUsage(args) {
64
64
  pricingProvider: meta?.pricingProvider || identity.pricingProvider,
65
65
  pricingSource: meta?.pricingSource || null,
66
66
  ...(args.inputTokensKnown === false ? { inputTokensKnown: false } : {}),
67
- ...(args.fast ? { fast: true } : {}),
67
+ ...(args.requestServiceTier ? { requestServiceTier: args.requestServiceTier } : {}),
68
68
  ...(args.serviceTier ? { serviceTier: args.serviceTier } : {}),
69
69
  ...(written1h ? { cacheWrite1hTokens: written1h } : {}),
70
70
  };
@@ -78,7 +78,19 @@ export function priceUsage(args) {
78
78
  },
79
79
  };
80
80
  const promptTokens = input + cached + written;
81
- if (args.historicalAggregate && meta.pricingTiers?.some((tier) => promptTokens > tier.aboveInputTokens)) {
81
+ // The tier the request itself carried decides its rate: subscription quota
82
+ // is charged by the tier sent, and an Anthropic fast request either runs
83
+ // fast or is rejected (its retry is a new, standard request). Only the
84
+ // OpenAI API bills a ramp-downgraded Priority request at Standard rates,
85
+ // reporting it as service_tier "default".
86
+ const sentTier = args.requestServiceTier || '';
87
+ const premium =
88
+ (sentTier === 'priority' || sentTier === 'fast') && !(args.kind === 'api' && args.serviceTier === 'default');
89
+ // Anthropic fast mode is a flat 2x on fast-capable Opus; every other
90
+ // premium request needs the model's published Fast/Priority rates.
91
+ const anthropicFast = premium && sentTier === 'fast' && supportsAnthropicFastMode(identity.pricingModel);
92
+ const rateMeta = premium && !anthropicFast ? meta.fastPricing || {} : meta;
93
+ if (args.historicalAggregate && rateMeta.pricingTiers?.some((tier) => promptTokens > tier.aboveInputTokens)) {
82
94
  // A daily sum cannot establish which individual requests crossed a
83
95
  // context boundary. Do not price the whole day as one huge prompt.
84
96
  return { input, costUsd: null, rates: { ...provenance, unpricedReason: 'request-boundaries-unavailable' } };
@@ -93,10 +105,10 @@ export function priceUsage(args) {
93
105
  if (!peak) multiplier *= meta.offPeakMultiplier;
94
106
  }
95
107
  // Fast mode bills 2x standard rates on every fast-capable Opus.
96
- if (supportsAnthropicFastMode(identity.pricingModel) && (args.fast || args.serviceTier === 'fast')) multiplier *= 2;
108
+ if (anthropicFast) multiplier *= 2;
97
109
  const keys = PRICING_RATE_KEYS;
98
110
  const tokens = [input, n(args.outputTokens), cached, written - written1h];
99
- const tierRates = ratesForPrompt(meta, promptTokens);
111
+ const tierRates = ratesForPrompt(rateMeta, promptTokens);
100
112
  const rates = {
101
113
  ...provenance,
102
114
  ...Object.fromEntries(keys.map((key) => [key, tierRates[key] == null ? null : tierRates[key] * multiplier])),
@@ -144,10 +144,11 @@ function projectLitellmRow(row) {
144
144
  const value = row[field];
145
145
  if (typeof value === 'number' && Number.isFinite(value)) out[field] = value;
146
146
  }
147
- // Preserve the published context-rate columns, not just the base price.
147
+ // Preserve the published context-rate and Priority-tier columns (read by
148
+ // litellmPricing), not just the base price.
148
149
  for (const [field, value] of Object.entries(row)) {
149
150
  if (
150
- /^(?:input_cost_per_token|output_cost_per_token|cache_read_input_token_cost|cache_creation_input_token_cost)_above_\d+k_tokens$/.test(
151
+ /^(?:input_cost_per_token|output_cost_per_token|cache_read_input_token_cost|cache_creation_input_token_cost)(?:_above_\d+k_tokens)?(?:_priority)?$/.test(
151
152
  field
152
153
  ) &&
153
154
  typeof value === 'number' &&
@@ -443,6 +443,44 @@ const PRICED_VARIANTS = Object.freeze({
443
443
  }),
444
444
  });
445
445
 
446
+ // Cursor-billed rates no catalog publishes: Cursor's first-party SKUs and Fast
447
+ // rates Cursor sells beyond the vendor's own catalog. `fast` is the rate of a
448
+ // request Cursor ran in Fast mode. Other Cursor Fast requests bill at the
449
+ // vendor's Fast rate (Anthropic fast mode, OpenAI Priority/Fast).
450
+ // Source: https://cursor.com/docs/models-and-pricing — list $/M, verified 2026-09-30.
451
+ const CURSOR_PRICING = Object.freeze({
452
+ 'composer-2.5': {
453
+ inputCostPerM: 0.5,
454
+ cacheReadCostPerM: 0.2,
455
+ outputCostPerM: 2.5,
456
+ fast: { inputCostPerM: 3, cacheReadCostPerM: 0.5, outputCostPerM: 15 },
457
+ },
458
+ 'grok-4.7': {
459
+ fast: {
460
+ inputCostPerM: 4,
461
+ cacheReadCostPerM: 1,
462
+ outputCostPerM: 12,
463
+ // Fast long context (>256k input) bills at 3x the standard rates.
464
+ pricingTiers: [{ aboveInputTokens: 256000, inputCostPerM: 6, cacheReadCostPerM: 1.5, outputCostPerM: 18 }],
465
+ },
466
+ },
467
+ 'grok-4.6': { fast: { inputCostPerM: 4, cacheReadCostPerM: 1, outputCostPerM: 12 } },
468
+ 'grok-4.5': { fast: { inputCostPerM: 4, cacheReadCostPerM: 1, outputCostPerM: 18 } },
469
+ 'claude-opus-4-7': {
470
+ fast: { inputCostPerM: 30, cacheWriteCostPerM: 37.5, cacheReadCostPerM: 3, outputCostPerM: 150 },
471
+ },
472
+ });
473
+
474
+ function listedRates(row) {
475
+ return {
476
+ ...Object.fromEntries(PRICING_RATE_KEYS.map((key) => [key, row[key] ?? null])),
477
+ pricingTiers: (row.pricingTiers || []).map((tier) => ({
478
+ aboveInputTokens: tier.aboveInputTokens,
479
+ ...Object.fromEntries(PRICING_RATE_KEYS.map((key) => [key, tier[key] ?? null])),
480
+ })),
481
+ };
482
+ }
483
+
446
484
  function scaledRates(row, factor) {
447
485
  return Object.fromEntries(PRICING_RATE_KEYS.map((key) => [key, row?.[key] == null ? null : row[key] * factor]));
448
486
  }
@@ -512,6 +550,13 @@ function lookupModelMetadata(originalId, provider, catalog, modelsDevCatalog) {
512
550
  };
513
551
  }
514
552
  }
553
+ const cursorRow = /^cursor-(?:oauth|api)$/.test(String(provider || '').toLowerCase()) ? CURSOR_PRICING[id] : null;
554
+ if (cursorRow) {
555
+ if (!PRICING_RATE_KEYS.some((key) => meta?.[key] != null)) {
556
+ meta = { ...meta, ...listedRates(cursorRow), pricingSource: 'override' };
557
+ }
558
+ if (cursorRow.fast) meta = { ...meta, fastPricing: listedRates(cursorRow.fast) };
559
+ }
515
560
  if (providerUsesEndpointScopedLimits(provider) && !providerNative && meta) {
516
561
  // OAuth/backend routes can expose smaller account/backend windows than
517
562
  // the public API SKU. External catalogs and manual overrides remain useful
@@ -546,10 +591,13 @@ function lookupModelMetadata(originalId, provider, catalog, modelsDevCatalog) {
546
591
 
547
592
  function _normalize(entry) {
548
593
  if (!entry || typeof entry !== 'object') return null;
594
+ // OpenAI's Priority processing (since renamed Fast mode) columns.
595
+ const fastPricing = litellmPricing(entry, '_priority');
549
596
  return {
550
597
  contextWindow: entry.max_input_tokens || entry.max_tokens || null,
551
598
  outputTokens: entry.max_output_tokens || null,
552
599
  ...litellmPricing(entry),
600
+ ...(PRICING_RATE_KEYS.some((key) => fastPricing[key] != null) ? { fastPricing } : {}),
553
601
  ...(entry.off_peak_multiplier ? { offPeakMultiplier: entry.off_peak_multiplier } : {}),
554
602
  supportsVision: entry.supports_vision === true,
555
603
  supportsFunctionCalling: entry.supports_function_calling === true,
@@ -20,24 +20,31 @@ function rates(source, keys, multiplier) {
20
20
  );
21
21
  }
22
22
 
23
- export function litellmPricing(entry) {
23
+ // `suffix` selects a service-tier column set (e.g. `_priority`), published as
24
+ // `<key>_priority` and `<key>_above_<N>k_tokens_priority`.
25
+ export function litellmPricing(entry, suffix = '') {
24
26
  const thresholds = new Set();
27
+ const tierKey = new RegExp(`^_above_(\\d+)k_tokens${suffix}$`);
25
28
  for (const key of Object.keys(entry || {})) {
26
29
  for (const prefix of LITELLM_KEYS) {
27
30
  if (!key.startsWith(`${prefix}_above_`)) continue;
28
- const match = key.slice(prefix.length).match(/^_above_(\d+)k_tokens$/);
31
+ const match = key.slice(prefix.length).match(tierKey);
29
32
  if (match) thresholds.add(Number(match[1]) * 1000);
30
33
  }
31
34
  }
32
35
  return {
33
- ...rates(entry, LITELLM_KEYS, 1_000_000),
36
+ ...rates(
37
+ entry,
38
+ LITELLM_KEYS.map((key) => `${key}${suffix}`),
39
+ 1_000_000
40
+ ),
34
41
  pricingTiers: [...thresholds]
35
42
  .sort((a, b) => a - b)
36
43
  .map((aboveInputTokens) => ({
37
44
  aboveInputTokens,
38
45
  ...rates(
39
46
  entry,
40
- LITELLM_KEYS.map((key) => `${key}_above_${aboveInputTokens / 1000}k_tokens`),
47
+ LITELLM_KEYS.map((key) => `${key}_above_${aboveInputTokens / 1000}k_tokens${suffix}`),
41
48
  1_000_000
42
49
  ),
43
50
  })),
@@ -23,8 +23,18 @@ export async function accountProviderSend(provider, instance, send, model, opts
23
23
  } catch (error) {
24
24
  openingError = error;
25
25
  }
26
+ const identity = {
27
+ provider,
28
+ requestedModel: model,
29
+ sessionId,
30
+ sourceType,
31
+ inputTokensInclusive,
32
+ };
26
33
  const record = async (result) => {
27
34
  if (!result?.usage) return;
35
+ // A nested send (e.g. a fallback model re-send) already stamped its own
36
+ // final attempt's tier; the outer context only saw the abandoned attempt.
37
+ result.requestServiceTier ??= identity.requestServiceTier || '';
28
38
  if (openingError) throw openingError;
29
39
  if (!ledger) return;
30
40
  const usage = result.usage;
@@ -46,7 +56,7 @@ export async function accountProviderSend(provider, instance, send, model, opts
46
56
  cacheWrite1hTokens: usage.cacheWrite1hTokens,
47
57
  costUsd: usage.costUsd,
48
58
  serviceTier: result.serviceTier || usage.raw?.service_tier,
49
- fast: opts.fast === true,
59
+ requestServiceTier: result.requestServiceTier,
50
60
  responseId: result.responseId,
51
61
  // The account this send is bound to, so quota history can tell one
52
62
  // connected subscription's records from another's.
@@ -68,16 +78,7 @@ export async function accountProviderSend(provider, instance, send, model, opts
68
78
  };
69
79
  let result;
70
80
  try {
71
- result = await withUsageContext(
72
- {
73
- provider,
74
- requestedModel: model,
75
- sessionId,
76
- sourceType,
77
- inputTokensInclusive,
78
- },
79
- send
80
- );
81
+ result = await withUsageContext(identity, send);
81
82
  } catch (error) {
82
83
  // Only provider-reported partial usage is recordable; never invent
83
84
  // tokens for a failed request or reinterpret an error as a success.
@@ -4,3 +4,10 @@ import { AsyncLocalStorage } from 'node:async_hooks';
4
4
  const context = new AsyncLocalStorage();
5
5
  export const withUsageContext = (identity, send) => context.run(identity, send);
6
6
  export const currentUsageContext = () => context.getStore();
7
+ // Each wire attempt stamps the tier it actually sent on its own send's
8
+ // context object, so the last attempt (the one that produced the usage) wins
9
+ // and concurrent sends never observe each other's tier.
10
+ export const noteRequestServiceTier = (tier) => {
11
+ const identity = context.getStore();
12
+ if (identity) identity.requestServiceTier = tier || '';
13
+ };
@@ -81,8 +81,10 @@ function* planUsageRepair(rows, { providerOverrides = {}, price = priceUsage })
81
81
  cacheReadTokens: row.cache_read,
82
82
  cacheWriteTokens: row.cache_write,
83
83
  cacheWrite1hTokens: oldRates?.cacheWrite1hTokens,
84
- fast: oldRates?.fast,
84
+ // Rows written before the sent tier was recorded carry `fast`.
85
+ requestServiceTier: oldRates?.requestServiceTier ?? (oldRates?.fast ? 'fast' : undefined),
85
86
  serviceTier: oldRates?.serviceTier,
87
+ kind,
86
88
  ts: row.ts,
87
89
  historical: true,
88
90
  });
@@ -232,7 +232,7 @@ export function makeUsageRecord(args) {
232
232
  const ts = Number(args.ts ?? Date.now());
233
233
  if (!Number.isSafeInteger(ts) || ts <= 0) throw new Error('Usage requires a valid timestamp');
234
234
  const kind = args.kind || usageRouteKind(provider);
235
- const priced = priceUsage({ ...args, provider, model, ts });
235
+ const priced = priceUsage({ ...args, provider, model, ts, kind });
236
236
  const supplied =
237
237
  args.costUsd !== null &&
238
238
  args.costUsd !== undefined &&
@@ -8,7 +8,8 @@ import { agentTerminalDetail, isAgentTool } from './agent-surface.mjs';
8
8
  import { backgroundTaskElapsed, backgroundTaskFailureDetail, isBackgroundTaskTool } from './background-task.mjs';
9
9
  import { shellDisplayStatus } from './shell-surface.mjs';
10
10
  import { normalizeTerminalStatus, resultTerminalStatus, shellResultElapsed } from './terminal-status.mjs';
11
- import { gitResultError, gitResultExitCode } from './git-result.mjs';
11
+ import { gitTerminalStatus } from './git-result.mjs';
12
+ import { isUserControlCancellation } from '../tool-status.mjs';
12
13
 
13
14
  function shellFragments(base, display, isShellSurface) {
14
15
  if (!isShellSurface) return { shellStatus: '', shellElapsed: '' };
@@ -59,16 +60,17 @@ export function deriveCardStatus(base, { normalizedName, parsedArgs, isShellSurf
59
60
  !pending && isAgentTool(normalizedName) && !agentHeaderFailure
60
61
  ? agentTerminalDetail(parsedArgs?.status, isError, elapsed, parsedArgs?.error)
61
62
  : '';
62
- const gitFailed =
63
- normalizedName === 'git' &&
64
- (gitResultExitCode(display.displayedResultText) !== null || Boolean(gitResultError(display.displayedResultText)));
65
- const failedOrCompleted = isError || failedCount > 0 || gitFailed ? 'failed' : 'completed';
63
+ const gitStatus = normalizedName === 'git' ? gitTerminalStatus(display.displayedResultText) : '';
64
+ const failedOrCompleted = isError || failedCount > 0 ? 'failed' : 'completed';
66
65
  const terminalStatus = pending
67
66
  ? 'running'
68
67
  : shellStatus ||
68
+ gitStatus ||
69
69
  normalizeTerminalStatus(display.backgroundMeta?.status) ||
70
70
  normalizeTerminalStatus(parsedArgs?.status) ||
71
71
  resultTerminalStatus(display.displayedResultText) ||
72
+ // A one-line error body is hidden from display, so read the raw result.
73
+ (isUserControlCancellation(base.rt) ? 'cancelled' : '') ||
72
74
  failedOrCompleted;
73
75
  return {
74
76
  shellStatus,