mixdog 1.0.7 → 1.0.9

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 (109) hide show
  1. package/package.json +2 -1
  2. package/scripts/lib/run-node-tests.mjs +13 -0
  3. package/scripts/session-efficiency-diag.mjs +283 -0
  4. package/src/defaults/skills/computer-use/SKILL.md +3 -3
  5. package/src/defaults/skills/local-provider/SKILL.md +2 -1
  6. package/src/defaults/skills/setup/SKILL.md +4 -4
  7. package/src/defaults/skills/setup/references/actions.md +1 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +2 -3
  9. package/src/headless-exec.mjs +1 -0
  10. package/src/rules/shared/70-delivery.md +2 -2
  11. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
  12. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
  13. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
  16. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
  18. package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
  19. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
  20. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
  21. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
  22. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
  23. package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
  24. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
  25. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
  26. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
  27. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
  29. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
  30. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
  31. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
  32. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
  33. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
  34. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
  36. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
  37. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
  38. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
  39. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
  40. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
  41. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +7 -0
  42. package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
  43. package/src/runtime/computer-bridge/action-schema.mjs +3 -3
  44. package/src/runtime/computer-bridge/actions.mjs +4 -0
  45. package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
  46. package/src/runtime/computer-bridge/client.mjs +10 -183
  47. package/src/runtime/computer-bridge/core-actions.mjs +2 -1
  48. package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
  49. package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
  50. package/src/runtime/local-provider/asset-installer.mjs +20 -10
  51. package/src/runtime/local-provider/catalog.mjs +4 -7
  52. package/src/runtime/local-provider/data/manifest.json +14 -1
  53. package/src/runtime/local-provider/hardware.mjs +36 -5
  54. package/src/runtime/local-provider/resumable-installations.mjs +2 -2
  55. package/src/runtime/local-provider/server.mjs +1 -1
  56. package/src/runtime/memory/index.mjs +2 -4
  57. package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
  58. package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
  59. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
  60. package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
  61. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
  62. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
  63. package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
  64. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
  65. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  66. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
  67. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
  68. package/src/runtime/memory/lib/memory.mjs +79 -32
  69. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
  70. package/src/runtime/office/com/com-adapter.mjs +3 -0
  71. package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
  72. package/src/runtime/office/pdf/pdf-render.mjs +2 -6
  73. package/src/runtime/office/pdf/pdf-security.mjs +8 -2
  74. package/src/runtime/office/portable/font-provisioner.mjs +28 -0
  75. package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
  76. package/src/runtime/office/portable/portable-soffice.mjs +44 -7
  77. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
  78. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
  79. package/src/runtime/shared/llm/default-context-window.mjs +42 -0
  80. package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
  81. package/src/runtime/shared/llm/model-catalog.mjs +2 -0
  82. package/src/runtime/shared/plugin-paths.mjs +9 -2
  83. package/src/runtime/shared/worker-exec-argv.mjs +25 -0
  84. package/src/runtime/web-search/lib/formatter.mjs +43 -18
  85. package/src/session-runtime/automation-agents.mjs +88 -0
  86. package/src/session-runtime/automation-workflow.mjs +19 -6
  87. package/src/session-runtime/boot/apis.mjs +0 -1
  88. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
  89. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
  90. package/src/session-runtime/model-recency.mjs +10 -2
  91. package/src/session-runtime/model-route/set-route.mjs +7 -1
  92. package/src/session-runtime/resource-api.mjs +8 -2
  93. package/src/session-runtime/resource-plugins-api.mjs +14 -2
  94. package/src/session-runtime/route-state.mjs +6 -2
  95. package/src/session-runtime/schedule-session-run.mjs +7 -1
  96. package/src/session-runtime/services/agent-tool/render.mjs +0 -5
  97. package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
  98. package/src/session-runtime/setup-tool/executor.mjs +7 -13
  99. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
  100. package/src/session-runtime/tool-policy-refresh.mjs +0 -2
  101. package/src/session-runtime/webhook-session-run.mjs +10 -11
  102. package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
  103. package/src/standalone/daemon-agent-control.mjs +8 -1
  104. package/src/standalone/daemon.mjs +4 -0
  105. package/src/tui/dist/index.mjs +3 -2
  106. package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
  107. package/src/tui/session/live-share.mjs +16 -1
  108. package/src/tui/session/notification-plan.mjs +4 -0
  109. package/src/session-runtime/bridge-first-use-gate.mjs +0 -72
