mixdog 0.9.157 → 0.9.158

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 (49) hide show
  1. package/README.md +30 -13
  2. package/package.json +2 -2
  3. package/scripts/build-runtime-linux.sh +7 -0
  4. package/scripts/build-runtime-macos.sh +7 -0
  5. package/scripts/lib/stage-postgres-runtime-windows.ps1 +13 -2
  6. package/scripts/smoke-runtime-negative.sh +3 -2
  7. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +234 -0
  8. package/src/runtime/agent/orchestrator/providers/cursor-wire-interactions.mjs +80 -0
  9. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +75 -1
  10. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +234 -93
  11. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +19 -7
  12. package/src/runtime/agent/orchestrator/session/context-utils.mjs +75 -0
  13. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +79 -8
  14. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +17 -2
  15. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +60 -0
  16. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +23 -3
  17. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +49 -3
  18. package/src/runtime/agent/orchestrator/tools/builtin/lib/glob-static-prefix.mjs +121 -0
  19. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +6 -14
  20. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +4 -5
  21. package/src/runtime/computer-bridge/tool-defs.mjs +2 -0
  22. package/src/runtime/memory/data/runtime-manifest.json +17 -17
  23. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +5 -3
  24. package/src/runtime/memory/lib/memory-cycle3.mjs +5 -5
  25. package/src/runtime/memory/lib/runtime-fetcher.mjs +16 -6
  26. package/src/runtime/office/capabilities.mjs +3 -1
  27. package/src/runtime/office/design-pptx-plan.mjs +210 -124
  28. package/src/runtime/office/design-pptx-stack.mjs +50 -0
  29. package/src/runtime/office/design-system.mjs +10 -0
  30. package/src/runtime/office/image-layout.mjs +81 -0
  31. package/src/runtime/office/office-com-host.ps1 +178 -22
  32. package/src/runtime/office/office-com-session-host.ps1 +32 -5
  33. package/src/runtime/office/portable-pptx-core.mjs +11 -1
  34. package/src/runtime/office/portable-pptx-shapes.mjs +32 -6
  35. package/src/runtime/office/portable-slide-shapes.mjs +14 -1
  36. package/src/runtime/office/portable-validation.mjs +23 -4
  37. package/src/runtime/office/text-metrics.mjs +96 -2
  38. package/src/runtime/office/tool-defs.mjs +5 -1
  39. package/src/session-runtime/builtin-features.mjs +105 -0
  40. package/src/session-runtime/config-lifecycle.mjs +6 -1
  41. package/src/session-runtime/context-status.mjs +14 -13
  42. package/src/session-runtime/runtime-core.mjs +22 -14
  43. package/src/session-runtime/settings-api.mjs +49 -2
  44. package/src/standalone/session-protocol.mjs +2 -0
  45. package/src/tui/dist/index.mjs +6 -12
  46. package/src/tui/lib/voice-setup.mjs +13 -20
  47. package/src/tui/session/context-state.mjs +7 -1
  48. package/src/tui/session/session-api-ext.mjs +10 -5
  49. package/src/tui/session/session-api.mjs +18 -0
package/README.md CHANGED
@@ -45,31 +45,35 @@ workflow setup.
45
45
 
46
46
  Terminal-Bench 2.1 — same model, same 89 tasks, same official verifier, with
47
47
  only the harness changed. Against the native CLI of each model family, Mixdog
48
- scores higher while spending less to get there.
48
+ delivers the same results at the same speed — on a fraction of the context,
49
+ for far less cost.
49
50
 
50
51
  ### GPT-5.6 Sol xhigh — Mixdog vs Codex CLI
51
52
 
