mixdog 0.9.141 → 0.9.142
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/native-binary-arch.mjs +93 -0
- package/scripts/native-binary-arch.test.mjs +75 -0
- package/scripts/native-tool-download.mjs +118 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +9 -9
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +1 -1
- package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
- package/src/tui/app/use-mouse-input.mjs +1 -1
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1 -1
- package/src/tui/themes/teal.mjs +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Architecture of a compiled artifact, read from its own header.
|
|
5
|
+
*
|
|
6
|
+
* The desktop runtime is prepared for a TARGET that need not match the build
|
|
7
|
+
* host, and the expensive failure is silent: an arm64 addon inside an x64 app
|
|
8
|
+
* packages, uploads, and publishes without complaint, then refuses to load on
|
|
9
|
+
* the user's machine. Reading the header turns that into a build error at the
|
|
10
|
+
* moment the archive is assembled.
|
|
11
|
+
*
|
|
12
|
+
* Only the fields that name the architecture are parsed; nothing here loads or
|
|
13
|
+
* executes the file.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Mach-O cpu_type_t. The 0x01000000 bit marks the 64-bit variants.
|
|
17
|
+
const MACHO_CPU = new Map([[0x01000007, 'x64'], [0x0100000c, 'arm64'], [7, 'ia32']])
|
|
18
|
+
// ELF e_machine.
|
|
19
|
+
const ELF_MACHINE = new Map([[0x3e, 'x64'], [0xb7, 'arm64'], [0x03, 'ia32']])
|
|
20
|
+
// PE IMAGE_FILE_HEADER.Machine.
|
|
21
|
+
const PE_MACHINE = new Map([[0x8664, 'x64'], [0xaa64, 'arm64'], [0x14c, 'ia32']])
|
|
22
|
+
|
|
23
|
+
function machOArch(buffer) {
|
|
24
|
+
if (buffer.length < 8) return null
|
|
25
|
+
const magic = buffer.readUInt32BE(0)
|
|
26
|
+
// Thin Mach-O: 0xfeedfacf (64-bit) / 0xfeedface (32-bit), either endianness.
|
|
27
|
+
if (magic === 0xfeedfacf || magic === 0xfeedface) return MACHO_CPU.get(buffer.readUInt32BE(4)) || null
|
|
28
|
+
if (magic === 0xcffaedfe || magic === 0xcefaedfe) return MACHO_CPU.get(buffer.readUInt32LE(4)) || null
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Universal binaries name every slice they carry, so they are read as a set. */
|
|
33
|
+
function machOFatArches(buffer) {
|
|
34
|
+
if (buffer.length < 8) return null
|
|
35
|
+
const magic = buffer.readUInt32BE(0)
|
|
36
|
+
// 0xcafebabe is also a Java class file; those never reach an addon path, and
|
|
37
|
+
// the slice count sanity check below rejects the overlap in practice.
|
|
38
|
+
if (magic !== 0xcafebabe && magic !== 0xcafebabf) return null
|
|
39
|
+
const wide = magic === 0xcafebabf
|
|
40
|
+
const count = buffer.readUInt32BE(4)
|
|
41
|
+
if (!count || count > 16) return null
|
|
42
|
+
const entrySize = wide ? 32 : 20
|
|
43
|
+
const arches = []
|
|
44
|
+
for (let index = 0; index < count; index += 1) {
|
|
45
|
+
const offset = 8 + index * entrySize
|
|
46
|
+
if (offset + 4 > buffer.length) return null
|
|
47
|
+
const arch = MACHO_CPU.get(buffer.readUInt32BE(offset))
|
|
48
|
+
if (arch) arches.push(arch)
|
|
49
|
+
}
|
|
50
|
+
return arches.length ? arches : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function elfArch(buffer) {
|
|
54
|
+
if (buffer.length < 20) return null
|
|
55
|
+
if (buffer.readUInt32BE(0) !== 0x7f454c46) return null
|
|
56
|
+
const littleEndian = buffer[5] === 1
|
|
57
|
+
const machine = littleEndian ? buffer.readUInt16LE(18) : buffer.readUInt16BE(18)
|
|
58
|
+
return ELF_MACHINE.get(machine) || null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function peArch(buffer) {
|
|
62
|
+
if (buffer.length < 64 || buffer.readUInt16LE(0) !== 0x5a4d) return null
|
|
63
|
+
const headerOffset = buffer.readUInt32LE(60)
|
|
64
|
+
if (headerOffset + 6 > buffer.length) return null
|
|
65
|
+
if (buffer.readUInt32LE(headerOffset) !== 0x00004550) return null
|
|
66
|
+
return PE_MACHINE.get(buffer.readUInt16LE(headerOffset + 4)) || null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every architecture the artifact can run on, or null when the bytes carry no
|
|
71
|
+
* recognizable object header (a text stub, a placeholder, a data file).
|
|
72
|
+
*/
|
|
73
|
+
export function nativeBinaryArches(buffer) {
|
|
74
|
+
const fat = machOFatArches(buffer)
|
|
75
|
+
if (fat) return fat
|
|
76
|
+
const single = machOArch(buffer) || elfArch(buffer) || peArch(buffer)
|
|
77
|
+
return single ? [single] : null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Convenience for the single-slice case; a fat binary reports its first slice. */
|
|
81
|
+
export function nativeBinaryArch(buffer) {
|
|
82
|
+
return nativeBinaryArches(buffer)?.[0] ?? null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when the artifact can run on `arch`. An unrecognized header answers
|
|
87
|
+
* true: the caller is verifying compiled objects, and refusing every file it
|
|
88
|
+
* cannot parse would fail builds over unrelated payloads.
|
|
89
|
+
*/
|
|
90
|
+
export function nativeBinaryRunsOn(buffer, arch) {
|
|
91
|
+
const arches = nativeBinaryArches(buffer)
|
|
92
|
+
return arches === null || arches.includes(arch)
|
|
93
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
nativeBinaryArch,
|
|
6
|
+
nativeBinaryArches,
|
|
7
|
+
nativeBinaryRunsOn,
|
|
8
|
+
} from './native-binary-arch.mjs'
|
|
9
|
+
|
|
10
|
+
function machO({ cpu, littleEndian = true, wide = true }) {
|
|
11
|
+
const buffer = Buffer.alloc(32)
|
|
12
|
+
const magic = wide ? 0xfeedfacf : 0xfeedface
|
|
13
|
+
if (littleEndian) {
|
|
14
|
+
buffer.writeUInt32BE(wide ? 0xcffaedfe : 0xcefaedfe, 0)
|
|
15
|
+
buffer.writeUInt32LE(cpu, 4)
|
|
16
|
+
} else {
|
|
17
|
+
buffer.writeUInt32BE(magic, 0)
|
|
18
|
+
buffer.writeUInt32BE(cpu, 4)
|
|
19
|
+
}
|
|
20
|
+
return buffer
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function machOFat(cpus) {
|
|
24
|
+
const buffer = Buffer.alloc(8 + cpus.length * 20)
|
|
25
|
+
buffer.writeUInt32BE(0xcafebabe, 0)
|
|
26
|
+
buffer.writeUInt32BE(cpus.length, 4)
|
|
27
|
+
cpus.forEach((cpu, index) => buffer.writeUInt32BE(cpu, 8 + index * 20))
|
|
28
|
+
return buffer
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function elf(machine) {
|
|
32
|
+
const buffer = Buffer.alloc(32)
|
|
33
|
+
buffer.writeUInt32BE(0x7f454c46, 0)
|
|
34
|
+
buffer[5] = 1
|
|
35
|
+
buffer.writeUInt16LE(machine, 18)
|
|
36
|
+
return buffer
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pe(machine) {
|
|
40
|
+
const buffer = Buffer.alloc(128)
|
|
41
|
+
buffer.writeUInt16LE(0x5a4d, 0)
|
|
42
|
+
buffer.writeUInt32LE(64, 60)
|
|
43
|
+
buffer.writeUInt32LE(0x00004550, 64)
|
|
44
|
+
buffer.writeUInt16LE(machine, 68)
|
|
45
|
+
return buffer
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test('Mach-O headers name their architecture in both endiannesses', () => {
|
|
49
|
+
assert.equal(nativeBinaryArch(machO({ cpu: 0x01000007 })), 'x64')
|
|
50
|
+
assert.equal(nativeBinaryArch(machO({ cpu: 0x0100000c })), 'arm64')
|
|
51
|
+
assert.equal(nativeBinaryArch(machO({ cpu: 0x01000007, littleEndian: false })), 'x64')
|
|
52
|
+
assert.equal(nativeBinaryArch(machO({ cpu: 7, wide: false })), 'ia32')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('universal binaries report every slice they carry', () => {
|
|
56
|
+
assert.deepEqual(nativeBinaryArches(machOFat([0x01000007, 0x0100000c])), ['x64', 'arm64'])
|
|
57
|
+
assert.equal(nativeBinaryRunsOn(machOFat([0x01000007, 0x0100000c]), 'arm64'), true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('ELF and PE headers are read too', () => {
|
|
61
|
+
assert.equal(nativeBinaryArch(elf(0x3e)), 'x64')
|
|
62
|
+
assert.equal(nativeBinaryArch(elf(0xb7)), 'arm64')
|
|
63
|
+
assert.equal(nativeBinaryArch(pe(0x8664)), 'x64')
|
|
64
|
+
assert.equal(nativeBinaryArch(pe(0xaa64)), 'arm64')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('a foreign architecture is rejected for the target', () => {
|
|
68
|
+
assert.equal(nativeBinaryRunsOn(machO({ cpu: 0x0100000c }), 'x64'), false)
|
|
69
|
+
assert.equal(nativeBinaryRunsOn(elf(0x3e), 'arm64'), false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('bytes without a recognizable object header do not fail a build', () => {
|
|
73
|
+
assert.equal(nativeBinaryArches(Buffer.from('#!/bin/sh\nexec node "$0"\n')), null)
|
|
74
|
+
assert.equal(nativeBinaryRunsOn(Buffer.alloc(4), 'x64'), true)
|
|
75
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Target-aware download of the four product-native tools for a desktop build.
|
|
5
|
+
*
|
|
6
|
+
* The runtime fetchers (graph/patch/spawn/token) resolve the HOST's asset.
|
|
7
|
+
* That is exactly right for an installed app fetching its own binary, and
|
|
8
|
+
* exactly wrong for preparing a runtime for another platform — so rather than
|
|
9
|
+
* teach four production code paths a build-only concern, this reads the same
|
|
10
|
+
* manifests and names the target explicitly.
|
|
11
|
+
*
|
|
12
|
+
* The URL is rebuilt from the manifest version and the target instead of being
|
|
13
|
+
* trusted verbatim, which keeps a rewritten manifest from redirecting the
|
|
14
|
+
* download; the recorded sha256 still has to match the bytes that arrive.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto'
|
|
17
|
+
import { mkdir, readFile, rename, rm } from 'node:fs/promises'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
MAX_NATIVE_BINARY_DOWNLOAD_BYTES,
|
|
23
|
+
streamResponseToFile,
|
|
24
|
+
} from '../src/runtime/shared/bounded-download.mjs'
|
|
25
|
+
|
|
26
|
+
const RELEASE_ROOT = 'https://github.com/tribgames/mixdog/releases/download'
|
|
27
|
+
const TOOLS_DIR = new URL('../src/runtime/agent/orchestrator/tools/', import.meta.url)
|
|
28
|
+
|
|
29
|
+
export const NATIVE_TOOL_KINDS = Object.freeze(['graph', 'patch', 'spawn', 'token'])
|
|
30
|
+
|
|
31
|
+
/** Released asset name. Only token ships as a Node addon; the rest are executables. */
|
|
32
|
+
export function nativeToolAssetName(kind, target) {
|
|
33
|
+
const suffix = kind === 'token' ? '.node' : (target.platform === 'win32' ? '.exe' : '')
|
|
34
|
+
return `mixdog-${kind}-${target.platform}-${target.arch}${suffix}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function nativeToolAssetUrl(kind, version, target) {
|
|
38
|
+
return `${RELEASE_ROOT}/${kind}-v${version}/${nativeToolAssetName(kind, target)}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Installed name inside the app, named for the TARGET rather than the host. */
|
|
42
|
+
export function nativeToolInstalledName(kind, target) {
|
|
43
|
+
if (kind === 'token') return 'mixdog-token.node'
|
|
44
|
+
return target.platform === 'win32' ? `mixdog-${kind}.exe` : `mixdog-${kind}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function sha256File(path) {
|
|
48
|
+
return createHash('sha256').update(await readFile(path)).digest('hex')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function download(url, destination, label) {
|
|
52
|
+
// 4 attempts, 1s/3s/9s backoff. A 4xx is the manifest being wrong, not the
|
|
53
|
+
// network being unlucky, so it ends the attempt sequence immediately.
|
|
54
|
+
const delays = [1000, 3000, 9000]
|
|
55
|
+
let lastError
|
|
56
|
+
for (let attempt = 0; attempt <= delays.length; attempt += 1) {
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(180_000) })
|
|
59
|
+
if (response.status >= 400 && response.status < 500) {
|
|
60
|
+
throw new Error(`${label}: HTTP ${response.status} is terminal — ${url}`)
|
|
61
|
+
}
|
|
62
|
+
if (!response.ok) throw new Error(`${label}: HTTP ${response.status} — ${url}`)
|
|
63
|
+
await streamResponseToFile(response, destination, {
|
|
64
|
+
maxBytes: MAX_NATIVE_BINARY_DOWNLOAD_BYTES,
|
|
65
|
+
label,
|
|
66
|
+
})
|
|
67
|
+
return
|
|
68
|
+
} catch (error) {
|
|
69
|
+
lastError = error
|
|
70
|
+
if (String(error?.message || '').includes('is terminal')) throw error
|
|
71
|
+
if (attempt < delays.length) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
`${label}: attempt ${attempt + 1} failed (${error?.message}); retrying…\n`,
|
|
74
|
+
)
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, delays[attempt]))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw lastError
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function downloadNativeTool(kind, target, cacheDir) {
|
|
83
|
+
const manifestPath = fileURLToPath(new URL(`${kind}-manifest.json`, TOOLS_DIR))
|
|
84
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
85
|
+
const version = String(manifest?.version || '')
|
|
86
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
87
|
+
throw new Error(`${kind}-manifest.json carries no usable version: ${version || '(empty)'}`)
|
|
88
|
+
}
|
|
89
|
+
const key = `${target.platform}-${target.arch}`
|
|
90
|
+
const asset = manifest?.assets?.[key]
|
|
91
|
+
const expectedSha256 = String(asset?.sha256 || '').toLowerCase()
|
|
92
|
+
if (!/^[a-f0-9]{64}$/.test(expectedSha256)) {
|
|
93
|
+
const supported = Object.keys(manifest?.assets || {}).join(', ') || '(none)'
|
|
94
|
+
throw new Error(`${kind}-manifest.json has no ${key} asset. Available: ${supported}.`)
|
|
95
|
+
}
|
|
96
|
+
const url = nativeToolAssetUrl(kind, version, target)
|
|
97
|
+
if (asset.url !== url) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`${kind}-manifest.json ${key} url does not match its own release identity.\n`
|
|
100
|
+
+ ` manifest: ${asset.url}\n expected: ${url}`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
await mkdir(cacheDir, { recursive: true })
|
|
104
|
+
const destination = join(cacheDir, `${nativeToolAssetName(kind, target)}-${version}`)
|
|
105
|
+
const label = `${kind} ${key} download`
|
|
106
|
+
try {
|
|
107
|
+
if (await sha256File(destination) === expectedSha256) return destination
|
|
108
|
+
} catch { /* absent or unreadable: download it */ }
|
|
109
|
+
const temporary = `${destination}.tmp-${process.pid}-${Date.now()}`
|
|
110
|
+
await download(url, temporary, label)
|
|
111
|
+
const actual = await sha256File(temporary)
|
|
112
|
+
if (actual !== expectedSha256) {
|
|
113
|
+
await rm(temporary, { force: true })
|
|
114
|
+
throw new Error(`${label}: sha256 mismatch — expected ${expectedSha256}, got ${actual}`)
|
|
115
|
+
}
|
|
116
|
+
await rename(temporary, destination)
|
|
117
|
+
return destination
|
|
118
|
+
}
|
|
@@ -164,7 +164,7 @@ export class GeminiProvider {
|
|
|
164
164
|
/**
|
|
165
165
|
* Stream-death recovery: re-issue a dead stream ONCE as a
|
|
166
166
|
* non-streaming generateContent call instead of failing the turn.
|
|
167
|
-
*
|
|
167
|
+
* Deliberately narrow — canFallbackNonStreaming() clears only a
|
|
168
168
|
* stream that exposed nothing, so rendered text is never duplicated and a
|
|
169
169
|
* dispatched tool can never run twice. Returns the aggregated response, or
|
|
170
170
|
* null when the failure is ineligible or the fallback itself fails.
|
|
@@ -130,7 +130,7 @@ export const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
|
130
130
|
// SSRF guard for any endpoint pulled from the discovery document or saved
|
|
131
131
|
// tokens. xAI OAuth endpoints must be https on x.ai / *.x.ai — reject
|
|
132
132
|
// anything else outright so a hostile discovery response can't redirect the
|
|
133
|
-
// token / refresh request.
|
|
133
|
+
// token / refresh request.
|
|
134
134
|
function assertTrustedXaiEndpoint(endpoint, label) {
|
|
135
135
|
let url;
|
|
136
136
|
try {
|
|
@@ -456,7 +456,7 @@ export async function consumeCompatChatCompletionStream(stream, {
|
|
|
456
456
|
if (leakGuard.enabled) flushLeak();
|
|
457
457
|
} catch (err) {
|
|
458
458
|
// Any mid-stream failure after live text was relayed is non-retryable —
|
|
459
|
-
// but the streamed partial must still ride on the error (
|
|
459
|
+
// but the streamed partial must still ride on the error (rule: once
|
|
460
460
|
// output is visible, keep it and finalize with a notice instead of
|
|
461
461
|
// discarding the turn). The loop's partial-final path consumes these.
|
|
462
462
|
if (emittedText) {
|
|
@@ -1029,7 +1029,7 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
1029
1029
|
const err = truncatedCompatStreamError(label, 'no response.completed');
|
|
1030
1030
|
if (state.emittedText) {
|
|
1031
1031
|
// Truncation after visible output: keep the streamed partial
|
|
1032
|
-
// (
|
|
1032
|
+
// (same rule) so the loop can finalize it as partial-final instead
|
|
1033
1033
|
// of dropping the turn. liveText marking still blocks replay.
|
|
1034
1034
|
markErrorLiveTextEmitted(err);
|
|
1035
1035
|
try {
|
|
@@ -312,7 +312,7 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
|
|
|
312
312
|
// xAI WS: do NOT pin x-grok-conv-id. Measured parallel runs show that
|
|
313
313
|
// forcing a routing shard via that header alternates cold caches across
|
|
314
314
|
// parallel workers; the automatic prompt-prefix cache holds up better
|
|
315
|
-
// when each handshake is unpinned.
|
|
315
|
+
// when each handshake is unpinned.
|
|
316
316
|
const headers = auth.type === 'xai'
|
|
317
317
|
? {
|
|
318
318
|
'Authorization': `Bearer ${auth.apiKey}`,
|
|
@@ -689,7 +689,7 @@ export async function _streamResponse({
|
|
|
689
689
|
finish();
|
|
690
690
|
}, interChunkMs);
|
|
691
691
|
};
|
|
692
|
-
//
|
|
692
|
+
// Per-event idle: (re)armed only on meaningful output deltas via
|
|
693
693
|
// bumpSemanticIdle(). Keepalive/metadata frames DON'T touch it, so a
|
|
694
694
|
// deltas-then-silent wedge trips this short semantic window.
|
|
695
695
|
const resetSemanticIdle = () => {
|
|
@@ -116,7 +116,7 @@ export function classifyError(err) {
|
|
|
116
116
|
const status = Number(err.httpStatus || err.status || err.response?.status || 0) || 0
|
|
117
117
|
if (AUTH_STATUSES.has(status)) return 'auth'
|
|
118
118
|
// Stale previous_response_id is recoverable by dropping the chain and
|
|
119
|
-
// re-issuing a full frame
|
|
119
|
+
// re-issuing a full frame, which is the retryable path. A typed
|
|
120
120
|
// 400 here is not a deterministic payload refusal.
|
|
121
121
|
if (shouldDropPreviousResponseId(err)) return 'transient'
|
|
122
122
|
if (typedErrorCode(err) === WEBSOCKET_CONNECTION_LIMIT) return 'transient'
|
|
@@ -137,7 +137,7 @@ export function classifyError(err) {
|
|
|
137
137
|
// ever attempted (observed live: pooled WS retired by the server between
|
|
138
138
|
// turns → close 1000 before response.created → the whole turn failed).
|
|
139
139
|
// Reference behavior retries the same disconnect (codex `CodexErr::Stream`
|
|
140
|
-
// is_retryable,
|
|
140
|
+
// is_retryable, and a stream fallback/retry applies); the exposure deny above
|
|
141
141
|
// still fails closed for anything already relayed or dispatched.
|
|
142
142
|
if (isNonTerminalStreamClose(err)) return 'transient'
|
|
143
143
|
|
|
@@ -162,10 +162,10 @@ export function classifyError(err) {
|
|
|
162
162
|
))) return 'transient'
|
|
163
163
|
|
|
164
164
|
// Provider wire error event (`response.failed` / terminal `error` frame):
|
|
165
|
-
// default-retry
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
165
|
+
// default-retry. Every response.failed whose typed code is not a
|
|
166
|
+
// deterministic refusal is retryable (fatal codes are an explicit
|
|
167
|
+
// allow-list), and server_error / server_is_overloaded count as retryable
|
|
168
|
+
// too. Evidence stays
|
|
169
169
|
// structural — the event's own typed code/type field — message text is
|
|
170
170
|
// never parsed. Exposure precedence is preserved: the replayUnsafe gate at
|
|
171
171
|
// the top of this function already returned 'permanent' for any stream
|
|
@@ -379,8 +379,8 @@ function isPermanentQuotaError(err) {
|
|
|
379
379
|
|
|
380
380
|
// ── Wire error events: default-retry with a fatal-code deny-list ────────────
|
|
381
381
|
// Deterministic refusal codes a `response.failed` / `error` wire event may
|
|
382
|
-
// carry: retrying the identical request can never succeed.
|
|
383
|
-
//
|
|
382
|
+
// carry: retrying the identical request can never succeed. The fatal set is
|
|
383
|
+
// context/quota/policy plus the auth/billing refusals the
|
|
384
384
|
// Responses and Anthropic wire formats use. Everything OUTSIDE this set —
|
|
385
385
|
// server_error, server_is_overloaded, slow_down, or an event with no code at
|
|
386
386
|
// all — is a server-side fault and is retried under the bounded budgets
|
|
@@ -753,7 +753,7 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
753
753
|
// A typed non-transient 4xx on the failure payload is a deterministic
|
|
754
754
|
// refusal even without a recognized code string.
|
|
755
755
|
if (failedStatus >= 400 && failedStatus < 500 && !TRANSIENT_STATUSES.has(failedStatus)) return null
|
|
756
|
-
// Default-retry
|
|
756
|
+
// Default-retry: a wire failure that is neither a fatal
|
|
757
757
|
// refusal nor a typed 4xx is a server-side fault — re-issue it under the
|
|
758
758
|
// bounded mid-stream budget instead of failing the turn.
|
|
759
759
|
return _allowMidstream('response_failed_retryable', attemptIndex, policy)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Post-compact file re-attachment
|
|
2
|
-
//
|
|
1
|
+
// Post-compact file re-attachment. Files the summarized-away head had `read`
|
|
2
|
+
// are re-read FRESH from
|
|
3
3
|
// disk and re-injected right after the summary message, so the model does not
|
|
4
4
|
// burn a turn (and tokens) re-reading files it was actively working with.
|
|
5
5
|
// - newest-first, capped at MAX_REATTACH_FILES and per-file/total token caps
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
stripWorkingFileSections,
|
|
60
60
|
} from './handoff.mjs';
|
|
61
61
|
|
|
62
|
-
// Post-compact file re-attachment
|
|
62
|
+
// Post-compact file re-attachment: re-inject fresh reads
|
|
63
63
|
// of files the summarized-away head was working with, when they still fit the
|
|
64
64
|
// budget. Semantic compact still uses this; recall-fasttrack lists paths
|
|
65
65
|
// instead of file bodies.
|
|
@@ -152,7 +152,7 @@ export function markSessionAskStart(id) {
|
|
|
152
152
|
// Publish heartbeat immediately so the status aggregator picks the
|
|
153
153
|
// session up in the connecting / requesting window. Without this the
|
|
154
154
|
// .hb file only landed on the first stream chunk — producing a 3–10s
|
|
155
|
-
// (xhigh: 30s+) invisible gap where agent sessions ran but the
|
|
155
|
+
// (xhigh: 30s+) invisible gap where agent sessions ran but the
|
|
156
156
|
// statusline showed no maintenance/agent badge. STREAM_FRESH_MS (5 min)
|
|
157
157
|
// still drops a session whose provider truly never returns a chunk;
|
|
158
158
|
// markSessionStreamDelta keeps refreshing once chunks arrive.
|
|
@@ -608,7 +608,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
608
608
|
try { bashAbortSignal = (await getAbortSignalForSession(options?.sessionId)) || null; }
|
|
609
609
|
catch { bashAbortSignal = null; }
|
|
610
610
|
combinedBashAbort = _combineAbortSignals(bashAbortSignal, options?.abortSignal || null);
|
|
611
|
-
// Promote-at-timeout
|
|
611
|
+
// Promote-at-timeout. When a
|
|
612
612
|
// foreground one-shot hits its timeout and is still running, adopt it
|
|
613
613
|
// as a background job (task_id + notify) instead of tree-killing it.
|
|
614
614
|
// The truthy MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS env restores the old
|
|
@@ -133,8 +133,8 @@ function normalizeFmt(fmt) {
|
|
|
133
133
|
return f === 'jpg' ? 'jpeg' : f;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
// Build the metadata text block prepended to a resized image
|
|
137
|
-
//
|
|
136
|
+
// Build the metadata text block prepended to a resized image:
|
|
137
|
+
// "[Image: WxH, displayed at ...]" plus a coordinate
|
|
138
138
|
// scale note when the image was downsampled.
|
|
139
139
|
export function imageMetadataText(dims, sourcePath) {
|
|
140
140
|
if (!dims) return sourcePath ? `[Image source: ${sourcePath}]` : null;
|
|
@@ -158,7 +158,7 @@ export function imageMetadataText(dims, sourcePath) {
|
|
|
158
158
|
|
|
159
159
|
// Resize / downsample an image buffer with sharp.
|
|
160
160
|
//
|
|
161
|
-
// Pipeline (
|
|
161
|
+
// Pipeline (resize / downsample under a token budget):
|
|
162
162
|
// 1. metadata() — read format + dimensions.
|
|
163
163
|
// 2. resize fit:inside withoutEnlargement to <= 2000x2000 (only when over
|
|
164
164
|
// dimension caps OR over the 3.75MB raw target).
|
|
@@ -170,7 +170,7 @@ export async function extractIpynbText(fullPath, { maxOutputBytes = DEFAULT_READ
|
|
|
170
170
|
const rawTxt = data['text/plain']
|
|
171
171
|
? (Array.isArray(data['text/plain']) ? data['text/plain'].join('') : data['text/plain'])
|
|
172
172
|
: (Array.isArray(out.text) ? out.text.join('') : out.text);
|
|
173
|
-
//
|
|
173
|
+
// A single huge output is replaced
|
|
174
174
|
// with a jq hint rather than dumped inline.
|
|
175
175
|
if (typeof rawTxt === 'string' && rawTxt.length > IPYNB_OUTPUT_MAX_CHARS) {
|
|
176
176
|
block += `\n# Output: [large output omitted — ${rawTxt.length} chars; inspect with: cat "${fullPath}" | jq '.cells[${cellIndex}].outputs']`;
|
|
@@ -116,7 +116,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
116
116
|
_recordReadSnapshot,
|
|
117
117
|
READ_MAX_OUTPUT_BYTES,
|
|
118
118
|
} = helpers;
|
|
119
|
-
//
|
|
119
|
+
// `file_path` alias — the official SDK schema uses `file_path`;
|
|
120
120
|
// mixdog has historically used `path`. Honor `file_path` so a
|
|
121
121
|
// CC-trained agent's call shape works without translation.
|
|
122
122
|
const usedFilePathAlias = typeof args.file_path === 'string' && !args.path;
|
|
@@ -232,8 +232,8 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
232
232
|
// result is sliced back into the original per-entry windows
|
|
233
233
|
// for response assembly. Non-same-path entries are untouched.
|
|
234
234
|
const rawEntries = args.path.map((r) => {
|
|
235
|
-
//
|
|
236
|
-
// 1-based (
|
|
235
|
+
// `file_path` alias on a per-entry batch: file_path is
|
|
236
|
+
// 1-based (SDK schema), so decrement a positive offset to
|
|
237
237
|
// match the 0-based `path` form. Mirrors the scalar
|
|
238
238
|
// alias adjustment at line 57.
|
|
239
239
|
const entryUsesFilePathAlias = typeof r?.file_path === 'string' && !r?.path;
|
|
@@ -599,7 +599,7 @@ export function execShellCommand({
|
|
|
599
599
|
if (grace.unref) grace.unref();
|
|
600
600
|
};
|
|
601
601
|
child.once('exit', _onChildExit);
|
|
602
|
-
// Auto-background transition
|
|
602
|
+
// Auto-background transition. Two triggers
|
|
603
603
|
// resolve the call immediately with a 'backgrounded' result while the
|
|
604
604
|
// child keeps running, adopted into the shell-jobs registry but still
|
|
605
605
|
// owned by this CLI process:
|
|
@@ -904,7 +904,7 @@ export function execShellCommand({
|
|
|
904
904
|
if (abortSignal) {
|
|
905
905
|
abortHandler = () => {
|
|
906
906
|
// Interrupt (the user typed a NEW message while this was running) is a
|
|
907
|
-
// "also look at this" signal, not "stop that" —
|
|
907
|
+
// "also look at this" signal, not "stop that" — it backgrounds instead
|
|
908
908
|
// of killing there, and throwing away a long build the user never asked
|
|
909
909
|
// to stop is the worse outcome. Explicit cancellation (ESC) keeps the
|
|
910
910
|
// kill. The promotion reuses the timeout path's guards verbatim so a
|
|
@@ -337,7 +337,7 @@ export class TaskOutput {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
// Direct mode has no JS write path, so byte counters must come from the
|
|
340
|
-
// filesystem (
|
|
340
|
+
// filesystem (a "poll the file tail" approach).
|
|
341
341
|
_refreshDirectSizes() {
|
|
342
342
|
if (!this.direct) return;
|
|
343
343
|
try { this.stdoutFileSize = statSync(this.stdoutPath).size; } catch {}
|
|
@@ -531,7 +531,7 @@ export class ExecResult {
|
|
|
531
531
|
// renderer in builtin/bash-tool.mjs turns this into a model-visible marker.
|
|
532
532
|
this.failurePhase = opts.failurePhase || null;
|
|
533
533
|
this.failureReason = opts.failureReason || null;
|
|
534
|
-
// Auto-background transition
|
|
534
|
+
// Auto-background transition. When a
|
|
535
535
|
// foreground command outlives autoBackgroundMs the call settles with
|
|
536
536
|
// backgrounded:true + the jobId for manual task control. The
|
|
537
537
|
// child stays owned by the CLI process; stdout/stderr keep flowing to
|
|
@@ -55,7 +55,7 @@ const BLOCKED_PATTERNS = [
|
|
|
55
55
|
// cmd `del /s` / `rd /s`) are NOT blocked outright — each is target-checked
|
|
56
56
|
// by a dedicated guard below via _isDangerousDeleteTarget, blocking only
|
|
57
57
|
// filesystem-root / home / top-level-system / whole-cwd targets (CC-level).
|
|
58
|
-
// `git reset --hard` is likewise no longer hard-blocked (
|
|
58
|
+
// `git reset --hard` is likewise no longer hard-blocked (a prompt suffices; the
|
|
59
59
|
// agent workflow already gates destructive git ops).
|
|
60
60
|
// Bare `git push --force` (and `--force=`) still blocks; the safer
|
|
61
61
|
// `--force-with-lease` / `--force-if-includes` variants pass.
|
|
@@ -289,7 +289,7 @@ function refreshActiveInstance(instanceId, meta, options) {
|
|
|
289
289
|
}
|
|
290
290
|
preservedExtra.gateway_server_pid = prevGatewayServerPid;
|
|
291
291
|
// Clear session-scoped gateway metrics when the transcript changes —
|
|
292
|
-
// a new
|
|
292
|
+
// a new session must not inherit the previous session's context
|
|
293
293
|
// usage before the gateway re-advertises.
|
|
294
294
|
const metricTranscript =
|
|
295
295
|
typeof prevForPreserve?.gateway_transcript_path === 'string' && prevForPreserve.gateway_transcript_path
|
|
@@ -660,7 +660,7 @@ export function useMouseInput({
|
|
|
660
660
|
// words/lines from this span (see buildSpanRect). Leave the drag
|
|
661
661
|
// ARMED (active:true): a release without motion keeps this highlight
|
|
662
662
|
// (buildSpanRect returns the span for an in-span target), while
|
|
663
|
-
// any motion extends it.
|
|
663
|
+
// any motion extends it. The word/line select sets
|
|
664
664
|
// isDragging=true + anchorSpan; the mouse-up finalizes.
|
|
665
665
|
const kind = clickCount === 2 ? 'word' : 'line';
|
|
666
666
|
const wr = kind === 'word' ? store.getWordRectAt?.(x, y) : store.getLineRectAt?.(y);
|
|
@@ -35,11 +35,11 @@ import { buildTableRender } from '../markdown/table-layout.mjs';
|
|
|
35
35
|
const DEFAULT_MARKDOWN_WIDTH = 80;
|
|
36
36
|
|
|
37
37
|
// Hard ceilings so a pathological tool result can never lock the render loop.
|
|
38
|
-
//
|
|
38
|
+
// The collapsed fold budgets roughly MAX_LINES*width*4; for the EXPANDED body we
|
|
39
39
|
// cap total characters processed and total lines kept, with an explicit marker.
|
|
40
40
|
const MAX_EXPANDED_CHARS = 256 * 1024; // 256 KB of text gets per-line processing
|
|
41
41
|
const MAX_EXPANDED_LINES = 4000; // logical-line safety ceiling; physical mount cap is separate
|
|
42
|
-
const MAX_JSON_FORMAT_LENGTH = 10_000; //
|
|
42
|
+
const MAX_JSON_FORMAT_LENGTH = 10_000; // JSON pretty-format cap
|
|
43
43
|
const MAX_HIGHLIGHT_LINE_CHARS = 2000; // skip token-scan on absurdly long lines
|
|
44
44
|
// Lockstep with ToolExecution collapsed fit budget (MIN_RESULT_LINE_CHARS).
|
|
45
45
|
const MIN_EXPANDED_BODY_COLS = 24;
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -2061,7 +2061,7 @@ var tealPalette = {
|
|
|
2061
2061
|
error: "rgb(204,102,102)",
|
|
2062
2062
|
// red #cc6666
|
|
2063
2063
|
warning: "rgb(255,255,0)",
|
|
2064
|
-
// yellow #ffff00 (
|
|
2064
|
+
// yellow #ffff00 (pure yellow by design)
|
|
2065
2065
|
suggestion: "rgb(129,162,190)",
|
|
2066
2066
|
// mdLink #81a2be
|
|
2067
2067
|
panelTitle: "rgb(138,190,183)",
|
package/src/tui/themes/teal.mjs
CHANGED
|
@@ -38,7 +38,7 @@ export const tealPalette = {
|
|
|
38
38
|
promptBorder: 'rgb(80,80,80)', // borderMuted/darkGray #505050
|
|
39
39
|
success: 'rgb(181,189,104)', // green #b5bd68
|
|
40
40
|
error: 'rgb(204,102,102)', // red #cc6666
|
|
41
|
-
warning: 'rgb(255,255,0)', // yellow #ffff00 (
|
|
41
|
+
warning: 'rgb(255,255,0)', // yellow #ffff00 (pure yellow by design)
|
|
42
42
|
suggestion: 'rgb(129,162,190)', // mdLink #81a2be
|
|
43
43
|
panelTitle: 'rgb(138,190,183)', // accent
|
|
44
44
|
permission: 'rgb(204,102,102)', // red
|