mixdog 0.9.14 → 0.9.16
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/bench/cache-probe-tasks.json +1 -1
- package/scripts/bench/lead-review-tasks-r3.json +1 -1
- package/scripts/bench/r4-mixed-tasks.json +1 -1
- package/scripts/bench/review-tasks.json +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/explore-bench.mjs +5 -4
- package/scripts/ingest-pure-conversation-smoke.mjs +27 -0
- package/scripts/output-style-smoke.mjs +3 -3
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/termio-input-smoke.mjs +199 -0
- package/scripts/tool-efficiency-diag.mjs +88 -0
- package/scripts/tool-smoke.mjs +40 -24
- package/scripts/tui-frame-harness-shim.mjs +32 -0
- package/scripts/tui-frame-harness.mjs +306 -0
- package/src/agents/heavy-worker/AGENT.md +6 -7
- package/src/agents/worker/AGENT.md +6 -7
- package/src/lib/keychain-cjs.cjs +28 -11
- package/src/lib/rules-builder.cjs +6 -10
- package/src/mixdog-session-runtime.mjs +90 -20
- package/src/output-styles/simple.md +22 -24
- package/src/rules/agent/00-core.md +12 -11
- package/src/rules/agent/30-explorer.md +22 -21
- package/src/rules/lead/01-general.md +8 -8
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/lead/lead-brief.md +11 -12
- package/src/rules/shared/01-tool.md +25 -14
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +29 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +13 -5
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +37 -15
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +55 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +177 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +12 -19
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +22 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -18
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/manager.mjs +59 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +33 -25
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +7 -5
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +73 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -10
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +21 -13
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -2
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +44 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -0
- package/src/runtime/channels/backends/telegram.mjs +29 -9
- package/src/runtime/channels/lib/event-queue.mjs +6 -2
- package/src/runtime/channels/lib/memory-client.mjs +66 -1
- package/src/runtime/channels/lib/webhook/deliveries.mjs +22 -1
- package/src/runtime/memory/index.mjs +95 -8
- package/src/runtime/memory/lib/cycle-scheduler.mjs +24 -5
- package/src/runtime/memory/lib/memory-cycle1.mjs +45 -3
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +7 -3
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +54 -10
- package/src/runtime/memory/lib/memory-cycle2.mjs +21 -8
- package/src/runtime/memory/lib/memory-embed.mjs +48 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +57 -13
- package/src/runtime/memory/lib/memory.mjs +88 -1
- package/src/runtime/memory/lib/session-ingest.mjs +34 -3
- package/src/runtime/memory/lib/trace-store.mjs +52 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +27 -4
- package/src/runtime/shared/child-guardian.mjs +4 -2
- package/src/runtime/shared/open-url.mjs +2 -1
- package/src/runtime/shared/singleton-owner.mjs +26 -0
- package/src/runtime/shared/tool-execution-contract.mjs +25 -0
- package/src/runtime/shared/transcript-writer.mjs +3 -1
- package/src/session-runtime/config-helpers.mjs +7 -2
- package/src/session-runtime/cwd-plugins.mjs +6 -1
- package/src/session-runtime/effort.mjs +6 -1
- package/src/session-runtime/plugin-mcp.mjs +116 -12
- package/src/session-runtime/settings-api.mjs +7 -1
- package/src/standalone/channel-worker.mjs +25 -3
- package/src/standalone/explore-tool.mjs +5 -5
- package/src/standalone/hook-bus/config.mjs +38 -2
- package/src/standalone/hook-bus/handlers.mjs +103 -11
- package/src/standalone/hook-bus.mjs +20 -6
- package/src/standalone/memory-runtime-proxy.mjs +40 -5
- package/src/tui/App.jsx +214 -30
- package/src/tui/app/core-memory-picker.mjs +6 -6
- package/src/tui/app/model-options.mjs +10 -4
- package/src/tui/app/settings-picker.mjs +5 -30
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/text-layout.mjs +57 -0
- package/src/tui/app/transcript-window.mjs +53 -2
- package/src/tui/app/use-mouse-input.mjs +60 -47
- package/src/tui/app/use-transcript-window.mjs +38 -10
- package/src/tui/components/PromptInput.jsx +18 -1
- package/src/tui/components/TextEntryPanel.jsx +97 -33
- package/src/tui/dist/index.mjs +567 -229
- package/src/tui/engine/tool-card-results.mjs +22 -5
- package/src/tui/engine.mjs +126 -7
- package/src/tui/index.jsx +4 -1
- package/src/workflows/default/WORKFLOW.md +27 -31
- package/vendor/ink/build/components/App.js +62 -17
- package/vendor/ink/build/ink.js +78 -3
- package/vendor/ink/build/input-parser.d.ts +9 -4
- package/vendor/ink/build/input-parser.js +45 -176
- package/vendor/ink/build/log-update.js +47 -2
- package/vendor/ink/build/termio-keypress.js +240 -0
- package/vendor/ink/build/termio-tokenize.js +253 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Mirror of vendor/ink/build/ink.js shouldClearTerminalForFrame, with the
|
|
2
|
+
// Windows branch parameterized so the harness can probe both platforms without
|
|
3
|
+
// relying on process.platform. Kept byte-faithful to the source predicate so
|
|
4
|
+
// the harness reflects real clear decisions.
|
|
5
|
+
export function shouldClearTerminalForFrameProbe({
|
|
6
|
+
isTty, viewportRows, previousViewportRows, previousOutputHeight,
|
|
7
|
+
nextOutputHeight, isUnmounting, isWindows,
|
|
8
|
+
}) {
|
|
9
|
+
if (!isTty) return false;
|
|
10
|
+
const priorViewportRows = previousViewportRows ?? viewportRows;
|
|
11
|
+
const hadPreviousFrame = previousOutputHeight > 0;
|
|
12
|
+
const wasFullscreen = previousOutputHeight >= priorViewportRows;
|
|
13
|
+
const wasOverflowing = previousOutputHeight > priorViewportRows;
|
|
14
|
+
const isOverflowing = nextOutputHeight > viewportRows;
|
|
15
|
+
const isFullscreen = nextOutputHeight >= viewportRows;
|
|
16
|
+
const isLeavingFullscreen = wasFullscreen && nextOutputHeight < viewportRows;
|
|
17
|
+
const isShrinkingAtViewport = hadPreviousFrame &&
|
|
18
|
+
nextOutputHeight < previousOutputHeight &&
|
|
19
|
+
(wasFullscreen || isFullscreen || wasOverflowing || isOverflowing);
|
|
20
|
+
const shouldClearOnUnmount = isUnmounting && wasFullscreen;
|
|
21
|
+
const viewportResized = previousViewportRows != null && previousViewportRows !== viewportRows;
|
|
22
|
+
if (isWindows && (wasFullscreen || isFullscreen) &&
|
|
23
|
+
(viewportResized || isShrinkingAtViewport || isLeavingFullscreen)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
wasOverflowing ||
|
|
28
|
+
(isOverflowing && hadPreviousFrame) ||
|
|
29
|
+
isLeavingFullscreen ||
|
|
30
|
+
isShrinkingAtViewport ||
|
|
31
|
+
shouldClearOnUnmount);
|
|
32
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/tui-frame-harness.mjs — renderer-level frame-grid evidence harness.
|
|
4
|
+
*
|
|
5
|
+
* Drives vendor/ink log-update createIncremental(fakeStream) through the SAME
|
|
6
|
+
* wrapper logic ink.js renderInteractiveFrame() applies (fullscreen detect +
|
|
7
|
+
* trailing-newline normalization + shouldClearTerminalForFrame), replays the
|
|
8
|
+
* emitted ANSI into a minimal VT grid interpreter, and asserts the absolute row
|
|
9
|
+
* of a tracked marker line (the "prompt" / "statusline") across each frame.
|
|
10
|
+
*
|
|
11
|
+
* A deviant frame = the marker's absolute row changes for a single frame then
|
|
12
|
+
* snaps back. That is the one-row-low dip reported under Windows Terminal.
|
|
13
|
+
*
|
|
14
|
+
* Run: node scripts/tui-frame-harness.mjs
|
|
15
|
+
*/
|
|
16
|
+
// [harness] log-update reads process.platform/WT_SESSION AT IMPORT to pick its
|
|
17
|
+
// Windows-safe absolute-cursor branch. Force WT_SESSION on BEFORE importing it
|
|
18
|
+
// so POSIX/CI runs exercise the same branch WT users hit. Must precede the
|
|
19
|
+
// dynamic import below.
|
|
20
|
+
process.env.WT_SESSION = process.env.WT_SESSION || '1';
|
|
21
|
+
const { default: logUpdate } = await import('../vendor/ink/build/log-update.js');
|
|
22
|
+
const { shouldClearTerminalForFrameProbe } = await import('./tui-frame-harness-shim.mjs');
|
|
23
|
+
const { default: ansiEscapes } = await import('ansi-escapes');
|
|
24
|
+
|
|
25
|
+
// Fail loudly if the Windows-safe branch is NOT engaged: without it the harness
|
|
26
|
+
// silently tests the wrong path and reports a false pass. Probe by driving a
|
|
27
|
+
// fullscreen→one-short pair through a throwaway log and asserting the branch's
|
|
28
|
+
// absolute cursorTo(0,y) addressing (not a relative cursorUp walk) is emitted.
|
|
29
|
+
function assertWindowsBranchEngaged() {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
const fs = { write: (s) => { chunks.push(s); return true; }, isTTY: true, columns: 20, rows: 4 };
|
|
32
|
+
const log = logUpdate.create(fs, { incremental: true });
|
|
33
|
+
log.sync('a\nb\nc\nd'); // 4 lines, fullscreen, no trailing nl
|
|
34
|
+
chunks.length = 0;
|
|
35
|
+
log('a\nX\nc\nd'); // change one middle row, still fullscreen
|
|
36
|
+
const emitted = chunks.join('');
|
|
37
|
+
// Windows-safe branch uses absolute cursorTo(0, i) => CSI <row>;1H. The POSIX
|
|
38
|
+
// relative branch uses cursorUp / cursorNextLine (CSI A / E) with no ;1H rows.
|
|
39
|
+
const hasAbsolute = /\x1b\[\d+;1H/.test(emitted);
|
|
40
|
+
if (!hasAbsolute) {
|
|
41
|
+
console.error('tui-frame-harness: FAIL — log-update Windows-safe branch NOT engaged '
|
|
42
|
+
+ '(WT_SESSION/platform did not force it at import). Emitted:', JSON.stringify(emitted));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
assertWindowsBranchEngaged();
|
|
47
|
+
|
|
48
|
+
// ---- Minimal VT grid interpreter -----------------------------------------
|
|
49
|
+
// Supports the escapes log-update / ink emit: cursorTo(x[,y]), cursorUp/Down/
|
|
50
|
+
// NextLine, eraseLine, eraseEndLine, eraseLines(n), clearTerminal, plain text,
|
|
51
|
+
// newline, SGR (ignored for geometry), hide/show cursor (ignored).
|
|
52
|
+
class VT {
|
|
53
|
+
constructor(rows, cols) {
|
|
54
|
+
this.rows = rows;
|
|
55
|
+
this.cols = cols;
|
|
56
|
+
this.grid = Array.from({ length: rows }, () => '');
|
|
57
|
+
this.cx = 0;
|
|
58
|
+
this.cy = 0;
|
|
59
|
+
}
|
|
60
|
+
_clampRow() { if (this.cy < 0) this.cy = 0; if (this.cy >= this.rows) this.cy = this.rows - 1; }
|
|
61
|
+
write(data) {
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < data.length) {
|
|
64
|
+
const ch = data[i];
|
|
65
|
+
if (ch === '\u001B') {
|
|
66
|
+
// CSI
|
|
67
|
+
if (data[i + 1] === '[') {
|
|
68
|
+
let j = i + 2;
|
|
69
|
+
let params = '';
|
|
70
|
+
while (j < data.length && /[0-9;]/.test(data[j])) { params += data[j]; j++; }
|
|
71
|
+
const cmd = data[j];
|
|
72
|
+
const nums = params.split(';').map((p) => (p === '' ? undefined : Number(p)));
|
|
73
|
+
this._csi(cmd, nums, params);
|
|
74
|
+
i = j + 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// other escapes (e.g. \u001B[?25l already handled via '[' path); skip 2
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === '\n') { this.cy += 1; this.cx = 0; this._clampRow(); i++; continue; }
|
|
82
|
+
if (ch === '\r') { this.cx = 0; i++; continue; }
|
|
83
|
+
// printable
|
|
84
|
+
this._clampRow();
|
|
85
|
+
const row = this.grid[this.cy];
|
|
86
|
+
const padded = row.length < this.cx ? row + ' '.repeat(this.cx - row.length) : row;
|
|
87
|
+
this.grid[this.cy] = padded.slice(0, this.cx) + ch + padded.slice(this.cx + 1);
|
|
88
|
+
this.cx += 1;
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
_csi(cmd, nums, params) {
|
|
93
|
+
const n = nums[0];
|
|
94
|
+
switch (cmd) {
|
|
95
|
+
case 'H': case 'f': { // cursor position (1-based row;col)
|
|
96
|
+
this.cy = (nums[0] ?? 1) - 1; this.cx = (nums[1] ?? 1) - 1; this._clampRow(); break;
|
|
97
|
+
}
|
|
98
|
+
case 'A': this.cy -= (n ?? 1); this._clampRow(); break; // up
|
|
99
|
+
case 'B': this.cy += (n ?? 1); this._clampRow(); break; // down
|
|
100
|
+
case 'C': this.cx += (n ?? 1); break; // forward
|
|
101
|
+
case 'D': this.cx -= (n ?? 1); if (this.cx < 0) this.cx = 0; break; // back
|
|
102
|
+
case 'E': this.cy += (n ?? 1); this.cx = 0; this._clampRow(); break; // next line
|
|
103
|
+
case 'F': this.cy -= (n ?? 1); this.cx = 0; this._clampRow(); break; // prev line
|
|
104
|
+
case 'G': this.cx = (n ?? 1) - 1; break; // column (cursorTo(x) → \u001B[(x+1)G)
|
|
105
|
+
case 'J': { // erase display; 2 = whole screen (clearTerminal uses 2J + H)
|
|
106
|
+
if (n === 2 || n === 3) { this.grid = Array.from({ length: this.rows }, () => ''); }
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'K': { // erase line (eraseEndLine=0/none; eraseLine full = 2K)
|
|
110
|
+
if (n === undefined || n === 0) { this.grid[this.cy] = this.grid[this.cy].slice(0, this.cx); }
|
|
111
|
+
else if (n === 1) { this.grid[this.cy] = ' '.repeat(this.cx) + this.grid[this.cy].slice(this.cx); }
|
|
112
|
+
else if (n === 2) { this.grid[this.cy] = ''; }
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
// SGR (m), hide/show cursor (h/l), etc. — no geometry effect
|
|
116
|
+
default: break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
markerRow(marker) {
|
|
120
|
+
for (let r = 0; r < this.rows; r++) {
|
|
121
|
+
if (this.grid[r].includes(marker)) return r;
|
|
122
|
+
}
|
|
123
|
+
return -1;
|
|
124
|
+
}
|
|
125
|
+
dump() {
|
|
126
|
+
return this.grid.map((r, i) => `${String(i).padStart(2)}|${r}`).join('\n');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- Wrapper mirroring ink.js renderInteractiveFrame ----------------------
|
|
131
|
+
function makeDriver({ rows, cols, isWindows }) {
|
|
132
|
+
const chunks = [];
|
|
133
|
+
const fakeStream = { write: (s) => { chunks.push(s); return true; }, isTTY: true, columns: cols, rows };
|
|
134
|
+
// Force the incremental renderer's Windows branch by faking env/platform is
|
|
135
|
+
// out of scope here; log-update reads process.platform/WT_SESSION at import.
|
|
136
|
+
const log = logUpdate.create(fakeStream, { incremental: true });
|
|
137
|
+
const vt = new VT(rows, cols);
|
|
138
|
+
let lastOutput = '';
|
|
139
|
+
let lastOutputHeight = 0;
|
|
140
|
+
let lastViewportRows = rows;
|
|
141
|
+
let lastOneShortPadded = false;
|
|
142
|
+
const commit = (output) => {
|
|
143
|
+
chunks.length = 0;
|
|
144
|
+
// [FAITHFUL] outputHeight = output.get().height = Output.height = the
|
|
145
|
+
// Yoga-computed ROOT height (renderer.js L45). The App pins the outer
|
|
146
|
+
// column to height=resizeState.rows, so a steady frame reports height=rows.
|
|
147
|
+
// BUT when the App's row accounting is off by one for a single commit
|
|
148
|
+
// (a reclaimed panel/hint row not yet refilled), the root lays out at
|
|
149
|
+
// rows-1 and outputHeight==rows-1. Model the height as the caller states
|
|
150
|
+
// it via a marker: the frame string's real line count is authoritative
|
|
151
|
+
// here because we construct each frame to physically carry `heightRows`
|
|
152
|
+
// lines. So derive it from the string.
|
|
153
|
+
const lineCount = output.split('\n').length;
|
|
154
|
+
let outputHeight = output === '' ? 0 : lineCount;
|
|
155
|
+
// Mirror ink.js (post-fix): an exactly-one-row-short frame following a
|
|
156
|
+
// fullscreen frame is padded with a leading blank line so the bottom
|
|
157
|
+
// cluster stays at its steady rows and the fullscreen path stays engaged.
|
|
158
|
+
const wasFullscreenFrame = lastOutputHeight >= lastViewportRows && lastOutputHeight > 0;
|
|
159
|
+
// Mirror ink.js guard exactly: Windows-like only + one-commit transient.
|
|
160
|
+
const isExactlyOneRowShort = isWindows && outputHeight === rows - 1
|
|
161
|
+
&& wasFullscreenFrame && !lastOneShortPadded;
|
|
162
|
+
if (isExactlyOneRowShort) {
|
|
163
|
+
output = '\n' + output;
|
|
164
|
+
outputHeight = rows;
|
|
165
|
+
lastOneShortPadded = true;
|
|
166
|
+
} else {
|
|
167
|
+
lastOneShortPadded = false;
|
|
168
|
+
}
|
|
169
|
+
const isFullscreen = outputHeight >= rows;
|
|
170
|
+
let outputToRender = isFullscreen ? output : output + '\n';
|
|
171
|
+
if (isFullscreen && outputToRender.endsWith('\n')) outputToRender += '\u001B[0m';
|
|
172
|
+
const clearDecision = shouldClearTerminalForFrameProbe({
|
|
173
|
+
isTty: true, viewportRows: rows, previousViewportRows: lastViewportRows,
|
|
174
|
+
previousOutputHeight: lastOutputHeight, nextOutputHeight: outputHeight,
|
|
175
|
+
isUnmounting: false, isWindows,
|
|
176
|
+
});
|
|
177
|
+
if (clearDecision) {
|
|
178
|
+
fakeStream.write('\u001B[0m' + ansiEscapes.clearTerminal + outputToRender);
|
|
179
|
+
log.sync(outputToRender);
|
|
180
|
+
} else {
|
|
181
|
+
log(outputToRender);
|
|
182
|
+
}
|
|
183
|
+
for (const c of chunks) vt.write(c);
|
|
184
|
+
lastOutput = output;
|
|
185
|
+
lastOutputHeight = outputHeight;
|
|
186
|
+
lastViewportRows = rows;
|
|
187
|
+
const trailingNL = outputToRender.endsWith('\n');
|
|
188
|
+
return { outputHeight, trailingNL, clearDecision, isFullscreen };
|
|
189
|
+
};
|
|
190
|
+
return { vt, commit };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---- Scenario builders ----------------------------------------------------
|
|
194
|
+
// Build an App-like frame with the prompt+statusline ALWAYS pinned to the two
|
|
195
|
+
// bottom-most non-blank rows (matching App.jsx: the bottom bar never moves).
|
|
196
|
+
// The App keeps total painted rows == viewport by ceding transcript rows to a
|
|
197
|
+
// panel; so the prompt row is invariant whether or not the palette is open.
|
|
198
|
+
// The only thing that changes between commits is: does the serialized output
|
|
199
|
+
// end on a blank row (→ trailing-newline flip, fs classification flip)?
|
|
200
|
+
// `bottomBlank` models a frame whose LAST painted row is blank padding.
|
|
201
|
+
// `shortByOne` = the root Yoga height is rows-1 for this commit (a reclaimed
|
|
202
|
+
// row that the App's accounting has NOT yet refilled). This is the documented
|
|
203
|
+
// deviant: outputHeight = rows-1 < viewportRows → ink.js takes output+'\n'
|
|
204
|
+
// (NON-fullscreen branch) → log-update relative cursorUp walk → one-row-low
|
|
205
|
+
// dip under Windows Terminal. A steady frame (shortByOne=false) fills the
|
|
206
|
+
// viewport, stays on the absolute cursorTo path, and is stable.
|
|
207
|
+
function frame({ rows, cols, palette, shortByOne, heightRows }) {
|
|
208
|
+
// [FAITHFUL] App.jsx pins the bottom cluster with the outer full-height
|
|
209
|
+
// column (height=resizeState.rows) + flexShrink={0} on the bottom bar. So the
|
|
210
|
+
// Yoga root height is `rows` when accounting is correct. When a reclaimed row
|
|
211
|
+
// is momentarily unaccounted, the laid-out tree is `rows-1` tall for one
|
|
212
|
+
// commit — that is `shortByOne`. In that frame the WHOLE column (including
|
|
213
|
+
// the pinned bottom cluster) sits one physical row higher.
|
|
214
|
+
// heightRows (explicit) overrides shortByOne — lets a caller drive an
|
|
215
|
+
// arbitrary frame height (e.g. rows-3) to exercise the real leave-fullscreen
|
|
216
|
+
// shrink chain, which the boolean shortByOne cannot express.
|
|
217
|
+
const height = heightRows != null ? heightRows : (shortByOne ? rows - 1 : rows);
|
|
218
|
+
const statusRow = height - 1;
|
|
219
|
+
const promptRow = statusRow - 1;
|
|
220
|
+
const lines = [];
|
|
221
|
+
for (let r = 0; r < height; r++) {
|
|
222
|
+
if (r === statusRow) { lines.push('STATUSLINE'); continue; }
|
|
223
|
+
if (r === promptRow) { lines.push('PROMPT>'); continue; }
|
|
224
|
+
if (palette && r === promptRow - 1) { lines.push('SLASHPALETTE'); continue; }
|
|
225
|
+
lines.push(`t${r}`);
|
|
226
|
+
}
|
|
227
|
+
// Serialized string carries exactly `height` lines (no trailing newline).
|
|
228
|
+
return lines.join('\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function run() {
|
|
232
|
+
const rows = 40, cols = 120;
|
|
233
|
+
const isWindows = true;
|
|
234
|
+
console.log(`# renderer harness rows=${rows} cols=${cols} isWindows(assumed)=${isWindows}`);
|
|
235
|
+
console.log(`# NOTE: log-update Windows branch active iff process.platform===win32||WT_SESSION at import`);
|
|
236
|
+
console.log(`# (set WT_SESSION=1 to force it on non-Windows)\n`);
|
|
237
|
+
|
|
238
|
+
// Each frame: { palette, shortByOne }. shortByOne=true is the ONE deviant
|
|
239
|
+
// commit where the App's row accounting leaves the laid-out tree one row
|
|
240
|
+
// short of the viewport (reclaimed panel/hint row not yet refilled).
|
|
241
|
+
const scenarios = [
|
|
242
|
+
{ name: 'palette close, always viewport-filling (correct accounting)', seq: [
|
|
243
|
+
{ palette: true, shortByOne: false },
|
|
244
|
+
{ palette: false, shortByOne: false },
|
|
245
|
+
{ palette: false, shortByOne: false },
|
|
246
|
+
]},
|
|
247
|
+
{ name: 'palette close, close commit ONE ROW SHORT (deviant accounting)', seq: [
|
|
248
|
+
{ palette: true, shortByOne: false },
|
|
249
|
+
{ palette: false, shortByOne: true }, // reclaimed row unaccounted
|
|
250
|
+
{ palette: false, shortByOne: false },
|
|
251
|
+
]},
|
|
252
|
+
{ name: 'prompt newline remove, transitional ONE ROW SHORT', seq: [
|
|
253
|
+
{ palette: false, shortByOne: false },
|
|
254
|
+
{ palette: false, shortByOne: true },
|
|
255
|
+
{ palette: false, shortByOne: false },
|
|
256
|
+
]},
|
|
257
|
+
// Steady one-short: repeated rows-1 frames. The pad must fire ONCE then
|
|
258
|
+
// stop (lastOneShortPadded), so f2/f3 are NOT re-padded and settle at the
|
|
259
|
+
// rows-1 layout (prompt rows-3 / status rows-2) with correct clear
|
|
260
|
+
// decisions — no infinite downward shift.
|
|
261
|
+
{ name: 'steady ONE ROW SHORT (repeated rows-1) — pad once, then stop', seq: [
|
|
262
|
+
{ palette: false, shortByOne: false },
|
|
263
|
+
{ palette: false, shortByOne: true },
|
|
264
|
+
{ palette: false, shortByOne: true },
|
|
265
|
+
{ palette: false, shortByOne: true },
|
|
266
|
+
]},
|
|
267
|
+
// Real leave-fullscreen shrink chain rows→rows-1→rows-3: after the padded
|
|
268
|
+
// rows-1 frame, a further shrink to rows-3 must reach the shrink/clear path
|
|
269
|
+
// (not be masked by a stale pad) and settle cleanly.
|
|
270
|
+
{ name: 'real shrink chain fullscreen→rows-1→rows-3', seq: [
|
|
271
|
+
{ palette: false, heightRows: 40 },
|
|
272
|
+
{ palette: false, heightRows: 39 },
|
|
273
|
+
{ palette: false, heightRows: 37 },
|
|
274
|
+
{ palette: false, heightRows: 37 },
|
|
275
|
+
]},
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
let anyDeviant = false;
|
|
279
|
+
for (const sc of scenarios) {
|
|
280
|
+
const { vt, commit } = makeDriver({ rows, cols, isWindows });
|
|
281
|
+
const promptRows = [];
|
|
282
|
+
console.log(`## ${sc.name}`);
|
|
283
|
+
sc.seq.forEach((f, idx) => {
|
|
284
|
+
const out = frame({ rows, cols, palette: f.palette, shortByOne: f.shortByOne, heightRows: f.heightRows });
|
|
285
|
+
const info = commit(out);
|
|
286
|
+
const pRow = vt.markerRow('PROMPT>');
|
|
287
|
+
const sRow = vt.markerRow('STATUSLINE');
|
|
288
|
+
promptRows.push(pRow);
|
|
289
|
+
console.log(` f${idx} short1=${f.shortByOne?1:0} h=${info.outputHeight} fs=${info.isFullscreen?1:0} trailNL=${info.trailingNL?1:0} clear=${info.clearDecision?1:0} promptRow=${pRow} statusRow=${sRow}`);
|
|
290
|
+
});
|
|
291
|
+
// Deviant = the prompt row BOUNCES (differs from the settled row for a
|
|
292
|
+
// transient frame then returns). A monotone shift to a new steady row
|
|
293
|
+
// (steady-one-short, real shrink) is NOT a bounce — check the LAST row is
|
|
294
|
+
// reached and held, and no interior frame differs from BOTH neighbors.
|
|
295
|
+
const settled = promptRows[promptRows.length - 1];
|
|
296
|
+
const bounce = promptRows.some((r, i) =>
|
|
297
|
+
i > 0 && i < promptRows.length - 1 &&
|
|
298
|
+
r >= 0 && r !== promptRows[i - 1] && r !== promptRows[i + 1]);
|
|
299
|
+
if (bounce) { anyDeviant = true; console.log(` >> DEVIANT(bounce): prompt rows ${JSON.stringify(promptRows)} vs settled ${settled}`); }
|
|
300
|
+
console.log('');
|
|
301
|
+
}
|
|
302
|
+
if (!anyDeviant) console.log('# no deviant frame reproduced at renderer level for these scenarios');
|
|
303
|
+
process.exitCode = 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
run();
|
|
@@ -9,13 +9,12 @@ Bounded slices; smallest coherent change, not rewrite. Stop: unclear scope,
|
|
|
9
9
|
growing blast radius, or Lead-only verification.
|
|
10
10
|
|
|
11
11
|
EDIT-FIRST DISCIPLINE. Survey the slice with ONE batched read/grep round, then
|
|
12
|
-
patch the first bounded piece —
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
deep verification is Lead's.
|
|
12
|
+
patch the first bounded piece — edit incrementally, don't read exhaustively.
|
|
13
|
+
NEVER "one more confirming read": a plausible anchor means the next call is
|
|
14
|
+
`apply_patch`. Repeated read-only turns without an edit = stalling; on a
|
|
15
|
+
runtime reminder, patch the piece you understand or report blocked — a valid
|
|
16
|
+
completion. Self-check comes AFTER edits; deep verification is Lead's.
|
|
18
17
|
|
|
19
18
|
Minimal checks + how-to-verify. Hand off outcome as fragments anchored to
|
|
20
|
-
`file:line`; no narration
|
|
19
|
+
`file:line`; no narration or bloat.
|
|
21
20
|
|
|
@@ -8,13 +8,12 @@ Scoped implementation agent.
|
|
|
8
8
|
Smallest scoped change; no drive-by cleanup. Stop when done/blocked.
|
|
9
9
|
|
|
10
10
|
EDIT-FIRST DISCIPLINE. Brief anchors (`file:line`) are pre-verified — trust and
|
|
11
|
-
patch. No anchor: locate with AT MOST 1-2 reads, then edit
|
|
12
|
-
confirming read"
|
|
13
|
-
`apply_patch`. Repeated read-only turns without an edit = stalling;
|
|
14
|
-
reminder
|
|
15
|
-
|
|
11
|
+
patch. No anchor: locate with AT MOST 1-2 reads, then edit — NEVER "one more
|
|
12
|
+
confirming read"; if you know the file and the change, the next call is
|
|
13
|
+
`apply_patch`. Repeated read-only turns without an edit = stalling; on a
|
|
14
|
+
runtime reminder, patch now or return blocked with what's missing — a blocked
|
|
15
|
+
report is a valid completion. Threshold is "plausible", not "proven";
|
|
16
16
|
self-check comes AFTER the edit, and Lead/Reviewer own final verification.
|
|
17
17
|
|
|
18
|
-
Hand off outcome as fragments anchored to `file:line`; no narration
|
|
19
|
-
bloat.
|
|
18
|
+
Hand off outcome as fragments anchored to `file:line`; no narration or bloat.
|
|
20
19
|
|
package/src/lib/keychain-cjs.cjs
CHANGED
|
@@ -182,23 +182,40 @@ function loadKeytar() {
|
|
|
182
182
|
function keytarSync(method, ...args) {
|
|
183
183
|
loadKeytar(); // throws if not installed — before spawning child
|
|
184
184
|
const { spawnSync } = require('child_process');
|
|
185
|
-
// Pass service/account/value via env to avoid shell injection
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
// Pass service/account/value via stdin (not env) to avoid shell injection
|
|
186
|
+
// and to keep secret values out of /proc/<pid>/environ on Linux.
|
|
187
|
+
// Build a minimal child env instead of spreading process.env: the parent
|
|
188
|
+
// may hold *_API_KEY secrets in its own environment, and those would
|
|
189
|
+
// otherwise be visible via /proc/<pid>/environ for the lifetime of this
|
|
190
|
+
// child. Only pass what PATH resolution / locale / the libsecret D-Bus
|
|
191
|
+
// session bridge actually need.
|
|
192
|
+
const passthroughKeys = [
|
|
193
|
+
'PATH', 'HOME', 'USER', 'LOGNAME',
|
|
194
|
+
'LANG', 'LC_ALL',
|
|
195
|
+
'TMPDIR', 'TMP', 'TEMP',
|
|
196
|
+
'DISPLAY', 'DBUS_SESSION_BUS_ADDRESS', 'XDG_RUNTIME_DIR', 'XDG_CURRENT_DESKTOP', 'XDG_DATA_DIRS',
|
|
197
|
+
];
|
|
198
|
+
const env = { _KEYTAR_METHOD: method };
|
|
199
|
+
for (const key of passthroughKeys) {
|
|
200
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
201
|
+
}
|
|
191
202
|
const script = [
|
|
192
203
|
'const kt = require("keytar");',
|
|
193
204
|
'const method = process.env._KEYTAR_METHOD;',
|
|
194
|
-
'
|
|
195
|
-
'
|
|
196
|
-
'
|
|
197
|
-
'
|
|
205
|
+
'let input = "";',
|
|
206
|
+
'process.stdin.setEncoding("utf8");',
|
|
207
|
+
'process.stdin.on("data", (chunk) => { input += chunk; });',
|
|
208
|
+
'process.stdin.on("end", () => {',
|
|
209
|
+
' const args = JSON.parse(input);',
|
|
210
|
+
' kt[method](...args)',
|
|
211
|
+
' .then(v => { process.stdout.write(JSON.stringify({ ok: true, value: v })); })',
|
|
212
|
+
' .catch(e => { process.stdout.write(JSON.stringify({ ok: false, error: e.message })); });',
|
|
213
|
+
'});',
|
|
198
214
|
].join(' ');
|
|
199
215
|
const r = spawnSync(process.execPath, ['-e', script], {
|
|
200
216
|
env,
|
|
201
|
-
|
|
217
|
+
input: JSON.stringify(args),
|
|
218
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
202
219
|
timeout: 5000,
|
|
203
220
|
encoding: 'utf8',
|
|
204
221
|
windowsHide: true,
|
|
@@ -129,10 +129,10 @@ function buildProfilePreferencesContent(dataDir) {
|
|
|
129
129
|
const profile = normalizeProfileConfig(readAgentConfig(dataDir).profile);
|
|
130
130
|
const lines = [];
|
|
131
131
|
if (profile.title) {
|
|
132
|
-
lines.push(`- Use "${profile.title}" when directly addressing the user (greetings, answers, questions)
|
|
132
|
+
lines.push(`- Use "${profile.title}" when directly addressing the user (greetings, answers, questions); do not repeat it in routine progress updates or pre-tool preambles.`);
|
|
133
133
|
}
|
|
134
134
|
const shell = process.platform === 'win32' ? 'powershell' : 'bash';
|
|
135
|
-
lines.push(`- Shell environment: ${shell}.
|
|
135
|
+
lines.push(`- Shell environment: ${shell}. Write shell commands and scripts in ${shell} syntax unless the user specifies otherwise; keep commands, paths, symbols, and exact errors verbatim.`);
|
|
136
136
|
return lines.length ? `# Profile Preferences\n\n${lines.join('\n')}` : '';
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -144,7 +144,7 @@ function buildLanguageSection(dataDir) {
|
|
|
144
144
|
? ` from system locale ${language.locale}`
|
|
145
145
|
: '';
|
|
146
146
|
const lines = [
|
|
147
|
-
`- Default user-facing response language${source}: ${language.prompt}.
|
|
147
|
+
`- Default user-facing response language${source}: ${language.prompt}. EVERY user-facing message — prose, pre-tool preambles (even single-line), progress updates, questions, final reports, notices — MUST be written in ${language.prompt} and no other language; this overrides any tone implied by the output style. Switch only when the user writes in another language or explicitly asks you to.`,
|
|
148
148
|
`- Code identifiers, paths, commands, symbols, API names, and exact errors should remain in their original form.`,
|
|
149
149
|
];
|
|
150
150
|
return `# Language\n\n${lines.join('\n')}`;
|
|
@@ -382,13 +382,9 @@ function buildAgentRoleContent({ PLUGIN_ROOT, profile = 'full' }) {
|
|
|
382
382
|
function buildAgentRetrievalInjectionContent({ PLUGIN_ROOT }) {
|
|
383
383
|
const AGENT_DIR = path.join(PLUGIN_ROOT, 'rules', 'agent');
|
|
384
384
|
const core = readOptional(path.join(AGENT_DIR, '00-core.md'));
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
'- Batch independent read-only lookups in the same tool turn.',
|
|
389
|
-
'- Use code_graph for symbols/dependencies, grep for exact text, find/glob/list for files, and read only known paths/windows.',
|
|
390
|
-
'',
|
|
391
|
-
];
|
|
385
|
+
// Full shared tool policy (01-tool.md) now ships via BP1 for retrieval
|
|
386
|
+
// roles too; no compact duplicate here.
|
|
387
|
+
const parts = [];
|
|
392
388
|
if (core) parts.push(core.trim());
|
|
393
389
|
parts.push('', '- Read-only retrieval role: do not edit files, run shell, or use git.');
|
|
394
390
|
return parts.join('\n');
|
|
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { performance } from 'node:perf_hooks';
|
|
6
|
+
import httpMod from 'node:http';
|
|
6
7
|
import { ensureStandaloneEnvironment } from './standalone/seeds.mjs';
|
|
7
8
|
import { createStandaloneAgent } from './standalone/agent-tool.mjs';
|
|
8
9
|
import { isAgentOwner } from './runtime/agent/orchestrator/agent-owner.mjs';
|
|
@@ -167,6 +168,9 @@ import {
|
|
|
167
168
|
normalizePluginMcpServerConfig,
|
|
168
169
|
pluginManifest,
|
|
169
170
|
pluginMcpServerName,
|
|
171
|
+
pluginRawMcpServers,
|
|
172
|
+
pluginMcpEnableScript,
|
|
173
|
+
resolveContainedPluginPath,
|
|
170
174
|
} from './session-runtime/plugin-mcp.mjs';
|
|
171
175
|
import {
|
|
172
176
|
WORKFLOW_ROUTE_SLOTS,
|
|
@@ -395,6 +399,13 @@ export async function createMixdogSessionRuntime({
|
|
|
395
399
|
]);
|
|
396
400
|
bootProfile('imports:ready', { ms: (performance.now() - importsStartedAt).toFixed(1) });
|
|
397
401
|
const pluginDataDir = cfgMod.getPluginData();
|
|
402
|
+
// Re-wire the idle/tombstone sweep. startIdleCleanup() lost its caller in a
|
|
403
|
+
// refactor, so closed-session tombstones were never deleted after their 24h
|
|
404
|
+
// grace — the store grew unbounded (observed: 1.8k files / 114MB), which
|
|
405
|
+
// made summary-index rebuilds and per-save index rewrites stall boot for
|
|
406
|
+
// seconds. Timer is unref'd and first fires after CLEANUP_INITIAL_DELAY_MS
|
|
407
|
+
// (5min), so this adds zero boot-path cost.
|
|
408
|
+
try { mgr.startIdleCleanup?.(); } catch { /* cleanup is best-effort */ }
|
|
398
409
|
const memoryRuntime = createStandaloneMemoryRuntime({
|
|
399
410
|
entry: join(STANDALONE_ROOT, MEMORY_RUNTIME.replace(/^\.\//, '')),
|
|
400
411
|
dataDir: pluginDataDir,
|
|
@@ -1531,11 +1542,42 @@ export async function createMixdogSessionRuntime({
|
|
|
1531
1542
|
// published to active-instance.json by getMemoryModule().init(). Without
|
|
1532
1543
|
// this, memory only starts on the first turn's getMemoryModule() call —
|
|
1533
1544
|
// so early channel traffic finds no memory_port and gets buffered (or,
|
|
1534
|
-
// pre-drainer, silently dropped).
|
|
1535
|
-
//
|
|
1536
|
-
//
|
|
1537
|
-
|
|
1538
|
-
|
|
1545
|
+
// pre-drainer, silently dropped). Runs BEFORE channel claim so the port is
|
|
1546
|
+
// racing to be live by the time the worker sends its first ingest.
|
|
1547
|
+
//
|
|
1548
|
+
// Not fire-and-forget: init() only resolves the module handle, so a bare
|
|
1549
|
+
// getMemoryModule() could return before /health reports ok — leaving early
|
|
1550
|
+
// ingests to hit a not-yet-listening port. Await the proxy start() and then
|
|
1551
|
+
// poll /health with a bounded retry so the daemon is provably reachable
|
|
1552
|
+
// (or logged failed) rather than assumed-up. Still non-blocking to the
|
|
1553
|
+
// caller: the whole probe is detached, but it internally awaits readiness.
|
|
1554
|
+
void (async () => {
|
|
1555
|
+
try {
|
|
1556
|
+
const mod = await getMemoryModule();
|
|
1557
|
+
const started = typeof mod?.start === 'function' ? await mod.start() : null;
|
|
1558
|
+
const port = started?.port;
|
|
1559
|
+
if (!port) { bootProfile('channels:memory-eager-init-failed', { error: 'no port from start()' }); return; }
|
|
1560
|
+
for (let i = 0; i < 30; i++) {
|
|
1561
|
+
try {
|
|
1562
|
+
const ok = await new Promise((res) => {
|
|
1563
|
+
const req = httpMod.request({ hostname: '127.0.0.1', port, path: '/health', timeout: 1500 }, (r) => {
|
|
1564
|
+
let d = ''; r.on('data', (c) => { d += c; }); r.on('end', () => {
|
|
1565
|
+
try { res(JSON.parse(d)?.status === 'ok'); } catch { res(false); }
|
|
1566
|
+
});
|
|
1567
|
+
});
|
|
1568
|
+
req.on('error', () => res(false));
|
|
1569
|
+
req.on('timeout', () => { req.destroy(); res(false); });
|
|
1570
|
+
req.end();
|
|
1571
|
+
});
|
|
1572
|
+
if (ok) { bootProfile('channels:memory-eager-init-ready', { port }); return; }
|
|
1573
|
+
} catch {}
|
|
1574
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1575
|
+
}
|
|
1576
|
+
bootProfile('channels:memory-eager-init-failed', { error: `health not ok after retries (port ${port})` });
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) });
|
|
1579
|
+
}
|
|
1580
|
+
})();
|
|
1539
1581
|
// Publish this session's record + transcript file BEFORE the worker's
|
|
1540
1582
|
// activate-time discovery polls, so output forwarding binds to this
|
|
1541
1583
|
// terminal session immediately instead of waiting for the first turn.
|
|
@@ -1640,8 +1682,38 @@ export async function createMixdogSessionRuntime({
|
|
|
1640
1682
|
// channel; see startRemote()/stopRemote() and the `/remote` toggle.
|
|
1641
1683
|
// `remote.autoStart` in mixdog-config.json makes every session claim remote
|
|
1642
1684
|
// at boot (same force-takeover semantics as `mixdog --remote` / `/remote`).
|
|
1643
|
-
|
|
1644
|
-
|
|
1685
|
+
// The flag lives in the TOP-LEVEL `remote` section of mixdog-config.json
|
|
1686
|
+
// (sibling of agent/ui/channels), not inside the agent section that
|
|
1687
|
+
// cfgMod.loadConfig returns — read it via the shared whole-file reader.
|
|
1688
|
+
// `config?.remote?.autoStart` is kept for back-compat with agent-section
|
|
1689
|
+
// placement.
|
|
1690
|
+
if (!remoteEnabled) {
|
|
1691
|
+
try {
|
|
1692
|
+
if (config?.remote?.autoStart === true
|
|
1693
|
+
|| sharedCfgMod?.readSection?.('remote')?.autoStart === true) {
|
|
1694
|
+
remoteEnabled = true;
|
|
1695
|
+
}
|
|
1696
|
+
} catch { /* unreadable config never blocks boot */ }
|
|
1697
|
+
}
|
|
1698
|
+
// Boot-time remote start (autoStart or --remote) is DEFERRED past the TUI's
|
|
1699
|
+
// first frame. startRemote() front-loads heavy work — memory daemon fork
|
|
1700
|
+
// (PG + forced ONNX embed warmup in the child), eager session create, and
|
|
1701
|
+
// the channel-worker fork — and running it inline here interleaves that
|
|
1702
|
+
// CPU/disk load with engine boot, visibly delaying the first ink frame by
|
|
1703
|
+
// seconds. The deferred timer reuses prewarmTimers.channelStartTimer so an
|
|
1704
|
+
// early /remote (startRemote clears it), stopRemote(), and close() all
|
|
1705
|
+
// cancel it through the existing clearTimeout paths. Runtime /remote calls
|
|
1706
|
+
// still start immediately (user-initiated, UI already painted).
|
|
1707
|
+
if (remoteEnabled) {
|
|
1708
|
+
const remoteAutoStartDelayMs = envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
1709
|
+
bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs });
|
|
1710
|
+
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
1711
|
+
prewarmTimers.channelStartTimer = null;
|
|
1712
|
+
if (closeRequested || !remoteEnabled) return;
|
|
1713
|
+
startRemote();
|
|
1714
|
+
}, remoteAutoStartDelayMs);
|
|
1715
|
+
prewarmTimers.channelStartTimer.unref?.();
|
|
1716
|
+
}
|
|
1645
1717
|
|
|
1646
1718
|
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
1647
1719
|
const compaction = session?.compaction || {};
|
|
@@ -2769,19 +2841,17 @@ export async function createMixdogSessionRuntime({
|
|
|
2769
2841
|
},
|
|
2770
2842
|
async enablePluginMcp(plugin = {}) {
|
|
2771
2843
|
const root = clean(plugin.root);
|
|
2772
|
-
const script =
|
|
2844
|
+
const script = pluginMcpEnableScript(root, plugin);
|
|
2773
2845
|
if (!root || !script) throw new Error('plugin has no MCP script');
|
|
2774
2846
|
const serverName = pluginMcpServerName(plugin);
|
|
2775
2847
|
const nextConfig = { ...config };
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
const keys = Object.keys(rawServers).filter((k) => isPlainObject(rawServers[k]));
|
|
2784
|
-
if (!keys.length) throw new Error(`plugin .mcp.json has no mcpServers: ${mcpJsonPath}`);
|
|
2848
|
+
const manifestMcp = pluginRawMcpServers(root, script);
|
|
2849
|
+
if (manifestMcp) {
|
|
2850
|
+
const { rawServers, mcpRoot } = manifestMcp;
|
|
2851
|
+
const keys = Object.keys(rawServers).filter((k) => {
|
|
2852
|
+
const v = rawServers[k];
|
|
2853
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
2854
|
+
});
|
|
2785
2855
|
const ownedPrefix = `${serverName}--`;
|
|
2786
2856
|
const nextServers = {};
|
|
2787
2857
|
for (const [k, v] of Object.entries(nextConfig.mcpServers || {})) {
|
|
@@ -2789,7 +2859,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2789
2859
|
nextServers[k] = v;
|
|
2790
2860
|
}
|
|
2791
2861
|
for (const serverKey of keys) {
|
|
2792
|
-
const cfg = normalizePluginMcpServerConfig(rawServers[serverKey],
|
|
2862
|
+
const cfg = normalizePluginMcpServerConfig(rawServers[serverKey], mcpRoot);
|
|
2793
2863
|
cfg.env = {
|
|
2794
2864
|
...(cfg.env || {}),
|
|
2795
2865
|
MIXDOG_PLUGIN_ROOT: root,
|
|
@@ -2800,8 +2870,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2800
2870
|
}
|
|
2801
2871
|
nextConfig.mcpServers = nextServers;
|
|
2802
2872
|
} else {
|
|
2803
|
-
const scriptPath =
|
|
2804
|
-
if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${
|
|
2873
|
+
const scriptPath = resolveContainedPluginPath(root, script);
|
|
2874
|
+
if (!scriptPath || !existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${join(root, script)}`);
|
|
2805
2875
|
nextConfig.mcpServers = {
|
|
2806
2876
|
...(nextConfig.mcpServers || {}),
|
|
2807
2877
|
[serverName]: {
|