mixdog 0.9.4 → 0.9.6
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/README.md +112 -38
- package/package.json +4 -1
- package/scripts/bench/r5-orchestrated-task.json +7 -0
- package/scripts/bench/round-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
- package/scripts/bench-run.mjs +40 -7
- package/scripts/internal-comms-bench.mjs +2 -4
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- package/scripts/recall-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/tool-smoke.mjs +2 -2
- package/src/agents/heavy-worker/AGENT.md +7 -7
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +9 -10
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/mixdog-session-runtime.mjs +15 -14
- package/src/output-styles/default.md +14 -18
- package/src/output-styles/minimal.md +4 -5
- package/src/output-styles/simple.md +7 -8
- package/src/rules/agent/00-common.md +6 -5
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +6 -3
- package/src/rules/lead/lead-tool.md +6 -4
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +53 -9
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +23 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +33 -4
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +93 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +27 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/channels/backends/discord-gateway.mjs +2 -2
- package/src/runtime/channels/backends/discord.mjs +15 -26
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +23 -43
- package/src/runtime/channels/lib/backend-dispatch.mjs +5 -3
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +4 -4
- package/src/runtime/channels/lib/event-queue.mjs +15 -9
- package/src/runtime/channels/lib/inbound-routing.mjs +2 -39
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- package/src/runtime/channels/lib/scheduler.mjs +27 -113
- package/src/runtime/channels/lib/webhook/deliveries.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +8 -47
- package/src/runtime/memory/index.mjs +9 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- package/src/runtime/memory/lib/ko-morph.mjs +195 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +47 -10
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/query-handlers.mjs +27 -5
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/shared/config.mjs +14 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/session-runtime/config-helpers.mjs +45 -10
- package/src/session-runtime/quick-model-rows.mjs +46 -11
- package/src/session-runtime/quick-search-models.mjs +28 -27
- package/src/session-runtime/settings-api.mjs +34 -1
- package/src/session-runtime/tool-defs.mjs +1 -1
- package/src/session-runtime/workflow.mjs +1 -1
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/agent-tool.mjs +30 -0
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/explore-tool.mjs +39 -10
- package/src/tui/App.jsx +131 -28
- package/src/tui/app/app-format.mjs +11 -4
- package/src/tui/app/channel-pickers.mjs +2 -4
- package/src/tui/app/maintenance-pickers.mjs +60 -28
- package/src/tui/app/model-options.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +0 -1
- package/src/tui/app/transcript-window.mjs +8 -2
- package/src/tui/app/use-transcript-scroll.mjs +3 -1
- package/src/tui/app/use-transcript-window.mjs +19 -1
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +32 -11
- package/src/tui/components/PromptInput.jsx +10 -1
- package/src/tui/components/StatusLine.jsx +6 -3
- package/src/tui/dist/index.mjs +441 -293
- package/src/tui/engine.mjs +48 -12
- package/src/tui/index.jsx +11 -52
- package/src/ui/statusline.mjs +10 -0
- package/src/workflows/default/WORKFLOW.md +15 -17
- package/src/workflows/sequential/WORKFLOW.md +15 -17
- package/vendor/ink/build/ink.js +9 -1
- package/scripts/recall-usecase-probe.json +0 -6
- package/src/runtime/channels/lib/holidays.mjs +0 -138
package/src/tui/engine.mjs
CHANGED
|
@@ -173,8 +173,12 @@ export async function createEngineSession({
|
|
|
173
173
|
if (!status) return;
|
|
174
174
|
const displayWindow = Number(status.contextWindow || 0);
|
|
175
175
|
const compactBoundary = Number(status.compaction?.boundaryTokens || 0);
|
|
176
|
+
// Prefer the resolved trigger (boundary - buffer): the statusline uses it
|
|
177
|
+
// as the display denominator so context % reads 100% exactly when
|
|
178
|
+
// auto-compact fires, instead of stalling at ~90% of the boundary.
|
|
176
179
|
const autoCompact = Number(
|
|
177
|
-
status.compaction?.
|
|
180
|
+
status.compaction?.triggerTokens
|
|
181
|
+
|| status.compaction?.autoCompactTokenLimit
|
|
178
182
|
|| runtime.session?.autoCompactTokenLimit
|
|
179
183
|
|| 0,
|
|
180
184
|
);
|
|
@@ -655,7 +659,13 @@ export async function createEngineSession({
|
|
|
655
659
|
// cards are pushed alongside the result-bearing one before their own delay
|
|
656
660
|
// elapses and would otherwise paint an empty reserved band.
|
|
657
661
|
// Mirrors components/ToolExecution.jsx TOOL_PENDING_SHOW_DELAY_MS.
|
|
658
|
-
|
|
662
|
+
// [jitter fix] Reserve the tool-card row immediately instead of after a
|
|
663
|
+
// 1000ms delay. Delayed insertion made rows appear late and shove the body
|
|
664
|
+
// vertically; pushing now (with the real 'Running' header height via
|
|
665
|
+
// deferredDisplayReady) keeps the layout stable. The timer path is kept as a
|
|
666
|
+
// 0ms microtask fallback for entries that are registered but not explicitly
|
|
667
|
+
// surfaced, preserving call-order flush semantics.
|
|
668
|
+
const TOOL_CARD_PUSH_DELAY_MS = 0;
|
|
659
669
|
let deferredSeqCounter = 0;
|
|
660
670
|
const deferredEntries = []; // creation-order list; each is pushed at most once
|
|
661
671
|
// Push this entry AND every earlier-created still-deferred entry, in order,
|
|
@@ -927,7 +937,7 @@ export async function createEngineSession({
|
|
|
927
937
|
// never held back waiting for the next Ink frame. 16ms (~60fps) left every
|
|
928
938
|
// other Ink frame idle, which made fast provider streams visibly land in
|
|
929
939
|
// coarse chunks ("10 chars at a time").
|
|
930
|
-
const STREAM_BATCH_INTERVAL_MS =
|
|
940
|
+
const STREAM_BATCH_INTERVAL_MS = 16;
|
|
931
941
|
let _batchTimer = null;
|
|
932
942
|
let _pendingTextFlush = false; // true when a text/spinner update is queued
|
|
933
943
|
let _pendingThinkFlush = false; // true when a thinking update is queued
|
|
@@ -1155,6 +1165,10 @@ export async function createEngineSession({
|
|
|
1155
1165
|
}
|
|
1156
1166
|
|
|
1157
1167
|
const touchedAggregates = new Set();
|
|
1168
|
+
// [jitter fix] Last standalone (Agent) card in this batch to reserve a
|
|
1169
|
+
// row for. Flushed AFTER the syncAggregateHeader loop so any earlier-seq
|
|
1170
|
+
// aggregate it would flush-through already has its pendingSpec built.
|
|
1171
|
+
let standaloneReserve = null;
|
|
1158
1172
|
for (let i = 0; i < batchCalls.length; i++) {
|
|
1159
1173
|
const c = batchCalls[i];
|
|
1160
1174
|
const name = toolCallName(c);
|
|
@@ -1207,6 +1221,13 @@ export async function createEngineSession({
|
|
|
1207
1221
|
cardByCallId.set(callId, card);
|
|
1208
1222
|
}
|
|
1209
1223
|
toolCards.push(card);
|
|
1224
|
+
// [jitter fix] Immediate row-reserve is deferred to after the
|
|
1225
|
+
// syncAggregateHeader loop below (see standaloneReserve): calling
|
|
1226
|
+
// ensureVisible() here would flushDeferredUpTo() every earlier-seq
|
|
1227
|
+
// entry — including an aggregate whose pendingSpec syncAggregateHeader
|
|
1228
|
+
// hasn't built yet — marking it pushed without inserting (lost/
|
|
1229
|
+
// out-of-order card). Record it and flush once headers exist.
|
|
1230
|
+
standaloneReserve = card;
|
|
1210
1231
|
// A standalone card (Agent) breaks the consecutive run too: a
|
|
1211
1232
|
// later same-bucket call must open a fresh card BELOW it, not
|
|
1212
1233
|
// merge into an aggregate above it.
|
|
@@ -1234,6 +1255,11 @@ export async function createEngineSession({
|
|
|
1234
1255
|
for (const aggregateCard of touchedAggregates) {
|
|
1235
1256
|
syncAggregateHeader(aggregateCard);
|
|
1236
1257
|
}
|
|
1258
|
+
// [jitter fix] Now that every touched aggregate has its pendingSpec,
|
|
1259
|
+
// reserve the standalone (Agent) card's row immediately. Its
|
|
1260
|
+
// ensureVisible() flushes earlier-seq entries too, but those aggregates
|
|
1261
|
+
// are now push-ready so none is marked pushed without inserting.
|
|
1262
|
+
standaloneReserve?.ensureVisible?.();
|
|
1237
1263
|
if (committedAssistantSegment) {
|
|
1238
1264
|
// A pre-tool assistant preamble has already had one render frame to
|
|
1239
1265
|
// settle. Do not let the first grouped tool card sit off-screen until
|
|
@@ -1280,7 +1306,7 @@ export async function createEngineSession({
|
|
|
1280
1306
|
detail: compactEventDetail(event),
|
|
1281
1307
|
});
|
|
1282
1308
|
},
|
|
1283
|
-
onStageChange: (stage) => {
|
|
1309
|
+
onStageChange: async (stage) => {
|
|
1284
1310
|
if (!state.spinner) return;
|
|
1285
1311
|
const value = String(stage || '');
|
|
1286
1312
|
if (value === 'compacting') {
|
|
@@ -1299,6 +1325,7 @@ export async function createEngineSession({
|
|
|
1299
1325
|
mode: 'compacting',
|
|
1300
1326
|
},
|
|
1301
1327
|
});
|
|
1328
|
+
await yieldToRenderer();
|
|
1302
1329
|
return;
|
|
1303
1330
|
}
|
|
1304
1331
|
if (value === 'requesting' || value === 'streaming') compactingActive = false;
|
|
@@ -2225,6 +2252,11 @@ export async function createEngineSession({
|
|
|
2225
2252
|
const startedAt = Date.now();
|
|
2226
2253
|
set({ commandBusy: true, commandStatus: { active: true, verb: 'Compacting conversation', startedAt, mode: 'compacting' } });
|
|
2227
2254
|
try {
|
|
2255
|
+
// Give Ink one event-loop turn to paint the compacting spinner before
|
|
2256
|
+
// runtime.compact() starts synchronous session/transcript work (same
|
|
2257
|
+
// yield as the auto-clear path; without it /compact looks frozen with
|
|
2258
|
+
// no spinner until the compact already finished).
|
|
2259
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2228
2260
|
const result = await runtime.compact({ recoverAgent: true });
|
|
2229
2261
|
syncContextStats({ allowEstimated: true });
|
|
2230
2262
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
@@ -2248,6 +2280,15 @@ export async function createEngineSession({
|
|
|
2248
2280
|
error: result.error,
|
|
2249
2281
|
}),
|
|
2250
2282
|
});
|
|
2283
|
+
} else {
|
|
2284
|
+
// null = session missing/closed: still surface a done row so
|
|
2285
|
+
// /compact never ends silently without a completion marker.
|
|
2286
|
+
pushItem({
|
|
2287
|
+
kind: 'statusdone',
|
|
2288
|
+
id: nextId(),
|
|
2289
|
+
label: 'Compact failed',
|
|
2290
|
+
detail: 'no active session',
|
|
2291
|
+
});
|
|
2251
2292
|
}
|
|
2252
2293
|
return result;
|
|
2253
2294
|
} finally {
|
|
@@ -2556,14 +2597,9 @@ export async function createEngineSession({
|
|
|
2556
2597
|
pushNotice('webhook/ngrok authtoken forgotten', 'info');
|
|
2557
2598
|
return result;
|
|
2558
2599
|
},
|
|
2559
|
-
|
|
2560
|
-
const result = runtime.
|
|
2561
|
-
pushNotice(
|
|
2562
|
-
return result;
|
|
2563
|
-
},
|
|
2564
|
-
deleteChannel: (name) => {
|
|
2565
|
-
const result = runtime.deleteChannel(name);
|
|
2566
|
-
pushNotice(`channel deleted: ${name}`, 'info');
|
|
2600
|
+
setChannel: (entry) => {
|
|
2601
|
+
const result = runtime.setChannel(entry);
|
|
2602
|
+
pushNotice('channel saved', 'info');
|
|
2567
2603
|
return result;
|
|
2568
2604
|
},
|
|
2569
2605
|
setWebhookConfig: (patch) => {
|
package/src/tui/index.jsx
CHANGED
|
@@ -16,6 +16,7 @@ import { installProcessSignalCleanup } from '../runtime/shared/process-shutdown.
|
|
|
16
16
|
import { touchUiHeartbeat } from '../runtime/channels/lib/runtime-paths.mjs';
|
|
17
17
|
import { emitTerminalBackground, loadThemeSettingFromConfig, theme } from './theme.mjs';
|
|
18
18
|
import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
|
|
19
|
+
import { displayWidth } from './display-width.mjs';
|
|
19
20
|
|
|
20
21
|
const TERMINAL_MODE_RESET = '\x1b[?1006l\x1b[?1005l\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?25h';
|
|
21
22
|
const TERMINAL_OSC_RESET_BG = '\x1b]111\x07';
|
|
@@ -152,16 +153,18 @@ function ansiFg(rgb) {
|
|
|
152
153
|
* the seconds between alt-screen entry and the first ink frame are not a blank
|
|
153
154
|
* void. The logo is drawn at the SAME rows/columns as App.jsx's welcome banner
|
|
154
155
|
* (marginTop 3 + 5 logo rows + blank + subtitle), so ink's first real frame
|
|
155
|
-
* overprints it in place with no visible jump.
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
156
|
+
* overprints it in place with no visible jump. Do NOT pre-paint the bottom
|
|
157
|
+
* picker/prompt frame here: the first ink frame may hide or move those rows
|
|
158
|
+
* while the initial project picker settles, and stale raw borders look like a
|
|
159
|
+
* broken frame. Returns { stop } — call it right before ink mounts.
|
|
159
160
|
*/
|
|
160
161
|
function paintBootSplash() {
|
|
161
162
|
try {
|
|
162
163
|
const cols = Math.max(1, Number(process.stdout.columns) || 80);
|
|
163
164
|
const rows = Math.max(1, Number(process.stdout.rows) || 24);
|
|
164
|
-
const
|
|
165
|
+
const windowsLikeTerminal = process.platform === 'win32' || Boolean(process.env.WT_SESSION);
|
|
166
|
+
const frameCols = Math.max(1, cols - (windowsLikeTerminal ? 1 : 0));
|
|
167
|
+
const center = (s) => `${' '.repeat(Math.max(0, Math.floor((frameCols - displayWidth(s)) / 2)))}${s}`;
|
|
165
168
|
const logo = [
|
|
166
169
|
'███╗ ███╗██╗██╗ ██╗██████╗ ██████╗ ██████╗ ',
|
|
167
170
|
'████╗ ████║██║╚██╗██╔╝██╔══██╗██╔═══██╗██╔════╝ ',
|
|
@@ -181,56 +184,12 @@ function paintBootSplash() {
|
|
|
181
184
|
}
|
|
182
185
|
out += '\r\n';
|
|
183
186
|
out += `${subtleFg}${center(`mixdog coding agent · ${process.cwd()}`)}${reset}`;
|
|
184
|
-
|
|
185
|
-
// floating panel sitting directly above the 3-row statusline — not the
|
|
186
|
-
// 3-row prompt box. Painting a short prompt silhouette here made the panel
|
|
187
|
-
// look like it "unfolded" from 3 rows to full height on the first ink
|
|
188
|
-
// frame. Mirror App.jsx's startup math instead:
|
|
189
|
-
// floatingPanelRows = min(17, rows - 15), bottom row = rows - 3.
|
|
190
|
-
const floating = Math.min(17, rows - 15);
|
|
191
|
-
let spinnerRow = 0;
|
|
192
|
-
let spinnerFg = '';
|
|
193
|
-
if (floating >= 3) {
|
|
194
|
-
const borderFg = ansiFg(theme.promptBorder) || subtleFg;
|
|
195
|
-
// Leave the last COLUMN untouched: writing the bottom-right cell makes
|
|
196
|
-
// Windows Terminal/conhost scroll the alt screen one row (same reason
|
|
197
|
-
// App.jsx reserves rightSafetyColumns), which made the whole splash jump
|
|
198
|
-
// when the box was painted at full width on the final row.
|
|
199
|
-
const boxW = Math.max(4, cols - 1);
|
|
200
|
-
const innerW = boxW - 2;
|
|
201
|
-
const topRow = rows - 3 - floating + 1;
|
|
202
|
-
out += `\x1b[${topRow};1H${borderFg}╭${'─'.repeat(innerW)}╮`;
|
|
203
|
-
for (let i = 1; i < floating - 1; i++) {
|
|
204
|
-
out += `\x1b[${topRow + i};1H│${' '.repeat(innerW)}│`;
|
|
205
|
-
}
|
|
206
|
-
out += `\x1b[${rows - 3};1H╰${'─'.repeat(innerW)}╯${reset}`;
|
|
207
|
-
// "Loading projects…" line inside the panel silhouette. Picker.jsx
|
|
208
|
-
// renders border, then (paddingX=1, no paddingY) title/help row as its
|
|
209
|
-
// very first interior row — not vertically centered. Match that rhythm
|
|
210
|
-
// (topRow + 1) instead of centering, so ink's first frame doesn't jump
|
|
211
|
-
// the text down/up when it overprints this splash.
|
|
212
|
-
spinnerRow = topRow + 1;
|
|
213
|
-
spinnerFg = subtleFg;
|
|
214
|
-
out += `\x1b[${spinnerRow};3H${subtleFg} Loading projects…${reset}`;
|
|
215
|
-
}
|
|
187
|
+
|
|
216
188
|
// Park the cursor at home so ink's first frame paints top-down over the
|
|
217
189
|
// splash instead of starting at the bottom row and scrolling the screen.
|
|
218
190
|
out += '\x1b[H';
|
|
219
191
|
process.stdout.write(out);
|
|
220
|
-
|
|
221
|
-
// so stopping mid-frame can't leave torn output for ink to overwrite.
|
|
222
|
-
if (spinnerRow > 0) {
|
|
223
|
-
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
224
|
-
let tick = 0;
|
|
225
|
-
const timer = setInterval(() => {
|
|
226
|
-
try {
|
|
227
|
-
process.stdout.write(`\x1b[${spinnerRow};3H${spinnerFg}${frames[tick % frames.length]}${reset}\x1b[H`);
|
|
228
|
-
tick += 1;
|
|
229
|
-
} catch { /* ignore */ }
|
|
230
|
-
}, 80);
|
|
231
|
-
timer.unref?.();
|
|
232
|
-
return { stop: () => clearInterval(timer) };
|
|
233
|
-
}
|
|
192
|
+
|
|
234
193
|
return { stop: () => {} };
|
|
235
194
|
} catch { /* cosmetic only — never block boot */ }
|
|
236
195
|
return { stop: () => {} };
|
|
@@ -472,7 +431,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
|
|
|
472
431
|
// does not exit abruptly. Explicit exits go through /exit or /quit so teardown
|
|
473
432
|
// still restores the cursor, mouse mode, and alternate screen cleanly.
|
|
474
433
|
try {
|
|
475
|
-
const instance = render(<App store={store} forceOnboarding={forceOnboarding === true} />, { exitOnCtrlC: false, maxFps:
|
|
434
|
+
const instance = render(<App store={store} forceOnboarding={forceOnboarding === true} />, { exitOnCtrlC: false, maxFps: 60, onRender: makeRenderProfiler() });
|
|
476
435
|
bootProfile('render:mounted', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
477
436
|
const { waitUntilExit } = instance;
|
|
478
437
|
// [mixdog fork] Hand the ink renderer's drag-selection setter to the store so
|
package/src/ui/statusline.mjs
CHANGED
|
@@ -173,6 +173,16 @@ function resolveContextUsedPct({
|
|
|
173
173
|
autoCompactTokenLimit,
|
|
174
174
|
compact,
|
|
175
175
|
});
|
|
176
|
+
// Trigger-as-denominator: when a sub-boundary compaction trigger
|
|
177
|
+
// (boundary - buffer) is known, context % is measured against IT so the
|
|
178
|
+
// gauge reads 100% exactly when auto-compact fires instead of stalling at
|
|
179
|
+
// ~90% of the boundary window. The gateway's own pct is computed against
|
|
180
|
+
// the boundary denominator, so it is bypassed in that case.
|
|
181
|
+
const trigger = num(autoCompactTokenLimit);
|
|
182
|
+
const triggerDenominator = trigger > 0 && (!(boundary > 0) || trigger < boundary);
|
|
183
|
+
if (triggerDenominator && numerator > 0) {
|
|
184
|
+
return (numerator / trigger) * 100;
|
|
185
|
+
}
|
|
176
186
|
const gatewayRawPct = gatewayStatus?.contextUsedPct;
|
|
177
187
|
if (
|
|
178
188
|
gatewayStatus
|
|
@@ -16,16 +16,15 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
|
|
|
16
16
|
- Debugger: deploy for very high complexity, or when root-causing has already
|
|
17
17
|
failed at least once.
|
|
18
18
|
|
|
19
|
-
1. Plan — discuss the request with the user, form a plan,
|
|
19
|
+
1. Plan — discuss the request with the user, form a plan, wait for approval.
|
|
20
20
|
2. Delegate — split into the maximum number of independent scopes.
|
|
21
21
|
- PARALLEL across independent scopes by default (implementation, analysis,
|
|
22
|
-
review, debugging
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- SEQUENTIAL within a single complex scope — split
|
|
27
|
-
|
|
28
|
-
between steps.
|
|
22
|
+
review, debugging alike). Spawn every scope in the SAME turn. Shared or
|
|
23
|
+
cross-cutting code does NOT justify merging: split per path and verify the
|
|
24
|
+
shared parts yourself. The only single scope is a genuinely inseparable
|
|
25
|
+
dependency — then state it.
|
|
26
|
+
- SEQUENTIAL within a single complex scope — split into ordered steps rather
|
|
27
|
+
than one shot, with a build/test-green gate between steps.
|
|
29
28
|
- Write briefs per the Lead brief contract (token-optimized labeled
|
|
30
29
|
fragments).
|
|
31
30
|
- After spawning async agent(s), END THE TURN — do not poll, guess, or start
|
|
@@ -34,15 +33,14 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
|
|
|
34
33
|
3. Review — pair one reviewer 1:1 with each implementation scope, spawned in the
|
|
35
34
|
same turn; never defer or batch the reviewer call — wait for its result.
|
|
36
35
|
Fact-check the agent response and cross-check implementation and review
|
|
37
|
-
results yourself before acting
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
issues prepare deploy/build/commit.
|
|
36
|
+
results yourself before acting. Send fixes back to the original scope and
|
|
37
|
+
loop verify -> fix -> re-verify until clean. Skip review only for simple,
|
|
38
|
+
low-risk tasks. If the user asks for debugging, or a bug survives 2+ fix
|
|
39
|
+
cycles, have the debugger investigate first instead of another fix round.
|
|
40
|
+
4. Report — synthesize results (outcome + key evidence; never forward raw agent
|
|
41
|
+
output), state the final state, and ask whether to ship/deploy when
|
|
42
|
+
relevant. Only after user feedback with no issues prepare
|
|
43
|
+
deploy/build/commit.
|
|
46
44
|
|
|
47
45
|
On any major change or direction shift mid-work, pause and re-consult the user
|
|
48
46
|
before continuing.
|
|
@@ -20,15 +20,13 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
|
|
|
20
20
|
- Debugger: deploy for very high complexity, or when root-causing has already
|
|
21
21
|
failed at least once.
|
|
22
22
|
|
|
23
|
-
1. Plan — discuss the request with the user, form a plan,
|
|
24
|
-
2. Delegate — split into ordered scopes and hand
|
|
23
|
+
1. Plan — discuss the request with the user, form a plan, wait for approval.
|
|
24
|
+
2. Delegate — split into ordered scopes and hand off ONE AT A TIME.
|
|
25
25
|
- NO PARALLEL, NO FAN-OUT. Even when scopes are independent, spawn exactly
|
|
26
|
-
one agent per turn; never
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
rather than handing it off in one shot, with a build/test-green gate
|
|
31
|
-
between steps.
|
|
26
|
+
one agent per turn; never multiple in the same turn. Wait for the current
|
|
27
|
+
scope's completion notification before spawning the next (1:1 sequential).
|
|
28
|
+
- SEQUENTIAL within a single complex scope — split into ordered steps rather
|
|
29
|
+
than one shot, with a build/test-green gate between steps.
|
|
32
30
|
- Write briefs per the Lead brief contract (token-optimized labeled
|
|
33
31
|
fragments).
|
|
34
32
|
- After spawning an async agent, END THE TURN — do not poll, guess, or start
|
|
@@ -37,15 +35,15 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
|
|
|
37
35
|
3. Review — after each implementation scope completes, pair one reviewer 1:1
|
|
38
36
|
with that scope (spawned on its own turn, not batched); never defer the
|
|
39
37
|
reviewer call — wait for its result. Fact-check the agent response and
|
|
40
|
-
cross-check implementation and review results yourself before acting
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
4. Report —
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
cross-check implementation and review results yourself before acting. Send
|
|
39
|
+
fixes back to the original scope and loop verify -> fix -> re-verify until
|
|
40
|
+
clean. Skip review only for simple, low-risk tasks. If the user asks for
|
|
41
|
+
debugging, or a bug survives 2+ fix cycles, have the debugger investigate
|
|
42
|
+
first instead of another fix round.
|
|
43
|
+
4. Report — synthesize results (outcome + key evidence; never forward raw agent
|
|
44
|
+
output), state the final state, and ask whether to ship/deploy when
|
|
45
|
+
relevant. Only after user feedback with no issues prepare
|
|
46
|
+
deploy/build/commit.
|
|
49
47
|
|
|
50
48
|
On any major change or direction shift mid-work, pause and re-consult the user
|
|
51
49
|
before continuing.
|
package/vendor/ink/build/ink.js
CHANGED
|
@@ -106,7 +106,15 @@ const shouldClearTerminalForFrame = ({ isTty, viewportRows, previousViewportRows
|
|
|
106
106
|
nextOutputHeight < previousOutputHeight &&
|
|
107
107
|
(wasFullscreen || isFullscreen || wasOverflowing || isOverflowing);
|
|
108
108
|
const shouldClearOnUnmount = isUnmounting && wasFullscreen;
|
|
109
|
-
|
|
109
|
+
// [jitter fix] Do not full-clear EVERY fullscreen frame on Windows consoles:
|
|
110
|
+
// that repaint-from-scratch caused visible vertical jitter. A full clear is
|
|
111
|
+
// only needed when the geometry actually changes — the viewport was resized
|
|
112
|
+
// (viewportRows differs from the previous frame) or the output height
|
|
113
|
+
// shrank at/over the viewport (stale wrapped rows possible). Otherwise
|
|
114
|
+
// repaint in place via the incremental erase path.
|
|
115
|
+
const viewportResized = previousViewportRows != null && previousViewportRows !== viewportRows;
|
|
116
|
+
if (isWindowsConsole && (wasFullscreen || isFullscreen) &&
|
|
117
|
+
(viewportResized || isShrinkingAtViewport || isLeavingFullscreen)) {
|
|
110
118
|
return true;
|
|
111
119
|
}
|
|
112
120
|
return (
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{ "id": "t1", "label": "topic ko-en mixed", "args": { "query": "recall period last 개선", "limit": 8 }, "expect": "results" },
|
|
3
|
-
{ "id": "t2", "label": "topic ko only", "args": { "query": "recall 개선", "limit": 8 }, "expect": "results" },
|
|
4
|
-
{ "id": "t3", "label": "topic en only", "args": { "query": "recall period last improvement", "limit": 8 }, "expect": "results" },
|
|
5
|
-
{ "id": "t4", "label": "category no-raw", "args": { "period": "7d", "category": "decision", "limit": 10, "includeRaw": false }, "expect": "browse" }
|
|
6
|
-
]
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
-
import { join, dirname } from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { DATA_DIR } from "./config.mjs";
|
|
5
|
-
// Computed lazily (not at module top) so this module can be imported in a
|
|
6
|
-
// cycle with config.mjs without touching DATA_DIR before config.mjs has
|
|
7
|
-
// finished evaluating it (ESM temporal-dead-zone safety).
|
|
8
|
-
function cacheFile() {
|
|
9
|
-
return join(DATA_DIR, "holidays-cache.json");
|
|
10
|
-
}
|
|
11
|
-
const FALLBACK_FILE = join(homedir(), ".mixdog", "schedules", "holidays.json");
|
|
12
|
-
const CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
13
|
-
// In-memory, pre-warmed holiday cache for SYNC reads (isHolidaySync).
|
|
14
|
-
// Keyed by `${year}:${countryCode}` -> Set<dateStr>. Absent key == cold
|
|
15
|
-
// (not yet warmed); callers fall back to their documented cold-cache
|
|
16
|
-
// behavior until a warm completes.
|
|
17
|
-
const memCache = /* @__PURE__ */ new Map();
|
|
18
|
-
const warming = /* @__PURE__ */ new Set();
|
|
19
|
-
function cacheKey(year, countryCode) {
|
|
20
|
-
return `${year}:${countryCode}`;
|
|
21
|
-
}
|
|
22
|
-
function holidaySet(holidays) {
|
|
23
|
-
return new Set((holidays || []).map((h) => h.date));
|
|
24
|
-
}
|
|
25
|
-
async function fetchHolidays(year, countryCode) {
|
|
26
|
-
const url = `https://date.nager.at/api/v3/publicholidays/${year}/${countryCode}`;
|
|
27
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
|
|
28
|
-
if (!res.ok) throw new Error(`Nager API ${res.status}: ${res.statusText}`);
|
|
29
|
-
return res.json();
|
|
30
|
-
}
|
|
31
|
-
function loadCache(year, countryCode) {
|
|
32
|
-
try {
|
|
33
|
-
const file = cacheFile();
|
|
34
|
-
if (!existsSync(file)) return null;
|
|
35
|
-
const cache = JSON.parse(readFileSync(file, "utf8"));
|
|
36
|
-
if (cache.year !== year || cache.countryCode !== countryCode) return null;
|
|
37
|
-
if (Date.now() - cache.fetchedAt > CACHE_MAX_AGE_MS) return null;
|
|
38
|
-
return cache.holidays;
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
function saveCache(year, countryCode, holidays) {
|
|
44
|
-
const cache = { year, countryCode, fetchedAt: Date.now(), holidays };
|
|
45
|
-
try {
|
|
46
|
-
const file = cacheFile();
|
|
47
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
48
|
-
writeFileSync(file, JSON.stringify(cache, null, 2));
|
|
49
|
-
} catch {
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function loadFallback() {
|
|
53
|
-
try {
|
|
54
|
-
if (!existsSync(FALLBACK_FILE)) return /* @__PURE__ */ new Set();
|
|
55
|
-
const data = JSON.parse(readFileSync(FALLBACK_FILE, "utf8"));
|
|
56
|
-
const dates = data.holidays ?? [];
|
|
57
|
-
return new Set(dates);
|
|
58
|
-
} catch {
|
|
59
|
-
return /* @__PURE__ */ new Set();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async function isHoliday(date, countryCode) {
|
|
63
|
-
const year = date.getFullYear();
|
|
64
|
-
const dateStr = `${year}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
65
|
-
let holidays = loadCache(year, countryCode);
|
|
66
|
-
if (!holidays) {
|
|
67
|
-
try {
|
|
68
|
-
holidays = await fetchHolidays(year, countryCode);
|
|
69
|
-
saveCache(year, countryCode, holidays);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
process.stderr.write(`mixdog holidays: API fetch failed: ${err}
|
|
72
|
-
`);
|
|
73
|
-
holidays = null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (holidays) {
|
|
77
|
-
return holidays.some((h) => h.date === dateStr);
|
|
78
|
-
}
|
|
79
|
-
const fallback = loadFallback();
|
|
80
|
-
return fallback.has(dateStr);
|
|
81
|
-
}
|
|
82
|
-
// Synchronous, non-blocking holiday lookup backed by the pre-warmed
|
|
83
|
-
// in-memory cache. `dateStr` is a plain 'YYYY-MM-DD' string (the same
|
|
84
|
-
// representation warmHolidays stores), so there is no Date round-trip to
|
|
85
|
-
// shift the day across host offsets. Returns:
|
|
86
|
-
// true — date is a recognized public holiday (cache warm),
|
|
87
|
-
// false — date is NOT a holiday (cache warm),
|
|
88
|
-
// null — cache is cold (not warmed yet); the async warm is kicked off
|
|
89
|
-
// and the caller should apply its documented cold-cache
|
|
90
|
-
// fallback. Never performs network OR blocking disk I/O — the
|
|
91
|
-
// cold path consults the in-memory cache only.
|
|
92
|
-
function isHolidaySync(dateStr, countryCode) {
|
|
93
|
-
if (!countryCode) return false;
|
|
94
|
-
const year = Number(dateStr.slice(0, 4));
|
|
95
|
-
const key = cacheKey(year, countryCode);
|
|
96
|
-
const set = memCache.get(key);
|
|
97
|
-
if (set === void 0) {
|
|
98
|
-
// Cold: no synchronous disk probe here. Kick off the async warm
|
|
99
|
-
// (which does any on-disk/network lookup off the sync path) and
|
|
100
|
-
// report cold so the caller applies its documented fallback.
|
|
101
|
-
warmHolidays(dateStr, countryCode);
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
return set.has(dateStr);
|
|
105
|
-
}
|
|
106
|
-
// Asynchronously populate the in-memory sync cache for the given
|
|
107
|
-
// dateStr's year/country (from the on-disk cache when fresh, else Nager).
|
|
108
|
-
// `dateStr` is a plain 'YYYY-MM-DD' string. Idempotent and
|
|
109
|
-
// self-deduplicating; failures leave the entry cold so the next sync
|
|
110
|
-
// read retries.
|
|
111
|
-
async function warmHolidays(dateStr, countryCode) {
|
|
112
|
-
if (!countryCode) return;
|
|
113
|
-
const year = Number(dateStr.slice(0, 4));
|
|
114
|
-
const key = cacheKey(year, countryCode);
|
|
115
|
-
if (memCache.has(key) || warming.has(key)) return;
|
|
116
|
-
warming.add(key);
|
|
117
|
-
try {
|
|
118
|
-
// Yield once before touching disk so the cold sync caller that
|
|
119
|
-
// triggered this warm is never blocked by the loadCache file read.
|
|
120
|
-
await Promise.resolve();
|
|
121
|
-
let holidays = loadCache(year, countryCode);
|
|
122
|
-
if (!holidays) {
|
|
123
|
-
holidays = await fetchHolidays(year, countryCode);
|
|
124
|
-
saveCache(year, countryCode, holidays);
|
|
125
|
-
}
|
|
126
|
-
memCache.set(key, holidaySet(holidays));
|
|
127
|
-
} catch (err) {
|
|
128
|
-
process.stderr.write(`mixdog holidays: warm failed: ${err}
|
|
129
|
-
`);
|
|
130
|
-
} finally {
|
|
131
|
-
warming.delete(key);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
export {
|
|
135
|
-
isHoliday,
|
|
136
|
-
isHolidaySync,
|
|
137
|
-
warmHolidays
|
|
138
|
-
};
|