@@ -1,8 +1,12 @@
1
1
  // Commands start in the foreground. Only work still running after the
2
- // 10 s coordination budget is promoted to a tracked background task.
2
+ // 15 s coordination budget is promoted to a tracked background task.
3
3
  // Short commands therefore complete in the original tool turn, while longer
4
4
  // work returns partial output plus task_id and finishes by notification.
5
- export const DEFAULT_SHELL_AUTO_BACKGROUND_MS = 10_000;
5
+ // 15 s because a command promoted moments before it ends costs a whole
6
+ // follow-up request on `task wait`. Session logs (2026-09-30..10-05), the 260
7
+ // promoted commands with a recorded duration: 108 had ended within 10 s, 144
8
+ // within 15 s and only 152 within 20 s.
9
+ export const DEFAULT_SHELL_AUTO_BACKGROUND_MS = 15_000;
6
10
 
7
11
  // JS timers (setTimeout) and PS WaitForExit(ms) are 32-bit: a delay above
8
12
  // 2^31-1 wraps to a tiny/negative value and fires immediately. Clamp the
@@ -13,12 +17,17 @@ const TIMER_MAX_MS = 2_147_483_647;
13
17
 
14
18
  // Main-agent blocking budget. A timeout is the command's total deadline, not
15
19
  // permission to hold the conversation open for that whole duration: after
16
- // 10 s a still-running command becomes a tracked background task and
20
+ // 15 s a still-running command becomes a tracked background task and
17
21
  // completion is pushed to the owner. MIXDOG_SHELL_AUTO_BACKGROUND_MS
18
22
  // overrides; an explicit 0 disables. Gated on backgroundOnTimeout so
19
23
  // disabled background tasks remain foreground.
20
- // A per-call wait_ms replaces that default for the call, capped by the
21
- // foreground maximum.
24
+ // A per-call wait_ms extends that default for the call, capped by the
25
+ // foreground maximum; it never shortens it. A shorter window promotes a
26
+ // command that would have returned its result in the starting call and spends
27
+ // a whole follow-up request on `task wait`. Session logs (2026-09-30..10-05,
28
+ // 3062 shell calls): 329 passed a wait_ms under the default (53% of one model
29
+ // family's calls), 256 of those were promoted, and 108 of the 192 with a
30
+ // recorded duration had finished inside the default window (10 s then).
22
31
  function autoBackgroundBudget(backgroundOnTimeout, timeout, waitMs, maxForegroundMs) {
23
32
  const raw = process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
24
33
  const parsed = Number(raw);
@@ -26,7 +35,7 @@ function autoBackgroundBudget(backgroundOnTimeout, timeout, waitMs, maxForegroun
26
35
  raw != null && String(raw).trim() !== '' && Number.isFinite(parsed) && parsed >= 0
27
36
  ? Math.floor(parsed)
28
37
  : DEFAULT_SHELL_AUTO_BACKGROUND_MS;
29
- if (waitMs > 0) defaultMs = Math.min(waitMs, maxForegroundMs);
38
+ if (waitMs > 0) defaultMs = Math.max(defaultMs, Math.min(waitMs, maxForegroundMs));
30
39
  if (!backgroundOnTimeout || defaultMs <= 0) return 0;
31
40
  return timeout > 0 ? Math.min(defaultMs, timeout) : defaultMs;
32
41
  }
@@ -146,7 +146,7 @@ export const BUILTIN_TOOLS = [
146
146
  openWorldHint: true,
147
147
  compressible: true,
148
148
  },
149
- description: `Run programs, builds, tests and computation. Never use shell commands or scripts for work covered by dedicated tools: cat/head/tail→read, ls→list, find→glob, filename lookup→find, grep/rg→grep, code structure→code_graph, file edits/writes (sed/awk/redirection)→edit/apply_patch, Git→git when that tool is on the surface. Tool names are not shell commands. ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'After a 10s foreground window (not a timeout; wait_ms extends it up to 120s for one call, e.g. a test suite), unfinished work continues under task_id; use task wait, not read polling. If no execution slot frees within the admission wait, the command is queued as a background task with a task_id.'}`,
149
+ description: `Run programs, builds, tests and computation. Never use shell commands or scripts for work covered by dedicated tools: cat/head/tail→read, ls→list, find→glob, filename lookup→find, grep/rg→grep, code structure→code_graph, file edits/writes (sed/awk/redirection)→edit/apply_patch, Git→git when that tool is on the surface. Tool names are not shell commands. ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'After a 15s foreground window (not a timeout; wait_ms extends it up to 120s for one call, e.g. a test suite), unfinished work continues under task_id; use task wait, not read polling. If no execution slot frees within the admission wait, the command is queued as a background task with a task_id.'}`,
150
150
  inputSchema: {
151
151
  type: 'object',
152
152
  properties: {
@@ -155,14 +155,14 @@ export const BUILTIN_TOOLS = [
155
155
  type: 'integer',
156
156
  minimum: 0,
157
157
  description:
158
- 'Hard kill deadline in ms (omit or 0 = none), separate from the 10s foreground window; set one for throwaway probes so a slow check fails fast.',
158
+ 'Hard kill deadline in ms (omit or 0 = none), separate from the 15s foreground window; set one for throwaway probes so a slow check fails fast.',
159
159
  },
160
160
  wait_ms: {
161
161
  type: 'integer',
162
- minimum: 1000,
162
+ minimum: 15000,
163
163
  maximum: 120000,
164
164
  description:
165
- 'Foreground wait in ms before unfinished work continues as a background task (default 10000); raise it for a command that should return its result in this call.',
165
+ 'Foreground wait in ms before unfinished work continues as a background task (default 15000); raise it for a command that should return its result in this call.',
166
166
  },
167
167
  },
168
168
  required: ['command'],
@@ -484,6 +484,13 @@ class NativePatchServer {
484
484
  this.#child.stdin.on('error', (err) => {
485
485
  this.failTransport(err);
486
486
  });
487
+ // A spawn that fails outright (an artifact without the execute bit, or one
488
+ // removed between the gate's read and the spawn) reports only through the
489
+ // child's own 'error' event; unheard, it crashed the host instead of
490
+ // failing the handshake closed.
491
+ this.#child.on('error', (err) => {
492
+ this.failTransport(err);
493
+ });
487
494
  this.#child.on('exit', (code, signal) => {
