neoagent 3.2.1-beta.1 → 3.2.1-beta.3

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 (144) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +358 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +20 -45
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +21 -33
  52. package/server/services/ai/providers/githubCopilot.js +41 -20
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +19 -27
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +111 -84
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/network/http.js +210 -0
  116. package/server/services/network/safe_request.js +307 -0
  117. package/server/services/runtime/backends/local-vm.js +214 -66
  118. package/server/services/runtime/manager.js +17 -12
  119. package/server/services/social_reach/channels/github.js +10 -4
  120. package/server/services/social_reach/channels/reddit.js +4 -4
  121. package/server/services/social_reach/channels/rss.js +2 -2
  122. package/server/services/social_reach/channels/social_video.js +12 -7
  123. package/server/services/social_reach/channels/v2ex.js +21 -8
  124. package/server/services/social_reach/channels/x.js +2 -2
  125. package/server/services/social_reach/channels/xueqiu.js +5 -5
  126. package/server/services/social_reach/service.js +9 -6
  127. package/server/services/social_reach/utils.js +65 -14
  128. package/server/services/social_video/service.js +160 -50
  129. package/server/services/tasks/integration_runtime.js +18 -8
  130. package/server/services/tasks/runtime.js +39 -4
  131. package/server/services/voice/agentBridge.js +17 -4
  132. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  133. package/server/services/voice/liveSession.js +31 -0
  134. package/server/services/voice/openaiSpeech.js +33 -8
  135. package/server/services/voice/providers.js +233 -151
  136. package/server/services/voice/runtimeManager.js +118 -20
  137. package/server/services/voice/turnRunner.js +6 -0
  138. package/server/services/wearable/firmware_manifest.js +51 -13
  139. package/server/services/wearable/service.js +1 -0
  140. package/server/utils/abort.js +96 -0
  141. package/server/utils/cloud-security.js +110 -3
  142. package/server/utils/files.js +31 -0
  143. package/server/utils/image_payload.js +95 -0
  144. package/server/utils/retry.js +107 -0
@@ -7,6 +7,22 @@ const FORCE_KILL_GRACE_MS = 5000;
7
7
  const MAX_STDOUT_CHARS = 50000;
8
8
  const MAX_STDERR_CHARS = 10000;
9
9
 