52
53
  ![Terminal-Bench 2.1: Mixdog with GPT-5.6 Sol xhigh versus Codex CLI](https://raw.githubusercontent.com/tribgames/mixdog/main/benchmarks/terminal-bench-2.1/tb21-sol-vs-codex.svg)
53
54
 
54
- - **86.5%** (385/445) vs Codex CLI's **84.3%** (75/89)
55
- - **42%** lower priced cost$0.641 vs $1.096 per trial
56
- - **45%** smaller median final context18.5k vs 33.5k tokens
57
- - **1.11×** faster — 339s vs 378s per trial
55
+ - **39%** lower priced cost $0.476 vs $0.782 per trial
56
+ - **46%** smaller median final context 18.5k vs 34.3k tokens
57
+ - **86.5%** (385/445) vs Codex CLI's **86.1%** (383/445) full `k=5` on both
58
+ sides, pass@5 **96.6%** vs 95.5%
59
+ - Matched speed — 415s vs 437s wall time per trial
58
60
 
59
61
  ### Claude Opus 5 — Mixdog vs Claude Code
60
62
 
61
63
  ![Terminal-Bench 2.1: Mixdog with Claude Opus 5 versus Claude Code](https://raw.githubusercontent.com/tribgames/mixdog/main/benchmarks/terminal-bench-2.1/tb21-opus-vs-claude-code.svg)
62
64
 
63
- - **79/89** vs Claude Code's **77/89**
64
65
  - **19%** lower priced cost — $104.29 vs $129.21 per run
65
66
  - **28%** smaller median final context — 27.6k vs 38.2k tokens
66
- - **1.15×** faster
67
+ - **79/89** vs Claude Code's **77/89**
68
+ - **1.16×** faster — 610s vs 708s wall time per trial
67
69
 
68
- Every run uses the official Harbor verifier, fast mode off, a 272k context
69
- window, and zero retries. The Mixdog Sol run follows the protocol the official
70
- Terminal-Bench leaderboard requires all 89 tasks repeated five times (`k=5`,
71
- 445 trials); the Codex CLI baseline and both Opus-side runs are single passes
72
- (`k=1`, 89 trials each).
70
+ Every run uses the official Harbor verifier, fast mode off, and a 272k context
71
+ window; task failures and agent timeouts are never retried. The Sol comparison
72
+ follows the protocol the official Terminal-Bench leaderboard requires on both
73
+ sides all 89 tasks repeated five times (`k=5`, 445 trials each); the
74
+ Opus-side runs are single passes (`k=1`, 89 trials each). Speed is the full
75
+ trial wall clock, and cost values both sides at the same current API list
76
+ rates.
73
77
 
74
78
  The leaderboard is not accepting community submissions, so every run here ships
75
79
  its raw artifacts instead — Harbor verdicts, official verifier output, pinned
@@ -88,6 +92,13 @@ the harness, presets, and metric scripts that recompute each number above:
88
92
  MCP, skills, hooks, and plugins.
89
93
  - **Local memory** — semantic and lexical recall with project-scoped context
90
94
  and multilingual retrieval.
95
+ - **Browser Use** — a logged-in Chromium pane the agent can drive: tabs,
96
+ forms, downloads, and snapshots, with one-time Chrome profile import
97
+ including cookies and passwords.
98
+ - **Computer Use** — agent control of the Windows desktop through screen
99
+ capture, accessibility, OCR, and a strict guarded input contract.
100
+ - **Office documents** — author and edit Word, Excel, and PowerPoint files
101
+ with model-authored design plans, charts, and assurance-checked output.
91
102
  - **Encrypted remote access** — pair the installable web app with Desktop and
92
103
  use Mixdog from a browser or phone over authenticated E2EE.
93
104
  - **Desktop coding app** — agent panes, Monaco editor, Git, terminals, file
@@ -202,14 +213,20 @@ Mixdog Desktop runs the same agent runtime as the CLI:
202
213
  - Git staging, commits, branches, and generated commit messages
203
214
  - File explorer with previews, thumbnails, search, and drag-and-drop
204
215
  - Integrated terminal tabs using the local system shell
216
+ - Browser Use pane with agent control and Chromium profile import
217
+ - Computer Use on Windows with guarded native input
218
+ - Office document authoring with rendered previews
205
219
  - Image and video generation Studio with a persistent local gallery
206
220
  - Visual workflow, agent, schedule, and webhook editors
207
221
  - Voice dictation with an optional local transcription runtime
222
+ - Extensions hub with guided setup for Git, Memory, Browser Use, Computer
223
+ Use, Office, and voice
208
224
  - Provider setup, usage, git identity, and remote pairing settings
209
225
 
210
226
  The paired remote web app is installable on desktop and mobile browsers. It
211
227
  uses an authenticated end-to-end encrypted connection before session state,
212
- terminal data, files, or operation requests cross the relay.
228
+ terminal data, files, or operation requests cross the relay, and adds mobile
229
+ share-target intake, push notifications, and remote Browser Use.
213
230
 
214
231
  For desktop development:
215
232
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.157",
3
+ "version": "0.9.158",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -72,7 +72,7 @@
72
72
  "test:session-transport": "node --test scripts/session-transport-test.mjs scripts/daemon-bootstrap-test.mjs src/runtime/shared/child-spawn-remote.test.mjs src/standalone/session-runtime-agent-control-client.test.mjs src/standalone/session-runtime-dispatch-cancel.test.mjs src/standalone/session-runtime-host-factory.test.mjs src/standalone/session-runtime-inline-host.test.mjs src/standalone/session-runtime-provider-cooldown.test.mjs src/standalone/session-runtime-shard-host.test.mjs src/standalone/session-runtime-shard-router.test.mjs",
73
73
  "test:session": "node --test scripts/runtime-turn-contract-test.mjs scripts/session-save-fault-store-test.mjs scripts/turn-review-revert-test.mjs src/runtime/shared/tool-surface.test.mjs src/runtime/bridge-clients.test.mjs",
74
74
  "test:quick": "npm run test:session && npm run test:session-transport && npm run test:desktop-main --prefix apps/desktop",
75
- "test:office": "node --test scripts/run-office-live-tests.test.mjs src/runtime/office/office-runtime.test.mjs src/runtime/office/office-assurance.test.mjs src/runtime/office/portable-authoring.test.mjs && npm run test:office-approval --prefix apps/desktop",
75
+ "test:office": "node --test scripts/run-office-live-tests.test.mjs src/runtime/office/office-runtime.test.mjs src/runtime/office/office-assurance.test.mjs src/runtime/office/portable-authoring.test.mjs src/runtime/office/slide-quality.test.mjs && npm run test:office-approval --prefix apps/desktop",
76
76
  "test:office:live": "node scripts/run-office-live-tests.mjs",
77
77
  "test:media": "node --test src/runtime/media/store.test.mjs src/runtime/media/renditions.test.mjs src/runtime/media/adapters/codex-image.test.mjs",
78
78
  "failures": "node scripts/tool-failures.mjs",
@@ -78,6 +78,7 @@ else
78
78
  make -j"$(nproc)"
79
79
  make install
80
80
  make -C contrib/pgcrypto install
81
+ make -C contrib/pg_trgm install
81
82
  fi
82
83
 
83
84
  PG_CONFIG="$STAGE_DIR/bin/pg_config"
@@ -255,6 +256,10 @@ for attempt in 1 2 3; do
255
256
  echo " vector extension version: $EXTV"
256
257
  echo " distance query result: $DIST"
257
258
  [[ "$EXTV" == "$PGVECTOR_VERSION" ]] || { echo "FAIL: extversion=$EXTV expected=$PGVECTOR_VERSION"; exit 1; }
259
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -c "CREATE EXTENSION pg_trgm;" > /dev/null
260
+ SIM="$("${RUN_AS[@]}" "$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -tAc "SELECT similarity('mixdog','mixdig') > 0;")"
261
+ echo " pg_trgm similarity probe: $SIM"
262
+ [[ "$SIM" == "t" ]] || { echo "FAIL: pg_trgm similarity=$SIM"; exit 1; }
258
263
  "${RUN_AS[@]}" "$RUNTIME_DIR/bin/pg_ctl" -D "$SMOKE_DATA" -m fast stop > /dev/null
259
264
  )
