mixdog 0.9.0 → 0.9.1
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 +3 -3
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/src/headless-role.mjs +1 -1
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +2 -2
- package/src/mixdog-session-runtime.mjs +0 -1
- package/src/repl.mjs +0 -2
- package/src/runtime/agent/orchestrator/internal-roles.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +27 -49
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +5 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +8 -27
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +52 -182
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +8 -30
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +258 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/channels/index.mjs +0 -30
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +1 -10
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +3 -24
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +9 -7
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/standalone/seeds.mjs +1 -11
- package/src/tui/App.jsx +35 -12
- package/src/tui/components/PromptInput.jsx +63 -2
- package/src/tui/components/ToolExecution.jsx +7 -2
- package/src/tui/components/tool-output-format.mjs +156 -22
- package/src/tui/components/tool-output-format.test.mjs +93 -1
- package/src/tui/dist/index.mjs +473 -116
- package/src/tui/markdown/format-token.mjs +267 -108
- package/src/tui/markdown/format-token.test.mjs +105 -9
- package/src/tui/theme.mjs +10 -0
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/vendor/ink/build/ink.js +54 -8
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
34
34
|
createTimeoutSignal,
|
|
35
35
|
} from '../stall-policy.mjs';
|
|
36
|
-
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
36
|
+
import { populateHttpStatusFromMessage, shouldFallbackTransport } from './retry-classifier.mjs';
|
|
37
37
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
38
38
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
39
39
|
import {
|
|
@@ -742,36 +742,14 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
|
742
742
|
return headers;
|
|
743
743
|
}
|
|
744
744
|
|
|
745
|
+
// WS→HTTP/SSE fallback predicate → shared shouldFallbackTransport
|
|
746
|
+
// (retry-classifier.mjs). The per-provider env flag is computed here and passed
|
|
747
|
+
// as `enabled`; the deny-order + allow-list are identical to the former copy.
|
|
745
748
|
function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
// concatenate a second attempt onto rendered output. Never fall back.
|
|
751
|
-
if (err?.liveTextEmitted === true) return false;
|
|
752
|
-
if (err?.emittedToolCall === true || err?.unsafeToRetry === true) return false;
|
|
753
|
-
const status = Number(err?.httpStatus || err?.status || 0);
|
|
754
|
-
if (status === 401 || status === 403 || status === 404 || status === 429) return false;
|
|
755
|
-
if (status >= 500 && status < 600) return true;
|
|
756
|
-
const code = String(err?.code || '');
|
|
757
|
-
if (['EWSACQUIRETIMEOUT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'EAI_NODATA', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE'].includes(code)) {
|
|
758
|
-
return true;
|
|
759
|
-
}
|
|
760
|
-
const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '');
|
|
761
|
-
if ([
|
|
762
|
-
'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
|
|
763
|
-
'first_byte_timeout', 'first_meaningful_timeout',
|
|
764
|
-
'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
|
|
765
|
-
'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
|
|
766
|
-
'ws_send_failed',
|
|
767
|
-
].includes(classifier)) {
|
|
768
|
-
return true;
|
|
769
|
-
}
|
|
770
|
-
if (/^http_5\d\d$/.test(classifier)) return true;
|
|
771
|
-
if (err?.firstByteTimeout) return true;
|
|
772
|
-
if (err?.firstMeaningfulTimeout) return true;
|
|
773
|
-
const msg = String(err?.message || '');
|
|
774
|
-
return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg);
|
|
749
|
+
return shouldFallbackTransport(err, {
|
|
750
|
+
signal: externalSignal,
|
|
751
|
+
enabled: _envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true),
|
|
752
|
+
});
|
|
775
753
|
}
|
|
776
754
|
|
|
777
755
|
// Exported for the single-emit regression smoke (scripts/openai-oauth-
|
|
@@ -231,6 +231,264 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
|
|
|
231
231
|
return Math.max(0, Math.round(base + offset))
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// ── Shared network-resilience interface ──────────────────────────────────────
|
|
235
|
+
// One home for the logic that used to be copied per provider: mid-stream
|
|
236
|
+
// classifier (WS + SSE), transport fallback predicate, stream-safety stamp
|
|
237
|
+
// latches, abort-aware sleep, handshake classifier, and the retry-budget table.
|
|
238
|
+
// Provider differences are passed as ARGUMENTS (policy objects), never branched
|
|
239
|
+
// on a hardcoded provider name. Behavior is preserved EXACTLY — each path below
|
|
240
|
+
// is the relocated original, selected by policy.mode.
|
|
241
|
+
|
|
242
|
+
// F) Retry-budget profiles as DATA. The numbers live ONLY here now.
|
|
243
|
+
// ws.transientCloseRetries (4) — ws_1006 / ws_1011 connection-loss buckets.
|
|
244
|
+
// ws.defaultRetries (2) — every other WS mid-stream bucket.
|
|
245
|
+
// sse.defaultRetries (3) — anthropic single-shot SSE mid-stream budget.
|
|
246
|
+
export const MIDSTREAM_RETRY_POLICY = {
|
|
247
|
+
ws: { transientCloseRetries: 4, defaultRetries: 2, backoff: [250, 1000, 2000, 4000] },
|
|
248
|
+
sse: { defaultRetries: 3, backoff: [250, 1000, 2000, 4000] },
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// WS buckets that earn the larger transient-close retry budget.
|
|
252
|
+
const WS_TRANSIENT_CLOSE_CLASSIFIERS = new Set(['ws_1006', 'ws_1011'])
|
|
253
|
+
|
|
254
|
+
function _midstreamLimitFor(classifier, policy) {
|
|
255
|
+
if (policy.mode === 'ws') {
|
|
256
|
+
return WS_TRANSIENT_CLOSE_CLASSIFIERS.has(classifier)
|
|
257
|
+
? policy.transientCloseRetries
|
|
258
|
+
: policy.defaultRetries
|
|
259
|
+
}
|
|
260
|
+
return policy.defaultRetries
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// WS gates each classifier against its own budget (mirrors the old
|
|
264
|
+
// _allowMidstreamRetry). SSE applies a single top-of-function budget gate and
|
|
265
|
+
// then returns raw classifier strings, so perClassifierGate:false returns the
|
|
266
|
+
// classifier unconditionally here.
|
|
267
|
+
function _allowMidstream(classifier, attemptIndex, policy) {
|
|
268
|
+
if (policy.perClassifierGate === false) return classifier
|
|
269
|
+
return attemptIndex < _midstreamLimitFor(classifier, policy) ? classifier : null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// A) Unified mid-stream classifier. Returns a classifier string or null.
|
|
273
|
+
// `signals` is the provider's mid-stream state object (field names unchanged
|
|
274
|
+
// from each provider's midState). `policy.mode` selects the WS or SSE path so
|
|
275
|
+
// both providers reproduce their exact current branch order and gating.
|
|
276
|
+
export function classifyMidstreamError(err, signals, policy = {}) {
|
|
277
|
+
if (!signals) return null
|
|
278
|
+
const attemptIndex = signals.attemptIndex | 0
|
|
279
|
+
if (policy.mode === 'sse') return _classifyMidstreamSse(err, signals, attemptIndex, policy)
|
|
280
|
+
return _classifyMidstreamWs(err, signals, attemptIndex, policy)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Verbatim relocation of openai-oauth-ws's _classifyMidstreamError. `signals`
|
|
284
|
+
// carries sawResponseCreated / sawCompleted / emittedText / emittedToolCall /
|
|
285
|
+
// wsCloseCode / firstByteTimeout / firstMeaningfulTimeout / wsSendFailed /
|
|
286
|
+
// userAbort / watchdogAbort / responseFailedPayload exactly as before.
|
|
287
|
+
function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
288
|
+
if (state.sawCompleted) return null
|
|
289
|
+
if (state.emittedToolCall) {
|
|
290
|
+
const _cc = Number(err?.wsCloseCode || state.wsCloseCode || 0)
|
|
291
|
+
if (!(_cc === 1000 && state.sawResponseCreated && !state.sawCompleted)) return null
|
|
292
|
+
}
|
|
293
|
+
if (state.emittedText || err?.liveTextEmitted) return null
|
|
294
|
+
if (state.firstByteTimeout || err?.firstByteTimeout) {
|
|
295
|
+
return _allowMidstream('first_byte_timeout', attemptIndex, policy)
|
|
296
|
+
}
|
|
297
|
+
if (state.firstMeaningfulTimeout || err?.firstMeaningfulTimeout) {
|
|
298
|
+
return _allowMidstream('first_meaningful_timeout', attemptIndex, policy)
|
|
299
|
+
}
|
|
300
|
+
if (err?.wsSendFailed || state.wsSendFailed) {
|
|
301
|
+
return _allowMidstream('ws_send_failed', attemptIndex, policy)
|
|
302
|
+
}
|
|
303
|
+
if (!state.sawResponseCreated) {
|
|
304
|
+
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
|
|
305
|
+
if (closeCode !== 1011 && closeCode !== 1012) return null
|
|
306
|
+
}
|
|
307
|
+
if (state.userAbort) return null
|
|
308
|
+
|
|
309
|
+
if (!err) return null
|
|
310
|
+
const status = Number(err?.httpStatus || 0)
|
|
311
|
+
if (status === 401 || status === 403 || status === 429) return null
|
|
312
|
+
if (status >= 500 && status < 600) {
|
|
313
|
+
return _allowMidstream(`http_${status}`, attemptIndex, policy)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const name = err?.name || ''
|
|
317
|
+
if (name === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
|
|
318
|
+
if (name === 'StreamStalledAbortError') return _allowMidstream('stream_stalled', attemptIndex, policy)
|
|
319
|
+
|
|
320
|
+
if (state.watchdogAbort === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
|
|
321
|
+
if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstream('stream_stalled', attemptIndex, policy)
|
|
322
|
+
|
|
323
|
+
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
|
|
324
|
+
if (closeCode === 1006) return _allowMidstream('ws_1006', attemptIndex, policy)
|
|
325
|
+
if (closeCode === 1011) return _allowMidstream('ws_1011', attemptIndex, policy)
|
|
326
|
+
if (closeCode === 1012) return _allowMidstream('ws_1012', attemptIndex, policy)
|
|
327
|
+
if (closeCode >= 4000 && closeCode < 5000 && closeCode !== 4000) return null
|
|
328
|
+
if (closeCode === 4000) return _allowMidstream('ws_4000', attemptIndex, policy)
|
|
329
|
+
if (closeCode === 1000 && state.sawResponseCreated && !state.sawCompleted) return _allowMidstream('ws_1000', attemptIndex, policy)
|
|
330
|
+
|
|
331
|
+
const failed = err?.responseFailed || state.responseFailedPayload
|
|
332
|
+
if (failed) {
|
|
333
|
+
try {
|
|
334
|
+
const blob = JSON.stringify(failed).toLowerCase()
|
|
335
|
+
if (blob.includes('stream_disconnected')) return _allowMidstream('response_failed_disconnected', attemptIndex, policy)
|
|
336
|
+
if (blob.includes('network_error')) return _allowMidstream('response_failed_network', attemptIndex, policy)
|
|
337
|
+
if (blob.includes('auth context expired')) return _allowMidstream('response_failed_auth_expired', attemptIndex, policy)
|
|
338
|
+
} catch {}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Verbatim relocation of anthropic-oauth's _classifyMidstreamError. `signals`
|
|
345
|
+
// carries sawMessageStart / sawCompleted / emittedToolCall / userAbort /
|
|
346
|
+
// watchdogAbort exactly as before.
|
|
347
|
+
function _classifyMidstreamSse(err, state, attemptIndex, policy) {
|
|
348
|
+
if (attemptIndex >= policy.defaultRetries) return null
|
|
349
|
+
if (state.sawCompleted) return null
|
|
350
|
+
if (!state.sawMessageStart) return null
|
|
351
|
+
if (state.userAbort) return null
|
|
352
|
+
if (state.emittedToolCall) return null
|
|
353
|
+
|
|
354
|
+
if (!err) return null
|
|
355
|
+
const status = Number(err?.httpStatus || 0)
|
|
356
|
+
if (status === 401 || status === 403 || status === 429) return null
|
|
357
|
+
|
|
358
|
+
const name = err?.name || ''
|
|
359
|
+
if (name === 'AgentStallAbortError') return 'agent_stall'
|
|
360
|
+
if (name === 'StreamStalledAbortError') return 'stream_stalled'
|
|
361
|
+
if (state.watchdogAbort === 'AgentStallAbortError') return 'agent_stall'
|
|
362
|
+
if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled'
|
|
363
|
+
|
|
364
|
+
const code = err?.code || err?.cause?.code || ''
|
|
365
|
+
if (code === 'ECONNRESET') return 'reset'
|
|
366
|
+
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout'
|
|
367
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'EAI_NODATA') return 'dns'
|
|
368
|
+
|
|
369
|
+
const msg = String(err?.message || '').toLowerCase()
|
|
370
|
+
if (msg.includes('stream timed out after') && msg.includes('of inactivity')) return 'sse_idle_timeout'
|
|
371
|
+
if (msg.includes('body stream') && msg.includes('terminated')) return 'stream_terminated'
|
|
372
|
+
if (msg.includes('fetch failed')) return 'fetch_failed'
|
|
373
|
+
|
|
374
|
+
return null
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// B) Unified transport (WS→HTTP) fallback predicate. Identical deny-order +
|
|
378
|
+
// allow-list to the two former copies; `enabled` replaces the per-provider
|
|
379
|
+
// env-flag check (caller computes the flag and passes it).
|
|
380
|
+
const TRANSPORT_FALLBACK_CLASSIFIERS = new Set([
|
|
381
|
+
'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
|
|
382
|
+
'first_byte_timeout', 'first_meaningful_timeout',
|
|
383
|
+
'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
|
|
384
|
+
'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
|
|
385
|
+
'ws_send_failed',
|
|
386
|
+
])
|
|
387
|
+
const TRANSPORT_FALLBACK_ERRNO = new Set([
|
|
388
|
+
'EWSACQUIRETIMEOUT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNRESET', 'EAI_AGAIN',
|
|
389
|
+
'ENOTFOUND', 'EAI_NODATA', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE',
|
|
390
|
+
])
|
|
391
|
+
|
|
392
|
+
export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
|
|
393
|
+
if (!enabled) return false
|
|
394
|
+
if (signal?.aborted) return false
|
|
395
|
+
if (err?.liveTextEmitted === true) return false
|
|
396
|
+
if (err?.emittedToolCall === true || err?.unsafeToRetry === true) return false
|
|
397
|
+
const status = Number(err?.httpStatus || err?.status || 0)
|
|
398
|
+
if (status === 401 || status === 403 || status === 404 || status === 429) return false
|
|
399
|
+
if (status >= 500 && status < 600) return true
|
|
400
|
+
const code = String(err?.code || '')
|
|
401
|
+
if (TRANSPORT_FALLBACK_ERRNO.has(code)) return true
|
|
402
|
+
const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '')
|
|
403
|
+
if (TRANSPORT_FALLBACK_CLASSIFIERS.has(classifier)) return true
|
|
404
|
+
if (/^http_5\d\d$/.test(classifier)) return true
|
|
405
|
+
if (err?.firstByteTimeout) return true
|
|
406
|
+
if (err?.firstMeaningfulTimeout) return true
|
|
407
|
+
const msg = String(err?.message || '')
|
|
408
|
+
return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// C) Stream-safety stamp latches. Mirrors openai-oauth-ws's _stampLiveText /
|
|
412
|
+
// _stampTool: once text/tool has been marked, every subsequent throw path
|
|
413
|
+
// re-applies the liveTextEmitted/emittedToolCall + unsafeToRetry markers so
|
|
414
|
+
// no upstream gate can reissue the turn and concatenate attempts.
|
|
415
|
+
export function createStreamSafetyStamps() {
|
|
416
|
+
let textLatched = false
|
|
417
|
+
let toolLatched = false
|
|
418
|
+
const stampText = (e) => {
|
|
419
|
+
if (textLatched && e) { try { e.liveTextEmitted = true; e.unsafeToRetry = true } catch {} }
|
|
420
|
+
return e
|
|
421
|
+
}
|
|
422
|
+
const stampTool = (e) => {
|
|
423
|
+
if (toolLatched && e) { try { e.emittedToolCall = true; e.unsafeToRetry = true } catch {} }
|
|
424
|
+
return e
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
markText() { textLatched = true },
|
|
428
|
+
markTool() { toolLatched = true },
|
|
429
|
+
stampText,
|
|
430
|
+
stampTool,
|
|
431
|
+
stampAll: (e) => stampTool(stampText(e)),
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const _defaultAbortSleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
436
|
+
|
|
437
|
+
// D) Abort-aware sleep (single copy). Resolves after `ms`, or rejects with the
|
|
438
|
+
// signal's reason (or `abortMessage`) the moment the signal aborts. `sleepFn`
|
|
439
|
+
// is the injectable no-signal sleep (test seam); `abortMessage` preserves
|
|
440
|
+
// each caller's prior fallback text when the abort reason is not an Error.
|
|
441
|
+
export async function sleepWithAbort(ms, signal, sleepFn = _defaultAbortSleep, abortMessage = 'sleep aborted') {
|
|
442
|
+
if (!ms) return
|
|
443
|
+
if (!signal) {
|
|
444
|
+
await (sleepFn || _defaultAbortSleep)(ms)
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
await new Promise((resolve, reject) => {
|
|
448
|
+
const t = setTimeout(() => {
|
|
449
|
+
try { signal.removeEventListener('abort', onAbort) } catch {}
|
|
450
|
+
resolve()
|
|
451
|
+
}, ms)
|
|
452
|
+
const onAbort = () => {
|
|
453
|
+
clearTimeout(t)
|
|
454
|
+
const reason = signal.reason
|
|
455
|
+
reject(reason instanceof Error ? reason : new Error(abortMessage))
|
|
456
|
+
}
|
|
457
|
+
if (signal.aborted) { onAbort(); return }
|
|
458
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// E) Handshake classifier (moved here from openai-oauth-ws). Default-deny:
|
|
463
|
+
// anything not recognized as transient returns null. 401/403/404/429 are
|
|
464
|
+
// permanent. Returns 'timeout' | 'reset' | 'dns' | 'refused' | 'network' |
|
|
465
|
+
// 'acquire_timeout' | `http_5xx` | null.
|
|
466
|
+
export function classifyHandshakeError(err) {
|
|
467
|
+
if (!err) return null
|
|
468
|
+
const code = err.code || ''
|
|
469
|
+
const msg = String(err.message || '')
|
|
470
|
+
const status = Number(err.httpStatus || 0)
|
|
471
|
+
|
|
472
|
+
if (status === 401 || status === 403 || status === 404 || status === 429) {
|
|
473
|
+
return null
|
|
474
|
+
}
|
|
475
|
+
if (status >= 500 && status < 600) {
|
|
476
|
+
return `http_${status}`
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (code === 'ECONNRESET') return 'reset'
|
|
480
|
+
if (code === 'EAI_AGAIN' || code === 'ENOTFOUND' || code === 'EAI_NODATA') return 'dns'
|
|
481
|
+
if (code === 'ECONNREFUSED') return 'refused'
|
|
482
|
+
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout'
|
|
483
|
+
if (code === 'EWSACQUIRETIMEOUT') return 'acquire_timeout'
|
|
484
|
+
if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'EPIPE') return 'network'
|
|
485
|
+
|
|
486
|
+
if (/opening handshake has timed out/i.test(msg)) return 'timeout'
|
|
487
|
+
if (/socket hang up/i.test(msg)) return 'reset'
|
|
488
|
+
|
|
489
|
+
return null
|
|
490
|
+
}
|
|
491
|
+
|
|
234
492
|
/**
|
|
235
493
|
* Run an async function with exponential-backoff retry on transient errors.
|
|
236
494
|
*
|
|
@@ -44,7 +44,7 @@ import { invalidateBuiltinResultCache, analyzeShellCommandEffects, preflightShel
|
|
|
44
44
|
import { markCodeGraphDirtyPaths, drainCodeGraphCache } from './code-graph-state.mjs';
|
|
45
45
|
import { maybeRewriteWmicProcessCommand } from './shell-policy.mjs';
|
|
46
46
|
import { _maybeEncodePowerShellCommand } from './shell-command.mjs';
|
|
47
|
-
import { _captureTrackedMtimes, _trackedDriftNoteAfter,
|
|
47
|
+
import { _captureTrackedMtimes, _trackedDriftNoteAfter, getDedupedDestructiveWarnings } from './builtin/bash-tool.mjs';
|
|
48
48
|
import { scrubLoaderVars, scrubProviderSecrets } from './env-scrub.mjs';
|
|
49
49
|
import { checkExecPolicyMessage } from './bash-policy-scan.mjs';
|
|
50
50
|
import { startChildGuardian } from '../../../shared/child-guardian.mjs';
|
|
@@ -76,11 +76,6 @@ function isStringOrStringArray(v) {
|
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function hasBackslashPipe(value) {
|
|
80
|
-
if (typeof value === 'string') return false;
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
79
|
function hasUnsupportedRipgrepRegex(value) {
|
|
85
80
|
const values = Array.isArray(value) ? value : [value];
|
|
86
81
|
return values.some((item) => {
|
|
@@ -144,9 +139,6 @@ function guardGrep(a) {
|
|
|
144
139
|
if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
|
|
145
140
|
return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
|
|
146
141
|
}
|
|
147
|
-
if (hasOwn(a, k) && hasBackslashPipe(a[k])) {
|
|
148
|
-
return `Error: grep arg "${k}" contains \\|. Use pattern:["a","b"] for OR terms, or unescaped | for regex alternation.`;
|
|
149
|
-
}
|
|
150
142
|
if (hasOwn(a, k) && hasUnsupportedRipgrepRegex(a[k])) {
|
|
151
143
|
return `Error: grep arg "${k}" uses regex syntax ripgrep does not support here (lookaround/backrefs). Use plain pattern arrays or simpler regex.`;
|
|
152
144
|
}
|
|
@@ -3,7 +3,7 @@ import { execShellCommand, stripAnsi } from '../shell-command.mjs';
|
|
|
3
3
|
import { wrapCommandWithSnapshot } from '../shell-snapshot.mjs';
|
|
4
4
|
import { getDestructiveCommandWarning } from '../destructive-warning.mjs';
|
|
5
5
|
import { maybeRewriteWmicProcessCommand } from '../shell-policy.mjs';
|
|
6
|
-
import { buildBashPolicyScanTargets, checkExecPolicyMessage
|
|
6
|
+
import { buildBashPolicyScanTargets, checkExecPolicyMessage } from '../bash-policy-scan.mjs';
|
|
7
7
|
import { markCodeGraphDirtyPaths, drainCodeGraphCache } from '../code-graph-state.mjs';
|
|
8
8
|
import {
|
|
9
9
|
buildJobNotFoundMessage,
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
cancelBackgroundTask,
|
|
28
28
|
completeBackgroundTask,
|
|
29
29
|
getBackgroundTask,
|
|
30
|
-
listBackgroundTasks,
|
|
31
30
|
registerBackgroundTask,
|
|
32
31
|
renderBackgroundTask,
|
|
33
32
|
renderBackgroundTaskList,
|
|
@@ -48,7 +47,6 @@ import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
|
|
|
48
47
|
// closing the "external write -> stale old_string -> code 8" gap when shell is
|
|
49
48
|
// routed through this tool. Bounded to the tracked-read set (capped) so cost
|
|
50
49
|
// stays off the whole-cwd path; emits nothing when no read file changed.
|
|
51
|
-
const _DRIFT_SCAN_CAP = 256;
|
|
52
50
|
export function _captureTrackedMtimes(_scope) {
|
|
53
51
|
return new Map();
|
|
54
52
|
}
|
|
@@ -86,53 +84,12 @@ function _combineAbortSignals(sessionSignal, externalSignal) {
|
|
|
86
84
|
return ctl.signal;
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
// Decode ANSI-C $'…' and locale $"…" escapes so the blocklist scan sees the
|
|
90
|
-
// literal command (e.g. $'\x72m' → "rm"). Defensive against quoting bypass.
|
|
91
|
-
function _decodeAnsiCQuotes(s) {
|
|
92
|
-
if (typeof s !== 'string') return '';
|
|
93
|
-
if (s.indexOf('$') === -1) return s;
|
|
94
|
-
return s.replace(/\$(['"])((?:\\.|[^\\])*?)\1/g, (_full, _q, body) =>
|
|
95
|
-
body
|
|
96
|
-
.replace(/\\x([0-9a-fA-F]{1,2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)))
|
|
97
|
-
.replace(/\\u([0-9a-fA-F]{1,4})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)))
|
|
98
|
-
.replace(/\\0([0-7]{1,3})/g, (_m, o) => String.fromCharCode(parseInt(o, 8)))
|
|
99
|
-
.replace(/\\([0-7]{1,3})/g, (_m, o) => String.fromCharCode(parseInt(o, 8)))
|
|
100
|
-
.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r')
|
|
101
|
-
.replace(/\\\\/g, '\\').replace(/\\(['"])/g, '$1'),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Extract $(…) and `…` command-substitution bodies so each is re-scanned by
|
|
106
|
-
// isBlockedCommand (e.g. eval $(printf 'rm -rf ~')).
|
|
107
|
-
function _extractSubstitutionBodies(s) {
|
|
108
|
-
if (typeof s !== 'string') return [];
|
|
109
|
-
const out = [];
|
|
110
|
-
const re = /\$\(([^()]*(?:\([^()]*\)[^()]*)*)\)|`([^`]*)`/g;
|
|
111
|
-
let m;
|
|
112
|
-
while ((m = re.exec(s)) !== null) {
|
|
113
|
-
const body = m[1] != null ? m[1] : m[2];
|
|
114
|
-
if (body && body.trim()) out.push(body);
|
|
115
|
-
}
|
|
116
|
-
return out;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Combined injection-aware block targets: decoded form + substitution bodies
|
|
120
|
-
// (and their decoded forms). Used on BOTH the persistent and stateless paths.
|
|
121
|
-
export function _injectionBlockTargets(cmd) {
|
|
122
|
-
return injectionBlockTargets(cmd);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
87
|
function _prefixPowerShellUtf8(command) {
|
|
126
88
|
const prefix = '[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=[System.Text.Encoding]::UTF8;';
|
|
127
89
|
const text = String(command || '');
|
|
128
90
|
return text.trimStart().startsWith(prefix) ? text : `${prefix}\n${text}`;
|
|
129
91
|
}
|
|
130
92
|
|
|
131
|
-
const _unquoteSpansForPolicy = (s) => s.replace(/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/g, (m) => m.slice(1, -1));
|
|
132
|
-
|
|
133
|
-
// Same normalized + decoded target set as hard-block (strip/unquote/shell -c/PS).
|
|
134
|
-
export { buildBashPolicyScanTargets } from '../bash-policy-scan.mjs';
|
|
135
|
-
|
|
136
93
|
export function getDedupedDestructiveWarnings(command) {
|
|
137
94
|
const seenMsg = new Set();
|
|
138
95
|
const warnings = [];
|
|
@@ -387,16 +387,6 @@ function sendNotifyToParent(method, params) {
|
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
const recapState = { state: 'idle', running: false, startedAt: null, lastCompletedAt: null, updatedAt: null, errorMessage: null };
|
|
391
|
-
function sendRecapStateToParent() {
|
|
392
|
-
if (!process.send) return;
|
|
393
|
-
try {
|
|
394
|
-
process.send({ type: 'recap_status', recap: { ...recapState } });
|
|
395
|
-
} catch (err) {
|
|
396
|
-
try { process.stderr.write(`mixdog channels: recap status IPC send failed: ${err && err.message || err}\n`); } catch {}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
390
|
// ── Memory worker bridge (worker → parent → memory) ─────────────────
|
|
401
391
|
// The channels worker does not own the memory worker handle. To trigger
|
|
402
392
|
// memory tool actions (e.g. cycle1) we send `memory_call_request` to the
|
|
@@ -1141,26 +1131,6 @@ const OWNER_ROUTES = {
|
|
|
1141
1131
|
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
1142
1132
|
}
|
|
1143
1133
|
},
|
|
1144
|
-
"/recap/reset": async (req, res /*, body*/) => {
|
|
1145
|
-
if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
|
|
1146
|
-
if (!requireOwnerToken(req, res)) return;
|
|
1147
|
-
// Called by hooks/session-start.cjs on `/clear` (matcher startup|clear).
|
|
1148
|
-
// The session-start hook runs in a separate cjs process with no IPC
|
|
1149
|
-
// handle to this forked channels child, so it can't drop recap
|
|
1150
|
-
// status directly. Reset to an `empty` baseline so the statusline
|
|
1151
|
-
// doesn't carry the prior session's `injected`/`error` recap badge
|
|
1152
|
-
// into the cleared session.
|
|
1153
|
-
const now = Date.now();
|
|
1154
|
-
recapState.state = 'empty';
|
|
1155
|
-
recapState.running = false;
|
|
1156
|
-
recapState.startedAt = null;
|
|
1157
|
-
recapState.lastCompletedAt = now;
|
|
1158
|
-
recapState.updatedAt = now;
|
|
1159
|
-
recapState.errorMessage = null;
|
|
1160
|
-
sendRecapStateToParent();
|
|
1161
|
-
res.writeHead(200);
|
|
1162
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1163
|
-
},
|
|
1164
1134
|
"/cycle1": async (req, res, body) => {
|
|
1165
1135
|
if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ ok: false, reason: "method-not-allowed", error: "POST required" })); return; }
|
|
1166
1136
|
if (!requireOwnerToken(req, res)) return;
|
|
@@ -109,41 +109,3 @@ export async function ingestTranscript(filePath, { cwd } = {}) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
export async function listCoreMemories(args = {}) {
|
|
113
|
-
const rawProjectId = args && Object.prototype.hasOwnProperty.call(args, 'project_id')
|
|
114
|
-
? args.project_id
|
|
115
|
-
: args?.projectScope
|
|
116
|
-
const projectId = rawProjectId == null || rawProjectId === 'all' ? '*' : rawProjectId
|
|
117
|
-
const result = await memoryFetch('POST', '/api/tool', {
|
|
118
|
-
name: 'memory',
|
|
119
|
-
arguments: {
|
|
120
|
-
action: 'core',
|
|
121
|
-
op: 'list',
|
|
122
|
-
project_id: projectId,
|
|
123
|
-
},
|
|
124
|
-
}, 30_000)
|
|
125
|
-
if (!result || result.error) {
|
|
126
|
-
throw new Error(result?.error || 'core memory list: empty response')
|
|
127
|
-
}
|
|
128
|
-
return result
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export async function searchMemories(args = {}) {
|
|
132
|
-
const result = await memoryFetch('POST', '/api/tool', {
|
|
133
|
-
name: 'search_memories',
|
|
134
|
-
arguments: args && typeof args === 'object' ? args : {},
|
|
135
|
-
}, 30_000)
|
|
136
|
-
if (!result || result.error) {
|
|
137
|
-
throw new Error(result?.error || 'memory_search: empty response')
|
|
138
|
-
}
|
|
139
|
-
return result
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export async function isHealthy() {
|
|
143
|
-
try {
|
|
144
|
-
const result = await memoryFetch('GET', '/health')
|
|
145
|
-
return result.status === 'ok'
|
|
146
|
-
} catch {
|
|
147
|
-
return false
|
|
148
|
-
}
|
|
149
|
-
}
|
|
@@ -754,12 +754,5 @@ ${_bt.trim()}` : _bt.trim();
|
|
|
754
754
|
}
|
|
755
755
|
}
|
|
756
756
|
export {
|
|
757
|
-
OutputForwarder
|
|
758
|
-
cwdToProjectSlug,
|
|
759
|
-
detectCurrentSessionTranscript,
|
|
760
|
-
discoverCurrentClaudeSession,
|
|
761
|
-
discoverSessionBoundTranscript,
|
|
762
|
-
findLatestTranscriptByMtime,
|
|
763
|
-
getLatestInteractiveClaudeSession,
|
|
764
|
-
listInteractiveClaudeSessions
|
|
757
|
+
OutputForwarder
|
|
765
758
|
};
|
|
@@ -468,11 +468,7 @@ function clearActiveInstance(instanceId) {
|
|
|
468
468
|
});
|
|
469
469
|
}
|
|
470
470
|
export {
|
|
471
|
-
ACTIVE_INSTANCE_FILE,
|
|
472
|
-
OWNER_DIR,
|
|
473
471
|
RUNTIME_ROOT,
|
|
474
|
-
RUNTIME_STALE_TTL,
|
|
475
|
-
buildActiveInstanceState,
|
|
476
472
|
cleanupInstanceRuntimeFiles,
|
|
477
473
|
cleanupStaleRuntimeFiles,
|
|
478
474
|
clearActiveInstance,
|
|
@@ -485,13 +481,11 @@ export {
|
|
|
485
481
|
getPermissionResultPath,
|
|
486
482
|
getTerminalLeadPid,
|
|
487
483
|
getStatusPath,
|
|
488
|
-
getStopFlagPath,
|
|
489
484
|
getTurnEndPath,
|
|
490
485
|
notePreviousServerIfAny,
|
|
491
486
|
makeInstanceId,
|
|
492
487
|
readActiveInstance,
|
|
493
488
|
refreshActiveInstance,
|
|
494
489
|
releaseOwnedChannelLocks,
|
|
495
|
-
writeActiveInstance,
|
|
496
490
|
writeServerPid
|
|
497
491
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, statSync } from "fs";
|
|
2
2
|
import { execFileSync } from "child_process";
|
|
3
3
|
import { basename, join, resolve } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
4
|
import { mixdogHome } from "../../shared/plugin-paths.mjs";
|
|
6
5
|
|
|
7
6
|
function cwdToProjectSlug(cwd) {
|
|
@@ -94,9 +93,6 @@ function getLatestInteractiveClaudeSession() {
|
|
|
94
93
|
|
|
95
94
|
export {
|
|
96
95
|
cwdToProjectSlug,
|
|
97
|
-
getParentPid,
|
|
98
|
-
readSessionRecord,
|
|
99
|
-
isInteractiveSession,
|
|
100
96
|
discoverCurrentClaudeSession,
|
|
101
97
|
listInteractiveClaudeSessions,
|
|
102
98
|
getLatestInteractiveClaudeSession
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readdirSync, existsSync, statSync } from "fs";
|
|
2
2
|
import { basename, join, resolve } from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
3
|
import {
|
|
5
4
|
cwdToProjectSlug,
|
|
6
5
|
discoverCurrentClaudeSession,
|
|
@@ -181,16 +180,8 @@ function discoverSessionBoundTranscript() {
|
|
|
181
180
|
}
|
|
182
181
|
|
|
183
182
|
export {
|
|
184
|
-
resolveTranscriptForSession,
|
|
185
183
|
findLatestTranscriptByMtime,
|
|
186
|
-
isPidAlive,
|
|
187
184
|
sameResolvedPath,
|
|
188
|
-
transcriptStat,
|
|
189
|
-
sessionTranscriptCandidate,
|
|
190
|
-
latestMtimeTranscriptCandidate,
|
|
191
|
-
candidateAffinity,
|
|
192
|
-
compareTranscriptCandidates,
|
|
193
185
|
detectCurrentSessionTranscript,
|
|
194
|
-
discoverSessionBoundTranscript
|
|
195
|
-
TRANSCRIPT_MTIME_DECISIVE_MS
|
|
186
|
+
discoverSessionBoundTranscript
|
|
196
187
|
};
|
|
@@ -148,7 +148,6 @@ function loadKeytar() {
|
|
|
148
148
|
// Avoids Atomics.wait on main thread (which hangs when SAB is not forwarded to worker).
|
|
149
149
|
function keytarSync(method, ...args) {
|
|
150
150
|
loadKeytar(); // throws if not installed — before spawning child
|
|
151
|
-
const { spawnSync } = require('child_process');
|
|
152
151
|
// Pass service/account/value via env to avoid shell injection entirely.
|
|
153
152
|
const env = {
|
|
154
153
|
...process.env,
|