mixdog 0.9.53 → 0.9.54
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/agent-model-liveness-test.mjs +68 -0
- package/scripts/anthropic-transport-policy-test.mjs +260 -0
- package/scripts/gemini-provider-test.mjs +396 -1
- package/scripts/grok-oauth-refresh-race-test.mjs +76 -1
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +81 -1
- package/scripts/process-lifecycle-test.mjs +13 -2
- package/scripts/provider-admission-scheduler-test.mjs +4 -5
- package/scripts/provider-contract-test.mjs +91 -33
- package/scripts/provider-toolcall-test.mjs +97 -17
- package/src/repl.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -25
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +50 -23
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +136 -27
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +104 -20
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +96 -75
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +40 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +49 -9
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +53 -13
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +53 -7
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +23 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +6 -13
- package/src/runtime/shared/memory-snapshot.mjs +45 -36
- package/src/runtime/shared/process-lifecycle.mjs +113 -36
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/tui/dist/index.mjs +31 -0
- package/src/tui/engine/turn.mjs +31 -0
|
@@ -15,10 +15,12 @@ import {
|
|
|
15
15
|
writeSync,
|
|
16
16
|
} from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { execFile } from 'node:child_process';
|
|
19
|
+
import { isDeepStrictEqual, promisify } from 'node:util';
|
|
20
|
+
import { withFileLock, withFileLockSync } from './atomic-file.mjs';
|
|
20
21
|
import { resolvePluginData } from './plugin-paths.mjs';
|
|
21
22
|
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
22
24
|
export const LIFECYCLE_LEDGER_MAX_BYTES = 64 * 1024;
|
|
23
25
|
export const LIFECYCLE_LEDGER_KEEP_FILES = 2;
|
|
24
26
|
const LEDGER_NAME = 'process-lifecycle.jsonl';
|
|
@@ -105,7 +107,33 @@ function appendEntry(active, entry) {
|
|
|
105
107
|
const fd = openSync(active.ledger, 'a', 0o600);
|
|
106
108
|
try {
|
|
107
109
|
writeSync(fd, line, null, 'utf8');
|
|
108
|
-
|
|
110
|
+
} finally {
|
|
111
|
+
closeSync(fd);
|
|
112
|
+
}
|
|
113
|
+
return statSync(active.ledger).size <= LIFECYCLE_LEDGER_MAX_BYTES;
|
|
114
|
+
}, {
|
|
115
|
+
timeoutMs: LEDGER_LOCK_TIMEOUT_MS,
|
|
116
|
+
staleMs: LEDGER_LOCK_STALE_MS,
|
|
117
|
+
});
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function appendEntryAsync(active, entry, shouldAppend) {
|
|
124
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
125
|
+
if (Buffer.byteLength(line) > LIFECYCLE_LEDGER_MAX_BYTES) return false;
|
|
126
|
+
try {
|
|
127
|
+
return await withFileLock(active.lock, () => {
|
|
128
|
+
if (!shouldAppend()) return false;
|
|
129
|
+
boundPreviousLedger(active);
|
|
130
|
+
if (existsSync(active.ledger)
|
|
131
|
+
&& statSync(active.ledger).size + Buffer.byteLength(line) > LIFECYCLE_LEDGER_MAX_BYTES) {
|
|
132
|
+
rotateLedger(active);
|
|
133
|
+
}
|
|
134
|
+
const fd = openSync(active.ledger, 'a', 0o600);
|
|
135
|
+
try {
|
|
136
|
+
writeSync(fd, line, null, 'utf8');
|
|
109
137
|
} finally {
|
|
110
138
|
closeSync(fd);
|
|
111
139
|
}
|
|
@@ -126,18 +154,30 @@ function currentProcessIdentity() {
|
|
|
126
154
|
? { kind: 'start-seconds', value, method: 'uptime' }
|
|
127
155
|
: null;
|
|
128
156
|
}
|
|
129
|
-
return
|
|
157
|
+
return linuxProcessIdentity(process.pid);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function linuxProcessIdentity(pid) {
|
|
161
|
+
try {
|
|
162
|
+
const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
163
|
+
const fields = raw.slice(raw.lastIndexOf(') ') + 2).trim().split(/\s+/);
|
|
164
|
+
return fields[19] && /^\d+$/.test(fields[19])
|
|
165
|
+
? { kind: 'linux-start-ticks', value: fields[19] }
|
|
166
|
+
: null;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
130
170
|
}
|
|
131
171
|
|
|
132
|
-
function windowsProcessIdentities(pids) {
|
|
172
|
+
async function windowsProcessIdentities(pids) {
|
|
133
173
|
const identities = new Map();
|
|
134
174
|
if (pids.length === 0) return identities;
|
|
135
175
|
try {
|
|
136
|
-
const
|
|
176
|
+
const { stdout } = await execFileAsync('powershell.exe', [
|
|
137
177
|
'-NoProfile', '-NonInteractive', '-Command',
|
|
138
178
|
`$ErrorActionPreference='SilentlyContinue'; Get-Process -Id @(${pids.join(',')}) | ForEach-Object { try { "$($_.Id):$(([DateTimeOffset]$_.StartTime).ToUnixTimeSeconds())" } catch {} }`,
|
|
139
179
|
], { encoding: 'utf8', timeout: 2000, windowsHide: true });
|
|
140
|
-
for (const line of String(
|
|
180
|
+
for (const line of String(stdout).split(/\r?\n/)) {
|
|
141
181
|
const match = /^(\d+):(\d+)$/.exec(line.trim());
|
|
142
182
|
if (!match) continue;
|
|
143
183
|
const pid = Number(match[1]);
|
|
@@ -150,38 +190,33 @@ function windowsProcessIdentities(pids) {
|
|
|
150
190
|
return identities;
|
|
151
191
|
}
|
|
152
192
|
|
|
153
|
-
function processIdentityForPid(pid) {
|
|
193
|
+
async function processIdentityForPid(pid) {
|
|
154
194
|
if (process.platform === 'linux') {
|
|
155
|
-
|
|
156
|
-
const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
157
|
-
const fields = raw.slice(raw.lastIndexOf(') ') + 2).trim().split(/\s+/);
|
|
158
|
-
return fields[19] && /^\d+$/.test(fields[19])
|
|
159
|
-
? { kind: 'linux-start-ticks', value: fields[19] }
|
|
160
|
-
: null;
|
|
161
|
-
} catch {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
195
|
+
return linuxProcessIdentity(pid);
|
|
164
196
|
}
|
|
165
197
|
try {
|
|
166
198
|
if (process.platform === 'win32') {
|
|
167
|
-
return windowsProcessIdentities([pid]).get(pid) || null;
|
|
199
|
+
return (await windowsProcessIdentities([pid])).get(pid) || null;
|
|
168
200
|
}
|
|
169
|
-
const
|
|
201
|
+
const { stdout } = await execFileAsync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
170
202
|
encoding: 'utf8',
|
|
171
203
|
timeout: 2000,
|
|
172
204
|
windowsHide: true,
|
|
173
205
|
});
|
|
174
|
-
const value = Math.floor(Date.parse(String(
|
|
206
|
+
const value = Math.floor(Date.parse(String(stdout).trim()) / 1000);
|
|
175
207
|
return Number.isInteger(value) ? { kind: 'start-seconds', value, method: 'ps' } : null;
|
|
176
208
|
} catch {
|
|
177
209
|
return null;
|
|
178
210
|
}
|
|
179
211
|
}
|
|
180
212
|
|
|
181
|
-
function processIdentitiesForPids(pids) {
|
|
213
|
+
async function processIdentitiesForPids(pids) {
|
|
182
214
|
const unique = [...new Set(pids)];
|
|
183
215
|
if (process.platform !== 'win32') {
|
|
184
|
-
return new Map(unique.map((pid) => [
|
|
216
|
+
return new Map(await Promise.all(unique.map(async (pid) => [
|
|
217
|
+
pid,
|
|
218
|
+
await processIdentityForPid(pid),
|
|
219
|
+
])));
|
|
185
220
|
}
|
|
186
221
|
|
|
187
222
|
const identities = new Map();
|
|
@@ -190,7 +225,7 @@ function processIdentitiesForPids(pids) {
|
|
|
190
225
|
if (pid === process.pid) identities.set(pid, currentProcessIdentity());
|
|
191
226
|
else otherPids.push(pid);
|
|
192
227
|
}
|
|
193
|
-
for (const [pid, identity] of windowsProcessIdentities(otherPids)) {
|
|
228
|
+
for (const [pid, identity] of await windowsProcessIdentities(otherPids)) {
|
|
194
229
|
identities.set(pid, identity);
|
|
195
230
|
}
|
|
196
231
|
return identities;
|
|
@@ -240,6 +275,36 @@ function recordPriorVanished(active, previous) {
|
|
|
240
275
|
});
|
|
241
276
|
}
|
|
242
277
|
|
|
278
|
+
function markerMatchesSnapshot(markerPath, previous) {
|
|
279
|
+
try {
|
|
280
|
+
const current = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
281
|
+
return current?.pid === previous.pid
|
|
282
|
+
&& current?.token === previous.token
|
|
283
|
+
&& isDeepStrictEqual(current?.processIdentity, previous.processIdentity);
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function recordPriorVanishedAsync(active, markerPath, previous) {
|
|
290
|
+
const written = await appendEntryAsync(active, {
|
|
291
|
+
version: 1,
|
|
292
|
+
timestamp: new Date().toISOString(),
|
|
293
|
+
pid: previous.pid,
|
|
294
|
+
ppid: Number.isInteger(previous.ppid) ? previous.ppid : null,
|
|
295
|
+
reason: 'prior-process-vanished',
|
|
296
|
+
exitCode: null,
|
|
297
|
+
}, () => sharedState().active === active && markerMatchesSnapshot(markerPath, previous));
|
|
298
|
+
if (!written || sharedState().active !== active) return false;
|
|
299
|
+
if (!markerMatchesSnapshot(markerPath, previous)) return false;
|
|
300
|
+
try {
|
|
301
|
+
unlinkSync(markerPath);
|
|
302
|
+
return true;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
243
308
|
function pidLiveness(pid) {
|
|
244
309
|
if (!Number.isInteger(pid) || pid < 1 || pid > 2147483647) return 'unknown';
|
|
245
310
|
try {
|
|
@@ -285,17 +350,20 @@ function scheduleProcessIdentityUpgrade(active) {
|
|
|
285
350
|
const scheduleAttempt = (attempt) => {
|
|
286
351
|
if (attempt >= IDENTITY_UPGRADE_DELAYS_MS.length) return;
|
|
287
352
|
const timer = setTimeout(() => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
353
|
+
void (async () => {
|
|
354
|
+
try {
|
|
355
|
+
if (sharedState().active !== active || !markerOwned(active)) return;
|
|
356
|
+
const processIdentity = await processIdentityForPid(process.pid);
|
|
357
|
+
if (sharedState().active !== active || !markerOwned(active)) return;
|
|
358
|
+
if (processIdentity && processIdentity.method !== 'uptime') {
|
|
359
|
+
const previousIdentity = active.processIdentity;
|
|
360
|
+
active.processIdentity = processIdentity;
|
|
361
|
+
if (writeMarker(active)) return;
|
|
362
|
+
active.processIdentity = previousIdentity;
|
|
363
|
+
}
|
|
364
|
+
} catch {}
|
|
365
|
+
scheduleAttempt(attempt + 1);
|
|
366
|
+
})();
|
|
299
367
|
}, IDENTITY_UPGRADE_DELAYS_MS[attempt]);
|
|
300
368
|
timer.unref?.();
|
|
301
369
|
};
|
|
@@ -325,14 +393,23 @@ function reapVanishedMarkers(active) {
|
|
|
325
393
|
else if (liveness === 'dead' && recordPriorVanished(active, previous)) unlinkSync(markerPath);
|
|
326
394
|
} catch {}
|
|
327
395
|
}
|
|
328
|
-
|
|
396
|
+
void reapOccupiedMarkers(active, occupied).catch(() => {});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function reapOccupiedMarkers(active, occupied) {
|
|
400
|
+
const identities = await processIdentitiesForPids(occupied.map(({ previous }) => previous.pid));
|
|
401
|
+
if (sharedState().active !== active) return;
|
|
329
402
|
for (const { markerPath, previous } of occupied) {
|
|
330
403
|
try {
|
|
404
|
+
if (sharedState().active !== active) return;
|
|
405
|
+
if (!markerMatchesSnapshot(markerPath, previous)) continue;
|
|
331
406
|
const identityMatch = sameProcessIdentity(
|
|
332
407
|
previous.processIdentity,
|
|
333
408
|
identities.get(previous.pid) || null,
|
|
334
409
|
);
|
|
335
|
-
if (identityMatch === false
|
|
410
|
+
if (identityMatch === false) {
|
|
411
|
+
await recordPriorVanishedAsync(active, markerPath, previous);
|
|
412
|
+
}
|
|
336
413
|
} catch {}
|
|
337
414
|
}
|
|
338
415
|
}
|
|
@@ -167,6 +167,7 @@ export function createSessionTurnApi(deps) {
|
|
|
167
167
|
options.prefetch || null,
|
|
168
168
|
{
|
|
169
169
|
onTextDelta: options.onTextDelta,
|
|
170
|
+
onTextReset: options.onTextReset,
|
|
170
171
|
onReasoningDelta: options.onReasoningDelta,
|
|
171
172
|
onAssistantText: (text) => {
|
|
172
173
|
if (getRemoteEnabled() && getTranscriptWriter()) {
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -25637,6 +25637,37 @@ function createRunTurn(bag) {
|
|
|
25637
25637
|
if (thinkingLastEndedAt) _pendingThinkingLastEndedAt = thinkingLastEndedAt;
|
|
25638
25638
|
scheduleStreamFlush();
|
|
25639
25639
|
},
|
|
25640
|
+
onTextReset: ({ chars } = {}) => {
|
|
25641
|
+
if (!isCurrentTurn()) return false;
|
|
25642
|
+
const count = Math.max(0, Number(chars) || 0);
|
|
25643
|
+
if (!count) return false;
|
|
25644
|
+
flushStreamBatch();
|
|
25645
|
+
assistantText = assistantText.slice(0, Math.max(0, assistantText.length - count));
|
|
25646
|
+
currentAssistantText = currentAssistantText.slice(
|
|
25647
|
+
0,
|
|
25648
|
+
Math.max(0, currentAssistantText.length - count)
|
|
25649
|
+
);
|
|
25650
|
+
_streamScanLen = 0;
|
|
25651
|
+
_lastNewlineIdx = -1;
|
|
25652
|
+
_emittedNewlineIdx = -2;
|
|
25653
|
+
_emittedVisibleText = "";
|
|
25654
|
+
if (currentAssistantId) {
|
|
25655
|
+
if (currentAssistantText) {
|
|
25656
|
+
set({
|
|
25657
|
+
streamingTail: {
|
|
25658
|
+
kind: "assistant",
|
|
25659
|
+
id: currentAssistantId,
|
|
25660
|
+
text: currentAssistantText,
|
|
25661
|
+
streaming: true
|
|
25662
|
+
}
|
|
25663
|
+
});
|
|
25664
|
+
} else {
|
|
25665
|
+
clearStreamingTail(currentAssistantId);
|
|
25666
|
+
currentAssistantId = null;
|
|
25667
|
+
}
|
|
25668
|
+
}
|
|
25669
|
+
return true;
|
|
25670
|
+
},
|
|
25640
25671
|
onAssistantText: (text) => {
|
|
25641
25672
|
const full = String(text ?? "");
|
|
25642
25673
|
if (!full.trim()) return;
|
package/src/tui/engine/turn.mjs
CHANGED
|
@@ -1073,6 +1073,37 @@ export function createRunTurn(bag) {
|
|
|
1073
1073
|
if (thinkingLastEndedAt) _pendingThinkingLastEndedAt = thinkingLastEndedAt;
|
|
1074
1074
|
scheduleStreamFlush();
|
|
1075
1075
|
},
|
|
1076
|
+
onTextReset: ({ chars } = {}) => {
|
|
1077
|
+
if (!isCurrentTurn()) return false;
|
|
1078
|
+
const count = Math.max(0, Number(chars) || 0);
|
|
1079
|
+
if (!count) return false;
|
|
1080
|
+
flushStreamBatch();
|
|
1081
|
+
assistantText = assistantText.slice(0, Math.max(0, assistantText.length - count));
|
|
1082
|
+
currentAssistantText = currentAssistantText.slice(
|
|
1083
|
+
0,
|
|
1084
|
+
Math.max(0, currentAssistantText.length - count),
|
|
1085
|
+
);
|
|
1086
|
+
_streamScanLen = 0;
|
|
1087
|
+
_lastNewlineIdx = -1;
|
|
1088
|
+
_emittedNewlineIdx = -2;
|
|
1089
|
+
_emittedVisibleText = '';
|
|
1090
|
+
if (currentAssistantId) {
|
|
1091
|
+
if (currentAssistantText) {
|
|
1092
|
+
set({
|
|
1093
|
+
streamingTail: {
|
|
1094
|
+
kind: 'assistant',
|
|
1095
|
+
id: currentAssistantId,
|
|
1096
|
+
text: currentAssistantText,
|
|
1097
|
+
streaming: true,
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
} else {
|
|
1101
|
+
clearStreamingTail(currentAssistantId);
|
|
1102
|
+
currentAssistantId = null;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return true;
|
|
1106
|
+
},
|
|
1076
1107
|
onAssistantText: (text) => {
|
|
1077
1108
|
// Mid-turn assistant text that precedes a tool call. Providers that
|
|
1078
1109
|
// stream via onTextDelta already accumulated it into assistantText;
|