@ycheung5/pi-computer-use 0.0.0-stage → 0.1.1

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 (95) hide show
  1. package/GLOSSARY.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -2
  4. package/assets/accessibility.cs +208 -0
  5. package/assets/browser-indicators.cs +37 -0
  6. package/assets/capture.cs +215 -0
  7. package/assets/clipboard.cs +197 -0
  8. package/assets/controls.cs +249 -0
  9. package/assets/indicator-preview.png +0 -0
  10. package/assets/indicators.cs +252 -0
  11. package/assets/input-timing.cs +42 -0
  12. package/assets/node-LICENSE.txt +2732 -0
  13. package/assets/ocr.cs +86 -0
  14. package/assets/ownership.cs +59 -0
  15. package/assets/pointer.cs +241 -0
  16. package/assets/puppeteer-LICENSE.txt +202 -0
  17. package/assets/readme-banner.svg +33 -0
  18. package/assets/stop.cs +197 -0
  19. package/assets/text.cs +279 -0
  20. package/assets/wgc.cs +167 -0
  21. package/assets/window-management.cs +217 -0
  22. package/assets/window-marker.cs +23 -0
  23. package/assets/window-references.cs +80 -0
  24. package/assets/windows.cs +127 -0
  25. package/dist/accessibility.d.ts +29 -0
  26. package/dist/accessibility.js +258 -0
  27. package/dist/browser-actions.d.ts +22 -0
  28. package/dist/browser-actions.js +246 -0
  29. package/dist/browser-indicators.d.ts +23 -0
  30. package/dist/browser-indicators.js +69 -0
  31. package/dist/browser-profiles.d.ts +10 -0
  32. package/dist/browser-profiles.js +174 -0
  33. package/dist/browser.d.ts +11 -0
  34. package/dist/browser.js +2071 -0
  35. package/dist/capture.d.ts +74 -0
  36. package/dist/capture.js +199 -0
  37. package/dist/client.d.ts +39 -0
  38. package/dist/client.js +384 -0
  39. package/dist/desktop.d.ts +20 -0
  40. package/dist/desktop.js +42 -0
  41. package/dist/extension.d.ts +4 -0
  42. package/dist/extension.js +409 -0
  43. package/dist/input-timing.d.ts +24 -0
  44. package/dist/input-timing.js +70 -0
  45. package/dist/launcher.d.ts +6 -0
  46. package/dist/launcher.js +85 -0
  47. package/dist/native.d.ts +11 -0
  48. package/dist/native.js +146 -0
  49. package/dist/ocr.d.ts +52 -0
  50. package/dist/ocr.js +246 -0
  51. package/dist/ownership.d.ts +4 -0
  52. package/dist/ownership.js +24 -0
  53. package/dist/pointer.d.ts +128 -0
  54. package/dist/pointer.js +198 -0
  55. package/dist/protocol.d.ts +15 -0
  56. package/dist/protocol.js +1 -0
  57. package/dist/runtime.d.ts +8 -0
  58. package/dist/runtime.js +72 -0
  59. package/dist/settings.d.ts +85 -0
  60. package/dist/settings.js +449 -0
  61. package/dist/stop.d.ts +24 -0
  62. package/dist/stop.js +156 -0
  63. package/dist/text.d.ts +120 -0
  64. package/dist/text.js +212 -0
  65. package/dist/waits.d.ts +19 -0
  66. package/dist/waits.js +262 -0
  67. package/dist/window-management.d.ts +25 -0
  68. package/dist/window-management.js +167 -0
  69. package/dist/worker.d.ts +1 -0
  70. package/dist/worker.js +553 -0
  71. package/docs/README.md +55 -0
  72. package/docs/adr/0001-preserve-reference-behavior.md +9 -0
  73. package/docs/adr/0002-window-lifetime-references.md +11 -0
  74. package/docs/adr/0003-exclusive-desktop-control.md +11 -0
  75. package/docs/adr/0004-pi-first-acceptance.md +9 -0
  76. package/docs/adr/0005-default-apps-and-workspace-reuse.md +7 -0
  77. package/docs/agents/domain.md +9 -0
  78. package/docs/agents/issue-tracker.md +10 -0
  79. package/docs/agents/triage-labels.md +9 -0
  80. package/docs/browser.md +113 -0
  81. package/docs/implementation-plan.md +153 -0
  82. package/docs/indicators.md +53 -0
  83. package/docs/native-interop-research.md +17 -0
  84. package/docs/ownership.md +25 -0
  85. package/docs/pi-extension-research.md +57 -0
  86. package/docs/pointer.md +77 -0
  87. package/docs/runtime.md +42 -0
  88. package/docs/settings.md +33 -0
  89. package/docs/spec.md +230 -0
  90. package/docs/stopping.md +83 -0
  91. package/docs/text.md +99 -0
  92. package/docs/tickets.md +85 -0
  93. package/docs/validation.md +1420 -0
  94. package/docs/windows.md +171 -0
  95. package/package.json +134 -4
