mixdog 0.7.11 → 0.7.13
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 +5 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +28 -74
- package/README.md +193 -249
- package/bin/statusline-launcher.mjs +5 -1
- package/bin/statusline-lib.mjs +14 -6
- package/bin/statusline.mjs +14 -6
- package/bun.lock +128 -3
- package/defaults/hidden-roles.json +3 -0
- package/defaults/user-workflow.json +1 -2
- package/defaults/user-workflow.md +5 -1
- package/hooks/lib/settings-loader.cjs +4 -3
- package/hooks/pre-tool-subagent.cjs +7 -2
- package/hooks/session-start.cjs +52 -24
- package/lib/mixdog-debug.cjs +163 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +9 -2
- package/scripts/builtin-utils-smoke.mjs +14 -8
- package/scripts/bump.mjs +80 -0
- package/scripts/doctor.mjs +8 -3
- package/scripts/ensure-deps.mjs +2 -2
- package/scripts/mutation-io-smoke.mjs +17 -1
- package/scripts/permission-eval-smoke.mjs +18 -1
- package/scripts/run-mcp.mjs +65 -9
- package/scripts/statusline-launcher-smoke.mjs +2 -2
- package/scripts/webhook-selfheal-smoke.mjs +1 -3
- package/server-main.mjs +57 -3
- package/setup/install.mjs +574 -574
- package/setup/launch-core.mjs +0 -1
- package/setup/setup-server.mjs +90 -35
- package/setup/setup.html +44 -11
- 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/providers/openai-oauth.mjs +9 -2
- package/src/agent/orchestrator/providers/openai-ws.mjs +23 -0
- 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/native-edit-runner.mjs +29 -8
- 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 +11 -11
- package/src/agent/tool-defs.mjs +1 -1
- package/src/channels/index.mjs +39 -9
- package/src/channels/lib/event-queue.mjs +24 -1
- package/src/channels/lib/hook-pipe-server.mjs +21 -8
- package/src/channels/lib/webhook.mjs +159 -20
- package/src/memory/index.mjs +5 -1
- package/src/memory/lib/core-memory-store.mjs +1 -1
- package/src/memory/lib/memory-cycle1.mjs +8 -4
- package/src/memory/lib/memory-cycle2.mjs +1 -1
- package/src/memory/lib/memory-cycle3.mjs +1 -1
- package/src/memory/lib/memory-recall-store.mjs +27 -10
- package/src/search/lib/backends/openai-oauth.mjs +6 -2
- package/src/search/lib/cache.mjs +55 -7
- package/tools.json +2 -2
- package/scripts/test-config-rmw-restore.mjs +0 -122
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdir } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
|
|
4
4
|
// Glob-to-RegExp compiler for name filters used by find_files and the
|
|
@@ -125,21 +125,34 @@ export const NOISE_DIR_NAMES = new Set([
|
|
|
125
125
|
'.gradle', 'coverage',
|
|
126
126
|
]);
|
|
127
127
|
|
|
128
|
+
export const MAX_WALK_ENTRIES = 200_000;
|
|
129
|
+
|
|
128
130
|
// Unified directory walk used by list / tree / find_files. The visitor
|
|
129
131
|
// callback owns the "should I record this entry?" decision; returning
|
|
130
132
|
// literal false aborts the whole walk.
|
|
131
133
|
// `onWarn(dir, err)` (optional) is invoked for any readdir failure so
|
|
132
134
|
// callers can surface skipped paths instead of silently dropping them.
|
|
133
|
-
export function walkDir(root, { hidden = false, maxDepth = Infinity, visit, sort, excludeDirNames, onWarn } = {}) {
|
|
135
|
+
export async function walkDir(root, { hidden = false, maxDepth = Infinity, visit, sort, excludeDirNames, onWarn, maxEntries = MAX_WALK_ENTRIES, signal } = {}) {
|
|
134
136
|
// Windows filesystems are case-insensitive — match exclusion names the
|
|
135
137
|
// same way so e.g. Node_Modules is pruned like node_modules.
|
|
136
138
|
const _exclCI = process.platform === 'win32' && excludeDirNames && excludeDirNames.size > 0
|
|
137
139
|
? new Set([...excludeDirNames].map((n) => n.toLowerCase()))
|
|
138
140
|
: null;
|
|
139
|
-
|
|
141
|
+
let truncated = false;
|
|
142
|
+
const cap = maxEntries;
|
|
143
|
+
let entriesVisited = 0;
|
|
144
|
+
const _walk = async (dir, depth) => {
|
|
145
|
+
if (signal?.aborted) {
|
|
146
|
+
truncated = true;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (entriesVisited >= cap) {
|
|
150
|
+
truncated = true;
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
140
153
|
if (depth > maxDepth) return true;
|
|
141
154
|
let entries;
|
|
142
|
-
try { entries =
|
|
155
|
+
try { entries = await readdir(dir, { withFileTypes: true }); }
|
|
143
156
|
catch (err) {
|
|
144
157
|
if (typeof onWarn === 'function') {
|
|
145
158
|
try { onWarn(dir, err); } catch { /* warning sink must not abort */ }
|
|
@@ -155,16 +168,26 @@ export function walkDir(root, { hidden = false, maxDepth = Infinity, visit, sort
|
|
|
155
168
|
if (sort) entries.sort(sort);
|
|
156
169
|
const total = entries.length;
|
|
157
170
|
for (let i = 0; i < total; i++) {
|
|
171
|
+
if (signal?.aborted) {
|
|
172
|
+
truncated = true;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (entriesVisited >= cap) {
|
|
176
|
+
truncated = true;
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
entriesVisited += 1;
|
|
158
180
|
const ent = entries[i];
|
|
159
181
|
const entPath = join(dir, ent.name);
|
|
160
182
|
const ctx = { depth, index: i, total, isLast: i === total - 1 };
|
|
161
183
|
const cont = visit(ent, entPath, ctx);
|
|
162
184
|
if (cont === false) return false;
|
|
163
185
|
if (ent.isDirectory()) {
|
|
164
|
-
if (_walk(entPath, depth + 1) === false) return false;
|
|
186
|
+
if ((await _walk(entPath, depth + 1)) === false) return false;
|
|
165
187
|
}
|
|
166
188
|
}
|
|
167
189
|
return true;
|
|
168
190
|
};
|
|
169
|
-
_walk(root, 1);
|
|
191
|
+
await _walk(root, 1);
|
|
192
|
+
return { truncated, entriesVisited };
|
|
170
193
|
}
|
|
@@ -111,10 +111,11 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
111
111
|
// never hit either bound, so normal behavior is unchanged.
|
|
112
112
|
let truncatedByCap = false;
|
|
113
113
|
const walkDeadline = Date.now() + LIST_WALK_TIMEOUT_MS;
|
|
114
|
-
walkDir(fullPath, {
|
|
114
|
+
await walkDir(fullPath, {
|
|
115
115
|
hidden,
|
|
116
116
|
maxDepth: depth,
|
|
117
117
|
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
118
|
+
signal: options.signal,
|
|
118
119
|
visit: (ent, entPath) => {
|
|
119
120
|
if (Date.now() > walkDeadline) { truncatedByCap = true; return false; }
|
|
120
121
|
const isDir = ent.isDirectory();
|
|
@@ -235,10 +236,11 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
235
236
|
const lines = [`${normalizeOutputPath(fullPath)}/`];
|
|
236
237
|
const prefixStack = [''];
|
|
237
238
|
const TREE_BRANCH_LINE_CAP = 500;
|
|
238
|
-
walkDir(fullPath, {
|
|
239
|
+
await walkDir(fullPath, {
|
|
239
240
|
hidden,
|
|
240
241
|
maxDepth: depth,
|
|
241
242
|
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
243
|
+
signal: options.signal,
|
|
242
244
|
sort: (a, b) => {
|
|
243
245
|
const ad = a.isDirectory(), bd = b.isDirectory();
|
|
244
246
|
if (ad !== bd) return ad ? -1 : 1;
|
|
@@ -500,10 +502,11 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
500
502
|
if (!handledByRgFiles && useBatchedStat) {
|
|
501
503
|
const candidates = [];
|
|
502
504
|
const walkDeadline1 = Date.now() + FIND_WALK_TIMEOUT_MS;
|
|
503
|
-
walkDir(fullPath, {
|
|
505
|
+
await walkDir(fullPath, {
|
|
504
506
|
hidden,
|
|
505
507
|
maxDepth: depth ?? Infinity,
|
|
506
508
|
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
509
|
+
signal: options.signal,
|
|
507
510
|
visit: (ent, entPath) => {
|
|
508
511
|
if (Date.now() > walkDeadline1) { truncatedByCap = true; return false; }
|
|
509
512
|
const isDir = ent.isDirectory();
|
|
@@ -532,10 +535,11 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
532
535
|
const effectiveTypeFilter = sizeFiltered && typeFilter === 'any' ? 'file' : typeFilter;
|
|
533
536
|
const candidates = [];
|
|
534
537
|
const walkDeadline2 = Date.now() + FIND_WALK_TIMEOUT_MS;
|
|
535
|
-
walkDir(fullPath, {
|
|
538
|
+
await walkDir(fullPath, {
|
|
536
539
|
hidden,
|
|
537
540
|
maxDepth: depth ?? Infinity,
|
|
538
541
|
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
542
|
+
signal: options.signal,
|
|
539
543
|
visit: (ent, entPath) => {
|
|
540
544
|
if (Date.now() > walkDeadline2) { truncatedByCap = true; return false; }
|
|
541
545
|
const isDir = ent.isDirectory();
|
|
@@ -19,21 +19,35 @@ export function nativeEditMode() {
|
|
|
19
19
|
return String(process.env.MIXDOG_EDIT_NATIVE || 'auto').toLowerCase();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
function nativeEditBinCandidate() {
|
|
23
23
|
const override = process.env.MIXDOG_EDIT_NATIVE_BIN || process.env.MIXDOG_PATCH_NATIVE_BIN;
|
|
24
|
-
if (override) return override;
|
|
25
|
-
if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return NATIVE_EDIT_DEFAULT_BIN;
|
|
26
|
-
|
|
24
|
+
if (override) return { path: override, kind: 'override' };
|
|
25
|
+
if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return { path: NATIVE_EDIT_DEFAULT_BIN, kind: 'local' };
|
|
26
|
+
const cached = findCachedPatchBinary(getPluginData());
|
|
27
|
+
if (cached) return { path: cached, kind: 'cached' };
|
|
28
|
+
return { path: NATIVE_EDIT_DEFAULT_BIN, kind: 'missing' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function nativeEditBinPath() {
|
|
32
|
+
return nativeEditBinCandidate().path;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloadedContent, preloadedRawBuf }) {
|
|
30
36
|
const mode = nativeEditMode();
|
|
31
37
|
if (/^(0|false|no|off|js|legacy)$/i.test(mode)) return false;
|
|
32
|
-
|
|
38
|
+
const forcedNative = /^(1|true|yes|on|native)$/i.test(mode);
|
|
39
|
+
const candidate = nativeEditBinCandidate();
|
|
40
|
+
if (!existsSync(candidate.path)) return false;
|
|
41
|
+
// Cached release prebuilds are guaranteed valid for apply_patch, but older
|
|
42
|
+
// manifests (currently v0.6.5 in clean CI) predate the EDIT server protocol.
|
|
43
|
+
// In auto mode, native edit is only an acceleration, so require either a
|
|
44
|
+
// local cargo build or an explicit override. If a user forces native mode,
|
|
45
|
+
// still try the cached binary and surface any protocol failure.
|
|
46
|
+
if (candidate.kind === 'cached' && !forcedNative) return false;
|
|
33
47
|
if (!snapshotCoversFullFile(editSnapshot)) return false;
|
|
34
48
|
if (preloadedContent !== null || preloadedRawBuf !== null) return false;
|
|
35
49
|
if (typeof oldStr !== 'string' || oldStr.length === 0 || typeof newStr !== 'string') return false;
|
|
36
|
-
if (
|
|
50
|
+
if (forcedNative) return true;
|
|
37
51
|
// auto: the persistent server removed per-call spawn cost, so route edits to
|
|
38
52
|
// native edit2 by default (B3). Same-size edits keep the JS in-place partial
|
|
39
53
|
// write, which rewrites bytes in place instead of the whole file.
|
|
@@ -44,6 +58,7 @@ export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloade
|
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll, signal = null }) {
|
|
61
|
+
const forcedNative = /^(1|true|yes|on|native)$/i.test(nativeEditMode());
|
|
47
62
|
if (signal?.aborted) {
|
|
48
63
|
return { ok: false, fallback: false, error: signal.reason?.message || signal.reason || 'native edit aborted' };
|
|
49
64
|
}
|
|
@@ -82,8 +97,14 @@ export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll,
|
|
|
82
97
|
}
|
|
83
98
|
const msg = String(err?.message || err);
|
|
84
99
|
// Tier misses and not-found map to a JS fallback; transport/spawn errors
|
|
85
|
-
// also fall back so a server hiccup never blocks an edit.
|
|
86
|
-
|
|
100
|
+
// also fall back so a server hiccup never blocks an edit. Older cached
|
|
101
|
+
// mixdog-patch binaries (for example the v0.6.5 release prebuilds used
|
|
102
|
+
// by clean CI before a local cargo build exists) support APPLY but not
|
|
103
|
+
// the EDIT server protocol, and answer EDIT with the APPLY parser's
|
|
104
|
+
// "bad header" error. In auto mode that means "native edit unavailable",
|
|
105
|
+
// not "the edit is invalid", so fall through to the JS editor. When the
|
|
106
|
+
// user explicitly forces native mode, keep surfacing the native failure.
|
|
107
|
+
const fallback = !forcedNative && /old_string (?:not found|found \d+ times)|not valid UTF-8|no exact match|not found|server|bad header|bad edit header/i.test(msg);
|
|
87
108
|
return { ok: false, fallback, error: msg };
|
|
88
109
|
}
|
|
89
110
|
}
|
|
@@ -361,6 +361,9 @@ function capToolOutput(result, options = {}) {
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
export async function executeBuiltinTool(name, args, cwd, options = {}) {
|
|
364
|
+
if (options.abortSignal && !options.signal) {
|
|
365
|
+
options = { ...options, signal: options.abortSignal };
|
|
366
|
+
}
|
|
364
367
|
const toolName = canonicalizeBuiltinToolName(name);
|
|
365
368
|
const argError = validateBuiltinArgs(toolName, args);
|
|
366
369
|
if (argError) return argError;
|
|
@@ -414,9 +417,9 @@ export async function executeBuiltinTool(name, args, cwd, options = {}) {
|
|
|
414
417
|
case 'list':
|
|
415
418
|
return executeListTool(args, workDir, options);
|
|
416
419
|
case 'tree':
|
|
417
|
-
return executeTreeTool(args, workDir);
|
|
420
|
+
return executeTreeTool(args, workDir, options);
|
|
418
421
|
case 'find_files':
|
|
419
|
-
return executeFindFilesTool(args, workDir);
|
|
422
|
+
return executeFindFilesTool(args, workDir, options);
|
|
420
423
|
case 'head':
|
|
421
424
|
return executeHeadTool(args, workDir, readStateScope, _readModeHelpers);
|
|
422
425
|
case 'tail':
|
|
@@ -58,15 +58,14 @@ const PRUNED_DIR_NAMES = new Set([...NOISE_DIR_NAMES, ...EXTRA_NOISE_DIR_NAMES])
|
|
|
58
58
|
|
|
59
59
|
let _listCache = null
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// a previously-recorded
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
_recordedRepos.clear()
|
|
61
|
+
async function _scanReposUnderFiltered(root, signal) {
|
|
62
|
+
// Per-scan set used by the visitor to mark repos whose subtree must not be
|
|
63
|
+
// descended. walkDir does not natively support "stop descent into this dir
|
|
64
|
+
// but keep the walk going", so we approximate by checking ancestry on every
|
|
65
|
+
// visit and bailing early when the entry lives under a previously-recorded
|
|
66
|
+
// repo. MUST be local per-call: walkDir is async, so a module-global set
|
|
67
|
+
// would race across concurrent/background scans suspended in `await readdir`.
|
|
68
|
+
const recordedRepos = new Set()
|
|
70
69
|
const repos = []
|
|
71
70
|
if (!root || !existsSync(root)) return repos
|
|
72
71
|
try {
|
|
@@ -79,17 +78,18 @@ function _scanReposUnderFiltered(root) {
|
|
|
79
78
|
return repos
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
walkDir(root, {
|
|
81
|
+
await walkDir(root, {
|
|
83
82
|
hidden: true,
|
|
84
83
|
maxDepth: 4,
|
|
85
84
|
excludeDirNames: PRUNED_DIR_NAMES,
|
|
85
|
+
signal,
|
|
86
86
|
visit: (ent, entPath) => {
|
|
87
87
|
if (!ent.isDirectory()) return
|
|
88
88
|
// Skip subtree of a previously-recorded repo. walkDir invokes
|
|
89
89
|
// visit before recursing, so flipping ent.isDirectory() here
|
|
90
90
|
// would not stop descent. Instead, check ancestry on every
|
|
91
91
|
// visit and bail early.
|
|
92
|
-
for (const recorded of
|
|
92
|
+
for (const recorded of recordedRepos) {
|
|
93
93
|
if (entPath === recorded || entPath.startsWith(recorded + '/') || entPath.startsWith(recorded + '\\')) {
|
|
94
94
|
return
|
|
95
95
|
}
|
|
@@ -97,7 +97,7 @@ function _scanReposUnderFiltered(root) {
|
|
|
97
97
|
try {
|
|
98
98
|
if (existsSync(`${entPath}/.git`)) {
|
|
99
99
|
repos.push(entPath)
|
|
100
|
-
|
|
100
|
+
recordedRepos.add(entPath)
|
|
101
101
|
}
|
|
102
102
|
} catch { /* ignore stat races */ }
|
|
103
103
|
},
|
|
@@ -151,7 +151,7 @@ function _authoredByIdentity(repoPath, emails) {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
function _buildRepoList(includeAll = false) {
|
|
154
|
+
async function _buildRepoList(includeAll = false, signal) {
|
|
155
155
|
const cfg = _cwdConfig()
|
|
156
156
|
const user = rawUserCwd()
|
|
157
157
|
const roots = []
|
|
@@ -169,7 +169,7 @@ function _buildRepoList(includeAll = false) {
|
|
|
169
169
|
|
|
170
170
|
const repoSet = new Map() // canonical → original path
|
|
171
171
|
for (const root of roots) {
|
|
172
|
-
for (const repo of _scanReposUnderFiltered(root)) {
|
|
172
|
+
for (const repo of await _scanReposUnderFiltered(root, signal)) {
|
|
173
173
|
const key = _canonicalSessionCwd(repo)
|
|
174
174
|
if (!repoSet.has(key)) repoSet.set(key, repo)
|
|
175
175
|
}
|
|
@@ -257,9 +257,9 @@ export async function executeCwdTool(name, args, callerCwd, opts = {}) {
|
|
|
257
257
|
if (action === 'list') {
|
|
258
258
|
// `all:true` bypasses the owned-project filter and transient prune — a
|
|
259
259
|
// raw scan of every .git repo under the roots. Not cached (rare).
|
|
260
|
-
if (args?.all === true) return _formatList(_buildRepoList(true))
|
|
260
|
+
if (args?.all === true) return _formatList(await _buildRepoList(true, opts.signal))
|
|
261
261
|
if (args?.refresh === true) _listCache = null
|
|
262
|
-
if (!_listCache) _listCache = _buildRepoList(false)
|
|
262
|
+
if (!_listCache) _listCache = await _buildRepoList(false, opts.signal)
|
|
263
263
|
return _formatList(_listCache)
|
|
264
264
|
}
|
|
265
265
|
|
|
@@ -293,6 +293,6 @@ export const CWD_TOOL_DEFS = [
|
|
|
293
293
|
// and unref'd so it never blocks startup or keeps the process alive.
|
|
294
294
|
try {
|
|
295
295
|
setTimeout(() => {
|
|
296
|
-
try { if (!_listCache) _listCache =
|
|
296
|
+
try { if (!_listCache) _buildRepoList(false).then((r) => { _listCache = r }) } catch { /* best-effort */ }
|
|
297
297
|
}, 1000).unref?.()
|
|
298
298
|
} catch { /* environments without setTimeout */ }
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.7.13",
|
|
3
3
|
"_comment": "Rewritten by .github/workflows/graph-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-graph binary on the GitHub release. A local cargo build under native/mixdog-graph/target/release always takes precedence at runtime. (v0.5.236 entries were filled manually after CI's commit step hit detached HEAD; the workflow now checks out ref: main so future releases self-update.)",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
7
|
-
"sha256": "
|
|
6
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-darwin-arm64",
|
|
7
|
+
"sha256": "75bfdd200b2f8553b72dc877ec2637208f581800083d1ee5f9caf33f87792bf7"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
11
|
-
"sha256": "
|
|
10
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-darwin-x64",
|
|
11
|
+
"sha256": "04742fbb4cbe09bb76943f312ee129c05814543e7bc9d37e1241fb4e65b97137"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
15
|
-
"sha256": "
|
|
14
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-linux-arm64",
|
|
15
|
+
"sha256": "4b3edcd7be1ffec7184c48fe6bc7d6bce42f2ea67d4709f44d4402e6b48564f2"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
19
|
-
"sha256": "
|
|
18
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-linux-x64",
|
|
19
|
+
"sha256": "4394bb7884a8706dd6a4eea55f8755c76ba584cd02248863802d94acc3e1413c"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-win32-x64.exe",
|
|
23
|
+
"sha256": "2acf1fc9040cfc78a643daa477b15c8eaa0f520fd8addd1cf6f2f8cddd5c914c"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.7.13",
|
|
3
3
|
"_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
7
|
-
"sha256": "
|
|
6
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-darwin-arm64",
|
|
7
|
+
"sha256": "836a0b60a443b0a6a8c1bbe24d15a79ed70ee92c2f0fbc05374c4e9ed2536415"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
11
|
-
"sha256": "
|
|
10
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-darwin-x64",
|
|
11
|
+
"sha256": "cab10c4e1e8b72d3958241dffdff764712ed74f4861d105bafa8258961215c98"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
15
|
-
"sha256": "
|
|
14
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-linux-arm64",
|
|
15
|
+
"sha256": "a90c32ce3417a7d853f2723f82f3613cf2cd030fe885cf710cfc9f8e4b193264"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
19
|
-
"sha256": "
|
|
18
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-linux-x64",
|
|
19
|
+
"sha256": "0fea40ab98acd35bfb47515756024d1882a2abbaddce8a0b51642d20ac405577"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-win32-x64.exe",
|
|
23
|
+
"sha256": "24b3c2b2b95a2e3b15e0e089ca41297056be7c12df8c4c7524fa91a4833b1f68"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/agent/tool-defs.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export const TOOL_DEFS = [
|
|
|
18
18
|
title: 'Explore',
|
|
19
19
|
aiWrapped: true,
|
|
20
20
|
annotations: { title: 'Explore', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
21
|
-
description: 'Read-only codebase EXPLORATION — fact-finding only: locate/map where and how things are implemented, for open-ended/unknown scope (for a known or partial identifier use code_graph; recall=memory, search=web). NOT a reviewer/auditor: explorers LOCATE and DESCRIBE code, never judge it — bug/quality/risk claims in explore output are UNVERIFIED leads; verify them (reviewer role or direct reads) before acting on or reporting them. Shape every query as a LOCATION/INVENTORY question ("where is X handled", "which files implement Y", "what does Z read/write") — NEVER a verdict question ("is X correct/missing/inconsistent?", "are there gaps/bugs?"); the judgment stays with the caller, applied to the coordinates explore returns. Query shaping
|
|
21
|
+
description: 'Read-only codebase EXPLORATION — fact-finding only: locate/map where and how things are implemented, for open-ended/unknown scope (for a known or partial identifier use code_graph; recall=memory, search=web). NOT a reviewer/auditor: explorers LOCATE and DESCRIBE code, never judge it — bug/quality/risk claims in explore output are UNVERIFIED leads; verify them (reviewer role or direct reads) before acting on or reporting them. Shape every query as a LOCATION/INVENTORY question ("where is X handled", "which files implement Y", "what does Z read/write") — NEVER a verdict question ("is X correct/missing/inconsistent?", "are there gaps/bugs?"); the judgment stays with the caller, applied to the coordinates explore returns. Query shaping rules are on the query parameter — follow them. Fan-out runs items in parallel; wall-clock = the slowest item. LEAD: default background:true (answer pushed via channel, avoids the 120s sync cap). BRIDGE WORKERS run it sync and SHOULD prefer it for a tree-wide enumeration or broad/unanchored exploration — ONE call offloads the whole sweep into a sub-agent instead of a long grep/code_graph storm; a bounded/known-anchor lookup stays a direct code_graph/grep call.',
|
|
22
22
|
inputSchema: {
|
|
23
23
|
type: 'object',
|
|
24
24
|
properties: {
|
package/src/channels/index.mjs
CHANGED
|
@@ -183,10 +183,18 @@ const _bootLogEarly = path.join(
|
|
|
183
183
|
process.env.CLAUDE_PLUGIN_DATA || path.join(os.tmpdir(), "mixdog"),
|
|
184
184
|
"boot.log"
|
|
185
185
|
);
|
|
186
|
+
const {
|
|
187
|
+
isMixdogDebugEnabled: isMixdogDebug,
|
|
188
|
+
pruneStalePluginDataLogSiblings,
|
|
189
|
+
appendSessionStartCriticalLog,
|
|
190
|
+
DEFAULT_STALE_LOG_SIBLING_MAX,
|
|
191
|
+
} = _require("../../lib/mixdog-debug.cjs");
|
|
186
192
|
// One-shot log rotation at worker boot (10 MB threshold, .1 suffix overwrite).
|
|
187
|
-
|
|
188
|
-
fs.
|
|
193
|
+
if (isMixdogDebug()) {
|
|
194
|
+
try { if (fs.statSync(_bootLogEarly).size > 10 * 1024 * 1024) fs.renameSync(_bootLogEarly, _bootLogEarly + '.1') } catch {}
|
|
195
|
+
fs.appendFileSync(_bootLogEarly, `[${localTimestamp()}] bootstrap start pid=${process.pid}
|
|
189
196
|
`);
|
|
197
|
+
}
|
|
190
198
|
const _bootLog = path.join(DATA_DIR, "boot.log");
|
|
191
199
|
let config = loadConfig();
|
|
192
200
|
let backend = createBackend(config);
|
|
@@ -238,6 +246,11 @@ try {
|
|
|
238
246
|
try { if (_now - fs.statSync(_p).mtimeMs > _STALE_SESSION_TTL_MS) fs.unlinkSync(_p); } catch {}
|
|
239
247
|
}
|
|
240
248
|
} catch {}
|
|
249
|
+
// Count-based cap: drop oldest *.log siblings when plugin-data accumulates
|
|
250
|
+
// hundreds of per-process files (doctor warns above 300).
|
|
251
|
+
try {
|
|
252
|
+
pruneStalePluginDataLogSiblings(DATA_DIR, DEFAULT_STALE_LOG_SIBLING_MAX);
|
|
253
|
+
} catch {}
|
|
241
254
|
|
|
242
255
|
// ── Buffered drop-trace writer (channels/index) ──────────────────────────────
|
|
243
256
|
// Flushes every 1 s OR when buffer reaches 64 KB — whichever fires first.
|
|
@@ -2030,8 +2043,12 @@ backend.onInteraction = (interaction) => {
|
|
|
2030
2043
|
const [, uuid, action] = match;
|
|
2031
2044
|
const access = config.access;
|
|
2032
2045
|
if (!access) {
|
|
2033
|
-
|
|
2034
|
-
|
|
2046
|
+
const _permDropLine = `[${localTimestamp()}] perm interaction dropped: no access config\n`;
|
|
2047
|
+
if (isMixdogDebug()) {
|
|
2048
|
+
fs.appendFileSync(_bootLog, _permDropLine);
|
|
2049
|
+
} else {
|
|
2050
|
+
appendSessionStartCriticalLog(DATA_DIR, `[channels] ${_permDropLine}`);
|
|
2051
|
+
}
|
|
2035
2052
|
return;
|
|
2036
2053
|
}
|
|
2037
2054
|
if (access.allowFrom?.length > 0 && !access.allowFrom.includes(interaction.userId)) {
|
|
@@ -2566,7 +2583,18 @@ async function handleToolCall(name, args, signal) {
|
|
|
2566
2583
|
}
|
|
2567
2584
|
case "reload_config": {
|
|
2568
2585
|
await reloadRuntimeConfig();
|
|
2569
|
-
|
|
2586
|
+
// Extend reload to the agent module so providers/presets/maintenance
|
|
2587
|
+
// hot-reload on the same call (dynamic import: agent/index.mjs does not
|
|
2588
|
+
// import channels, so this stays acyclic and tolerant of load order).
|
|
2589
|
+
let agentReloadMsg = "";
|
|
2590
|
+
try {
|
|
2591
|
+
const { reloadAgentConfig } = await import("../agent/index.mjs");
|
|
2592
|
+
await reloadAgentConfig("reload_config tool");
|
|
2593
|
+
agentReloadMsg = ", agent providers/presets/maintenance";
|
|
2594
|
+
} catch (err) {
|
|
2595
|
+
process.stderr.write(`[reload_config] agent reload failed: ${err?.message || String(err)}\n`);
|
|
2596
|
+
}
|
|
2597
|
+
result = { content: [{ type: "text", text: `config reloaded — schedules, webhooks, events${agentReloadMsg} re-registered` }] };
|
|
2570
2598
|
break;
|
|
2571
2599
|
}
|
|
2572
2600
|
case "inject_command": {
|
|
@@ -3016,12 +3044,14 @@ async function stop() {
|
|
|
3016
3044
|
return false;
|
|
3017
3045
|
};
|
|
3018
3046
|
_channelFlagDetected = detectChannelFlag();
|
|
3019
|
-
|
|
3020
|
-
`);
|
|
3047
|
+
if (isMixdogDebug()) {
|
|
3048
|
+
fs.appendFileSync(_bootLog, `[${localTimestamp()}] channelFlag: ${_channelFlagDetected}\n`);
|
|
3049
|
+
if (_channelFlagDetected) {
|
|
3050
|
+
fs.appendFileSync(_bootLog, `[${localTimestamp()}] channel mode detected — bridge auto-activated\n`);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3021
3053
|
if (_channelFlagDetected) {
|
|
3022
3054
|
channelBridgeActive = true;
|
|
3023
|
-
fs.appendFileSync(_bootLog, `[${localTimestamp()}] channel mode detected \u2014 bridge auto-activated
|
|
3024
|
-
`);
|
|
3025
3055
|
}
|
|
3026
3056
|
writeBridgeState(channelBridgeActive);
|
|
3027
3057
|
const previousOwner = readActiveInstance();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, existsSync as fsExistsSync } from "fs";
|
|
1
|
+
import { readdirSync, readFileSync, existsSync as fsExistsSync, statSync, unlinkSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { DATA_DIR } from "./config.mjs";
|
|
4
4
|
import { ensureDir } from "./state-file.mjs";
|
|
@@ -7,6 +7,27 @@ import { renameWithRetrySync, writeJsonAtomicSync } from "../../shared/atomic-fi
|
|
|
7
7
|
const QUEUE_DIR = join(DATA_DIR, "events", "queue");
|
|
8
8
|
const IN_PROGRESS_DIR = join(DATA_DIR, "events", "in-progress");
|
|
9
9
|
const PROCESSED_DIR = join(DATA_DIR, "events", "processed");
|
|
10
|
+
const PROCESSED_DIR_MAX_ENTRIES = 200;
|
|
11
|
+
function pruneProcessedDir() {
|
|
12
|
+
try {
|
|
13
|
+
if (!fsExistsSync(PROCESSED_DIR)) return;
|
|
14
|
+
const names = readdirSync(PROCESSED_DIR);
|
|
15
|
+
if (names.length <= PROCESSED_DIR_MAX_ENTRIES) return;
|
|
16
|
+
const ranked = [];
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
try {
|
|
19
|
+
const st = statSync(join(PROCESSED_DIR, name));
|
|
20
|
+
ranked.push({ name, mtime: st.mtimeMs });
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
ranked.sort((a, b) => b.mtime - a.mtime);
|
|
24
|
+
for (let i = PROCESSED_DIR_MAX_ENTRIES; i < ranked.length; i++) {
|
|
25
|
+
try {
|
|
26
|
+
unlinkSync(join(PROCESSED_DIR, ranked[i].name));
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
10
31
|
function finiteInt(value, { min, max, def }) {
|
|
11
32
|
const n = Number(value);
|
|
12
33
|
if (!Number.isFinite(n)) return def;
|
|
@@ -290,6 +311,7 @@ ${p.item.prompt}`).join("\n\n")}`;
|
|
|
290
311
|
const fromQueue = join(QUEUE_DIR, file);
|
|
291
312
|
const src = this.existsSync(fromInProgress) ? fromInProgress : fromQueue;
|
|
292
313
|
renameWithRetrySync(src, join(PROCESSED_DIR, `${status}-${file}`));
|
|
314
|
+
pruneProcessedDir();
|
|
293
315
|
} catch {
|
|
294
316
|
}
|
|
295
317
|
}
|
|
@@ -297,6 +319,7 @@ ${p.item.prompt}`).join("\n\n")}`;
|
|
|
297
319
|
try {
|
|
298
320
|
ensureDir(PROCESSED_DIR);
|
|
299
321
|
renameWithRetrySync(join(IN_PROGRESS_DIR, file), join(PROCESSED_DIR, `${status}-${file}`));
|
|
322
|
+
pruneProcessedDir();
|
|
300
323
|
} catch {
|
|
301
324
|
}
|
|
302
325
|
}
|
|
@@ -24,6 +24,11 @@ import { request as httpsRequest } from 'node:https'
|
|
|
24
24
|
import { createRequire } from 'node:module'
|
|
25
25
|
|
|
26
26
|
const moduleRequire = createRequire(import.meta.url)
|
|
27
|
+
const {
|
|
28
|
+
isMixdogDebugEnabled,
|
|
29
|
+
pruneStalePluginDataLogSiblings,
|
|
30
|
+
DEFAULT_STALE_LOG_SIBLING_MAX,
|
|
31
|
+
} = moduleRequire('../../../lib/mixdog-debug.cjs')
|
|
27
32
|
|
|
28
33
|
// IPC transport path. Windows uses a named pipe (`\\.\pipe\…`); Unix uses a
|
|
29
34
|
// Unix domain socket under XDG_RUNTIME_DIR (or /tmp as fallback). Node's
|
|
@@ -57,9 +62,13 @@ const POLL_INTERVAL_MS = 2000
|
|
|
57
62
|
const SUBAGENT_TIMEOUT_MS = 120_000
|
|
58
63
|
const DEFAULT_DISPATCH_TIMEOUT_MS = 15_000
|
|
59
64
|
const SESSION_START_MEMORY_DISPATCH_TIMEOUT_MS = 125_000
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
const MIXDOG_DEBUG_ENABLED = isMixdogDebugEnabled()
|
|
66
|
+
let _hookPipeLogsPruned = false
|
|
67
|
+
|
|
68
|
+
function hookPipeDebugStderr(line) {
|
|
69
|
+
if (!MIXDOG_DEBUG_ENABLED) return
|
|
70
|
+
try { process.stderr.write(line) } catch {}
|
|
71
|
+
}
|
|
63
72
|
|
|
64
73
|
let _started = false
|
|
65
74
|
let _server = null
|
|
@@ -82,13 +91,17 @@ function formatError(err) {
|
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
function traceSessionStart(message) {
|
|
85
|
-
if (!
|
|
94
|
+
if (!MIXDOG_DEBUG_ENABLED) return
|
|
86
95
|
const line = `[${new Date().toISOString()}] [hook-pipe][session-start] ${message}\n`
|
|
87
96
|
try { process.stderr.write(line) } catch {}
|
|
88
97
|
try {
|
|
89
98
|
const dataDir = process.env.CLAUDE_PLUGIN_DATA ||
|
|
90
99
|
join(homedir(), '.claude', 'plugins', 'data', 'mixdog-trib-plugin')
|
|
91
100
|
mkdirSync(dataDir, { recursive: true })
|
|
101
|
+
if (!_hookPipeLogsPruned) {
|
|
102
|
+
_hookPipeLogsPruned = true
|
|
103
|
+
pruneStalePluginDataLogSiblings(dataDir, DEFAULT_STALE_LOG_SIBLING_MAX)
|
|
104
|
+
}
|
|
92
105
|
appendFileSync(join(dataDir, 'session-start.log'), line)
|
|
93
106
|
} catch {}
|
|
94
107
|
}
|
|
@@ -320,10 +333,10 @@ async function handlePreToolSubagent(payload) {
|
|
|
320
333
|
}
|
|
321
334
|
const route = routeMod.shouldRoutePermissionToDiscord()
|
|
322
335
|
if (route.route !== 'discord') {
|
|
323
|
-
|
|
336
|
+
hookPipeDebugStderr(`[hook-pipe] pre-tool-subagent discord-route=off agent=${agentIdRaw || 'unknown'} tool=${toolName} reason=${route.reason || 'inactive'}\n`)
|
|
324
337
|
return null
|
|
325
338
|
}
|
|
326
|
-
|
|
339
|
+
hookPipeDebugStderr(`[hook-pipe] pre-tool-subagent discord-route=on agent=${agentIdRaw || 'unknown'} tool=${toolName}\n`)
|
|
327
340
|
|
|
328
341
|
let getDiscordToken
|
|
329
342
|
try {
|
|
@@ -715,7 +728,7 @@ export function startHookPipeServer() {
|
|
|
715
728
|
_server.on('error', (err) => {
|
|
716
729
|
const msg = String(err?.message || err || '')
|
|
717
730
|
if (err?.code === 'EADDRINUSE' || msg.includes('EADDRINUSE') || msg.includes('Failed to listen')) {
|
|
718
|
-
|
|
731
|
+
hookPipeDebugStderr(`[hook-pipe] ${PIPE_PATH} already owned by a peer daemon; standby for hook IPC\n`)
|
|
719
732
|
_server = null
|
|
720
733
|
_started = false
|
|
721
734
|
return
|
|
@@ -727,7 +740,7 @@ export function startHookPipeServer() {
|
|
|
727
740
|
try {
|
|
728
741
|
_server.listen(PIPE_PATH, () => {
|
|
729
742
|
_started = true
|
|
730
|
-
|
|
743
|
+
hookPipeDebugStderr(`[hook-pipe] listening on ${PIPE_PATH}\n`)
|
|
731
744
|
})
|
|
732
745
|
} catch (err) {
|
|
733
746
|
process.stderr.write(`[hook-pipe] listen failed: ${err?.message || err}\n`)
|