mixdog 0.9.140 → 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/LICENSES/MIT.txt +4 -0
- package/NOTICE.md +12 -46
- package/package.json +1 -1
- package/scripts/manifest-cache-key.mjs +44 -0
- 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 +2 -2
- 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 +12 -12
- 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-constants.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +2 -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/parent-bridge.mjs +2 -2
- package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
- package/src/standalone/agent-tool/shard-spread.mjs +5 -3
- package/src/standalone/agent-tool/shard-spread.test.mjs +16 -1
- package/src/tui/App.jsx +3 -3
- package/src/tui/app/transcript-window.mjs +4 -5
- package/src/tui/app/use-mouse-input.mjs +2 -2
- package/src/tui/app/use-transcript-scroll.mjs +37 -29
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +21 -31
- package/src/tui/themes/teal.mjs +1 -1
- package/vendor/ink/build/ink.js +7 -85
- package/vendor/ink/build/output.js +7 -9
- package/vendor/ink/build/selection-rect.js +80 -0
- package/vendor/ink/build/termio-keypress.js +110 -79
- package/vendor/ink/build/_baseline-termio-keypress.js +0 -240
- package/vendor/ink/build/_baseline-termio-tokenize.js +0 -253
package/LICENSES/MIT.txt
CHANGED
|
@@ -12,6 +12,10 @@ component listed here. Each component keeps its own copyright notice.
|
|
|
12
12
|
https://github.com/microsoft/vscode-codicons
|
|
13
13
|
Copyright (c) Microsoft Corporation
|
|
14
14
|
|
|
15
|
+
Octicons
|
|
16
|
+
https://github.com/primer/octicons
|
|
17
|
+
Copyright (c) GitHub, Inc.
|
|
18
|
+
|
|
15
19
|
GitHub Pull Requests and Issues (VS Code extension)
|
|
16
20
|
https://github.com/microsoft/vscode-pull-request-github
|
|
17
21
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
package/NOTICE.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# NOTICE
|
|
2
2
|
|
|
3
|
-
Mixdog itself is MIT-licensed (see `LICENSE`). The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
implementation, with no code taken. Everything is kept here in one place so
|
|
7
|
-
the individual source files stay free of scattered attribution comments.
|
|
3
|
+
Mixdog itself is MIT-licensed (see `LICENSE`). The sections below cover the
|
|
4
|
+
third-party code and data that Mixdog actually carries, kept here in one place
|
|
5
|
+
so the individual source files stay free of scattered attribution comments.
|
|
8
6
|
|
|
9
7
|
Full license texts live in `LICENSES/`:
|
|
10
8
|
|
|
@@ -47,6 +45,15 @@ The chrome-level UI glyph font used by the desktop and web renderer. The code
|
|
|
47
45
|
is MIT; the icons themselves are licensed under Creative Commons Attribution
|
|
48
46
|
4.0 International (CC BY 4.0).
|
|
49
47
|
|
|
48
|
+
### Octicons — Copyright (c) GitHub, Inc.
|
|
49
|
+
|
|
50
|
+
<https://github.com/primer/octicons>
|
|
51
|
+
|
|
52
|
+
The five changed-file status glyphs in
|
|
53
|
+
`apps/desktop/src/renderer/ScmStatusIcon.tsx` carry Octicons 16px path data
|
|
54
|
+
unmodified: `diffAdded`, `diffModified`, `diffRemoved`, `diffRenamed` and
|
|
55
|
+
`alert`.
|
|
56
|
+
|
|
50
57
|
### GitHub Pull Requests extension — Copyright (c) Microsoft Corporation
|
|
51
58
|
|
|
52
59
|
<https://github.com/microsoft/vscode-pull-request-github>
|
|
@@ -98,44 +105,3 @@ upstream NOTICE file is preserved as `LICENSES/codex-NOTICE.txt`.
|
|
|
98
105
|
The Apache-2.0 terms require this notice to travel with any redistribution of
|
|
99
106
|
the derived files. Full license text: `LICENSES/Apache-2.0.txt`
|
|
100
107
|
(<https://www.apache.org/licenses/LICENSE-2.0>).
|
|
101
|
-
|
|
102
|
-
## Behavioral references
|
|
103
|
-
|
|
104
|
-
The projects below informed Mixdog through publicly observable behavior, a
|
|
105
|
-
published wire contract, or a documented algorithm. No source code from them
|
|
106
|
-
is present in Mixdog, so no license obligation travels with the result; they
|
|
107
|
-
are recorded here because the source files themselves carry no attribution
|
|
108
|
-
comments.
|
|
109
|
-
|
|
110
|
-
- **Chromium** — the workspace tab strip follows Chromium's tab-strip layout
|
|
111
|
-
semantics: two layout domains around the crossover width, the active-tab
|
|
112
|
-
floor, the inactive sliver floor, the left-to-right remainder grant and the
|
|
113
|
-
icon visibility ladder. The implementation is independent TypeScript and CSS
|
|
114
|
-
on Mixdog's own constants (`apps/desktop/src/renderer/WorkspaceTabStrip.tsx`,
|
|
115
|
-
`apps/desktop/src/renderer/desktop/04-workspace-tabs.css`).
|
|
116
|
-
- **GitHub Desktop** — the Source Control dock grammar: commit-form layout,
|
|
117
|
-
changed-file status glyph semantics, single-sentence path colouring, history
|
|
118
|
-
context-menu gating, and detached-checkout / tag rules. Painted entirely on
|
|
119
|
-
Mixdog's own semantic tokens.
|
|
120
|
-
- **OpenCode** — transcript auto-scroll gesture grammar, virtual-timeline
|
|
121
|
-
anchoring and the streaming-markdown projection model.
|
|
122
|
-
- **Orca** — editor-surface and tab-hierarchy structure, the terminal host
|
|
123
|
-
portal, the keep-awake power-save blocker and the hosted-review link
|
|
124
|
-
derivation.
|
|
125
|
-
- **Files** — the folder pane's grouping keys, date-span labels, size buckets
|
|
126
|
-
and discrete layout size ladder
|
|
127
|
-
(`apps/desktop/src/renderer/FolderPane.lazy.tsx`).
|
|
128
|
-
- **Claude Code** (Anthropic) — terminal input tokenizing and keypress
|
|
129
|
-
parsing, the TUI selection word model in `vendor/ink`, and interactive turn
|
|
130
|
-
semantics such as queued-message recall, message-selector rewind and
|
|
131
|
-
background-task lifetime. The Anthropic OAuth route additionally sends the
|
|
132
|
-
client identity that Anthropic's token edge validates; those values are a
|
|
133
|
-
wire requirement, not a derivation.
|
|
134
|
-
- **opencode-antigravity-auth** — MIT
|
|
135
|
-
(<https://github.com/NoeFabris/opencode-antigravity-auth>). The Google Cloud
|
|
136
|
-
Code Assist wire contract used by the Antigravity OAuth provider: OAuth
|
|
137
|
-
client parameters, endpoint fallback order, client impersonation headers,
|
|
138
|
-
request envelope and thinking-signature handling.
|
|
139
|
-
- **assistant-ui, Chatbox, Cherry Studio, Jan, Zed** — the heading-size and
|
|
140
|
-
list-density survey behind the desktop markdown ladder in
|
|
141
|
-
`apps/desktop/src/renderer/desktop/22-markdown.css`.
|
package/package.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Version-neutral digest for release cache keys.
|
|
5
|
+
*
|
|
6
|
+
* Deploy rewrites the version field of package.json / package-lock.json on
|
|
7
|
+
* every release, so a cache key built from hashFiles() over those manifests is
|
|
8
|
+
* guaranteed to miss on the one run that needs it: the desktop bundle and the
|
|
9
|
+
* five prepared platform runtimes were rebuilt from scratch each release even
|
|
10
|
+
* though their inputs were byte-identical apart from the version string.
|
|
11
|
+
* Hashing the same manifests with the package identity stripped keeps the key
|
|
12
|
+
* pinned to the dependency tree that actually decides the output.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto'
|
|
15
|
+
import { readFile } from 'node:fs/promises'
|
|
16
|
+
import { resolve } from 'node:path'
|
|
17
|
+
import { pathToFileURL } from 'node:url'
|
|
18
|
+
|
|
19
|
+
import { normalizeRuntimeLockfile } from './runtime-dependency-cache-key.mjs'
|
|
20
|
+
|
|
21
|
+
export const MANIFEST_CACHE_SCHEMA = 1
|
|
22
|
+
|
|
23
|
+
export function manifestCacheKey(manifests) {
|
|
24
|
+
const digest = createHash('sha256')
|
|
25
|
+
digest.update(`manifest-v${MANIFEST_CACHE_SCHEMA}`)
|
|
26
|
+
for (const manifest of manifests) {
|
|
27
|
+
digest.update(JSON.stringify(normalizeRuntimeLockfile(manifest)))
|
|
28
|
+
}
|
|
29
|
+
return digest.digest('hex')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''
|
|
33
|
+
if (invokedPath === import.meta.url) {
|
|
34
|
+
const manifestPaths = process.argv.slice(2)
|
|
35
|
+
if (!manifestPaths.length) {
|
|
36
|
+
throw new Error('Usage: manifest-cache-key.mjs <package.json|package-lock.json>...')
|
|
37
|
+
}
|
|
38
|
+
Promise.all(manifestPaths.map((path) => readFile(resolve(path))))
|
|
39
|
+
.then((manifests) => process.stdout.write(`${manifestCacheKey(manifests)}\n`))
|
|
40
|
+
.catch((error) => {
|
|
41
|
+
process.stderr.write(`Manifest cache key failed: ${error?.message || error}\n`)
|
|
42
|
+
process.exitCode = 1
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -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.
|
|
@@ -110,7 +110,7 @@ export function resolveGrokOAuthResponsesTransport() {
|
|
|
110
110
|
// Retired model aliases xAI no longer exposes by their old ids. The live
|
|
111
111
|
// catalog surfaces the coding model as grok-build-0.1; map the legacy ids to
|
|
112
112
|
// it so a stale config selection doesn't hit a model-not-found. Exact table,
|
|
113
|
-
// not a heuristic.
|
|
113
|
+
// not a heuristic.
|
|
114
114
|
const RETIRED_MODEL_ALIASES = Object.freeze({
|
|
115
115
|
'grok-code-fast-1': 'grok-build-0.1',
|
|
116
116
|
'grok-code-fast': 'grok-build-0.1',
|
|
@@ -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
|
|
@@ -215,10 +215,10 @@ export function isNonTerminalStreamClose(err) {
|
|
|
215
215
|
/**
|
|
216
216
|
* Should this failed stream be re-issued as a NON-STREAMING request?
|
|
217
217
|
*
|
|
218
|
-
*
|
|
219
|
-
* `stream:false` instead of failing the turn (
|
|
218
|
+
* A last safety net: when a stream dies, repeat the same request with
|
|
219
|
+
* `stream:false` instead of failing the turn (gated off only when
|
|
220
220
|
* streaming tool execution could double-run a tool). MixDog dispatches tools
|
|
221
|
-
* eagerly, so this stays deliberately
|
|
221
|
+
* eagerly, so this stays deliberately narrow: only a stream that
|
|
222
222
|
* exposed NOTHING qualifies. An exposed stream is already covered by the
|
|
223
223
|
* loop-level retraction replay (send-with-recovery), which asks the owner to
|
|
224
224
|
* withdraw the rendered characters first.
|
|
@@ -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
|
|
@@ -3,9 +3,8 @@ import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
|
3
3
|
|
|
4
4
|
// Read tool caps.
|
|
5
5
|
//
|
|
6
|
-
// READ_MAX_SIZE_BYTES (10 MB) — fast-path file-size threshold
|
|
7
|
-
//
|
|
8
|
-
// this size use readFile + in-memory split by default, which CC measured at
|
|
6
|
+
// READ_MAX_SIZE_BYTES (10 MB) — fast-path file-size threshold. Files at or below
|
|
7
|
+
// this size use readFile + in-memory split by default, measured at
|
|
9
8
|
// ~2x faster than createReadStream + readline for typical source. Explicit
|
|
10
9
|
// offset/limit windows on files above READ_STREAM_RANGE_MIN_BYTES take the
|
|
11
10
|
// streaming path too, so a targeted read avoids materialising a whole
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Optional-sharp image resize / downsample helper
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// budget-bounded image block instead of refusing oversized originals.
|
|
1
|
+
// Optional-sharp image resize / downsample helper: a `read` on an image
|
|
2
|
+
// returns a viewable, budget-bounded image block instead of refusing
|
|
3
|
+
// oversized originals.
|
|
5
4
|
//
|
|
6
5
|
// sharp is a direct runtime dependency. Entry points still degrade to `null`
|
|
7
6
|
// when a platform-native binding cannot load so a damaged install reports the
|
|
@@ -134,8 +133,8 @@ function normalizeFmt(fmt) {
|
|
|
134
133
|
return f === 'jpg' ? 'jpeg' : f;
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
// Build the metadata text block prepended to a resized image
|
|
138
|
-
//
|
|
136
|
+
// Build the metadata text block prepended to a resized image:
|
|
137
|
+
// "[Image: WxH, displayed at ...]" plus a coordinate
|
|
139
138
|
// scale note when the image was downsampled.
|
|
140
139
|
export function imageMetadataText(dims, sourcePath) {
|
|
141
140
|
if (!dims) return sourcePath ? `[Image source: ${sourcePath}]` : null;
|
|
@@ -159,7 +158,7 @@ export function imageMetadataText(dims, sourcePath) {
|
|
|
159
158
|
|
|
160
159
|
// Resize / downsample an image buffer with sharp.
|
|
161
160
|
//
|
|
162
|
-
// Pipeline (
|
|
161
|
+
// Pipeline (resize / downsample under a token budget):
|
|
163
162
|
// 1. metadata() — read format + dimensions.
|
|
164
163
|
// 2. resize fit:inside withoutEnlargement to <= 2000x2000 (only when over
|
|
165
164
|
// dimension caps OR over the 3.75MB raw target).
|