mixdog 0.7.12 → 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 +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 +1 -2
- package/defaults/user-workflow.md +5 -1
- 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 +7 -7
- 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
|
@@ -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();
|
|
@@ -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.7.
|
|
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.
|
|
6
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-darwin-arm64",
|
|
7
7
|
"sha256": "75bfdd200b2f8553b72dc877ec2637208f581800083d1ee5f9caf33f87792bf7"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
10
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-darwin-x64",
|
|
11
11
|
"sha256": "04742fbb4cbe09bb76943f312ee129c05814543e7bc9d37e1241fb4e65b97137"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
14
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-linux-arm64",
|
|
15
15
|
"sha256": "4b3edcd7be1ffec7184c48fe6bc7d6bce42f2ea67d4709f44d4402e6b48564f2"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
18
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-linux-x64",
|
|
19
19
|
"sha256": "4394bb7884a8706dd6a4eea55f8755c76ba584cd02248863802d94acc3e1413c"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
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.7.
|
|
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.
|
|
6
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-darwin-arm64",
|
|
7
7
|
"sha256": "836a0b60a443b0a6a8c1bbe24d15a79ed70ee92c2f0fbc05374c4e9ed2536415"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
10
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-darwin-x64",
|
|
11
11
|
"sha256": "cab10c4e1e8b72d3958241dffdff764712ed74f4861d105bafa8258961215c98"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
14
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-linux-arm64",
|
|
15
15
|
"sha256": "a90c32ce3417a7d853f2723f82f3613cf2cd030fe885cf710cfc9f8e4b193264"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
18
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-linux-x64",
|
|
19
19
|
"sha256": "0fea40ab98acd35bfb47515756024d1882a2abbaddce8a0b51642d20ac405577"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
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
|
@@ -2583,7 +2583,18 @@ async function handleToolCall(name, args, signal) {
|
|
|
2583
2583
|
}
|
|
2584
2584
|
case "reload_config": {
|
|
2585
2585
|
await reloadRuntimeConfig();
|
|
2586
|
-
|
|
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` }] };
|
|
2587
2598
|
break;
|
|
2588
2599
|
}
|
|
2589
2600
|
case "inject_command": {
|
|
@@ -171,19 +171,37 @@ function _deliveryIndexFor(name) {
|
|
|
171
171
|
}
|
|
172
172
|
return map;
|
|
173
173
|
}
|
|
174
|
+
// Bound retained ids so successful ("done") deliveries cannot accumulate
|
|
175
|
+
// forever in RAM or on disk. In-flight claims (received/processing) are
|
|
176
|
+
// ALWAYS kept — dropping one would let a duplicate dispatch through. The
|
|
177
|
+
// remaining DELIVERY_INDEX_MAX_IDS budget goes to the newest "done" rows
|
|
178
|
+
// (dedup of recent retries) first, then newest terminal rows for history.
|
|
179
|
+
// Older "done" rows age out; a sender re-delivering an id that stale is
|
|
180
|
+
// treated as new — acceptable beyond any realistic retry window.
|
|
181
|
+
function _retainedDeliveryIds(entries) {
|
|
182
|
+
const inflight = [];
|
|
183
|
+
const done = [];
|
|
184
|
+
const other = [];
|
|
185
|
+
for (const e of entries) {
|
|
186
|
+
if (e.status === "received" || e.status === "processing") inflight.push(e);
|
|
187
|
+
else if (e.status === "done") done.push(e);
|
|
188
|
+
else other.push(e);
|
|
189
|
+
}
|
|
190
|
+
const keep = new Set(inflight.map((e) => e.id));
|
|
191
|
+
const byTsDesc = (a, b) => String(b.ts || "").localeCompare(String(a.ts || ""));
|
|
192
|
+
done.sort(byTsDesc);
|
|
193
|
+
other.sort(byTsDesc);
|
|
194
|
+
for (const e of [...done, ...other]) {
|
|
195
|
+
if (keep.size >= DELIVERY_INDEX_MAX_IDS) break;
|
|
196
|
+
keep.add(e.id);
|
|
197
|
+
}
|
|
198
|
+
return keep;
|
|
199
|
+
}
|
|
174
200
|
function _pruneDeliveryIndexMap(byId) {
|
|
175
201
|
if (byId.size <= DELIVERY_INDEX_MAX_IDS) return;
|
|
176
|
-
const
|
|
177
|
-
const blocking = rows.filter(([, e]) => _isBlockingDeliveryStatus(e.status));
|
|
178
|
-
const nonBlocking = rows.filter(([, e]) => !_isBlockingDeliveryStatus(e.status));
|
|
179
|
-
nonBlocking.sort((a, b) => String(b[1].ts || "").localeCompare(String(a[1].ts || "")));
|
|
180
|
-
const keepIds = new Set(blocking.map(([id]) => id));
|
|
181
|
-
for (const [id] of nonBlocking) {
|
|
182
|
-
if (keepIds.size >= DELIVERY_INDEX_MAX_IDS) break;
|
|
183
|
-
keepIds.add(id);
|
|
184
|
-
}
|
|
202
|
+
const keep = _retainedDeliveryIds([...byId.values()]);
|
|
185
203
|
for (const id of byId.keys()) {
|
|
186
|
-
if (!
|
|
204
|
+
if (!keep.has(id)) byId.delete(id);
|
|
187
205
|
}
|
|
188
206
|
}
|
|
189
207
|
function _deliveryLogLineCount(name) {
|
|
@@ -221,7 +239,10 @@ function _ingestDeliveriesFileIntoIndex(name) {
|
|
|
221
239
|
for (const [id, row] of merged) byId.set(id, row);
|
|
222
240
|
_pruneDeliveryIndexMap(byId);
|
|
223
241
|
_setDeliveryLogLineCount(name, lineCount);
|
|
224
|
-
|
|
242
|
+
// Track the RETAINED (post-prune) distinct count, not the raw file count:
|
|
243
|
+
// a pre-existing oversized log then trips the compaction trigger promptly
|
|
244
|
+
// instead of inflating the threshold until it grows even larger.
|
|
245
|
+
_deliveryKeptCountByEndpoint.set(name, byId.size);
|
|
225
246
|
}
|
|
226
247
|
function _ensureDeliveryIndex(name) {
|
|
227
248
|
if (_deliveryIndexWarmed.has(name)) return;
|
|
@@ -245,14 +266,10 @@ function _compactDeliveriesLogIfNeeded(name) {
|
|
|
245
266
|
if (_deliveryLogLineCount(name) <= threshold) return;
|
|
246
267
|
const { byId: merged } = _readDeliveriesFileMerged(name);
|
|
247
268
|
const rows = [...merged.values()];
|
|
248
|
-
const
|
|
249
|
-
const nonBlocking = rows.filter((e) => !_isBlockingDeliveryStatus(e.status));
|
|
250
|
-
nonBlocking.sort((a, b) => String(b.ts || "").localeCompare(String(a.ts || "")));
|
|
269
|
+
const keepIds = _retainedDeliveryIds(rows);
|
|
251
270
|
const keep = new Map();
|
|
252
|
-
for (const e of
|
|
253
|
-
|
|
254
|
-
if (keep.size >= DELIVERY_INDEX_MAX_IDS) break;
|
|
255
|
-
if (!keep.has(e.id)) keep.set(e.id, e);
|
|
271
|
+
for (const e of rows) {
|
|
272
|
+
if (keepIds.has(e.id)) keep.set(e.id, e);
|
|
256
273
|
}
|
|
257
274
|
const lines = [...keep.values()]
|
|
258
275
|
.sort((a, b) => String(a.ts || "").localeCompare(String(b.ts || "")))
|
package/src/memory/index.mjs
CHANGED
|
@@ -996,6 +996,10 @@ async function _finalizeCycle2Run(result) {
|
|
|
996
996
|
}
|
|
997
997
|
|
|
998
998
|
async function checkCycles() {
|
|
999
|
+
// Poll-on-use: re-read memory config each tick so changed enabled/interval
|
|
1000
|
+
// values apply without a restart (mirrors search/cwd poll-on-use). The fixed
|
|
1001
|
+
// 60s poll bounds latency; manual `memory` tool calls already re-read per-call.
|
|
1002
|
+
mainConfig = readMainConfig();
|
|
999
1003
|
if (mainConfig?.enabled === false) return
|
|
1000
1004
|
|
|
1001
1005
|
const cycle1Ms = parseInterval(mainConfig?.cycle1?.interval || '10m')
|
|
@@ -2827,7 +2831,7 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
2827
2831
|
}
|
|
2828
2832
|
const chosen = windows.slice(0, sets)
|
|
2829
2833
|
|
|
2830
|
-
const preset = resolveMaintenancePreset('
|
|
2834
|
+
const preset = resolveMaintenancePreset('memory')
|
|
2831
2835
|
|
|
2832
2836
|
function summariseChunks(chunks, totalEntries) {
|
|
2833
2837
|
const usedIdx = new Set()
|
|
@@ -167,7 +167,7 @@ async function _llmJudgeMerge(existing, incoming) {
|
|
|
167
167
|
role: 'cycle2-agent',
|
|
168
168
|
taskType: 'maintenance',
|
|
169
169
|
mode: 'core-merge-judge',
|
|
170
|
-
preset: resolveMaintenancePreset('
|
|
170
|
+
preset: resolveMaintenancePreset('memory'),
|
|
171
171
|
timeout: 30_000,
|
|
172
172
|
cwd: null,
|
|
173
173
|
}, prompt)
|
|
@@ -268,7 +268,7 @@ async function _runCycle1Impl(db, config = {}, options = {}, dataDir = null) {
|
|
|
268
268
|
// Fallback chain handles flat config + nested cycle1 wrap shapes.
|
|
269
269
|
const minBatch = Math.max(1, Number(config?.min_batch ?? config?.cycle1?.min_batch ?? CYCLE1_MIN_BATCH))
|
|
270
270
|
const sessionCap = Math.max(1, Number(config?.session_cap ?? config?.cycle1?.session_cap ?? CYCLE1_SESSION_CAP))
|
|
271
|
-
const preset = options.preset || resolveMaintenancePreset('
|
|
271
|
+
const preset = options.preset || resolveMaintenancePreset('memory')
|
|
272
272
|
// Inner LLM timeout aligns to caller deadline -1s so the channel side can ack gracefully.
|
|
273
273
|
const callerDeadlineMs = Number(options.callerDeadlineMs ?? 0)
|
|
274
274
|
const baseTimeout = Number(config?.timeout ?? config?.cycle1?.timeout ?? 180000)
|
|
@@ -640,7 +640,7 @@ export async function runUnifiedGate(db, rows, activeContext, config = {}, optio
|
|
|
640
640
|
.replace('{{ACTIVE_COUNT}}', String(activeCount))
|
|
641
641
|
.replace('{{ACTIVE_CAP}}', String(activeCap))
|
|
642
642
|
|
|
643
|
-
const preset = options.preset || resolveMaintenancePreset('
|
|
643
|
+
const preset = options.preset || resolveMaintenancePreset('memory')
|
|
644
644
|
const timeout = Number(config?.cycle2?.timeout ?? 600000)
|
|
645
645
|
const mode = 'cycle2-unified'
|
|
646
646
|
|
|
@@ -318,7 +318,7 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
|
|
|
318
318
|
.replace('{{CORE_REVIEW}}', coreReview)
|
|
319
319
|
.replace('{{CURRENT_RULES}}', rulesDigest)
|
|
320
320
|
|
|
321
|
-
const preset = resolveMaintenancePreset('
|
|
321
|
+
const preset = resolveMaintenancePreset('memory')
|
|
322
322
|
const timeout = Number(config?.cycle3?.timeout ?? 600000)
|
|
323
323
|
const mode = 'cycle3-review'
|
|
324
324
|
|
package/tools.json
CHANGED
|
@@ -697,7 +697,7 @@
|
|
|
697
697
|
"idempotentHint": true,
|
|
698
698
|
"openWorldHint": false
|
|
699
699
|
},
|
|
700
|
-
"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
|
|
700
|
+
"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.",
|
|
701
701
|
"inputSchema": {
|
|
702
702
|
"type": "object",
|
|
703
703
|
"properties": {
|
|
@@ -1459,7 +1459,7 @@
|
|
|
1459
1459
|
"openWorldHint": true,
|
|
1460
1460
|
"compressible": true
|
|
1461
1461
|
},
|
|
1462
|
-
"description": "Shell for git/build/test/run.
|
|
1462
|
+
"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.",
|
|
1463
1463
|
"inputSchema": {
|
|
1464
1464
|
"type": "object",
|
|
1465
1465
|
"properties": {
|