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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/scripts/agent-model-liveness-test.mjs +68 -0
  3. package/scripts/anthropic-transport-policy-test.mjs +260 -0
  4. package/scripts/gemini-provider-test.mjs +396 -1
  5. package/scripts/grok-oauth-refresh-race-test.mjs +76 -1
  6. package/scripts/openai-oauth-ws-1006-retry-test.mjs +81 -1
  7. package/scripts/process-lifecycle-test.mjs +13 -2
  8. package/scripts/provider-admission-scheduler-test.mjs +4 -5
  9. package/scripts/provider-contract-test.mjs +91 -33
  10. package/scripts/provider-toolcall-test.mjs +97 -17
  11. package/src/repl.mjs +11 -0
  12. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -25
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -2
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +50 -23
  15. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +15 -4
  16. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +136 -27
  17. package/src/runtime/agent/orchestrator/providers/gemini.mjs +104 -20
  18. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +96 -75
  19. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +40 -1
  20. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +5 -0
  21. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +35 -4
  22. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -1
  23. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +49 -9
  24. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +14 -2
  25. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -4
  26. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +53 -13
  27. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +53 -7
  28. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +23 -1
  29. package/src/runtime/agent/orchestrator/stall-policy.mjs +6 -13
  30. package/src/runtime/shared/memory-snapshot.mjs +45 -36
  31. package/src/runtime/shared/process-lifecycle.mjs +113 -36
  32. package/src/session-runtime/session-turn-api.mjs +1 -0
  33. package/src/tui/dist/index.mjs +31 -0
  34. 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 { execFileSync } from 'node:child_process';
19
- import { withFileLockSync } from './atomic-file.mjs';
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
- fsyncSync(fd);
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 processIdentityForPid(process.pid);
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 out = execFileSync('powershell.exe', [
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(out).split(/\r?\n/)) {
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
- try {
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 out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
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(out).trim()) / 1000);
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) => [pid, processIdentityForPid(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
- 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);
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
- const identities = processIdentitiesForPids(occupied.map(({ previous }) => previous.pid));
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 && recordPriorVanished(active, previous)) unlinkSync(markerPath);
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()) {
@@ -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;
@@ -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;