mixdog 1.0.8 → 1.0.9
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 +2 -1
- package/scripts/lib/run-node-tests.mjs +13 -0
- package/scripts/session-efficiency-diag.mjs +283 -0
- package/src/defaults/skills/computer-use/SKILL.md +3 -3
- package/src/defaults/skills/local-provider/SKILL.md +2 -1
- package/src/defaults/skills/setup/SKILL.md +4 -4
- package/src/defaults/skills/setup/references/actions.md +1 -3
- package/src/defaults/skills/setup/references/surfaces.md +2 -3
- package/src/headless-exec.mjs +1 -0
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
- package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +7 -0
- package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
- package/src/runtime/computer-bridge/action-schema.mjs +3 -3
- package/src/runtime/computer-bridge/actions.mjs +4 -0
- package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
- package/src/runtime/computer-bridge/client.mjs +10 -183
- package/src/runtime/computer-bridge/core-actions.mjs +2 -1
- package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
- package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
- package/src/runtime/local-provider/asset-installer.mjs +20 -10
- package/src/runtime/local-provider/catalog.mjs +4 -7
- package/src/runtime/local-provider/data/manifest.json +14 -1
- package/src/runtime/local-provider/hardware.mjs +36 -5
- package/src/runtime/local-provider/resumable-installations.mjs +2 -2
- package/src/runtime/local-provider/server.mjs +1 -1
- package/src/runtime/memory/index.mjs +2 -4
- package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
- package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
- package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
- package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
- package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
- package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
- package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
- package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
- package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
- package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
- package/src/runtime/memory/lib/memory.mjs +79 -32
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
- package/src/runtime/office/com/com-adapter.mjs +3 -0
- package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
- package/src/runtime/office/pdf/pdf-render.mjs +2 -6
- package/src/runtime/office/pdf/pdf-security.mjs +8 -2
- package/src/runtime/office/portable/font-provisioner.mjs +28 -0
- package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
- package/src/runtime/office/portable/portable-soffice.mjs +44 -7
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
- package/src/runtime/shared/llm/default-context-window.mjs +42 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
- package/src/runtime/shared/llm/model-catalog.mjs +2 -0
- package/src/runtime/shared/plugin-paths.mjs +9 -2
- package/src/runtime/shared/worker-exec-argv.mjs +25 -0
- package/src/runtime/web-search/lib/formatter.mjs +43 -18
- package/src/session-runtime/automation-agents.mjs +88 -0
- package/src/session-runtime/automation-workflow.mjs +19 -6
- package/src/session-runtime/boot/apis.mjs +0 -1
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
- package/src/session-runtime/model-recency.mjs +10 -2
- package/src/session-runtime/model-route/set-route.mjs +7 -1
- package/src/session-runtime/resource-api.mjs +8 -2
- package/src/session-runtime/resource-plugins-api.mjs +14 -2
- package/src/session-runtime/route-state.mjs +6 -2
- package/src/session-runtime/schedule-session-run.mjs +7 -1
- package/src/session-runtime/services/agent-tool/render.mjs +0 -5
- package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
- package/src/session-runtime/setup-tool/executor.mjs +7 -13
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
- package/src/session-runtime/tool-policy-refresh.mjs +0 -2
- package/src/session-runtime/webhook-session-run.mjs +10 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
- package/src/standalone/daemon-agent-control.mjs +8 -1
- package/src/standalone/daemon.mjs +4 -0
- package/src/tui/dist/index.mjs +3 -2
- package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
- package/src/tui/session/live-share.mjs +16 -1
- package/src/tui/session/notification-plan.mjs +4 -0
- package/src/session-runtime/bridge-first-use-gate.mjs +0 -72
|
@@ -217,7 +217,35 @@ async function installFont(fontDef) {
|
|
|
217
217
|
return { installed: true, skipped: false, path: targetPath };
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// Office for Mac keeps its faces (Calibri, Cambria, Aptos, Malgun Gothic, Yu
|
|
221
|
+
// Gothic, ...) inside each app bundle and never registers them with the system,
|
|
222
|
+
// so a Mac with Excel installed measured Calibri in a fallback face — its digit
|
|
223
|
+
// read 9 px where Excel lays out 7, and every fitted column came out wide. The
|
|
224
|
+
// apps share one copy; the first bundle found is the one Excel draws with.
|
|
225
|
+
const MAC_OFFICE_FONT_DIRECTORIES = Object.freeze(
|
|
226
|
+
['Microsoft Excel', 'Microsoft Word', 'Microsoft PowerPoint', 'Microsoft Outlook'].map(
|
|
227
|
+
(app) => `/Applications/${app}.app/Contents/Resources/DFonts`
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
/** The font folder of the installed Office for Mac, or '' elsewhere or without Office. */
|
|
232
|
+
export function macOfficeFontDirectory() {
|
|
233
|
+
if (platform() !== 'darwin') return '';
|
|
234
|
+
return MAC_OFFICE_FONT_DIRECTORIES.find((candidate) => existsSync(candidate)) || '';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function registerMacOfficeFonts() {
|
|
238
|
+
const directory = macOfficeFontDirectory();
|
|
239
|
+
if (!directory) return;
|
|
240
|
+
try {
|
|
241
|
+
GlobalFonts.loadFontsFromDir(directory);
|
|
242
|
+
} catch {
|
|
243
|
+
// non-fatal: measurement falls back to the installed faces
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
220
247
|
export function warmupInstalledOfficeFonts() {
|
|
248
|
+
registerMacOfficeFonts();
|
|
221
249
|
for (const fontDef of NOTO_FONT_DEFINITIONS) {
|
|
222
250
|
const status = isFontInstalled(fontDef);
|
|
223
251
|
if (status.installed) {
|
|
@@ -14,9 +14,11 @@ import { appendDocxBlock, docxBodyModel } from './portable-snapshot.mjs';
|
|
|
14
14
|
import {
|
|
15
15
|
OFFICE_RELATIONSHIP_BASE,
|
|
16
16
|
XML_HEADER,
|
|
17
|
+
containerInner,
|
|
17
18
|
paragraphTexts,
|
|
18
19
|
replaceAcrossRuns,
|
|
19
20
|
textNodes,
|
|
21
|
+
topLevelElements,
|
|
20
22
|
upsertOrderedChild,
|
|
21
23
|
xmlEncode,
|
|
22
24
|
} from './portable-xml.mjs';
|
|
@@ -110,8 +112,33 @@ function bodyParagraphAt(model, ordinal) {
|
|
|
110
112
|
return paragraph;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
// The paragraphs inside the body's tables, with offsets into the body like a
|
|
116
|
+
// top-level paragraph's, so one can be spliced back the same way.
|
|
117
|
+
function tableCellParagraphs(model) {
|
|
118
|
+
const paragraphs = [];
|
|
119
|
+
const inside = (xml, tag, base, accepted, visit) => {
|
|
120
|
+
const container = containerInner(xml, tag);
|
|
121
|
+
if (!container) return;
|
|
122
|
+
for (const child of topLevelElements(container.inner, accepted)) visit(child, base + container.start + child.start);
|
|
123
|
+
};
|
|
124
|
+
for (const block of model.blocks.filter((entry) => entry.name === 'w:tbl')) {
|
|
125
|
+
inside(block.xml, 'w:tbl', block.start, ['w:tr'], (row, rowStart) =>
|
|
126
|
+
inside(row.xml, 'w:tr', rowStart, ['w:tc'], (cell, cellStart) =>
|
|
127
|
+
inside(cell.xml, 'w:tc', cellStart, ['w:p'], (paragraph, start) =>
|
|
128
|
+
paragraphs.push({ ...paragraph, start, end: start + paragraph.xml.length })
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return paragraphs;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A phrase the body does not hold may sit in a table cell, where Word anchors
|
|
137
|
+
// a comment, hyperlink, or bookmark just as well; without Office this was the
|
|
138
|
+
// only backend, and a Mac could not comment on any table text.
|
|
113
139
|
function paragraphContaining(model, find) {
|
|
114
|
-
|
|
140
|
+
const holds = (entry) => paragraphTexts(entry.xml, 'w:t').join('').includes(find);
|
|
141
|
+
return bodyParagraphs(model).find(holds) || tableCellParagraphs(model).find(holds);
|
|
115
142
|
}
|
|
116
143
|
|
|
117
144
|
// Splice one rewritten paragraph back into the document body.
|
|
@@ -14,8 +14,9 @@ import {
|
|
|
14
14
|
workbookSheets,
|
|
15
15
|
writeCachedValues,
|
|
16
16
|
} from './portable-cells.mjs';
|
|
17
|
-
import { xmlDecode } from './portable-xml.mjs';
|
|
17
|
+
import { xmlDecode, xmlEncode } from './portable-xml.mjs';
|
|
18
18
|
import { refreshChartCaches } from './portable-xlsx-charts.mjs';
|
|
19
|
+
import { macOfficeFontDirectory } from './font-provisioner.mjs';
|
|
19
20
|
|
|
20
21
|
const SOFFICE_PROBE_TIMEOUT_MS = 20_000;
|
|
21
22
|
const SOFFICE_RENDER_TIMEOUT_MS = 120_000;
|
|
@@ -38,7 +39,9 @@ const SOFFICE_QUIET_ARGS = [
|
|
|
38
39
|
function killQuietly(child) {
|
|
39
40
|
try {
|
|
40
41
|
child.kill();
|
|
41
|
-
} catch {
|
|
42
|
+
} catch {
|
|
43
|
+
// Already exited: nothing left to stop.
|
|
44
|
+
}
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
// A detection probe must always answer. soffice.exe is a GUI launcher that can
|
|
@@ -141,7 +144,9 @@ function sharedProfileDir() {
|
|
|
141
144
|
for (const profile of createdProfiles) {
|
|
142
145
|
try {
|
|
143
146
|
rmSync(profile, { recursive: true, force: true });
|
|
144
|
-
} catch {
|
|
147
|
+
} catch {
|
|
148
|
+
// The process is exiting; a profile left behind lives in the temp directory.
|
|
149
|
+
}
|
|
145
150
|
}
|
|
146
151
|
});
|
|
147
152
|
}
|
|
@@ -168,12 +173,16 @@ function queueConversion(work) {
|
|
|
168
173
|
// stderr is kept up to this many characters, which is plenty to say why it failed.
|
|
169
174
|
export const MAX_SOFFICE_STDERR_CHARS = 64 * 1024;
|
|
170
175
|
|
|
171
|
-
export function runSoffice(program, args, { signal, timeoutMs, timeoutMessage, cancelMessage }) {
|
|
176
|
+
export function runSoffice(program, args, { signal, timeoutMs, timeoutMessage, cancelMessage, env }) {
|
|
172
177
|
return new Promise((resolve) => {
|
|
173
178
|
let settled = false;
|
|
174
179
|
let timer = null;
|
|
175
180
|
let stderr = '';
|
|
176
|
-
const child = spawn(program, args, {
|
|
181
|
+
const child = spawn(program, args, {
|
|
182
|
+
windowsHide: true,
|
|
183
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
184
|
+
...(env ? { env: { ...process.env, ...env } } : {}),
|
|
185
|
+
});
|
|
177
186
|
const finish = (value) => {
|
|
178
187
|
if (settled) return;
|
|
179
188
|
settled = true;
|
|
@@ -218,6 +227,7 @@ function convertWithLibreOffice(program, input, { to, outDir, signal = null, tim
|
|
|
218
227
|
// one discards it, and the path it used is gone by the time this one runs.
|
|
219
228
|
return queueConversion(async () => {
|
|
220
229
|
const profile = await sharedProfileDir();
|
|
230
|
+
const env = await headlessFontEnvironment(profile);
|
|
221
231
|
const result = await runSoffice(
|
|
222
232
|
program,
|
|
223
233
|
[
|
|
@@ -229,13 +239,40 @@ function convertWithLibreOffice(program, input, { to, outDir, signal = null, tim
|
|
|
229
239
|
outDir,
|
|
230
240
|
input,
|
|
231
241
|
],
|
|
232
|
-
{ signal, timeoutMs, timeoutMessage: messages.timeout, cancelMessage: messages.cancelled }
|
|
242
|
+
{ signal, timeoutMs, timeoutMessage: messages.timeout, cancelMessage: messages.cancelled, env }
|
|
233
243
|
);
|
|
234
244
|
if (!result.ok) await discardProfileDir();
|
|
235
245
|
return result;
|
|
236
246
|
});
|
|
237
247
|
}
|
|
238
248
|
|
|
249
|
+
// Headless LibreOffice on macOS finds fonts through its bundled fontconfig,
|
|
250
|
+
// which names none of the Mac's font folders: every face but the few it ships
|
|
251
|
+
// was missing, and a Hangul, CJK, or Thai run rendered as nothing at all — a
|
|
252
|
+
// Korean header came out of the preview as "( )". The config names the system,
|
|
253
|
+
// local, and user folders, and the Office for Mac bundle where Excel keeps
|
|
254
|
+
// Calibri and Malgun Gothic, so the preview draws a workbook in Excel's faces.
|
|
255
|
+
export function macFontConfig(cacheDir, officeFontDir = macOfficeFontDirectory()) {
|
|
256
|
+
const directories = ['/System/Library/Fonts', '/Library/Fonts', join(homedir(), 'Library', 'Fonts')];
|
|
257
|
+
if (officeFontDir) directories.push(officeFontDir);
|
|
258
|
+
return [
|
|
259
|
+
'<?xml version="1.0"?>',
|
|
260
|
+
'<!DOCTYPE fontconfig SYSTEM "fonts.dtd">',
|
|
261
|
+
'<fontconfig>',
|
|
262
|
+
...directories.map((directory) => ` <dir>${xmlEncode(directory)}</dir>`),
|
|
263
|
+
` <cachedir>${xmlEncode(cacheDir)}</cachedir>`,
|
|
264
|
+
'</fontconfig>',
|
|
265
|
+
'',
|
|
266
|
+
].join('\n');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function headlessFontEnvironment(profile) {
|
|
270
|
+
if (process.platform !== 'darwin') return undefined;
|
|
271
|
+
const file = join(profile, 'mixdog-fonts.conf');
|
|
272
|
+
await writeFile(file, macFontConfig(join(profile, 'fontconfig-cache')));
|
|
273
|
+
return { FONTCONFIG_FILE: file };
|
|
274
|
+
}
|
|
275
|
+
|
|
239
276
|
// LibreOffice names what it writes after the source it read, minus the
|
|
240
277
|
// source's own extension: `report.v2.docx` converted to pdf lands in the out
|
|
241
278
|
// directory as `report.v2.pdf`. Every caller finds its output by that rule.
|
|
@@ -396,7 +433,7 @@ export async function recalculateLibreOfficeWorkbook(path, { force = false, sign
|
|
|
396
433
|
// reopened answered with the numbers from before the edit — and an error a
|
|
397
434
|
// guard had swallowed stayed swallowed. The edit path marks the workbook for
|
|
398
435
|
// a full calculation, and that mark is what staleness looks like on disk.
|
|
399
|
-
const stale = workbookCalculation(await zipText(zip, 'xl/workbook.xml')).fullCalcOnLoad
|
|
436
|
+
const stale = workbookCalculation(await zipText(zip, 'xl/workbook.xml')).fullCalcOnLoad;
|
|
400
437
|
const needed = counts.formulaCount > 0 && (force || stale || counts.missingCachedValues > 0);
|
|
401
438
|
if (!needed) return { needed: false, recalculated: false, ...counts };
|
|
402
439
|
const unavailable = (reason) => ({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EFFORT_BETA_HEADER } from './anthropic-effort.mjs';
|
|
2
2
|
import { normalizeClaudeModelId } from './anthropic-thinking-contract.mjs';
|
|
3
|
+
import { getModelMetadataSync } from './model-catalog.mjs';
|
|
3
4
|
|
|
4
5
|
const EXTENDED_CACHE_TTL_BETA_HEADER = 'extended-cache-ttl-2025-04-11';
|
|
5
6
|
const INTERLEAVED_THINKING_BETA_HEADER = 'interleaved-thinking-2025-05-14';
|
|
@@ -10,9 +11,14 @@ export const SERVER_SIDE_FALLBACK_BETA_HEADER = 'server-side-fallback-2026-07-01
|
|
|
10
11
|
|
|
11
12
|
// Fast mode ships on Opus 5.5, Opus 5 and Opus 4.8 only. Opus 4.7 rejects
|
|
12
13
|
// `speed: "fast"` and Opus 4.6 silently runs at standard speed.
|
|
14
|
+
const KNOWN_FAST_MODE_MODELS = /^claude-opus-(?:4-8|5)(?:$|[-@])/;
|
|
15
|
+
|
|
16
|
+
// The id pattern is the offline floor. A model the pricing catalog declares
|
|
17
|
+
// Fast-capable is accepted too, so a later release needs no edit here.
|
|
13
18
|
export function supportsAnthropicFastMode(model) {
|
|
14
19
|
const id = normalizeClaudeModelId(model);
|
|
15
|
-
|
|
20
|
+
if (!id) return false;
|
|
21
|
+
return KNOWN_FAST_MODE_MODELS.test(id) || getModelMetadataSync(id, 'anthropic')?.supportsFastMode === true;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export function buildAnthropicBetaHeaders({
|
|
@@ -19,7 +19,7 @@ export function isAnthropicAdaptiveOnlyModel(model) {
|
|
|
19
19
|
// preambles) as progress-update thinking blocks instead of text blocks.
|
|
20
20
|
export function emitsAnthropicProgressUpdates(model) {
|
|
21
21
|
const id = normalizeClaudeModelId(model);
|
|
22
|
-
return /^claude-(?:opus-5-5|fable-5-1|mythos-5-1|fable-5)(?:-\d{8})?$/.test(id);
|
|
22
|
+
return /^claude-(?:opus-5-5|sonnet-5-5|fable-5-1|mythos-5-1|fable-5)(?:-\d{8})?$/.test(id);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function assertAnthropicManualBudgetSupported(model) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The context window a model starts with when the user has not picked one.
|
|
2
|
+
//
|
|
3
|
+
// A provider row states the window it serves by default (`contextWindow`) and,
|
|
4
|
+
// when it can serve more, the largest one (`maxContextWindow`). The picker's
|
|
5
|
+
// 10% stops and the session's compaction boundary both follow that pair. A row
|
|
6
|
+
// that serves its whole window by default makes the largest window the
|
|
7
|
+
// starting point, and every request then re-sends whatever the session has
|
|
8
|
+
// accumulated up to it: on stored sessions of 1M rows left at 100%
|
|
9
|
+
// (2026-09-25..10-05) the average lead request carried 225K tokens and 62% of
|
|
10
|
+
// the token volume was spent above 300K. Replaying those sessions against a
|
|
11
|
+
// 500K boundary cut that volume by 19% (lead) and 14% (agents).
|
|
12
|
+
//
|
|
13
|
+
// The default is therefore set by range: a window up to the ceiling is used
|
|
14
|
+
// whole, a larger one starts at the ceiling and keeps the rest selectable — the
|
|
15
|
+
// shape provider rows with a smaller default of their own (272K of 872K)
|
|
16
|
+
// already have. An explicit selection is never touched.
|
|
17
|
+
export const DEFAULT_CONTEXT_WINDOW_CEILING = 500_000;
|
|
18
|
+
|
|
19
|
+
// A local model's window is the memory its runtime allocated, not a budget.
|
|
20
|
+
const LOCAL_PROVIDERS = new Set(['mixdog-local', 'local']);
|
|
21
|
+
|
|
22
|
+
function positiveTokens(value) {
|
|
23
|
+
const tokens = Number(value);
|
|
24
|
+
return Number.isFinite(tokens) && tokens > 0 ? Math.floor(tokens) : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The default and the largest selectable window of one model row, in tokens.
|
|
29
|
+
* `defaultWindow` is 0 when the row states no window to start from.
|
|
30
|
+
*/
|
|
31
|
+
export function contextWindowRange({ provider = '', contextWindow, maxContextWindow } = {}) {
|
|
32
|
+
const served = positiveTokens(contextWindow);
|
|
33
|
+
const maxWindow = Math.max(served, positiveTokens(maxContextWindow));
|
|
34
|
+
const whole = { defaultWindow: served, maxWindow };
|
|
35
|
+
if (served <= DEFAULT_CONTEXT_WINDOW_CEILING) return whole;
|
|
36
|
+
if (LOCAL_PROVIDERS.has(String(provider || '').toLowerCase())) return whole;
|
|
37
|
+
// The picker moves in 10% stops of the largest window. A window so close to
|
|
38
|
+
// the ceiling that both round to the last stop keeps its whole size: that
|
|
39
|
+
// stop would otherwise name the ceiling and leave the full window unreachable.
|
|
40
|
+
if (Math.round((DEFAULT_CONTEXT_WINDOW_CEILING / maxWindow) * 10) >= 10) return whole;
|
|
41
|
+
return { defaultWindow: DEFAULT_CONTEXT_WINDOW_CEILING, maxWindow };
|
|
42
|
+
}
|
|
@@ -138,6 +138,8 @@ const LITELLM_FLAG_FIELDS = [
|
|
|
138
138
|
'supports_websearch',
|
|
139
139
|
'supports_prompt_caching',
|
|
140
140
|
'supports_reasoning',
|
|
141
|
+
// Reader: supportsAnthropicFastMode (anthropic-betas.mjs).
|
|
142
|
+
'supports_fast_mode',
|
|
141
143
|
];
|
|
142
144
|
const LITELLM_STRING_FIELDS = ['mode', 'litellm_provider', 'reasoning_content_field'];
|
|
143
145
|
|
|
@@ -604,6 +604,7 @@ function _normalize(entry) {
|
|
|
604
604
|
supportsFunctionCalling: entry.supports_function_calling === true,
|
|
605
605
|
supportsWebSearch: entry.supports_web_search === true || entry.supports_websearch === true,
|
|
606
606
|
supportsPromptCaching: entry.supports_prompt_caching === true,
|
|
607
|
+
supportsFastMode: entry.supports_fast_mode === true,
|
|
607
608
|
supportsReasoning: entry.supports_reasoning === true,
|
|
608
609
|
reasoningOptions: Array.isArray(entry.reasoning_options) ? entry.reasoning_options : [],
|
|
609
610
|
reasoningContentField: entry.reasoning_content_field || null,
|
|
@@ -648,6 +649,7 @@ function mergeModelMetadata(base, overlay, opts = {}) {
|
|
|
648
649
|
supportsFunctionCalling: base.supportsFunctionCalling || overlay.supportsFunctionCalling,
|
|
649
650
|
supportsWebSearch: base.supportsWebSearch || overlay.supportsWebSearch,
|
|
650
651
|
supportsPromptCaching: base.supportsPromptCaching || overlay.supportsPromptCaching,
|
|
652
|
+
supportsFastMode: base.supportsFastMode || overlay.supportsFastMode,
|
|
651
653
|
supportsReasoning: base.supportsReasoning || overlay.supportsReasoning,
|
|
652
654
|
reasoningOptions: overlay.reasoningOptions?.length ? overlay.reasoningOptions : base.reasoningOptions || [],
|
|
653
655
|
reasoningContentField: overlay.reasoningContentField || base.reasoningContentField || null,
|
|
@@ -10,7 +10,14 @@ import { homedir } from 'node:os';
|
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Resolved on first use, not at load: the desktop service bundle (CJS) carries
|
|
14
|
+
// this module through the model catalog, and import.meta is empty there, so
|
|
15
|
+
// computing it at load crashed the whole bundle before any caller needed it.
|
|
16
|
+
let defaultRoot = null;
|
|
17
|
+
function packagedRoot() {
|
|
18
|
+
defaultRoot ??= join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
19
|
+
return defaultRoot;
|
|
20
|
+
}
|
|
14
21
|
|
|
15
22
|
// The defaults are joins over values that almost never change; they are hot
|
|
16
23
|
// (every session path resolves the data dir), so each default join is reused
|
|
@@ -32,7 +39,7 @@ export function mixdogHome() {
|
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
export function mixdogRoot() {
|
|
35
|
-
return process.env.MIXDOG_ROOT ||
|
|
42
|
+
return process.env.MIXDOG_ROOT || packagedRoot();
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
export function resolvePluginData() {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The parent's flags a worker thread may inherit. A worker rejects any flag that
|
|
2
|
+
// governs the process itself (heap sizes, inspector, TLS, test-runner settings)
|
|
3
|
+
// with ERR_WORKER_INVALID_EXEC_ARGV, and Node 24's test runner hands a child its
|
|
4
|
+
// whole option table in execArgv (--use-system-ca, --tls-cipher-list, ...), so a
|
|
5
|
+
// list of known-bad flags kept failing. Only what loads and resolves modules,
|
|
6
|
+
// and what governs warnings, carries over.
|
|
7
|
+
const INHERITED_FLAG =
|
|
8
|
+
/^--(?:import|require|loader|experimental-loader|conditions|enable-source-maps|preserve-symlinks(?:-main)?|no-warnings|disable-warning|trace-warnings|no-deprecation|(?:no-)?experimental-[a-z0-9-]+)(?:=|$)/;
|
|
9
|
+
const SHORT_WITH_VALUE = new Set(['-r', '-C']);
|
|
10
|
+
|
|
11
|
+
export function workerExecArgv(execArgv = process.execArgv) {
|
|
12
|
+
const source = Array.isArray(execArgv) ? execArgv.map(String) : [];
|
|
13
|
+
const kept = [];
|
|
14
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
15
|
+
const argument = source[index];
|
|
16
|
+
const short = SHORT_WITH_VALUE.has(argument);
|
|
17
|
+
if (!short && !INHERITED_FLAG.test(argument)) continue;
|
|
18
|
+
kept.push(argument);
|
|
19
|
+
// A flag written apart from its value (`--import tsx`) keeps the value too.
|
|
20
|
+
const takesValue =
|
|
21
|
+
short || /^--(?:import|require|loader|experimental-loader|conditions|disable-warning)$/.test(argument);
|
|
22
|
+
if (takesValue && index + 1 < source.length) kept.push(source[++index]);
|
|
23
|
+
}
|
|
24
|
+
return kept;
|
|
25
|
+
}
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
* Response formatter — strips metadata, returns human-readable text.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// Whether `text` names exactly this URL: an occurrence that runs on into a
|
|
6
|
+
// longer URL (`…/docs` inside `…/docs/guide`) is a different page.
|
|
7
|
+
function textCitesUrl(text, url) {
|
|
8
|
+
for (let at = text.indexOf(url); at !== -1; at = text.indexOf(url, at + 1)) {
|
|
9
|
+
const rest = text.slice(at + url.length, at + url.length + 2);
|
|
10
|
+
if (!/^(?:[A-Za-z0-9/?#&=%_~-]|\.[A-Za-z0-9])/.test(rest)) return true;
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
function formatWebSearchResults(data) {
|
|
6
16
|
// data may be the full jsonText payload: { tool, providers, response, cache, ... }
|
|
7
17
|
const response = data.response || data;
|
|
@@ -20,25 +30,40 @@ function formatWebSearchResults(data) {
|
|
|
20
30
|
const SNIPPET_CAP = 600;
|
|
21
31
|
const ANSWER_CAP = 4000;
|
|
22
32
|
const clip = (text, cap) => (text.length > cap ? `${text.slice(0, cap)}…` : text);
|
|
23
|
-
|
|
33
|
+
const shownAnswer = answer ? clip(answer, ANSWER_CAP) : '';
|
|
34
|
+
if (shownAnswer) blocks.push(shownAnswer);
|
|
24
35
|
if (!results.length) return blocks.join('\n\n');
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
// A native search route reports the pages a query reached without their
|
|
37
|
+
// titles, and the adapters label each one with that query. A label shared by
|
|
38
|
+
// several URLs names the search, not a page, so such a source prints as its
|
|
39
|
+
// URL alone — and not at all when the answer above already cites that URL.
|
|
40
|
+
// Measured on stored results: every source had this shape and 66% of their
|
|
41
|
+
// URLs were already in the answer.
|
|
42
|
+
const urlsByTitle = new Map();
|
|
43
|
+
for (const r of results) {
|
|
44
|
+
if (!r.title || !r.url) continue;
|
|
45
|
+
if (!urlsByTitle.has(r.title)) urlsByTitle.set(r.title, new Set());
|
|
46
|
+
urlsByTitle.get(r.title).add(r.url);
|
|
47
|
+
}
|
|
48
|
+
const entries = [];
|
|
49
|
+
for (const r of results) {
|
|
50
|
+
const url = r.url || '';
|
|
51
|
+
const date = r.publishedDate || '';
|
|
52
|
+
const snippet = clip((r.snippet || '').trim(), SNIPPET_CAP);
|
|
53
|
+
const urlPart = [url, date].filter(Boolean).join(' — ');
|
|
54
|
+
const queryLabelled =
|
|
55
|
+
!r.title || r.title === url || r.source === 'web_search_call' || urlsByTitle.get(r.title)?.size > 1;
|
|
56
|
+
if (url && !snippet && queryLabelled) {
|
|
57
|
+
if (!textCitesUrl(shownAnswer, url)) entries.push(`${entries.length + 1}. ${urlPart}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const title = clip(r.title || '(no title)', TITLE_CAP);
|
|
61
|
+
const lines = [`${entries.length + 1}. ${title}`];
|
|
62
|
+
if (urlPart) lines.push(` ${urlPart}`);
|
|
63
|
+
if (snippet && snippet !== title && snippet !== url) lines.push(` ${snippet}`);
|
|
64
|
+
entries.push(lines.join('\n'));
|
|
65
|
+
}
|
|
66
|
+
if (entries.length) blocks.push(entries.join('\n\n'));
|
|
42
67
|
return blocks.join('\n\n');
|
|
43
68
|
}
|
|
44
69
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent delegation for automation sessions (schedules, webhooks). They run
|
|
3
|
+
* headless in the daemon, so no session runtime registers tools for them or
|
|
4
|
+
* receives their agents' completions. The daemon's canonical agent control is
|
|
5
|
+
* exposed under a dedicated internal-tool scope instead, and the completions of
|
|
6
|
+
* the agents a run starts come back here, where the run turns them into the
|
|
7
|
+
* conversation's next turn — what a Lead in a window does when one arrives.
|
|
8
|
+
*/
|
|
9
|
+
import { setInternalToolsProvider } from '../runtime/agent/orchestrator/internal-tools.mjs';
|
|
10
|
+
import { AGENT_TOOL } from './services/agent-tool/tool-def.mjs';
|
|
11
|
+
|
|
12
|
+
export const AUTOMATION_TOOL_SCOPE = 'automation';
|
|
13
|
+
// How long a run waits for the next result of an agent it started; past it the
|
|
14
|
+
// run ends with the conversation as it stands.
|
|
15
|
+
const AGENT_RESULT_WAIT_MS = 60 * 60_000;
|
|
16
|
+
|
|
17
|
+
const runs = new Map(); // sessionId → { pending: Set<taskId>, ready: string[], wake }
|
|
18
|
+
|
|
19
|
+
function resultText(result) {
|
|
20
|
+
return typeof result === 'string' ? result : JSON.stringify(result ?? '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Registers the agent tool for automation sessions; returns its disposer. */
|
|
24
|
+
export function registerAutomationAgentTool(executeAgentControl) {
|
|
25
|
+
return setInternalToolsProvider({
|
|
26
|
+
scopeId: AUTOMATION_TOOL_SCOPE,
|
|
27
|
+
tools: [AGENT_TOOL],
|
|
28
|
+
async executor(_name, args, callerCtx = {}) {
|
|
29
|
+
const result = await executeAgentControl(args, {
|
|
30
|
+
callerCwd: callerCtx.callerCwd,
|
|
31
|
+
invocationSource: 'model-tool',
|
|
32
|
+
callerSessionId: callerCtx.callerSessionId || null,
|
|
33
|
+
clientHostPid: process.pid,
|
|
34
|
+
signal: callerCtx.signal,
|
|
35
|
+
});
|
|
36
|
+
// A spawn/send acknowledgement ("agent task: <id>", "status: running")
|
|
37
|
+
// is a result still to come for this run.
|
|
38
|
+
const text = resultText(result);
|
|
39
|
+
const taskId = /agent task: ([\w.:-]+)/.exec(text)?.[1];
|
|
40
|
+
const run = runs.get(String(callerCtx.callerSessionId || ''));
|
|
41
|
+
if (run && taskId && /status: running/.test(text)) run.pending.add(taskId);
|
|
42
|
+
return result;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Hands an agent completion to the automation run that started the task; false when none did. */
|
|
48
|
+
export function deliverAutomationCompletion(sessionId, text, meta = {}) {
|
|
49
|
+
const run = runs.get(String(sessionId || ''));
|
|
50
|
+
const taskId = String(meta?.execution_id || '');
|
|
51
|
+
if (!run?.pending.has(taskId)) return false;
|
|
52
|
+
// The header-only preview precedes the body-carrying completion.
|
|
53
|
+
if (meta?.model_visible === false) return true;
|
|
54
|
+
run.pending.delete(taskId);
|
|
55
|
+
run.ready.push(String(text || ''));
|
|
56
|
+
run.wake?.();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Runs an automation conversation to its end: the first turn, then one more
|
|
62
|
+
* turn per batch of agent results while agents it started are still out.
|
|
63
|
+
* Resolves with the last turn's result.
|
|
64
|
+
*/
|
|
65
|
+
export async function runAutomationTurns(sessionId, firstTurn, nextTurn, { waitMs = AGENT_RESULT_WAIT_MS } = {}) {
|
|
66
|
+
const run = { pending: new Set(), ready: [], wake: null };
|
|
67
|
+
runs.set(sessionId, run);
|
|
68
|
+
try {
|
|
69
|
+
let result = await firstTurn();
|
|
70
|
+
while (run.pending.size > 0 || run.ready.length > 0) {
|
|
71
|
+
if (run.ready.length === 0) {
|
|
72
|
+
const arrived = await new Promise((resolve) => {
|
|
73
|
+
const timer = setTimeout(() => resolve(false), waitMs);
|
|
74
|
+
run.wake = () => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
resolve(true);
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
run.wake = null;
|
|
80
|
+
if (!arrived) break;
|
|
81
|
+
}
|
|
82
|
+
result = await nextTurn(run.ready.splice(0).join('\n\n'));
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
} finally {
|
|
86
|
+
runs.delete(sessionId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -10,6 +10,9 @@ import { createWorkflowHelpers } from '../runtime/agent/orchestrator/runtime-cor
|
|
|
10
10
|
import { STANDALONE_ROOT, STANDALONE_DATA_DIR } from './runtime-paths.mjs';
|
|
11
11
|
import { readMarkdownDocument, normalizeAgentPermissionOrNone } from '../runtime/shared/markdown-frontmatter.mjs';
|
|
12
12
|
import { IMPLICIT_APPROVAL_MODE } from '../runtime/agent/orchestrator/session/approval-mode.mjs';
|
|
13
|
+
import { loadConfig } from '../runtime/agent/orchestrator/config.mjs';
|
|
14
|
+
import { configuredOrchestrationMode } from '../runtime/shared/orchestration.mjs';
|
|
15
|
+
import { AUTOMATION_TOOL_SCOPE } from './automation-agents.mjs';
|
|
13
16
|
|
|
14
17
|
let _helpers = null;
|
|
15
18
|
function helpers() {
|
|
@@ -26,21 +29,31 @@ function helpers() {
|
|
|
26
29
|
|
|
27
30
|
/** Non-interactive createSession opts, plus workflow context when the id resolves. */
|
|
28
31
|
export function automationWorkflowOpts(workflowId) {
|
|
29
|
-
const
|
|
32
|
+
const config = loadConfig({ secrets: false });
|
|
33
|
+
// The user's orchestration setting governs an automation exactly as it does a
|
|
34
|
+
// New task: a workflow id alone never turns delegation on, and the agent
|
|
35
|
+
// tool reaches the run through the automation tool scope.
|
|
36
|
+
const baseOpts = {
|
|
37
|
+
approvalMode: IMPLICIT_APPROVAL_MODE,
|
|
38
|
+
orchestrationMode: configuredOrchestrationMode(config),
|
|
39
|
+
mcpScopeId: AUTOMATION_TOOL_SCOPE,
|
|
40
|
+
};
|
|
30
41
|
const id = String(workflowId || '').trim();
|
|
31
|
-
if (!id) return
|
|
42
|
+
if (!id) return baseOpts;
|
|
32
43
|
try {
|
|
33
44
|
const h = helpers();
|
|
34
45
|
const pack = h.loadWorkflowPack(undefined, id);
|
|
35
|
-
if (!pack) return
|
|
36
|
-
|
|
46
|
+
if (!pack) return baseOpts;
|
|
47
|
+
// The whole config, so disabled agents and the orchestration mode are the
|
|
48
|
+
// user's, with only the active workflow swapped for the automation's own.
|
|
49
|
+
const resolved = h.activeWorkflowContext({ ...config, workflow: { ...config.workflow, active: id } }, undefined);
|
|
37
50
|
return {
|
|
38
|
-
...
|
|
51
|
+
...baseOpts,
|
|
39
52
|
workflow: resolved.summary,
|
|
40
53
|
workflowContext: resolved.context,
|
|
41
54
|
orchestrationMode: resolved.orchestrationMode,
|
|
42
55
|
};
|
|
43
56
|
} catch {
|
|
44
|
-
return
|
|
57
|
+
return baseOpts;
|
|
45
58
|
}
|
|
46
59
|
}
|
|
@@ -107,7 +107,6 @@ function wireToolPolicyRefresh(boot) {
|
|
|
107
107
|
getDataDir: () => dataDirOf(cfgMod),
|
|
108
108
|
modelStandaloneTools: boot.modelStandaloneTools,
|
|
109
109
|
featureDisallowedTools: boot.featureDisallowedTools,
|
|
110
|
-
memoryToolsEnabled: boot.memoryToolsEnabledFn,
|
|
111
110
|
loadCoreMemoryContext: boot.loadCoreMemoryContext,
|
|
112
111
|
activeWorkflowContext: workflowHelpers.activeWorkflowContext,
|
|
113
112
|
invalidatePreSessionToolSurface: boot.invalidatePreSessionToolSurface,
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// cwd-plugins/core-memory-context.mjs — the user-curated core-memory block
|
|
2
2
|
// injected into new sessions, read from the memory runtime's atomic snapshot.
|
|
3
|
-
import { featureEnvOverride
|
|
3
|
+
import { featureEnvOverride } from '../../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
|
|
4
4
|
import { readSessionCoreMemoryPayload } from '../../runtime/memory/lib/core-memory-file.mjs';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// Budget for the whole injected block in UTF-8 bytes: bytes track token cost
|
|
7
|
+
// more evenly across scripts than characters do.
|
|
8
|
+
const CORE_MEMORY_MAX_BYTES = 32 * 1024;
|
|
9
|
+
const TRUNCATED_MARK = '\n- ...';
|
|
10
|
+
|
|
11
|
+
export function createCoreMemoryContext({ getCurrentCwd, bootProfile, clean, cfgMod, STANDALONE_DATA_DIR }) {
|
|
7
12
|
function formatCoreMemoryLines(payload = {}) {
|
|
8
13
|
const seen = new Set();
|
|
9
14
|
const lines = [];
|
|
@@ -14,21 +19,18 @@ export function createCoreMemoryContext({ getCurrentCwd, getConfig, bootProfile,
|
|
|
14
19
|
if (seen.has(key)) continue;
|
|
15
20
|
seen.add(key);
|
|
16
21
|
lines.push(`- ${text}`);
|
|
17
|
-
if (lines.length >= 40) break;
|
|
18
22
|
}
|
|
19
23
|
const out = lines.join('\n');
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
if (Buffer.byteLength(out, 'utf8') <= CORE_MEMORY_MAX_BYTES) return out;
|
|
25
|
+
const budget = CORE_MEMORY_MAX_BYTES - Buffer.byteLength(TRUNCATED_MARK, 'utf8');
|
|
26
|
+
// A byte cut can split a multi-byte character; drop its replacement chars.
|
|
27
|
+
const head = Buffer.from(out, 'utf8').subarray(0, budget).toString('utf8').replace(/\uFFFD+$/, '');
|
|
28
|
+
return `${head.replace(/\s+\S*$/, '')}${TRUNCATED_MARK}`;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
async function loadCoreMemoryContext() {
|
|
25
|
-
// User-curated core memory injects into new sessions
|
|
26
|
-
//
|
|
27
|
-
// core-memory block — the profile injects regardless.
|
|
28
|
-
if (!(featureEnvOverride('MIXDOG_FEATURE_MEMORY') ?? memoryToolsEnabled(getConfig(), true))) {
|
|
29
|
-
bootProfile('core-memory:disabled');
|
|
30
|
-
return '';
|
|
31
|
-
}
|
|
32
|
+
// User-curated core memory always injects into new sessions: the Memory
|
|
33
|
+
// install marker and toggle gate only the model-facing memory/recall tools.
|
|
32
34
|
// Explicit opt-out (MIXDOG_BOOT_CORE_MEMORY=0/false/no/off) skips this
|
|
33
35
|
// file-backed prompt block. Recall and memory tools remain available.
|
|
34
36
|
if (featureEnvOverride('MIXDOG_BOOT_CORE_MEMORY') === false) {
|