260
265
  attempt_rc=$?
@@ -323,6 +328,8 @@ for attempt in 1 2 3; do
323
328
  "$EXTRACT_DIR/bin/psql" -h 127.0.0.1 -p "$EXTRACT_PORT" -U postgres -d postgres -tAc \
324
329
  "SELECT extversion FROM pg_extension WHERE extname='vector';")"
325
330
  [[ "$EXTV2" == "$PGVECTOR_VERSION" ]] || { echo "FAIL: extracted-smoke extversion=$EXTV2"; exit 1; }
331
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
332
+ "$EXTRACT_DIR/bin/psql" -h 127.0.0.1 -p "$EXTRACT_PORT" -U postgres -d postgres -c "CREATE EXTENSION pg_trgm;" > /dev/null
326
333
  "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
327
334
  "$EXTRACT_DIR/bin/pg_ctl" -D "$EXTRACT_DATA" -m fast stop > /dev/null
328
335
  )
@@ -73,6 +73,7 @@ else
73
73
  make -j"$(sysctl -n hw.logicalcpu)"
74
74
  make install
75
75
  make -C contrib/pgcrypto install
76
+ make -C contrib/pg_trgm install
76
77
  fi
77
78
 
78
79
  PG_CONFIG="$STAGE_DIR/bin/pg_config"
