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
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
fsyncSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
statSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
writeSync,
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { freemem, totalmem } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { promisify } from 'node:util';
|
|
17
|
+
import { withFileLockSync } from './atomic-file.mjs';
|
|
18
|
+
import { resolvePluginData } from './plugin-paths.mjs';
|
|
19
|
+
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
export const MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES = 64 * 1024;
|
|
22
|
+
const SNAPSHOT_NAME = 'memory-pressure.jsonl';
|
|
23
|
+
const SNAPSHOT_INTERVAL_MS = 10 * 60 * 1000;
|
|
24
|
+
const SNAPSHOT_RATE_LIMIT_MS = 60 * 1000;
|
|
25
|
+
const SNAPSHOT_LOCK_TIMEOUT_MS = 2000;
|
|
26
|
+
const SNAPSHOT_LOCK_STALE_MS = 10000;
|
|
27
|
+
|
|
28
|
+
let periodicTimer = null;
|
|
29
|
+
let lastPressureSnapshotAt = 0;
|
|
30
|
+
|
|
31
|
+
function enabled() {
|
|
32
|
+
return process.env.MIXDOG_MEMORY_SNAPSHOT !== '0';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function snapshotPaths() {
|
|
36
|
+
const directory = join(resolvePluginData(), 'diagnostics');
|
|
37
|
+
const snapshot = join(directory, SNAPSHOT_NAME);
|
|
38
|
+
return {
|
|
39
|
+
directory,
|
|
40
|
+
snapshot,
|
|
41
|
+
previousSnapshot: `${snapshot}.1`,
|
|
42
|
+
lock: join(directory, 'memory-pressure.lock'),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function memoryUsage() {
|
|
47
|
+
try {
|
|
48
|
+
return process.memoryUsage();
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function systemMemory() {
|
|
55
|
+
try {
|
|
56
|
+
return { freeMemoryBytes: freemem(), totalMemoryBytes: totalmem() };
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rotateSnapshot(paths) {
|
|
63
|
+
const size = Number(statSync(paths.snapshot).size) || 0;
|
|
64
|
+
try { unlinkSync(paths.previousSnapshot); } catch {}
|
|
65
|
+
if (size <= MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) {
|
|
66
|
+
renameSync(paths.snapshot, paths.previousSnapshot);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
writeFileSync(paths.previousSnapshot, '', { mode: 0o600 });
|
|
70
|
+
unlinkSync(paths.snapshot);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function appendSnapshot(entry) {
|
|
74
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
75
|
+
if (Buffer.byteLength(line) > MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) return false;
|
|
76
|
+
const paths = snapshotPaths();
|
|
77
|
+
try {
|
|
78
|
+
mkdirSync(paths.directory, { recursive: true, mode: 0o700 });
|
|
79
|
+
return withFileLockSync(paths.lock, () => {
|
|
80
|
+
if (existsSync(paths.previousSnapshot)
|
|
81
|
+
&& statSync(paths.previousSnapshot).size > MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) {
|
|
82
|
+
writeFileSync(paths.previousSnapshot, '', { mode: 0o600 });
|
|
83
|
+
}
|
|
84
|
+
if (existsSync(paths.snapshot)
|
|
85
|
+
&& statSync(paths.snapshot).size + Buffer.byteLength(line) > MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) {
|
|
86
|
+
rotateSnapshot(paths);
|
|
87
|
+
}
|
|
88
|
+
const fd = openSync(paths.snapshot, 'a', 0o600);
|
|
89
|
+
try {
|
|
90
|
+
writeSync(fd, line, null, 'utf8');
|
|
91
|
+
fsyncSync(fd);
|
|
92
|
+
} finally {
|
|
93
|
+
closeSync(fd);
|
|
94
|
+
}
|
|
95
|
+
return statSync(paths.snapshot).size <= MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES;
|
|
96
|
+
}, {
|
|
97
|
+
timeoutMs: SNAPSHOT_LOCK_TIMEOUT_MS,
|
|
98
|
+
staleMs: SNAPSHOT_LOCK_STALE_MS,
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function workingSetMb(bytes) {
|
|
106
|
+
return Math.round((Number(bytes) / (1024 * 1024)) * 10) / 10;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function nullableProcessText(value) {
|
|
110
|
+
if (typeof value !== 'string') return null;
|
|
111
|
+
if (!value.trim()) return null;
|
|
112
|
+
return value.length > 200 ? value.slice(0, 200) : value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function nullablePid(value) {
|
|
116
|
+
if ((typeof value !== 'number' && typeof value !== 'string')
|
|
117
|
+
|| (typeof value === 'string' && !value.trim())) return null;
|
|
118
|
+
const pid = Number(value);
|
|
119
|
+
return Number.isInteger(pid) ? pid : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseWindowsProcesses(stdout) {
|
|
123
|
+
const rows = JSON.parse(String(stdout).trim() || '[]');
|
|
124
|
+
return (Array.isArray(rows) ? rows : [rows])
|
|
125
|
+
.map((row) => ({
|
|
126
|
+
name: String(row?.Name || ''),
|
|
127
|
+
pid: Number(row?.ProcessId),
|
|
128
|
+
workingSetMb: workingSetMb(row?.WorkingSetSize),
|
|
129
|
+
commandLine: nullableProcessText(row?.CommandLine),
|
|
130
|
+
parentPid: nullablePid(row?.ParentProcessId),
|
|
131
|
+
}))
|
|
132
|
+
.filter((row) => row.name && Number.isInteger(row.pid) && Number.isFinite(row.workingSetMb))
|
|
133
|
+
.slice(0, 8);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parsePosixProcesses(stdout) {
|
|
137
|
+
return String(stdout).split(/\r?\n/)
|
|
138
|
+
.map((line) => {
|
|
139
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*?)\s+(\d+)\s*$/);
|
|
140
|
+
if (!match) return null;
|
|
141
|
+
return {
|
|
142
|
+
pid: Number(match[1]),
|
|
143
|
+
parentPid: nullablePid(match[2]),
|
|
144
|
+
name: match[3],
|
|
145
|
+
commandLine: nullableProcessText(match[4]),
|
|
146
|
+
workingSetMb: workingSetMb(Number(match[5]) * 1024),
|
|
147
|
+
};
|
|
148
|
+
})
|
|
149
|
+
.filter((row) => row && Number.isInteger(row.pid) && Number.isFinite(row.workingSetMb))
|
|
150
|
+
.sort((left, right) => right.workingSetMb - left.workingSetMb)
|
|
151
|
+
.slice(0, 8);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function topProcesses() {
|
|
155
|
+
try {
|
|
156
|
+
if (process.platform === 'win32') {
|
|
157
|
+
const { stdout } = await execFileAsync('powershell.exe', [
|
|
158
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
159
|
+
'Get-CimInstance Win32_Process | Sort-Object WorkingSetSize -Descending | Select-Object -First 8 ProcessId,Name,WorkingSetSize,CommandLine,ParentProcessId | ConvertTo-Json -Compress',
|
|
160
|
+
], {
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
timeout: 3000,
|
|
163
|
+
windowsHide: true,
|
|
164
|
+
});
|
|
165
|
+
return parseWindowsProcesses(stdout);
|
|
166
|
+
}
|
|
167
|
+
const { stdout } = await execFileAsync('ps', ['-eo', 'pid=,ppid=,comm=,args=,rss='], {
|
|
168
|
+
encoding: 'utf8',
|
|
169
|
+
timeout: 3000,
|
|
170
|
+
windowsHide: true,
|
|
171
|
+
});
|
|
172
|
+
return parsePosixProcesses(stdout);
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function baseSnapshot(reason) {
|
|
179
|
+
return {
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
pid: process.pid,
|
|
182
|
+
reason,
|
|
183
|
+
memoryUsage: memoryUsage(),
|
|
184
|
+
systemMemory: systemMemory(),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Collect a full attribution snapshot. Process enumeration failures are
|
|
190
|
+
* intentionally omitted from the record, rather than surfacing to callers.
|
|
191
|
+
*/
|
|
192
|
+
export async function captureMemoryPressureSnapshot(reason = 'memory-pressure') {
|
|
193
|
+
if (!enabled()) return false;
|
|
194
|
+
const entry = baseSnapshot(reason);
|
|
195
|
+
const processes = await topProcesses();
|
|
196
|
+
if (processes) entry.topProcesses = processes;
|
|
197
|
+
return appendSnapshot(entry);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function capturePeriodicMemorySnapshot() {
|
|
201
|
+
if (!enabled()) return false;
|
|
202
|
+
return appendSnapshot(baseSnapshot('periodic'));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Schedules one full snapshot without work on the admission path. This
|
|
207
|
+
* process-wide limiter deliberately reserves the interval before scheduling.
|
|
208
|
+
*/
|
|
209
|
+
export function requestMemoryPressureSnapshot(reason) {
|
|
210
|
+
try {
|
|
211
|
+
if (!enabled()) return false;
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
if (now - lastPressureSnapshotAt < SNAPSHOT_RATE_LIMIT_MS) return false;
|
|
214
|
+
lastPressureSnapshotAt = now;
|
|
215
|
+
const immediate = setImmediate(() => {
|
|
216
|
+
void captureMemoryPressureSnapshot(reason).catch(() => {});
|
|
217
|
+
});
|
|
218
|
+
immediate.unref?.();
|
|
219
|
+
return true;
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function armMemoryPressureSampling() {
|
|
226
|
+
if (!enabled() || periodicTimer) return false;
|
|
227
|
+
periodicTimer = setInterval(() => { capturePeriodicMemorySnapshot(); }, SNAPSHOT_INTERVAL_MS);
|
|
228
|
+
periodicTimer.unref?.();
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function memoryPressureSnapshotPath() {
|
|
233
|
+
return snapshotPaths().snapshot;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
armMemoryPressureSampling();
|
|
@@ -28,6 +28,7 @@ const REPORT_NAME = 'mixdog-node-report.json';
|
|
|
28
28
|
const LEDGER_LOCK_NAME = 'process-lifecycle.lock';
|
|
29
29
|
const LEDGER_LOCK_TIMEOUT_MS = 2000;
|
|
30
30
|
const LEDGER_LOCK_STALE_MS = 10000;
|
|
31
|
+
const IDENTITY_UPGRADE_DELAYS_MS = [1000, 9000, 20000];
|
|
31
32
|
const STATE_KEY = Symbol.for('mixdog.processLifecycle.v1');
|
|
32
33
|
const VALID_REASONS = new Set([
|
|
33
34
|
'process-start',
|
|
@@ -119,9 +120,36 @@ function appendEntry(active, entry) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
function currentProcessIdentity() {
|
|
123
|
+
if (process.platform !== 'linux') {
|
|
124
|
+
const value = Math.floor((Date.now() - (process.uptime() * 1000)) / 1000);
|
|
125
|
+
return Number.isSafeInteger(value) && value > 0
|
|
126
|
+
? { kind: 'start-seconds', value, method: 'uptime' }
|
|
127
|
+
: null;
|
|
128
|
+
}
|
|
122
129
|
return processIdentityForPid(process.pid);
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
function windowsProcessIdentities(pids) {
|
|
133
|
+
const identities = new Map();
|
|
134
|
+
if (pids.length === 0) return identities;
|
|
135
|
+
try {
|
|
136
|
+
const out = execFileSync('powershell.exe', [
|
|
137
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
138
|
+
`$ErrorActionPreference='SilentlyContinue'; Get-Process -Id @(${pids.join(',')}) | ForEach-Object { try { "$($_.Id):$(([DateTimeOffset]$_.StartTime).ToUnixTimeSeconds())" } catch {} }`,
|
|
139
|
+
], { encoding: 'utf8', timeout: 2000, windowsHide: true });
|
|
140
|
+
for (const line of String(out).split(/\r?\n/)) {
|
|
141
|
+
const match = /^(\d+):(\d+)$/.exec(line.trim());
|
|
142
|
+
if (!match) continue;
|
|
143
|
+
const pid = Number(match[1]);
|
|
144
|
+
const value = Number(match[2]);
|
|
145
|
+
if (Number.isSafeInteger(pid) && Number.isSafeInteger(value) && value > 0) {
|
|
146
|
+
identities.set(pid, { kind: 'start-seconds', value, method: 'powershell' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch {}
|
|
150
|
+
return identities;
|
|
151
|
+
}
|
|
152
|
+
|
|
125
153
|
function processIdentityForPid(pid) {
|
|
126
154
|
if (process.platform === 'linux') {
|
|
127
155
|
try {
|
|
@@ -136,13 +164,7 @@ function processIdentityForPid(pid) {
|
|
|
136
164
|
}
|
|
137
165
|
try {
|
|
138
166
|
if (process.platform === 'win32') {
|
|
139
|
-
|
|
140
|
-
'-NoProfile', '-NonInteractive', '-Command',
|
|
141
|
-
`([DateTimeOffset](Get-Process -Id ${pid} -ErrorAction Stop).StartTime).ToUnixTimeSeconds()`,
|
|
142
|
-
], { encoding: 'utf8', timeout: 2000, windowsHide: true });
|
|
143
|
-
const text = String(out).trim();
|
|
144
|
-
const value = /^\d+$/.test(text) ? Number(text) : NaN;
|
|
145
|
-
return Number.isSafeInteger(value) ? { kind: 'start-seconds', value } : null;
|
|
167
|
+
return windowsProcessIdentities([pid]).get(pid) || null;
|
|
146
168
|
}
|
|
147
169
|
const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
148
170
|
encoding: 'utf8',
|
|
@@ -150,12 +172,30 @@ function processIdentityForPid(pid) {
|
|
|
150
172
|
windowsHide: true,
|
|
151
173
|
});
|
|
152
174
|
const value = Math.floor(Date.parse(String(out).trim()) / 1000);
|
|
153
|
-
return Number.isInteger(value) ? { kind: 'start-seconds', value } : null;
|
|
175
|
+
return Number.isInteger(value) ? { kind: 'start-seconds', value, method: 'ps' } : null;
|
|
154
176
|
} catch {
|
|
155
177
|
return null;
|
|
156
178
|
}
|
|
157
179
|
}
|
|
158
180
|
|
|
181
|
+
function processIdentitiesForPids(pids) {
|
|
182
|
+
const unique = [...new Set(pids)];
|
|
183
|
+
if (process.platform !== 'win32') {
|
|
184
|
+
return new Map(unique.map((pid) => [pid, processIdentityForPid(pid)]));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const identities = new Map();
|
|
188
|
+
const otherPids = [];
|
|
189
|
+
for (const pid of unique) {
|
|
190
|
+
if (pid === process.pid) identities.set(pid, currentProcessIdentity());
|
|
191
|
+
else otherPids.push(pid);
|
|
192
|
+
}
|
|
193
|
+
for (const [pid, identity] of windowsProcessIdentities(otherPids)) {
|
|
194
|
+
identities.set(pid, identity);
|
|
195
|
+
}
|
|
196
|
+
return identities;
|
|
197
|
+
}
|
|
198
|
+
|
|
159
199
|
function sameProcessIdentity(expected, observed) {
|
|
160
200
|
if (!expected || !observed || expected.kind !== observed.kind) return null;
|
|
161
201
|
if (expected.kind === 'linux-start-ticks') {
|
|
@@ -166,7 +206,10 @@ function sameProcessIdentity(expected, observed) {
|
|
|
166
206
|
if (expected.kind === 'start-seconds') {
|
|
167
207
|
if (!Number.isSafeInteger(expected.value) || expected.value < 1
|
|
168
208
|
|| !Number.isSafeInteger(observed.value) || observed.value < 1) return null;
|
|
169
|
-
|
|
209
|
+
const crossMethod = expected.method === 'uptime' || observed.method === 'uptime';
|
|
210
|
+
return crossMethod
|
|
211
|
+
? Math.abs(expected.value - observed.value) <= 2
|
|
212
|
+
: expected.value === observed.value;
|
|
170
213
|
}
|
|
171
214
|
return null;
|
|
172
215
|
}
|
|
@@ -237,6 +280,28 @@ function writeMarker(active) {
|
|
|
237
280
|
}
|
|
238
281
|
}
|
|
239
282
|
|
|
283
|
+
function scheduleProcessIdentityUpgrade(active) {
|
|
284
|
+
if (active.processIdentity?.method !== 'uptime') return;
|
|
285
|
+
const scheduleAttempt = (attempt) => {
|
|
286
|
+
if (attempt >= IDENTITY_UPGRADE_DELAYS_MS.length) return;
|
|
287
|
+
const timer = setTimeout(() => {
|
|
288
|
+
try {
|
|
289
|
+
if (sharedState().active !== active || !markerOwned(active)) return;
|
|
290
|
+
const processIdentity = processIdentityForPid(process.pid);
|
|
291
|
+
if (processIdentity && processIdentity.method !== 'uptime') {
|
|
292
|
+
const previousIdentity = active.processIdentity;
|
|
293
|
+
active.processIdentity = processIdentity;
|
|
294
|
+
if (writeMarker(active)) return;
|
|
295
|
+
active.processIdentity = previousIdentity;
|
|
296
|
+
}
|
|
297
|
+
} catch {}
|
|
298
|
+
scheduleAttempt(attempt + 1);
|
|
299
|
+
}, IDENTITY_UPGRADE_DELAYS_MS[attempt]);
|
|
300
|
+
timer.unref?.();
|
|
301
|
+
};
|
|
302
|
+
scheduleAttempt(0);
|
|
303
|
+
}
|
|
304
|
+
|
|
240
305
|
function reapVanishedMarkers(active) {
|
|
241
306
|
const candidates = [];
|
|
242
307
|
try {
|
|
@@ -247,20 +312,27 @@ function reapVanishedMarkers(active) {
|
|
|
247
312
|
}
|
|
248
313
|
} catch {}
|
|
249
314
|
if (existsSync(active.legacyMarker)) candidates.push(active.legacyMarker);
|
|
315
|
+
const occupied = [];
|
|
250
316
|
for (const markerPath of candidates) {
|
|
251
317
|
try {
|
|
252
318
|
const previous = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const identityMatch = sameProcessIdentity(
|
|
257
|
-
previous.processIdentity,
|
|
258
|
-
processIdentityForPid(previous.pid),
|
|
259
|
-
);
|
|
260
|
-
vanished = identityMatch === false;
|
|
319
|
+
if (previous?.pid === process.pid && previous?.token !== active.token) {
|
|
320
|
+
if (recordPriorVanished(active, previous)) unlinkSync(markerPath);
|
|
321
|
+
continue;
|
|
261
322
|
}
|
|
262
|
-
|
|
263
|
-
if (
|
|
323
|
+
const liveness = pidLiveness(previous?.pid);
|
|
324
|
+
if (liveness === 'occupied') occupied.push({ markerPath, previous });
|
|
325
|
+
else if (liveness === 'dead' && recordPriorVanished(active, previous)) unlinkSync(markerPath);
|
|
326
|
+
} catch {}
|
|
327
|
+
}
|
|
328
|
+
const identities = processIdentitiesForPids(occupied.map(({ previous }) => previous.pid));
|
|
329
|
+
for (const { markerPath, previous } of occupied) {
|
|
330
|
+
try {
|
|
331
|
+
const identityMatch = sameProcessIdentity(
|
|
332
|
+
previous.processIdentity,
|
|
333
|
+
identities.get(previous.pid) || null,
|
|
334
|
+
);
|
|
335
|
+
if (identityMatch === false && recordPriorVanished(active, previous)) unlinkSync(markerPath);
|
|
264
336
|
} catch {}
|
|
265
337
|
}
|
|
266
338
|
}
|
|
@@ -307,6 +379,7 @@ export function beginProcessLifecycle({
|
|
|
307
379
|
reapVanishedMarkers(active);
|
|
308
380
|
writeMarker(active);
|
|
309
381
|
recordCurrent('process-start');
|
|
382
|
+
scheduleProcessIdentityUpgrade(active);
|
|
310
383
|
if (configureReports && safeCommandLine) {
|
|
311
384
|
try { unlinkSync(`${active.report}.1`); } catch {}
|
|
312
385
|
try { renameSync(active.report, `${active.report}.1`); } catch {}
|
|
@@ -57,6 +57,7 @@ export function installProcessSignalCleanup({
|
|
|
57
57
|
beforeCleanup,
|
|
58
58
|
cleanup,
|
|
59
59
|
afterCleanup,
|
|
60
|
+
restoreTerminal,
|
|
60
61
|
log = writeStderr,
|
|
61
62
|
} = {}) {
|
|
62
63
|
let installed = true;
|
|
@@ -71,6 +72,7 @@ export function installProcessSignalCleanup({
|
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
const hardExit = (code) => {
|
|
75
|
+
try { restoreTerminal?.('forced-cleanup', { code }); } catch {}
|
|
74
76
|
finishProcessLifecycle('forced-cleanup', code);
|
|
75
77
|
try { process.exit(code); } catch {}
|
|
76
78
|
};
|
|
@@ -148,9 +150,13 @@ export function installProcessSignalCleanup({
|
|
|
148
150
|
|
|
149
151
|
if (fatal) {
|
|
150
152
|
add('uncaughtException', (error) => {
|
|
153
|
+
// Run terminal restoration synchronously, before the async cleanup path
|
|
154
|
+
// or its hard-exit fallback can be interrupted by another fatal error.
|
|
155
|
+
try { restoreTerminal?.('uncaughtException', { code: 1, error }); } catch {}
|
|
151
156
|
void run('uncaughtException', { code: 1, shouldExit: exit, error });
|
|
152
157
|
});
|
|
153
158
|
add('unhandledRejection', (error) => {
|
|
159
|
+
try { restoreTerminal?.('unhandledRejection', { code: 1, error }); } catch {}
|
|
154
160
|
void run('unhandledRejection', { code: 1, shouldExit: exit, error });
|
|
155
161
|
});
|
|
156
162
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { freemem, totalmem } from 'node:os';
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { requestMemoryPressureSnapshot } from './memory-snapshot.mjs';
|
|
3
4
|
|
|
4
5
|
const MB = 1024 * 1024;
|
|
5
6
|
|
|
@@ -100,16 +101,20 @@ export class ResourceAdmissionController {
|
|
|
100
101
|
const rssMb = Number(sample.rssBytes) / MB;
|
|
101
102
|
const freeMb = Number(sample.freeMemoryBytes) / MB;
|
|
102
103
|
if (this.limits.maxRssMb > 0 && Number.isFinite(rssMb) && rssMb >= this.limits.maxRssMb) {
|
|
103
|
-
|
|
104
|
+
const error = new ResourcePressureError(
|
|
104
105
|
`Mixdog RSS ${Math.ceil(rssMb)} MB reached ${this.limits.maxRssMb} MB limit; retry after memory recovers`,
|
|
105
106
|
{ kind, rssMb, limitMb: this.limits.maxRssMb, metric: 'rss' },
|
|
106
107
|
);
|
|
108
|
+
requestMemoryPressureSnapshot(error.message);
|
|
109
|
+
return error;
|
|
107
110
|
}
|
|
108
111
|
if (this.limits.minFreeMemoryMb > 0 && Number.isFinite(freeMb) && freeMb < this.limits.minFreeMemoryMb) {
|
|
109
|
-
|
|
112
|
+
const error = new ResourcePressureError(
|
|
110
113
|
`host free memory ${Math.floor(freeMb)} MB is below ${this.limits.minFreeMemoryMb} MB minimum; retry after memory recovers`,
|
|
111
114
|
{ kind, freeMb, limitMb: this.limits.minFreeMemoryMb, metric: 'free-memory' },
|
|
112
115
|
);
|
|
116
|
+
requestMemoryPressureSnapshot(error.message);
|
|
117
|
+
return error;
|
|
113
118
|
}
|
|
114
119
|
return null;
|
|
115
120
|
}
|
|
@@ -2,12 +2,12 @@ import {
|
|
|
2
2
|
channelSetup,
|
|
3
3
|
deleteSchedule,
|
|
4
4
|
deleteWebhook,
|
|
5
|
-
|
|
5
|
+
setChannelAsync,
|
|
6
6
|
saveSchedule,
|
|
7
7
|
saveWebhook,
|
|
8
8
|
setScheduleEnabled,
|
|
9
9
|
setWebhookEnabled,
|
|
10
|
-
|
|
10
|
+
setWebhookConfigAsync,
|
|
11
11
|
} from '../standalone/channel-admin.mjs';
|
|
12
12
|
|
|
13
13
|
// Channel/webhook/schedule config surface. Extracted verbatim from the runtime
|
|
@@ -20,19 +20,19 @@ export function createChannelConfigApi({ flushBackendSave, channels, reloadChann
|
|
|
20
20
|
// Flush a pending debounced backend switch first so setup readers
|
|
21
21
|
// (Settings → Channel Setting, remote toggles) never observe the
|
|
22
22
|
// previous backend during the 150ms debounce window.
|
|
23
|
-
try { flushBackendSave(); } catch {}
|
|
23
|
+
try { await flushBackendSave(); } catch {}
|
|
24
24
|
return channelSetup();
|
|
25
25
|
},
|
|
26
26
|
getChannelWorkerStatus() {
|
|
27
27
|
return channels.status();
|
|
28
28
|
},
|
|
29
|
-
setChannel(entry) {
|
|
30
|
-
const result =
|
|
29
|
+
async setChannel(entry) {
|
|
30
|
+
const result = await setChannelAsync(entry);
|
|
31
31
|
reloadChannelsSoon();
|
|
32
32
|
return result;
|
|
33
33
|
},
|
|
34
|
-
setWebhookConfig(patch) {
|
|
35
|
-
const result =
|
|
34
|
+
async setWebhookConfig(patch) {
|
|
35
|
+
const result = await setWebhookConfigAsync(patch);
|
|
36
36
|
reloadChannelsSoon();
|
|
37
37
|
return result;
|
|
38
38
|
},
|
|
@@ -30,7 +30,6 @@ export function createConfigLifecycle({
|
|
|
30
30
|
// shared modules / helpers
|
|
31
31
|
cfgMod,
|
|
32
32
|
sharedCfgMod,
|
|
33
|
-
setBackend,
|
|
34
33
|
setBackendAsync,
|
|
35
34
|
setConfiguredShell,
|
|
36
35
|
normalizeSystemShellConfig,
|
|
@@ -218,21 +217,6 @@ export function createConfigLifecycle({
|
|
|
218
217
|
return p;
|
|
219
218
|
}
|
|
220
219
|
|
|
221
|
-
function flushBackendSave() {
|
|
222
|
-
if (backendSaveTimer) {
|
|
223
|
-
clearTimeout(backendSaveTimer);
|
|
224
|
-
backendSaveTimer = null;
|
|
225
|
-
}
|
|
226
|
-
if (pendingBackendName === null) return;
|
|
227
|
-
const name = pendingBackendName;
|
|
228
|
-
pendingBackendName = null;
|
|
229
|
-
try {
|
|
230
|
-
setBackend(name);
|
|
231
|
-
} catch (err) {
|
|
232
|
-
process.stderr.write(`[channels] debounced setBackend failed: ${err?.message || err}\n`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
220
|
function scheduleBackendSave(name) {
|
|
237
221
|
pendingBackendName = name;
|
|
238
222
|
if (backendSaveTimer) clearTimeout(backendSaveTimer);
|
|
@@ -335,6 +319,21 @@ export function createConfigLifecycle({
|
|
|
335
319
|
return p;
|
|
336
320
|
}
|
|
337
321
|
|
|
322
|
+
// Teardown barrier for every in-process writer that can hold the shared
|
|
323
|
+
// mixdog-config lock. Start/drain all debounce channels through their async
|
|
324
|
+
// variants, then resolve only when every promise tail (including skills,
|
|
325
|
+
// which config flushes after its whole-section write) has settled.
|
|
326
|
+
async function flushAllConfigSavesAsync() {
|
|
327
|
+
await Promise.all([
|
|
328
|
+
flushConfigSaveAsync(),
|
|
329
|
+
flushBackendSaveAsync(),
|
|
330
|
+
flushOutputStyleSaveAsync(),
|
|
331
|
+
]);
|
|
332
|
+
// The shared config layer also tracks writes started directly by channel,
|
|
333
|
+
// webhook, voice, Discord access, and future async RMW callers.
|
|
334
|
+
await sharedCfgMod.pendingConfigWrites();
|
|
335
|
+
}
|
|
336
|
+
|
|
338
337
|
function flushOutputStyleSave() {
|
|
339
338
|
if (outputStyleSaveTimer) {
|
|
340
339
|
clearTimeout(outputStyleSaveTimer);
|
|
@@ -418,12 +417,16 @@ export function createConfigLifecycle({
|
|
|
418
417
|
adoptConfig,
|
|
419
418
|
saveConfigAndAdopt,
|
|
420
419
|
flushConfigSave,
|
|
421
|
-
|
|
420
|
+
// Every lifecycle flush uses the async lock path. A synchronous waiter on
|
|
421
|
+
// the same lock would block the event loop needed by an in-flight async
|
|
422
|
+
// writer, so callers must await this before a dependent read/start.
|
|
423
|
+
flushBackendSave: flushBackendSaveAsync,
|
|
422
424
|
scheduleBackendSave,
|
|
423
425
|
flushSkillsSave,
|
|
424
426
|
scheduleSkillsSave,
|
|
425
427
|
flushOutputStyleSave,
|
|
426
428
|
scheduleOutputStyleSave,
|
|
429
|
+
flushAllConfigSavesAsync,
|
|
427
430
|
// reload / ensure
|
|
428
431
|
reloadFullConfig,
|
|
429
432
|
ensureFullConfig,
|
|
@@ -96,7 +96,7 @@ export function createCwdPlugins({
|
|
|
96
96
|
return next;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function applyResolvedCwd(nextCwd, { markRefresh = true } = {}) {
|
|
99
|
+
function applyResolvedCwd(nextCwd, { markRefresh = true, waitForMcpReset = false } = {}) {
|
|
100
100
|
const resolved = resolve(nextCwd);
|
|
101
101
|
const stat = statSync(resolved);
|
|
102
102
|
if (!stat.isDirectory()) throw new Error(`cwd: not a directory: ${resolved}`);
|
|
@@ -125,16 +125,16 @@ export function createCwdPlugins({
|
|
|
125
125
|
const delay = getCodeGraphPrewarmDelayMs();
|
|
126
126
|
scheduleCodeGraphPrewarm(changed ? 0 : delay, changed ? 'cwd-change' : 'cwd');
|
|
127
127
|
}
|
|
128
|
-
// Project-local `.mcp.json` follows the cwd
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
128
|
+
// Project-local `.mcp.json` follows the cwd. Ordinary in-session cwd changes
|
|
129
|
+
// reconnect in the background, while desktop context replacement requests
|
|
130
|
+
// an awaitable reset so the next session/turn cannot observe the old registry.
|
|
131
|
+
let mcpReset = null;
|
|
132
132
|
if (changed) {
|
|
133
133
|
try {
|
|
134
134
|
const nextKey = resolved + '\u0000' + JSON.stringify(readProjectMcpServers(resolved));
|
|
135
135
|
if (nextKey !== getLastProjectMcpKey()) {
|
|
136
136
|
setLastProjectMcpKey(nextKey);
|
|
137
|
-
|
|
137
|
+
mcpReset = Promise.resolve(connectConfiguredMcp({ reset: true }))
|
|
138
138
|
.then(() => invalidatePreSessionToolSurface())
|
|
139
139
|
.catch(() => {});
|
|
140
140
|
}
|
|
@@ -145,7 +145,9 @@ export function createCwdPlugins({
|
|
|
145
145
|
if (changed) {
|
|
146
146
|
try { void hooks.dispatch('CwdChanged', hookCommonPayload({ cwd: currentCwd })); } catch {}
|
|
147
147
|
}
|
|
148
|
-
return
|
|
148
|
+
return waitForMcpReset
|
|
149
|
+
? Promise.resolve(mcpReset).then(() => currentCwd)
|
|
150
|
+
: currentCwd;
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
async function refreshSessionForCwdIfNeeded(reason = 'cwd-change') {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// Environment-variable coercion helpers shared by
|
|
2
|
-
// Extracted verbatim from mixdog-session-runtime.mjs during the facade split.
|
|
1
|
+
// Environment-variable coercion helpers shared by session-runtime modules.
|
|
3
2
|
export function envFlag(name) {
|
|
4
3
|
return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
|
|
5
4
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { monitorEventLoopDelay } from 'node:perf_hooks';
|
|
2
|
+
import { envFlag } from './env.mjs';
|
|
3
|
+
|
|
4
|
+
const HITCH_PROFILE_ENABLED = envFlag('MIXDOG_HITCH_PROFILE');
|
|
5
|
+
|
|
6
|
+
function positiveNumberEnv(name, fallback) {
|
|
7
|
+
const value = Number(process.env[name]);
|
|
8
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// The histogram and sampling timer are never constructed unless explicitly
|
|
12
|
+
// enabled, leaving the normal runtime with no event-loop profiling work.
|
|
13
|
+
export function installHitchProfiler() {
|
|
14
|
+
if (!HITCH_PROFILE_ENABLED) return null;
|
|
15
|
+
|
|
16
|
+
const thresholdMs = positiveNumberEnv('MIXDOG_HITCH_THRESHOLD_MS', 100);
|
|
17
|
+
const resolutionMs = Math.max(1, Math.min(100, positiveNumberEnv('MIXDOG_HITCH_RESOLUTION_MS', 20)));
|
|
18
|
+
const sampleMs = Math.max(resolutionMs * 2, positiveNumberEnv('MIXDOG_HITCH_SAMPLE_MS', 100));
|
|
19
|
+
const histogram = monitorEventLoopDelay({ resolution: resolutionMs });
|
|
20
|
+
histogram.enable();
|
|
21
|
+
let lastSampleAt = performance.now();
|
|
22
|
+
|
|
23
|
+
const timer = setInterval(() => {
|
|
24
|
+
const now = performance.now();
|
|
25
|
+
// Histogram max is the primary signal. Timer drift covers a stall ending
|
|
26
|
+
// in the same timers phase before the histogram's internal sampler runs.
|
|
27
|
+
const delayMs = Math.max(histogram.max / 1e6, now - lastSampleAt - sampleMs);
|
|
28
|
+
lastSampleAt = now;
|
|
29
|
+
histogram.reset();
|
|
30
|
+
if (delayMs < thresholdMs) return;
|
|
31
|
+
try {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
`[mixdog-hitch] timestamp=${new Date().toISOString()} delayMs=${delayMs.toFixed(1)} thresholdMs=${thresholdMs}\n`,
|
|
34
|
+
);
|
|
35
|
+
} catch {}
|
|
36
|
+
}, sampleMs);
|
|
37
|
+
timer.unref?.();
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
clearInterval(timer);
|
|
41
|
+
histogram.disable();
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
installHitchProfiler();
|