488
495
  this.exited = true;
489
496
  const err = new Error(`native patch server exited code=${code} signal=${signal} stderr=${this.stderr}`);
@@ -98,6 +98,35 @@ async function probeAudioDurationSec(filePath) {
98
98
  }
99
99
  }
100
100
 
101
+ // Leading silence so a clipped onset still decodes as the first word. The
102
+ // bundled ffmpeg carries only aformat/anull/aresample
103
+ // (scripts/build-ffmpeg-runtime.sh), so the pad is spliced into the converted
104
+ // wav here: zero samples in front of its PCM data.
105
+ const ONSET_SILENCE_SECONDS = 0.3;
106
+
107
+ export function prependWavSilence(wavPath, seconds = ONSET_SILENCE_SECONDS) {
108
+ const wav = fs.readFileSync(wavPath);
109
+ let sampleRate = 0;
110
+ let blockAlign = 0;
111
+ for (let offset = 12; offset + 8 <= wav.length; ) {
112
+ const id = wav.toString('ascii', offset, offset + 4);
113
+ const size = wav.readUInt32LE(offset + 4);
114
+ if (id === 'fmt ') {
115
+ sampleRate = wav.readUInt32LE(offset + 12);
116
+ blockAlign = wav.readUInt16LE(offset + 20);
117
+ } else if (id === 'data') {
118
+ const silence = Buffer.alloc(Math.round(sampleRate * seconds) * blockAlign);
119
+ const out = Buffer.concat([wav.subarray(0, offset + 8), silence, wav.subarray(offset + 8)]);
120
+ out.writeUInt32LE(size + silence.length, offset + 4);
121
+ out.writeUInt32LE(out.length - 8, 4);
122
+ fs.writeFileSync(wavPath, out);
123
+ return;
124
+ }
125
+ offset += 8 + size + (size % 2);
126
+ }
127
+ throw new Error(`no PCM data chunk in ${wavPath}`);
128
+ }
129
+
101
130
  // One whisper-ready wav per attachment: a cached conversion is reused while its
102
131
  // file still exists, and parallel callers for the same key share a single
