mixdog 0.9.37 → 0.9.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -4
- package/scripts/abort-recovery-test.mjs +43 -2
- package/scripts/agent-tag-reuse-smoke.mjs +146 -6
- package/scripts/agent-terminal-reap-test.mjs +127 -0
- package/scripts/agent-trace-io-test.mjs +69 -0
- package/scripts/code-graph-disk-hit-test.mjs +224 -0
- package/scripts/dispatch-persist-recovery-test.mjs +141 -0
- package/scripts/execution-completion-dedup-test.mjs +157 -0
- package/scripts/execution-pending-resume-kick-test.mjs +57 -2
- package/scripts/execution-resume-esc-integration-test.mjs +174 -0
- package/scripts/explore-bench.mjs +101 -8
- package/scripts/explore-prompt-policy-test.mjs +156 -11
- package/scripts/find-fuzzy-hidden-test.mjs +122 -0
- package/scripts/internal-comms-bench-test.mjs +226 -0
- package/scripts/internal-comms-bench.mjs +185 -58
- package/scripts/internal-comms-smoke.mjs +171 -23
- package/scripts/live-worker-smoke.mjs +38 -2
- package/scripts/memory-cycle-routing-test.mjs +111 -0
- package/scripts/memory-rule-contract-test.mjs +93 -0
- package/scripts/notify-completion-mirror-test.mjs +73 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/rg-runner-test.mjs +240 -0
- package/scripts/routing-corpus-test.mjs +349 -0
- package/scripts/routing-corpus.mjs +211 -32
- package/scripts/session-orphan-sweep-test.mjs +83 -0
- package/scripts/session-sweep.mjs +266 -0
- package/scripts/steering-drain-buckets-test.mjs +179 -0
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/src/agents/heavy-worker/AGENT.md +10 -7
- package/src/agents/reviewer/AGENT.md +6 -4
- package/src/agents/worker/AGENT.md +7 -5
- package/src/rules/agent/00-common.md +4 -4
- package/src/rules/agent/00-core.md +11 -14
- package/src/rules/agent/20-skip-protocol.md +3 -3
- package/src/rules/agent/30-explorer.md +56 -48
- package/src/rules/agent/40-cycle1-agent.md +15 -24
- package/src/rules/agent/41-cycle2-agent.md +33 -57
- package/src/rules/agent/42-cycle3-agent.md +28 -42
- package/src/rules/lead/01-general.md +7 -10
- package/src/rules/lead/lead-brief.md +11 -14
- package/src/rules/lead/lead-tool.md +6 -5
- package/src/rules/shared/01-tool.md +44 -41
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +37 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +18 -52
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +41 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +339 -63
- package/src/runtime/agent/orchestrator/stall-policy.mjs +37 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
- package/src/runtime/shared/tool-primitives.mjs +4 -1
- package/src/runtime/shared/tool-status.mjs +27 -0
- package/src/runtime/shared/tool-surface.mjs +6 -3
- package/src/session-runtime/config-helpers.mjs +14 -0
- package/src/session-runtime/context-status.mjs +1 -0
- package/src/session-runtime/effort.mjs +6 -2
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/provider-models.mjs +3 -3
- package/src/session-runtime/runtime-core.mjs +78 -10
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/session-runtime/warmup-schedulers.mjs +7 -1
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +45 -69
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +31 -0
- package/src/tui/app/model-options.mjs +5 -3
- package/src/tui/app/model-picker.mjs +12 -24
- package/src/tui/app/transcript-window.mjs +1 -0
- package/src/tui/components/ToolExecution.jsx +11 -6
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
- package/src/tui/components/tool-execution/text-format.mjs +10 -19
- package/src/tui/dist/index.mjs +533 -143
- package/src/tui/engine/agent-job-feed.mjs +153 -16
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/queue-helpers.mjs +8 -0
- package/src/tui/engine/session-api.mjs +8 -2
- package/src/tui/engine/session-flow.mjs +34 -2
- package/src/tui/engine/tool-card-results.mjs +54 -32
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +83 -43
- package/src/tui/engine.mjs +63 -2
- package/src/workflows/bench/WORKFLOW.md +25 -35
- package/src/workflows/default/WORKFLOW.md +38 -32
- package/src/workflows/solo/WORKFLOW.md +19 -22
- package/scripts/_jitter-fuzz.mjs +0 -44410
- package/scripts/_jitter-fuzz2.mjs +0 -44400
- package/scripts/_jitter-probe.mjs +0 -44397
- package/scripts/_jp2.mjs +0 -45614
|
@@ -191,6 +191,43 @@ export const PROVIDER_WS_ACQUIRE_TIMEOUT_MS = resolveTimeoutMs(
|
|
|
191
191
|
{ minMs: 5_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
192
192
|
);
|
|
193
193
|
|
|
194
|
+
// WS pool liveness (ping/pong) — closes the gap between a socket going
|
|
195
|
+
// half-open (peer gone / TLS wedge) and the semantic-idle watchdog noticing
|
|
196
|
+
// (120s). Node `ws` send() is fire-and-forget, so a dead pooled socket
|
|
197
|
+
// silently blackholes response.create frames until a downstream timeout. An
|
|
198
|
+
// idle pooled socket is pinged on this cadence; a reused socket that has been
|
|
199
|
+
// quiet longer than the stale window is ping-probed before hand-out.
|
|
200
|
+
// Gated OFF by default for codex/opencode parity — neither pings model
|
|
201
|
+
// sockets. Enable with MIXDOG_PROVIDER_WS_PING_ENABLED=1. When disabled, no
|
|
202
|
+
// liveness interval is armed and the acquire-reuse ping probe is skipped (the
|
|
203
|
+
// non-OPEN eviction scan still runs); lastAliveAt stamping is harmless.
|
|
204
|
+
const _wsPingRaw = process.env.MIXDOG_PROVIDER_WS_PING_ENABLED;
|
|
205
|
+
export const PROVIDER_WS_PING_ENABLED = _wsPingRaw === '1' || _wsPingRaw === 'true' || _wsPingRaw === 'yes';
|
|
206
|
+
|
|
207
|
+
export const PROVIDER_WS_PING_INTERVAL_MS = resolveTimeoutMs(
|
|
208
|
+
'MIXDOG_PROVIDER_WS_PING_INTERVAL_MS',
|
|
209
|
+
30_000,
|
|
210
|
+
{ minMs: 5_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Missed-pong grace: after a ping, the pong must land within this bound or the
|
|
214
|
+
// socket is treated as dead (closed + evicted). Doubles as the short probe
|
|
215
|
+
// bound on the acquire-reuse path so a busy caller is never handed a wedged
|
|
216
|
+
// socket.
|
|
217
|
+
export const PROVIDER_WS_PONG_TIMEOUT_MS = resolveTimeoutMs(
|
|
218
|
+
'MIXDOG_PROVIDER_WS_PONG_TIMEOUT_MS',
|
|
219
|
+
5_000,
|
|
220
|
+
{ minMs: 1_000, maxMs: 60_000 },
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Activity freshness window: a pooled socket with observed activity (pong /
|
|
224
|
+
// release) newer than this is assumed live and skips the acquire-reuse probe.
|
|
225
|
+
export const PROVIDER_WS_LIVENESS_STALE_MS = resolveTimeoutMs(
|
|
226
|
+
'MIXDOG_PROVIDER_WS_LIVENESS_STALE_MS',
|
|
227
|
+
30_000,
|
|
228
|
+
{ minMs: 5_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
229
|
+
);
|
|
230
|
+
|
|
194
231
|
// Single inter-chunk idle timer (300s), matching the upstream WS provider's
|
|
195
232
|
// default stream idle timeout. Mixdog resets one idle timer on every received
|
|
196
233
|
// WS frame (openai-oauth-ws messageHandler resets on every parsed event). There
|
|
@@ -79,6 +79,11 @@ function clearGrepContextKeys(args, keys) {
|
|
|
79
79
|
/** Lead-facing grep context: canonicalize aliases and clamp explicit values (overrides still apply). */
|
|
80
80
|
export function applyGrepContextLeadPolicy(args) {
|
|
81
81
|
if (!args || typeof args !== 'object' || Array.isArray(args)) return;
|
|
82
|
+
// Idempotent guard: the outer builtin guard (validateBuiltinArgs) and the
|
|
83
|
+
// executor (executeGrepTool) both call this on the SAME args object. The
|
|
84
|
+
// first pass folds aliases onto -A/-B/-C and clamps; a second pass would
|
|
85
|
+
// recompute the same folding for no effect. Skip once applied.
|
|
86
|
+
if (args._grepContextPolicyApplied) return;
|
|
82
87
|
for (const [canonical, keys] of GREP_CONTEXT_KEY_GROUPS) {
|
|
83
88
|
const found = firstNonZeroGrepContextArg(args, keys) || firstGrepContextArg(args, keys);
|
|
84
89
|
if (!found) continue;
|
|
@@ -87,6 +92,12 @@ export function applyGrepContextLeadPolicy(args) {
|
|
|
87
92
|
clearGrepContextKeys(args, keys);
|
|
88
93
|
args[canonical] = shaped;
|
|
89
94
|
}
|
|
95
|
+
// Non-enumerable so it never leaks into arg spreads, cache keys, or output.
|
|
96
|
+
try {
|
|
97
|
+
Object.defineProperty(args, '_grepContextPolicyApplied', {
|
|
98
|
+
value: true, enumerable: false, configurable: true, writable: true,
|
|
99
|
+
});
|
|
100
|
+
} catch { args._grepContextPolicyApplied = true; }
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
function isString(v) {
|
|
@@ -435,7 +446,7 @@ function guardGrep(a) {
|
|
|
435
446
|
const grepMode = a.output_mode || a.mode;
|
|
436
447
|
const hasExplicitCtx = ['-A', '-B', '-C', 'context'].some((k) => grepContextKeyPresent(a, k));
|
|
437
448
|
const isCountOrFiles = grepMode === 'files_with_matches' || grepMode === 'count';
|
|
438
|
-
if (!isCountOrFiles && (grepMode === 'content_with_context' || hasExplicitCtx)) {
|
|
449
|
+
if (!isCountOrFiles && (grepMode == null || grepMode === 'content_with_context' || hasExplicitCtx)) {
|
|
439
450
|
const hl = Number(a.head_limit);
|
|
440
451
|
if (grepContextKeyPresent(a, 'head_limit') && Number.isFinite(hl) && hl > GREP_CTX_HEAD_LIMIT_MAX) {
|
|
441
452
|
a.head_limit = GREP_CTX_HEAD_LIMIT_MAX;
|
|
@@ -39,7 +39,7 @@ export const BUILTIN_TOOLS = [
|
|
|
39
39
|
name: 'read',
|
|
40
40
|
title: 'Mixdog Read',
|
|
41
41
|
annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
42
|
-
description: 'Read verified file path(s)
|
|
42
|
+
description: 'Read verified file path(s); guessed path/name → find first. Array-first: batch paths/regions as real arrays in one call; put targets in path[] or {path,offset,limit}[] and read the whole logical unit. Content_with_context is actionable; do not page or re-read returned spans. Not for directory listing.',
|
|
43
43
|
inputSchema: {
|
|
44
44
|
type: 'object',
|
|
45
45
|
properties: {
|
|
@@ -58,29 +58,31 @@ export const BUILTIN_TOOLS = [
|
|
|
58
58
|
offset: { type: 'number', minimum: 0 },
|
|
59
59
|
limit: { type: 'number', minimum: 1 },
|
|
60
60
|
},
|
|
61
|
+
required: ['path'],
|
|
61
62
|
},
|
|
62
63
|
],
|
|
63
64
|
},
|
|
64
65
|
minItems: 1,
|
|
65
66
|
},
|
|
66
67
|
],
|
|
67
|
-
description: 'Verified file path
|
|
68
|
+
description: 'Verified file path; use {path,offset,limit}[] targets. Pass arrays directly; JSON strings are legacy recovery only.',
|
|
68
69
|
},
|
|
69
|
-
offset: { type: 'number', minimum: 0, description: 'Numeric lines to skip
|
|
70
|
-
limit: { type: 'number', minimum: 1, description: 'Numeric max lines
|
|
70
|
+
offset: { type: 'number', minimum: 0, description: 'Numeric lines to skip; 0 starts at line 1. Continue with offset:N.' },
|
|
71
|
+
limit: { type: 'number', minimum: 1, description: 'Numeric max lines after offset.' },
|
|
71
72
|
},
|
|
73
|
+
required: ['path'],
|
|
72
74
|
},
|
|
73
75
|
},
|
|
74
76
|
{
|
|
75
77
|
name: 'shell',
|
|
76
78
|
title: 'Mixdog Shell',
|
|
77
79
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
78
|
-
description: `Run programs or change system state
|
|
80
|
+
description: `Run programs or change system state; set shell: powershell or bash. Not for reading, listing or searching. Shell/write calls are serial; use ;/&& only for intentional in-command sequencing.${_shellSyntaxCheat} ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
|
|
79
81
|
inputSchema: {
|
|
80
82
|
type: 'object',
|
|
81
83
|
properties: {
|
|
82
84
|
command: { type: 'string', description: 'Command.' },
|
|
83
|
-
cwd: { type: 'string', description: 'Working directory
|
|
85
|
+
cwd: { type: 'string', description: 'Working directory; persists across calls. Omit to reuse; absolute path changes it.' },
|
|
84
86
|
timeout: { type: 'number', description: `Timeout ms. Default ${_shellDefaultTimeoutMs()} (${_shellDefaultTimeoutMs() / 60000} min) when omitted; an explicit value is honored uncapped — long-running commands may set it higher. On timeout the command is MOVED TO BACKGROUND (a task_id you can wait/status/read/cancel) and keeps running instead of being killed; sleep-like commands and MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS opt out (killed with a [timeout] marker). async/background runs with timeout omitted have NO timeout (run until done/cancelled); an explicit timeout is still enforced.` },
|
|
85
87
|
merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
|
|
86
88
|
mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
|
|
@@ -93,12 +95,12 @@ export const BUILTIN_TOOLS = [
|
|
|
93
95
|
name: 'task',
|
|
94
96
|
title: 'Background Task Control',
|
|
95
97
|
annotations: { title: 'Background Task Control', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
96
|
-
description: `
|
|
98
|
+
description: `Control a shell async task_id: actions list/status/read/wait/cancel. ${TOOL_MANUAL_CONTROL_CONTRACT} Not sess_* or agent ids.`,
|
|
97
99
|
inputSchema: {
|
|
98
100
|
type: 'object',
|
|
99
101
|
properties: {
|
|
100
102
|
task_id: { type: 'string', description: 'shell async task_id.' },
|
|
101
|
-
action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: '
|
|
103
|
+
action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Action; avoid polling loops.' },
|
|
102
104
|
timeout_ms: { type: 'number', description: 'Wait timeout ms.' },
|
|
103
105
|
poll_ms: { type: 'number', description: 'Wait poll ms.' },
|
|
104
106
|
},
|
|
@@ -109,7 +111,7 @@ export const BUILTIN_TOOLS = [
|
|
|
109
111
|
name: 'grep',
|
|
110
112
|
title: 'Mixdog Grep',
|
|
111
113
|
annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
112
|
-
description: '
|
|
114
|
+
description: 'Quoted/non-identifier literal or regex→grep. Search in verified scope (project root counts as verified); guessed path fragment → find first. Omitted output_mode and content_with_context are contextual; a nonzero content_with_context result resolves that search concept—act directly, without regex tweaks, narrowing or re-search. Choose files_with_matches/count for existence; Only zero/error results may change tokens or scope. No path "." + guessed src/**.',
|
|
113
115
|
inputSchema: {
|
|
114
116
|
type: 'object',
|
|
115
117
|
properties: {
|
|
@@ -118,14 +120,14 @@ export const BUILTIN_TOOLS = [
|
|
|
118
120
|
{ type: 'string' },
|
|
119
121
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
120
122
|
],
|
|
121
|
-
description: 'Text/regex. Array = variants in one call.',
|
|
123
|
+
description: 'Text/regex. Array = variants in one call; pattern[] batches tokens. path[] batches verified scopes only; file/span reads use read path[] regions.',
|
|
122
124
|
},
|
|
123
125
|
path: {
|
|
124
126
|
anyOf: [
|
|
125
127
|
{ type: 'string' },
|
|
126
128
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
127
129
|
],
|
|
128
|
-
description: 'Verified file/dir
|
|
130
|
+
description: 'Verified file/dir or project root; guessed → find first.',
|
|
129
131
|
},
|
|
130
132
|
glob: {
|
|
131
133
|
anyOf: [
|
|
@@ -134,21 +136,24 @@ export const BUILTIN_TOOLS = [
|
|
|
134
136
|
],
|
|
135
137
|
description: 'Glob filter; no guessed src/** under path ".".',
|
|
136
138
|
},
|
|
137
|
-
output_mode: { type: 'string', enum: ['content_with_context', '
|
|
139
|
+
output_mode: { type: 'string', enum: ['content_with_context', 'files_with_matches', 'count'], description: 'Omitted/contextual content; content_with_context; files_with_matches/count are existence modes. Legacy content is runtime-compatible, not public.' },
|
|
138
140
|
head_limit: { type: 'number', minimum: 0, description: 'Max results.' },
|
|
139
141
|
offset: { type: 'number', minimum: 0, description: 'Skip results for paging.' },
|
|
140
142
|
'-A': { type: 'number', minimum: 0, description: 'Lines after each match.' },
|
|
141
143
|
'-B': { type: 'number', minimum: 0, description: 'Lines before each match.' },
|
|
142
144
|
'-C': { type: 'number', minimum: 0, description: 'Lines before/after each match.' },
|
|
143
145
|
},
|
|
144
|
-
|
|
146
|
+
anyOf: [
|
|
147
|
+
{ required: ['pattern'] },
|
|
148
|
+
{ required: ['glob'] },
|
|
149
|
+
],
|
|
145
150
|
},
|
|
146
151
|
},
|
|
147
152
|
{
|
|
148
153
|
name: 'glob',
|
|
149
154
|
title: 'Mixdog Glob',
|
|
150
155
|
annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
151
|
-
description: 'Exact glob from verified roots
|
|
156
|
+
description: 'Exact glob from verified roots (project root is verified); guessed root/name → find first. Array-first: batch related pattern[]/path[]. No path "." + guessed src/**.',
|
|
152
157
|
inputSchema: {
|
|
153
158
|
type: 'object',
|
|
154
159
|
properties: {
|
|
@@ -157,14 +162,14 @@ export const BUILTIN_TOOLS = [
|
|
|
157
162
|
{ type: 'string' },
|
|
158
163
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
159
164
|
],
|
|
160
|
-
description: 'Exact glob pattern(s)
|
|
165
|
+
description: 'Exact glob pattern(s); batch related pattern[].',
|
|
161
166
|
},
|
|
162
167
|
path: {
|
|
163
168
|
anyOf: [
|
|
164
169
|
{ type: 'string' },
|
|
165
170
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
166
171
|
],
|
|
167
|
-
description: 'Verified base dir(s);
|
|
172
|
+
description: 'Verified base dir(s)/project root; guessed → find first; batch path[].',
|
|
168
173
|
},
|
|
169
174
|
head_limit: { type: 'number', description: 'Max entries.' },
|
|
170
175
|
offset: { type: 'number', description: 'Skip entries.' },
|
|
@@ -176,7 +181,7 @@ export const BUILTIN_TOOLS = [
|
|
|
176
181
|
name: 'find',
|
|
177
182
|
title: 'Mixdog Find Files',
|
|
178
183
|
annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
179
|
-
description: 'Partial path/name lookup
|
|
184
|
+
description: 'Partial path/name lookup only for unknown partial paths/names (including dot dirs); verify roots before grep/glob. Not for the project root or already-verified roots. Output paths are verified downstream. Array-first: batch query[].',
|
|
180
185
|
inputSchema: {
|
|
181
186
|
type: 'object',
|
|
182
187
|
properties: {
|
|
@@ -185,7 +190,7 @@ export const BUILTIN_TOOLS = [
|
|
|
185
190
|
{ type: 'string' },
|
|
186
191
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
187
192
|
],
|
|
188
|
-
description: 'Partial path/name words
|
|
193
|
+
description: 'Partial path/name words, not contents; query[] batches lookups.',
|
|
189
194
|
},
|
|
190
195
|
path: { type: 'string', description: 'Base directory.' },
|
|
191
196
|
head_limit: { type: 'number', description: 'Max paths.' },
|
|
@@ -197,7 +202,7 @@ export const BUILTIN_TOOLS = [
|
|
|
197
202
|
name: 'list',
|
|
198
203
|
title: 'Mixdog List Directory',
|
|
199
204
|
annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
200
|
-
description: 'List verified directories
|
|
205
|
+
description: 'List verified directories (project root included); Guessed dir → find first. Array-first: batch independent dirs as path[].',
|
|
201
206
|
inputSchema: {
|
|
202
207
|
type: 'object',
|
|
203
208
|
properties: {
|
|
@@ -206,7 +211,7 @@ export const BUILTIN_TOOLS = [
|
|
|
206
211
|
{ type: 'string' },
|
|
207
212
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
208
213
|
],
|
|
209
|
-
description: 'Verified directory
|
|
214
|
+
description: 'Verified directory; path[] batches directories.',
|
|
210
215
|
},
|
|
211
216
|
head_limit: { type: 'number', description: 'Max entries.' },
|
|
212
217
|
offset: { type: 'number', description: 'Skip N entries for paging.' },
|
|
@@ -19,10 +19,13 @@ export const FS_REACHABILITY_DEADLINE_MS = 5000;
|
|
|
19
19
|
// error). Reject with EFSUNREACHABLE only when the stat itself exceeds the
|
|
20
20
|
// deadline, which is the dead-mount / hung-FS signature.
|
|
21
21
|
export async function assertPathReachable(path, deadlineMs = FS_REACHABILITY_DEADLINE_MS) {
|
|
22
|
-
if (typeof path !== 'string' || path.length === 0) return;
|
|
22
|
+
if (typeof path !== 'string' || path.length === 0) return null;
|
|
23
23
|
const ms = Number(deadlineMs) > 0 ? Number(deadlineMs) : FS_REACHABILITY_DEADLINE_MS;
|
|
24
24
|
let timer = null;
|
|
25
|
-
|
|
25
|
+
// Resolve to the Stats on success so a caller can seed its stat cache and
|
|
26
|
+
// avoid an immediate redundant synchronous re-stat of the same path; a
|
|
27
|
+
// clean FS rejection (ENOENT/EACCES) resolves to null (still "reachable").
|
|
28
|
+
const probe = stat(path).then((s) => s, () => null);
|
|
26
29
|
const deadline = new Promise((resolve) => {
|
|
27
30
|
timer = setTimeout(() => resolve('TIMEOUT'), ms);
|
|
28
31
|
});
|
|
@@ -37,6 +40,7 @@ export async function assertPathReachable(path, deadlineMs = FS_REACHABILITY_DEA
|
|
|
37
40
|
err.code = 'EFSUNREACHABLE';
|
|
38
41
|
throw err;
|
|
39
42
|
}
|
|
43
|
+
return result; // Stats on success, null on a clean FS rejection
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
// Batch variant: reject if ANY path is unreachable. Runs probes concurrently so
|
|
@@ -25,62 +25,69 @@ function isHump(prevCh, ch) {
|
|
|
25
25
|
&& /[A-Z]/.test(ch);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
* @param {string} query user query (partial name)
|
|
30
|
-
* @param {string} str candidate path (relative)
|
|
31
|
-
* @returns {number|null} score, or null if `query` is not a subsequence
|
|
32
|
-
*/
|
|
33
|
-
export function fuzzyScore(query, str) {
|
|
28
|
+
function prepareFuzzyScore(query) {
|
|
34
29
|
if (!query) return 0;
|
|
35
30
|
const normalizedQuery = String(query).replace(/[\/\\_.\-\s]+/g, '');
|
|
36
31
|
if (!normalizedQuery) return 0;
|
|
37
32
|
const q = normalizedQuery.toLowerCase();
|
|
38
|
-
const s = str.toLowerCase();
|
|
39
33
|
const qlen = q.length;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let score = 0;
|
|
47
|
-
let si = 0;
|
|
48
|
-
let prevMatch = -2;
|
|
49
|
-
let firstMatchIdx = -1;
|
|
50
|
-
|
|
51
|
-
for (let qi = 0; qi < qlen; qi++) {
|
|
52
|
-
const qc = q[qi];
|
|
53
|
-
let found = -1;
|
|
54
|
-
for (let k = si; k < slen; k++) {
|
|
55
|
-
if (s[k] === qc) { found = k; break; }
|
|
56
|
-
}
|
|
57
|
-
if (found === -1) return null;
|
|
58
|
-
if (firstMatchIdx === -1) firstMatchIdx = found;
|
|
34
|
+
return (str) => {
|
|
35
|
+
const s = str.toLowerCase();
|
|
36
|
+
const slen = s.length;
|
|
37
|
+
if (qlen === 0) return 0;
|
|
38
|
+
if (qlen > slen) return null;
|
|
59
39
|
|
|
60
|
-
|
|
40
|
+
const lastSep = Math.max(str.lastIndexOf('/'), str.lastIndexOf('\\'));
|
|
61
41
|
|
|
62
|
-
|
|
42
|
+
let score = 0;
|
|
43
|
+
let si = 0;
|
|
44
|
+
let prevMatch = -2;
|
|
45
|
+
let firstMatchIdx = -1;
|
|
63
46
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
for (let qi = 0; qi < qlen; qi++) {
|
|
48
|
+
const qc = q[qi];
|
|
49
|
+
let found = -1;
|
|
50
|
+
for (let k = si; k < slen; k++) {
|
|
51
|
+
if (s[k] === qc) { found = k; break; }
|
|
52
|
+
}
|
|
53
|
+
if (found === -1) return null;
|
|
54
|
+
if (firstMatchIdx === -1) firstMatchIdx = found;
|
|
68
55
|
|
|
69
|
-
|
|
56
|
+
score += 1; // base point per matched char
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
si = found + 1;
|
|
73
|
-
}
|
|
58
|
+
if (found === prevMatch + 1) score += 5; // contiguous run
|
|
74
59
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
60
|
+
const prevCh = found > 0 ? str[found - 1] : undefined;
|
|
61
|
+
if (prevCh === undefined || isBoundaryChar(prevCh) || isHump(prevCh, str[found])) {
|
|
62
|
+
score += 8; // word-boundary start
|
|
63
|
+
}
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
score -= Math.floor(slen / 16);
|
|
81
|
-
score -= Math.floor(firstMatchIdx / 8);
|
|
65
|
+
if (str[found] === normalizedQuery[qi]) score += 1; // exact-case tie-break
|
|
82
66
|
|
|
83
|
-
|
|
67
|
+
prevMatch = found;
|
|
68
|
+
si = found + 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Matches that begin inside the basename (after the last separator) are far
|
|
72
|
+
// more relevant than ones buried in directory components.
|
|
73
|
+
if (firstMatchIdx > lastSep) score += 10;
|
|
74
|
+
|
|
75
|
+
// Mild pulls: shorter candidates and earlier first matches rank higher.
|
|
76
|
+
score -= Math.floor(slen / 16);
|
|
77
|
+
score -= Math.floor(firstMatchIdx / 8);
|
|
78
|
+
|
|
79
|
+
return score;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} query user query (partial name)
|
|
85
|
+
* @param {string} str candidate path (relative)
|
|
86
|
+
* @returns {number|null} score, or null if `query` is not a subsequence
|
|
87
|
+
*/
|
|
88
|
+
export function fuzzyScore(query, str) {
|
|
89
|
+
const scorer = prepareFuzzyScore(query);
|
|
90
|
+
return typeof scorer === 'function' ? scorer(str) : scorer;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
// Below this per-query-char score, a subsequence-only match (query chars in
|
|
@@ -97,6 +104,53 @@ function normalizeForContains(s) {
|
|
|
97
104
|
return String(s || '').toLowerCase().replace(/[\/\\_.\-\s]+/g, '');
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
function compareRanked(a, b) {
|
|
108
|
+
return (b.score - a.score)
|
|
109
|
+
|| (a.item.path < b.item.path ? -1 : a.item.path > b.item.path ? 1 : 0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function compareRankedNodes(a, b) {
|
|
113
|
+
return compareRanked(a.entry, b.entry) || (a.ordinal - b.ordinal);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function siftDownRankedHeap(heap, index) {
|
|
117
|
+
for (;;) {
|
|
118
|
+
const left = index * 2 + 1;
|
|
119
|
+
const right = left + 1;
|
|
120
|
+
let worst = index;
|
|
121
|
+
if (left < heap.length && compareRankedNodes(heap[left], heap[worst]) > 0) worst = left;
|
|
122
|
+
if (right < heap.length && compareRankedNodes(heap[right], heap[worst]) > 0) worst = right;
|
|
123
|
+
if (worst === index) return;
|
|
124
|
+
const node = heap[index];
|
|
125
|
+
heap[index] = heap[worst];
|
|
126
|
+
heap[worst] = node;
|
|
127
|
+
index = worst;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Keep the worst retained candidate at index zero. The ordinal completes the
|
|
132
|
+
// public comparator only for heap bookkeeping, preserving stable-sort behavior
|
|
133
|
+
// for duplicate score/path entries.
|
|
134
|
+
function retainTopRanked(heap, entry, ordinal, limit) {
|
|
135
|
+
const node = { entry, ordinal };
|
|
136
|
+
if (heap.length < limit) {
|
|
137
|
+
heap.push(node);
|
|
138
|
+
for (let index = heap.length - 1; index > 0;) {
|
|
139
|
+
const parent = Math.floor((index - 1) / 2);
|
|
140
|
+
if (compareRankedNodes(heap[index], heap[parent]) <= 0) break;
|
|
141
|
+
const parentNode = heap[parent];
|
|
142
|
+
heap[parent] = heap[index];
|
|
143
|
+
heap[index] = parentNode;
|
|
144
|
+
index = parent;
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (compareRankedNodes(node, heap[0]) < 0) {
|
|
149
|
+
heap[0] = node;
|
|
150
|
+
siftDownRankedHeap(heap, 0);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
100
154
|
/**
|
|
101
155
|
* Rank candidates by fuzzy score against `query`, dropping non-matches.
|
|
102
156
|
* @param {string} query
|
|
@@ -106,30 +160,41 @@ function normalizeForContains(s) {
|
|
|
106
160
|
*/
|
|
107
161
|
export function fuzzyRank(query, items, limit = 0) {
|
|
108
162
|
const normQuery = normalizeForContains(query);
|
|
163
|
+
const score = prepareFuzzyScore(query);
|
|
109
164
|
// Floor scales with query length: a scattered subsequence earns ~1 point
|
|
110
165
|
// per char, while any contiguous run (+5/char) or word-boundary hit
|
|
111
166
|
// (+8) pushes a genuine match well past 4/char.
|
|
112
167
|
const floor = normQuery.length * SUBSEQUENCE_MIN_PER_CHAR;
|
|
113
168
|
const scored = [];
|
|
114
|
-
|
|
169
|
+
// Preserve slice's legacy coercion behavior for non-integer public callers;
|
|
170
|
+
// normal tool limits are positive integers and take the bounded fast path.
|
|
171
|
+
const bounded = Number.isInteger(limit) && limit > 0;
|
|
172
|
+
for (let ordinal = 0; ordinal < items.length; ordinal++) {
|
|
173
|
+
const item = items[ordinal];
|
|
115
174
|
const p = String(item.path || '');
|
|
116
|
-
const
|
|
117
|
-
const base = p.
|
|
118
|
-
const
|
|
175
|
+
const lastSep = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
|
|
176
|
+
const base = p.slice(lastSep + 1);
|
|
177
|
+
const pathScore = typeof score === 'function' ? score(p) : score;
|
|
178
|
+
const baseScore = typeof score === 'function' ? score(base) : score;
|
|
119
179
|
const sc = Math.max(pathScore ?? -Infinity, baseScore === null ? -Infinity : baseScore + 40);
|
|
120
180
|
if (!Number.isFinite(sc)) continue;
|
|
121
181
|
// Strong match: the query (separators stripped) is a contiguous
|
|
122
182
|
// substring of the basename or the full path. These ALWAYS pass so an
|
|
123
183
|
// exact substring/basename hit can never be starved out as noise.
|
|
124
|
-
const strong = normQuery.length > 0
|
|
125
|
-
&& (normalizeForContains(base).includes(normQuery)
|
|
126
|
-
|| normalizeForContains(p).includes(normQuery));
|
|
184
|
+
const strong = normQuery.length > 0 && normalizeForContains(p).includes(normQuery);
|
|
127
185
|
// Otherwise it is subsequence-only: keep it only if it clears the
|
|
128
186
|
// per-char floor. Weak scattered matches (the pgAdmin-style junk that
|
|
129
187
|
// merely contains the query chars in order) fall below it and drop out.
|
|
130
188
|
if (!strong && sc < floor) continue;
|
|
131
|
-
|
|
189
|
+
const entry = { item, score: sc };
|
|
190
|
+
if (bounded) retainTopRanked(scored, entry, ordinal, limit);
|
|
191
|
+
else scored.push(entry);
|
|
192
|
+
}
|
|
193
|
+
if (!bounded) {
|
|
194
|
+
scored.sort(compareRanked);
|
|
195
|
+
return limit > 0 ? scored.slice(0, limit) : scored;
|
|
132
196
|
}
|
|
133
|
-
scored
|
|
134
|
-
|
|
197
|
+
return scored
|
|
198
|
+
.sort(compareRankedNodes)
|
|
199
|
+
.map(({ entry }) => entry);
|
|
135
200
|
}
|