mixdog 0.9.175 → 1.0.1
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/package.json +1 -1
- package/scripts/context-renewal-live-check.mjs +2 -1
- package/scripts/plan-runtime-release-tag.mjs +57 -0
- package/src/defaults/skills/computer-use/SKILL.md +13 -12
- package/src/defaults/skills/setup/SKILL.md +1 -4
- package/src/defaults/skills/setup/references/actions.md +0 -2
- package/src/defaults/skills/setup/references/surfaces.md +2 -3
- package/src/defaults/skills/xlsx/SKILL.md +1 -0
- package/src/rules/shared/70-delivery.md +3 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -12
- package/src/runtime/agent/orchestrator/config.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +20 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/registry-errors.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +3 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-tasks.mjs +25 -6
- package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +3 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +27 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +39 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +13 -5
- package/src/runtime/computer-bridge/error-recovery.mjs +1 -1
- package/src/runtime/memory/lib/runtime-fetcher.mjs +94 -23
- package/src/runtime/office/capabilities-catalog.mjs +2 -0
- package/src/runtime/office/capabilities-signatures.mjs +12 -6
- package/src/runtime/office/capabilities.mjs +30 -10
- package/src/runtime/office/core/office-actions-open.mjs +1 -6
- package/src/runtime/office/core/office-sessions.mjs +9 -0
- package/src/runtime/office/portable/portable-xlsx-operations.mjs +2 -49
- package/src/runtime/office/portable/xlsx-contract.mjs +37 -3
- package/src/runtime/office/tool-defs.mjs +2 -2
- package/src/runtime/shared/config-patch.mjs +2 -0
- package/src/runtime/shared/llm/cost.mjs +16 -4
- package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
- package/src/runtime/shared/llm/model-catalog.mjs +48 -0
- package/src/runtime/shared/llm/model-pricing-rates.mjs +11 -4
- package/src/runtime/shared/llm/usage-accounting.mjs +12 -11
- package/src/runtime/shared/llm/usage-context.mjs +7 -0
- package/src/runtime/shared/llm/usage-ledger-repair.mjs +3 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +1 -1
- package/src/runtime/shared/tool-card-model/card-status.mjs +7 -5
- package/src/runtime/shared/tool-card-model/git-result.mjs +10 -0
- package/src/runtime/shared/tool-card-model/terminal-status.mjs +6 -2
- package/src/runtime/shared/tool-card-model.golden-cases.mjs +2 -1
- package/src/runtime/shared/tool-result-summary.mjs +1 -0
- package/src/runtime/shared/tool-status.mjs +35 -2
- package/src/runtime/shared/user-data-guard.mjs +1 -1
- package/src/runtime/tidy/report.mjs +2 -2
- package/src/runtime/tidy/tool.mjs +6 -1
- package/src/session-runtime/config-lifecycle.mjs +11 -1
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +1 -1
- package/src/session-runtime/native-web-search/tool-shape.mjs +16 -4
- package/src/session-runtime/provider-models/catalog-load.mjs +18 -3
- package/src/session-runtime/setup-tool/executor.mjs +0 -1
- package/src/session-runtime/setup-tool/settings-contract.mjs +2 -13
- package/src/tui/dist/index.mjs +89 -38
- package/src/tui/session/session-api/settings.mjs +16 -10
- package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -2
- package/src/workflows/default/WORKFLOW.md +0 -2
- package/src/workflows/headless/WORKFLOW.md +0 -2
|
@@ -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,
|
|
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
|
-
|
|
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'
|
|
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:
|
|
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
|
|
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: {
|
|
@@ -25,6 +25,8 @@ function applyField(current, path, change) {
|
|
|
25
25
|
const next = isPlainObject(current) ? { ...current } : {};
|
|
26
26
|
const [key, ...rest] = path;
|
|
27
27
|
if (change.remove && rest.length && !Object.hasOwn(next, key)) return next;
|
|
28
|
+
// Seed-only field: written where it is missing, never over a stored value.
|
|
29
|
+
if (change.ifAbsent && !rest.length && Object.hasOwn(next, key)) return next;
|
|
28
30
|
if (!rest.length && change.remove) {
|
|
29
31
|
delete next[key];
|
|
30
32
|
} else {
|
|
@@ -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.
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
63
|
-
|
|
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,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hasBenignExitOutcome } from '../tool-status.mjs';
|
|
2
|
+
|
|
1
3
|
// Only command result headers, not arbitrary lines in a patch, carry exits.
|
|
2
4
|
export function gitResultExitCode(text) {
|
|
3
5
|
const match = /(?:^(?:## [^\n]+\n)?|\n## [^\n]+\n)exit ([1-9]\d*)\r?(?:\n|$)/.exec(String(text ?? ''));
|
|
@@ -7,3 +9,11 @@ export function gitResultExitCode(text) {
|
|
|
7
9
|
export function gitResultError(text) {
|
|
8
10
|
return /(?:^|\n)error: command failed: [^\n]+|^error: [^\n]+/.exec(String(text ?? ''))?.[0].trim() || '';
|
|
9
11
|
}
|
|
12
|
+
|
|
13
|
+
// A tool error fails the call; a git process that ran and exited non-zero is
|
|
14
|
+
// a command exit, like the shell's, unless the tool marked the exit a signal.
|
|
15
|
+
export function gitTerminalStatus(text) {
|
|
16
|
+
if (gitResultError(text)) return 'failed';
|
|
17
|
+
if (gitResultExitCode(text) !== null && !hasBenignExitOutcome(text)) return 'exit';
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* terminal-status.mjs — terminal status words, their outcome tone, status
|
|
3
3
|
* marker stripping and the ` · <time>` detail conventions.
|
|
4
4
|
*/
|
|
5
|
-
import { normalizeToolTerminalStatus, toolResultTerminalStatus } from '../tool-status.mjs';
|
|
5
|
+
import { leadingResultBlock, normalizeToolTerminalStatus, toolResultTerminalStatus } from '../tool-status.mjs';
|
|
6
6
|
import { formatElapsed } from '../time-format.mjs';
|
|
7
7
|
|
|
8
8
|
export function shellResultStatus(value) {
|
|
9
|
-
const match =
|
|
9
|
+
const match = leadingResultBlock(value).match(
|
|
10
10
|
/(?:^|\b)status:\s*(running|pending|queued|completed|failed|cancelled|canceled)\b/im
|
|
11
11
|
);
|
|
12
12
|
return match ? String(match[1] || '').toLowerCase() : '';
|
|
@@ -48,6 +48,7 @@ const TERMINAL_STATUS_LABELS = new Map([
|
|
|
48
48
|
['cancelled', 'Cancelled'],
|
|
49
49
|
['denied', 'Denied'],
|
|
50
50
|
]);
|
|
51
|
+
const BARE_STATUS_WORDS = new Set([...TERMINAL_STATUS_LABELS.values()].map((label) => label.toLowerCase()));
|
|
51
52
|
|
|
52
53
|
export function displayTerminalStatus(value) {
|
|
53
54
|
// 'exit' is a shell-only pseudo-status (command RAN but exited non-zero); it
|
|
@@ -107,5 +108,8 @@ export function mergeTerminalDetail(status, detail = '') {
|
|
|
107
108
|
if (label === 'Finished' && text) return text;
|
|
108
109
|
if (!text) return label;
|
|
109
110
|
if (text.toLowerCase().startsWith(label.toLowerCase())) return text;
|
|
111
|
+
// A bare status word (the generic "Failed" of an error without a body) is
|
|
112
|
+
// superseded by the resolved status, e.g. a user-takeover "Cancelled".
|
|
113
|
+
if (BARE_STATUS_WORDS.has(text.toLowerCase())) return label;
|
|
110
114
|
return `${label} · ${text}`;
|
|
111
115
|
}
|
|
@@ -16,7 +16,8 @@ export const GIT_GOLDEN_CASES = [
|
|
|
16
16
|
summary: 'diff --git a/a.txt b/a.txt',
|
|
17
17
|
status: 'completed',
|
|
18
18
|
},
|
|
19
|
-
{ result: 'exit 128\nfatal: missing ref\n', summary: 'Exit 128', status: '
|
|
19
|
+
{ result: 'exit 128\nfatal: missing ref\n', summary: 'Exit 128', status: 'exit' },
|
|
20
|
+
{ result: 'exit 1\n[outcome: no-match]\ndiff --git a/a.txt b/a.txt\n', summary: 'Exit 1', status: 'completed' },
|
|
20
21
|
{
|
|
21
22
|
result: '## git show missing\nexit 128\nfatal: missing\nerror: command failed: git show missing',
|
|
22
23
|
summary: 'Exit 128',
|
|
@@ -312,6 +312,7 @@ function summarizeLineCount({ text, trimmed }, { zero, singular, plural }) {
|
|
|
312
312
|
function summarizeReadResult({ text, trimmed }) {
|
|
313
313
|
if (/^\[image:/i.test(trimmed)) return 'Image';
|
|
314
314
|
if (!trimmed) return null;
|
|
315
|
+
if (trimmed.startsWith('(no lines in range')) return '0 lines';
|
|
315
316
|
const n = text.split('\n').length;
|
|
316
317
|
return `${n} ${pluralize(n, 'line')}`;
|
|
317
318
|
}
|
|
@@ -23,15 +23,48 @@ export function normalizeToolTerminalStatus(value) {
|
|
|
23
23
|
return '';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Status markers live in the result's leading block: the cancel marker, the
|
|
27
|
+
// shell exit header, a background-task header, a task/agent status line.
|
|
28
|
+
// Command output and fetched documents follow a blank line and can carry
|
|
29
|
+
// `status:` text of their own, which says nothing about the tool call.
|
|
30
|
+
export function leadingResultBlock(text) {
|
|
31
|
+
const lines = String(text || '').split('\n');
|
|
32
|
+
let index = 0;
|
|
33
|
+
if (/^\[tool output offloaded:/i.test(String(lines[0] || '').trim())) {
|
|
34
|
+
index = 1;
|
|
35
|
+
while (index < lines.length && !lines[index].trim()) index += 1;
|
|
36
|
+
}
|
|
37
|
+
const block = [];
|
|
38
|
+
for (; index < lines.length && lines[index].trim(); index += 1) block.push(lines[index]);
|
|
39
|
+
return block.join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The user taking over the browser/desktop, or the session cancelling a
|
|
43
|
+
// computer command, stops the call on purpose; it is not a failed invocation.
|
|
44
|
+
const USER_CONTROL_CANCELLATION_RE =
|
|
45
|
+
/^(?:Error:\s*)?(?:Browser command interrupted by local user input\.|computer_session_aborted:|computer_user_control_active:)/i;
|
|
46
|
+
|
|
47
|
+
export function isUserControlCancellation(text) {
|
|
48
|
+
return USER_CONTROL_CANCELLATION_RE.test(String(text || '').trimStart());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A non-zero exit the tool itself marked as a signal (search no-match, a
|
|
52
|
+
// `git diff --exit-code` difference) rather than a command failure.
|
|
53
|
+
export function hasBenignExitOutcome(text) {
|
|
54
|
+
return /^\[outcome:\s*(?:no-match|no-change)\]\s*$/im.test(String(text || ''));
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
export function toolResultTerminalStatus(text) {
|
|
27
58
|
const body = String(text || '');
|
|
28
59
|
const tagged = body.match(/<status[^>]*>([\s\S]*?)<\/status>/i)?.[1]?.trim();
|
|
29
60
|
if (tagged) return normalizeToolTerminalStatus(tagged);
|
|
30
|
-
const
|
|
61
|
+
const head = leadingResultBlock(body);
|
|
62
|
+
const bracketed = head.match(/^\[status:\s*([^\]]*)\]/im)?.[1]?.trim();
|
|
31
63
|
if (bracketed) return normalizeToolTerminalStatus(bracketed);
|
|
32
|
-
const inline =
|
|
64
|
+
const inline = head.match(/^(?:status|state):\s*([^\s·,;]+)/im)?.[1]?.trim();
|
|
33
65
|
const fromInline = normalizeToolTerminalStatus(inline);
|
|
34
66
|
if (fromInline) return fromInline;
|
|
67
|
+
if (isUserControlCancellation(body)) return 'cancelled';
|
|
35
68
|
// Bare control bodies written on cancel/crash (current + legacy).
|
|
36
69
|
const trimmed = body.trim();
|
|
37
70
|
if (/^(?:cancelled|canceled)$/i.test(trimmed)) return 'cancelled';
|
|
@@ -28,7 +28,7 @@ function getBackupRoot() {
|
|
|
28
28
|
return process.env.MIXDOG_USER_DATA_BACKUP_ROOT || join(mixdogConfigBaseDir(), 'backups', 'user-data');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const USER_DATA_FILES = ['mixdog-config.json'
|
|
31
|
+
const USER_DATA_FILES = ['mixdog-config.json'];
|
|
32
32
|
|
|
33
33
|
const USER_DATA_DIRS = ['schedules', 'webhooks', 'workflows'];
|
|
34
34
|
|
|
@@ -11,10 +11,10 @@ const FILE_LIST_CAP = 25;
|
|
|
11
11
|
export const RESULTS_PAGE_MAX = 100;
|
|
12
12
|
const TRIM_STEPS = [8, 3, 0];
|
|
13
13
|
|
|
14
|
-
export function tidyToolResult(value, isError = false) {
|
|
14
|
+
export function tidyToolResult(value, isError = value?.ok === false) {
|
|
15
15
|
return {
|
|
16
16
|
content: [{ type: 'text', text: JSON.stringify(value) }],
|
|
17
|
-
...(isError
|
|
17
|
+
...(isError ? { isError: true } : {}),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -571,7 +571,12 @@ export async function executeTidyTool(args = {}, { cwd = process.cwd(), signal =
|
|
|
571
571
|
return tidyToolResult(await rulesAction({ cwd, signal, startedAt }));
|
|
572
572
|
}
|
|
573
573
|
if (action === 'results') {
|
|
574
|
-
|
|
574
|
+
// Paging a stored run succeeds even when that run was incomplete; the
|
|
575
|
+
// report's ok/status still describe the run itself.
|
|
576
|
+
return tidyToolResult(
|
|
577
|
+
await resultsAction({ args, cwd, sessionId, engineFilter, pathFilter: scope, startedAt }),
|
|
578
|
+
false
|
|
579
|
+
);
|
|
575
580
|
}
|
|
576
581
|
return tidyToolResult(
|
|
577
582
|
await runAction({
|
|
@@ -102,10 +102,20 @@ export function createConfigLifecycle({
|
|
|
102
102
|
const adopted = adoptConfig(nextConfig, { hasSecrets });
|
|
103
103
|
const changes = cfgMod.createConfigPatch(configSaveBaseline, adopted);
|
|
104
104
|
configSaveBaseline = structuredClone(adopted);
|
|
105
|
-
if (changes.length) writers.queueConfigChanges(changes);
|
|
105
|
+
if (changes.length) writers.queueConfigChanges([...changes, ...seedChanges(adopted, changes)]);
|
|
106
106
|
return adopted;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// A fresh profile's install markers (an empty `builtins`) and off-by-default
|
|
110
|
+
// agents equal the defaults, so no diff ever carries them — yet on disk
|
|
111
|
+
// their absence reads as a grandfathered profile with every agent on. The
|
|
112
|
+
// first write seeds them where the stored section lacks them.
|
|
113
|
+
function seedChanges(config, changes) {
|
|
114
|
+
return ['builtins', 'disabledAgents']
|
|
115
|
+
.filter((key) => config[key] !== undefined && !changes.some((change) => change.path[0] === key))
|
|
116
|
+
.map((key) => ({ path: [key], value: structuredClone(config[key]), ifAbsent: true }));
|
|
117
|
+
}
|
|
118
|
+
|
|
109
119
|
function scheduleSkillsSave(names) {
|
|
110
120
|
// This field belongs to its dedicated writer, not a later unrelated save.
|
|
111
121
|
configSaveBaseline.skills = structuredClone(getConfig().skills);
|
|
@@ -24,7 +24,7 @@ export function createCoreMemoryContext({ getCurrentCwd, getConfig, bootProfile,
|
|
|
24
24
|
async function loadCoreMemoryContext() {
|
|
25
25
|
// User-curated core memory injects into new sessions by default. The
|
|
26
26
|
// Memory toggle (settings → General) OFF skips ONLY this accumulated
|
|
27
|
-
// core-memory block — profile
|
|
27
|
+
// core-memory block — the profile injects regardless.
|
|
28
28
|
if (!(featureEnvOverride('MIXDOG_FEATURE_MEMORY') ?? memoryToolsEnabled(getConfig(), true))) {
|
|
29
29
|
bootProfile('core-memory:disabled');
|
|
30
30
|
return '';
|