mixdog 0.9.2 → 0.9.3
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 +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
|
@@ -1367,7 +1367,7 @@ function prepareInput(patchStr) {
|
|
|
1367
1367
|
return String(patchStr).replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
1368
1368
|
}
|
|
1369
1369
|
|
|
1370
|
-
function
|
|
1370
|
+
function isApplyPatchEnvelope(patchStr) {
|
|
1371
1371
|
const text = prepareInput(patchStr).trimStart();
|
|
1372
1372
|
return text.startsWith('*** Begin Patch')
|
|
1373
1373
|
|| text.startsWith('*** Add File:')
|
|
@@ -1377,7 +1377,7 @@ function isCodexApplyPatchEnvelope(patchStr) {
|
|
|
1377
1377
|
|
|
1378
1378
|
function isV4APatchInput(patchStr, format) {
|
|
1379
1379
|
return String(format || '').toLowerCase() === 'v4a'
|
|
1380
|
-
||
|
|
1380
|
+
|| isApplyPatchEnvelope(patchStr);
|
|
1381
1381
|
}
|
|
1382
1382
|
|
|
1383
1383
|
const UNIFIED_HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
|
|
@@ -2947,7 +2947,7 @@ async function _executePatchTool(name, args, cwd, options = {}) {
|
|
|
2947
2947
|
return errText;
|
|
2948
2948
|
}
|
|
2949
2949
|
if (isPatchErrorText(String(result))) maybeCapturePatchReplay(args, effectiveCwd, String(result));
|
|
2950
|
-
// ② completion progress (
|
|
2950
|
+
// ② completion progress ("Found N" summary line). Best-effort, no-op
|
|
2951
2951
|
// when onProgress is absent (no progressToken). Never throws — only
|
|
2952
2952
|
// emits on success (an "Error:" body is left to the tool result alone).
|
|
2953
2953
|
if (typeof options?.onProgress === 'function') {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// Per-tool live-status message builder
|
|
2
|
-
// generalization / getActivityDescription). Dependency-light and
|
|
1
|
+
// Per-tool live-status message builder. Dependency-light and
|
|
3
2
|
// side-effect-free: given a tool name + its raw args, return a short
|
|
4
3
|
// human-readable "what's happening now" string. Used by the central dispatch
|
|
5
4
|
// path to emit a single start-of-tool progress notification. Every tool gets a
|
|
@@ -53,8 +53,7 @@ const SHELL_OUTPUT_DISK_CAP = 100 * 1024 * 1024;
|
|
|
53
53
|
|
|
54
54
|
// Background-task disk watchdog cadence. The size guard polls the spilled
|
|
55
55
|
// stdout/stderr files every interval and SIGKILLs the child once the
|
|
56
|
-
// combined size exceeds SHELL_OUTPUT_DISK_CAP
|
|
57
|
-
// upstream cadence — short enough that a runaway loop is caught within a
|
|
56
|
+
// combined size exceeds SHELL_OUTPUT_DISK_CAP — short enough that a runaway loop is caught within a
|
|
58
57
|
// few seconds, long enough that the stat overhead is negligible.
|
|
59
58
|
const SIZE_WATCHDOG_INTERVAL_MS = 1_000;
|
|
60
59
|
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// pyenv / mise / asdf / direnv setup the user gets in their interactive
|
|
9
9
|
// terminal — without paying a fresh login-shell startup on every call.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
// createAndSaveSnapshot). Simpler scope: bash and zsh only, no embedded
|
|
11
|
+
// Scope: bash and zsh only, no embedded
|
|
13
12
|
// search-tool injection (mixdog ships its own grep/glob helpers).
|
|
14
13
|
|
|
15
14
|
import { spawn } from 'node:child_process';
|
|
@@ -34,8 +33,7 @@ const _cache = new Map();
|
|
|
34
33
|
// command. Cleared on process exit (process-scoped Set).
|
|
35
34
|
const _failedShells = new Set();
|
|
36
35
|
|
|
37
|
-
//
|
|
38
|
-
// cleanupRegistry.ts + ShellSnapshot.ts:534-545). Snapshot files are
|
|
36
|
+
// Snapshot files are
|
|
39
37
|
// session-scoped and must be unlinked on graceful shutdown — otherwise
|
|
40
38
|
// they pile up forever (each rc-file mtime change creates a new file).
|
|
41
39
|
const _activeSnapshots = new Set();
|
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
-
import {
|
|
4
|
-
ListToolsRequestSchema,
|
|
5
|
-
CallToolRequestSchema
|
|
6
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
1
|
import { spawn } from "child_process";
|
|
8
2
|
import * as fs from "fs";
|
|
9
|
-
import * as http from "http";
|
|
10
3
|
import * as os from "os";
|
|
11
4
|
import * as path from "path";
|
|
12
5
|
import { performance } from "perf_hooks";
|
|
13
|
-
import { pathToFileURL } from "url";
|
|
14
6
|
import { createRequire } from "module";
|
|
15
7
|
const _require = createRequire(import.meta.url);
|
|
16
8
|
import { loadConfig, createBackend, loadProfileConfig, DATA_DIR } from "./lib/config.mjs";
|
|
@@ -62,128 +54,27 @@ import {
|
|
|
62
54
|
RUNTIME_ROOT
|
|
63
55
|
} from "./lib/runtime-paths.mjs";
|
|
64
56
|
import { getDiscordToken } from "./lib/config.mjs";
|
|
57
|
+
import { bootProfile, localTimestamp } from "./lib/boot-profile.mjs";
|
|
58
|
+
import {
|
|
59
|
+
isChannelsDegraded,
|
|
60
|
+
logCrash,
|
|
61
|
+
_isBenignCrash,
|
|
62
|
+
BENIGN_CRASH_FATAL_THRESHOLD,
|
|
63
|
+
BENIGN_CRASH_STREAK_WINDOW_MS,
|
|
64
|
+
} from "./lib/crash-log.mjs";
|
|
65
|
+
import { dropTrace, preview, _dtIdxFlush } from "./lib/index-drop-trace.mjs";
|
|
66
|
+
import { normalizeWhisperLanguage, detectDeviceLanguage } from "./lib/whisper-language.mjs";
|
|
65
67
|
const memoryClientModulePath = new URL("./lib/memory-client.mjs", import.meta.url).href;
|
|
66
68
|
const {
|
|
67
69
|
appendEntry: memoryAppendEntry,
|
|
68
70
|
ingestTranscript: memoryIngestTranscript,
|
|
69
71
|
} = await import(memoryClientModulePath);
|
|
70
|
-
const DEFAULT_PLUGIN_VERSION = "0.0.1";
|
|
71
|
-
const BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ""));
|
|
72
|
-
const BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance.now());
|
|
73
|
-
function bootProfile(event, fields = {}) {
|
|
74
|
-
if (!BOOT_PROFILE_ENABLED) return;
|
|
75
|
-
const elapsedMs = performance.now() - BOOT_PROFILE_START;
|
|
76
|
-
const parts = [`[mixdog-boot] +${elapsedMs.toFixed(1)}ms`, `channels:${event}`];
|
|
77
|
-
for (const [key, value] of Object.entries(fields || {})) {
|
|
78
|
-
if (value === undefined || value === null || value === "") continue;
|
|
79
|
-
parts.push(`${key}=${String(value).replace(/\s+/g, "_")}`);
|
|
80
|
-
}
|
|
81
|
-
try { process.stderr.write(`${parts.join(" ")}\n`); } catch {}
|
|
82
|
-
}
|
|
83
|
-
function localTimestamp() {
|
|
84
|
-
return (/* @__PURE__ */ new Date()).toLocaleString("sv-SE", { hour12: false });
|
|
85
|
-
}
|
|
86
|
-
function readPluginVersion() {
|
|
87
|
-
try {
|
|
88
|
-
const pkg = JSON.parse(fs.readFileSync(new URL("../../../package.json", import.meta.url), "utf8"));
|
|
89
|
-
return pkg.version || DEFAULT_PLUGIN_VERSION;
|
|
90
|
-
} catch {
|
|
91
|
-
return DEFAULT_PLUGIN_VERSION;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const PLUGIN_VERSION = readPluginVersion();
|
|
95
|
-
let crashLogging = false;
|
|
96
|
-
let _channelsDegraded = false;
|
|
97
|
-
let _stderrBroken = false;
|
|
98
|
-
function isChannelsDegraded() { return _channelsDegraded; }
|
|
99
|
-
|
|
100
|
-
// stderr can break when the parent stdio pipe closes. Node then emits an
|
|
101
|
-
// async 'error' on process.stderr, which sync try/catch around write() does
|
|
102
|
-
// not catch — without a listener, that error becomes uncaughtException and
|
|
103
|
-
// re-enters logCrash, looping until the disk fills. Register a suppressor
|
|
104
|
-
// once at load time and stop writing to stderr after the first EPIPE so the
|
|
105
|
-
// loop cannot start.
|
|
106
|
-
try {
|
|
107
|
-
process.stderr.on('error', (e) => {
|
|
108
|
-
if (e && (e.code === 'EPIPE' || /EPIPE/.test(String(e.message || '')))) {
|
|
109
|
-
_stderrBroken = true;
|
|
110
|
-
_channelsDegraded = true;
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
} catch {}
|
|
114
|
-
|
|
115
|
-
// Crash log guards: dedup repeated identical errors (a single broken handler
|
|
116
|
-
// can fire thousands of times per minute) and rotate at a 10 MB cap so the
|
|
117
|
-
// file cannot grow unbounded. One .old generation is kept; older rolls drop.
|
|
118
|
-
const CRASH_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
119
|
-
let _lastCrashSig = "";
|
|
120
|
-
let _crashRepeatCount = 0;
|
|
121
|
-
|
|
122
|
-
function _writeCrashLine(crashLog, line) {
|
|
123
|
-
try {
|
|
124
|
-
let size = 0;
|
|
125
|
-
try { size = fs.statSync(crashLog).size; } catch {}
|
|
126
|
-
if (size + line.length > CRASH_LOG_MAX_BYTES) {
|
|
127
|
-
try { fs.renameSync(crashLog, crashLog + ".old"); } catch {}
|
|
128
|
-
}
|
|
129
|
-
fs.appendFileSync(crashLog, line);
|
|
130
|
-
} catch {}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function logCrash(label, err) {
|
|
134
|
-
if (crashLogging) return;
|
|
135
|
-
crashLogging = true;
|
|
136
|
-
const msg = `[${localTimestamp()}] mixdog: ${label}: ${err}
|
|
137
|
-
${err instanceof Error ? err.stack : ""}
|
|
138
|
-
`;
|
|
139
|
-
if (!_stderrBroken) {
|
|
140
|
-
try { process.stderr.write(msg); } catch (e) {
|
|
141
|
-
if (e && (e.code === 'EPIPE' || /EPIPE/.test(String(e.message || '')))) {
|
|
142
|
-
_stderrBroken = true;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const sig = `${label}|${err && err.message ? err.message : String(err)}`;
|
|
147
|
-
const crashLog = path.join(DATA_DIR, "crash.log");
|
|
148
|
-
if (sig === _lastCrashSig) {
|
|
149
|
-
// Same error repeating — count it but skip the disk write. The next
|
|
150
|
-
// distinct error (or EPIPE branch below) flushes the suppressed total.
|
|
151
|
-
_crashRepeatCount += 1;
|
|
152
|
-
} else {
|
|
153
|
-
if (_crashRepeatCount > 0) {
|
|
154
|
-
_writeCrashLine(crashLog, `[${localTimestamp()}] mixdog: previous error repeated ${_crashRepeatCount} more time(s)\n`);
|
|
155
|
-
_crashRepeatCount = 0;
|
|
156
|
-
}
|
|
157
|
-
_lastCrashSig = sig;
|
|
158
|
-
_writeCrashLine(crashLog, msg);
|
|
159
|
-
}
|
|
160
|
-
if (err instanceof Error && err.message.includes("EPIPE")) {
|
|
161
|
-
_channelsDegraded = true;
|
|
162
|
-
_stderrBroken = true;
|
|
163
|
-
}
|
|
164
|
-
crashLogging = false;
|
|
165
|
-
}
|
|
166
72
|
// Zombie-Lead repro (2026-07-02): logCrash-then-survive left a worker alive
|
|
167
73
|
// after an unhandled rejection whose async state was already corrupted
|
|
168
74
|
// (observed: EPERM on active-instance.json rename retry), so it spun
|
|
169
75
|
// forever doing nothing useful — a zombie Lead. Fatal-exit on repeat.
|
|
170
|
-
// Benign whitelist: transient EPERM/EACCES/EBUSY on the active-instance
|
|
171
|
-
// rename path is expected under Windows file-lock contention and is
|
|
172
|
-
// already retried elsewhere (atomic-file.mjs RETRY_CODES) — a single
|
|
173
|
-
// occurrence must NOT be fatal, only a run of 3+ in a row without an
|
|
174
|
-
// intervening distinct/successful event.
|
|
175
|
-
const BENIGN_CRASH_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
176
|
-
const BENIGN_CRASH_FATAL_THRESHOLD = 3;
|
|
177
|
-
// "In a row" needs a time dimension: benign errors minutes/hours apart are
|
|
178
|
-
// independent contention events, not a corrupted-state run. Only count a
|
|
179
|
-
// streak when hits land within this window of the previous one.
|
|
180
|
-
const BENIGN_CRASH_STREAK_WINDOW_MS = 60_000;
|
|
181
76
|
let _benignCrashStreak = 0;
|
|
182
77
|
let _lastBenignCrashAt = 0;
|
|
183
|
-
function _isBenignCrash(err) {
|
|
184
|
-
const code = err?.code || (/\b(EPERM|EACCES|EBUSY)\b/.exec(String(err?.message || err)) || [])[0];
|
|
185
|
-
return BENIGN_CRASH_CODES.has(code);
|
|
186
|
-
}
|
|
187
78
|
function _fatalCrash(label, err) {
|
|
188
79
|
logCrash(label, err);
|
|
189
80
|
const benign = _isBenignCrash(err);
|
|
@@ -236,17 +127,6 @@ let config = loadConfig();
|
|
|
236
127
|
let backend = createBackend(config);
|
|
237
128
|
const INSTANCE_ID = makeInstanceId();
|
|
238
129
|
const TERMINAL_LEAD_PID = getTerminalLeadPid();
|
|
239
|
-
// ── drop-trace instrumentation ──────────────────────────────────────────────
|
|
240
|
-
const _dropTraceLog = path.join(DATA_DIR, "drop-trace.log");
|
|
241
|
-
const DROP_TRACE_ENABLED =
|
|
242
|
-
process.env.MIXDOG_DROP_TRACE === "1" ||
|
|
243
|
-
process.env.MIXDOG_DROP_TRACE === "true" ||
|
|
244
|
-
process.env.MIXDOG_DEBUG_CHANNELS === "1" ||
|
|
245
|
-
process.env.MIXDOG_DEBUG_CHANNELS === "true";
|
|
246
|
-
// One-shot rotation for drop-trace.log at worker boot.
|
|
247
|
-
if (DROP_TRACE_ENABLED) {
|
|
248
|
-
try { if (fs.statSync(_dropTraceLog).size > 10 * 1024 * 1024) fs.renameSync(_dropTraceLog, _dropTraceLog + '.1') } catch {}
|
|
249
|
-
}
|
|
250
130
|
// Rotate additional worker logs (10 MB threshold).
|
|
251
131
|
for (const _rotLog of ["channels-worker.log", "schedule.log", "event.log", "memory-worker.log", "mcp-debug.log", "webhook.log", "pg.log", "session-start.log"]) {
|
|
252
132
|
const _rotPath = path.join(DATA_DIR, _rotLog);
|
|
@@ -287,42 +167,6 @@ try {
|
|
|
287
167
|
try {
|
|
288
168
|
pruneStalePluginDataLogSiblings(DATA_DIR, DEFAULT_STALE_LOG_SIBLING_MAX);
|
|
289
169
|
} catch {}
|
|
290
|
-
|
|
291
|
-
// ── Buffered drop-trace writer (channels/index) ──────────────────────────────
|
|
292
|
-
// Flushes every 1 s OR when buffer reaches 64 KB — whichever fires first.
|
|
293
|
-
// Drains on process exit so no log lines are lost.
|
|
294
|
-
let _dtIdxBuf = "";
|
|
295
|
-
let _dtIdxBytes = 0;
|
|
296
|
-
let _dtIdxFlushTimer = null;
|
|
297
|
-
let _dtIdxStream = null;
|
|
298
|
-
function _dtIdxGetStream() {
|
|
299
|
-
if (!_dtIdxStream) _dtIdxStream = fs.createWriteStream(_dropTraceLog, { flags: "a" });
|
|
300
|
-
return _dtIdxStream;
|
|
301
|
-
}
|
|
302
|
-
async function _dtIdxFlush() {
|
|
303
|
-
if (_dtIdxFlushTimer) { clearTimeout(_dtIdxFlushTimer); _dtIdxFlushTimer = null; }
|
|
304
|
-
if (!_dtIdxBuf) return;
|
|
305
|
-
const stream = _dtIdxGetStream();
|
|
306
|
-
const buf = _dtIdxBuf;
|
|
307
|
-
_dtIdxBuf = "";
|
|
308
|
-
_dtIdxBytes = 0;
|
|
309
|
-
try {
|
|
310
|
-
const ok = stream.write(buf);
|
|
311
|
-
if (!ok) { const { once } = await import("node:events"); await once(stream, "drain").catch(() => {}); }
|
|
312
|
-
} catch {}
|
|
313
|
-
}
|
|
314
|
-
function _dtIdxScheduleFlush() {
|
|
315
|
-
if (_dtIdxFlushTimer) return;
|
|
316
|
-
_dtIdxFlushTimer = setTimeout(() => { void _dtIdxFlush(); }, 1000);
|
|
317
|
-
if (_dtIdxFlushTimer.unref) _dtIdxFlushTimer.unref();
|
|
318
|
-
}
|
|
319
|
-
function _dtIdxAppend(line) {
|
|
320
|
-
_dtIdxBuf += line;
|
|
321
|
-
_dtIdxBytes += Buffer.byteLength(line);
|
|
322
|
-
if (_dtIdxBytes >= 65536) { void _dtIdxFlush(); return; }
|
|
323
|
-
_dtIdxScheduleFlush();
|
|
324
|
-
}
|
|
325
|
-
process.on("exit", () => { void _dtIdxFlush(); });
|
|
326
170
|
// SIGTERM: flush the drop-trace buffer, but do NOT exit here. In worker
|
|
327
171
|
// mode the graceful `_channelsShutdownHandler` below owns shutdown
|
|
328
172
|
// (stop() → cleanup → process.exit). In non-worker mode no SIGTERM
|
|
@@ -332,21 +176,6 @@ process.on("SIGTERM", () => {
|
|
|
332
176
|
void _dtIdxFlush();
|
|
333
177
|
if (!_isWorkerMode) process.exit(0);
|
|
334
178
|
});
|
|
335
|
-
|
|
336
|
-
function preview(text) {
|
|
337
|
-
if (!text) return "";
|
|
338
|
-
const s = String(text).replace(/\n/g, "\\n");
|
|
339
|
-
return s.length > 120 ? s.slice(0, 120) + "…" : s;
|
|
340
|
-
}
|
|
341
|
-
function dropTrace(event, fields) {
|
|
342
|
-
if (!DROP_TRACE_ENABLED) return;
|
|
343
|
-
try {
|
|
344
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
345
|
-
const loc = `[${ts}][pid=${process.pid}] ${event}`;
|
|
346
|
-
const kv = fields ? " " + Object.entries(fields).map(([k, v]) => `${k}=${v}`).join(" ") : "";
|
|
347
|
-
_dtIdxAppend(loc + kv + "\n");
|
|
348
|
-
} catch {}
|
|
349
|
-
}
|
|
350
179
|
// ────────────────────────────────────────────────────────────────────────────
|
|
351
180
|
ensureRuntimeDirs();
|
|
352
181
|
cleanupStaleRuntimeFiles();
|
|
@@ -1620,41 +1449,6 @@ function runCmd(cmd, args, capture = false) {
|
|
|
1620
1449
|
proc.on("error", reject);
|
|
1621
1450
|
});
|
|
1622
1451
|
}
|
|
1623
|
-
let resolvedWhisperLanguage = null;
|
|
1624
|
-
function normalizeWhisperLanguage(value) {
|
|
1625
|
-
const raw = String(value ?? "").trim().toLowerCase();
|
|
1626
|
-
if (!raw || raw === "auto") return null;
|
|
1627
|
-
if (raw.startsWith("ko")) return "ko";
|
|
1628
|
-
if (raw.startsWith("ja")) return "ja";
|
|
1629
|
-
if (raw.startsWith("en")) return "en";
|
|
1630
|
-
if (raw.startsWith("zh")) return "zh";
|
|
1631
|
-
if (raw.startsWith("de")) return "de";
|
|
1632
|
-
if (raw.startsWith("fr")) return "fr";
|
|
1633
|
-
if (raw.startsWith("es")) return "es";
|
|
1634
|
-
if (raw.startsWith("it")) return "it";
|
|
1635
|
-
if (raw.startsWith("pt")) return "pt";
|
|
1636
|
-
if (raw.startsWith("ru")) return "ru";
|
|
1637
|
-
return raw;
|
|
1638
|
-
}
|
|
1639
|
-
function detectDeviceLanguage() {
|
|
1640
|
-
if (resolvedWhisperLanguage) return resolvedWhisperLanguage;
|
|
1641
|
-
const candidates = [
|
|
1642
|
-
process.env.MIXDOG_CHANNELS_WHISPER_LANGUAGE,
|
|
1643
|
-
process.env.LC_ALL,
|
|
1644
|
-
process.env.LC_MESSAGES,
|
|
1645
|
-
process.env.LANG,
|
|
1646
|
-
Intl.DateTimeFormat().resolvedOptions().locale
|
|
1647
|
-
];
|
|
1648
|
-
for (const candidate of candidates) {
|
|
1649
|
-
const normalized = normalizeWhisperLanguage(candidate);
|
|
1650
|
-
if (normalized) {
|
|
1651
|
-
resolvedWhisperLanguage = normalized;
|
|
1652
|
-
return normalized;
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
resolvedWhisperLanguage = "auto";
|
|
1656
|
-
return resolvedWhisperLanguage;
|
|
1657
|
-
}
|
|
1658
1452
|
// ── voice.transcription concurrency queue (max=1 by default, config-driven) ──
|
|
1659
1453
|
const _voiceTranscriptionQueue = (() => {
|
|
1660
1454
|
let running = 0;
|
|
@@ -1789,23 +1583,10 @@ async function _doTranscribeVoice(audioPath, attachmentId) {
|
|
|
1789
1583
|
}
|
|
1790
1584
|
}
|
|
1791
1585
|
import { TOOL_DEFS } from './tool-defs.mjs';
|
|
1792
|
-
function createHttpMcpServer() {
|
|
1793
|
-
const s = new Server(
|
|
1794
|
-
{ name: "mixdog", version: PLUGIN_VERSION },
|
|
1795
|
-
{ capabilities: { tools: {} }, instructions: INSTRUCTIONS }
|
|
1796
|
-
);
|
|
1797
|
-
s.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS }));
|
|
1798
|
-
s.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1799
|
-
const toolName = req.params.name;
|
|
1800
|
-
const args = req.params.arguments ?? {};
|
|
1801
|
-
return handleToolCallWithBridgeRetry(toolName, args);
|
|
1802
|
-
});
|
|
1803
|
-
return s;
|
|
1804
|
-
}
|
|
1805
1586
|
// Tool dispatch in worker mode goes through the IPC `call` handler at the
|
|
1806
|
-
// bottom of this file (parent's `callWorker` → `handleToolCall`).
|
|
1807
|
-
//
|
|
1808
|
-
//
|
|
1587
|
+
// bottom of this file (parent's `callWorker` → `handleToolCall`). There is no
|
|
1588
|
+
// orphan worker-level MCP Server: the parent (server.mjs) owns the single
|
|
1589
|
+
// connected transport and routes CallTool through the IPC `call` path.
|
|
1809
1590
|
const BACKEND_TOOLS = /* @__PURE__ */ new Set(["reply", "fetch", "react", "edit_message", "download_attachment", "trigger_schedule"]);
|
|
1810
1591
|
// ── Backend-tool dispatch helpers ───────────────────────────────────────────
|
|
1811
1592
|
// Each helper dispatches through the local backend (this process is always the
|
|
@@ -1908,7 +1689,7 @@ async function dispatchDownloadAttachment(args) {
|
|
|
1908
1689
|
${lines.join("\n")}` }] };
|
|
1909
1690
|
}
|
|
1910
1691
|
async function handleToolCall(name, args, _signal) {
|
|
1911
|
-
if (
|
|
1692
|
+
if (isChannelsDegraded()) {
|
|
1912
1693
|
return { content: [{ type: 'text', text: `[channels degraded] ${name} unavailable — restart MCP to recover` }], isError: true }
|
|
1913
1694
|
}
|
|
1914
1695
|
let result;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { performance } from "perf_hooks";
|
|
2
|
+
|
|
3
|
+
// Boot-timing instrumentation + shared local timestamp helper.
|
|
4
|
+
// Extracted verbatim from channels/index.mjs (behavior-preserving).
|
|
5
|
+
const BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ""));
|
|
6
|
+
const BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance.now());
|
|
7
|
+
|
|
8
|
+
function bootProfile(event, fields = {}) {
|
|
9
|
+
if (!BOOT_PROFILE_ENABLED) return;
|
|
10
|
+
const elapsedMs = performance.now() - BOOT_PROFILE_START;
|
|
11
|
+
const parts = [`[mixdog-boot] +${elapsedMs.toFixed(1)}ms`, `channels:${event}`];
|
|
12
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
13
|
+
if (value === undefined || value === null || value === "") continue;
|
|
14
|
+
parts.push(`${key}=${String(value).replace(/\s+/g, "_")}`);
|
|
15
|
+
}
|
|
16
|
+
try { process.stderr.write(`${parts.join(" ")}\n`); } catch {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function localTimestamp() {
|
|
20
|
+
return (/* @__PURE__ */ new Date()).toLocaleString("sv-SE", { hour12: false });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { BOOT_PROFILE_ENABLED, BOOT_PROFILE_START, bootProfile, localTimestamp };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { DATA_DIR } from "./config.mjs";
|
|
4
|
+
import { localTimestamp } from "./boot-profile.mjs";
|
|
5
|
+
|
|
6
|
+
// Crash logging + degraded-state tracking for the channels worker.
|
|
7
|
+
// Extracted verbatim from channels/index.mjs (behavior-preserving).
|
|
8
|
+
//
|
|
9
|
+
// Degraded/stderr-broken state is module-scoped here; index.mjs reads it via
|
|
10
|
+
// isChannelsDegraded() and installs the process-level unhandledRejection/
|
|
11
|
+
// uncaughtException handlers (which need the worker's stop()).
|
|
12
|
+
let crashLogging = false;
|
|
13
|
+
let _channelsDegraded = false;
|
|
14
|
+
let _stderrBroken = false;
|
|
15
|
+
function isChannelsDegraded() { return _channelsDegraded; }
|
|
16
|
+
|
|
17
|
+
// stderr can break when the parent stdio pipe closes. Node then emits an
|
|
18
|
+
// async 'error' on process.stderr, which sync try/catch around write() does
|
|
19
|
+
// not catch — without a listener, that error becomes uncaughtException and
|
|
20
|
+
// re-enters logCrash, looping until the disk fills. Register a suppressor
|
|
21
|
+
// once at load time and stop writing to stderr after the first EPIPE so the
|
|
22
|
+
// loop cannot start.
|
|
23
|
+
try {
|
|
24
|
+
process.stderr.on('error', (e) => {
|
|
25
|
+
if (e && (e.code === 'EPIPE' || /EPIPE/.test(String(e.message || '')))) {
|
|
26
|
+
_stderrBroken = true;
|
|
27
|
+
_channelsDegraded = true;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
} catch {}
|
|
31
|
+
|
|
32
|
+
// Crash log guards: dedup repeated identical errors (a single broken handler
|
|
33
|
+
// can fire thousands of times per minute) and rotate at a 10 MB cap so the
|
|
34
|
+
// file cannot grow unbounded. One .old generation is kept; older rolls drop.
|
|
35
|
+
const CRASH_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
36
|
+
let _lastCrashSig = "";
|
|
37
|
+
let _crashRepeatCount = 0;
|
|
38
|
+
|
|
39
|
+
function _writeCrashLine(crashLog, line) {
|
|
40
|
+
try {
|
|
41
|
+
let size = 0;
|
|
42
|
+
try { size = fs.statSync(crashLog).size; } catch {}
|
|
43
|
+
if (size + line.length > CRASH_LOG_MAX_BYTES) {
|
|
44
|
+
try { fs.renameSync(crashLog, crashLog + ".old"); } catch {}
|
|
45
|
+
}
|
|
46
|
+
fs.appendFileSync(crashLog, line);
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function logCrash(label, err) {
|
|
51
|
+
if (crashLogging) return;
|
|
52
|
+
crashLogging = true;
|
|
53
|
+
const msg = `[${localTimestamp()}] mixdog: ${label}: ${err}
|
|
54
|
+
${err instanceof Error ? err.stack : ""}
|
|
55
|
+
`;
|
|
56
|
+
if (!_stderrBroken) {
|
|
57
|
+
try { process.stderr.write(msg); } catch (e) {
|
|
58
|
+
if (e && (e.code === 'EPIPE' || /EPIPE/.test(String(e.message || '')))) {
|
|
59
|
+
_stderrBroken = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const sig = `${label}|${err && err.message ? err.message : String(err)}`;
|
|
64
|
+
const crashLog = path.join(DATA_DIR, "crash.log");
|
|
65
|
+
if (sig === _lastCrashSig) {
|
|
66
|
+
// Same error repeating — count it but skip the disk write. The next
|
|
67
|
+
// distinct error (or EPIPE branch below) flushes the suppressed total.
|
|
68
|
+
_crashRepeatCount += 1;
|
|
69
|
+
} else {
|
|
70
|
+
if (_crashRepeatCount > 0) {
|
|
71
|
+
_writeCrashLine(crashLog, `[${localTimestamp()}] mixdog: previous error repeated ${_crashRepeatCount} more time(s)\n`);
|
|
72
|
+
_crashRepeatCount = 0;
|
|
73
|
+
}
|
|
74
|
+
_lastCrashSig = sig;
|
|
75
|
+
_writeCrashLine(crashLog, msg);
|
|
76
|
+
}
|
|
77
|
+
if (err instanceof Error && err.message.includes("EPIPE")) {
|
|
78
|
+
_channelsDegraded = true;
|
|
79
|
+
_stderrBroken = true;
|
|
80
|
+
}
|
|
81
|
+
crashLogging = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Benign whitelist: transient EPERM/EACCES/EBUSY on the active-instance
|
|
85
|
+
// rename path is expected under Windows file-lock contention and is
|
|
86
|
+
// already retried elsewhere (atomic-file.mjs RETRY_CODES) — a single
|
|
87
|
+
// occurrence must NOT be fatal, only a run of 3+ in a row without an
|
|
88
|
+
// intervening distinct/successful event.
|
|
89
|
+
const BENIGN_CRASH_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
90
|
+
const BENIGN_CRASH_FATAL_THRESHOLD = 3;
|
|
91
|
+
// "In a row" needs a time dimension: benign errors minutes/hours apart are
|
|
92
|
+
// independent contention events, not a corrupted-state run. Only count a
|
|
93
|
+
// streak when hits land within this window of the previous one.
|
|
94
|
+
const BENIGN_CRASH_STREAK_WINDOW_MS = 60_000;
|
|
95
|
+
function _isBenignCrash(err) {
|
|
96
|
+
const code = err?.code || (/\b(EPERM|EACCES|EBUSY)\b/.exec(String(err?.message || err)) || [])[0];
|
|
97
|
+
return BENIGN_CRASH_CODES.has(code);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
isChannelsDegraded,
|
|
102
|
+
logCrash,
|
|
103
|
+
_isBenignCrash,
|
|
104
|
+
BENIGN_CRASH_FATAL_THRESHOLD,
|
|
105
|
+
BENIGN_CRASH_STREAK_WINDOW_MS,
|
|
106
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { DATA_DIR } from "./config.mjs";
|
|
4
|
+
|
|
5
|
+
// Drop-trace instrumentation for channels/index.mjs.
|
|
6
|
+
// Extracted verbatim (behavior-preserving). Distinct from lib/drop-trace.mjs
|
|
7
|
+
// (which the output-forwarder uses): this instance is owned by index.mjs and
|
|
8
|
+
// keeps its own buffered writer + `preview` helper so the two trace streams
|
|
9
|
+
// stay byte-identical to the pre-split behavior.
|
|
10
|
+
const _dropTraceLog = path.join(DATA_DIR, "drop-trace.log");
|
|
11
|
+
const DROP_TRACE_ENABLED =
|
|
12
|
+
process.env.MIXDOG_DROP_TRACE === "1" ||
|
|
13
|
+
process.env.MIXDOG_DROP_TRACE === "true" ||
|
|
14
|
+
process.env.MIXDOG_DEBUG_CHANNELS === "1" ||
|
|
15
|
+
process.env.MIXDOG_DEBUG_CHANNELS === "true";
|
|
16
|
+
// One-shot rotation for drop-trace.log at worker boot.
|
|
17
|
+
if (DROP_TRACE_ENABLED) {
|
|
18
|
+
try { if (fs.statSync(_dropTraceLog).size > 10 * 1024 * 1024) fs.renameSync(_dropTraceLog, _dropTraceLog + '.1') } catch {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── Buffered drop-trace writer (channels/index) ──────────────────────────────
|
|
22
|
+
// Flushes every 1 s OR when buffer reaches 64 KB — whichever fires first.
|
|
23
|
+
// Drains on process exit so no log lines are lost.
|
|
24
|
+
let _dtIdxBuf = "";
|
|
25
|
+
let _dtIdxBytes = 0;
|
|
26
|
+
let _dtIdxFlushTimer = null;
|
|
27
|
+
let _dtIdxStream = null;
|
|
28
|
+
function _dtIdxGetStream() {
|
|
29
|
+
if (!_dtIdxStream) _dtIdxStream = fs.createWriteStream(_dropTraceLog, { flags: "a" });
|
|
30
|
+
return _dtIdxStream;
|
|
31
|
+
}
|
|
32
|
+
async function _dtIdxFlush() {
|
|
33
|
+
if (_dtIdxFlushTimer) { clearTimeout(_dtIdxFlushTimer); _dtIdxFlushTimer = null; }
|
|
34
|
+
if (!_dtIdxBuf) return;
|
|
35
|
+
const stream = _dtIdxGetStream();
|
|
36
|
+
const buf = _dtIdxBuf;
|
|
37
|
+
_dtIdxBuf = "";
|
|
38
|
+
_dtIdxBytes = 0;
|
|
39
|
+
try {
|
|
40
|
+
const ok = stream.write(buf);
|
|
41
|
+
if (!ok) { const { once } = await import("node:events"); await once(stream, "drain").catch(() => {}); }
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
function _dtIdxScheduleFlush() {
|
|
45
|
+
if (_dtIdxFlushTimer) return;
|
|
46
|
+
_dtIdxFlushTimer = setTimeout(() => { void _dtIdxFlush(); }, 1000);
|
|
47
|
+
if (_dtIdxFlushTimer.unref) _dtIdxFlushTimer.unref();
|
|
48
|
+
}
|
|
49
|
+
function _dtIdxAppend(line) {
|
|
50
|
+
_dtIdxBuf += line;
|
|
51
|
+
_dtIdxBytes += Buffer.byteLength(line);
|
|
52
|
+
if (_dtIdxBytes >= 65536) { void _dtIdxFlush(); return; }
|
|
53
|
+
_dtIdxScheduleFlush();
|
|
54
|
+
}
|
|
55
|
+
process.on("exit", () => { void _dtIdxFlush(); });
|
|
56
|
+
|
|
57
|
+
function preview(text) {
|
|
58
|
+
if (!text) return "";
|
|
59
|
+
const s = String(text).replace(/\n/g, "\\n");
|
|
60
|
+
return s.length > 120 ? s.slice(0, 120) + "…" : s;
|
|
61
|
+
}
|
|
62
|
+
function dropTrace(event, fields) {
|
|
63
|
+
if (!DROP_TRACE_ENABLED) return;
|
|
64
|
+
try {
|
|
65
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
66
|
+
const loc = `[${ts}][pid=${process.pid}] ${event}`;
|
|
67
|
+
const kv = fields ? " " + Object.entries(fields).map(([k, v]) => `${k}=${v}`).join(" ") : "";
|
|
68
|
+
_dtIdxAppend(loc + kv + "\n");
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { DROP_TRACE_ENABLED, dropTrace, preview, _dtIdxFlush };
|
|
@@ -154,6 +154,14 @@ class OutputForwarder {
|
|
|
154
154
|
try {
|
|
155
155
|
const stat = this._pendingStat ?? statSync(this.transcriptPath);
|
|
156
156
|
this._pendingStat = null;
|
|
157
|
+
// File shrank below our cursor: the transcript was rotated out from
|
|
158
|
+
// under us (buffered-appender rotation, or an external truncation).
|
|
159
|
+
// Treat it as a fresh file and reset the cursor instead of getting
|
|
160
|
+
// stuck forever (stat.size <= readFileSize would otherwise short
|
|
161
|
+
// circuit below and never read the new content).
|
|
162
|
+
if (stat.size < this.readFileSize) {
|
|
163
|
+
this.readFileSize = 0;
|
|
164
|
+
}
|
|
157
165
|
if (stat.size <= this.readFileSize) {
|
|
158
166
|
return { lines: [], nextFileSize: this.readFileSize };
|
|
159
167
|
}
|