@@ -165,6 +166,10 @@ DIST="$("$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d pos
165
166
  echo " vector extension version: $EXTV"
166
167
  echo " distance query result: $DIST"
167
168
  [[ "$EXTV" == "$PGVECTOR_VERSION" ]] || { echo "FAIL: extversion=$EXTV expected=$PGVECTOR_VERSION"; exit 1; }
169
+ "$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -c "CREATE EXTENSION pg_trgm;" > /dev/null
170
+ SIM="$("$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -tAc "SELECT similarity('mixdog','mixdig') > 0;")"
171
+ echo " pg_trgm similarity probe: $SIM"
172
+ [[ "$SIM" == "t" ]] || { echo "FAIL: pg_trgm similarity=$SIM"; exit 1; }
168
173
  "$RUNTIME_DIR/bin/pg_ctl" -D "$SMOKE_DATA" -m fast stop > /dev/null
169
174
  trap - EXIT
170
175
  rm -rf "$SMOKE_DATA"
@@ -207,6 +212,8 @@ env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
207
212
  "$EXTRACT_DIR/bin/psql" -h 127.0.0.1 -p "$EXTRACT_PORT" -U postgres -d postgres -c "CREATE EXTENSION vector;" > /dev/null
208
213
  EXTV2="$(env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" "$EXTRACT_DIR/bin/psql" -h 127.0.0.1 -p "$EXTRACT_PORT" -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';")"
209
214
  [[ "$EXTV2" == "$PGVECTOR_VERSION" ]] || { echo "FAIL: extracted-smoke extversion=$EXTV2"; exit 1; }
215
+ env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
216
+ "$EXTRACT_DIR/bin/psql" -h 127.0.0.1 -p "$EXTRACT_PORT" -U postgres -d postgres -c "CREATE EXTENSION pg_trgm;" > /dev/null
210
217
  env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
211
218
  "$EXTRACT_DIR/bin/pg_ctl" -D "$EXTRACT_DATA" -m fast stop > /dev/null
212
219
  trap - EXIT
@@ -64,6 +64,7 @@ function Stage-MixdogWindowsPgRuntime {
64
64
  )
65
65
  $ModuleSources = [ordered]@{
66
66
  'dict_snowball.dll' = Join-Path $PgRoot 'lib\dict_snowball.dll'
67
+ 'pg_trgm.dll' = Join-Path $PgRoot 'lib\pg_trgm.dll'
67
68
  'plpgsql.dll' = Join-Path $PgRoot 'lib\plpgsql.dll'
68
69
  'vector.dll' = $VectorDll
69
70
  }
@@ -137,6 +138,15 @@ function Stage-MixdogWindowsPgRuntime {
137
138
  }
138
139
  $PlpgsqlAssets | Copy-Item -Destination $RuntimeExtensionDir -Force
139
140
 
