mixdog 0.9.115 → 0.9.117
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 +1 -1
- package/scripts/tool-stress.mjs +143 -0
- package/src/cli.mjs +1 -0
- package/src/rules/shared/01-tool.md +24 -20
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +24 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +54 -5
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +13 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +19 -9
- package/src/runtime/channels/lib/tool-dispatch.mjs +0 -26
- package/src/runtime/channels/lib/worker-main.mjs +0 -18
- package/src/runtime/channels/tool-defs.mjs +3 -45
- package/src/runtime/shared/child-spawn-gate.mjs +16 -5
- package/src/runtime/shared/tool-result-summary.mjs +0 -1
- package/src/runtime/shared/tool-surface.mjs +0 -7
- package/src/runtime/shared/uv-threadpool-boot.mjs +13 -0
- package/src/session-runtime/runtime-core.mjs +1 -0
- package/src/session-runtime/tool-catalog-data.mjs +1 -3
- package/src/session-runtime/warmup-schedulers.mjs +8 -0
- package/src/session-runtime/workflow.mjs +13 -1
- package/src/standalone/channel-worker.mjs +0 -2
- package/src/standalone/daemon.mjs +3 -0
- package/src/tui/dist/index.mjs +0 -8
- package/src/tui/session/session-api.mjs +74 -10
- package/src/runtime/channels/lib/provider-dispatch.mjs +0 -61
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Tool-layer burst stress: hammers the in-process tool execution surface with
|
|
2
|
+
// concurrent multi-session waves and checks that results stay correct and
|
|
3
|
+
// bounded under load (no unhandled rejections, no resource-pressure failures,
|
|
4
|
+
// patch/read integrity preserved, cancellation clean). One-shot script:
|
|
5
|
+
// `node scripts/tool-stress.mjs`. Exit 0 = stable, 1 = instability found.
|
|
6
|
+
import '../src/runtime/shared/uv-threadpool-boot.mjs';
|
|
7
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { executeBuiltinTool } from '../src/runtime/agent/orchestrator/tools/builtin.mjs';
|
|
12
|
+
import { executeCodeGraphTool } from '../src/runtime/agent/orchestrator/tools/code-graph.mjs';
|
|
13
|
+
import { executePatchTool } from '../src/runtime/agent/orchestrator/tools/patch.mjs';
|
|
14
|
+
|
|
15
|
+
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
|
16
|
+
const SESSIONS = 8;
|
|
17
|
+
const WAVES = 5;
|
|
18
|
+
const stats = new Map(); // tool -> {n, errs:[], lat:[]}
|
|
19
|
+
const failures = [];
|
|
20
|
+
|
|
21
|
+
function record(tool, ms, result, expectRe) {
|
|
22
|
+
let s = stats.get(tool);
|
|
23
|
+
if (!s) { s = { n: 0, errs: [], lat: [] }; stats.set(tool, s); }
|
|
24
|
+
s.n += 1; s.lat.push(ms);
|
|
25
|
+
const text = String(result ?? '');
|
|
26
|
+
if (/^Error:|resource pressure|ERESOURCEPRESSURE|EAGAIN/i.test(text)) s.errs.push(text.slice(0, 160));
|
|
27
|
+
else if (expectRe && !expectRe.test(text)) s.errs.push(`unexpected output: ${text.slice(0, 120)}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function timed(tool, expectRe, fn) {
|
|
31
|
+
const t0 = Date.now();
|
|
32
|
+
try {
|
|
33
|
+
const out = await fn();
|
|
34
|
+
record(tool, Date.now() - t0, out, expectRe);
|
|
35
|
+
return out;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
record(tool, Date.now() - t0, `Error: thrown ${err?.message || err}`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pct(list, p) {
|
|
43
|
+
if (!list.length) return 0;
|
|
44
|
+
const sorted = [...list].sort((a, b) => a - b);
|
|
45
|
+
return sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tmp = mkdtempSync(join(tmpdir(), 'mixdog-tool-stress-'));
|
|
49
|
+
const t0 = Date.now();
|
|
50
|
+
try {
|
|
51
|
+
// ── Phase A+C: concurrent multi-session waves (search/read/graph/shell +
|
|
52
|
+
// per-session patch integrity riding the same load) ──────────────────────
|
|
53
|
+
for (let wave = 0; wave < WAVES; wave++) {
|
|
54
|
+
const calls = [];
|
|
55
|
+
for (let s = 0; s < SESSIONS; s++) {
|
|
56
|
+
const opts = { sessionId: `stress-s${s}` };
|
|
57
|
+
const marker = `stress_w${wave}_s${s}`;
|
|
58
|
+
calls.push(
|
|
59
|
+
timed('grep', /path-string|paths only|grep|\(no matches\)|Fuzzy/i, () => executeBuiltinTool('grep', {
|
|
60
|
+
pattern: ['Fuzzy filename', 'paths only'], path: 'src/runtime/agent/orchestrator/tools/builtin', glob: '*.mjs', limit: 20, context: 0,
|
|
61
|
+
}, root, opts)),
|
|
62
|
+
timed('glob', /tool-defs\.mjs|\.mjs/, () => executeBuiltinTool('glob', {
|
|
63
|
+
pattern: '**/*.mjs', path: 'src/session-runtime', limit: 40,
|
|
64
|
+
}, root, opts)),
|
|
65
|
+
timed('find', /tool-defs|no fuzzy match/, () => executeBuiltinTool('find', {
|
|
66
|
+
query: 'tool-defs', limit: 8,
|
|
67
|
+
}, root, opts)),
|
|
68
|
+
timed('list', /01-tool\.md|file/, () => executeBuiltinTool('list', {
|
|
69
|
+
path: 'src/rules/shared',
|
|
70
|
+
}, root, opts)),
|
|
71
|
+
timed('read', /Tool Use|read/, () => executeBuiltinTool('read', {
|
|
72
|
+
path: [['src/rules/shared/01-tool.md', 0, 10], ['package.json', 0, 5]],
|
|
73
|
+
}, root, opts)),
|
|
74
|
+
timed('code_graph', /symbol|binding|files|edges/i, () => executeCodeGraphTool('code_graph', {
|
|
75
|
+
mode: 'symbols', files: 'scripts/smoke.mjs',
|
|
76
|
+
}, root)),
|
|
77
|
+
timed('shell', /55350/, () => executeBuiltinTool('shell', {
|
|
78
|
+
command: 'node -e "console.log(123*450)"', timeout: 60_000,
|
|
79
|
+
}, root, opts)),
|
|
80
|
+
(async () => {
|
|
81
|
+
const patch = [
|
|
82
|
+
'*** Begin Patch',
|
|
83
|
+
`*** Add File: ${marker}.txt`,
|
|
84
|
+
`+payload ${marker}`,
|
|
85
|
+
'*** End Patch',
|
|
86
|
+
].join('\n');
|
|
87
|
+
await timed('apply_patch', /applied|OK/i, () => executePatchTool('apply_patch', { patch, base_path: tmp }, tmp, opts));
|
|
88
|
+
const back = await timed('read-verify', new RegExp(`payload ${marker}`), () => executeBuiltinTool('read', { path: `${marker}.txt` }, tmp, opts));
|
|
89
|
+
if (!String(back || '').includes(`payload ${marker}`)) failures.push(`patch integrity lost for ${marker}`);
|
|
90
|
+
})(),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
await Promise.all(calls);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Phase B: oversized inputs stay budget-bounded ─────────────────────────
|
|
97
|
+
await Promise.all([
|
|
98
|
+
timed('grep-broad', /Showing|import|export/, () => executeBuiltinTool('grep', {
|
|
99
|
+
pattern: 'import', path: 'src', limit: 300, mode: 'files',
|
|
100
|
+
}, root, { sessionId: 'stress-big' })),
|
|
101
|
+
timed('glob-broad', /\.mjs|entries/, () => executeBuiltinTool('glob', {
|
|
102
|
+
pattern: '**/*', path: 'src/runtime/agent/orchestrator/tools', limit: 0,
|
|
103
|
+
}, root, { sessionId: 'stress-big' })),
|
|
104
|
+
timed('read-big', /./, () => executeBuiltinTool('read', {
|
|
105
|
+
path: 'src/tui/dist/index.mjs', limit: 2000,
|
|
106
|
+
}, root, { sessionId: 'stress-big' })),
|
|
107
|
+
]);
|
|
108
|
+
for (const [tool, s] of stats) {
|
|
109
|
+
if (tool.endsWith('-broad') || tool === 'read-big') {
|
|
110
|
+
const worst = Math.max(...s.lat);
|
|
111
|
+
if (worst > 30_000) failures.push(`${tool} exceeded 30s budget: ${worst}ms`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Phase D: cancellation under load ─────────────────────────────────────
|
|
116
|
+
const bg = await timed('shell-async', /task_id/, () => executeBuiltinTool('shell', {
|
|
117
|
+
command: 'node -e "setTimeout(()=>{}, 30000)"', mode: 'async', timeout: 60_000,
|
|
118
|
+
}, root, { sessionId: 'stress-cancel' }));
|
|
119
|
+
const bgId = (/task_id:\s*(\S+)/.exec(String(bg)) || [])[1];
|
|
120
|
+
if (!bgId) failures.push('async shell did not return task_id');
|
|
121
|
+
else {
|
|
122
|
+
await timed('task-cancel', /cancelled/, () => executeBuiltinTool('task', { action: 'cancel', task_id: bgId }, root, { sessionId: 'stress-cancel' }));
|
|
123
|
+
const st = await timed('task-status', /cancelled|failed/, () => executeBuiltinTool('task', { action: 'status', task_id: bgId }, root, { sessionId: 'stress-cancel' }));
|
|
124
|
+
if (!/cancelled/.test(String(st))) failures.push(`cancelled task not reported cancelled: ${String(st).slice(0, 120)}`);
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
try { rmSync(tmp, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Report ──────────────────────────────────────────────────────────────────
|
|
131
|
+
let errTotal = 0;
|
|
132
|
+
for (const [tool, s] of [...stats.entries()].sort()) {
|
|
133
|
+
errTotal += s.errs.length;
|
|
134
|
+
console.log(
|
|
135
|
+
`${tool.padEnd(14)} n=${String(s.n).padStart(3)} errs=${s.errs.length}`
|
|
136
|
+
+ ` p50=${pct(s.lat, 50)}ms p95=${pct(s.lat, 95)}ms max=${Math.max(...s.lat)}ms`,
|
|
137
|
+
);
|
|
138
|
+
for (const e of s.errs.slice(0, 3)) console.log(` ! ${e}`);
|
|
139
|
+
}
|
|
140
|
+
for (const f of failures) console.log(`FAIL ${f}`);
|
|
141
|
+
const calls = [...stats.values()].reduce((a, s) => a + s.n, 0);
|
|
142
|
+
console.log(`tool stress ${failures.length || errTotal ? 'FAILED' : 'passed'} calls=${calls} errors=${errTotal} failures=${failures.length} elapsed=${Math.round((Date.now() - t0) / 1000)}s`);
|
|
143
|
+
process.exit(failures.length || errTotal ? 1 : 0);
|
package/src/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Tool Use
|
|
2
2
|
|
|
3
3
|
- Baseline routing assigns each facet directly by the evidence needed to
|
|
4
|
-
determine the complete edit:
|
|
4
|
+
determine the complete answer or edit:
|
|
5
5
|
path/name only→`find`; wildcard/recursive paths→`glob` (including known-root
|
|
6
6
|
unknown descendants); exact directory entries→`list`;
|
|
7
7
|
source content/value/`path:line`→`grep`; exact symbol/relation→`code_graph`;
|
|
@@ -18,41 +18,45 @@
|
|
|
18
18
|
relies on it. Within the current project, pass
|
|
19
19
|
project-relative paths and omit optional scopes equal to its root; explicit
|
|
20
20
|
paths may be outside cwd only for targets outside the project.
|
|
21
|
-
- Plan the fewest dependent rounds, then the fewest calls.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
re-
|
|
21
|
+
- Plan the fewest dependent rounds, then the fewest calls. Known state —
|
|
22
|
+
anything the task supplied, a tool returned, or a check already proved —
|
|
23
|
+
is never re-found, re-derived, or re-verified; a change to its subject
|
|
24
|
+
re-opens it. Batch calls iff none needs
|
|
25
25
|
another's output or can change another's inputs/state; otherwise
|
|
26
26
|
serialize. Before each batch, deduplicate the facets still required by the request,
|
|
27
27
|
route each once to the cheapest sufficient tool with all required
|
|
28
28
|
variants/scopes — every distinct sample/format in the same batch —
|
|
29
29
|
and launch every independent call together — never
|
|
30
30
|
split or duplicate a facet across tools, mutate merely to widen
|
|
31
|
-
retrieval, reserve known work, or cap fanout.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
retrieval, reserve known work, or cap fanout. Each round asks everything
|
|
32
|
+
it can and keeps everything it gets: guesses go wide in one batch, scopes
|
|
33
|
+
narrow only on verified cues — returned siblings/conventions or known
|
|
34
|
+
literals — and returned output is fully mined before the next round.
|
|
35
|
+
Symbol relations end at
|
|
35
36
|
`code_graph`; values/locations end at the context grep returns; `read`
|
|
36
37
|
covers only what returned spans cannot, as an anchored offset/limit
|
|
37
|
-
window.
|
|
38
|
-
|
|
38
|
+
window. A conclusive result ends its facet; evidence that determines the
|
|
39
|
+
answer, edit, or deliverable ends retrieval — patch if needed.
|
|
39
40
|
- Once the edit or deliverable is determined, finish in one assistant turn:
|
|
40
41
|
before `apply_patch`, obtain every target hunk's exact current content and
|
|
41
42
|
anchor from `grep`, `code_graph`, or `read`; never infer patch context from
|
|
42
43
|
another file, a sample, or expected text. Then issue `apply_patch` calls
|
|
43
44
|
serially, never in parallel; use one cohesive call with one file section per
|
|
44
|
-
target, all patches first,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
after a
|
|
45
|
+
target, all patches first, then their one batched verification `shell` in
|
|
46
|
+
the same turn — the runtime runs it after every patch and only if all
|
|
47
|
+
succeeded, so verification never needs its own turn. It runs the real
|
|
48
|
+
required postconditions on every changed file and produced artifact,
|
|
49
|
+
never echoing a claim; a postcondition that did not actually run is
|
|
50
|
+
unresolved, not passed. Retry only failed envelopes; rerun a failed check only
|
|
51
|
+
after a change that can alter its outcome — commands alike; else switch
|
|
52
|
+
route or report it unresolved.
|
|
51
53
|
Hand-authored text is edited only with `apply_patch`; computed artifacts
|
|
52
54
|
(data/reports/derived values) come from `shell` computation, never
|
|
53
55
|
hand-transcribed numbers. Earlier `shell` is only for runtime/state
|
|
54
56
|
evidence unavailable to file tools—an independent facet, batched with
|
|
55
57
|
the rest; independent probes are parallel shell calls, never one
|
|
56
58
|
serial script per round.
|
|
57
|
-
- A
|
|
58
|
-
|
|
59
|
+
- A long or uncertain command runs async — never nohup — and its
|
|
60
|
+
`task_id` ends the turn; completion resumes work. Never poll in any
|
|
61
|
+
form — sleep/status probes included; task control is for recovery or a
|
|
62
|
+
required blocking result only.
|
|
@@ -19,8 +19,6 @@ const WORKER_DENIED_TOOLS = new Set([
|
|
|
19
19
|
// (type=spawn|send|close|list). Denying the one name blocks all worker
|
|
20
20
|
// session control.
|
|
21
21
|
'agent',
|
|
22
|
-
// channels module (owner/Discord-facing)
|
|
23
|
-
'reply', 'fetch',
|
|
24
22
|
// host input injection
|
|
25
23
|
'inject_input',
|
|
26
24
|
]);
|
|
@@ -32,7 +32,7 @@ export const BUILTIN_TOOLS = [
|
|
|
32
32
|
name: 'read',
|
|
33
33
|
title: 'Mixdog Read',
|
|
34
34
|
annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
35
|
-
description: 'Known-file contents or line ranges; not directories. Replaces cat/head/tail.',
|
|
35
|
+
description: 'Known-file contents or line ranges; images render for viewing; not directories. Replaces cat/head/tail.',
|
|
36
36
|
inputSchema: {
|
|
37
37
|
type: 'object',
|
|
38
38
|
properties: {
|
|
@@ -75,7 +75,7 @@ export const BUILTIN_TOOLS = [
|
|
|
75
75
|
name: 'shell',
|
|
76
76
|
title: 'Mixdog Shell',
|
|
77
77
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
78
|
-
description: 'Run a shell command
|
|
78
|
+
description: 'Run a shell command; async returns task_id and sends a completion notification. Executable/runtime/state evidence only — never file exploration in any command segment: NOT ls/find/cat/head/tail/grep/rg/sed; dedicated file tools cover those.',
|
|
79
79
|
inputSchema: {
|
|
80
80
|
type: 'object',
|
|
81
81
|
properties: {
|
|
@@ -194,7 +194,7 @@ export const BUILTIN_TOOLS = [
|
|
|
194
194
|
description: 'Filename or directory path fragments matched against path strings; query[] batches.',
|
|
195
195
|
},
|
|
196
196
|
path: { type: 'string', description: 'Project-relative base; omit for project root; absolute only outside.' },
|
|
197
|
-
limit: { type: 'number', description: 'Max paths
|
|
197
|
+
limit: { type: 'number', description: 'Max paths; default 25; 0 unlimited.' },
|
|
198
198
|
include_noise: { type: 'boolean', description: 'Also search gitignored/dependency trees.' },
|
|
199
199
|
},
|
|
200
200
|
required: ['query'],
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
import { stat } from 'node:fs/promises';
|
|
14
14
|
|
|
15
15
|
const FS_REACHABILITY_DEADLINE_MS = 5000;
|
|
16
|
+
// Under burst load the libuv threadpool can queue a healthy local stat past
|
|
17
|
+
// the base deadline (slow ≠ dead). Before declaring the path unreachable,
|
|
18
|
+
// keep waiting for the SAME pending probe up to this extended ceiling: a
|
|
19
|
+
// loaded-but-alive filesystem answers within it, a dead mount stays silent.
|
|
20
|
+
const FS_REACHABILITY_EXTENDED_MS = 15_000;
|
|
21
|
+
let _lastSlowStatWarnAt = 0;
|
|
16
22
|
|
|
17
23
|
// Resolve true when the path is reachable (exists OR cleanly absent — ENOENT,
|
|
18
24
|
// EACCES, etc. are "the FS answered", let the real sync logic produce its own
|
|
@@ -34,8 +40,25 @@ export async function assertPathReachable(path, deadlineMs = FS_REACHABILITY_DEA
|
|
|
34
40
|
deadline,
|
|
35
41
|
]);
|
|
36
42
|
if (result === 'TIMEOUT') {
|
|
43
|
+
// Grace window: distinguish threadpool queueing from a dead mount by
|
|
44
|
+
// waiting longer for the probe that is still in flight.
|
|
45
|
+
let extTimer = null;
|
|
46
|
+
const extended = await Promise.race([
|
|
47
|
+
probe.finally(() => { if (extTimer) clearTimeout(extTimer); }),
|
|
48
|
+
new Promise((resolve) => {
|
|
49
|
+
extTimer = setTimeout(() => resolve('TIMEOUT'), Math.max(ms, FS_REACHABILITY_EXTENDED_MS - ms));
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
if (extended !== 'TIMEOUT') {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
if (now - _lastSlowStatWarnAt > 30_000) {
|
|
55
|
+
_lastSlowStatWarnAt = now;
|
|
56
|
+
process.stderr.write(`[fs-reachability] slow stat >${ms}ms under load (resolved in grace window): ${path}\n`);
|
|
57
|
+
}
|
|
58
|
+
return extended;
|
|
59
|
+
}
|
|
37
60
|
const err = new Error(
|
|
38
|
-
`path unreachable: stat exceeded ${
|
|
61
|
+
`path unreachable: stat exceeded ${FS_REACHABILITY_EXTENDED_MS}ms (possible dead mount / hung filesystem): ${path}`,
|
|
39
62
|
);
|
|
40
63
|
err.code = 'EFSUNREACHABLE';
|
|
41
64
|
throw err;
|
|
@@ -19,6 +19,13 @@ let _server = null; // { child, pending: Map, sequence }
|
|
|
19
19
|
let _binaryPath = undefined; // undefined = unresolved, null = unavailable
|
|
20
20
|
let _lastFailureAt = 0;
|
|
21
21
|
|
|
22
|
+
function _setServerReferenced(server, referenced) {
|
|
23
|
+
const method = referenced ? 'ref' : 'unref';
|
|
24
|
+
try { server?.child?.[method]?.(); } catch {}
|
|
25
|
+
try { server?.child?.stdin?.[method]?.(); } catch {}
|
|
26
|
+
try { server?.child?.stdout?.[method]?.(); } catch {}
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
function _resolveBinary() {
|
|
23
30
|
if (_binaryPath !== undefined) return _binaryPath;
|
|
24
31
|
const explicit = String(process.env.MIXDOG_SEARCH_SERVER_BIN || '').trim();
|
|
@@ -77,7 +84,10 @@ function _ensureServer() {
|
|
|
77
84
|
});
|
|
78
85
|
child.on('error', (error) => { if (_server === server) _teardown(error); });
|
|
79
86
|
child.on('exit', () => { if (_server === server) _teardown(); });
|
|
80
|
-
child
|
|
87
|
+
// A detached child can still pin a one-shot CLI/test through its pipe
|
|
88
|
+
// handles. Keep the resident server idle-unreferenced, then ref all handles
|
|
89
|
+
// only while a request is awaiting a response.
|
|
90
|
+
_setServerReferenced(server, false);
|
|
81
91
|
_server = server;
|
|
82
92
|
return server;
|
|
83
93
|
}
|
|
@@ -85,11 +95,30 @@ function _ensureServer() {
|
|
|
85
95
|
/** rg-runner seam: returns a runRgWindowedLines-shaped result, or null when
|
|
86
96
|
* the server is unavailable / the request shape is unsupported (caller then
|
|
87
97
|
* spawns rg exactly as before). */
|
|
98
|
+
// Boot-time prewarm: binary resolution is async (dynamic import), so the
|
|
99
|
+
// first search of a cold session otherwise races it and falls back to a
|
|
100
|
+
// spawn. Long-lived hosts call this fire-and-forget to have the resident
|
|
101
|
+
// server up before the first tool call. Honors the same kill switch.
|
|
102
|
+
export async function warmNativeSearchServer() {
|
|
103
|
+
try {
|
|
104
|
+
if (process.env.MIXDOG_SEARCH_SERVER === '0') return false;
|
|
105
|
+
if (_resolveBinary() === null) {
|
|
106
|
+
const mod = await import('../code-graph/graph-binary.mjs');
|
|
107
|
+
const candidate = mod.graphBinaryPath?.() || mod.resolveGraphBinaryPath?.() || null;
|
|
108
|
+
if (candidate && existsSync(candidate)) _binaryPath = candidate;
|
|
109
|
+
}
|
|
110
|
+
return Boolean(_ensureServer());
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
88
116
|
export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
|
|
89
117
|
if (process.env.MIXDOG_SEARCH_SERVER === '0') return null;
|
|
90
118
|
const server = _ensureServer();
|
|
91
119
|
if (!server) return null;
|
|
92
120
|
const id = ++server.sequence;
|
|
121
|
+
_setServerReferenced(server, true);
|
|
93
122
|
const request = {
|
|
94
123
|
id,
|
|
95
124
|
cwd: String(execOptions.cwd || process.cwd()),
|
|
@@ -100,17 +129,37 @@ export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
|
|
|
100
129
|
: 0,
|
|
101
130
|
};
|
|
102
131
|
const response = await new Promise((resolve) => {
|
|
132
|
+
let settled = false;
|
|
133
|
+
let onAbort = null;
|
|
134
|
+
const cancelServerWork = () => {
|
|
135
|
+
try { server.child.stdin.write(`${JSON.stringify({ cancel: id })}\n`); } catch {}
|
|
136
|
+
};
|
|
137
|
+
const settle = (value, cancel = false) => {
|
|
138
|
+
if (settled) return;
|
|
139
|
+
settled = true;
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
if (onAbort) {
|
|
142
|
+
try { execOptions.signal?.removeEventListener?.('abort', onAbort); } catch {}
|
|
143
|
+
onAbort = null;
|
|
144
|
+
}
|
|
145
|
+
if (cancel) cancelServerWork();
|
|
146
|
+
resolve(value);
|
|
147
|
+
if (server.pending.size === 0) _setServerReferenced(server, false);
|
|
148
|
+
};
|
|
103
149
|
const timer = setTimeout(() => {
|
|
104
150
|
server.pending.delete(id);
|
|
105
|
-
|
|
151
|
+
settle(null, true);
|
|
106
152
|
}, REQUEST_TIMEOUT_MS);
|
|
107
153
|
timer.unref?.();
|
|
108
|
-
const settle = (value) => { clearTimeout(timer); resolve(value); };
|
|
109
154
|
server.pending.set(id, { resolve: settle, reject: () => settle(null) });
|
|
110
|
-
|
|
155
|
+
onAbort = () => {
|
|
111
156
|
server.pending.delete(id);
|
|
112
|
-
settle(null);
|
|
157
|
+
settle(null, true);
|
|
113
158
|
};
|
|
159
|
+
if (execOptions.signal?.aborted) {
|
|
160
|
+
onAbort();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
114
163
|
execOptions.signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
115
164
|
try {
|
|
116
165
|
server.child.stdin.write(`${JSON.stringify(request)}\n`);
|
|
@@ -582,15 +582,14 @@ function spawnRg(argsList, execOptions) {
|
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
export async function runRg(argsList, execOptions = {}) {
|
|
585
|
-
// `rg --files` (glob tool)
|
|
586
|
-
// content grep
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
585
|
+
// Resident server first for BOTH profiles — `rg --files` (glob tool) and
|
|
586
|
+
// buffered content grep share the spawn-dominated cost. Unsupported args
|
|
587
|
+
// answer null and fall through to the real spawn. Only a COMPLETE served
|
|
588
|
+
// result substitutes for the buffered-stdout contract.
|
|
589
|
+
try {
|
|
590
|
+
const served = await tryServeSearch(argsList, execOptions, { offset: 0, limit: 0 });
|
|
591
|
+
if (served && served.complete) return served.lines.join('\n');
|
|
592
|
+
} catch { /* server is an accelerator only */ }
|
|
594
593
|
await assertRgAvailable();
|
|
595
594
|
try {
|
|
596
595
|
return await spawnRg(argsList, execOptions);
|
|
@@ -63,6 +63,17 @@ import {
|
|
|
63
63
|
psSingleQuote,
|
|
64
64
|
isPowerShellShell,
|
|
65
65
|
} from './lib/shell-spawn-helpers.mjs';
|
|
66
|
+
import os from 'node:os';
|
|
67
|
+
|
|
68
|
+
// Background work yields CPU to interactive tool calls: demote the job's
|
|
69
|
+
// process priority best-effort (below-normal). Children inherit it. Purely a
|
|
70
|
+
// scheduling hint — failures (permissions, dead pid) are ignored.
|
|
71
|
+
// MIXDOG_BG_PRIORITY=0 disables.
|
|
72
|
+
export function demoteBackgroundShellPriority(pid) {
|
|
73
|
+
if (process.env.MIXDOG_BG_PRIORITY === '0') return;
|
|
74
|
+
if (!Number.isFinite(pid) || pid <= 0) return;
|
|
75
|
+
try { os.setPriority(pid, os.constants.priority.PRIORITY_BELOW_NORMAL); } catch { /* best-effort */ }
|
|
76
|
+
}
|
|
66
77
|
|
|
67
78
|
// Facade re-exports: path/detail helpers and the job-not-found message moved
|
|
68
79
|
// to sibling modules; keep existing importers of shell-jobs.mjs resolving.
|
|
@@ -186,6 +197,7 @@ export async function _startBackgroundShellJobImpl({
|
|
|
186
197
|
try { unlinkSync(wrappedTempPath); } catch {}
|
|
187
198
|
return { jobId, kind: 'bash', status: 'failed', error: `failed to spawn shell background task: ${e?.message || e}` };
|
|
188
199
|
}
|
|
200
|
+
demoteBackgroundShellPriority(child.pid);
|
|
189
201
|
const detail = {
|
|
190
202
|
jobId,
|
|
191
203
|
kind: 'bash',
|
|
@@ -429,6 +441,7 @@ async function startBackgroundPowerShellJob({
|
|
|
429
441
|
if (!Number.isFinite(childPid) || childPid <= 0) {
|
|
430
442
|
return { jobId, kind: 'bash', status: 'failed', error: 'PowerShell background task spawn returned no pid' };
|
|
431
443
|
}
|
|
444
|
+
demoteBackgroundShellPriority(childPid);
|
|
432
445
|
const detail = {
|
|
433
446
|
jobId,
|
|
434
447
|
kind: 'bash',
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
getBackgroundTask,
|
|
19
19
|
} from '../../../../shared/background-tasks.mjs';
|
|
20
20
|
import { startChildGuardian } from '../../../../shared/child-guardian.mjs';
|
|
21
|
-
import { _startBackgroundShellJobImpl } from './shell-job-spawn.mjs';
|
|
21
|
+
import { _startBackgroundShellJobImpl, demoteBackgroundShellPriority } from './shell-job-spawn.mjs';
|
|
22
22
|
import { detachedSpawnOpts } from '../../../../shared/spawn-flags.mjs';
|
|
23
23
|
import {
|
|
24
24
|
getShellJobsDir,
|
|
@@ -804,6 +804,9 @@ export function watchBackgroundShellJob(jobId, notifyCtx) {
|
|
|
804
804
|
// 'running' detail.
|
|
805
805
|
export function adoptForegroundShellJob({ command, cwd, pid, timeoutMs, mergeStderr, stdoutPath, stderrPath, clientHostPid, ownerSessionId }) {
|
|
806
806
|
const jobId = `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
807
|
+
// Auto-backgrounded work is by definition long-running: yield CPU to
|
|
808
|
+
// interactive tool calls from here on (children inherit the class).
|
|
809
|
+
demoteBackgroundShellPriority(pid);
|
|
807
810
|
const exitPath = shellJobExitPath(jobId);
|
|
808
811
|
const donePath = shellJobDonePath(jobId);
|
|
809
812
|
const detail = {
|
|
@@ -33,6 +33,13 @@ function _graphBinaryPath() {
|
|
|
33
33
|
try { return findCachedGraphBinary(getPluginData()); } catch { return null; }
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Public resolver for consumers outside the graph build (the resident
|
|
37
|
+
// native-search-client duck-types this exact name). Returns an absolute
|
|
38
|
+
// path or null; never throws.
|
|
39
|
+
export function graphBinaryPath() {
|
|
40
|
+
try { return _graphBinaryPath() || null; } catch { return null; }
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null, signal = null) {
|
|
37
44
|
let binPath = _graphBinaryPath();
|
|
38
45
|
if (!binPath) {
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
2
|
+
"version": "0.1.4",
|
|
3
3
|
"_comment": "Synced from immutable graph-v release assets.",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
7
|
-
"sha256": "
|
|
6
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-darwin-arm64",
|
|
7
|
+
"sha256": "1531098011a3c32ad9a6d002e7240b829352fda60afc5708699dd0d335405201"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
11
|
-
"sha256": "
|
|
10
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-darwin-x64",
|
|
11
|
+
"sha256": "ead5684bebc2ef001724b2d71f23f3e7fc6e52d60d6deb5cea17b73fc3768a1b"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
15
|
-
"sha256": "
|
|
14
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-linux-arm64",
|
|
15
|
+
"sha256": "8d6d24490dfff31f66967b8384f4afe8f85051f9352c3866a2da32e6ef52d644"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
19
|
-
"sha256": "
|
|
18
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-linux-x64",
|
|
19
|
+
"sha256": "ed26c40956d90d9863d18c028cc7766e9abf5af33405a9cab02f7cbc4a3c7eac"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-win32-x64.exe",
|
|
23
|
+
"sha256": "f87bd63e70ecc0a7639f0143b5954a2b673d84e6c59e1ca8f7a8e5cdcc50a21a"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -48,6 +48,7 @@ const APPLY_PATCH_JSON_DESCRIPTION = [
|
|
|
48
48
|
'Hunks start with @@ or @@ <symbol|1-based line>; lines start space, -, or +; every Update hunk needs >=1 +/- line; optional *** End of File.',
|
|
49
49
|
'Use 3 verbatim context lines from newest output (post-patch body after edits); avoid overlap; stack @@ only if ambiguous.',
|
|
50
50
|
'Project-relative paths; explicit absolute paths only outside the project; + every added line. Never send compacted-history markers; re-read first.',
|
|
51
|
+
'A same-turn shell runs after all patches succeed.',
|
|
51
52
|
].join('\n');
|
|
52
53
|
|
|
53
54
|
export const PATCH_TOOL_DEFS = [
|
|
@@ -72,12 +72,6 @@ export function formatToolStartProgress(name, args = {}) {
|
|
|
72
72
|
case 'memory':
|
|
73
73
|
return 'managing memory';
|
|
74
74
|
|
|
75
|
-
// ── channels module ──────────────────────────────────────────────
|
|
76
|
-
case 'reply':
|
|
77
|
-
return 'replying';
|
|
78
|
-
case 'fetch':
|
|
79
|
-
return 'fetching messages';
|
|
80
|
-
|
|
81
75
|
// ── host_input / cwd ─────────────────────────────────────────────
|
|
82
76
|
case 'inject_input':
|
|
83
77
|
return 'injecting input';
|
|
@@ -35,6 +35,7 @@ import * as nodeUtil from 'node:util';
|
|
|
35
35
|
import { getPluginData } from '../config.mjs';
|
|
36
36
|
import { startChildGuardian } from '../../../shared/child-guardian.mjs';
|
|
37
37
|
import { resourceAdmission } from '../../../shared/resource-admission.mjs';
|
|
38
|
+
import { acquire as acquireChildSpawnSlot } from '../../../shared/child-spawn-gate.mjs';
|
|
38
39
|
// Runtime-only import (used inside execShellCommand's auto-background
|
|
39
40
|
// transition). shell-jobs.mjs imports stripAnsi from this module, so this is
|
|
40
41
|
// a static cycle — safe because neither binding is touched at module-eval
|
|
@@ -415,7 +416,17 @@ export function execShellCommand({
|
|
|
415
416
|
// 'close' fires with 'exit' and grandchildren cannot hold the capture
|
|
416
417
|
// open. Falls back to pipe capture if the files cannot be opened.
|
|
417
418
|
const _directCapture = SHELL_DIRECT_CAPTURE ? taskOutput.openDirectCapture() : null;
|
|
418
|
-
|
|
419
|
+
// Spawn-burst gate: hold a 'process-spawn' slot only across process
|
|
420
|
+
// creation (CreateProcess + AV scan + EPERM retries), released the
|
|
421
|
+
// moment the child exists. Bounds the Defender convoy a shell burst
|
|
422
|
+
// creates without limiting how many commands RUN concurrently — the
|
|
423
|
+
// full-lifetime gating concern in the note below stays true.
|
|
424
|
+
const _releaseSpawnSlot = await acquireChildSpawnSlot(abortSignal || null, 'process-spawn', {
|
|
425
|
+
ownerKey: ownerSessionId,
|
|
426
|
+
});
|
|
427
|
+
let spawned;
|
|
428
|
+
try {
|
|
429
|
+
spawned = await _spawnShellWithRetry({
|
|
419
430
|
shell,
|
|
420
431
|
argv,
|
|
421
432
|
shellArg,
|
|
@@ -427,20 +438,19 @@ export function execShellCommand({
|
|
|
427
438
|
stdio: _directCapture
|
|
428
439
|
? ['ignore', _directCapture.stdoutFd, _directCapture.stderrFd]
|
|
429
440
|
: ['ignore', 'pipe', 'pipe'],
|
|
430
|
-
// NOTE (child-spawn-gate):
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
// the whole lifetime would let a few long shells starve rg/code_graph —
|
|
434
|
-
// the opposite of the gate's intent. TODO: if shell saturation becomes
|
|
435
|
-
// a problem, gate only the brief spawn burst (release on first output /
|
|
436
|
-
// adoption), not the full run.
|
|
441
|
+
// NOTE (child-spawn-gate): the full command lifetime is intentionally
|
|
442
|
+
// NOT gated — bash/pwsh commands can run for minutes and would starve
|
|
443
|
+
// rg/code_graph. Only the spawn window above holds a slot.
|
|
437
444
|
// POSIX: detached gives the child its own process group so treeKill can
|
|
438
445
|
// signal the whole group. The child is still CLI-owned because we do
|
|
439
446
|
// not unref it after adoption. Windows detached has different console
|
|
440
447
|
// semantics, so it stays off there.
|
|
441
448
|
detached: process.platform !== 'win32',
|
|
442
449
|
},
|
|
443
|
-
|
|
450
|
+
});
|
|
451
|
+
} finally {
|
|
452
|
+
try { _releaseSpawnSlot(); } catch { /* idempotent */ }
|
|
453
|
+
}
|
|
444
454
|
child = spawned.child;
|
|
445
455
|
spawned.adoptErrorHandler(_onChildError);
|
|
446
456
|
}
|
|
@@ -10,10 +10,7 @@ import { OutputForwarder } from "./output-forwarder.mjs";
|
|
|
10
10
|
// call time — matching the original in-file closure semantics.
|
|
11
11
|
function createToolDispatch({
|
|
12
12
|
getForwarder,
|
|
13
|
-
PROVIDER_TOOLS,
|
|
14
13
|
isChannelsDegraded,
|
|
15
|
-
dispatchReply,
|
|
16
|
-
dispatchFetch,
|
|
17
14
|
lifecycle,
|
|
18
15
|
}) {
|
|
19
16
|
const {
|
|
@@ -39,12 +36,6 @@ function createToolDispatch({
|
|
|
39
36
|
let result;
|
|
40
37
|
try {
|
|
41
38
|
switch (name) {
|
|
42
|
-
case "reply":
|
|
43
|
-
result = await dispatchReply(args);
|
|
44
|
-
break;
|
|
45
|
-
case "fetch":
|
|
46
|
-
result = await dispatchFetch(args);
|
|
47
|
-
break;
|
|
48
39
|
case "activate_channel_bridge": {
|
|
49
40
|
const active = args.active === true;
|
|
50
41
|
const wasActive = getChannelBridgeActive();
|
|
@@ -152,23 +143,6 @@ function createToolDispatch({
|
|
|
152
143
|
_lastForwardMs = now;
|
|
153
144
|
await forwarder.forwardNewText();
|
|
154
145
|
}
|
|
155
|
-
if (PROVIDER_TOOLS.has(toolName) && !getBridgeRuntimeConnected()) {
|
|
156
|
-
// Remote-owner startup: ensure this owner's provider is connected.
|
|
157
|
-
for (let i = 0; i < 2 && !getBridgeRuntimeConnected(); i++) {
|
|
158
|
-
try {
|
|
159
|
-
// Auto-connect this owner's provider (daemon singleton — no seat claim).
|
|
160
|
-
await refreshBridgeOwnership();
|
|
161
|
-
} catch {
|
|
162
|
-
}
|
|
163
|
-
if (!getBridgeRuntimeConnected()) await new Promise((r) => setTimeout(r, 300));
|
|
164
|
-
}
|
|
165
|
-
if (!getBridgeRuntimeConnected()) {
|
|
166
|
-
return {
|
|
167
|
-
content: [{ type: "text", text: `Discord auto-connect failed after retries. Check token and network.` }],
|
|
168
|
-
isError: true
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
146
|
const result = await handleToolCall(toolName, args, signal);
|
|
173
147
|
const toolLine = OutputForwarder.buildToolLine(toolName, args);
|
|
174
148
|
if (toolLine) {
|
|
@@ -61,7 +61,6 @@ import {
|
|
|
61
61
|
} from "./crash-log.mjs";
|
|
62
62
|
import { dropTrace, preview, _dtIdxFlush } from "./index-drop-trace.mjs";
|
|
63
63
|
import { createVoiceTranscription } from "./voice-transcription.mjs";
|
|
64
|
-
import { createProviderDispatch } from "./provider-dispatch.mjs";
|
|
65
64
|
import { createParentBridge } from "./parent-bridge.mjs";
|
|
66
65
|
import { createInboundRouting } from "./inbound-routing.mjs";
|
|
67
66
|
import { createToolDispatch } from "./tool-dispatch.mjs";
|
|
@@ -500,7 +499,6 @@ import { TOOL_DEFS } from '../tool-defs.mjs';
|
|
|
500
499
|
// bottom of this file (parent's `callWorker` → `handleToolCall`). There is no
|
|
501
500
|
// orphan worker-level MCP Server: the parent (server.mjs) owns the single
|
|
502
501
|
// connected transport and routes CallTool through the IPC `call` path.
|
|
503
|
-
const PROVIDER_TOOLS = /* @__PURE__ */ new Set(["reply", "fetch"]);
|
|
504
502
|
// ── Inbound routing / dedup / ownership helpers ─────────────────────────────
|
|
505
503
|
// Extracted → lib/inbound-routing.mjs. Bound to live config/identity getters.
|
|
506
504
|
const {
|
|
@@ -512,19 +510,6 @@ const {
|
|
|
512
510
|
getInstanceId: () => INSTANCE_ID,
|
|
513
511
|
getChannelOwnerPath,
|
|
514
512
|
});
|
|
515
|
-
// ── Provider-tool dispatch helpers ───────────────────────────────────────────
|
|
516
|
-
// Each helper dispatches through the local provider (this process is always the
|
|
517
|
-
// owner in opt-in remote mode). Extracted → lib/provider-dispatch.mjs. Bound to
|
|
518
|
-
// live config/provider getters so runtime reloads keep the original file-level
|
|
519
|
-
// reference semantics.
|
|
520
|
-
const {
|
|
521
|
-
dispatchReply,
|
|
522
|
-
dispatchFetch,
|
|
523
|
-
} = createProviderDispatch({
|
|
524
|
-
getConfig: () => config,
|
|
525
|
-
getProvider: () => provider,
|
|
526
|
-
scheduler,
|
|
527
|
-
});
|
|
528
513
|
// ── Worker/HTTP tool-call dispatch ──────────────────────────────────────────
|
|
529
514
|
// handleToolCall switch + bridge auto-connect retry wrapper. Extracted →
|
|
530
515
|
// lib/tool-dispatch.mjs. The switch is entangled with ~8 runtime-lifecycle
|
|
@@ -538,10 +523,7 @@ const {
|
|
|
538
523
|
handleToolCallWithBridgeRetry,
|
|
539
524
|
} = createToolDispatch({
|
|
540
525
|
getForwarder: () => forwarder,
|
|
541
|
-
PROVIDER_TOOLS,
|
|
542
526
|
isChannelsDegraded,
|
|
543
|
-
dispatchReply,
|
|
544
|
-
dispatchFetch,
|
|
545
527
|
lifecycle: {
|
|
546
528
|
getBridgeRuntimeConnected: () => bridgeRuntimeConnected,
|
|
547
529
|
getChannelBridgeActive: () => channelBridgeActive,
|
|
@@ -1,49 +1,7 @@
|
|
|
1
1
|
export const TOOL_DEFS = [
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
annotations: { title: "Discord Reply", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
6
|
-
description: "Send message to configured channel. files are local paths.",
|
|
7
|
-
inputSchema: {
|
|
8
|
-
type: "object",
|
|
9
|
-
properties: {
|
|
10
|
-
message: { type: "string", description: "Message text for the configured channel." },
|
|
11
|
-
reply_to: { type: "string", description: "Reply message id." },
|
|
12
|
-
files: {
|
|
13
|
-
type: "array",
|
|
14
|
-
items: { type: "string" },
|
|
15
|
-
description: "Local file paths."
|
|
16
|
-
},
|
|
17
|
-
embeds: {
|
|
18
|
-
type: "array",
|
|
19
|
-
items: { type: "object", additionalProperties: true },
|
|
20
|
-
description: "Discord embeds."
|
|
21
|
-
},
|
|
22
|
-
components: {
|
|
23
|
-
type: "array",
|
|
24
|
-
items: { type: "object", additionalProperties: true },
|
|
25
|
-
description: "Discord components."
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
required: ["message"],
|
|
29
|
-
additionalProperties: false
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
name: "fetch",
|
|
34
|
-
title: "Fetch",
|
|
35
|
-
annotations: { title: "Fetch", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
36
|
-
description: "Discord-only: read recent messages from a channel.",
|
|
37
|
-
inputSchema: {
|
|
38
|
-
type: "object",
|
|
39
|
-
properties: {
|
|
40
|
-
channel: { type: "string", description: "Discord channel id." },
|
|
41
|
-
limit: { type: "number", description: "Max messages." }
|
|
42
|
-
},
|
|
43
|
-
required: ["channel"],
|
|
44
|
-
additionalProperties: false
|
|
45
|
-
}
|
|
46
|
-
},
|
|
2
|
+
// reply/fetch model-facing tools removed 2026-08: the output forwarder
|
|
3
|
+
// delivers session output to the channel and the inbound bridge injects
|
|
4
|
+
// incoming messages, so the model never needed to call them directly.
|
|
47
5
|
// memory and recall_memory tools are now provided by memory-service.mjs via MCP
|
|
48
6
|
// react/edit_message/download_attachment tools removed (no remaining
|
|
49
7
|
// callers); provider editMessage/downloadAttachment/react methods stay for
|
|
@@ -40,13 +40,24 @@ export function resolveDefaultChildSpawnLaneMaxInflight(
|
|
|
40
40
|
if (Number.isFinite(laneOverride) && laneOverride >= 1) return Math.floor(laneOverride);
|
|
41
41
|
const sharedOverride = Number(env.MIXDOG_CHILD_SPAWN_MAX_INFLIGHT);
|
|
42
42
|
if (Number.isFinite(sharedOverride) && sharedOverride >= 1) return Math.floor(sharedOverride);
|
|
43
|
-
|
|
43
|
+
const cpus = Math.max(1, Math.floor(Number(parallelism) || 1));
|
|
44
|
+
if (platform !== 'win32') {
|
|
45
|
+
// No AV amplification, but disk/CPU saturation is platform-neutral: an
|
|
46
|
+
// unbounded multi-agent burst still convoys. Bound search generously.
|
|
47
|
+
return lane === 'search' ? Math.max(4, Math.min(12, Math.ceil(cpus / 2))) : Infinity;
|
|
48
|
+
}
|
|
49
|
+
// Shell/child process creation window (CreateProcess + Defender scan).
|
|
50
|
+
// Slots are held only across the spawn itself, so a small cap smooths the
|
|
51
|
+
// AV convoy without limiting how many commands run concurrently.
|
|
52
|
+
if (lane === 'process-spawn') return Math.max(2, Math.min(4, Math.floor(cpus / 6) || 2));
|
|
53
|
+
// One cold graph build saturates memory/disk; allow a second only on big hosts.
|
|
54
|
+
if (lane === 'code-graph') return cpus >= 12 ? 2 : 1;
|
|
44
55
|
if (lane !== 'search') return 1;
|
|
45
56
|
// Multiple rg processes beat one under multi-session load, but each process
|
|
46
|
-
// is itself threaded and Defender amplifies excess fan-out.
|
|
47
|
-
// cap
|
|
48
|
-
|
|
49
|
-
return Math.max(
|
|
57
|
+
// is itself threaded and Defender amplifies excess fan-out. Burst stress
|
|
58
|
+
// measured cap-4 starvation (find p50 15s at 8 concurrent sessions), so
|
|
59
|
+
// scale with cores and cap at eight: 4→2, 8→3, 12→4, 18→6, 24+→8.
|
|
60
|
+
return Math.max(2, Math.min(8, Math.ceil(cpus / 3)));
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
const DEFAULT_MAX_INFLIGHT = resolveDefaultChildSpawnMaxInflight();
|
|
@@ -175,8 +175,6 @@ export function displayToolName(name, args = {}) {
|
|
|
175
175
|
return 'Agent';
|
|
176
176
|
case 'code_graph':
|
|
177
177
|
return codeGraphLabel(parseToolArgs(args));
|
|
178
|
-
case 'reply':
|
|
179
|
-
return 'Channel';
|
|
180
178
|
default:
|
|
181
179
|
return titleizeToolName(name);
|
|
182
180
|
}
|
|
@@ -308,8 +306,6 @@ export function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}
|
|
|
308
306
|
}
|
|
309
307
|
case 'code_graph':
|
|
310
308
|
return codeGraphSummary(a, max);
|
|
311
|
-
case 'reply':
|
|
312
|
-
return truncateToolText(a.channel || a.channelId || a.messageId || a.emoji || '', max);
|
|
313
309
|
case 'skill':
|
|
314
310
|
case 'skill_execute':
|
|
315
311
|
case 'skill_view':
|
|
@@ -384,7 +380,6 @@ const TOOL_CATEGORY = new Map([
|
|
|
384
380
|
['job_wait', 'Shell'],
|
|
385
381
|
['task', 'Task'],
|
|
386
382
|
['agent', 'Agent'],
|
|
387
|
-
['reply', 'Channel'],
|
|
388
383
|
['list_mcp_resources', 'Setup'],
|
|
389
384
|
['list_mcp_resource_templates', 'Setup'],
|
|
390
385
|
['cwd', 'Setup'],
|
|
@@ -576,8 +571,6 @@ export function toolWorkUnit(name, args = {}, category = '') {
|
|
|
576
571
|
case 'skills_list':
|
|
577
572
|
case 'use_skill':
|
|
578
573
|
return unitDescriptor('Skill', { count: queryCount(a, 'name', 'skill', 'skill_name', 'query', 'q') || 1, noun: 'skill' });
|
|
579
|
-
case 'reply':
|
|
580
|
-
return unitDescriptor('Channel', { count: queryCount(a, 'messages', 'messageId', 'text') || 1, noun: 'message' });
|
|
581
574
|
case 'code_graph': {
|
|
582
575
|
const mode = String(a.mode || a.action || '').toLowerCase();
|
|
583
576
|
const searching = mode === 'search' || mode === 'find_symbol' || mode === 'references' || mode === 'callers' || mode === 'callees';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Evaluate BEFORE the first libuv threadpool consumer (async fs/dns/zlib/
|
|
2
|
+
// crypto): Node sizes the pool lazily on first use from UV_THREADPOOL_SIZE.
|
|
3
|
+
// The default of 4 starves bursty multi-session tool execution — dozens of
|
|
4
|
+
// concurrent async stats/reads queue behind 4 workers and simple stats take
|
|
5
|
+
// seconds (observed >5s, tripping the dead-mount reachability guard on live
|
|
6
|
+
// local paths). Size to the host, bounded: operators override by setting the
|
|
7
|
+
// env var themselves.
|
|
8
|
+
import { availableParallelism } from 'node:os';
|
|
9
|
+
|
|
10
|
+
if (!process.env.UV_THREADPOOL_SIZE) {
|
|
11
|
+
const cpus = Math.max(1, Number(availableParallelism()) || 1);
|
|
12
|
+
process.env.UV_THREADPOOL_SIZE = String(Math.min(16, Math.max(8, cpus)));
|
|
13
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import '../runtime/shared/uv-threadpool-boot.mjs';
|
|
1
2
|
import { createSessionLifecycle } from './session-lifecycle.mjs';
|
|
2
3
|
import { createSessionTitleController } from './session-title.mjs';
|
|
3
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -42,7 +42,7 @@ export const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
|
|
|
42
42
|
|
|
43
43
|
export const READONLY_TOOL_NAMES = new Set([
|
|
44
44
|
'read', 'list', 'grep', 'find', 'glob', 'code_graph', 'search',
|
|
45
|
-
'web_fetch', 'recall', 'memory', '
|
|
45
|
+
'web_fetch', 'recall', 'memory', 'Skill',
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
48
|
export const DEFERRED_SELECT_ALIASES = {
|
|
@@ -50,8 +50,6 @@ export const DEFERRED_SELECT_ALIASES = {
|
|
|
50
50
|
search: ['search', 'web_fetch'],
|
|
51
51
|
web: ['web_fetch', 'search'],
|
|
52
52
|
memory: ['memory', 'recall'],
|
|
53
|
-
channels: ['reply', 'fetch'],
|
|
54
|
-
discord: ['reply', 'fetch'],
|
|
55
53
|
agent: ['agent'],
|
|
56
54
|
graph: ['code_graph'],
|
|
57
55
|
code: ['code_graph'],
|
|
@@ -145,6 +145,14 @@ export function createWarmupSchedulers({
|
|
|
145
145
|
void warmCatalogsInBackground()
|
|
146
146
|
.then(() => bootProfile('model-catalog:warm-ready'))
|
|
147
147
|
.catch((error) => bootProfile('model-catalog:warm-failed', { error: error?.message || String(error) }));
|
|
148
|
+
// Resident native search server: pre-resolve the binary and start the
|
|
149
|
+
// warm process so the session's FIRST grep/glob already skips the rg
|
|
150
|
+
// spawn instead of racing the async binary resolution. Best-effort;
|
|
151
|
+
// honors MIXDOG_SEARCH_SERVER=0 inside the warm call.
|
|
152
|
+
void import('../runtime/agent/orchestrator/tools/builtin/native-search-client.mjs')
|
|
153
|
+
.then((mod) => mod.warmNativeSearchServer?.())
|
|
154
|
+
.then((up) => bootProfile('native-search:warm', { up: up === true }))
|
|
155
|
+
.catch(() => {});
|
|
148
156
|
}, delayMs);
|
|
149
157
|
timers.modelCatalogWarmupTimer.unref?.();
|
|
150
158
|
}
|
|
@@ -24,6 +24,11 @@ const AGENT_ROLE_IDS = new Set(FIXED_AGENT_SLOTS.map((agent) => agent.id));
|
|
|
24
24
|
const BUILTIN_SLOT_AGENT_IDS = new Set(
|
|
25
25
|
FIXED_AGENT_SLOTS.filter((agent) => agent.workflowSlot).map((agent) => agent.id),
|
|
26
26
|
);
|
|
27
|
+
const STARTER_AGENT_ORDER = new Map([
|
|
28
|
+
['worker', 0],
|
|
29
|
+
['heavy-worker', 1],
|
|
30
|
+
['reviewer', 2],
|
|
31
|
+
]);
|
|
27
32
|
export const DEFAULT_WORKFLOW_ID = 'default';
|
|
28
33
|
|
|
29
34
|
const SEARCH_CAPABLE_PROVIDERS = new Set([
|
|
@@ -157,7 +162,14 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
157
162
|
ids.add(id);
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
|
-
return [...ids].sort()
|
|
165
|
+
return [...ids].sort((left, right) => {
|
|
166
|
+
const leftRank = STARTER_AGENT_ORDER.get(left);
|
|
167
|
+
const rightRank = STARTER_AGENT_ORDER.get(right);
|
|
168
|
+
if (leftRank !== undefined || rightRank !== undefined) {
|
|
169
|
+
return (leftRank ?? Number.MAX_SAFE_INTEGER) - (rightRank ?? Number.MAX_SAFE_INTEGER);
|
|
170
|
+
}
|
|
171
|
+
return left.localeCompare(right);
|
|
172
|
+
});
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
function readWorkflowPackFromDir(dir, source = 'built-in', dirName = '') {
|
|
@@ -11,8 +11,6 @@ import { rotateBoundedLog, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '.
|
|
|
11
11
|
import { attachChannel, readChannelDiscovery, probeChannelHealth } from './channel-client.mjs';
|
|
12
12
|
|
|
13
13
|
const CHANNEL_TOOLS = new Set([
|
|
14
|
-
'reply',
|
|
15
|
-
'fetch',
|
|
16
14
|
'activate_channel_bridge',
|
|
17
15
|
'reload_config',
|
|
18
16
|
'rebind_current_transcript',
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
process.env.MIXDOG_WORKER_MODE = process.env.MIXDOG_WORKER_MODE || '1';
|
|
18
18
|
// This process owns session runtimes and must never proxy back into itself.
|
|
19
19
|
process.env.MIXDOG_DAEMON_HOST = '1';
|
|
20
|
+
// Size the libuv threadpool before any async fs work spins it up (see
|
|
21
|
+
// uv-threadpool-boot.mjs) — imports below already touch fs.
|
|
22
|
+
await import('../runtime/shared/uv-threadpool-boot.mjs');
|
|
20
23
|
|
|
21
24
|
// V8 compile cache: the daemon is a standalone child entry (not via cli.mjs);
|
|
22
25
|
// caching compiled bytecode across restarts removes the channels+memory
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -3719,7 +3719,6 @@ function summarizeToolResult(name, args, resultText, isError = false) {
|
|
|
3719
3719
|
case "remember":
|
|
3720
3720
|
case "save_memory":
|
|
3721
3721
|
case "update_memory":
|
|
3722
|
-
case "reply":
|
|
3723
3722
|
case "request_user_input":
|
|
3724
3723
|
case "update_plan":
|
|
3725
3724
|
case "cwd":
|
|
@@ -3914,8 +3913,6 @@ function displayToolName(name, args = {}) {
|
|
|
3914
3913
|
return "Agent";
|
|
3915
3914
|
case "code_graph":
|
|
3916
3915
|
return codeGraphLabel(parseToolArgs(args));
|
|
3917
|
-
case "reply":
|
|
3918
|
-
return "Channel";
|
|
3919
3916
|
default:
|
|
3920
3917
|
return titleizeToolName(name);
|
|
3921
3918
|
}
|
|
@@ -4045,8 +4042,6 @@ function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}) {
|
|
|
4045
4042
|
}
|
|
4046
4043
|
case "code_graph":
|
|
4047
4044
|
return codeGraphSummary(a, max);
|
|
4048
|
-
case "reply":
|
|
4049
|
-
return truncateToolText(a.channel || a.channelId || a.messageId || a.emoji || "", max);
|
|
4050
4045
|
case "skill":
|
|
4051
4046
|
case "skill_execute":
|
|
4052
4047
|
case "skill_view":
|
|
@@ -4125,7 +4120,6 @@ var TOOL_CATEGORY = /* @__PURE__ */ new Map([
|
|
|
4125
4120
|
["job_wait", "Shell"],
|
|
4126
4121
|
["task", "Task"],
|
|
4127
4122
|
["agent", "Agent"],
|
|
4128
|
-
["reply", "Channel"],
|
|
4129
4123
|
["list_mcp_resources", "Setup"],
|
|
4130
4124
|
["list_mcp_resource_templates", "Setup"],
|
|
4131
4125
|
["cwd", "Setup"],
|
|
@@ -4293,8 +4287,6 @@ function toolWorkUnit(name, args = {}, category = "") {
|
|
|
4293
4287
|
case "skills_list":
|
|
4294
4288
|
case "use_skill":
|
|
4295
4289
|
return unitDescriptor("Skill", { count: queryCount(a, "name", "skill", "skill_name", "query", "q") || 1, noun: "skill" });
|
|
4296
|
-
case "reply":
|
|
4297
|
-
return unitDescriptor("Channel", { count: queryCount(a, "messages", "messageId", "text") || 1, noun: "message" });
|
|
4298
4290
|
case "code_graph": {
|
|
4299
4291
|
const mode = String(a.mode || a.action || "").toLowerCase();
|
|
4300
4292
|
const searching = mode === "search" || mode === "find_symbol" || mode === "references" || mode === "callers" || mode === "callees";
|
|
@@ -50,6 +50,10 @@ export function createSessionApiA(bag) {
|
|
|
50
50
|
const {
|
|
51
51
|
runtime, nextId, flags, pending, listeners, getState, getPublishedState = getState, set, flushEmitImmediate, pushItem, patchItem, replaceItems, restoreOlderTranscript, restoreNewerTranscript, settleStreamingTail, clearStreamingTail, pushNotice, autoClearState, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, updateAgentJobCard, requeueEntriesFront, enqueue, autoClearBeforeSubmit, restoreQueued, prioritizeQueued, resetStatsAndSyncContext, drain, flushDeferredExecutionPendingResumeKick, discardExecutionPendingResume,
|
|
52
52
|
} = bag;
|
|
53
|
+
// submitAsync may be awaiting auto-clear while the renderer already owns an
|
|
54
|
+
// optimistic user row. Keep that intake addressable so Esc can reclaim it
|
|
55
|
+
// before enqueue()/busy publication without racing a delayed snapshot.
|
|
56
|
+
const acceptingSubmissions = new Map();
|
|
53
57
|
const submission = (text, options = {}) => {
|
|
54
58
|
const t = promptDisplayText(text, options).trim();
|
|
55
59
|
if (!t) return null;
|
|
@@ -58,7 +62,7 @@ export function createSessionApiA(bag) {
|
|
|
58
62
|
// priority, so it is injected at the next tool/model boundary. Explicit
|
|
59
63
|
// options.priority still wins.
|
|
60
64
|
const priority = options.priority;
|
|
61
|
-
|
|
65
|
+
const intake = {
|
|
62
66
|
text,
|
|
63
67
|
queueOptions: {
|
|
64
68
|
...options,
|
|
@@ -70,6 +74,14 @@ export function createSessionApiA(bag) {
|
|
|
70
74
|
priority,
|
|
71
75
|
},
|
|
72
76
|
};
|
|
77
|
+
acceptingSubmissions.set(intake.queueOptions.id, intake);
|
|
78
|
+
return intake;
|
|
79
|
+
};
|
|
80
|
+
const enqueueSubmission = (intake) => {
|
|
81
|
+
const submissionId = String(intake?.queueOptions?.id || '').trim();
|
|
82
|
+
if (submissionId) acceptingSubmissions.delete(submissionId);
|
|
83
|
+
if (intake?.cancelled === true) return false;
|
|
84
|
+
return enqueue(intake.text, intake.queueOptions);
|
|
73
85
|
};
|
|
74
86
|
const submit = (text, options = {}) => {
|
|
75
87
|
const intake = submission(text, options);
|
|
@@ -77,7 +89,7 @@ export function createSessionApiA(bag) {
|
|
|
77
89
|
// A running clear (idle auto-clear or session_manage) sets commandBusy;
|
|
78
90
|
// queue the prompt instead of dropping it — it drains after the clear.
|
|
79
91
|
if (flags.autoClearRunning) {
|
|
80
|
-
return
|
|
92
|
+
return enqueueSubmission(intake) !== false;
|
|
81
93
|
}
|
|
82
94
|
// Any in-flight session command (clear/setModel/newSession/resume/...)
|
|
83
95
|
// holds commandBusy. Previously the prompt was dropped here and only the
|
|
@@ -85,16 +97,16 @@ export function createSessionApiA(bag) {
|
|
|
85
97
|
// commandBusy, and the central release hook re-kicks drain once the
|
|
86
98
|
// command settles, so the prompt runs afterwards rather than vanishing.
|
|
87
99
|
if (getState().commandBusy) {
|
|
88
|
-
return
|
|
100
|
+
return enqueueSubmission(intake) !== false;
|
|
89
101
|
}
|
|
90
102
|
if (getState().busy) {
|
|
91
|
-
return
|
|
103
|
+
return enqueueSubmission(intake) !== false;
|
|
92
104
|
}
|
|
93
105
|
// If autoClearBeforeSubmit rejects (e.g. compaction timeout throws), the
|
|
94
106
|
// prompt must still be queued — swallow the rejection so enqueue always
|
|
95
107
|
// runs and the submit is never silently lost.
|
|
96
108
|
void autoClearBeforeSubmit().catch(() => {}).then(
|
|
97
|
-
() =>
|
|
109
|
+
() => enqueueSubmission(intake),
|
|
98
110
|
);
|
|
99
111
|
return true;
|
|
100
112
|
};
|
|
@@ -108,7 +120,7 @@ export function createSessionApiA(bag) {
|
|
|
108
120
|
if (!flags.autoClearRunning && !getState().commandBusy && !getState().busy) {
|
|
109
121
|
await autoClearBeforeSubmit().catch(() => {});
|
|
110
122
|
}
|
|
111
|
-
return
|
|
123
|
+
return enqueueSubmission(intake) !== false;
|
|
112
124
|
};
|
|
113
125
|
return {
|
|
114
126
|
getState: () => getPublishedState(),
|
|
@@ -634,11 +646,33 @@ export function createSessionApiA(bag) {
|
|
|
634
646
|
set({ commandBusy: false, commandStatus: null });
|
|
635
647
|
}
|
|
636
648
|
},
|
|
637
|
-
compact: async ()
|
|
649
|
+
compact: async function compactCommand() {
|
|
638
650
|
if (getState().commandBusy) return null;
|
|
639
651
|
if (getState().busy) {
|
|
640
|
-
|
|
641
|
-
|
|
652
|
+
// Schedule instead of dropping: /compact mid-turn runs once the turn
|
|
653
|
+
// AND the queued follow-ups finish (compacting between queued turns
|
|
654
|
+
// would interleave a summary pass into the user's planned sequence).
|
|
655
|
+
// One pending schedule at a time; the timer is unref'd so it never
|
|
656
|
+
// holds the process open, and any error path clears it.
|
|
657
|
+
if (bag._scheduledCompactTimer) {
|
|
658
|
+
pushNotice('Compact already scheduled for turn end', 'info');
|
|
659
|
+
return { changed: false, scheduled: true };
|
|
660
|
+
}
|
|
661
|
+
pushNotice('Compact scheduled: runs when the current turn finishes', 'info');
|
|
662
|
+
bag._scheduledCompactTimer = setInterval(() => {
|
|
663
|
+
try {
|
|
664
|
+
const s = getState();
|
|
665
|
+
if (s.busy || s.commandBusy || (s.queued || []).length > 0) return;
|
|
666
|
+
clearInterval(bag._scheduledCompactTimer);
|
|
667
|
+
bag._scheduledCompactTimer = null;
|
|
668
|
+
void compactCommand();
|
|
669
|
+
} catch {
|
|
670
|
+
try { clearInterval(bag._scheduledCompactTimer); } catch { /* gone */ }
|
|
671
|
+
bag._scheduledCompactTimer = null;
|
|
672
|
+
}
|
|
673
|
+
}, 750);
|
|
674
|
+
bag._scheduledCompactTimer.unref?.();
|
|
675
|
+
return { changed: false, scheduled: true };
|
|
642
676
|
}
|
|
643
677
|
const startedAt = Date.now();
|
|
644
678
|
set({ commandBusy: true, commandStatus: { active: true, verb: 'Compacting conversation', startedAt, mode: 'compacting' } });
|
|
@@ -690,7 +724,34 @@ export function createSessionApiA(bag) {
|
|
|
690
724
|
}
|
|
691
725
|
},
|
|
692
726
|
abort: (options = {}) => {
|
|
693
|
-
|
|
727
|
+
const submissionId = String(options?.submissionId || '').trim();
|
|
728
|
+
if (!getState().busy) {
|
|
729
|
+
if (!submissionId) return false;
|
|
730
|
+
const restored = restoreQueued('', submissionId);
|
|
731
|
+
if (!restored || Number(restored.count) < 1) {
|
|
732
|
+
const intake = acceptingSubmissions.get(submissionId);
|
|
733
|
+
if (!intake) return false;
|
|
734
|
+
intake.cancelled = true;
|
|
735
|
+
const attachments = hydratePastedAttachments(
|
|
736
|
+
intake.queueOptions.pastedImages,
|
|
737
|
+
intake.queueOptions.pastedTexts,
|
|
738
|
+
);
|
|
739
|
+
return {
|
|
740
|
+
aborted: false,
|
|
741
|
+
restoreText: String(intake.queueOptions.displayText || '').trim(),
|
|
742
|
+
pastedImages: attachments.pastedImages,
|
|
743
|
+
pastedTexts: attachments.pastedTexts,
|
|
744
|
+
restoredSubmissionIds: [submissionId],
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
aborted: false,
|
|
749
|
+
restoreText: restored.text,
|
|
750
|
+
pastedImages: restored.pastedImages,
|
|
751
|
+
pastedTexts: restored.pastedTexts,
|
|
752
|
+
restoredSubmissionIds: restored.ids,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
694
755
|
denyAllToolApprovals('interrupted by user');
|
|
695
756
|
const restoreState = flags.activePromptRestore;
|
|
696
757
|
// A queued steering prompt means the user already redirected the turn:
|
|
@@ -771,6 +832,9 @@ export function createSessionApiA(bag) {
|
|
|
771
832
|
discardPastedImages,
|
|
772
833
|
pastedTexts: restored.pastedTexts,
|
|
773
834
|
discardPastedTexts,
|
|
835
|
+
restoredSubmissionIds: restoreText
|
|
836
|
+
? (restoreState?.submittedIds || []).map(String).filter(Boolean)
|
|
837
|
+
: [],
|
|
774
838
|
};
|
|
775
839
|
},
|
|
776
840
|
};
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { recordFetchedMessages } from "./status-snapshot.mjs";
|
|
2
|
-
|
|
3
|
-
// Provider-tool dispatch helpers. Extracted verbatim from channels/index.mjs
|
|
4
|
-
// (behavior-preserving). Bound to live getters so runtime config/provider
|
|
5
|
-
// reloads keep the original file-level reference semantics.
|
|
6
|
-
function createProviderDispatch({ getConfig, getProvider, scheduler }) {
|
|
7
|
-
async function dispatchReply(args) {
|
|
8
|
-
const config = getConfig();
|
|
9
|
-
const channelId = String(args?.chat_id || args?.channel_id || args?.channel || config.channelId || '').trim();
|
|
10
|
-
const message = args?.message ?? args?.text;
|
|
11
|
-
if (!channelId) throw new Error('reply requires a configured channel id');
|
|
12
|
-
if (typeof message !== 'string' || !message.trim()) throw new Error('reply requires message text');
|
|
13
|
-
let files = args?.files ?? [];
|
|
14
|
-
if (typeof files === 'string') {
|
|
15
|
-
try {
|
|
16
|
-
const parsed = JSON.parse(files);
|
|
17
|
-
files = Array.isArray(parsed) ? parsed : [files];
|
|
18
|
-
} catch {
|
|
19
|
-
files = [files];
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
if (!Array.isArray(files)) files = [files];
|
|
23
|
-
const sendOpts = {
|
|
24
|
-
replyTo: args?.reply_to ?? args?.replyTo,
|
|
25
|
-
files,
|
|
26
|
-
embeds: args?.embeds ?? [],
|
|
27
|
-
components: args?.components ?? []
|
|
28
|
-
};
|
|
29
|
-
let ids;
|
|
30
|
-
// Pre-send activity bump keeps idle gating consistent during the await.
|
|
31
|
-
scheduler.noteActivity();
|
|
32
|
-
const sendResult = await getProvider().sendMessage(channelId, message, sendOpts);
|
|
33
|
-
scheduler.noteActivity();
|
|
34
|
-
ids = sendResult.sentIds;
|
|
35
|
-
const text = ids.length === 1 ? `sent (id: ${ids[0]})` : `sent ${ids.length} parts (ids: ${ids.join(", ")})`;
|
|
36
|
-
return { content: [{ type: "text", text }] };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function dispatchFetch(args) {
|
|
40
|
-
// `args.channel` is a raw channel id (no label resolution anymore); when
|
|
41
|
-
// omitted, fall back to the single configured main channel id.
|
|
42
|
-
const config = getConfig();
|
|
43
|
-
const channelId = args.channel || config.channelId || "";
|
|
44
|
-
const limit = args.limit ?? 20;
|
|
45
|
-
let msgs;
|
|
46
|
-
msgs = await getProvider().fetchMessages(channelId, limit);
|
|
47
|
-
recordFetchedMessages(channelId, channelId, msgs);
|
|
48
|
-
const text = msgs.length === 0 ? "(no messages)" : msgs.map((m) => {
|
|
49
|
-
const atts = m.attachmentCount > 0 ? ` +${m.attachmentCount}att` : "";
|
|
50
|
-
return `[${m.ts}] ${m.user}: ${m.text} (id: ${m.id}${atts})`;
|
|
51
|
-
}).join("\n");
|
|
52
|
-
return { content: [{ type: "text", text }] };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
dispatchReply,
|
|
57
|
-
dispatchFetch,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export { createProviderDispatch };
|