mixdog 0.7.12 → 0.7.14
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +28 -74
- package/bun.lock +128 -3
- package/defaults/hidden-roles.json +3 -0
- package/defaults/user-workflow.json +3 -3
- package/defaults/user-workflow.md +16 -11
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +9 -2
- package/scripts/ensure-deps.mjs +2 -2
- package/scripts/run-mcp.mjs +65 -9
- package/setup/launch-core.mjs +0 -1
- package/setup/setup-server.mjs +80 -33
- package/setup/setup.html +1 -3
- package/skills/setup/SKILL.md +12 -2
- package/src/agent/index.mjs +1 -1
- package/src/agent/orchestrator/config.mjs +58 -6
- package/src/agent/orchestrator/providers/model-catalog.mjs +1 -1
- package/src/agent/orchestrator/session/loop.mjs +3 -3
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +6 -2
- package/src/agent/orchestrator/tools/bash-session.mjs +1 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +29 -6
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +8 -4
- package/src/agent/orchestrator/tools/builtin.mjs +5 -2
- package/src/agent/orchestrator/tools/cwd-tool.mjs +17 -17
- package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/agent/orchestrator/tools/patch-manifest.json +7 -7
- package/src/agent/tool-defs.mjs +1 -1
- package/src/channels/index.mjs +12 -1
- package/src/channels/lib/webhook.mjs +35 -18
- package/src/memory/index.mjs +5 -1
- package/src/memory/lib/core-memory-store.mjs +1 -1
- package/src/memory/lib/memory-cycle1.mjs +1 -1
- package/src/memory/lib/memory-cycle2.mjs +1 -1
- package/src/memory/lib/memory-cycle3.mjs +1 -1
- package/tools.json +2 -2
package/scripts/ensure-deps.mjs
CHANGED
|
@@ -8,10 +8,10 @@ import * as os from 'os';
|
|
|
8
8
|
import { dirname, join } from 'path';
|
|
9
9
|
import { assertSafeOwnedDir } from '../src/shared/user-data-guard.mjs';
|
|
10
10
|
|
|
11
|
-
const RENAME_RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY', 'EEXIST']);
|
|
11
|
+
export const RENAME_RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY', 'EEXIST']);
|
|
12
12
|
const RENAME_BACKOFFS_MS = Object.freeze([25, 50, 100, 200, 400, 800, 1200, 1600]);
|
|
13
13
|
|
|
14
|
-
function sleepSync(ms) {
|
|
14
|
+
export function sleepSync(ms) {
|
|
15
15
|
try {
|
|
16
16
|
const buf = new SharedArrayBuffer(4);
|
|
17
17
|
Atomics.wait(new Int32Array(buf), 0, 0, Math.max(1, Number(ms) || 1));
|
package/scripts/run-mcp.mjs
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
ensureRuntimeDeps,
|
|
27
27
|
hasRequiredDeps,
|
|
28
28
|
renameWithRetrySync,
|
|
29
|
+
RENAME_RETRY_CODES,
|
|
30
|
+
sleepSync,
|
|
29
31
|
} from './ensure-deps.mjs';
|
|
30
32
|
|
|
31
33
|
// Stable per-terminal session id for this proxy supervisor's lifetime. The
|
|
@@ -453,6 +455,12 @@ let childHasResponded = false;
|
|
|
453
455
|
let announceListChangedOnReady = false;
|
|
454
456
|
let cachedInitRequest = null; // { id, params } from client's first initialize
|
|
455
457
|
let cachedInitDone = false; // initialized notification observed from client
|
|
458
|
+
// One-shot latch: flips true the instant the client's initialize response is
|
|
459
|
+
// forwarded and never resets. It selects replayInitToChild's mode (real-id vs
|
|
460
|
+
// swallow) and tells handleChildGone whether the pending initialize may be
|
|
461
|
+
// flushed. Until it is true the client has NOT seen its initialize result, so
|
|
462
|
+
// that request must survive a child death and be re-driven, not errored.
|
|
463
|
+
let clientInitAnswered = false;
|
|
456
464
|
let internalIdSeq = -1; // negative ids reserved for supervisor-internal requests
|
|
457
465
|
const pendingFromClient = new Map(); // request id (from client) → { method }
|
|
458
466
|
const pendingInternal = new Set(); // internal ids (init replay) — drop responses
|
|
@@ -670,14 +678,33 @@ function flushPendingClientErrors(tag) {
|
|
|
670
678
|
|
|
671
679
|
function replayInitToChild() {
|
|
672
680
|
if (!cachedInitRequest) return;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
+
// Mode select by invariant — has the client's initialize been answered yet?
|
|
682
|
+
// • Already answered (steady-state respawn): the client is fully
|
|
683
|
+
// initialized and must NOT see a second result. Replay under an internal
|
|
684
|
+
// negative id and swallow the response.
|
|
685
|
+
// • Not yet (first-boot / handshake-time crash): replay under the client's
|
|
686
|
+
// OWN id. Its request is still pending (preserved across the child-gone
|
|
687
|
+
// flush in handleChildGone), so the new child's initialize response flows
|
|
688
|
+
// back through the normal forward path — the client sees one clean result
|
|
689
|
+
// instead of the -32603 that previously killed the connection and forced
|
|
690
|
+
// a manual /mcp reconnect.
|
|
691
|
+
if (clientInitAnswered) {
|
|
692
|
+
const internalId = internalIdSeq--;
|
|
693
|
+
pendingInternal.add(internalId);
|
|
694
|
+
writeToChild(JSON.stringify({
|
|
695
|
+
jsonrpc: '2.0',
|
|
696
|
+
id: internalId,
|
|
697
|
+
method: 'initialize',
|
|
698
|
+
params: cachedInitRequest.params,
|
|
699
|
+
}));
|
|
700
|
+
} else {
|
|
701
|
+
writeToChild(JSON.stringify({
|
|
702
|
+
jsonrpc: '2.0',
|
|
703
|
+
id: cachedInitRequest.id,
|
|
704
|
+
method: 'initialize',
|
|
705
|
+
params: cachedInitRequest.params,
|
|
706
|
+
}));
|
|
707
|
+
}
|
|
681
708
|
if (cachedInitDone) {
|
|
682
709
|
// Notification — no id, no response expected.
|
|
683
710
|
writeToChild(JSON.stringify({
|
|
@@ -816,7 +843,13 @@ function handleChildLine(line) {
|
|
|
816
843
|
for (const item of scanned) {
|
|
817
844
|
if (item && item.id !== undefined) {
|
|
818
845
|
if (pendingInternal.has(item.id)) { internalIds.add(item.id); pendingInternal.delete(item.id); _maybeResolveLivenessPong(item.id); }
|
|
819
|
-
else {
|
|
846
|
+
else {
|
|
847
|
+
pendingFromClient.delete(item.id);
|
|
848
|
+
// Same latch as the scalar path below — keep the invariant consistent
|
|
849
|
+
// even if the client's initialize ever returns inside a batch, so a
|
|
850
|
+
// later respawn swallows its replay instead of re-driving the real id.
|
|
851
|
+
if (cachedInitRequest && item.id === cachedInitRequest.id) clientInitAnswered = true;
|
|
852
|
+
}
|
|
820
853
|
}
|
|
821
854
|
}
|
|
822
855
|
if (internalIds.size) {
|
|
@@ -846,6 +879,10 @@ function handleChildLine(line) {
|
|
|
846
879
|
return;
|
|
847
880
|
}
|
|
848
881
|
pendingFromClient.delete(scanned.id);
|
|
882
|
+
// The client's initialize is satisfied the instant its response is
|
|
883
|
+
// forwarded. Latch it so the next respawn swallows its replay (steady
|
|
884
|
+
// state) and so handleChildGone is free to error this id on a later death.
|
|
885
|
+
if (cachedInitRequest && scanned.id === cachedInitRequest.id) clientInitAnswered = true;
|
|
849
886
|
}
|
|
850
887
|
writeToClient(line);
|
|
851
888
|
}
|
|
@@ -896,10 +933,29 @@ function handleChildGone(why) {
|
|
|
896
933
|
}
|
|
897
934
|
const _pendingClientAtGone = pendingFromClient.size;
|
|
898
935
|
const _pendingInternalAtGone = pendingInternal.size;
|
|
936
|
+
// First-boot recovery invariant: the client's initialize must receive exactly
|
|
937
|
+
// one success response, from whichever child completes the handshake. If it
|
|
938
|
+
// has not been answered yet (clientInitAnswered=false), erroring it here makes
|
|
939
|
+
// the client mark the MCP server failed — it never re-issues initialize on its
|
|
940
|
+
// own, and the replay (internal id) never reaches it. That is the "startup
|
|
941
|
+
// fails, /mcp fixes it" symptom. Keep that single id pending across the flush;
|
|
942
|
+
// replayInitToChild re-drives it under the client's own id against the fresh
|
|
943
|
+
// child so the success response flows straight back. shuttingDown never
|
|
944
|
+
// preserves (the supervisor is exiting; nothing will replay).
|
|
945
|
+
const _preserveInitId = (!shuttingDown && !clientInitAnswered && cachedInitRequest)
|
|
946
|
+
? cachedInitRequest.id
|
|
947
|
+
: undefined;
|
|
948
|
+
const _preservedInit = _preserveInitId !== undefined
|
|
949
|
+
? pendingFromClient.get(_preserveInitId)
|
|
950
|
+
: undefined;
|
|
899
951
|
for (const [id] of pendingFromClient) {
|
|
952
|
+
if (id === _preserveInitId) continue;
|
|
900
953
|
sendErrorToClient(id, -32603, `[run-mcp] mcp child ${why.tag}; retry`);
|
|
901
954
|
}
|
|
902
955
|
pendingFromClient.clear();
|
|
956
|
+
if (_preserveInitId !== undefined && _preservedInit !== undefined) {
|
|
957
|
+
pendingFromClient.set(_preserveInitId, _preservedInit);
|
|
958
|
+
}
|
|
903
959
|
pendingInternal.clear();
|
|
904
960
|
// Fresh child = fresh response path; discard any in-flight liveness probe.
|
|
905
961
|
_livenessPingId = null;
|
package/setup/launch-core.mjs
CHANGED
|
@@ -285,7 +285,6 @@ async function spawnServerWithLog(pluginRoot, pluginData, { openOnStart = true }
|
|
|
285
285
|
MIXDOG_SETUP_OPEN_ON_START: openOnStart ? '1' : '0',
|
|
286
286
|
MIXDOG_SETUP_PARENT_PID: String(findAncestorPid() || ''),
|
|
287
287
|
},
|
|
288
|
-
windowsHide: true,
|
|
289
288
|
});
|
|
290
289
|
} catch (error) {
|
|
291
290
|
closeLog(launchLog.fd);
|
package/setup/setup-server.mjs
CHANGED
|
@@ -1236,28 +1236,18 @@ async function openAppWindowSequence() {
|
|
|
1236
1236
|
// inherit so they do not allocate their own conhost instances.
|
|
1237
1237
|
const escVbs = s => String(s).replace(/"/g, '""');
|
|
1238
1238
|
const argsStr = args.join(' ');
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
//
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
// crash. A fresh `--app` launch then rendezvouses with that dead
|
|
1252
|
-
// instance over the singleton socket, forwards its URL, and exits
|
|
1253
|
-
// WITHOUT opening a window — the reported cold-open bug (URL
|
|
1254
|
-
// printed, no window, a later /open works once the lock is reaped).
|
|
1255
|
-
// Deleting the stale Singleton* files first guarantees chrome
|
|
1256
|
-
// boots a real window instead of IPC-forwarding to a ghost. This
|
|
1257
|
-
// is the invariant ("spawn into a clean singleton when no live
|
|
1258
|
-
// owner"), not a retry. The title-scoped taskkill is kept only as
|
|
1259
|
-
// a defensive belt for a same-title window with a non-matching
|
|
1260
|
-
// command line; it is a no-op in the common case.
|
|
1239
|
+
// Hybrid warm-focus / ghost-respawn. A chrome process can outlive its
|
|
1240
|
+
// window (closed / crashed / IPC-forwarded over the singleton socket),
|
|
1241
|
+
// so "a process exists" never proves "a window is visible" — the old
|
|
1242
|
+
// focus-if-found path then opened nothing (the "URL printed, no window"
|
|
1243
|
+
// cold-open bug), while killing+respawning on EVERY open cold-boots
|
|
1244
|
+
// chrome each time (slow). Balance: TryFocus the existing MAIN with a
|
|
1245
|
+
// short bound. If it activates → healthy window, just focus it (fast,
|
|
1246
|
+
// no respawn). If it can't (ghost) or none exists → KillMixdogChromes
|
|
1247
|
+
// for this profile (--user-data-dir match, MAIN + helpers), clear the
|
|
1248
|
+
// stale Singleton* locks, spawn one fresh --app window. The VBS finally
|
|
1249
|
+
// re-checks FindChromePid and exits 0 only if a MAIN is live (else 2 →
|
|
1250
|
+
// default-browser fallback in JS), so /open's ok reflects a real window.
|
|
1261
1251
|
//
|
|
1262
1252
|
// The taskkill → chrome chain runs under one hidden cmd.exe (one
|
|
1263
1253
|
// cmd.exe per /open). `&` runs both regardless of exit code (taskkill
|
|
@@ -1275,7 +1265,7 @@ async function openAppWindowSequence() {
|
|
|
1275
1265
|
const vbsLines = [
|
|
1276
1266
|
'Option Explicit',
|
|
1277
1267
|
'Const HIDDEN_WINDOW = 0',
|
|
1278
|
-
'Dim Wmi, Startup, Wsh, cmdLine, cmdPid, rc, appNeedle, profileNeedle, existingPid, profileDir, procName',
|
|
1268
|
+
'Dim Wmi, Startup, Wsh, cmdLine, cmdPid, rc, appNeedle, profileNeedle, existingPid, profileDir, procName, focused',
|
|
1279
1269
|
'Set Wmi = GetObject("winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\cimv2")',
|
|
1280
1270
|
'Set Wsh = CreateObject("WScript.Shell")',
|
|
1281
1271
|
`appNeedle = "${escVbs(appNeedle)}"`,
|
|
@@ -1283,15 +1273,22 @@ async function openAppWindowSequence() {
|
|
|
1283
1273
|
`profileDir = "${escVbs(chromeProfile)}"`,
|
|
1284
1274
|
`procName = "${escVbs(procName)}"`,
|
|
1285
1275
|
'existingPid = FindChromePid(Wmi, appNeedle, profileNeedle)',
|
|
1286
|
-
'
|
|
1276
|
+
'focused = False',
|
|
1277
|
+
'If existingPid <> 0 Then focused = TryFocus(Wsh, existingPid, 8)',
|
|
1278
|
+
'If Not focused Then',
|
|
1279
|
+
' Call KillMixdogChromes(Wmi, profileNeedle)',
|
|
1280
|
+
' WScript.Sleep 200',
|
|
1287
1281
|
' Call ClearSingletonLocks(profileDir)',
|
|
1288
1282
|
' Set Startup = Wmi.Get("Win32_ProcessStartup").SpawnInstance_',
|
|
1289
1283
|
' Startup.ShowWindow = HIDDEN_WINDOW',
|
|
1290
1284
|
` cmdLine = "cmd.exe ${escVbs(cmdArg)}"`,
|
|
1291
1285
|
' rc = Wmi.Get("Win32_Process").Create(cmdLine, Null, Startup, cmdPid)',
|
|
1292
1286
|
' If rc <> 0 Then WScript.Quit rc',
|
|
1287
|
+
' Call FocusMixdogWindow(Wmi, Wsh, appNeedle, profileNeedle)',
|
|
1293
1288
|
'End If',
|
|
1294
|
-
'
|
|
1289
|
+
'existingPid = FindChromePid(Wmi, appNeedle, profileNeedle)',
|
|
1290
|
+
'If existingPid = 0 Then WScript.Quit 2',
|
|
1291
|
+
'WScript.Quit 0',
|
|
1295
1292
|
'',
|
|
1296
1293
|
'Sub ClearSingletonLocks(profileDir)',
|
|
1297
1294
|
' Dim Fso, names, i, p',
|
|
@@ -1305,6 +1302,32 @@ async function openAppWindowSequence() {
|
|
|
1305
1302
|
' On Error GoTo 0',
|
|
1306
1303
|
'End Sub',
|
|
1307
1304
|
'',
|
|
1305
|
+
'Sub KillMixdogChromes(Wmi, profileNeedle)',
|
|
1306
|
+
' Dim proc, commandLine',
|
|
1307
|
+
' On Error Resume Next',
|
|
1308
|
+
' For Each proc In Wmi.ExecQuery("SELECT ProcessId,CommandLine FROM Win32_Process WHERE Name = \'" & procName & "\'")',
|
|
1309
|
+
' commandLine = ""',
|
|
1310
|
+
' If Not IsNull(proc.CommandLine) Then commandLine = CStr(proc.CommandLine)',
|
|
1311
|
+
' If InStr(1, commandLine, profileNeedle, vbTextCompare) > 0 Then proc.Terminate',
|
|
1312
|
+
' Next',
|
|
1313
|
+
' On Error GoTo 0',
|
|
1314
|
+
'End Sub',
|
|
1315
|
+
'',
|
|
1316
|
+
'Function TryFocus(Wsh, pid, maxTicks)',
|
|
1317
|
+
' Dim i, ok',
|
|
1318
|
+
' ok = False',
|
|
1319
|
+
' On Error Resume Next',
|
|
1320
|
+
' For i = 1 To maxTicks',
|
|
1321
|
+
' Wsh.SendKeys "%"',
|
|
1322
|
+
' WScript.Sleep 25',
|
|
1323
|
+
' ok = Wsh.AppActivate(CLng(pid))',
|
|
1324
|
+
' If ok Then Exit For',
|
|
1325
|
+
' WScript.Sleep 120',
|
|
1326
|
+
' Next',
|
|
1327
|
+
' On Error GoTo 0',
|
|
1328
|
+
' TryFocus = ok',
|
|
1329
|
+
'End Function',
|
|
1330
|
+
'',
|
|
1308
1331
|
'Sub FocusMixdogWindow(Wmi, Wsh, appNeedle, profileNeedle)',
|
|
1309
1332
|
' Dim i, pid, activated',
|
|
1310
1333
|
' On Error Resume Next',
|
|
@@ -1370,29 +1393,37 @@ async function openAppWindowSequence() {
|
|
|
1370
1393
|
// parented to wscript, so a tree-kill of wscript reaps only the focus
|
|
1371
1394
|
// loop.
|
|
1372
1395
|
const WSCRIPT_OPEN_DEADLINE_MS = 12000;
|
|
1373
|
-
const
|
|
1396
|
+
const outcome = await new Promise(resolve => {
|
|
1374
1397
|
const wscriptChild = spawn('wscript.exe', ['//B', '//NoLogo', vbsPath], {
|
|
1375
1398
|
stdio: 'ignore', windowsHide: true,
|
|
1376
1399
|
});
|
|
1377
1400
|
let settled = false;
|
|
1378
|
-
const finish =
|
|
1401
|
+
const finish = v => { if (settled) return; settled = true; clearTimeout(timer); resolve(v); };
|
|
1379
1402
|
const timer = setTimeout(() => {
|
|
1380
1403
|
// Tree-kill the wscript launcher only; the detached browser lives on.
|
|
1381
1404
|
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(wscriptChild.pid)], { windowsHide: true, stdio: 'ignore', timeout: 4000 }); } catch {}
|
|
1382
1405
|
try { wscriptChild.kill(); } catch {}
|
|
1383
|
-
finish(true);
|
|
1406
|
+
finish({ timedOut: true });
|
|
1384
1407
|
}, WSCRIPT_OPEN_DEADLINE_MS);
|
|
1385
1408
|
if (typeof timer.unref === 'function') timer.unref();
|
|
1386
|
-
wscriptChild.once('error', () => finish(false));
|
|
1387
|
-
wscriptChild.once('exit',
|
|
1409
|
+
wscriptChild.once('error', () => finish({ timedOut: false, code: -1 }));
|
|
1410
|
+
wscriptChild.once('exit', code => finish({ timedOut: false, code }));
|
|
1388
1411
|
});
|
|
1389
|
-
if (timedOut) {
|
|
1412
|
+
if (outcome.timedOut) {
|
|
1390
1413
|
const err = `wscript launcher did not exit within ${WSCRIPT_OPEN_DEADLINE_MS}ms; killed launcher (browser left running)`;
|
|
1391
1414
|
attempts.push({ method: 'browser app mode (wscript)', ok: false, error: err });
|
|
1392
1415
|
logOpenFailure('browser app mode (wscript)', err);
|
|
1393
|
-
} else {
|
|
1416
|
+
} else if (outcome.code === 0) {
|
|
1417
|
+
// VBS exits 0 only after it re-checks FindChromePid post-spawn and a
|
|
1418
|
+
// MAIN (non-helper) chrome for this profile is live — an honest
|
|
1419
|
+
// "window materialized" signal, not just "the launcher exited". Any
|
|
1420
|
+
// other code (esp. 2 = no window) falls through to the default browser.
|
|
1394
1421
|
chromeSpawnOk = true;
|
|
1395
1422
|
attempts.push({ method: 'browser app mode (wscript)', ok: true });
|
|
1423
|
+
} else {
|
|
1424
|
+
const err = `wscript exited ${outcome.code}; config window did not materialize`;
|
|
1425
|
+
attempts.push({ method: 'browser app mode (wscript)', ok: false, error: err });
|
|
1426
|
+
logOpenFailure('browser app mode (wscript)', err);
|
|
1396
1427
|
}
|
|
1397
1428
|
} catch (error) {
|
|
1398
1429
|
attempts.push({ method: 'browser app mode (wscript)', ok: false, error: formatOpenError(error) });
|
|
@@ -2218,12 +2249,19 @@ async function handleRequest(req, res) {
|
|
|
2218
2249
|
const rawMaint = cfg.maintenance || {};
|
|
2219
2250
|
// Strip legacy keys that no longer belong in maintenance
|
|
2220
2251
|
// (classification/recap were retired with the cycle1 split;
|
|
2221
|
-
// scheduler/webhook keep their model per-entry
|
|
2252
|
+
// scheduler/webhook keep their model per-entry; the three memory-cycle
|
|
2253
|
+
// MODEL presets collapsed into a single `memory` key — fold the first
|
|
2254
|
+
// present legacy cycle value into `memory` before stripping).
|
|
2222
2255
|
// Persist back when the stored config carried any of them so the Setup
|
|
2223
2256
|
// panel and the runtime resolver stop having to dual-match name vs id.
|
|
2224
2257
|
const allowedKeys = new Set([...Object.keys(DEFAULT_MAINTENANCE), ...MAINTENANCE_SLOTS]);
|
|
2225
2258
|
const cleanMaint = {};
|
|
2226
2259
|
let changed = false;
|
|
2260
|
+
const legacyCycleKeys = ['cycle1', 'cycle2', 'cycle3'];
|
|
2261
|
+
if (!('memory' in rawMaint) && legacyCycleKeys.some(k => k in rawMaint)) {
|
|
2262
|
+
cleanMaint.memory = rawMaint.cycle1 ?? rawMaint.cycle2 ?? rawMaint.cycle3 ?? DEFAULT_MAINTENANCE.memory;
|
|
2263
|
+
changed = true;
|
|
2264
|
+
}
|
|
2227
2265
|
for (const [k, v] of Object.entries(rawMaint)) {
|
|
2228
2266
|
if (allowedKeys.has(k)) cleanMaint[k] = v;
|
|
2229
2267
|
else changed = true;
|
|
@@ -2267,6 +2305,15 @@ async function handleRequest(req, res) {
|
|
|
2267
2305
|
return;
|
|
2268
2306
|
}
|
|
2269
2307
|
const nextMaint = { ...(cfg.maintenance || {}) };
|
|
2308
|
+
// Migrate any stored legacy cycle1/2/3 model keys into `memory` (one-time
|
|
2309
|
+
// schema collapse) so the persisted config never carries them forward.
|
|
2310
|
+
{
|
|
2311
|
+
const legacyCycleKeys = ['cycle1', 'cycle2', 'cycle3'];
|
|
2312
|
+
if (!('memory' in nextMaint) && legacyCycleKeys.some(k => k in nextMaint)) {
|
|
2313
|
+
nextMaint.memory = nextMaint.cycle1 ?? nextMaint.cycle2 ?? nextMaint.cycle3 ?? DEFAULT_MAINTENANCE.memory;
|
|
2314
|
+
}
|
|
2315
|
+
for (const k of legacyCycleKeys) delete nextMaint[k];
|
|
2316
|
+
}
|
|
2270
2317
|
for (const [k, v] of Object.entries(data)) {
|
|
2271
2318
|
if (v == null || v === '') delete nextMaint[k]; // inherit → remove override
|
|
2272
2319
|
else nextMaint[k] = v;
|
package/setup/setup.html
CHANGED
|
@@ -3642,9 +3642,7 @@ async function srSavePanel() {
|
|
|
3642
3642
|
// -- Agent Maintenance --
|
|
3643
3643
|
const AG_MAINT_TASKS = [
|
|
3644
3644
|
{ id: 'explore', label: 'Explore', desc: 'Filesystem exploration agent (explore tool)' },
|
|
3645
|
-
{ id: '
|
|
3646
|
-
{ id: 'cycle2', label: 'Memory Cycle 2', desc: 'Root re-scorer (core memory promotion)' },
|
|
3647
|
-
{ id: 'cycle3', label: 'Memory Cycle 3', desc: 'Core memory reviewer' },
|
|
3645
|
+
{ id: 'memory', label: 'Memory Cycles', desc: 'Chunker / re-scorer / core reviewer (cycles 1-3)' },
|
|
3648
3646
|
];
|
|
3649
3647
|
let agMaintenance = {};
|
|
3650
3648
|
let agMaintenanceDefaults = {};
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup
|
|
3
|
-
description: Invoke for ANY mixdog setup or config task — onboarding a fresh install AND editing existing config. Covers agent providers/models, bridge role→preset mapping (changing worker/reviewer/tester/debugger models), agent.presets, channels/Discord, memory, search, webhook/ngrok, quiet hours, DM access, address form (user title), launch flag, and secrets. Trigger on intents like "mixdog config", "edit/change settings", "set up", "세팅"/"설정 수정", "호칭 변경" (change how the assistant addresses the user), "조용시간"/quiet hours, DM access, "change worker (or any role) model", "add a preset", "switch provider", or first-time "what goes where". Also handles uninstall/restore intents ("remove mixdog", "restore pre-install state", "원상복구") — route those to `node scripts/uninstall.mjs` and UNINSTALL.md.
|
|
3
|
+
description: Invoke for ANY mixdog setup or config task — onboarding a fresh install AND editing existing config. Covers agent providers/models, bridge role→preset mapping (changing worker/reviewer/tester/debugger models), agent.presets, the cross-verification workflow text & role roster (add/remove a role, tune the debugger/reviewer loop prose), channels/Discord, memory, search, webhook/ngrok, quiet hours, DM access, address form (user title), launch flag, and secrets. Trigger on intents like "mixdog config", "edit/change settings", "set up", "세팅"/"설정 수정", "호칭 변경" (change how the assistant addresses the user), "조용시간"/quiet hours, DM access, "change worker (or any role) model", "add a preset", "switch provider", "remove the tester role", "tune the debugger loop", "edit the workflow prose", or first-time "what goes where". Also handles uninstall/restore intents ("remove mixdog", "restore pre-install state", "원상복구") — route those to `node scripts/uninstall.mjs` and UNINSTALL.md.
|
|
4
4
|
version: 0.1.0
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -14,6 +14,7 @@ Guided onboarding for a fresh mixdog install **and** a reference for editing exi
|
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| Any setting value (channels, memory, agent presets, search, capabilities, cwd) | `${DATA}/mixdog-config.json` | `defaults/*.template.json`, anything under `<plugin>/cache/` |
|
|
16
16
|
| Which model a role uses | `${DATA}/user-workflow.json` (`roles[].preset`) | `defaults/user-workflow.json` |
|
|
17
|
+
| Add/remove a role (e.g. drop `tester`), or tune the workflow loop prose (cross-verification / debugger fan-out) | `${DATA}/user-workflow.json` (`roles[]`) **and** `${DATA}/user-workflow.md` (loop text) | the `defaults/` + `cache/` copies — those are the shipped template, edited only to change fresh-install defaults |
|
|
17
18
|
| A secret (bot token, API key, authtoken) | OS keychain / `MIXDOG_*` env | any JSON file |
|
|
18
19
|
| Skill text / prompts / plugin code | the marketplace **source** (`<plugin>/marketplaces/trib-plugin/`; dev installs: the source repo, then sync) | the `cache/` copy — it is overwritten on update/sync |
|
|
19
20
|
|
|
@@ -51,10 +52,19 @@ Guided onboarding for a fresh mixdog install **and** a reference for editing exi
|
|
|
51
52
|
## Config structure & editing
|
|
52
53
|
|
|
53
54
|
- Config lives under the plugin data dir `~/.claude/plugins/data/mixdog-trib-plugin/` (the `CLAUDE_PLUGIN_DATA` env var overrides it). Two files matter:
|
|
54
|
-
- **user-workflow.json** — `roles[] {name, preset, permission}` maps each bridge role to a preset (the active role set is whatever this file defines — e.g. worker / reviewer / debugger
|
|
55
|
+
- **user-workflow.json** — `roles[] {name, preset, permission}` maps each bridge role to a preset (the active role set is whatever this file defines — e.g. worker / reviewer / debugger). This sets which model each role uses. To change a role's model, set its `preset` to one of the preset names below (takes effect LIVE — bridge dispatch re-reads this file on every spawn; only the Lead's injected `# Roles` list waits for the next SessionStart).
|
|
55
56
|
- **mixdog-config.json → `agent.presets[] {id, name, type, provider, model, tools, effort?, fast?, xaiCacheMaxInFlight?}`** defines each preset, e.g. opus-high → claude-opus-4-8 / high, composer-2.5 → grok-composer-2.5-fast, gpt-5.5-xhigh → gpt-5.5 / xhigh. `type` is `"bridge"` and `tools` is `"full"` for normal worker presets; `xaiCacheMaxInFlight` is a grok-oauth-only tuning knob. `agent.default` is the fallback preset for any role without its own mapping. To offer a new model, add a preset here first, then point a role at it in user-workflow.json.
|
|
56
57
|
- Edit via the Setup UI (Custom Workflow for role→preset, Agent presets for presets) or edit the JSON files directly (reloads next session). These are user data — no rebuild needed.
|
|
57
58
|
|
|
59
|
+
### Workflow files — quick-edit map (NO exploration needed)
|
|
60
|
+
|
|
61
|
+
Two sibling files under `${DATA}` (`~/.claude/plugins/data/mixdog-trib-plugin/`) define the active bridge workflow — edit them directly, do not go hunting:
|
|
62
|
+
- **`${DATA}/user-workflow.json`** — `roles[] {name, preset, permission}`. The active role set IS exactly this list. Add or remove a role here (e.g. drop `tester`); removal takes effect on the next bridge spawn (live re-read). Keep each remaining role's `preset` untouched.
|
|
63
|
+
- **`${DATA}/user-workflow.md`** — the role-assignment table + cross-verification loop prose, injected as the Lead's `# User Workflow` rule. Tune the debugger fan-out / reviewer pairing text here. The Lead's injected copy refreshes on the next SessionStart.
|
|
64
|
+
- To change the **shipped fresh-install defaults** (not just this user), edit the repo's `defaults/user-workflow.{json,md}` to match. `cache/` copies are never the edit target.
|
|
65
|
+
|
|
66
|
+
After editing `${DATA}` files, call `reload_config`; after editing repo `defaults/`, run dev-sync to propagate.
|
|
67
|
+
|
|
58
68
|
## Agent providers — supported list & onboarding
|
|
59
69
|
|
|
60
70
|
A preset's `provider` field selects the backend. Authoritative allow-list:
|
package/src/agent/index.mjs
CHANGED
|
@@ -190,7 +190,7 @@ function scheduleAgentConfigReload(reason = 'change') {
|
|
|
190
190
|
_agentConfigReloadTimer.unref?.();
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
async function reloadAgentConfig(reason = 'change') {
|
|
193
|
+
export async function reloadAgentConfig(reason = 'change') {
|
|
194
194
|
if (_agentConfigReloadRunning) {
|
|
195
195
|
_agentConfigReloadQueued = true;
|
|
196
196
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolvePluginData } from '../../shared/plugin-paths.mjs';
|
|
2
2
|
import { readSection, updateSection, getAgentApiKey } from '../../shared/config.mjs';
|
|
3
|
+
import { OPENAI_COMPAT_PRESETS } from './providers/openai-compat.mjs';
|
|
3
4
|
import { hasAnthropicOAuthCredentials } from './providers/anthropic-oauth.mjs';
|
|
4
5
|
import { hasOpenAIOAuthCredentials } from './providers/openai-oauth.mjs';
|
|
5
6
|
import { hasGrokOAuthCredentials } from './providers/grok-oauth.mjs';
|
|
@@ -23,15 +24,17 @@ const ENV_KEY_MAP = {
|
|
|
23
24
|
// resolvePresetName() (bridge-llm) always resolves a model directly from
|
|
24
25
|
// `maint[slot]` — no shared `defaultPreset` fallback is needed or used.
|
|
25
26
|
// Memory cycles + Lead helper fan-out (explore/cycle1/cycle2/cycle3) and
|
|
26
|
-
// entry-driven dispatch (scheduler/webhook) all default to `haiku`.
|
|
27
|
+
// entry-driven dispatch (scheduler/webhook) all default to `haiku`. The three
|
|
28
|
+
// memory cycles (chunker / re-scorer / core reviewer) share ONE `memory`
|
|
29
|
+
// preset knob — the cycle agents stay separate (cycle1/2/3-agent, distinct
|
|
30
|
+
// slots and invokedBy) but resolve their model from `maint.memory` via the
|
|
31
|
+
// `maintKey` override on their hidden-role entries.
|
|
27
32
|
// scheduler/webhook still let a per-entry config.json model win first (the
|
|
28
33
|
// caller passes it explicitly via opts.preset); the haiku default below only
|
|
29
34
|
// applies when an entry omits its own model.
|
|
30
35
|
export const DEFAULT_MAINTENANCE = Object.freeze({
|
|
31
36
|
explore: 'haiku',
|
|
32
|
-
|
|
33
|
-
cycle2: 'haiku',
|
|
34
|
-
cycle3: 'haiku',
|
|
37
|
+
memory: 'haiku',
|
|
35
38
|
scheduler: 'haiku',
|
|
36
39
|
webhook: 'haiku',
|
|
37
40
|
});
|
|
@@ -41,7 +44,7 @@ export const DEFAULT_MAINTENANCE = Object.freeze({
|
|
|
41
44
|
// SUBSET of DEFAULT_MAINTENANCE: scheduler/webhook carry a per-entry model and
|
|
42
45
|
// are not shown as shared rows, but still inherit the haiku default above when
|
|
43
46
|
// an entry omits its own model.
|
|
44
|
-
export const MAINTENANCE_SLOTS = Object.freeze(['explore', '
|
|
47
|
+
export const MAINTENANCE_SLOTS = Object.freeze(['explore', 'memory']);
|
|
45
48
|
|
|
46
49
|
// Map short Anthropic family labels to the full model ids used by the API.
|
|
47
50
|
// Honors ANTHROPIC_DEFAULT_{OPUS|SONNET|HAIKU}_MODEL env overrides.
|
|
@@ -135,7 +138,11 @@ export function loadConfig() {
|
|
|
135
138
|
// Provider API keys live in the OS keychain (std env / MIXDOG_AGENT_*
|
|
136
139
|
// -> keychain), never plaintext in config. Overlay them so the
|
|
137
140
|
// provider clients see config.apiKey populated.
|
|
138
|
-
|
|
141
|
+
// ENV_KEY_MAP covers first-class key providers; OPENAI_COMPAT_PRESETS
|
|
142
|
+
// covers compat providers (opencode-go, …) whose key also lives in
|
|
143
|
+
// the keychain. Without the union, a compat provider with a valid
|
|
144
|
+
// stored key still ships 'no-key' → 401.
|
|
145
|
+
for (const name of new Set([...Object.keys(ENV_KEY_MAP), ...Object.keys(OPENAI_COMPAT_PRESETS)])) {
|
|
139
146
|
const kc = getAgentApiKey(name);
|
|
140
147
|
if (kc) mergedProviders[name] = { ...(mergedProviders[name] || {}), apiKey: kc, enabled: true };
|
|
141
148
|
}
|
|
@@ -148,6 +155,19 @@ export function loadConfig() {
|
|
|
148
155
|
for (const [k, v] of Object.entries(raw.maintenance || {})) {
|
|
149
156
|
if (allowedMaintKeys.has(k)) rawMaint[k] = v;
|
|
150
157
|
}
|
|
158
|
+
// One-time schema migration: the three memory-cycle MODEL presets
|
|
159
|
+
// (cycle1/cycle2/cycle3) collapsed into a single `memory` key. If the
|
|
160
|
+
// stored config still carries any old cycle key and no `memory`, fold
|
|
161
|
+
// the first present value into `memory` (preserving the user's
|
|
162
|
+
// choice), then the old keys drop via the allow-list above. This is a
|
|
163
|
+
// schema migration, NOT a runtime fallback — the persisted config is
|
|
164
|
+
// cleaned once so runtime never has to re-migrate.
|
|
165
|
+
const legacyCycleKeys = ['cycle1', 'cycle2', 'cycle3'];
|
|
166
|
+
let migratedMaintenance = false;
|
|
167
|
+
if (!('memory' in rawMaint) && legacyCycleKeys.some(k => k in (raw.maintenance || {}))) {
|
|
168
|
+
rawMaint.memory = raw.maintenance.cycle1 ?? raw.maintenance.cycle2 ?? raw.maintenance.cycle3 ?? DEFAULT_MAINTENANCE.memory;
|
|
169
|
+
migratedMaintenance = true;
|
|
170
|
+
}
|
|
151
171
|
// Self-ref guard: mcpServers.mixdog / mcpServers["trib-plugin"]
|
|
152
172
|
// would self-spawn through the in-process tool bridge. Strip on
|
|
153
173
|
// ingress so user-edited configs cannot brick the agent boot.
|
|
@@ -180,6 +200,38 @@ export function loadConfig() {
|
|
|
180
200
|
process.stderr.write(`[config] persist sanitized config failed: ${err?.message}\n`);
|
|
181
201
|
}
|
|
182
202
|
}
|
|
203
|
+
// Persist the memory-cycle schema migration once. rawMaint already
|
|
204
|
+
// carries the folded `memory` key and excludes the dropped cycle1/2/3
|
|
205
|
+
// keys (not in the allow-list); rebase onto the in-lock current so a
|
|
206
|
+
// concurrent writer's unrelated edits survive, mirroring the
|
|
207
|
+
// mcpServers self-ref strip above.
|
|
208
|
+
if (migratedMaintenance) {
|
|
209
|
+
try {
|
|
210
|
+
persistAgentConfig((current) => {
|
|
211
|
+
const cur = { ...current };
|
|
212
|
+
const target = (cur.agent && cur.agent.providers)
|
|
213
|
+
? (cur.agent = { ...cur.agent })
|
|
214
|
+
: cur;
|
|
215
|
+
const curMaint = (target.maintenance && typeof target.maintenance === 'object') ? { ...target.maintenance } : {};
|
|
216
|
+
// Derive `memory` from the IN-LOCK current, not the
|
|
217
|
+
// pre-lock rawMaint snapshot — a concurrent writer may
|
|
218
|
+
// have set maintenance.memory or changed a legacy cycle
|
|
219
|
+
// value between this loadConfig()'s read and the lock.
|
|
220
|
+
// If `memory` is already present in-lock, preserve it
|
|
221
|
+
// (lost-update guard); otherwise fold the in-lock legacy
|
|
222
|
+
// cycle value first, with the pre-lock snapshot as the
|
|
223
|
+
// last-resort seed.
|
|
224
|
+
if (!('memory' in curMaint)) {
|
|
225
|
+
curMaint.memory = curMaint.cycle1 ?? curMaint.cycle2 ?? curMaint.cycle3 ?? rawMaint.memory;
|
|
226
|
+
}
|
|
227
|
+
for (const k of legacyCycleKeys) delete curMaint[k];
|
|
228
|
+
target.maintenance = curMaint;
|
|
229
|
+
return cur;
|
|
230
|
+
});
|
|
231
|
+
} catch (err) {
|
|
232
|
+
process.stderr.write(`[config] persist maintenance migration failed: ${err?.message}\n`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
183
235
|
const rawPresets = Array.isArray(raw.presets) ? raw.presets : [];
|
|
184
236
|
const normalizedPresets = rawPresets.map(p => normalizePreset(p)).filter(Boolean);
|
|
185
237
|
return {
|
|
@@ -32,7 +32,7 @@ const MODELSDEV_URL = 'https://models.dev/api.json';
|
|
|
32
32
|
const MODELSDEV_CACHE_FILE = 'modelsdev-catalog.json';
|
|
33
33
|
|
|
34
34
|
// mixdog provider id → models.dev provider id. Identity for ids that already
|
|
35
|
-
// match (opencode-go / deepseek / xai /
|
|
35
|
+
// match (opencode-go / deepseek / xai / openai / anthropic / groq /
|
|
36
36
|
// mistral); only the OAuth aliases and gemini→google need remapping.
|
|
37
37
|
const _MODELSDEV_PROVIDER_ALIAS = {
|
|
38
38
|
'anthropic-oauth': 'anthropic',
|
|
@@ -628,7 +628,7 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
628
628
|
if (isBuiltinTool(name)) {
|
|
629
629
|
// clientHostPid threaded for the same per-terminal job-scope reason as
|
|
630
630
|
// the bash branch above (see resolveJobOwnerHostPid).
|
|
631
|
-
return executeBuiltinTool(name, args, cwd, { sessionId: callerSessionId, clientHostPid: sessionRef?.clientHostPid, ...toolOpts });
|
|
631
|
+
return executeBuiltinTool(name, args, cwd, { sessionId: callerSessionId, clientHostPid: sessionRef?.clientHostPid, signal: executeOpts.signal, ...toolOpts });
|
|
632
632
|
}
|
|
633
633
|
return formatUnknownBuiltinToolMessage(name, args, 'tool');
|
|
634
634
|
}
|
|
@@ -807,7 +807,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
807
807
|
try {
|
|
808
808
|
const permBlocked = _checkWorkerPermission(call.name, call.arguments, sessionRef);
|
|
809
809
|
if (permBlocked !== null) return { ok: true, value: permBlocked };
|
|
810
|
-
return { ok: true, value: await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id }) };
|
|
810
|
+
return { ok: true, value: await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal }) };
|
|
811
811
|
} catch (error) {
|
|
812
812
|
return { ok: false, error };
|
|
813
813
|
}
|
|
@@ -1185,7 +1185,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1185
1185
|
toolEndedAt = Date.now();
|
|
1186
1186
|
_resultKind = 'error';
|
|
1187
1187
|
} else {
|
|
1188
|
-
result = await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id });
|
|
1188
|
+
result = await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal });
|
|
1189
1189
|
toolEndedAt = Date.now();
|
|
1190
1190
|
// Boundary: tool-return string convention → structural kind.
|
|
1191
1191
|
// The only prefix check in this codebase; downstream layers
|
|
@@ -117,13 +117,17 @@ export function resolvePresetName({ preset, optsPreset, role, config: cfgIn = nu
|
|
|
117
117
|
// Hidden roles resolve their maintenance preset by SLOT. Every slot carries
|
|
118
118
|
// a concrete default in DEFAULT_MAINTENANCE, so `maint[slot]` resolves
|
|
119
119
|
// directly; the Setup panel can still tune each slot independently.
|
|
120
|
-
// (explorer.slot = 'explore', cycle1-agent.slot = 'cycle1', …)
|
|
120
|
+
// (explorer.slot = 'explore', cycle1-agent.slot = 'cycle1', …). A hidden
|
|
121
|
+
// role may override which maintenance key it reads via `maintKey`
|
|
122
|
+
// (e.g. the cycle1/2/3 agents all read `maint.memory` instead of their
|
|
123
|
+
// own slot) so several agents can share one model knob while keeping
|
|
124
|
+
// distinct slots/identity.
|
|
121
125
|
const hidden = getHiddenRole(role);
|
|
122
126
|
if (hidden) {
|
|
123
127
|
try {
|
|
124
128
|
const config = cfgIn || loadConfig();
|
|
125
129
|
const maint = config?.maintenance || {};
|
|
126
|
-
return maint[hidden.slot] || null;
|
|
130
|
+
return maint[hidden.maintKey || hidden.slot] || null;
|
|
127
131
|
} catch { return null; }
|
|
128
132
|
}
|
|
129
133
|
try {
|
|
@@ -47,6 +47,7 @@ import { stripQuotedAndHeredoc, extractShellCInner } from './destructive-warning
|
|
|
47
47
|
import { _maybeEncodePowerShellCommand } from './shell-command.mjs';
|
|
48
48
|
import { _captureTrackedMtimes, _trackedDriftNoteAfter, _injectionBlockTargets, getDedupedDestructiveWarnings } from './builtin/bash-tool.mjs';
|
|
49
49
|
import { scrubLoaderVars, scrubProviderSecrets } from './env-scrub.mjs';
|
|
50
|
+
import { checkExecPolicyMessage } from './bash-policy-scan.mjs';
|
|
50
51
|
|
|
51
52
|
// Default 600 s (10 min), max 1800 s. Aligned with the one-shot bash tool's
|
|
52
53
|
// 600 s default (builtin/bash-tool.mjs); the persistent shell carries
|
|
@@ -125,7 +125,7 @@ export const BUILTIN_TOOLS = [
|
|
|
125
125
|
name: 'bash',
|
|
126
126
|
title: 'Mixdog Shell',
|
|
127
127
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
128
|
-
description: "Shell for git/build/test/run.
|
|
128
|
+
description: "Shell for git/build/test/run. ALWAYS set `shell` explicitly ('bash' = POSIX via Git Bash, 'powershell' = PS cmdlets); omitting defaults to the OS shell (Windows = PowerShell, POSIX = /bin/sh) and mis-parses the other syntax. run_in_background works for both shells, including Windows shell:'bash' (Git Bash). Single shell entry point; not for inline code you were asked to return.",
|
|
129
129
|
inputSchema: {
|
|
130
130
|
type: 'object',
|
|
131
131
|
properties: {
|