@@ -0,0 +1,74 @@
1
+ import type { WorkerResult } from "./protocol.js";
2
+ export declare const screenshotParameters: {
3
+ type: string;
4
+ additionalProperties: boolean;
5
+ properties: {
6
+ scope: {
7
+ type: string;
8
+ enum: string[];
9
+ default: string;
10
+ };
11
+ browserTab: {
12
+ type: string;
13
+ minLength: number;
14
+ };
15
+ windowRef: {
16
+ type: string;
17
+ minLength: number;
18
+ };
19
+ windowTitle: {
20
+ type: string;
21
+ minLength: number;
22
+ };
23
+ processId: {
24
+ type: string;
25
+ minimum: number;
26
+ };
27
+ nativeWindowHandle: {
28
+ type: string;
29
+ minimum: number;
30
+ };
31
+ activate: {
32
+ type: string;
33
+ default: boolean;
34
+ };
35
+ windowOnly: {
36
+ type: string;
37
+ default: boolean;
38
+ description: string;
39
+ };
40
+ maxWidth: {
41
+ type: string;
42
+ minimum: number;
43
+ maximum: number;
44
+ default: number;
45
+ };
46
+ };
47
+ };
48
+ export declare function screenshot(args: Record<string, unknown>, signal?: AbortSignal): Promise<WorkerResult>;
49
+ export declare function screenshotContent(result: WorkerResult): {
50
+ content: {
51
+ type: "text";
52
+ text: string;
53
+ }[];
54
+ details: WorkerResult;
55
+ isError: boolean;
56
+ } | {
57
+ content: ({
58
+ type: "text";
59
+ text: string;
60
+ data?: undefined;
61
+ mimeType?: undefined;
62
+ } | {
63
+ type: "image";
64
+ data: string;
65
+ mimeType: string;
66
+ text?: undefined;
67
+ })[];
68
+ details: {
69
+ screenshot: {};
70
+ ok: boolean;
71
+ action: string;
72
+ };
73
+ isError: boolean;
74
+ };
@@ -0,0 +1,199 @@
1
+ import { access } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { WorkerClient } from "./client.js";
4
+ import { compileNative, nativeDesktop } from "./native.js";
5
+ export const screenshotParameters = {
6
+ type: "object",
7
+ additionalProperties: false,
8
+ properties: {
9
+ scope: {
10
+ type: "string",
11
+ enum: ["active_window", "desktop"],
12
+ default: "active_window",
13
+ },
14
+ browserTab: { type: "string", minLength: 1 },
15
+ windowRef: { type: "string", minLength: 1 },
16
+ windowTitle: { type: "string", minLength: 1 },
17
+ processId: { type: "integer", minimum: 1 },
18
+ nativeWindowHandle: { type: "integer", minimum: 1 },
19
+ activate: { type: "boolean", default: false },
20
+ windowOnly: {
21
+ type: "boolean",
22
+ default: false,
23
+ description: "Native window scope only: use PrintWindow/WGC; refuse screen-copy and desktop fallbacks.",
24
+ },
25
+ maxWidth: { type: "integer", minimum: 0, maximum: 7680, default: 1600 },
26
+ },
27
+ };
28
+ function createWgc() {
29
+ return new WorkerClient({
30
+ launch: async (signal) => {
31
+ const windows = process.env.WINDIR;
32
+ if (!windows)
33
+ throw new Error("WGC_UNAVAILABLE: Windows directory is unknown");
34
+ const framework = join(windows, "Microsoft.NET", "Framework64", "v4.0.30319");
35
+ const metadata = join(windows, "System32", "WinMetadata");
36
+ const graphics = [];
37
+ for (const name of [
38
+ "Windows.Graphics.winmd",
39
+ "Windows.Graphics.Capture.winmd",
40
+ "Windows.Graphics.DirectX.winmd",
41
+ "Windows.Graphics.DirectX.Direct3D11.winmd",
42
+ "Windows.Graphics.Imaging.winmd",
43
+ ]) {
44
+ const candidate = join(metadata, name);
45
+ try {
46
+ await access(candidate);
47
+ graphics.push(candidate);
48
+ }
49
+ catch {
50
+ /* Optional split/consolidated metadata differs across Windows versions. */
51
+ }
52
+ }
53
+ if (graphics.length === 0)
54
+ throw new Error("WGC_UNAVAILABLE: Windows graphics metadata is missing");
55
+ return compileNative(["wgc.cs", "window-marker.cs"], [
56
+ "System.Drawing.dll",
57
+ join(framework, "System.Runtime.dll"),
58
+ join(framework, "System.Runtime.WindowsRuntime.dll"),
59
+ join(metadata, "Windows.Foundation.winmd"),
60
+ ...graphics,
61
+ ], signal);
62
+ },
63
+ timeoutMs: 8000,
64
+ });
65
+ }
66
+ export async function screenshot(args, signal) {
67
+ signal?.throwIfAborted();
68
+ const maxWidth = args.maxWidth ?? 1600;
69
+ const scope = args.scope ?? "active_window";
70
+ const allowed = new Set([
71
+ "windowRef",
72
+ "maxWidth",
73
+ "scope",
74
+ "windowTitle",
75
+ "processId",
76
+ "nativeWindowHandle",
77
+ "activate",
78
+ "windowOnly",
79
+ ]);
80
+ const invalid = (args.windowRef !== undefined &&
81
+ (typeof args.windowRef !== "string" ||
82
+ !args.windowRef.trim() ||
83
+ scope === "desktop")) ||
84
+ Object.keys(args).some((key) => !allowed.has(key)) ||
85
+ !Number.isSafeInteger(maxWidth) ||
86
+ typeof maxWidth !== "number" ||
87
+ maxWidth < 0 ||
88
+ maxWidth > 7680 ||
89
+ (scope !== "active_window" && scope !== "desktop") ||
90
+ (args.windowOnly !== undefined && typeof args.windowOnly !== "boolean") ||
91
+ (args.windowOnly === true && scope === "desktop") ||
92
+ (args.windowTitle !== undefined &&
93
+ (typeof args.windowTitle !== "string" || !args.windowTitle.trim())) ||
94
+ [args.processId, args.nativeWindowHandle].some((value) => value !== undefined &&
95
+ (typeof value !== "number" ||
96
+ !Number.isSafeInteger(value) ||
97
+ value < 1)) ||
98
+ (args.activate !== undefined && typeof args.activate !== "boolean");
99
+ if (invalid)
100
+ return {
101
+ ok: false,
102
+ action: "screenshot",
103
+ error: "INVALID_ARGUMENTS: invalid screenshot target, scope, windowOnly or maxWidth (integer 0–7680)",
104
+ };
105
+ let nativeCaptureError;
106
+ let wgcError;
107
+ try {
108
+ const result = await nativeDesktop.execute("screenshot", { ...args, scope, maxWidth }, signal);
109
+ nativeCaptureError = result.nativeCaptureError;
110
+ const target = result.wgcTarget;
111
+ delete result.wgcTarget;
112
+ if ((result.ok ||
113
+ ((result.error === "SCREEN_REGION_CAPTURE_FAILED" ||
114
+ (args.windowOnly === true &&
115
+ result.error === "WINDOW_CAPTURE_UNAVAILABLE")) &&
116
+ result.cleanupConfirmed !== false &&
117
+ result.identityWarning === undefined)) &&
118
+ target &&
119
+ typeof target === "object" &&
120
+ !Array.isArray(target)) {
121
+ let failure;
122
+ const wgc = createWgc();
123
+ try {
124
+ const composited = await wgc.execute("screenshot", { ...target, maxWidth }, signal);
125
+ if (result.nativeCaptureError !== undefined)
126
+ composited.nativeCaptureError = result.nativeCaptureError;
127
+ if (composited.ok) {
128
+ if (result.windowRefUnavailable !== undefined) {
129
+ composited.windowRefUnavailable = result.windowRefUnavailable;
130
+ if (composited.screenshot &&
131
+ typeof composited.screenshot === "object")
132
+ composited.screenshot.windowRefUnavailable = result.windowRefUnavailable;
133
+ }
134
+ return composited;
135
+ }
136
+ wgcError = composited.error;
137
+ if (!result.screenshot ||
138
+ composited.cleanupConfirmed === false ||
139
+ /WINDOW_REFERENCE|CANCEL|STOP|CLEANUP_UNCONFIRMED/.test(String(composited.error)))
140
+ return composited;
141
+ failure = String(composited.error);
142
+ }
143
+ catch (error) {
144
+ if (signal?.aborted)
145
+ throw error;
146
+ failure = error instanceof Error ? error.message : String(error);
147
+ wgcError = failure;
148
+ if (/WINDOW_REFERENCE|CANCEL|STOP|CLEANUP_UNCONFIRMED/.test(failure))
149
+ throw error;
150
+ }
151
+ finally {
152
+ await wgc.dispose();
153
+ }
154
+ result.wgcError = failure;
155
+ if (result.screenshot && typeof result.screenshot === "object") {
156
+ const capture = result.screenshot;
157
+ capture.captureWarnings = [
158
+ ...(Array.isArray(capture.captureWarnings)
159
+ ? capture.captureWarnings
160
+ : []),
161
+ `WGC_UNAVAILABLE: ${failure}`,
162
+ ];
163
+ }
164
+ }
165
+ return result;
166
+ }
167
+ catch (error) {
168
+ return {
169
+ ok: false,
170
+ action: "screenshot",
171
+ error: error instanceof Error ? error.message : String(error),
172
+ ...(nativeCaptureError === undefined ? {} : { nativeCaptureError }),
173
+ ...(wgcError === undefined ? {} : { wgcError }),
174
+ };
175
+ }
176
+ }
177
+ export function screenshotContent(result) {
178
+ const capture = result.screenshot;
179
+ if (!capture ||
180
+ typeof capture !== "object" ||
181
+ !("base64" in capture) ||
182
+ typeof capture.base64 !== "string") {
183
+ return {
184
+ content: [{ type: "text", text: JSON.stringify(result) }],
185
+ details: result,
186
+ isError: !result.ok,
187
+ };
188
+ }
189
+ const { base64, ...metadata } = capture;
190
+ const details = { ...result, screenshot: metadata };
191
+ return {
192
+ content: [
193
+ { type: "text", text: JSON.stringify(details) },
194
+ { type: "image", data: base64, mimeType: "image/png" },
195
+ ],
196
+ details,
197
+ isError: !result.ok,
198
+ };
199
+ }
@@ -0,0 +1,39 @@
1
+ import type { WorkerResult } from "./protocol.js";
2
+ export interface WorkerLaunch {
3
+ executable: string;
4
+ args: string[];
5
+ cwd?: string;
6
+ }
7
+ export interface WorkerClientOptions {
8
+ launch: (signal?: AbortSignal) => Promise<WorkerLaunch>;
9
+ timeoutMs?: number;
10
+ shutdownMs?: number;
11
+ canTerminate?: () => boolean;
12
+ onStop?: (operationId: string) => void;
13
+ onInputState?: (active: boolean, operationId?: string) => void;
14
+ onExit?: () => void;
15
+ }
16
+ export declare class WorkerClient {
17
+ private readonly options;
18
+ private child;
19
+ private starting;
20
+ private readonly pending;
21
+ private nextId;
22
+ private disposed;
23
+ private failure;
24
+ private inputActive;
25
+ private retirementFailure;
26
+ private retirement;
27
+ private shutdownChanged;
28
+ private readonly lifetime;
29
+ private queue;
30
+ private readonly deadlineCleanup;
31
+ constructor(options: WorkerClientOptions);
32
+ private start;
33
+ private fail;
34
+ execute(action: string, args: Record<string, unknown>, signal?: AbortSignal, operationId?: string): Promise<WorkerResult>;
35
+ private executeNow;
36
+ stopOperation(operationId: string): void;
37
+ retire(reason?: string): Promise<void>;
38
+ dispose(): Promise<void>;
39
+ }
package/dist/client.js ADDED
@@ -0,0 +1,384 @@
1
+ import { spawn } from "node:child_process";
2
+ import { executionDeadline, pressTiming, textTiming } from "./input-timing.js";
3
+ function abortable(operation, signal, retain) {
4
+ return new Promise((resolve, reject) => {
5
+ const abort = () => {
6
+ if (!retain?.())
7
+ reject(signal.reason);
8
+ };
9
+ signal.addEventListener("abort", abort, { once: true });
10
+ if (signal.aborted)
11
+ abort();
12
+ operation
13
+ .then(resolve, reject)
14
+ .finally(() => signal.removeEventListener("abort", abort));
15
+ });
16
+ }
17
+ export class WorkerClient {
18
+ options;
19
+ child;
20
+ starting;
21
+ pending = new Map();
22
+ nextId = 0;
23
+ disposed = false;
24
+ failure;
25
+ inputActive = false;
26
+ retirementFailure;
27
+ retirement;
28
+ shutdownChanged;
29
+ lifetime = new AbortController();
30
+ queue = Promise.resolve();
31
+ deadlineCleanup = new Set();
32
+ constructor(options) {
33
+ this.options = options;
34
+ }
35
+ async start(signal) {
36
+ if (this.disposed)
37
+ throw new Error("WORKER_DISPOSED: session has closed");
38
+ signal?.throwIfAborted();
39
+ if (this.retirement)
40
+ await this.retirement;
41
+ if (this.retirementFailure)
42
+ throw this.retirementFailure;
43
+ if (this.inputActive && !this.child)
44
+ throw new Error("INPUT_CLEANUP_UNCONFIRMED: input owner exited without cleanup acknowledgment");
45
+ if (this.child && this.failure)
46
+ await this.retire(this.failure.message);
47
+ if (this.child)
48
+ return this.child;
49
+ const launchSignal = signal
50
+ ? AbortSignal.any([signal, this.lifetime.signal])
51
+ : this.lifetime.signal;
52
+ this.starting ??= abortable(this.options.launch(launchSignal), launchSignal)
53
+ .then((launch) => {
54
+ if (this.disposed)
55
+ throw new Error("WORKER_DISPOSED: session has closed");
56
+ signal?.throwIfAborted();
57
+ const child = spawn(launch.executable, launch.args, {
58
+ stdio: ["pipe", "pipe", "pipe"],
59
+ windowsHide: true,
60
+ cwd: launch.cwd,
61
+ });
62
+ this.child = child;
63
+ let output = "";
64
+ let diagnostic = "";
65
+ child.stdout.setEncoding("utf8");
66
+ child.stderr.setEncoding("utf8");
67
+ child.stderr.on("data", (chunk) => {
68
+ diagnostic = (diagnostic + chunk).slice(-4096);
69
+ });
70
+ child.stdout.on("data", (chunk) => {
71
+ output += chunk;
72
+ if (output.length > 32 * 1024 * 1024) {
73
+ this.fail(child, new Error("WORKER_PROTOCOL_ERROR: response exceeds 32 MiB"));
74
+ if (!this.inputActive && this.options.canTerminate?.() !== false)
75
+ child.kill();
76
+ return;
77
+ }
78
+ for (;;) {
79
+ const newline = output.indexOf("\n");
80
+ if (newline < 0)
81
+ break;
82
+ const line = output.slice(0, newline);
83
+ output = output.slice(newline + 1);
84
+ try {
85
+ const response = JSON.parse(line);
86
+ if (response.event === "shutdown_state") {
87
+ if (this.child === child &&
88
+ (response.phase === "started" ||
89
+ response.phase === "complete" ||
90
+ response.phase === "failed"))
91
+ this.shutdownChanged?.(response.phase, typeof response.error === "string"
92
+ ? response.error.slice(0, 2048)
93
+ : undefined);
94
+ continue;
95
+ }
96
+ if (response.event === "input_prepare") {
97
+ const pending = Number.isSafeInteger(response.requestId)
98
+ ? this.pending.get(Number(response.requestId))
99
+ : undefined;
100
+ if (!pending ||
101
+ typeof response.token !== "string" ||
102
+ response.token.length < 1 ||
103
+ response.token.length > 200 ||
104
+ typeof response.operationId !== "string" ||
105
+ (pending.operationId !== undefined &&
106
+ pending.operationId !== response.operationId))
107
+ continue;
108
+ this.inputActive = true;
109
+ this.options.onInputState?.(true, response.operationId);
110
+ child.stdin.write(`${JSON.stringify({ inputAck: response.token, requestId: response.requestId })}\n`);
111
+ continue;
112
+ }
113
+ if (response.event === "input_state" &&
114
+ typeof response.active === "boolean") {
115
+ this.inputActive = response.active;
116
+ this.options.onInputState?.(response.active, response.operationId);
117
+ continue;
118
+ }
119
+ if (response.event === "stopped" &&
120
+ typeof response.operationId === "string") {
121
+ this.options.onStop?.(response.operationId);
122
+ continue;
123
+ }
124
+ if (!Number.isSafeInteger(response.id) ||
125
+ typeof response.result?.ok !== "boolean" ||
126
+ typeof response.result.action !== "string")
127
+ throw new Error("Invalid response");
128
+ const pending = this.pending.get(response.id);
129
+ if (pending) {
130
+ this.pending.delete(response.id);
131
+ pending.cleanup();
132
+ pending.resolve(response.result);
133
+ }
134
+ }
135
+ catch {
136
+ this.fail(child, new Error("WORKER_PROTOCOL_ERROR: invalid response"));
137
+ if (!this.inputActive && this.options.canTerminate?.() !== false)
138
+ child.kill();
139
+ }
140
+ }
141
+ });
142
+ child.once("error", (error) => this.fail(child, new Error(`WORKER_START_FAILED: ${error.message}`), child.pid === undefined));
143
+ child.once("close", (code) => {
144
+ const current = this.child === child;
145
+ this.fail(child, new Error(`WORKER_EXITED: code ${code}${diagnostic ? `; ${diagnostic.trim()}` : ""}`), true);
146
+ if (current)
147
+ this.options.onExit?.();
148
+ });
149
+ child.stdin.on("error", (error) => this.fail(child, new Error(`WORKER_WRITE_FAILED: ${error.message}`)));
150
+ return child;
151
+ })
152
+ .finally(() => {
153
+ this.starting = undefined;
154
+ });
155
+ return this.starting;
156
+ }
157
+ fail(child, error, confirmedExit = false) {
158
+ if (this.child !== child)
159
+ return;
160
+ if (confirmedExit) {
161
+ this.child = undefined;
162
+ this.failure = undefined;
163
+ // Process death is not an acknowledgement that its resources were freed.
164
+ if (!this.retirementFailure?.message.startsWith("WORKER_CLEANUP_UNCONFIRMED:"))
165
+ this.retirementFailure = undefined;
166
+ }
167
+ else
168
+ this.failure = error;
169
+ for (const [id, pending] of this.pending) {
170
+ this.pending.delete(id);
171
+ pending.cleanup();
172
+ pending.reject(error);
173
+ }
174
+ }
175
+ execute(action, args, signal, operationId) {
176
+ if (this.disposed)
177
+ return Promise.reject(new Error("WORKER_DISPOSED: session has closed"));
178
+ const callSignal = signal
179
+ ? AbortSignal.any([signal, this.lifetime.signal])
180
+ : this.lifetime.signal;
181
+ const result = this.queue
182
+ .catch(() => undefined)
183
+ .then(() => {
184
+ callSignal.throwIfAborted();
185
+ return this.executeNow(action, args, callSignal, operationId);
186
+ });
187
+ this.queue = result.catch(() => undefined);
188
+ return abortable(result, callSignal, () => this.deadlineCleanup.has(callSignal));
189
+ }
190
+ async executeNow(action, args, signal, operationId) {
191
+ let inputPolicy;
192
+ if (action === "keypress" || action === "click" || action === "type_text") {
193
+ try {
194
+ inputPolicy = (action === "type_text" ? textTiming : pressTiming)(args, this.options.timeoutMs ?? 30_000);
195
+ }
196
+ catch (error) {
197
+ return {
198
+ ok: false,
199
+ action,
200
+ error: error instanceof Error ? error.message : String(error),
201
+ };
202
+ }
203
+ }
204
+ const child = await this.start(signal);
205
+ if (this.disposed)
206
+ throw new Error("WORKER_DISPOSED: session has closed");
207
+ signal?.throwIfAborted();
208
+ const id = ++this.nextId;
209
+ return new Promise((resolve, reject) => {
210
+ let expired = false;
211
+ let cleanupTimer;
212
+ const timeoutError = () => new Error(`WORKER_TIMEOUT: ${action} exceeded the execution deadline; input was not replayed`);
213
+ const timer = executionDeadline(inputPolicy?.timeoutMs ??
214
+ Math.max(1, this.options.timeoutMs ?? 30_000, action === "wait" &&
215
+ Number.isSafeInteger(args.milliseconds ?? 500) &&
216
+ Number(args.milliseconds ?? 500) >= 1 &&
217
+ Number(args.milliseconds ?? 500) <= 30000
218
+ ? Number(args.milliseconds ?? 500) + 2000
219
+ : 0, action === "wait_for" &&
220
+ Number.isSafeInteger(args.timeoutMs ?? 5000) &&
221
+ Number(args.timeoutMs ?? 5000) >= 100 &&
222
+ Number(args.timeoutMs ?? 5000) <= 60000
223
+ ? Number(args.timeoutMs ?? 5000) + 10000
224
+ : 0), () => {
225
+ if (inputPolicy) {
226
+ expired = true;
227
+ if (signal)
228
+ this.deadlineCleanup.add(signal);
229
+ // Keep the request and its owner until cooperative release is acknowledged.
230
+ child.stdin.write(`${JSON.stringify({ cancel: id })}\n`);
231
+ cleanupTimer = setTimeout(() => {
232
+ this.fail(child, timeoutError());
233
+ if (!this.inputActive && this.options.canTerminate?.() !== false)
234
+ child.kill();
235
+ }, 5000);
236
+ return;
237
+ }
238
+ this.fail(child, new Error(`WORKER_TIMEOUT: ${action} exceeded the execution deadline; input was not replayed`));
239
+ if (!this.inputActive && this.options.canTerminate?.() !== false)
240
+ child.kill();
241
+ });
242
+ const abort = () => {
243
+ if (expired)
244
+ return;
245
+ const pending = this.pending.get(id);
246
+ if (!pending)
247
+ return;
248
+ this.pending.delete(id);
249
+ pending.cleanup();
250
+ // Health has no held input. Cooperative input stopping is added before input tools.
251
+ child.stdin.write(`${JSON.stringify({ cancel: id })}\n`);
252
+ pending.reject(new Error("COMPUTER_USE_CANCELLED"));
253
+ };
254
+ this.pending.set(id, {
255
+ operationId,
256
+ resolve: (result) => resolve(expired
257
+ ? {
258
+ ...result,
259
+ ok: false,
260
+ error: result.cleanupConfirmed === false ||
261
+ /CLEANUP_UNCONFIRMED/.test(String(result.error))
262
+ ? (result.error ??
263
+ "INPUT_CLEANUP_UNCONFIRMED: deadline expired before release was confirmed")
264
+ : timeoutError().message,
265
+ executionDeadlineExpired: true,
266
+ interrupted: true,
267
+ timing: inputPolicy,
268
+ }
269
+ : result),
270
+ reject,
271
+ cleanup: () => {
272
+ timer();
273
+ if (signal)
274
+ this.deadlineCleanup.delete(signal);
275
+ clearTimeout(cleanupTimer);
276
+ signal?.removeEventListener("abort", abort);
277
+ },
278
+ });
279
+ signal?.addEventListener("abort", abort, { once: true });
280
+ if (signal?.aborted) {
281
+ abort();
282
+ return;
283
+ }
284
+ child.stdin.write(`${JSON.stringify({ id, action, args, operationId })}\n`, (error) => {
285
+ if (error)
286
+ this.fail(child, new Error(`WORKER_WRITE_FAILED: ${error.message}`));
287
+ });
288
+ });
289
+ }
290
+ stopOperation(operationId) {
291
+ // Control messages bypass queued calls, including a busy native provider.
292
+ this.child?.stdin.write(`${JSON.stringify({ stop: operationId })}\n`);
293
+ }
294
+ async retire(reason = "WORKER_RETIRED: operation stopped") {
295
+ if (this.inputActive || this.options.canTerminate?.() === false)
296
+ throw new Error("INPUT_CLEANUP_UNCONFIRMED: active input prevents bridge retirement");
297
+ if (this.retirementFailure)
298
+ throw this.retirementFailure;
299
+ if (this.retirement)
300
+ return this.retirement;
301
+ const child = this.child;
302
+ if (!child)
303
+ return;
304
+ this.fail(child, new Error(reason));
305
+ this.retirement = new Promise((resolve, reject) => {
306
+ let forced;
307
+ let timer;
308
+ let started = false;
309
+ let complete = false;
310
+ let settled = false;
311
+ const fail = (error) => {
312
+ settled = true;
313
+ clearTimeout(timer);
314
+ clearTimeout(forced);
315
+ reject(error);
316
+ };
317
+ const expire = () => {
318
+ if (this.inputActive || this.options.canTerminate?.() === false) {
319
+ fail(new Error("INPUT_CLEANUP_UNCONFIRMED: active input prevents bridge retirement"));
320
+ return;
321
+ }
322
+ child.kill();
323
+ forced = setTimeout(() => fail(new Error("WORKER_RETIRE_FAILED: child exit was not confirmed; desktop ownership must stay held")), 1000);
324
+ };
325
+ const arm = (milliseconds) => {
326
+ clearTimeout(timer);
327
+ timer = setTimeout(expire, milliseconds);
328
+ };
329
+ this.shutdownChanged = (phase, error) => {
330
+ if (settled)
331
+ return;
332
+ if (phase === "started" && !started) {
333
+ started = true;
334
+ // Resource cleanup is an operation, not an idle exit. Give it the
335
+ // existing execution budget; fast completion adds no fixed delay.
336
+ arm(this.options.timeoutMs ?? 30_000);
337
+ }
338
+ else if (started && phase === "complete" && !complete) {
339
+ complete = true;
340
+ arm(this.options.shutdownMs ?? 1000);
341
+ }
342
+ else if (started && phase === "failed") {
343
+ fail(new Error(`WORKER_CLEANUP_UNCONFIRMED: ${error ?? "worker resource cleanup failed"}`));
344
+ }
345
+ };
346
+ arm(this.options.shutdownMs ?? 1000);
347
+ child.once("close", () => {
348
+ clearTimeout(timer);
349
+ clearTimeout(forced);
350
+ if (settled)
351
+ return;
352
+ if (this.inputActive || this.options.canTerminate?.() === false) {
353
+ fail(new Error("INPUT_CLEANUP_UNCONFIRMED: input owner exited without release"));
354
+ return;
355
+ }
356
+ if (started && !complete) {
357
+ fail(new Error("WORKER_CLEANUP_UNCONFIRMED: worker exited before shutdown completed"));
358
+ return;
359
+ }
360
+ settled = true;
361
+ resolve();
362
+ });
363
+ child.stdin.end();
364
+ });
365
+ try {
366
+ await this.retirement;
367
+ }
368
+ catch (error) {
369
+ this.retirementFailure =
370
+ error instanceof Error ? error : new Error(String(error));
371
+ throw error;
372
+ }
373
+ finally {
374
+ this.shutdownChanged = undefined;
375
+ this.retirement = undefined;
376
+ }
377
+ }
378
+ async dispose() {
379
+ this.disposed = true;
380
+ this.lifetime.abort(new Error("WORKER_DISPOSED: session has closed"));
381
+ await this.starting?.catch(() => undefined);
382
+ await this.retire("WORKER_DISPOSED: session has closed");
383
+ }
384
+ }