141
+ # pg_trgm ships with the runtime: memory recall's hybrid trigram CTEs and
142
+ # transcript inserts depend on it (runtime-v0.4.0 omitted it and every
143
+ # memory write/recall failed with 'could not access file "$libdir/pg_trgm"').
144
+ $PgTrgmAssets = @(Get-ChildItem -Path "$SourceShare\extension\pg_trgm*" -File)
145
+ if ($PgTrgmAssets.Count -eq 0) {
146
+ throw "Required pg_trgm extension assets not found under $SourceShare\extension"
147
+ }
148
+ $PgTrgmAssets | Copy-Item -Destination $RuntimeExtensionDir -Force
149
+
140
150
  $RuntimeBytes = (
141
151
  Get-ChildItem -LiteralPath $RuntimeDir -File -Recurse |
142
152
  Measure-Object -Property Length -Sum
@@ -174,7 +184,7 @@ function Assert-MixdogWindowsPgRuntime {
174
184
  throw "Unexpected PostgreSQL executable set: $($ActualExecutables -join ', ')"
175
185
  }
176
186
 
177
- $ExpectedModules = @('dict_snowball.dll', 'plpgsql.dll', 'vector.dll')
187
+ $ExpectedModules = @('dict_snowball.dll', 'pg_trgm.dll', 'plpgsql.dll', 'vector.dll')
178
188
  $ActualModules = @(
179
189
  Get-ChildItem -LiteralPath "$RuntimeDir\lib" -File -Filter '*.dll' |
180
190
  ForEach-Object Name |
@@ -195,7 +205,7 @@ function Assert-MixdogWindowsPgRuntime {
195
205
 
196
206
  $UnexpectedExtensionAssets = @(
197
207
  Get-ChildItem -LiteralPath "$RuntimeDir\share\extension" -Force |
198
- Where-Object { $_.Name -notlike 'plpgsql*' -and $_.Name -notlike 'vector*' }
208
+ Where-Object { $_.Name -notlike 'pg_trgm*' -and $_.Name -notlike 'plpgsql*' -and $_.Name -notlike 'vector*' }
199
209
  )
200
210
  if ($UnexpectedExtensionAssets.Count -gt 0) {
201
211
  throw "Unexpected extension assets leaked into runtime: $($UnexpectedExtensionAssets.Count)"
@@ -205,6 +215,7 @@ function Assert-MixdogWindowsPgRuntime {
205
215
  "$RuntimeDir\share\extension\vector.control",
206
216
  "$RuntimeDir\share\extension\vector--$PgVectorVersion.sql",
207
217
  "$RuntimeDir\share\extension\plpgsql.control",
218
+ "$RuntimeDir\share\extension\pg_trgm.control",
208
219
  "$RuntimeDir\share\postgres.bki"
209
220
  )
210
221
  foreach ($Path in $RequiredPaths) {
@@ -9,7 +9,7 @@
9
9
 
10
10
  set -euo pipefail
11
11
 
12
- TAG="${TAG:-runtime-v0.4.0}"
12
+ TAG="${TAG:-runtime-v0.4.1}"
13
13
  OS="${OS:-$(uname -s | tr '[:upper:]' '[:lower:]')}"
14
14
  ARCH="${ARCH:-$(uname -m | sed 's/x86_64/x64/')}"
15
15
  PG_VER="16.4"
@@ -76,7 +76,8 @@ echo "==> ldd vector.so (under env -i):"
76
76
  "${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/psql" -h 127.0.0.1 -p "$PORT" -U postgres -d postgres -c "CREATE EXTENSION vector;" > /dev/null
77
77
  EXTV="$("${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/psql" -h 127.0.0.1 -p "$PORT" -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';")"
78
78
  [[ "$EXTV" == "$PGVECTOR_VER" ]] || { echo "FAIL: extversion=$EXTV"; exit 1; }
79
- echo " PASS: fresh-extract boot + vector extension"
79
+ "${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/psql" -h 127.0.0.1 -p "$PORT" -U postgres -d postgres -c "CREATE EXTENSION pg_trgm;" > /dev/null
80
+ echo " PASS: fresh-extract boot + vector + pg_trgm extensions"
80
81
 
81
82
  echo "==> Test 5: same tarball extracted twice → second initdb refuses (data-dir already initialized)"
82
83
  mkdir -p "$WORK/twice"
@@ -0,0 +1,234 @@
1
+ const SCHEMA_ANNOTATIONS = new Set([
2
+ '$comment',
3
+ '$id',
4
+ '$schema',
5
+ 'default',
6
+ 'deprecated',
7
+ 'description',
8
+ 'examples',
9
+ 'readOnly',
10
+ 'title',
11
+ 'writeOnly',
12
+ ]);
13
+
14
+ export const MAX_CONNECT_FRAME_BYTES = 64 * 1024 * 1024;
15
+ export const MAX_CHECKPOINT_BYTES = 48 * 1024 * 1024;
16
+ export const MAX_TOOL_TEXT_BYTES = 512 * 1024;
17
+ export const MAX_TOOL_MEDIA_BYTES = 16 * 1024 * 1024;
18
+ export const MAX_USER_IMAGE_BYTES = 5 * 1024 * 1024;
19
+ export const MAX_CONVERSATION_BLOB_BYTES = 128 * 1024 * 1024;
20
+ export const MAX_CONVERSATION_BLOB_ENTRIES = 512;
21
+ export const MAX_INDIVIDUAL_BLOB_BYTES = 32 * 1024 * 1024;
22
+
23
+ function boundedInteger(value, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
24
+ const parsed = Number(value);
25
+ if (!Number.isFinite(parsed) || parsed < min) return fallback;
26
+ return Math.min(max, Math.floor(parsed));
27
+ }
28
+
29
+ export function resolveCursorStreamTuning(env = process.env) {
30
+ return {
31
+ idleTimeoutMs: boundedInteger(env.MIXDOG_CURSOR_STREAM_IDLE_TIMEOUT_MS, 180_000),
32
+ parkTimeoutMs: boundedInteger(env.MIXDOG_CURSOR_STREAM_PARK_TIMEOUT_MS, 45_000, { min: 1_000 }),
33
+ maxRetries: boundedInteger(env.MIXDOG_CURSOR_STREAM_IDLE_MAX_RETRIES, 5, { max: 10 }),
34
+ };
35
+ }
36
+
37
+ function slimSchema(value, depth = 0) {
38
+ if (value == null || depth > 12) return value;
39
+ if (Array.isArray(value)) return value.map((entry) => slimSchema(entry, depth + 1));
40
+ if (typeof value !== 'object') return value;
41
+ const output = {};
42
+ for (const [key, child] of Object.entries(value)) {
43
+ if (SCHEMA_ANNOTATIONS.has(key) || child === undefined) continue;
44
+ if (key === 'additionalProperties' && child === true) continue;
45
+ if (key === 'required' && Array.isArray(child) && child.length === 0) continue;
46
+ output[key] = slimSchema(child, depth + 1);
47
+ }
48
+ return output;
49
+ }
50
+
51
+ function conciseDescription(value) {
52
+ const normalized = String(value || '').replace(/\s+/g, ' ').trim();
53
+ if (normalized.length <= 120) return normalized;
54
+ const sentence = normalized.match(/^.{24,117}?[.!?](?:\s|$)/)?.[0]?.trim();
55
+ return sentence || `${normalized.slice(0, 117)}...`;
56
+ }
57
+
58
+ export function prepareCursorToolDefinition(tool) {
59
+ const fn = tool?.function || tool || {};
60
+ const inputSchema = fn.parameters && typeof fn.parameters === 'object'
61
+ ? slimSchema(fn.parameters)
62
+ : { type: 'object', properties: {} };
63
+ return {
64
+ name: fn.name,
65
+ description: conciseDescription(fn.description),
66
+ inputSchema,
67
+ };
68
+ }
69
+
70
+ function truncateUtf8(value, maxBytes) {
71
+ const text = String(value ?? '');
72
+ const encoded = Buffer.from(text, 'utf8');
73
+ if (encoded.length <= maxBytes) return text;
74
+ const notice = Buffer.from('\n\n[Mixdog truncated this Cursor tool result to stay within the wire limit.]');
75
+ const end = Math.max(0, maxBytes - notice.length);
76
+ const clipped = encoded.subarray(0, end).toString('utf8').replace(/\uFFFD$/u, '');
77
+ return `${clipped}${notice.toString('utf8')}`;
78
+ }
79
+
80
+ export function capCursorToolResult(result) {
81
+ const media = [];
82
+ let mediaBytes = 0;
83
+ for (const image of Array.isArray(result?.media) ? result.media : []) {
84
+ const bytes = image?.data instanceof Uint8Array ? image.data : null;
85
+ if (!bytes || bytes.byteLength > MAX_TOOL_MEDIA_BYTES) continue;
86
+ if (mediaBytes + bytes.byteLength > MAX_TOOL_MEDIA_BYTES) break;
87
+ media.push(image);
88
+ mediaBytes += bytes.byteLength;
89
+ }
90
+ return {
91
+ ...result,
92
+ content: truncateUtf8(result?.content, MAX_TOOL_TEXT_BYTES),
93
+ media,
94
+ };
95
+ }
96
+
97
+ export function assertCursorUserImages(images) {
98
+ for (const image of images || []) {
99
+ const size = image?.data?.byteLength || 0;
100
+ if (size > MAX_USER_IMAGE_BYTES) {
101
+ const error = new Error(`Cursor image exceeds ${MAX_USER_IMAGE_BYTES} bytes`);
102
+ error.code = 'cursor_payload_too_large';
103
+ throw error;
104
+ }
105
+ }
106
+ }
107
+
108
+ function blobBytes(blobs) {
109
+ let total = 0;
110
+ for (const value of blobs.values()) total += value?.byteLength || 0;
111
+ return total;
112
+ }
113
+
114
+ export function storeCursorBlob(blobs, key, value) {
115
+ const data = value instanceof Uint8Array ? value : new Uint8Array(value);
116
+ if (data.byteLength > MAX_INDIVIDUAL_BLOB_BYTES) {
117
+ const error = new Error(`Cursor blob exceeds ${MAX_INDIVIDUAL_BLOB_BYTES} bytes`);
118
+ error.code = 'cursor_payload_too_large';
119
+ throw error;
120
+ }
121
+ if (blobs.has(key)) blobs.delete(key);
122
+ blobs.set(key, data);
123
+ while (
124
+ blobs.size > MAX_CONVERSATION_BLOB_ENTRIES
125
+ || blobBytes(blobs) > MAX_CONVERSATION_BLOB_BYTES
126
+ ) {
127
+ const oldest = blobs.keys().next().value;
128
+ if (oldest === undefined || (oldest === key && blobs.size === 1)) break;
129
+ blobs.delete(oldest);
130
+ }
131
+ return data;
132
+ }
133
+
134
+ export function createCursorByteQueue() {
135
+ let chunks = [];
136
+ let chunkIndex = 0;
137
+ let chunkOffset = 0;
138
+ let byteLength = 0;
139
+
140
+ const compact = () => {
141
+ if (chunkIndex < 64) return;
142
+ chunks = chunks.slice(chunkIndex);
143
+ chunkIndex = 0;
144
+ };
145
+ const copy = (count, consume) => {
146
+ if (count > byteLength) throw new Error('Cursor byte queue underflow');
147
+ const output = Buffer.allocUnsafe(count);
148
+ let written = 0;
149
+ let index = chunkIndex;
150
+ let offset = chunkOffset;
151
+ while (written < count) {
152
+ const chunk = chunks[index];
153
+ const take = Math.min(count - written, chunk.length - offset);
154
+ chunk.copy(output, written, offset, offset + take);
155
+ written += take;
156
+ offset += take;
157
+ if (offset === chunk.length) {
158
+ index += 1;
159
+ offset = 0;
160
+ }
161
+ }
162
+ if (consume) {
163
+ chunkIndex = index;
164
+ chunkOffset = offset;
165
+ byteLength -= count;
166
+ compact();
167
+ }
168
+ return output;
169
+ };
170
+
171
+ return {
172
+ get byteLength() { return byteLength; },
173
+ append(value) {
174
+ const chunk = Buffer.from(value);
175
+ if (!chunk.length) return;
176
+ chunks.push(chunk);
177
+ byteLength += chunk.length;
178
+ },
179
+ peek(count) { return copy(count, false); },
180
+ read(count) { return copy(count, true); },
181
+ };
182
+ }
183
+
184
+ export function createCursorStreamWatchdog({ idleTimeoutMs, parkTimeoutMs, onTimeout }) {
185
+ let timer = null;
186
+ let stopped = false;
187
+ const arm = (timeoutMs, kind) => {
188
+ if (timer) clearTimeout(timer);
189
+ timer = null;
190
+ if (stopped || timeoutMs <= 0) return;
191
+ timer = setTimeout(() => onTimeout(kind), timeoutMs);
192
+ timer.unref?.();
193
+ };
194
+ return {
195
+ start() { arm(idleTimeoutMs, 'idle'); },
196
+ progress(kind) {
197
+ if (kind === 'work') arm(idleTimeoutMs, 'idle');
198
+ else if (kind === 'park') arm(parkTimeoutMs, 'park');
199
+ },
200
+ stop() {
201
+ stopped = true;
202
+ if (timer) clearTimeout(timer);
203
+ timer = null;
204
+ },
205
+ };
206
+ }
207
+
208
+ export function cursorInteractionProgress(update) {
209
+ if (!update) return 'none';
210
+ if (update.heartbeat) return 'liveness';
211
+ if (update.textDelta?.text || update.thinkingDelta?.text) return 'work';
212
+ if (
213
+ update.tokenDelta
214
+ || update.toolCallStarted
215
+ || update.toolCallCompleted
216
+ || update.partialToolCall
217
+ || update.toolCallDelta
218
+ || update.thinkingCompleted
219
+ || update.summary
220
+ || update.summaryStarted
221
+ || update.summaryCompleted
222
+ || update.stepStarted
223
+ || update.stepCompleted
224
+ || update.turnEnded
225
+ ) return 'work';
226
+ return 'none';
227
+ }
228
+
229
+ export function isRetryableCursorStreamError(error) {
230
+ const status = Number(error?.httpStatus || error?.status || 0);
231
+ if ([400, 401, 403, 404, 429].includes(status)) return false;
232
+ if (error?.name === 'AbortError') return false;
233
+ return status === 0 || status >= 500;
234
+ }
@@ -0,0 +1,80 @@
1
+ const REJECT_REASON = 'This Cursor-side interaction is unavailable. Use the Mixdog tools instead.';
2
+
3
+ function response(name, value) {
4
+ return {
5
+ handled: true,
6
+ action: `${name}_rejected`,
7
+ message: { interactionResponse: value },
8
+ };
9
+ }
10
+
11
+ export function buildCursorInteractionResponse(query = {}) {
12
+ const id = Number(query.id) || 0;
13
+ if (query.webSearchRequestQuery) {
14
+ return response('web_search', {
15
+ id,
16
+ webSearchRequestResponse: { rejected: { reason: REJECT_REASON } },
17
+ });
18
+ }
19
+ if (query.exaSearchRequestQuery) {
20
+ return response('exa_search', {
21
+ id,
22
+ exaSearchRequestResponse: { rejected: { reason: REJECT_REASON } },
23
+ });
24
+ }
25
+ if (query.exaFetchRequestQuery) {
26
+ return response('exa_fetch', {
27
+ id,
28
+ exaFetchRequestResponse: { rejected: { reason: REJECT_REASON } },
29
+ });
30
+ }
31
+ if (query.switchModeRequestQuery) {
32
+ return response('switch_mode', {
33
+ id,
34
+ switchModeRequestResponse: { rejected: { reason: REJECT_REASON } },
35
+ });
36
+ }
37
+ if (query.askQuestionInteractionQuery) {
38
+ return response('ask_question', {
39
+ id,
40
+ askQuestionInteractionResponse: { result: { rejected: { reason: REJECT_REASON } } },
41
+ });
42
+ }
43
+ if (query.createPlanRequestQuery) {
44
+ return response('create_plan', {
45
+ id,
46
+ createPlanRequestResponse: { result: { error: { error: REJECT_REASON } } },
47
+ });
48
+ }
49
+ if (query.setupVmEnvironmentArgs) {
50
+ return {
51
+ handled: true,
52
+ action: 'setup_vm_acknowledged',
53
+ message: { interactionResponse: { id, setupVmEnvironmentResult: { success: {} } } },
54
+ };
55
+ }
56
+ if (query.$unknown?.some((field) => field.no === 9)) {
57
+ return response('web_fetch', {
58
+ id,
59
+ webFetchRequestResponse: { rejected: { reason: REJECT_REASON } },
60
+ });
61
+ }
62
+ const field = query.$unknown?.[0]?.no;
63
+ return {
64
+ handled: false,
65
+ action: 'unsupported',
66
+ queryCase: field ? `field_${field}` : 'unknown',
67
+ message: null,
68
+ };
69
+ }
70
+
71
+ export function buildCursorExecThrow(exec = {}, detail = 'Unsupported Cursor exec') {
72
+ return {
73
+ execClientControlMessage: {
74
+ throw: {
75
+ id: Number(exec.id) || 0,
76
+ error: `${detail}. Use a Mixdog tool instead.`,
77
+ },
78
+ },
79
+ };
80
+ }