@gajae-code/coding-agent 0.11.2 → 0.11.4
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/CHANGELOG.md +34 -0
- package/dist/types/cli/launch-disposition.d.ts +23 -0
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +85 -10
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/tool-transcript-format.d.ts +19 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +39 -12
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/tools/subagent.d.ts +1 -0
- package/dist/types/utils/file-mentions.d.ts +3 -3
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +24 -8
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +3 -2
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +55 -10
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +51 -212
- package/src/main.ts +99 -22
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/session-observer-overlay.ts +10 -23
- package/src/modes/components/tool-transcript-format.ts +72 -0
- package/src/modes/components/transcript-viewer-overlay.ts +42 -2
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +228 -15
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/index.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +50 -10
- package/src/sdk/host/host.ts +13 -1
- package/src/sdk/session.ts +4 -1
- package/src/session/internal/managed-session-scope.ts +15 -1
- package/src/tools/ask.ts +216 -41
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/output-meta.ts +6 -2
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/read.ts +189 -64
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/subagent.ts +43 -2
- package/src/utils/file-mentions.ts +4 -4
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
|
@@ -19,7 +19,6 @@ import type {
|
|
|
19
19
|
RunResultOk,
|
|
20
20
|
SessionSnapshot,
|
|
21
21
|
Transferable,
|
|
22
|
-
Transport,
|
|
23
22
|
WorkerInbound,
|
|
24
23
|
WorkerInitPayload,
|
|
25
24
|
WorkerOutbound,
|
|
@@ -38,8 +37,8 @@ interface WorkerHandle {
|
|
|
38
37
|
send(msg: WorkerInbound, transferList?: Transferable[]): void;
|
|
39
38
|
onMessage(handler: (msg: WorkerOutbound) => void): () => void;
|
|
40
39
|
onError(handler: (error: Error) => void): () => void;
|
|
40
|
+
onClose(handler: () => void): () => void;
|
|
41
41
|
terminate(): Promise<void>;
|
|
42
|
-
readonly mode: "worker" | "inline";
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
export type DialogPolicy = "accept" | "dismiss";
|
|
@@ -118,6 +117,13 @@ let workerInitializerForTest:
|
|
|
118
117
|
| ((worker: WorkerHandle, payload: WorkerInitPayload, timeoutMs: number) => Promise<ReadyInfo>)
|
|
119
118
|
| undefined;
|
|
120
119
|
const GRACE_MS = 750;
|
|
120
|
+
const TAB_WORKER_MODE = "native-free";
|
|
121
|
+
|
|
122
|
+
function startupError(stage: string): ToolError {
|
|
123
|
+
return new ToolError(
|
|
124
|
+
`Tab worker startup failed (stage=${stage}, mode=${TAB_WORKER_MODE}, platform=${process.platform}).`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
121
127
|
|
|
122
128
|
interface DeadTabDescriptor {
|
|
123
129
|
tab: TabSession;
|
|
@@ -152,7 +158,7 @@ async function awaitWithinBudget(op: Promise<unknown>, remainingMs: number, labe
|
|
|
152
158
|
logger.debug("close teardown budget already spent; detaching step", { label });
|
|
153
159
|
return false;
|
|
154
160
|
}
|
|
155
|
-
let timer:
|
|
161
|
+
let timer: NodeJS.Timeout | undefined;
|
|
156
162
|
const timedOut = new Promise<boolean>(resolve => {
|
|
157
163
|
timer = setTimeout(() => resolve(false), remainingMs);
|
|
158
164
|
});
|
|
@@ -358,64 +364,69 @@ export async function acquireTab(
|
|
|
358
364
|
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
359
365
|
throw error;
|
|
360
366
|
}
|
|
361
|
-
let worker
|
|
362
|
-
let info: ReadyInfo;
|
|
367
|
+
let worker: WorkerHandle;
|
|
363
368
|
try {
|
|
364
|
-
|
|
365
|
-
} catch
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
|
|
369
|
+
worker = await spawnAcquireWorker();
|
|
370
|
+
} catch {
|
|
371
|
+
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
372
|
+
else if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
373
|
+
throw startupError("spawn");
|
|
374
|
+
}
|
|
375
|
+
let registeredTab: TabSession | undefined;
|
|
376
|
+
let startupFailure: Error | undefined;
|
|
377
|
+
let browserHeld = false;
|
|
378
|
+
const observeFailure = (error: Error): void => {
|
|
379
|
+
if (registeredTab) markTabDead(registeredTab, error);
|
|
380
|
+
else startupFailure ??= error;
|
|
381
|
+
};
|
|
382
|
+
const unlistenLifetimeMessages = worker.onMessage(msg => {
|
|
383
|
+
if (msg.type === "closed") observeFailure(startupError("protocol-closed"));
|
|
384
|
+
});
|
|
385
|
+
const unlistenLifetimeErrors = worker.onError(() => observeFailure(startupError("error")));
|
|
386
|
+
const unlistenLifetimeClose = worker.onClose(() => observeFailure(startupError("physical-close")));
|
|
387
|
+
try {
|
|
388
|
+
const info = await initializeAcquireWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
|
|
389
|
+
afterWorkerInitializationForTest?.(name);
|
|
390
|
+
if (startupFailure) throw startupFailure;
|
|
391
|
+
if (opts.requireVacantName && tabs.has(name))
|
|
392
|
+
throw new ToolError(`Tab ${JSON.stringify(name)} was replaced during recovery.`);
|
|
393
|
+
|
|
394
|
+
holdBrowser(browser);
|
|
395
|
+
browserHeld = true;
|
|
396
|
+
if (replacementHold) {
|
|
397
|
+
await releaseBrowser(browser, { kill: false });
|
|
398
|
+
replacementHold = false;
|
|
390
399
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
400
|
+
if (startupFailure) throw startupFailure;
|
|
401
|
+
|
|
402
|
+
const tab: TabSession = {
|
|
403
|
+
name,
|
|
404
|
+
browser,
|
|
405
|
+
targetId: info.targetId,
|
|
406
|
+
worker,
|
|
407
|
+
state: "alive",
|
|
408
|
+
info,
|
|
409
|
+
pending: new Map(),
|
|
410
|
+
dialogPolicy: opts.dialogs,
|
|
411
|
+
kindTag: browser.kind.kind,
|
|
412
|
+
ownerId: opts.ownerId,
|
|
413
|
+
lastUsedAt: Date.now(),
|
|
414
|
+
recoveryOpts: freezeRecoveryOptions(opts),
|
|
415
|
+
};
|
|
416
|
+
registeredTab = tab;
|
|
417
|
+
worker.onMessage(msg => handleTabMessage(tab, msg));
|
|
418
|
+
tabs.set(name, tab);
|
|
419
|
+
return { tab, created: true };
|
|
420
|
+
} catch (error) {
|
|
421
|
+
unlistenLifetimeMessages();
|
|
422
|
+
unlistenLifetimeErrors();
|
|
423
|
+
unlistenLifetimeClose();
|
|
395
424
|
await worker.terminate().catch(() => undefined);
|
|
425
|
+
if (browserHeld) await releaseBrowser(browser, { kill: false });
|
|
396
426
|
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
397
|
-
|
|
427
|
+
else if (!browserHeld && browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
428
|
+
throw error;
|
|
398
429
|
}
|
|
399
|
-
holdBrowser(browser);
|
|
400
|
-
if (replacementHold) await releaseBrowser(browser, { kill: false });
|
|
401
|
-
const tab: TabSession = {
|
|
402
|
-
name,
|
|
403
|
-
browser,
|
|
404
|
-
targetId: info.targetId,
|
|
405
|
-
worker,
|
|
406
|
-
state: "alive",
|
|
407
|
-
info,
|
|
408
|
-
pending: new Map(),
|
|
409
|
-
dialogPolicy: opts.dialogs,
|
|
410
|
-
kindTag: browser.kind.kind,
|
|
411
|
-
ownerId: opts.ownerId,
|
|
412
|
-
lastUsedAt: Date.now(),
|
|
413
|
-
recoveryOpts: freezeRecoveryOptions(opts),
|
|
414
|
-
};
|
|
415
|
-
worker.onMessage(msg => handleTabMessage(tab, msg));
|
|
416
|
-
worker.onError(error => markTabDead(tab, error));
|
|
417
|
-
tabs.set(name, tab);
|
|
418
|
-
return { tab, created: true };
|
|
419
430
|
}
|
|
420
431
|
|
|
421
432
|
export async function runInTab(name: string, opts: RunInTabOptions): Promise<RunResultOk> {
|
|
@@ -871,22 +882,14 @@ async function raceWithTimeout<T>(
|
|
|
871
882
|
}
|
|
872
883
|
|
|
873
884
|
async function spawnTabWorker(): Promise<WorkerHandle> {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
return wrapBunWorker(worker);
|
|
879
|
-
} catch (err) {
|
|
880
|
-
logger.warn("Bun Worker spawn failed; using inline tab worker (no sync-loop guard)", {
|
|
881
|
-
error: err instanceof Error ? err.message : String(err),
|
|
882
|
-
});
|
|
883
|
-
return spawnInlineWorker();
|
|
884
|
-
}
|
|
885
|
+
const worker = isCompiledBinary()
|
|
886
|
+
? new Worker("./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", { type: "module" })
|
|
887
|
+
: new Worker(new URL("./tab-worker-entry.ts", import.meta.url).href, { type: "module" });
|
|
888
|
+
return wrapBunWorker(worker);
|
|
885
889
|
}
|
|
886
890
|
|
|
887
891
|
function wrapBunWorker(worker: Worker): WorkerHandle {
|
|
888
892
|
return {
|
|
889
|
-
mode: "worker",
|
|
890
893
|
send(msg, transferList) {
|
|
891
894
|
worker.postMessage(msg, { transfer: transferList ?? [] });
|
|
892
895
|
},
|
|
@@ -896,9 +899,8 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
|
|
|
896
899
|
return () => worker.removeEventListener("message", wrap);
|
|
897
900
|
},
|
|
898
901
|
onError(handler) {
|
|
899
|
-
const onError = (
|
|
900
|
-
const onMessageError = (
|
|
901
|
-
handler(new ToolError(`Tab worker message error: ${String(event.data)}`));
|
|
902
|
+
const onError = (): void => handler(startupError("error"));
|
|
903
|
+
const onMessageError = (): void => handler(startupError("messageerror"));
|
|
902
904
|
worker.addEventListener("error", onError);
|
|
903
905
|
worker.addEventListener("messageerror", onMessageError);
|
|
904
906
|
return () => {
|
|
@@ -906,69 +908,76 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
|
|
|
906
908
|
worker.removeEventListener("messageerror", onMessageError);
|
|
907
909
|
};
|
|
908
910
|
},
|
|
911
|
+
onClose(handler) {
|
|
912
|
+
const onClose = (): void => handler();
|
|
913
|
+
worker.addEventListener("close", onClose);
|
|
914
|
+
return () => worker.removeEventListener("close", onClose);
|
|
915
|
+
},
|
|
909
916
|
async terminate() {
|
|
910
917
|
worker.terminate();
|
|
911
918
|
},
|
|
912
919
|
};
|
|
913
920
|
}
|
|
914
921
|
|
|
915
|
-
/**
|
|
916
|
-
* Inline fallback for environments where Bun cannot compile or spawn the worker
|
|
917
|
-
* entry. This preserves normal browser behavior but cannot interrupt synchronous
|
|
918
|
-
* infinite loops because user code runs on the main thread.
|
|
919
|
-
*/
|
|
920
|
-
async function spawnInlineWorker(): Promise<WorkerHandle> {
|
|
921
|
-
const hostListeners = new Set<(message: WorkerOutbound) => void>();
|
|
922
|
-
const workerListeners = new Set<(message: WorkerInbound) => void>();
|
|
923
|
-
const workerTransport: Transport = {
|
|
924
|
-
send: msg =>
|
|
925
|
-
queueMicrotask(() => {
|
|
926
|
-
for (const listener of hostListeners) listener(msg as WorkerOutbound);
|
|
927
|
-
}),
|
|
928
|
-
onMessage: handler => {
|
|
929
|
-
const typed = handler as (message: WorkerInbound) => void;
|
|
930
|
-
workerListeners.add(typed);
|
|
931
|
-
return () => workerListeners.delete(typed);
|
|
932
|
-
},
|
|
933
|
-
close: () => {},
|
|
934
|
-
};
|
|
935
|
-
const { WorkerCore } = await import("./tab-worker");
|
|
936
|
-
new WorkerCore(workerTransport);
|
|
937
|
-
return {
|
|
938
|
-
mode: "inline",
|
|
939
|
-
send: msg =>
|
|
940
|
-
queueMicrotask(() => {
|
|
941
|
-
for (const listener of workerListeners) listener(msg);
|
|
942
|
-
}),
|
|
943
|
-
onMessage: handler => {
|
|
944
|
-
hostListeners.add(handler);
|
|
945
|
-
return () => hostListeners.delete(handler);
|
|
946
|
-
},
|
|
947
|
-
onError: () => () => {},
|
|
948
|
-
async terminate() {},
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
|
|
952
922
|
async function initializeTabWorker(
|
|
953
923
|
worker: WorkerHandle,
|
|
954
924
|
payload: WorkerInitPayload,
|
|
955
925
|
timeoutMs: number,
|
|
956
926
|
): Promise<ReadyInfo> {
|
|
957
|
-
|
|
927
|
+
type StartupPhase = "await-bootstrap" | "bootstrap-confirmed" | "await-init";
|
|
928
|
+
let phase: StartupPhase = "await-bootstrap";
|
|
929
|
+
const { promise: bootstrap, resolve: bootstrapReady, reject: rejectBootstrap } = Promise.withResolvers<void>();
|
|
930
|
+
const { promise: initialized, resolve: ready, reject: rejectInitialized } = Promise.withResolvers<ReadyInfo>();
|
|
931
|
+
void initialized.catch(() => undefined);
|
|
932
|
+
const fail = (stage: string): void => {
|
|
933
|
+
const error = startupError(stage);
|
|
934
|
+
rejectBootstrap(error);
|
|
935
|
+
rejectInitialized(error);
|
|
936
|
+
};
|
|
958
937
|
const unlisten = worker.onMessage(msg => {
|
|
959
|
-
if (msg.type === "
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
938
|
+
if (msg.type === "log") {
|
|
939
|
+
logWorkerMessage(msg);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (phase === "await-bootstrap") {
|
|
943
|
+
if (msg.type === "bootstrap-ready" && msg.version === 1 && msg.mode === TAB_WORKER_MODE) {
|
|
944
|
+
phase = "bootstrap-confirmed";
|
|
945
|
+
bootstrapReady();
|
|
946
|
+
} else if (msg.type === "bootstrap-failed") {
|
|
947
|
+
fail("bootstrap");
|
|
948
|
+
} else if (msg.type === "closed") {
|
|
949
|
+
fail("protocol-closed");
|
|
950
|
+
} else {
|
|
951
|
+
fail("protocol-phase");
|
|
952
|
+
}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (phase !== "await-init") {
|
|
956
|
+
fail("protocol-phase");
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (msg.type === "ready") {
|
|
960
|
+
ready(msg.info);
|
|
961
|
+
} else if (msg.type === "init-failed") {
|
|
962
|
+
fail("init");
|
|
963
|
+
} else if (msg.type === "closed") {
|
|
964
|
+
fail("protocol-closed");
|
|
965
|
+
} else {
|
|
966
|
+
fail("protocol-phase");
|
|
967
|
+
}
|
|
965
968
|
});
|
|
969
|
+
const unlistenError = worker.onError(() => fail("error"));
|
|
970
|
+
const unlistenClose = worker.onClose(() => fail("physical-close"));
|
|
966
971
|
try {
|
|
972
|
+
worker.send({ type: "bootstrap", version: 1, mode: TAB_WORKER_MODE });
|
|
973
|
+
await raceWithTimeout(bootstrap, timeoutMs, startupError("bootstrap-timeout").message);
|
|
974
|
+
phase = "await-init";
|
|
967
975
|
worker.send({ type: "init", payload });
|
|
968
|
-
return await raceWithTimeout(
|
|
976
|
+
return await raceWithTimeout(initialized, timeoutMs, startupError("init-timeout").message);
|
|
969
977
|
} finally {
|
|
970
978
|
unlisten();
|
|
971
979
|
unlistenError();
|
|
980
|
+
unlistenClose();
|
|
972
981
|
}
|
|
973
982
|
}
|
|
974
983
|
|
|
@@ -979,9 +988,3 @@ export function initializeTabWorkerForTest(
|
|
|
979
988
|
): Promise<ReadyInfo> {
|
|
980
989
|
return initializeTabWorker(worker, payload, timeoutMs);
|
|
981
990
|
}
|
|
982
|
-
|
|
983
|
-
function errorFromWorkerEvent(event: ErrorEvent): Error {
|
|
984
|
-
if (event.error instanceof Error) return event.error;
|
|
985
|
-
if (event.message) return new Error(event.message);
|
|
986
|
-
return new Error("Unknown tab worker error");
|
|
987
|
-
}
|
|
@@ -3,19 +3,38 @@ import type { Transport, WorkerInbound, WorkerOutbound } from "./tab-protocol";
|
|
|
3
3
|
import { WorkerCore } from "./tab-worker";
|
|
4
4
|
|
|
5
5
|
if (!parentPort) throw new Error("tab-worker-entry: missing parentPort");
|
|
6
|
+
const port = parentPort;
|
|
7
|
+
port.once("message", bootstrap => {
|
|
8
|
+
if (!isBootstrap(bootstrap)) {
|
|
9
|
+
port.postMessage({ type: "bootstrap-failed", error: "Invalid tab worker bootstrap." });
|
|
10
|
+
port.close();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
6
13
|
|
|
7
|
-
const transport: Transport = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
14
|
+
const transport: Transport = {
|
|
15
|
+
send(msg, transferList) {
|
|
16
|
+
port.postMessage(msg, transferList ?? []);
|
|
17
|
+
},
|
|
18
|
+
onMessage(handler) {
|
|
19
|
+
const wrap = (message: unknown): void => handler(message as WorkerOutbound | WorkerInbound);
|
|
20
|
+
port.on("message", wrap);
|
|
21
|
+
return () => port.off("message", wrap);
|
|
22
|
+
},
|
|
23
|
+
close() {
|
|
24
|
+
port.close();
|
|
25
|
+
},
|
|
26
|
+
};
|
|
20
27
|
|
|
21
|
-
new WorkerCore(transport);
|
|
28
|
+
new WorkerCore(transport);
|
|
29
|
+
port.postMessage({ type: "bootstrap-ready", version: 1, mode: "native-free" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function isBootstrap(message: unknown): message is Extract<WorkerInbound, { type: "bootstrap" }> {
|
|
33
|
+
return (
|
|
34
|
+
typeof message === "object" &&
|
|
35
|
+
message !== null &&
|
|
36
|
+
(message as { type?: unknown }).type === "bootstrap" &&
|
|
37
|
+
(message as { version?: unknown }).version === 1 &&
|
|
38
|
+
(message as { mode?: unknown }).mode === "native-free"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as url from "node:url";
|
|
4
|
+
|
|
5
|
+
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
6
|
+
const TOP_LEVEL_INTERNAL_URL_PREFIXES = ["agent://", "artifact://", "rule://", "local://"] as const;
|
|
7
|
+
|
|
8
|
+
function normalizeAtPrefix(filePath: string): string {
|
|
9
|
+
if (!filePath.startsWith("@")) return filePath;
|
|
10
|
+
|
|
11
|
+
const withoutAt = filePath.slice(1);
|
|
12
|
+
if (
|
|
13
|
+
withoutAt.startsWith("/") ||
|
|
14
|
+
withoutAt === "~" ||
|
|
15
|
+
withoutAt.startsWith("~/") ||
|
|
16
|
+
path.win32.isAbsolute(withoutAt) ||
|
|
17
|
+
withoutAt.startsWith("agent://") ||
|
|
18
|
+
withoutAt.startsWith("artifact://") ||
|
|
19
|
+
withoutAt.startsWith("rule://") ||
|
|
20
|
+
withoutAt.startsWith("local:")
|
|
21
|
+
) {
|
|
22
|
+
return withoutAt;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return filePath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function expandPath(filePath: string): string {
|
|
29
|
+
const normalized = normalizeAtPrefix(filePath).replace(UNICODE_SPACES, " ");
|
|
30
|
+
if (normalized.toLowerCase().startsWith("file://")) {
|
|
31
|
+
try {
|
|
32
|
+
return expandTilde(url.fileURLToPath(normalized));
|
|
33
|
+
} catch {
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return expandTilde(normalized);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function expandTilde(filePath: string): string {
|
|
41
|
+
const home = os.homedir();
|
|
42
|
+
if (filePath === "~") return home;
|
|
43
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) return home + filePath.slice(1);
|
|
44
|
+
if (filePath.startsWith("~")) return path.join(home, filePath.slice(1));
|
|
45
|
+
return filePath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeLocalScheme(filePath: string): string {
|
|
49
|
+
return filePath.replace(/^(local:)\/(?!\/)/, "$1//");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Resolve filesystem-only worker inputs without evaluating the broad path utility module. */
|
|
53
|
+
export function resolveTabWorkerPath(filePath: string, cwd: string): string {
|
|
54
|
+
const normalized = normalizeLocalScheme(filePath);
|
|
55
|
+
const expanded = expandPath(normalized);
|
|
56
|
+
const expandedAndNormalized = normalizeLocalScheme(expanded);
|
|
57
|
+
for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
|
|
58
|
+
if (expandedAndNormalized.startsWith(prefix)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Path "${normalized}" uses internal scheme "${prefix}" and must be resolved through the proper protocol handler, not as a filesystem path.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (/^\/+$/u.test(expanded)) return cwd;
|
|
66
|
+
return path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
|
|
67
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isCompiledBinary } from "@gajae-code/utils/env";
|
|
2
|
+
import type { WorkerInbound, WorkerOutbound } from "./tab-protocol";
|
|
3
|
+
|
|
4
|
+
interface SmokeWorkerHandle {
|
|
5
|
+
postMessage(message: WorkerInbound): void;
|
|
6
|
+
addEventListener(type: string, listener: EventListener): void;
|
|
7
|
+
removeEventListener(type: string, listener: EventListener): void;
|
|
8
|
+
terminate(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SMOKE_TIMEOUT_MS = 5_000;
|
|
12
|
+
const TAB_WORKER_MODE = "native-free";
|
|
13
|
+
|
|
14
|
+
function smokeStartupError(stage: string): Error {
|
|
15
|
+
return new Error(
|
|
16
|
+
`Tab worker startup failed (stage=${stage}, mode=${TAB_WORKER_MODE}, platform=${process.platform}).`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Starts the real tab worker entry without connecting to a browser, then proves
|
|
22
|
+
* its bootstrap and shutdown protocol. The compiled branch deliberately keeps
|
|
23
|
+
* a static literal for Bun's --compile worker discovery.
|
|
24
|
+
*/
|
|
25
|
+
export async function smokeTestTabWorker(timeoutMs = SMOKE_TIMEOUT_MS): Promise<void> {
|
|
26
|
+
const worker = isCompiledBinary()
|
|
27
|
+
? new Worker("./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", { type: "module" })
|
|
28
|
+
: new Worker(new URL("./tab-worker-entry.ts", import.meta.url).href, { type: "module" });
|
|
29
|
+
await smokeTestTabWorkerWithWorkerForTest(worker, timeoutMs);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Test-only: verify the smoke handshake against a controlled worker transport. */
|
|
33
|
+
export async function smokeTestTabWorkerWithWorkerForTest(worker: SmokeWorkerHandle, timeoutMs: number): Promise<void> {
|
|
34
|
+
type SmokePhase = "await-bootstrap" | "await-closed";
|
|
35
|
+
let phase: SmokePhase = "await-bootstrap";
|
|
36
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
37
|
+
let settled = false;
|
|
38
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
39
|
+
|
|
40
|
+
const cleanup = (): void => {
|
|
41
|
+
if (timeout) clearTimeout(timeout);
|
|
42
|
+
worker.removeEventListener("message", onMessage);
|
|
43
|
+
worker.removeEventListener("error", onError);
|
|
44
|
+
worker.removeEventListener("messageerror", onMessageError);
|
|
45
|
+
};
|
|
46
|
+
const succeed = (): void => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
resolve();
|
|
50
|
+
};
|
|
51
|
+
const fail = (stage: string): void => {
|
|
52
|
+
if (settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
reject(smokeStartupError(stage));
|
|
55
|
+
};
|
|
56
|
+
const onMessage: EventListener = event => {
|
|
57
|
+
const message = (event as MessageEvent<WorkerOutbound>).data;
|
|
58
|
+
if (message.type === "log") return;
|
|
59
|
+
if (phase === "await-bootstrap") {
|
|
60
|
+
if (message.type === "bootstrap-ready" && message.version === 1 && message.mode === TAB_WORKER_MODE) {
|
|
61
|
+
phase = "await-closed";
|
|
62
|
+
worker.postMessage({ type: "close" } satisfies WorkerInbound);
|
|
63
|
+
} else if (message.type === "bootstrap-failed") {
|
|
64
|
+
fail("bootstrap");
|
|
65
|
+
} else {
|
|
66
|
+
fail("protocol-phase");
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (message.type === "closed") succeed();
|
|
71
|
+
else fail("protocol-phase");
|
|
72
|
+
};
|
|
73
|
+
const onError: EventListener = () => fail("error");
|
|
74
|
+
const onMessageError: EventListener = () => fail("messageerror");
|
|
75
|
+
|
|
76
|
+
worker.addEventListener("message", onMessage);
|
|
77
|
+
worker.addEventListener("error", onError);
|
|
78
|
+
worker.addEventListener("messageerror", onMessageError);
|
|
79
|
+
timeout = setTimeout(() => fail("smoke-timeout"), timeoutMs);
|
|
80
|
+
try {
|
|
81
|
+
worker.postMessage({ type: "bootstrap", version: 1, mode: TAB_WORKER_MODE } satisfies WorkerInbound);
|
|
82
|
+
await promise;
|
|
83
|
+
} finally {
|
|
84
|
+
cleanup();
|
|
85
|
+
worker.terminate();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -2,7 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { untilAborted } from "@gajae-code/utils/abortable";
|
|
6
|
+
import { Snowflake } from "@gajae-code/utils/snowflake";
|
|
6
7
|
import type { HTMLElement } from "linkedom";
|
|
7
8
|
import type {
|
|
8
9
|
Browser,
|
|
@@ -17,8 +18,6 @@ import type {
|
|
|
17
18
|
import { JsRuntime, type RuntimeHooks } from "../../eval/js/shared/runtime";
|
|
18
19
|
import type { JsDisplayOutput } from "../../eval/js/shared/types";
|
|
19
20
|
import { resizeImage } from "../../utils/image-resize";
|
|
20
|
-
import { resolveToCwd } from "../path-utils";
|
|
21
|
-
import { formatScreenshot } from "../render-utils";
|
|
22
21
|
import { ToolAbortError, ToolError, throwIfAborted } from "../tool-errors";
|
|
23
22
|
import {
|
|
24
23
|
applyStealthPatches,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
loadPuppeteerInWorker,
|
|
29
28
|
} from "./launch";
|
|
30
29
|
import { extractReadableFromHtml, type ReadableFormat, type ReadableResult } from "./readable";
|
|
30
|
+
import { formatScreenshot } from "./screenshot-format";
|
|
31
31
|
import type {
|
|
32
32
|
Observation,
|
|
33
33
|
ObservationEntry,
|
|
@@ -41,6 +41,7 @@ import type {
|
|
|
41
41
|
WorkerInbound,
|
|
42
42
|
WorkerInitPayload,
|
|
43
43
|
} from "./tab-protocol";
|
|
44
|
+
import { resolveTabWorkerPath } from "./tab-worker-path-resolver";
|
|
44
45
|
|
|
45
46
|
declare global {
|
|
46
47
|
interface Element extends HTMLElement {}
|
|
@@ -813,7 +814,8 @@ export class WorkerCore {
|
|
|
813
814
|
{ type: "image", data: buffer.toBase64(), mimeType: "image/png" },
|
|
814
815
|
{ maxWidth: 1024, maxHeight: 1024, maxBytes: 150 * 1024, jpegQuality: 70 },
|
|
815
816
|
);
|
|
816
|
-
const explicitPath = opts.save ?
|
|
817
|
+
const explicitPath = opts.save ? resolveTabWorkerPath(opts.save, session.cwd) : undefined;
|
|
818
|
+
|
|
817
819
|
const dest =
|
|
818
820
|
explicitPath ??
|
|
819
821
|
(session.browserScreenshotDir
|
|
@@ -950,7 +952,7 @@ export class WorkerCore {
|
|
|
950
952
|
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
951
953
|
)) as ElementHandle;
|
|
952
954
|
try {
|
|
953
|
-
const absolute = filePaths.map(filePath =>
|
|
955
|
+
const absolute = filePaths.map(filePath => resolveTabWorkerPath(filePath, session.cwd));
|
|
954
956
|
const upload = handle as unknown as { uploadFile: (...paths: string[]) => Promise<void> };
|
|
955
957
|
const tagName = (await untilAborted(signal, () =>
|
|
956
958
|
handle.evaluate(el => (el as unknown as { tagName: string }).tagName),
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -605,6 +605,10 @@ async function spillLargeResultToArtifact(
|
|
|
605
605
|
toolName: string,
|
|
606
606
|
context: AgentToolContext | undefined,
|
|
607
607
|
): Promise<AgentToolResult> {
|
|
608
|
+
if (toolName === "read" && (result.details as { spillEligible?: boolean } | undefined)?.spillEligible !== true) {
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
|
|
608
612
|
const artifactCapability = artifactCapabilityForContext(context);
|
|
609
613
|
if (!artifactCapability) return result;
|
|
610
614
|
const { threshold, readThreshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
|
|
@@ -709,8 +713,8 @@ async function spillLargeResultToArtifact(
|
|
|
709
713
|
/**
|
|
710
714
|
* Absolute inline-size backstop enforced after {@link spillLargeResultToArtifact}.
|
|
711
715
|
*
|
|
712
|
-
* The threshold-based spill above has escape hatches: it
|
|
713
|
-
*
|
|
716
|
+
* The threshold-based spill above has escape hatches: it skips ineligible `read`
|
|
717
|
+
* results and results that already carry an `artifactId` (a tool may set
|
|
714
718
|
* partial truncation meta yet still emit oversized inline text). This backstop
|
|
715
719
|
* closes those gaps: when `tools.maxInlineResultBytes` is configured (> 0), any
|
|
716
720
|
* final result whose inline text exceeds the cap is force-saved to an artifact
|