103
132
  // ffmpeg spawn instead of racing two conversions onto the same output path.
@@ -126,9 +155,6 @@ function createVoiceWavCache() {
126
155
  const _ffmpegPromise = runCmd(ffmpegPath, [
127
156
  '-i',
128
157
  audioPath,
129
- // Leading silence so a clipped onset still decodes as the first word.
130
- '-af',
131
- 'adelay=300:all=1',
132
158
  '-ar',
133
159
  String(sampleRate),
134
160
  '-ac',
@@ -137,7 +163,7 @@ function createVoiceWavCache() {
137
163
  String(threadCount),
138
164
  '-y',
139
165
  wavPath,
140
- ]);
166
+ ]).then(() => prependWavSilence(wavPath));
141
167
  ffmpegInflight.set(_ffmpegKey, _ffmpegPromise);
142
168
  try {
143
169
  await _ffmpegPromise;
@@ -118,11 +118,11 @@ const captureProperties = {
118
118
  include_ocr: {
119
119
  type: 'boolean',
120
120
  description:
121
- 'State/SOM automatically use offline Windows OCR when semantic targets are absent; true always runs OCR even when semantic targets exist. OCR shares max_elements. Zoom recognizes the crop itself, which is how enlarged text becomes readable.',
121
+ 'State/SOM automatically use offline OS OCR when semantic targets are absent; true always runs OCR even when semantic targets exist. OCR shares max_elements. Zoom recognizes the crop itself, which is how enlarged text becomes readable.',
122
122
  },
123
123
  ocr_language: {
124
124
  ...ocrLanguage,
125
- description: 'Installed Windows OCR language tag, e.g. ko or en-US.',
125
+ description: 'Installed OS OCR language tag, e.g. ko or en-US.',
126
126
  },
127
127
  // Image encoding (JPEG quality, downscale width) and the OCR word cap are
128
128
  // host defaults: the element budget already bounds OCR, and a frame the
@@ -213,7 +213,7 @@ export const COMPUTER_INPUT_SCHEMA = {
213
213
  ...windowTarget,
214
214
  ocr_language: {
215
215
  ...ocrLanguage,
216
- description: 'Optional Windows OCR language tag to verify, e.g. ko or en-US.',
216
+ description: 'Optional OS OCR language tag to verify, e.g. ko or en-US.',
217
217
  },
218
218
  }),
219
219
  false
@@ -31,6 +31,10 @@ action('ocr_image', nativeRead);
31
31
  action('ocr_status', nativeRead);
32
32
  action('release_session', { retainNativeRefs: true });
33
33
  action('release_cursor_theme', { retainNativeRefs: true });
34
+ // Handing held input, focus and pointer back changes no target's content, so a
35
+ // turn's end keeps the refs of its last observation for a follow-up turn.
36
+ action('release_held_input', { retainNativeRefs: true });
37
+ action('restore_input_state', { retainNativeRefs: true });
34
38
  action('list_apps', { ...hostRead, ...replay, policy: 'list' });
35
39
  action('list_installed_apps', { ...nativeRead, ...replay, policy: 'list' });
36
40
  action('list_history', { ...hostRead, ...replay, policy: 'list' });
@@ -0,0 +1,22 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ // Creates a temp directory and points the env var `name` at it until `cleanup()`.
6
+ export async function makeTempEnvDir(name, prefix) {
7
+ const directory = await mkdtemp(join(tmpdir(), prefix));
8
+ const previous = process.env[name];
9
+ process.env[name] = directory;
10
+ return {
11
+ directory,
12
+ async cleanup() {
13
+ if (previous === undefined) delete process.env[name];
14
+ else process.env[name] = previous;
15
+ await rm(directory, { recursive: true, force: true });
16
+ },
17
+ };
18
+ }
19
+
20
+ export function writeBridgeDiscovery(directory, fields) {
21
+ return writeFile(join(directory, 'computer-bridge.json'), JSON.stringify({ version: 1, ...fields }));
22
+ }
@@ -13,13 +13,20 @@ import {
13
13
  toComputerHostCommand,
14
14
  validateComputerToolArgs,
15
15
  } from './action-schema.mjs';
16
- import { computerResultRecovery, formatComputerToolError } from './error-recovery.mjs';
16
+ import { formatComputerToolError } from './error-recovery.mjs';
17
+ import {
18
+ canonicalComputerResultIsError,
19
+ canonicalComputerResultText,
20
+ computerImageDimensions,
21
+ } from './result-canonical.mjs';
17
22
  import { computerErrorCode } from './error-code.mjs';
18
23
  import { bridgeDiscoveryChanged, readBridgeDiscovery } from '../bridge-discovery.mjs';
19
24
  import { MAX_COMPUTER_REQUEST_BYTES, readComputerBridgeJson, validateComputerReply } from './limits.mjs';
20
25
  import { computerActionHas } from './actions.mjs';
21
26
  import { continuePendingComputerWork, isPendingComputerWork } from './pending-continuation.mjs';
22
27
 
28
+ export { canonicalComputerResultText };
29
+
23
30
  const DISCOVERY_FILE = 'computer-bridge.json';
24
31
  // Desktop UI Automation queries and input dispatch can be slow; sit above the
25
32
  // bridge's own per-action timeouts so its specific error wins over a bare abort.
@@ -48,186 +55,6 @@ const activeComputerExecutions = new Set();
48
55
  const BRIDGE_UNAVAILABLE_MESSAGE =
49
56
  'computer use is unavailable; open the Mixdog desktop app and enable Computer Use in settings';
50
57
 
51
- const ACT_STEP_STATUSES = new Set(['succeeded', 'failed', 'skipped', 'pending', 'uncertain']);
52
-
53
- function canonicalizeActResult(value, args) {
54
- value.completed_actions = value.completed_steps;
55
- value.total_actions = value.total_steps;
56
- const canonicalStep = (row, index) => {
57
- const normalized = { ...row };
58
- normalized.type = args.input?.actions?.[index]?.type || normalized.action;
59
- if (!ACT_STEP_STATUSES.has(normalized.status)) {
60
- normalized.status = normalized.ok === false ? 'failed' : 'succeeded';
61
- }
62
- delete normalized.action;
63
- delete normalized.ok;
64
- return normalized;
65
- };
66
- value.actions = Array.isArray(value.steps) ? value.steps.map(canonicalStep) : value.steps;
67
- // A single-window act reports the same transition on the act and its step.
68
- const transition = JSON.stringify(value.window_transition);
69
- for (const row of Array.isArray(value.actions) ? value.actions : []) {
70
- if (transition && JSON.stringify(row?.window_transition) === transition) delete row.window_transition;
71
- }
72
- delete value.completed_steps;
73
- delete value.total_steps;
74
- delete value.steps;
75
- }
76
-
77
- // Host diagnostics stay in the host's run record; the model acts on none of them.
78
- const HOST_DIAGNOSTIC_FIELDS = ['timings_ms', 'capture_attempts'];
79
-
80
- function dropHostDiagnostics(value) {
81
- if (!value || typeof value !== 'object') return;
82
- if (Array.isArray(value)) {
83
- for (const item of value) dropHostDiagnostics(item);
84
- return;
85
- }
86
- for (const field of HOST_DIAGNOSTIC_FIELDS) delete value[field];
87
- for (const item of Object.values(value)) dropHostDiagnostics(item);
88
- }
89
-
90
- // Elements and OCR text reach the model one line each, the shape browser
91
- // snapshots use, because a field name costs more than the value it labels:
92
- // `#mark [ref] Role "name" value="…" state=… source=… disabled focused
93
- // @x,y,width,height actions`. Flags appear only when they differ from the
94
- // common case the capture description states (an enabled UIA element), and
95
- // the geometry a som frame repeats as x/y/width/height, center and
96
- // screen_bounds collapses into its one bounds box.
97
- const DERIVED_GEOMETRY_FIELDS = new Set([
98
- 'x',
99
- 'y',
100
- 'width',
101
- 'height',
102
- 'center_x',
103
- 'center_y',
104
- 'center',
105
- 'screen_bounds',
106
- ]);
107
-
108
- const quotedText = (text) => JSON.stringify(String(text));
109
- const bareText = (text) => (/^[^\s"]+$/.test(String(text)) ? String(text) : quotedText(text));
110
- const isUnsetField = (field) =>
111
- field === undefined || field === null || field === '' || field === false || (Array.isArray(field) && !field.length);
112
-
113
- function boundsText(box) {
114
- return box.length === 4 && box.every((edge) => Number.isFinite(edge)) ? `@${box.join(',')}` : null;
115
- }
116
-
117
- function extraFieldTexts(rest) {
118
- return Object.entries(rest)
119
- .filter(([key, field]) => !DERIVED_GEOMETRY_FIELDS.has(key) && !isUnsetField(field))
120
- .map(([key, field]) => `${key}=${typeof field === 'string' ? bareText(field) : JSON.stringify(field)}`);
121
- }
122
-
123
- function elementLine(element) {
124
- if (!element || typeof element !== 'object') return element;
125
- const { mark, ref, role, name, value, state, source, enabled, has_keyboard_focus, bounds, actions, ...rest } =
126
- element;
127
- return [
128
- isUnsetField(mark) ? null : `#${mark}`,
129
- isUnsetField(ref) ? null : `[${ref}]`,
130
- bareText(role || 'Unknown'),
131
- quotedText(name ?? ''),
132
- isUnsetField(value) ? null : `value=${quotedText(value)}`,
133
- isUnsetField(state) || state === source ? null : `state=${bareText(state)}`,
134
- isUnsetField(source) || source === 'uia' ? null : `source=${bareText(source)}`,
135
- enabled === false ? 'disabled' : null,
136
- has_keyboard_focus === true ? 'focused' : null,
137
- boundsText(Array.isArray(bounds) ? bounds : [rest.x, rest.y, rest.width, rest.height]),
138
- Array.isArray(actions) && actions.length ? actions.join(',') : null,
139
- ...extraFieldTexts(rest),
140
- ]
141
- .filter(Boolean)
142
- .join(' ');
143
- }
144
-
145
- function ocrTextLine(row) {
146
- if (!row || typeof row !== 'object') return row;
147
- const { mark, text, line, x, y, width, height, ...rest } = row;
148
- return [
149
- isUnsetField(mark) ? null : `#${mark}`,
150
- quotedText(text ?? ''),
151
- boundsText([x, y, width, height]),
152
- isUnsetField(line) ? null : `line=${line}`,
153
- ...extraFieldTexts(rest),
154
- ]
155
- .filter(Boolean)
156
- .join(' ');
157
- }
158
-
159
- function renderObservationLines(frame) {
160
- if (!frame || typeof frame !== 'object') return;
161
- if (Array.isArray(frame.elements)) frame.elements = frame.elements.map(elementLine);
162
- for (const key of ['lines', 'words']) {
163
- if (Array.isArray(frame.ocr?.[key])) frame.ocr[key] = frame.ocr[key].map(ocrTextLine);
164
- }
165
- }
166
-
167
- export function canonicalComputerResultText(text, args) {
168
- if (args.action === 'clipboard' && args.input?.operation === 'read') return text;
169
- let value;
170
- try {
171
- value = JSON.parse(text);
172
- } catch {
173
- return text;
174
- }
175
- if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.action !== 'string') {
176
- return text;
177
- }
178
- value.action = args.action;
179
- if (args.action === 'list') value.kind = args.input?.kind;
180
- if (args.action === 'capture') value.mode = args.input?.mode || 'state';
181
- if (args.action === 'window') value.operation = args.input?.operation;
182
- if (args.action === 'clipboard') value.operation = args.input?.operation;
183
- if (args.action === 'act') canonicalizeActResult(value, args);
184
- if (value.capture_after && value.observation === undefined) {
185
- value.observation = value.capture_after;
186
- delete value.capture_after;
187
- }
188
- dropHostDiagnostics(value);
189
- renderObservationLines(value);
190
- renderObservationLines(value.observation);
191
- if (value.ok === false && value.recovery === undefined) {
192
- const recovery = computerResultRecovery(value, args);
193
- if (recovery) value.recovery = recovery;
194
- }
195
- return JSON.stringify(value);
196
- }
197
-
198
- // A capture payload reports the frame's real pixel size (`width`/`height`,
199
- // nested under `observation` for action replies). Carrying it on the image
200
- // block lets the context estimator bill a screenshot at its true vision cost
201
- // instead of the flat unknown-image allowance. Provider normalizers rebuild
202
- // the wire block from `type`/`source` alone, so these fields never reach an
203
- // API and never shift the cached prefix.
204
- function computerImageDimensions(text) {
205
- let value;
206
- try {
207
- value = JSON.parse(text);
208
- } catch {
209
- return null;
210
- }
211
- for (const frame of [value, value?.observation]) {
212
- const width = Number(frame?.width);
213
- const height = Number(frame?.height);
214
- if (width > 0 && height > 0 && Number.isFinite(width) && Number.isFinite(height)) return { width, height };
215
- }
216
- return null;
217
- }
218
-
219
- function canonicalComputerResultIsError(text, args) {
220
- if (args.action === 'clipboard' && args.input?.operation === 'read') return false;
221
- try {
222
- const value = JSON.parse(text);
223
- return Boolean(
224
- value && typeof value === 'object' && !Array.isArray(value) && value.action === args.action && value.ok === false
225
- );
226
- } catch {
227
- return false;
228
- }
229
- }
230
-
231
58
  /** Sync gate for the session tool surface (featureDisallowedTools). */
232
59
  export function computerBridgeAvailableSync() {
233
60
  return readBridgeDiscovery(DISCOVERY_FILE) !== null;
@@ -541,8 +368,8 @@ async function abortComputerSession(sessionId) {
541
368
  return aborted;
542
369
  }
543
370
 
544
- /** End the visible Computer Use execution at agent-turn settlement without
545
- * dropping the warm worker and observation refs kept for a possible follow-up. */
371
+ /** End the visible Computer Use execution at agent-turn settlement and hand the
372
+ * desktop back, keeping the warm worker and observation refs for a possible follow-up. */
546
373
  export async function endComputerExecution(sessionId) {
547
374
  const id = String(sessionId || '').trim();
548
375
  if (!id || !activeComputerExecutions.has(id)) return false;
@@ -276,7 +276,8 @@ function actionArgumentsError(action, type, label, frameId) {
276
276
  if (hasOwn(action, 'ref') && hasOwn(action, 'element')) return `${label} accepts only one of ref or element`;
277
277
  return typeof action.value === 'string' ? null : `${label} requires value`;
278
278
  }
279
- if (type === 'key' || type === 'key_down' || type === 'key_up') return typeof action.keys === 'string' ? null : `${label} requires keys`;
279
+ if (type === 'key' || type === 'key_down' || type === 'key_up')
280
+ return typeof action.keys === 'string' ? null : `${label} requires keys`;
280
281
  if (type === 'wait') {
281
282
  const { duration } = action;
282
283
  const valid = typeof duration === 'number' && Number.isFinite(duration) && duration >= 0 && duration <= 5;
@@ -21,11 +21,7 @@ function targetLabel(args) {
21
21
  const LIST_WINDOW_CODES = new Set(['ambiguous_window_target', 'window_stale', 'window_target_not_found']);
22
22
  const STALE_TARGET_CODES = new Set(['stale_frame', 'stale_target', 'target_mismatch']);
23
23
  const USER_YIELD_CODES = new Set(['computer_user_control_active', 'computer_user_takeover', 'user_input_active']);
24
- const DIAGNOSE_CODES = new Set([
25
- 'input_observation_unavailable',
26
- 'input_recovery_unconfirmed',
27
- 'computer_cursor_unavailable',
28
- ]);
24
+ const DIAGNOSE_CODES = new Set(['input_observation_unavailable', 'computer_cursor_unavailable']);
29
25
  const CLEANUP_CODES = new Set([
30
26
  'computer_cleanup_pending',
31
27
  'computer_abort_cleanup_unconfirmed',
@@ -47,6 +43,40 @@ function recaptureLeaseGuidance(code, target) {
47
43
  return `The target lease is now available. Capture ${target} again before issuing any input.`;
48
44
  }
49
45
 
46
+ function timeoutRecovery(code, args, target) {
47
+ if (READ_ONLY_ACTIONS.has(String(args?.action || ''))) {
48
+ return {
49
+ code,
50
+ next: 'capture',
51
+ guidance: `The read exceeded its bounded budget and sent no input, so no cleanup or user-control guard applies. Capture ${target} with mode="state" or "som": pixels and OCR stay usable while that window's accessibility provider is slow.`,
52
+ };
53
+ }
54
+ return {
55
+ code,
56
+ next: 'diagnose',
57
+ guidance: `The command may have executed before timing out. Do not repeat it or switch delivery modes. Diagnose the host first; cleanup and user-control guards must clear through verified recovery. Then capture ${target} and inspect the effect before issuing any new input.`,
58
+ };
59
+ }
60
+
61
+ function foregroundRecovery(code, target) {
62
+ if (code === 'foreground_changed') {
63
+ return {
64
+ code,
65
+ next: 'user',
66
+ guidance:
67
+ 'Focus changed during dispatch. Do not pull it back or retry input automatically. Check user control, then obtain fresh state when control is available.',
68
+ };
69
+ }
70
+ if (code === 'foreground_unavailable') {
71
+ return {
72
+ code,
73
+ next: 'user',
74
+ guidance: `The system did not grant foreground focus (macOS refuses it while another app is active). Ask the user to activate ${target}, or use a foreground click on the window, which activates it; then capture fresh state. Do not substitute background input or repeat the failed gesture.`,
75
+ };
76
+ }
77
+ return undefined;
78
+ }
79
+
50
80
  function recoveryForCode(code, args) {
51
81
  const target = targetLabel(args);
52
82
  if (code === 'computer_background_cleanup_unconfirmed') {
@@ -82,6 +112,14 @@ function recoveryForCode(code, args) {
82
112
  "Computer Use yielded to the user. Call wait_for_user for bounded waiting. The user's Resume on the overlay continues the pause, and ordinary physical input may also resume after the host-configured quiet interval (default 5 seconds); explicit stops and uncertain cleanup/observation require the user. Stop cancels the task. Timeout does not authorize input. After resumed, capture fresh state; never replay interrupted input.",
83
113
  };
84
114
  }
115
+ if (code === 'input_recovery_unconfirmed') {
116
+ return {
117
+ code,
118
+ next: 'wait_for_user',
119
+ guidance:
120
+ 'The desktop state after the action could not be verified, so Computer Use stopped the session and handed the desktop to the user. Do not repeat the mutation. Call wait_for_user: the host resumes by itself once the desktop has been quiet for its interval, or when the user presses Resume. After resumed, capture fresh state and inspect the effect before any new input.',
121
+ };
122
+ }
85
123
  if (DIAGNOSE_CODES.has(code)) {
86
124
  return {
87
125
  code,
@@ -105,20 +143,7 @@ function recoveryForCode(code, args) {
105
143
  guidance: `No input was sent. Foreground delivery needs an observation taken while the user's own input was idle. Let their input settle and capture ${target} again, or send this action through background delivery when it supports one.`,
106
144
  };
107
145
  }
108
- if (code === 'computer_command_timeout') {
109
- if (READ_ONLY_ACTIONS.has(String(args?.action || ''))) {
110
- return {
111
- code,
112
- next: 'capture',
113
- guidance: `The read exceeded its bounded budget and sent no input, so no cleanup or user-control guard applies. Capture ${target} with mode="state" or "som": pixels and OCR stay usable while that window's accessibility provider is slow.`,
114
- };
115
- }
116
- return {
117
- code,
118
- next: 'diagnose',
119
- guidance: `The command may have executed before timing out. Do not repeat it or switch delivery modes. Diagnose the host first; cleanup and user-control guards must clear through verified recovery. Then capture ${target} and inspect the effect before issuing any new input.`,
120
- };
121
- }
146
+ if (code === 'computer_command_timeout') return timeoutRecovery(code, args, target);
122
147
  if (code.startsWith('menu_')) {
123
148
  return {
124
149
  code,
@@ -126,21 +151,8 @@ function recoveryForCode(code, args) {
126
151
  guidance: `Capture ${target} again; empty accessibility automatically uses OCR. Use a fresh OCR mark or frame point and do not retry the same menu path unchanged.`,
127
152
  };
128
153
  }
129
- if (code === 'foreground_changed') {
130
- return {
131
- code,
132
- next: 'user',
133
- guidance:
134
- 'Focus changed during dispatch. Do not pull it back or retry input automatically. Check user control, then obtain fresh state when control is available.',
135
- };
136
- }
137
- if (code === 'foreground_unavailable') {
138
- return {
139
- code,
140
- next: 'user',
141
- guidance: `Windows did not grant foreground focus. Ask the user to activate ${target}, then capture fresh state. Do not substitute background input or repeat the failed gesture.`,
142
- };
143
- }
154
+ const foreground = foregroundRecovery(code, target);
155
+ if (foreground) return foreground;
144
156
  if (code.startsWith('background_')) {
145
157
  return {
146
158
  code,