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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/scripts/native-binary-arch.mjs +93 -0
  3. package/scripts/native-binary-arch.test.mjs +75 -0
  4. package/scripts/native-tool-download.mjs +118 -0
  5. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
  6. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -1
  7. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -2
  8. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +1 -1
  9. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
  10. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +9 -9
  11. package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
  12. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +1 -1
  13. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +3 -3
  16. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -3
  18. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -2
  19. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +1 -1
  21. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  22. package/src/tui/app/use-mouse-input.mjs +1 -1
  23. package/src/tui/components/tool-output-format.mjs +2 -2
  24. package/src/tui/dist/index.mjs +1 -1
  25. package/src/tui/themes/teal.mjs +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.141",
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,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.
@@ -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
@@ -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
@@ -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. Mirrors CC
137
- // 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
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 (mirrors CC maybeResizeAndDownsampleImageBuffer + token budget):
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
- // Port CC behaviour: a single huge output is replaced
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
- // CC `file_path` alias — official SDK schema uses `file_path`;
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
- // CC `file_path` alias on a per-entry batch: file_path is
236
- // 1-based (CC schema), so decrement a positive offset to
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 (CC startBackgrounding analogue). Two triggers
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" — CC backgrounds instead
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 (CC "poll the file tail" analogue).
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 (CC startBackgrounding analogue). When a
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 (CC prompts; the
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 CC session must not inherit the previous session's context
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. Mirrors selectWordAt/selectLineAt setting
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
- // CC uses ~MAX_LINES*width*4 for the collapsed fold; for the EXPANDED body we
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; // mirror CC's tryJsonFormatContent cap
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;
@@ -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 (pi uses pure yellow)
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)",
@@ -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 (pi uses pure yellow)
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