10
+ function abortedResult(command, cwd, startedAt = Date.now()) {
11
+ return {
12
+ exitCode: null,
13
+ stdout: '',
14
+ stderr: 'Command aborted before it started.',
15
+ killed: true,
16
+ timedOut: false,
17
+ aborted: true,
18
+ signal: null,
19
+ durationMs: Date.now() - startedAt,
20
+ pid: null,
21
+ command,
22
+ cwd,
23
+ };
24
+ }
25
+
10
26
  function resolveDefaultShell() {
11
27
  const candidates = [
12
28
  process.env.SHELL,
@@ -63,6 +79,22 @@ function terminateProcess(proc, signal = 'SIGTERM') {
63
79
  proc.kill?.(signal) || proc.kill?.();
64
80
  }
65
81
 
82
+ function terminateWithEscalation(proc, isActive) {
83
+ terminateProcess(proc, 'SIGTERM');
84
+ if (proc.__neoagentForceKillTimer) return;
85
+ proc.__neoagentForceKillTimer = setTimeout(() => {
86
+ if (isActive()) terminateProcess(proc, 'SIGKILL');
87
+ }, FORCE_KILL_GRACE_MS);
88
+ proc.__neoagentForceKillTimer.unref?.();
89
+ }
90
+
91
+ function clearForceKill(proc) {
92
+ if (proc?.__neoagentForceKillTimer) {
93
+ clearTimeout(proc.__neoagentForceKillTimer);
94
+ proc.__neoagentForceKillTimer = null;
95
+ }
96
+ }
97
+
66
98
  function shellSupportsPipefail(shellPath) {
67
99
  const normalized = String(shellPath || '').trim().toLowerCase();
68
100
  return /(?:^|\/)(?:bash|zsh|ksh|mksh|yash)$/.test(normalized);
@@ -111,6 +143,7 @@ class CLIExecutor {
111
143
  const cwd = options.cwd || process.env.HOME;
112
144
  const timeout = clampTimeout(options.timeout, DEFAULT_TIMEOUT_MS);
113
145
  const stdinInput = options.stdinInput;
146
+ if (options.signal?.aborted) return abortedResult(command, cwd);
114
147
 
115
148
  return new Promise((resolve) => {
116
149
  let stdout = '';
@@ -131,6 +164,13 @@ class CLIExecutor {
131
164
  const pid = proc.pid;
132
165
  this.activeProcesses.set(pid, proc);
133
166
  options.onSpawn?.(pid);
167
+ const onAbort = () => {
168
+ killed = true;
169
+ proc.__neoagentKilled = true;
170
+ proc.__neoagentKillReason = 'aborted';
171
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
172
+ };
173
+ options.signal?.addEventListener('abort', onAbort, { once: true });
134
174
 
135
175
  proc.stdout.on('data', (data) => {
136
176
  stdout += data.toString();
@@ -156,14 +196,14 @@ class CLIExecutor {
156
196
  timedOut = true;
157
197
  proc.__neoagentKilled = true;
158
198
  proc.__neoagentKillReason = 'timeout';
159
- terminateProcess(proc, 'SIGTERM');
160
- setTimeout(() => {
161
- if (!proc.killed) terminateProcess(proc, 'SIGKILL');
162
- }, FORCE_KILL_GRACE_MS);
199
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
163
200
  }, timeout);
201
+ timer.unref?.();
164
202
 
165
203
  proc.on('close', (code, signal) => {
166
204
  clearTimeout(timer);
205
+ clearForceKill(proc);
206
+ options.signal?.removeEventListener('abort', onAbort);
167
207
  this.activeProcesses.delete(pid);
168
208
  const durationMs = Date.now() - startedAt;
169
209
 
@@ -173,6 +213,7 @@ class CLIExecutor {
173
213
  stderr: truncateOutput(stderr.trim(), MAX_STDERR_CHARS),
174
214
  killed: killed || proc.__neoagentKilled === true,
175
215
  timedOut: timedOut || proc.__neoagentKillReason === 'timeout',
216
+ aborted: proc.__neoagentKillReason === 'aborted',
176
217
  signal: signal || null,
177
218
  durationMs,
178
219
  pid,
@@ -183,6 +224,8 @@ class CLIExecutor {
183
224
 
184
225
  proc.on('error', (err) => {
185
226
  clearTimeout(timer);
227
+ clearForceKill(proc);
228
+ options.signal?.removeEventListener('abort', onAbort);
186
229
  this.activeProcesses.delete(pid);
187
230
  resolve({
188
231
  exitCode: -1,
@@ -204,6 +247,7 @@ class CLIExecutor {
204
247
  async executeInteractive(command, inputs = [], options = {}) {
205
248
  const cwd = options.cwd || process.env.HOME;
206
249
  const timeout = clampTimeout(options.timeout, DEFAULT_INTERACTIVE_TIMEOUT_MS);
250
+ if (options.signal?.aborted) return abortedResult(command, cwd);
207
251
 
208
252
  return new Promise((resolve) => {
209
253
  let output = '';
@@ -231,6 +275,13 @@ class CLIExecutor {
231
275
  const pid = proc.pid;
232
276
  this.activeProcesses.set(pid, proc);
233
277
  options.onSpawn?.(pid);
278
+ const onAbort = () => {
279
+ killed = true;
280
+ proc.__neoagentKilled = true;
281
+ proc.__neoagentKillReason = 'aborted';
282
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
283
+ };
284
+ options.signal?.addEventListener('abort', onAbort, { once: true });
234
285
 
235
286
  proc.onData((data) => {
236
287
  output += data;
@@ -256,11 +307,14 @@ class CLIExecutor {
256
307
  timedOut = true;
257
308
  proc.__neoagentKilled = true;
258
309
  proc.__neoagentKillReason = 'timeout';
259
- proc.kill();
310
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
260
311
  }, timeout);
312
+ timer.unref?.();
261
313
 
262
314
  proc.onExit(({ exitCode, signal }) => {
263
315
  clearTimeout(timer);
316
+ clearForceKill(proc);
317
+ options.signal?.removeEventListener('abort', onAbort);
264
318
  this.activeProcesses.delete(pid);
265
319
 
266
320
  const cleanOutput = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').trim();
@@ -270,6 +324,7 @@ class CLIExecutor {
270
324
  stderr: '',
271
325
  killed: killed || proc.__neoagentKilled === true,
272
326
  timedOut: timedOut || proc.__neoagentKillReason === 'timeout',
327
+ aborted: proc.__neoagentKillReason === 'aborted',
273
328
  signal: typeof signal === 'number' ? String(signal) : signal || null,
274
329
  durationMs: Date.now() - startedAt,
275
330
  pid,
@@ -286,8 +341,7 @@ class CLIExecutor {
286
341
  if (proc) {
287
342
  proc.__neoagentKilled = true;
288
343
  proc.__neoagentKillReason = reason;
289
- terminateProcess(proc, 'SIGTERM');
290
- this.activeProcesses.delete(pid);
344
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
291
345
  return true;
292
346
  }
293
347
  return false;
@@ -301,9 +355,8 @@ class CLIExecutor {
301
355
  for (const [pid, proc] of this.activeProcesses) {
302
356
  proc.__neoagentKilled = true;
303
357
  proc.__neoagentKillReason = reason;
304
- terminateProcess(proc, 'SIGTERM');
358
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
305
359
  }
306
- this.activeProcesses.clear();
307
360
  }
308
361
  }
309
362
 
@@ -1,3 +1,5 @@
1
+ 'use strict';
2
+
1
3
  const { WebSocketServer } = require('ws');
2
4
  const {
3
5
  DESKTOP_COMPANION_WS_PATH,
@@ -15,6 +17,9 @@ const {
15
17
  const UPGRADE_RATE_LIMIT_WINDOW_MS = 60 * 1000;
16
18
  const UPGRADE_RATE_LIMIT_MAX_ATTEMPTS = 30;
17
19
  const UPGRADE_RATE_LIMIT_ENTRY_TTL_MS = 10 * 60 * 1000;
20
+ const UPGRADE_AUTH_TIMEOUT_MS = 5000;
21
+ const HELLO_TIMEOUT_MS = 5000;
22
+ const MAX_HELLO_BYTES = 64 * 1024;
18
23
 
19
24
  function rejectUpgrade(socket, statusCode, message) {
20
25
  try {
@@ -88,6 +93,8 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
88
93
  });
89
94
  const upgradeAttempts = new Map();
90
95
  const upgradeThrottleObserver = createUpgradeThrottleObserver();
96
+ let closing = false;
97
+ let closePromise = null;
91
98
 
92
99
  if (app?.locals) {
93
100
  app.locals.getDesktopGatewayRateLimitSnapshot = () => upgradeThrottleObserver.snapshot();
@@ -115,7 +122,7 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
115
122
  return true;
116
123
  }
117
124
 
118
- httpServer.on('upgrade', (req, socket, head) => {
125
+ const handleUpgrade = (req, socket, head) => {
119
126
  let url;
120
127
  try {
121
128
  url = new URL(req.url, 'http://localhost');
@@ -125,6 +132,10 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
125
132
  if (url.pathname !== DESKTOP_COMPANION_WS_PATH) {
126
133
  return;
127
134
  }
135
+ if (closing) {
136
+ rejectUpgrade(socket, 503, 'Service Unavailable');
137
+ return;
138
+ }
128
139
 
129
140
  const remoteAddress = remoteAddressFromRequest(req);
130
141
  if (!allowUpgradeAttempt(remoteAddress)) {
@@ -133,7 +144,16 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
133
144
  return;
134
145
  }
135
146
 
147
+ const authTimer = setTimeout(() => {
148
+ rejectUpgrade(socket, 504, 'Gateway Timeout');
149
+ }, UPGRADE_AUTH_TIMEOUT_MS);
150
+ authTimer.unref?.();
136
151
  sessionMiddleware(req, {}, (err) => {
152
+ clearTimeout(authTimer);
153
+ if (closing || socket.destroyed) {
154
+ if (!socket.destroyed) rejectUpgrade(socket, 503, 'Service Unavailable');
155
+ return;
156
+ }
137
157
  if (err) {
138
158
  rejectUpgrade(socket, 500, 'Session Error');
139
159
  return;
@@ -155,11 +175,18 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
155
175
  if (!initialized) {
156
176
  try { ws.close(1008, 'Desktop companion hello timed out'); } catch {}
157
177
  }
158
- }, 5000);
178
+ }, HELLO_TIMEOUT_MS);
179
+ helloTimer.unref?.();
180
+ const clearHelloTimer = () => clearTimeout(helloTimer);
181
+ ws.once('close', clearHelloTimer);
182
+ ws.once('error', clearHelloTimer);
159
183
 
160
184
  ws.once('message', (data) => {
161
185
  clearTimeout(helloTimer);
162
186
  try {
187
+ if (Buffer.byteLength(data) > MAX_HELLO_BYTES) {
188
+ throw Object.assign(new Error('Desktop companion hello is too large.'), { status: 413 });
189
+ }
163
190
  const message = parseDesktopMessage(data);
164
191
  if (!isDesktopCompanionHello(message)) {
165
192
  throw Object.assign(new Error('Desktop companion hello is required.'), { status: 400 });
@@ -227,11 +254,21 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
227
254
  });
228
255
  });
229
256
  });
230
- });
257
+ };
258
+ httpServer.on('upgrade', handleUpgrade);
231
259
 
232
260
  if (app?.locals) {
233
261
  app.locals.desktopCompanionGateway = {
234
- close: () => new Promise((resolve) => wss.close(() => resolve())),
262
+ close: () => {
263
+ if (closePromise) return closePromise;
264
+ closing = true;
265
+ httpServer.removeListener('upgrade', handleUpgrade);
266
+ for (const client of wss.clients) {
267
+ try { client.terminate(); } catch {}
268
+ }
269
+ closePromise = new Promise((resolve) => wss.close(() => resolve()));
270
+ return closePromise;
271
+ },
235
272
  };
236
273
  }
237
274
 
@@ -1,3 +1,5 @@
1
+ 'use strict';
2
+
1
3
  const DESKTOP_COMPANION_WS_PATH = '/api/desktop/ws';
2
4
 
3
5
  const DESKTOP_COMMANDS = Object.freeze({
@@ -17,6 +19,7 @@ const DESKTOP_COMMANDS = Object.freeze({
17
19
  GET_TREE: 'getTree',
18
20
  PAUSE_CONTROL: 'pauseControl',
19
21
  EXECUTE_COMMAND: 'executeCommand',
22
+ CANCEL_COMMAND: 'cancelCommand',
20
23
  PING: 'ping',
21
24
  MOUSE_MOVE: 'mouseMove',
22
25
  });
@@ -1,28 +1,18 @@
1
+ 'use strict';
2
+
1
3
  const fs = require('fs');
2
4
  const path = require('path');
3
5
  const { DATA_DIR } = require('../../../runtime/paths');
6
+ const { writeBufferAtomic } = require('../../utils/files');
7
+ const { decodeBase64Image } = require('../../utils/image_payload');
4
8
  const {
5
9
  DESKTOP_COMMANDS,
6
- DesktopCompanionSelectionError,
7
10
  DesktopCompanionUnavailableError,
8
11
  } = require('./protocol');
9
12
 
10
13
  const SCREENSHOTS_DIR = path.join(DATA_DIR, 'screenshots');
11
14
  if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
12
15
 
13
- function extractBase64Image(value) {
14
- const text = String(value || '');
15
- if (!text) return null;
16
- const match = text.match(/^data:image\/(?:png|jpeg|jpg);base64,(.+)$/i);
17
- return match ? match[1] : text;
18
- }
19
-
20
- function guessExtension(result = {}) {
21
- const mime = String(result.contentType || result.mimeType || 'image/png').toLowerCase();
22
- if (mime.includes('jpeg') || mime.includes('jpg')) return 'jpg';
23
- return 'png';
24
- }
25
-
26
16
  class DesktopProvider {
27
17
  constructor(options = {}) {
28
18
  this.registry = options.registry;
@@ -36,24 +26,21 @@ class DesktopProvider {
36
26
  }
37
27
  }
38
28
 
39
- _writeScreenshotArtifact(base64, result = {}) {
40
- const buffer = Buffer.from(base64, 'base64');
41
- const extension = guessExtension(result);
42
- const contentType = result.contentType || (extension === 'jpg' ? 'image/jpeg' : 'image/png');
29
+ async _writeScreenshotArtifact(image, result = {}, options = {}) {
43
30
  if (this.artifactStore && this.userId != null) {
44
- const artifact = this.artifactStore.allocateFile(this.userId, {
31
+ const artifact = await this.artifactStore.createBufferArtifact(this.userId, {
45
32
  kind: 'desktop-screenshot',
46
33
  backend: 'desktop-companion',
47
- extension,
48
- contentType,
34
+ extension: image.extension,
35
+ contentType: image.contentType,
49
36
  filenameBase: 'desktop-companion-screenshot',
37
+ content: image.buffer,
38
+ signal: options.signal,
50
39
  metadata: {
51
40
  deviceId: result.device?.deviceId || null,
52
41
  displayId: result.displayId || result.device?.activeDisplayId || null,
53
42
  },
54
43
  });
55
- fs.writeFileSync(artifact.storagePath, buffer);
56
- this.artifactStore.finalizeFile(artifact.artifactId, artifact.storagePath);
57
44
  return {
58
45
  screenshotPath: artifact.url,
59
46
  artifactId: artifact.artifactId,
@@ -62,9 +49,9 @@ class DesktopProvider {
62
49
  };
63
50
  }
64
51
 
65
- const filename = `desktop_${Date.now()}_${Math.random().toString(16).slice(2)}.${extension}`;
52
+ const filename = `desktop_${Date.now()}_${Math.random().toString(16).slice(2)}.${image.extension}`;
66
53
  const fullPath = path.join(SCREENSHOTS_DIR, filename);
67
- fs.writeFileSync(fullPath, buffer);
54
+ await writeBufferAtomic(fullPath, image.buffer, { signal: options.signal });
68
55
  return {
69
56
  screenshotPath: `/screenshots/${filename}`,
70
57
  artifactId: null,
@@ -73,13 +60,14 @@ class DesktopProvider {
73
60
  };
74
61
  }
75
62
 
76
- _materialize(result) {
63
+ async _materialize(result, options = {}) {
77
64
  if (!result || typeof result !== 'object') return result;
78
65
  const raw = result.screenshotDataUrl || result.screenshotData || result.screenshotBase64;
79
66
  if (!raw) return result;
80
- const base64 = extractBase64Image(raw);
81
- if (!base64) return result;
82
- const screenshot = this._writeScreenshotArtifact(base64, result);
67
+ const image = decodeBase64Image(raw, {
68
+ allowedTypes: ['image/png', 'image/jpeg'],
69
+ });
70
+ const screenshot = await this._writeScreenshotArtifact(image, result, options);
83
71
  const next = { ...result, ...screenshot };
84
72
  delete next.screenshotDataUrl;
85
73
  delete next.screenshotData;
@@ -89,16 +77,19 @@ class DesktopProvider {
89
77
 
90
78
  async _dispatch(command, payload = {}, options = {}) {
91
79
  this._assertReady();
92
- try {
93
- return this._materialize(
94
- await this.registry.dispatch(this.userId, payload.deviceId || null, command, payload, options),
95
- );
96
- } catch (error) {
97
- if (error instanceof DesktopCompanionSelectionError || error instanceof DesktopCompanionUnavailableError) {
98
- throw error;
99
- }
100
- throw error;
101
- }
80
+ const safePayload = { ...(payload || {}) };
81
+ const signal = options.signal || safePayload.signal || null;
82
+ const timeoutMs = options.timeoutMs ?? safePayload.timeoutMs;
83
+ delete safePayload.signal;
84
+ delete safePayload.timeoutMs;
85
+ const result = await this.registry.dispatch(
86
+ this.userId,
87
+ safePayload.deviceId || null,
88
+ command,
89
+ safePayload,
90
+ { ...options, signal, timeoutMs },
91
+ );
92
+ return this._materialize(result, { signal });
102
93
  }
103
94
 
104
95
  getStatus() {
@@ -121,9 +112,9 @@ class DesktopProvider {
121
112
  return this.registry.revoke(this.userId, deviceId);
122
113
  }
123
114
 
124
- pauseDevice(deviceId, paused = true) {
115
+ pauseDevice(deviceId, paused = true, options = {}) {
125
116
  this._assertReady();
126
- return this.registry.pause(this.userId, deviceId, paused);
117
+ return this.registry.pause(this.userId, deviceId, paused, options);
127
118
  }
128
119
 
129
120
  screenshot(options = {}) {
@@ -137,7 +128,7 @@ class DesktopProvider {
137
128
 
138
129
  stopStream(options = {}) {
139
130
  this._assertReady();
140
- return this.registry.stopStream(this.userId, options.deviceId || null);
131
+ return this.registry.stopStream(this.userId, options.deviceId || null, options);
141
132
  }
142
133
 
143
134
  observe(options = {}) {
@@ -192,6 +183,12 @@ class DesktopProvider {
192
183
  stdin_input: options.stdinInput || null,
193
184
  pty: options.pty === true,
194
185
  inputs: options.inputs || [],
186
+ signal: options.signal,
187
+ }, {
188
+ signal: options.signal,
189
+ timeoutMs: Number(options.timeout || 0) > 0
190
+ ? Number(options.timeout) + 10_000
191
+ : undefined,
195
192
  });
196
193
  }
197
194
  }