mixdog 0.9.52 → 0.9.53
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/bench-run.mjs +2 -2
- package/scripts/compact-pressure-test.mjs +104 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/lifecycle-api-test.mjs +65 -4
- package/scripts/max-output-recovery-test.mjs +31 -0
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +80 -22
- package/scripts/provider-contract-test.mjs +257 -0
- package/scripts/provider-toolcall-test.mjs +172 -10
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +24 -198
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +92 -19
- package/src/runtime/shared/process-shutdown.mjs +6 -0
- package/src/runtime/shared/resource-admission.mjs +7 -2
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +36 -9
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/runtime-core.mjs +44 -103
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +15 -89
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +12 -162
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +202 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- package/src/tui/index.jsx +1 -0
- package/src/tui/lib/voice-setup.mjs +5 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync, statSync, unlinkSync, watch as fsWatch, writeFileSync } from 'fs';
|
|
3
3
|
import { basename } from 'path';
|
|
4
|
-
import { stripAnsi } from '../shell-command.mjs';
|
|
5
4
|
import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
|
|
6
5
|
import {
|
|
7
6
|
normalizeToolNotifyContext,
|
|
@@ -43,313 +42,38 @@ import {
|
|
|
43
42
|
_liveJobIdsByPid,
|
|
44
43
|
_killLiveJobPid,
|
|
45
44
|
} from './shell-job-process.mjs';
|
|
45
|
+
import {
|
|
46
|
+
attachJobInsights,
|
|
47
|
+
looksLikeInteractivePrompt,
|
|
48
|
+
readPromptTail,
|
|
49
|
+
shellJobOutputBytes,
|
|
50
|
+
shellJobPublicTaskResult,
|
|
51
|
+
SHELL_JOB_OUTPUT_DISK_CAP,
|
|
52
|
+
} from './lib/shell-job-insights.mjs';
|
|
53
|
+
import {
|
|
54
|
+
sleep,
|
|
55
|
+
awaitSpawnReady,
|
|
56
|
+
adoptSpawnErrorHandler,
|
|
57
|
+
discardSpawnErrorGuard,
|
|
58
|
+
rollbackSpawnedChild,
|
|
59
|
+
shellQuoteSingle,
|
|
60
|
+
psSingleQuote,
|
|
61
|
+
isPowerShellShell,
|
|
62
|
+
} from './lib/shell-spawn-helpers.mjs';
|
|
46
63
|
|
|
47
64
|
// Facade re-exports: path/detail helpers and the job-not-found message moved
|
|
48
65
|
// to sibling modules; keep existing importers of shell-jobs.mjs resolving.
|
|
49
66
|
export { buildJobNotFoundMessage } from './shell-job-paths.mjs';
|
|
67
|
+
export { shellJobPublicTaskResult } from './lib/shell-job-insights.mjs';
|
|
50
68
|
|
|
51
69
|
globalThis.__mixdogShellJobsRuntimeLoaded = true;
|
|
52
70
|
|
|
53
|
-
function sleep(ms) {
|
|
54
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const SPAWN_ERROR_GUARD = Symbol('mixdog.spawnErrorGuard');
|
|
58
|
-
function awaitSpawnReady(child, label) {
|
|
59
|
-
return new Promise((resolve, reject) => {
|
|
60
|
-
if (!child || typeof child.once !== 'function') {
|
|
61
|
-
reject(new Error(`${label} spawn returned no child process`));
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
let spawned = false;
|
|
65
|
-
let bufferedError = null;
|
|
66
|
-
const cleanup = () => child.removeListener('spawn', onSpawn);
|
|
67
|
-
const onError = (error) => {
|
|
68
|
-
if (spawned) {
|
|
69
|
-
bufferedError = bufferedError || error;
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
cleanup();
|
|
73
|
-
child.removeListener('error', onError);
|
|
74
|
-
reject(error);
|
|
75
|
-
};
|
|
76
|
-
const onSpawn = () => {
|
|
77
|
-
cleanup();
|
|
78
|
-
spawned = true;
|
|
79
|
-
const pid = Number(child.pid);
|
|
80
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
81
|
-
child.removeListener('error', onError);
|
|
82
|
-
reject(new Error(`${label} spawn returned no pid`));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
child[SPAWN_ERROR_GUARD] = {
|
|
86
|
-
adopt(handler) {
|
|
87
|
-
child.on('error', handler);
|
|
88
|
-
child.removeListener('error', onError);
|
|
89
|
-
if (bufferedError) handler(bufferedError);
|
|
90
|
-
delete child[SPAWN_ERROR_GUARD];
|
|
91
|
-
},
|
|
92
|
-
discard() {
|
|
93
|
-
child.removeListener('error', onError);
|
|
94
|
-
delete child[SPAWN_ERROR_GUARD];
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
resolve(child);
|
|
98
|
-
};
|
|
99
|
-
child.once('error', onError);
|
|
100
|
-
child.once('spawn', onSpawn);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function adoptSpawnErrorHandler(child, handler) {
|
|
105
|
-
const guard = child?.[SPAWN_ERROR_GUARD];
|
|
106
|
-
if (guard) guard.adopt(handler);
|
|
107
|
-
else child?.on?.('error', handler);
|
|
108
|
-
}
|
|
109
71
|
|
|
110
|
-
async function rollbackSpawnedChild(child, { timeoutMs = 5000 } = {}) {
|
|
111
|
-
if (!child || child.exitCode != null || child.signalCode != null) {
|
|
112
|
-
return { confirmed: true, errors: [] };
|
|
113
|
-
}
|
|
114
|
-
const errors = [];
|
|
115
|
-
let timer = null;
|
|
116
|
-
let settled = false;
|
|
117
|
-
const outcome = await new Promise((resolve) => {
|
|
118
|
-
const finish = (confirmed) => {
|
|
119
|
-
if (settled) return;
|
|
120
|
-
settled = true;
|
|
121
|
-
if (timer) clearTimeout(timer);
|
|
122
|
-
child.removeListener('exit', onExit);
|
|
123
|
-
child.removeListener('close', onExit);
|
|
124
|
-
child.removeListener('error', onError);
|
|
125
|
-
resolve({ confirmed, errors });
|
|
126
|
-
};
|
|
127
|
-
const onExit = () => finish(true);
|
|
128
|
-
const onError = (error) => { errors.push(error); };
|
|
129
|
-
child.on('error', onError);
|
|
130
|
-
child.once('exit', onExit);
|
|
131
|
-
child.once('close', onExit);
|
|
132
|
-
timer = setTimeout(() => finish(false), Math.max(1, Number(timeoutMs) || 5000));
|
|
133
|
-
try { killProcessTree(child.pid, 'SIGKILL'); } catch (error) { errors.push(error); }
|
|
134
|
-
try { child.kill?.('SIGKILL'); } catch (error) { errors.push(error); }
|
|
135
|
-
});
|
|
136
|
-
return outcome;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const JOB_STATUS_PREVIEW_MAX_BYTES = 4096;
|
|
140
|
-
const JOB_STATUS_PREVIEW_MAX_LINES = 20;
|
|
141
|
-
const JOB_STATUS_PREVIEW_MAX_CHARS = 1200;
|
|
142
|
-
// Hard ceiling on a background job's on-disk stdout+stderr. Mirrors the
|
|
143
|
-
// foreground SHELL_OUTPUT_DISK_CAP (shell-command.mjs) so a runaway
|
|
144
|
-
// background loop is killed and flagged instead of filling the filesystem.
|
|
145
|
-
const SHELL_JOB_OUTPUT_DISK_CAP = 100 * 1024 * 1024;
|
|
146
72
|
// Poll cadence for the adopted-job output-cap self-tick (mirrors the
|
|
147
73
|
// foreground sizeWatchdog in shell-command.mjs).
|
|
148
74
|
const ADOPTED_JOB_CAP_POLL_MS = 1_000;
|
|
149
75
|
|
|
150
|
-
// Combined byte size of a job's spilled stdout/stderr files, or 0 if
|
|
151
|
-
// unreadable. mergeStderr collapses both onto stdoutPath, so count it once.
|
|
152
|
-
function shellJobOutputBytes(detail) {
|
|
153
|
-
let total = 0;
|
|
154
|
-
const seen = new Set();
|
|
155
|
-
for (const p of [detail?.stdoutPath, detail?.stderrPath]) {
|
|
156
|
-
if (!p || seen.has(p)) continue;
|
|
157
|
-
seen.add(p);
|
|
158
|
-
try {
|
|
159
|
-
if (existsSync(p)) total += statSync(p).size;
|
|
160
|
-
} catch { /* ignore */ }
|
|
161
|
-
}
|
|
162
|
-
return total;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function shellQuoteSingle(s) {
|
|
166
|
-
return `'${String(s).replace(/'/g, `'\"'\"'`)}'`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function psSingleQuote(s) {
|
|
170
|
-
return `'${String(s).replace(/'/g, "''")}'`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function isPowerShellShell(shell, shellType) {
|
|
174
|
-
if (shellType === 'powershell') return true;
|
|
175
|
-
const stem = basename(String(shell || '')).toLowerCase().replace(/\.exe$/, '');
|
|
176
|
-
return stem === 'pwsh' || stem === 'powershell';
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function readTailPreviewSync(filePath, { maxBytes = JOB_STATUS_PREVIEW_MAX_BYTES, maxLines = JOB_STATUS_PREVIEW_MAX_LINES, maxChars = JOB_STATUS_PREVIEW_MAX_CHARS } = {}) {
|
|
180
|
-
try {
|
|
181
|
-
if (!filePath || !existsSync(filePath)) return null;
|
|
182
|
-
const st = statSync(filePath);
|
|
183
|
-
if (!st.isFile()) return null;
|
|
184
|
-
const size = st.size;
|
|
185
|
-
if (size <= 0) return { bytes: 0, preview: '' };
|
|
186
|
-
const readBytes = Math.min(size, maxBytes);
|
|
187
|
-
const fd = openSync(filePath, 'r');
|
|
188
|
-
try {
|
|
189
|
-
const buf = Buffer.alloc(readBytes);
|
|
190
|
-
readSync(fd, buf, 0, readBytes, size - readBytes);
|
|
191
|
-
let text = buf.toString('utf8');
|
|
192
|
-
if (size > readBytes) {
|
|
193
|
-
const nl = text.indexOf('\n');
|
|
194
|
-
if (nl !== -1) text = text.slice(nl + 1);
|
|
195
|
-
}
|
|
196
|
-
let lines = text.split(/\r?\n/);
|
|
197
|
-
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
198
|
-
let truncated = size > readBytes;
|
|
199
|
-
if (lines.length > maxLines) {
|
|
200
|
-
lines = lines.slice(-maxLines);
|
|
201
|
-
truncated = true;
|
|
202
|
-
}
|
|
203
|
-
let preview = lines.join('\n');
|
|
204
|
-
if (preview.length > maxChars) {
|
|
205
|
-
preview = preview.slice(preview.length - maxChars);
|
|
206
|
-
const nl = preview.indexOf('\n');
|
|
207
|
-
if (nl !== -1) preview = preview.slice(nl + 1);
|
|
208
|
-
truncated = true;
|
|
209
|
-
}
|
|
210
|
-
return {
|
|
211
|
-
bytes: size,
|
|
212
|
-
preview,
|
|
213
|
-
truncated,
|
|
214
|
-
};
|
|
215
|
-
} finally {
|
|
216
|
-
try { closeSync(fd); } catch { /* ignore */ }
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function attachJobPreview(detail) {
|
|
224
|
-
if (!detail || typeof detail !== 'object') return detail;
|
|
225
|
-
const withPreview = { ...detail };
|
|
226
|
-
const stdoutInfo = readTailPreviewSync(detail.stdoutPath);
|
|
227
|
-
if (stdoutInfo) {
|
|
228
|
-
withPreview.stdoutBytes = stdoutInfo.bytes;
|
|
229
|
-
if (stdoutInfo.preview) withPreview.stdoutPreview = stdoutInfo.preview;
|
|
230
|
-
if (stdoutInfo.truncated) withPreview.stdoutPreviewTruncated = true;
|
|
231
|
-
}
|
|
232
|
-
if (detail.mergeStderr !== true) {
|
|
233
|
-
const stderrInfo = readTailPreviewSync(detail.stderrPath);
|
|
234
|
-
if (stderrInfo) {
|
|
235
|
-
withPreview.stderrBytes = stderrInfo.bytes;
|
|
236
|
-
if (stderrInfo.preview) withPreview.stderrPreview = stderrInfo.preview;
|
|
237
|
-
if (stderrInfo.truncated) withPreview.stderrPreviewTruncated = true;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return withPreview;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function summarizeJobPreviewText(text, maxChars = 160) {
|
|
244
|
-
if (typeof text !== 'string' || !text.trim()) return '';
|
|
245
|
-
const lines = text
|
|
246
|
-
.split(/\r?\n/)
|
|
247
|
-
.map((line) => stripAnsi(line).replace(/\s+/g, ' ').trim())
|
|
248
|
-
.filter(Boolean);
|
|
249
|
-
if (lines.length === 0) return '';
|
|
250
|
-
let summary = lines[lines.length - 1];
|
|
251
|
-
if (summary.length > maxChars) summary = `${summary.slice(0, maxChars - 1)}…`;
|
|
252
|
-
return summary;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
76
|
const SHELL_JOB_PROMPT_STALL_MS = 45_000;
|
|
256
|
-
const SHELL_JOB_PROMPT_TAIL_BYTES = 1024;
|
|
257
|
-
const SHELL_JOB_PROMPT_TAIL_LINES = 16;
|
|
258
|
-
const SHELL_JOB_PROMPT_TAIL_CHARS = 1024;
|
|
259
|
-
const SHELL_JOB_PROMPT_PATTERNS = [
|
|
260
|
-
/\((?:y|yes)\/(?:n|no)\)\s*[:?]?\s*$/i,
|
|
261
|
-
/\[(?:y|yes)\/(?:n|no)\]\s*[:?]?\s*$/i,
|
|
262
|
-
/\b(?:continue|proceed|confirm|overwrite|replace)\b[^\n]*[?:]\s*$/i,
|
|
263
|
-
/\bpress\s+(?:enter|return)\b[^\n]*$/i,
|
|
264
|
-
/\bdo you (?:want|wish|agree|accept)\b[^\n]*\?\s*$/i,
|
|
265
|
-
/\b(?:password|passphrase|otp|verification code)\b[^\n]*[:?]\s*$/i,
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
function looksLikeInteractivePrompt(text) {
|
|
269
|
-
const tail = stripAnsi(String(text || '')).trim();
|
|
270
|
-
if (!tail) return false;
|
|
271
|
-
const last = tail.split(/\r?\n/).slice(-4).join('\n').trim();
|
|
272
|
-
return SHELL_JOB_PROMPT_PATTERNS.some((pattern) => pattern.test(last));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function readPromptTail(detail) {
|
|
276
|
-
if (!detail || typeof detail !== 'object') return { bytes: 0, text: '' };
|
|
277
|
-
const stdoutInfo = readTailPreviewSync(detail.stdoutPath, {
|
|
278
|
-
maxBytes: SHELL_JOB_PROMPT_TAIL_BYTES,
|
|
279
|
-
maxLines: SHELL_JOB_PROMPT_TAIL_LINES,
|
|
280
|
-
maxChars: SHELL_JOB_PROMPT_TAIL_CHARS,
|
|
281
|
-
});
|
|
282
|
-
const stderrInfo = detail.mergeStderr === true ? null : readTailPreviewSync(detail.stderrPath, {
|
|
283
|
-
maxBytes: SHELL_JOB_PROMPT_TAIL_BYTES,
|
|
284
|
-
maxLines: SHELL_JOB_PROMPT_TAIL_LINES,
|
|
285
|
-
maxChars: SHELL_JOB_PROMPT_TAIL_CHARS,
|
|
286
|
-
});
|
|
287
|
-
const bytes = (stdoutInfo?.bytes || 0) + (stderrInfo?.bytes || 0);
|
|
288
|
-
const parts = [
|
|
289
|
-
stdoutInfo?.preview ? `[stdout tail]\n${stdoutInfo.preview}` : '',
|
|
290
|
-
stderrInfo?.preview ? `[stderr tail]\n${stderrInfo.preview}` : '',
|
|
291
|
-
].filter(Boolean);
|
|
292
|
-
return { bytes, text: parts.join('\n\n') };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function attachJobInsights(detail) {
|
|
296
|
-
const withPreview = attachJobPreview(detail);
|
|
297
|
-
if (!withPreview || typeof withPreview !== 'object') return withPreview;
|
|
298
|
-
let summary = '';
|
|
299
|
-
let summarySource = '';
|
|
300
|
-
if (withPreview.status === 'completed') {
|
|
301
|
-
summary = summarizeJobPreviewText(withPreview.stdoutPreview)
|
|
302
|
-
|| summarizeJobPreviewText(withPreview.stderrPreview);
|
|
303
|
-
summarySource = summary ? (withPreview.stdoutPreview ? 'stdout' : 'stderr') : '';
|
|
304
|
-
} else if (withPreview.status === 'failed') {
|
|
305
|
-
summary = summarizeJobPreviewText(withPreview.stderrPreview)
|
|
306
|
-
|| summarizeJobPreviewText(withPreview.stdoutPreview)
|
|
307
|
-
|| String(withPreview.error || '').trim();
|
|
308
|
-
summarySource = summary ? (withPreview.stderrPreview ? 'stderr' : (withPreview.stdoutPreview ? 'stdout' : 'status')) : '';
|
|
309
|
-
} else if (withPreview.status === 'cancelled') {
|
|
310
|
-
summary = 'cancelled before completion';
|
|
311
|
-
summarySource = 'status';
|
|
312
|
-
} else if (withPreview.status === 'running') {
|
|
313
|
-
summary = summarizeJobPreviewText(withPreview.stdoutPreview)
|
|
314
|
-
|| summarizeJobPreviewText(withPreview.stderrPreview);
|
|
315
|
-
summarySource = summary ? (withPreview.stdoutPreview ? 'stdout' : 'stderr') : '';
|
|
316
|
-
}
|
|
317
|
-
if (summary) {
|
|
318
|
-
withPreview.summary = summary;
|
|
319
|
-
withPreview.summarySource = summarySource;
|
|
320
|
-
}
|
|
321
|
-
return withPreview;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export function shellJobPublicTaskResult(detail) {
|
|
325
|
-
if (!detail || typeof detail !== 'object') return detail;
|
|
326
|
-
const result = {
|
|
327
|
-
task_id: detail.jobId || detail.task_id || null,
|
|
328
|
-
shell: detail.shellType || null,
|
|
329
|
-
status: detail.status || null,
|
|
330
|
-
cwd: detail.cwd || null,
|
|
331
|
-
pid: detail.pid || null,
|
|
332
|
-
exit_code: (typeof detail.exitCode === 'number') ? detail.exitCode : null,
|
|
333
|
-
signal: detail.signal || null,
|
|
334
|
-
timed_out: detail.timedOut === true ? true : null,
|
|
335
|
-
killed: detail.killed === true ? true : null,
|
|
336
|
-
stdout_bytes: (typeof detail.stdoutBytes === 'number') ? detail.stdoutBytes : null,
|
|
337
|
-
stderr_bytes: (typeof detail.stderrBytes === 'number') ? detail.stderrBytes : null,
|
|
338
|
-
stdout_preview: detail.stdoutPreview || null,
|
|
339
|
-
stderr_preview: detail.stderrPreview || null,
|
|
340
|
-
summary: detail.summary || null,
|
|
341
|
-
summary_source: detail.summarySource || null,
|
|
342
|
-
waited_ms: (typeof detail.waitedMs === 'number') ? detail.waitedMs : null,
|
|
343
|
-
wait_timed_out: detail.waitTimedOut === true ? true : null,
|
|
344
|
-
started_at: detail.startedAt || null,
|
|
345
|
-
finished_at: detail.finishedAt || null,
|
|
346
|
-
error: detail.error || null,
|
|
347
|
-
};
|
|
348
|
-
for (const [key, value] of Object.entries(result)) {
|
|
349
|
-
if (value == null || value === '') delete result[key];
|
|
350
|
-
}
|
|
351
|
-
return result;
|
|
352
|
-
}
|
|
353
77
|
|
|
354
78
|
export async function waitForShellJob(jobId, { timeoutMs = 30_000, pollMs = 250 } = {}) {
|
|
355
79
|
const started = Date.now();
|
|
@@ -1184,7 +908,7 @@ async function _startBackgroundShellJobImpl({
|
|
|
1184
908
|
terminal = true;
|
|
1185
909
|
const rollback = await rollbackSpawnedChild(child, { timeoutMs: rollbackTimeoutMs });
|
|
1186
910
|
child.removeListener('error', onRuntimeError);
|
|
1187
|
-
child
|
|
911
|
+
discardSpawnErrorGuard(child);
|
|
1188
912
|
try { unlinkSync(wrappedTempPath); } catch {}
|
|
1189
913
|
const failure = { jobId, kind: 'bash', status: 'failed', error: `failed to persist shell background task: ${e?.message || e}` };
|
|
1190
914
|
if (!rollback.confirmed) {
|
|
@@ -1425,7 +1149,7 @@ async function startBackgroundPowerShellJob({
|
|
|
1425
1149
|
terminal = true;
|
|
1426
1150
|
const rollback = await rollbackSpawnedChild(child, { timeoutMs: rollbackTimeoutMs });
|
|
1427
1151
|
child.removeListener('error', onRuntimeError);
|
|
1428
|
-
child
|
|
1152
|
+
discardSpawnErrorGuard(child);
|
|
1429
1153
|
try { unlinkSync(wrappedTempPath); } catch {}
|
|
1430
1154
|
try { unlinkSync(innerTempPath); } catch {}
|
|
1431
1155
|
const failure = { jobId, kind: 'bash', status: 'failed', error: `failed to persist PowerShell background task: ${e?.message || e}` };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
let activeShellSpawns = 0;
|
|
4
|
+
|
|
5
|
+
function isPowerShellSpawn(shell, shellArg) {
|
|
6
|
+
return /pwsh|powershell/i.test(String(shell || '')) || shellArg === '-Command';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function spawnShellWithRetry({ shell, argv, spawnOptions, shellArg, cwd }) {
|
|
10
|
+
const delays = [100, 300, 700];
|
|
11
|
+
const isPowerShell = isPowerShellSpawn(shell, shellArg);
|
|
12
|
+
activeShellSpawns++;
|
|
13
|
+
try {
|
|
14
|
+
let attempt = 0;
|
|
15
|
+
for (;;) {
|
|
16
|
+
try {
|
|
17
|
+
const child = spawn(shell, argv, spawnOptions);
|
|
18
|
+
let bufferedError = null;
|
|
19
|
+
const guardError = (err) => { bufferedError = bufferedError || err; };
|
|
20
|
+
child.on('error', guardError);
|
|
21
|
+
try {
|
|
22
|
+
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
23
|
+
const onSpawn = () => {
|
|
24
|
+
child.removeListener('error', onError);
|
|
25
|
+
resolveSpawn();
|
|
26
|
+
};
|
|
27
|
+
const onError = (err) => {
|
|
28
|
+
child.removeListener('spawn', onSpawn);
|
|
29
|
+
rejectSpawn(err);
|
|
30
|
+
};
|
|
31
|
+
child.once('spawn', onSpawn);
|
|
32
|
+
child.once('error', onError);
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
child.removeListener('error', guardError);
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
child,
|
|
40
|
+
adoptErrorHandler(handler) {
|
|
41
|
+
child.on('error', handler);
|
|
42
|
+
child.removeListener('error', guardError);
|
|
43
|
+
if (bufferedError) handler(bufferedError);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
} catch (err) {
|
|
47
|
+
try {
|
|
48
|
+
console.error('[shell-spawn-retry] ' + JSON.stringify({
|
|
49
|
+
code: (err && err.code) || null,
|
|
50
|
+
syscall: (err && err.syscall) || null,
|
|
51
|
+
shell,
|
|
52
|
+
cwd,
|
|
53
|
+
activeSpawnCount: activeShellSpawns,
|
|
54
|
+
}));
|
|
55
|
+
} catch { /* logging must never mask the spawn error */ }
|
|
56
|
+
const canRetry = err && err.code === 'EPERM'
|
|
57
|
+
&& process.platform === 'win32'
|
|
58
|
+
&& isPowerShell
|
|
59
|
+
&& attempt < delays.length;
|
|
60
|
+
if (!canRetry) throw err;
|
|
61
|
+
await new Promise((r) => setTimeout(r, delays[attempt++]));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
activeShellSpawns--;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
_maybeEncodePowerShellCommand,
|
|
45
45
|
extractPowerShellCommandInner,
|
|
46
46
|
} from './shell-powershell.mjs';
|
|
47
|
+
import { spawnShellWithRetry as _spawnShellWithRetry } from './lib/shell-spawn-retry.mjs';
|
|
47
48
|
|
|
48
49
|
export {
|
|
49
50
|
_maybeEncodePowerShellCommand,
|
|
@@ -447,12 +448,6 @@ async function _execPolicyBlockMessage(command) {
|
|
|
447
448
|
// EPERM backoff). Logged with each failed spawn so a Defender-induced storm
|
|
448
449
|
// is reconstructable: activeSpawnCount > 1 means concurrent spawns were
|
|
449
450
|
// racing the AV scan when the failure hit.
|
|
450
|
-
let _activeShellSpawns = 0;
|
|
451
|
-
|
|
452
|
-
function _isPowerShellSpawn(shell, shellArg) {
|
|
453
|
-
return /pwsh|powershell/i.test(String(shell || '')) || shellArg === '-Command';
|
|
454
|
-
}
|
|
455
|
-
|
|
456
451
|
// Windows Defender intermittently fails node→PowerShell spawns with EPERM
|
|
457
452
|
// while it scans the child image (see shell-runtime.mjs Trojan false-positive
|
|
458
453
|
// note). The failure is at spawn() time — before any stdio/side effect — so a
|
|
@@ -460,71 +455,6 @@ function _isPowerShellSpawn(shell, shellArg) {
|
|
|
460
455
|
// Retry ONLY on EPERM/win32/powershell; everything else throws on first
|
|
461
456
|
// failure. Backoff 100/300/700ms caps added latency at ~1.1s. Every failed
|
|
462
457
|
// attempt logs one diagnostic line for later reconstruction.
|
|
463
|
-
async function _spawnShellWithRetry({ shell, argv, spawnOptions, shellArg, cwd }) {
|
|
464
|
-
const _delays = [100, 300, 700];
|
|
465
|
-
const _isPowerShell = _isPowerShellSpawn(shell, shellArg);
|
|
466
|
-
_activeShellSpawns++;
|
|
467
|
-
try {
|
|
468
|
-
let attempt = 0;
|
|
469
|
-
for (;;) {
|
|
470
|
-
try {
|
|
471
|
-
const child = spawn(shell, argv, spawnOptions);
|
|
472
|
-
// spawn() reports ENOENT/EACCES on nextTick. Keep a guard listener
|
|
473
|
-
// attached from the same synchronous frame as spawn(), and do not
|
|
474
|
-
// resolve this helper until the child emits `spawn`. The caller adopts
|
|
475
|
-
// the guard atomically (new listener first, guard removal second), so
|
|
476
|
-
// there is never an unhandled-error window across either await.
|
|
477
|
-
let bufferedError = null;
|
|
478
|
-
const guardError = (err) => { bufferedError = bufferedError || err; };
|
|
479
|
-
child.on('error', guardError);
|
|
480
|
-
try {
|
|
481
|
-
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
482
|
-
const onSpawn = () => {
|
|
483
|
-
child.removeListener('error', onError);
|
|
484
|
-
resolveSpawn();
|
|
485
|
-
};
|
|
486
|
-
const onError = (err) => {
|
|
487
|
-
child.removeListener('spawn', onSpawn);
|
|
488
|
-
rejectSpawn(err);
|
|
489
|
-
};
|
|
490
|
-
child.once('spawn', onSpawn);
|
|
491
|
-
child.once('error', onError);
|
|
492
|
-
});
|
|
493
|
-
} catch (err) {
|
|
494
|
-
child.removeListener('error', guardError);
|
|
495
|
-
throw err;
|
|
496
|
-
}
|
|
497
|
-
return {
|
|
498
|
-
child,
|
|
499
|
-
adoptErrorHandler(handler) {
|
|
500
|
-
child.on('error', handler);
|
|
501
|
-
child.removeListener('error', guardError);
|
|
502
|
-
if (bufferedError) handler(bufferedError);
|
|
503
|
-
},
|
|
504
|
-
};
|
|
505
|
-
} catch (err) {
|
|
506
|
-
try {
|
|
507
|
-
console.error('[shell-spawn-retry] ' + JSON.stringify({
|
|
508
|
-
code: (err && err.code) || null,
|
|
509
|
-
syscall: (err && err.syscall) || null,
|
|
510
|
-
shell,
|
|
511
|
-
cwd,
|
|
512
|
-
activeSpawnCount: _activeShellSpawns,
|
|
513
|
-
}));
|
|
514
|
-
} catch { /* logging must never mask the spawn error */ }
|
|
515
|
-
const _canRetry = err && err.code === 'EPERM'
|
|
516
|
-
&& process.platform === 'win32'
|
|
517
|
-
&& _isPowerShell
|
|
518
|
-
&& attempt < _delays.length;
|
|
519
|
-
if (!_canRetry) throw err;
|
|
520
|
-
await new Promise((r) => setTimeout(r, _delays[attempt++]));
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
} finally {
|
|
524
|
-
_activeShellSpawns--;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
458
|
export function execShellCommand({
|
|
529
459
|
shell,
|
|
530
460
|
shellArg,
|
|
@@ -18,7 +18,7 @@ import { join, sep } from "path";
|
|
|
18
18
|
import { createHash } from "crypto";
|
|
19
19
|
import { chunk, formatForDiscord, MAX_DISCORD_MESSAGE } from "../lib/format.mjs";
|
|
20
20
|
import { withConfigLock } from "../lib/config-lock.mjs";
|
|
21
|
-
import { readSection,
|
|
21
|
+
import { readSection, updateSectionAsync } from "../../shared/config.mjs";
|
|
22
22
|
import { normalizeAccess, safeAttName } from "./discord-access.mjs";
|
|
23
23
|
import { MAX_ATTACHMENT_BYTES, downloadSingleAttachment } from "./discord-attachments.mjs";
|
|
24
24
|
import * as gateway from "./discord-gateway.mjs";
|
|
@@ -473,7 +473,7 @@ class DiscordBackend {
|
|
|
473
473
|
return this.initialAccess;
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
|
-
persistAccessFromMainChannel() {
|
|
476
|
+
async persistAccessFromMainChannel() {
|
|
477
477
|
if (this.isStatic || !this.configFile) return;
|
|
478
478
|
try {
|
|
479
479
|
const id = this.mainChannelId;
|
|
@@ -482,7 +482,7 @@ class DiscordBackend {
|
|
|
482
482
|
const access = normalizeAccess(parsed.access);
|
|
483
483
|
if (!(id in access.channels)) {
|
|
484
484
|
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
485
|
-
this.saveAccess(access);
|
|
485
|
+
await this.saveAccess(access);
|
|
486
486
|
}
|
|
487
487
|
} catch (err) {
|
|
488
488
|
process.stderr.write(`mixdog discord: persistAccessFromMainChannel failed: ${err}\n`);
|
|
@@ -499,10 +499,10 @@ class DiscordBackend {
|
|
|
499
499
|
}
|
|
500
500
|
return a;
|
|
501
501
|
}
|
|
502
|
-
saveAccess(a) {
|
|
502
|
+
async saveAccess(a) {
|
|
503
503
|
if (this.isStatic) return;
|
|
504
504
|
if (!this.configFile) return;
|
|
505
|
-
return withConfigLock(() => {
|
|
505
|
+
return withConfigLock(async () => {
|
|
506
506
|
mkdirSync(this.stateDir, { recursive: true, mode: 448 });
|
|
507
507
|
const access = {
|
|
508
508
|
dmPolicy: a.dmPolicy,
|
|
@@ -513,7 +513,7 @@ class DiscordBackend {
|
|
|
513
513
|
...a.replyToMode ? { replyToMode: a.replyToMode } : {},
|
|
514
514
|
...a.textChunkLimit ? { textChunkLimit: a.textChunkLimit } : {}
|
|
515
515
|
};
|
|
516
|
-
|
|
516
|
+
await updateSectionAsync("channels", (channels) => ({
|
|
517
517
|
...channels,
|
|
518
518
|
access
|
|
519
519
|
}));
|
|
@@ -2,7 +2,16 @@ import { readFileSync, mkdirSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { DiscordBackend } from "../backends/discord.mjs";
|
|
4
4
|
import { TelegramBackend } from "../backends/telegram.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
readSection,
|
|
7
|
+
updateSection,
|
|
8
|
+
CONFIG_PATH as MIXDOG_CONFIG_PATH,
|
|
9
|
+
SECRET_ACCOUNTS,
|
|
10
|
+
getDiscordToken,
|
|
11
|
+
getTelegramToken,
|
|
12
|
+
diagnoseDiscordTokenValue,
|
|
13
|
+
invalidateSecretReadCache,
|
|
14
|
+
} from "../../shared/config.mjs";
|
|
6
15
|
import { listSchedules } from "../../shared/schedules-db.mjs";
|
|
7
16
|
import { resolvePluginData } from "../../shared/plugin-paths.mjs";
|
|
8
17
|
const DATA_DIR = resolvePluginData();
|
|
@@ -46,8 +55,12 @@ function resolveChannelId(raw = {}, backend = "discord") {
|
|
|
46
55
|
return "";
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
async function loadConfig() {
|
|
58
|
+
async function loadConfig({ freshSecrets = false } = {}) {
|
|
50
59
|
try {
|
|
60
|
+
if (freshSecrets) {
|
|
61
|
+
invalidateSecretReadCache(SECRET_ACCOUNTS.discordToken);
|
|
62
|
+
invalidateSecretReadCache(SECRET_ACCOUNTS.telegramToken);
|
|
63
|
+
}
|
|
51
64
|
let raw = readSection("channels");
|
|
52
65
|
raw = raw && typeof raw === "object" ? raw : {};
|
|
53
66
|
// Schedules are the PG `scheduler.schedules` table (single source of
|
|
@@ -218,6 +218,16 @@ function atomicWrite(targetPath, contents) {
|
|
|
218
218
|
// site, so no drain/replay format or TTL semantics change.
|
|
219
219
|
const _dedupeIndex = new Map()
|
|
220
220
|
let _dedupeIndexSeeded = false
|
|
221
|
+
function dropDedupeIndexForPath(bufferPath) {
|
|
222
|
+
for (const [key, indexedPath] of _dedupeIndex) {
|
|
223
|
+
if (indexedPath === bufferPath) _dedupeIndex.delete(key)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function replaceDedupeIndexPath(previousPath, nextPath) {
|
|
227
|
+
for (const [key, indexedPath] of _dedupeIndex) {
|
|
228
|
+
if (indexedPath === previousPath) _dedupeIndex.set(key, nextPath)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
221
231
|
function seedDedupeIndex(kind) {
|
|
222
232
|
if (_dedupeIndexSeeded) return
|
|
223
233
|
_dedupeIndexSeeded = true
|
|
@@ -266,9 +276,11 @@ function bufferToDisk(kind, payload, { dedupeKey = null } = {}) {
|
|
|
266
276
|
// leaves the file in place and returns false so the caller skips it this pass.
|
|
267
277
|
function moveToDead(name, reason) {
|
|
268
278
|
process.stderr.write(`[memory-client] quarantining ${name} to dead/ (${reason})\n`)
|
|
279
|
+
const bufferPath = path.join(BUFFER_DIR, name)
|
|
269
280
|
try {
|
|
270
281
|
fs.mkdirSync(DEAD_DIR, { recursive: true })
|
|
271
|
-
fs.renameSync(
|
|
282
|
+
fs.renameSync(bufferPath, path.join(DEAD_DIR, name))
|
|
283
|
+
dropDedupeIndexForPath(bufferPath)
|
|
272
284
|
return true
|
|
273
285
|
} catch (e) {
|
|
274
286
|
process.stderr.write(`[memory-client] quarantine of ${name} failed (${e.message}) — leaving in place\n`)
|
|
@@ -355,7 +367,10 @@ export async function drainBuffer() {
|
|
|
355
367
|
// throwOnError: non-2xx status or an {error} body REJECTS, so the
|
|
356
368
|
// file is kept/aged for retry instead of unlinked (HIGH: data loss).
|
|
357
369
|
await memoryFetch('POST', endpoint, payload, 10_000, { throwOnError: true })
|
|
358
|
-
try {
|
|
370
|
+
try {
|
|
371
|
+
fs.unlinkSync(bufferPath)
|
|
372
|
+
dropDedupeIndexForPath(bufferPath)
|
|
373
|
+
} catch {}
|
|
359
374
|
drained++
|
|
360
375
|
} catch (e) {
|
|
361
376
|
const attempts = parseRetry(name) + 1
|
|
@@ -375,7 +390,9 @@ export async function drainBuffer() {
|
|
|
375
390
|
// THIS pass so it can't block the oldest-first queue forever — the
|
|
376
391
|
// next drain re-reads its (unchanged) .rN and retries.
|
|
377
392
|
try {
|
|
378
|
-
|
|
393
|
+
const nextPath = path.join(BUFFER_DIR, retryName(name, attempts))
|
|
394
|
+
fs.renameSync(bufferPath, nextPath)
|
|
395
|
+
replaceDedupeIndexPath(bufferPath, nextPath)
|
|
379
396
|
} catch {
|
|
380
397
|
// Rename lock/EPERM: don't break the whole pass on a file we
|
|
381
398
|
// couldn't even age — skip it and CONTINUE so later buffered
|