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.
Files changed (42) hide show
  1. package/LICENSES/MIT.txt +4 -0
  2. package/NOTICE.md +12 -46
  3. package/package.json +1 -1
  4. package/scripts/manifest-cache-key.mjs +44 -0
  5. package/scripts/native-binary-arch.mjs +93 -0
  6. package/scripts/native-binary-arch.test.mjs +75 -0
  7. package/scripts/native-tool-download.mjs +118 -0
  8. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
  9. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +2 -2
  10. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -2
  11. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +1 -1
  12. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
  13. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +12 -12
  14. package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
  15. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +1 -1
  16. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -1
  18. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -3
  19. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -7
  20. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +2 -3
  21. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +1 -1
  22. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -2
  24. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +1 -1
  26. package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
  27. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  28. package/src/standalone/agent-tool/shard-spread.mjs +5 -3
  29. package/src/standalone/agent-tool/shard-spread.test.mjs +16 -1
  30. package/src/tui/App.jsx +3 -3
  31. package/src/tui/app/transcript-window.mjs +4 -5
  32. package/src/tui/app/use-mouse-input.mjs +2 -2
  33. package/src/tui/app/use-transcript-scroll.mjs +37 -29
  34. package/src/tui/components/tool-output-format.mjs +2 -2
  35. package/src/tui/dist/index.mjs +21 -31
  36. package/src/tui/themes/teal.mjs +1 -1
  37. package/vendor/ink/build/ink.js +7 -85
  38. package/vendor/ink/build/output.js +7 -9
  39. package/vendor/ink/build/selection-rect.js +80 -0
  40. package/vendor/ink/build/termio-keypress.js +110 -79
  41. package/vendor/ink/build/_baseline-termio-keypress.js +0 -240
  42. 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 license sections below
4
- cover third-party code and data that Mixdog actually carries. The closing
5
- "Behavioral references" section records projects that only informed the
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.140",
3
+ "version": "0.9.142",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -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
- * Narrower than cc by design — canFallbackNonStreaming() clears only a
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. Mirrors openclaw extensions/xai/model-definitions.ts.
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. Mirrors openclaw's isTrustedXaiOAuthEndpoint.
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 (CC rule: once
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
- // (CC rule) so the loop can finalize it as partial-final instead
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. Reference: vercel/ai xai provider.
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
- // pi per-event idle: (re)armed only on meaningful output deltas via
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 (codex maps it to ApiError::Retryable). A typed
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, cc falls back/retries the stream); the exposure deny above
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, codex parity. codex maps every response.failed whose typed
166
- // code is not a deterministic refusal to ApiError::Retryable (fatal codes
167
- // are an explicit allow-list); cc retries all 5xx/overloaded 10×; opencode
168
- // marks server_error/server_is_overloaded isRetryable. Evidence stays
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
- * cc's last safety net: when a stream dies it repeats the same request with
219
- * `stream:false` instead of failing the turn (claude.ts, gated off only when
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 narrower than cc: only a stream that
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. Mirrors codex's
383
- // fatal set (context/quota/policy) plus the auth/billing refusals the
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 (codex parity): a wire failure that is neither a fatal
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 (claude-code createPostCompactFileAttachments
2
- // parity). Files the summarized-away head had `read` are re-read FRESH from
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 (claude-code parity): re-inject fresh reads
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 CC
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 (CC shouldAutoBackground parity). When a
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 mirroring
7
- // Reference FAST_PATH_MAX_SIZE (readFileInRange.ts:44). Files at or below
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. Mirrors reference
2
- // imageResizer.ts (maybeResizeAndDownsampleImageBuffer +
3
- // readImageWithTokenBudget) so a `read` on an image returns a viewable,
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. Mirrors CC
138
- // createImageMetadataText: "[Image: WxH, displayed at ...]" plus a coordinate
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 (mirrors CC maybeResizeAndDownsampleImageBuffer + token budget):
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).