mixdog 1.0.8 → 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.
- package/package.json +2 -1
- package/scripts/lib/run-node-tests.mjs +13 -0
- package/scripts/session-efficiency-diag.mjs +283 -0
- package/src/defaults/skills/computer-use/SKILL.md +3 -3
- package/src/defaults/skills/local-provider/SKILL.md +2 -1
- package/src/defaults/skills/setup/SKILL.md +4 -4
- package/src/defaults/skills/setup/references/actions.md +1 -3
- package/src/defaults/skills/setup/references/surfaces.md +2 -3
- package/src/headless-exec.mjs +1 -0
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
- package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +7 -0
- package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
- package/src/runtime/computer-bridge/action-schema.mjs +3 -3
- package/src/runtime/computer-bridge/actions.mjs +4 -0
- package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
- package/src/runtime/computer-bridge/client.mjs +10 -183
- package/src/runtime/computer-bridge/core-actions.mjs +2 -1
- package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
- package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
- package/src/runtime/local-provider/asset-installer.mjs +20 -10
- package/src/runtime/local-provider/catalog.mjs +4 -7
- package/src/runtime/local-provider/data/manifest.json +14 -1
- package/src/runtime/local-provider/hardware.mjs +36 -5
- package/src/runtime/local-provider/resumable-installations.mjs +2 -2
- package/src/runtime/local-provider/server.mjs +1 -1
- package/src/runtime/memory/index.mjs +2 -4
- package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
- package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
- package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
- package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
- package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
- package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
- package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
- package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
- package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
- package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
- package/src/runtime/memory/lib/memory.mjs +79 -32
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
- package/src/runtime/office/com/com-adapter.mjs +3 -0
- package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
- package/src/runtime/office/pdf/pdf-render.mjs +2 -6
- package/src/runtime/office/pdf/pdf-security.mjs +8 -2
- package/src/runtime/office/portable/font-provisioner.mjs +28 -0
- package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
- package/src/runtime/office/portable/portable-soffice.mjs +44 -7
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
- package/src/runtime/shared/llm/default-context-window.mjs +42 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
- package/src/runtime/shared/llm/model-catalog.mjs +2 -0
- package/src/runtime/shared/plugin-paths.mjs +9 -2
- package/src/runtime/shared/worker-exec-argv.mjs +25 -0
- package/src/runtime/web-search/lib/formatter.mjs +43 -18
- package/src/session-runtime/automation-agents.mjs +88 -0
- package/src/session-runtime/automation-workflow.mjs +19 -6
- package/src/session-runtime/boot/apis.mjs +0 -1
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
- package/src/session-runtime/model-recency.mjs +10 -2
- package/src/session-runtime/model-route/set-route.mjs +7 -1
- package/src/session-runtime/resource-api.mjs +8 -2
- package/src/session-runtime/resource-plugins-api.mjs +14 -2
- package/src/session-runtime/route-state.mjs +6 -2
- package/src/session-runtime/schedule-session-run.mjs +7 -1
- package/src/session-runtime/services/agent-tool/render.mjs +0 -5
- package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
- package/src/session-runtime/setup-tool/executor.mjs +7 -13
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
- package/src/session-runtime/tool-policy-refresh.mjs +0 -2
- package/src/session-runtime/webhook-session-run.mjs +10 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
- package/src/standalone/daemon-agent-control.mjs +8 -1
- package/src/standalone/daemon.mjs +4 -0
- package/src/tui/dist/index.mjs +3 -2
- package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
- package/src/tui/session/live-share.mjs +16 -1
- package/src/tui/session/notification-plan.mjs +4 -0
- 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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
162
|
+
minimum: 15000,
|
|
163
163
|
maximum: 120000,
|
|
164
164
|
description:
|
|
165
|
-
'Foreground wait in ms before unfinished work continues as a background task (default
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
545
|
-
*
|
|
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')
|
|
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
|
-
|
|
130
|
-
|
|
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,
|