codex-chatgpt-control 0.5.1-alpha.3 → 0.5.1-alpha.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 +27 -0
- package/README.md +16 -0
- package/contracts/v1/fixtures/download-blocked-by-browser.json +22 -0
- package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
- package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
- package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
- package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
- package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
- package/contracts/v1/manifest.json +30 -0
- package/contracts/v1/parity-suite.json +10 -7
- package/dist/codex-chatgpt-control-backend.mjs +3795 -2077
- package/dist/codex-chatgpt-control-journal.mjs +5547 -0
- package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
- package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
- package/dist/codex-chatgpt-control.bundle.mjs +3403 -1369
- package/dist/src/browser/chatgpt-url.d.ts +2 -0
- package/dist/src/browser/chatgpt-url.js +9 -0
- package/dist/src/browser/downloads.d.ts +15 -1
- package/dist/src/browser/downloads.js +150 -28
- package/dist/src/browser/page-state.js +6 -0
- package/dist/src/client.d.ts +5 -0
- package/dist/src/client.js +69 -12
- package/dist/src/commands/artifacts.js +6 -4
- package/dist/src/commands/chat-popover.d.ts +37 -0
- package/dist/src/commands/chat-popover.js +537 -0
- package/dist/src/commands/configuration.d.ts +5 -0
- package/dist/src/commands/configuration.js +121 -14
- package/dist/src/commands/context.js +10 -3
- package/dist/src/commands/doctor.js +1 -1
- package/dist/src/commands/experience.d.ts +5 -0
- package/dist/src/commands/experience.js +69 -21
- package/dist/src/commands/files.js +10 -6
- package/dist/src/commands/modes.js +36 -4
- package/dist/src/commands/power-discovery.js +52 -10
- package/dist/src/commands/session.js +12 -4
- package/dist/src/dom/composer-text.d.ts +6 -0
- package/dist/src/dom/composer-text.js +85 -0
- package/dist/src/operations/artifact-transfer.d.ts +1 -1
- package/dist/src/operations/artifact-transfer.js +3 -3
- package/dist/src/operations/browser-adapter.d.ts +10 -4
- package/dist/src/operations/browser-adapter.js +95 -15
- package/dist/src/operations/browser-observation.d.ts +1 -1
- package/dist/src/operations/browser-observation.js +97 -49
- package/dist/src/operations/browser-target.d.ts +3 -1
- package/dist/src/operations/browser-target.js +49 -7
- package/dist/src/operations/chatgpt-runtime.js +85 -3
- package/dist/src/operations/client.js +2 -0
- package/dist/src/operations/configuration-routing.d.ts +4 -0
- package/dist/src/operations/configuration-routing.js +14 -0
- package/dist/src/operations/index.d.ts +3 -0
- package/dist/src/operations/index.js +3 -0
- package/dist/src/operations/journal-authority.d.ts +12 -0
- package/dist/src/operations/journal-authority.js +1 -0
- package/dist/src/operations/journal-rpc-client.d.ts +12 -0
- package/dist/src/operations/journal-rpc-client.js +225 -0
- package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
- package/dist/src/operations/journal-rpc-protocol.js +214 -0
- package/dist/src/operations/journal-rpc-server.d.ts +15 -0
- package/dist/src/operations/journal-rpc-server.js +272 -0
- package/dist/src/operations/journal.d.ts +1 -0
- package/dist/src/operations/journal.js +28 -2
- package/dist/src/operations/production-attachments.d.ts +3 -1
- package/dist/src/operations/production-attachments.js +25 -8
- package/dist/src/operations/production-chatgpt-artifacts.js +3 -3
- package/dist/src/operations/production-chatgpt-attachments.d.ts +10 -1
- package/dist/src/operations/production-chatgpt-attachments.js +133 -57
- package/dist/src/operations/production-configuration.d.ts +1 -1
- package/dist/src/operations/production-configuration.js +205 -24
- package/dist/src/operations/production-primitives.js +35 -252
- package/dist/src/operations/production-work-steer.js +35 -35
- package/dist/src/operations/runtime-adapter.d.ts +8 -0
- package/dist/src/operations/runtime-adapter.js +20 -4
- package/dist/src/operations/service.d.ts +7 -2
- package/dist/src/operations/service.js +80 -59
- package/dist/src/operations/transactional-chat-power.d.ts +17 -0
- package/dist/src/operations/transactional-chat-power.js +212 -0
- package/dist/src/runtime/coordinated-page.d.ts +2 -0
- package/dist/src/runtime/coordinated-page.js +23 -6
- package/dist/src/safety/blockers.js +8 -1
- package/dist/src/scripts/capture-surface-profile.js +7 -2
- package/dist/src/scripts/journal-server.d.ts +2 -0
- package/dist/src/scripts/journal-server.js +37 -0
- package/dist/src/scripts/live-smoke/scenarios.d.ts +16 -3
- package/dist/src/scripts/live-smoke/scenarios.js +47 -22
- package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
- package/dist/src/scripts/live-smoke/transactional.js +254 -0
- package/package.json +8 -3
- package/references/2026-09-06-journal-service.md +140 -0
- package/references/backend-protocol.md +103 -0
- package/references/python-parity.md +65 -0
|
@@ -6,3 +6,5 @@ export declare const CHATGPT_HOME = "https://chatgpt.com/";
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function isChatGPTUrl(value: string | undefined): boolean;
|
|
8
8
|
export declare function requireChatGPTUrl(value: string, label: string): string;
|
|
9
|
+
/** Public context for ChatGPT's signed attachment endpoint, never its token. */
|
|
10
|
+
export declare function chatGPTAttachmentContextUrl(value: string | undefined): string | undefined;
|
|
@@ -26,3 +26,12 @@ export function requireChatGPTUrl(value, label) {
|
|
|
26
26
|
}
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
|
+
/** Public context for ChatGPT's signed attachment endpoint, never its token. */
|
|
30
|
+
export function chatGPTAttachmentContextUrl(value) {
|
|
31
|
+
if (!isChatGPTUrl(value))
|
|
32
|
+
return undefined;
|
|
33
|
+
const url = new URL(value);
|
|
34
|
+
if (url.pathname !== "/backend-api/estuary/content")
|
|
35
|
+
return undefined;
|
|
36
|
+
return `${url.origin}${url.pathname}`;
|
|
37
|
+
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import type { DownloadedFile, PageLike } from "../types.js";
|
|
2
|
+
import { ChatGPTControlError } from "../errors.js";
|
|
2
3
|
export type DownloadLike = {
|
|
3
4
|
suggestedFilename?: () => string;
|
|
4
5
|
/** Capability-based stream exposed by Playwright's Download implementation. */
|
|
5
6
|
createReadStream?: () => Promise<AsyncIterable<Uint8Array>>;
|
|
6
7
|
saveAs?: (path: string) => Promise<void>;
|
|
7
|
-
path?: (
|
|
8
|
+
path?: (options?: {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}) => Promise<string | null>;
|
|
8
11
|
};
|
|
12
|
+
export declare class DownloadReceiptTimeoutError extends ChatGPTControlError {
|
|
13
|
+
constructor(stage: string);
|
|
14
|
+
}
|
|
15
|
+
export declare class DownloadBrowserBlockedError extends ChatGPTControlError {
|
|
16
|
+
constructor();
|
|
17
|
+
}
|
|
18
|
+
export declare class DownloadReceiptFailedError extends ChatGPTControlError {
|
|
19
|
+
constructor();
|
|
20
|
+
}
|
|
21
|
+
export declare function isTerminalDownloadError(error: unknown): error is DownloadReceiptTimeoutError | DownloadBrowserBlockedError | DownloadReceiptFailedError;
|
|
22
|
+
export declare function isTerminalDownloadCode(code: string | undefined): boolean;
|
|
9
23
|
export declare function waitForDownloadFromClick(page: PageLike, click: () => Promise<void>, destDir: string, timeoutMs: number, filenameHint?: string): Promise<DownloadedFile>;
|
|
@@ -1,39 +1,161 @@
|
|
|
1
1
|
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { ChatGPTControlError } from "../errors.js";
|
|
4
|
+
import { createDeadline, remainingMs } from "../commands/deadline.js";
|
|
5
|
+
import { withTimeout } from "../commands/timeouts.js";
|
|
6
|
+
import { coordinatedEventRegistrationBarrier, withCoordinatedMutationGuard } from "../runtime/coordinated-page.js";
|
|
7
|
+
export class DownloadReceiptTimeoutError extends ChatGPTControlError {
|
|
8
|
+
constructor(stage) {
|
|
9
|
+
super(`Download ${stage} did not complete before the deadline. Completion is unverified; inspect the existing download before retrying.`, "download_unavailable", false, undefined, { code: "download_receipt_timeout", resumable: false });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class DownloadBrowserBlockedError extends ChatGPTControlError {
|
|
13
|
+
constructor() {
|
|
14
|
+
super("Chrome displayed ERR_BLOCKED_BY_CLIENT during the download. Inspect the existing download and Chrome restrictions before retrying; completion is unverified.", "download_unavailable", false, undefined, { code: "download_blocked_by_browser", resumable: false });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class DownloadReceiptFailedError extends ChatGPTControlError {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("The browser did not provide a verified download receipt. Inspect the existing download before retrying; completion is unverified.", "download_unavailable", false, undefined, { code: "download_receipt_failed", resumable: false });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function isTerminalDownloadError(error) {
|
|
23
|
+
return error instanceof DownloadReceiptTimeoutError || error instanceof DownloadBrowserBlockedError || error instanceof DownloadReceiptFailedError;
|
|
24
|
+
}
|
|
25
|
+
export function isTerminalDownloadCode(code) {
|
|
26
|
+
return code === "download_receipt_timeout" || code === "download_blocked_by_browser" || code === "download_receipt_failed";
|
|
27
|
+
}
|
|
28
|
+
async function checkBrowserDownloadError(page, deadline) {
|
|
29
|
+
if (typeof page.evaluate !== "function")
|
|
30
|
+
return;
|
|
31
|
+
// A missing or stalled diagnostic must leave time for the native receipt.
|
|
32
|
+
// Both probes are read-only, bounded by the existing operation deadline,
|
|
33
|
+
// and return an enum instead of exporting error-page URLs or body text.
|
|
34
|
+
const timeoutMs = Math.min(1000, Math.floor(remainingMs(deadline) / 4));
|
|
35
|
+
if (timeoutMs <= 0)
|
|
36
|
+
return;
|
|
37
|
+
let state;
|
|
38
|
+
try {
|
|
39
|
+
state = await withTimeout(page.evaluate(function inspectBrowserDownloadError() {
|
|
40
|
+
if (!document.body?.getAttribute("class")?.split(/\s+/).includes("neterror"))
|
|
41
|
+
return "unavailable";
|
|
42
|
+
const frames = document.querySelectorAll("#main-frame-error");
|
|
43
|
+
if (frames.length !== 1)
|
|
44
|
+
return "unavailable";
|
|
45
|
+
const codes = frames[0].querySelectorAll(".error-code");
|
|
46
|
+
return codes.length === 1 && codes[0].textContent?.trim() === "ERR_BLOCKED_BY_CLIENT"
|
|
47
|
+
? "blocked_by_client" : "unavailable";
|
|
48
|
+
}, undefined, { timeoutMs }), timeoutMs, "Browser download diagnostic timed out.");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (state === "blocked_by_client")
|
|
54
|
+
throw new DownloadBrowserBlockedError();
|
|
55
|
+
}
|
|
56
|
+
async function downloadStep(deadline, stage, run, capMs) {
|
|
57
|
+
const timeoutMs = Math.min(remainingMs(deadline), capMs ?? Number.MAX_SAFE_INTEGER);
|
|
58
|
+
const timeoutError = new DownloadReceiptTimeoutError(stage);
|
|
59
|
+
if (timeoutMs <= 0)
|
|
60
|
+
throw timeoutError;
|
|
61
|
+
try {
|
|
62
|
+
return await withTimeout(run(), timeoutMs, timeoutError.message);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof Error && error.message === timeoutError.message)
|
|
66
|
+
throw timeoutError;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
4
70
|
export async function waitForDownloadFromClick(page, click, destDir, timeoutMs, filenameHint) {
|
|
71
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
|
|
72
|
+
throw new Error("Download timeout must be positive and within the supported timer range.");
|
|
73
|
+
}
|
|
74
|
+
const deadline = createDeadline(timeoutMs);
|
|
75
|
+
await checkBrowserDownloadError(page, deadline);
|
|
5
76
|
const absoluteDest = resolve(destDir);
|
|
6
|
-
await mkdir(absoluteDest, { recursive: true });
|
|
7
|
-
|
|
8
|
-
if (downloadPromise === undefined) {
|
|
77
|
+
await downloadStep(deadline, "destination preparation", () => mkdir(absoluteDest, { recursive: true }));
|
|
78
|
+
if (typeof page.waitForEvent !== "function") {
|
|
9
79
|
throw new Error("The active browser page does not expose download events.");
|
|
10
80
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
81
|
+
// Arm before activation. Promise.all observes both rejections immediately,
|
|
82
|
+
// including a late event rejection after a failed or timed-out click.
|
|
83
|
+
const nativeTimeoutMs = remainingMs(deadline);
|
|
84
|
+
if (nativeTimeoutMs <= 0)
|
|
85
|
+
throw new DownloadReceiptTimeoutError("event registration");
|
|
86
|
+
const activation = new AbortController();
|
|
87
|
+
let failure;
|
|
88
|
+
const assertActive = () => {
|
|
89
|
+
if (failure !== undefined)
|
|
90
|
+
throw failure;
|
|
91
|
+
if (activation.signal.aborted || remainingMs(deadline) <= 0)
|
|
92
|
+
throw new DownloadReceiptTimeoutError("control click");
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
const rawWait = page.waitForEvent("download", { timeout: nativeTimeoutMs, timeoutMs: nativeTimeoutMs });
|
|
96
|
+
const registration = coordinatedEventRegistrationBarrier(rawWait);
|
|
97
|
+
let receiptFailed = false;
|
|
98
|
+
const downloadPromise = downloadStep(deadline, "event receipt", () => rawWait).catch(error => {
|
|
99
|
+
receiptFailed = true;
|
|
100
|
+
failure = isTerminalDownloadError(error) ? error : new DownloadReceiptFailedError();
|
|
101
|
+
activation.abort(failure);
|
|
102
|
+
throw failure;
|
|
103
|
+
});
|
|
104
|
+
const [download] = await Promise.all([
|
|
105
|
+
downloadPromise,
|
|
106
|
+
(async () => {
|
|
107
|
+
// Actor fairness may choose a queued mutation before another read.
|
|
108
|
+
// Registration must finish before the activating click is even queued.
|
|
109
|
+
if (registration !== undefined)
|
|
110
|
+
await downloadStep(deadline, "event registration", () => registration);
|
|
111
|
+
try {
|
|
112
|
+
await downloadStep(deadline, "control click", () => withCoordinatedMutationGuard(activation.signal, assertActive, click), 10_000);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (!receiptFailed)
|
|
116
|
+
await checkBrowserDownloadError(page, deadline);
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
if (!receiptFailed)
|
|
120
|
+
await checkBrowserDownloadError(page, deadline);
|
|
121
|
+
})()
|
|
122
|
+
]);
|
|
123
|
+
// saveAs is already a complete capability; an optional path() call can hang
|
|
124
|
+
// on bridge implementations and is unnecessary when saveAs is available.
|
|
125
|
+
const sourcePath = typeof download.saveAs !== "function" && typeof download.path === "function"
|
|
126
|
+
? await downloadStep(deadline, "source path", () => download.path({ timeoutMs: remainingMs(deadline) })) : null;
|
|
127
|
+
const suggestedFilename = filenameHint
|
|
128
|
+
?? download.suggestedFilename?.()
|
|
129
|
+
?? (sourcePath === null ? undefined : basename(sourcePath))
|
|
130
|
+
?? `chatgpt-download-${Date.now()}`;
|
|
131
|
+
const targetPath = join(absoluteDest, basename(suggestedFilename));
|
|
132
|
+
if (typeof download.saveAs === "function") {
|
|
133
|
+
await downloadStep(deadline, "file save", () => download.saveAs(targetPath));
|
|
134
|
+
}
|
|
135
|
+
else if (sourcePath !== null) {
|
|
136
|
+
if (resolve(sourcePath) !== resolve(targetPath)) {
|
|
137
|
+
await downloadStep(deadline, "file copy", () => copyFile(sourcePath, targetPath));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
throw new Error("The browser download object exposes neither saveAs() nor a completed local path().");
|
|
25
142
|
}
|
|
143
|
+
const saved = await downloadStep(deadline, "file verification", () => stat(targetPath));
|
|
144
|
+
if (saved.size <= 0) {
|
|
145
|
+
throw new Error(`Downloaded file is empty: ${targetPath}`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
path: targetPath,
|
|
149
|
+
suggestedFilename,
|
|
150
|
+
bytes: saved.size
|
|
151
|
+
};
|
|
26
152
|
}
|
|
27
|
-
|
|
28
|
-
throw
|
|
153
|
+
catch (error) {
|
|
154
|
+
throw failure ?? (isTerminalDownloadError(error) ? error : new DownloadReceiptFailedError());
|
|
29
155
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
156
|
+
finally {
|
|
157
|
+
// Cancels locally queued activation only. A native download already in
|
|
158
|
+
// progress, or its native event listener, is not claimed to be cancelled.
|
|
159
|
+
activation.abort();
|
|
33
160
|
}
|
|
34
|
-
return {
|
|
35
|
-
path: targetPath,
|
|
36
|
-
suggestedFilename,
|
|
37
|
-
bytes: saved.size
|
|
38
|
-
};
|
|
39
161
|
}
|
|
@@ -2,6 +2,7 @@ import { classifyVisibleText } from "../safety/blockers.js";
|
|
|
2
2
|
import { compactVisibleText } from "../safety/redaction.js";
|
|
3
3
|
import { escapeRegExp, localeLabels } from "../dom/locale-labels.js";
|
|
4
4
|
import { withTimeout } from "../commands/timeouts.js";
|
|
5
|
+
import { chatGPTAttachmentContextUrl } from "./chatgpt-url.js";
|
|
5
6
|
export function parseConversationId(url) {
|
|
6
7
|
let parsed;
|
|
7
8
|
try {
|
|
@@ -19,6 +20,11 @@ export function parseConversationId(url) {
|
|
|
19
20
|
export async function readPageState(page) {
|
|
20
21
|
const rawUrl = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
|
|
21
22
|
const url = typeof rawUrl === "string" ? rawUrl : "";
|
|
23
|
+
// This is a file transfer endpoint, not a conversation surface. Keep the
|
|
24
|
+
// internal URL for origin checks without inspecting error-page content.
|
|
25
|
+
if (chatGPTAttachmentContextUrl(url) !== undefined) {
|
|
26
|
+
return { url, visibleText: "", signedIn: false };
|
|
27
|
+
}
|
|
22
28
|
const rawTitle = typeof page.title === "function" ? await page.title().catch(() => undefined) : undefined;
|
|
23
29
|
const title = typeof rawTitle === "string" ? rawTitle : undefined;
|
|
24
30
|
const surface = await readPageSurfaceSnapshot(page);
|
package/dist/src/client.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export type ChatGPTClientOptions = RuntimeEnv & {
|
|
|
38
38
|
*/
|
|
39
39
|
export type ChatGPTOperationsOptions = Readonly<{
|
|
40
40
|
stateRoot?: string;
|
|
41
|
+
/** Explicit private-file connection to a normal Node journal authority. */
|
|
42
|
+
journalService?: Readonly<{
|
|
43
|
+
descriptorPath: string;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
}>;
|
|
41
46
|
adapter?: OperationBrowserAdapter;
|
|
42
47
|
adapterFactory?: OperationAdapterFactory;
|
|
43
48
|
handleAdapterFactory?: OperationHandleAdapterFactory;
|
package/dist/src/client.js
CHANGED
|
@@ -23,7 +23,8 @@ import { streamFromRunResult } from "./runner/stream.js";
|
|
|
23
23
|
import { redactReportValue } from "./safety/report-redaction.js";
|
|
24
24
|
import { explainCommandBlocker } from "./diagnostics/blockers.js";
|
|
25
25
|
import { OperationClient, OperationClientError } from "./operations/client.js";
|
|
26
|
-
import { OperationJournal } from "./operations/journal.js";
|
|
26
|
+
import { JOURNAL_RUNTIME_UNAVAILABLE_MESSAGE, OperationJournal, OperationJournalError } from "./operations/journal.js";
|
|
27
|
+
import { createJournalRpcClientFromDescriptor } from "./operations/journal-rpc-client.js";
|
|
27
28
|
import { OperationService } from "./operations/service.js";
|
|
28
29
|
import { OPERATION_CONTROL_REQUEST_SCHEMA_VERSION, OPERATION_REQUEST_SCHEMA_VERSION } from "./operations/types.js";
|
|
29
30
|
import { createRuntimeEnvSession } from "./runtime/runtime-session.js";
|
|
@@ -634,7 +635,14 @@ function runtimeEnv(options) {
|
|
|
634
635
|
}
|
|
635
636
|
async function createOperationClientForChatGPT(options, runtimeEnvironment, owner) {
|
|
636
637
|
const operationOptions = options.operations ?? {};
|
|
637
|
-
|
|
638
|
+
if (operationOptions.journalService !== undefined && operationOptions.stateRoot !== undefined) {
|
|
639
|
+
throw new OperationJournalError("journal_service_configuration_conflict", "The journal service owns its state root; configure either journalService or stateRoot.");
|
|
640
|
+
}
|
|
641
|
+
const journal = operationOptions.journalService === undefined
|
|
642
|
+
? await OperationJournal.open(operationOptions.stateRoot === undefined ? {} : { stateRoot: operationOptions.stateRoot })
|
|
643
|
+
: await createJournalRpcClientFromDescriptor(operationOptions.journalService.descriptorPath, {
|
|
644
|
+
...(operationOptions.journalService.timeoutMs === undefined ? {} : { timeoutMs: operationOptions.journalService.timeoutMs })
|
|
645
|
+
});
|
|
638
646
|
const serviceOptions = {
|
|
639
647
|
...(operationOptions.maxCasRetries === undefined ? {} : { maxCasRetries: operationOptions.maxCasRetries }),
|
|
640
648
|
...(options.now === undefined ? {} : { now: () => options.now().getTime() })
|
|
@@ -1158,11 +1166,15 @@ function transactionalWorkBlockerResult(data, code, uncertain, recoverable) {
|
|
|
1158
1166
|
}
|
|
1159
1167
|
function transactionalWorkError(operationId, error, controlActionId, parentHandle) {
|
|
1160
1168
|
const code = safeOwnErrorCode(error) ?? "operation_error";
|
|
1161
|
-
const
|
|
1169
|
+
const runtimeUnavailable = code === "journal_runtime_unavailable";
|
|
1170
|
+
const serviceUnavailable = JOURNAL_SERVICE_FAILURE_CODES.has(code);
|
|
1171
|
+
const indeterminate = code === "journal_rpc_outcome_indeterminate";
|
|
1172
|
+
const message = runtimeUnavailable ? JOURNAL_RUNTIME_UNAVAILABLE_MESSAGE : serviceUnavailable ? journalServiceFailureMessage(code) : `Transactional Work operation failed (${code.replaceAll("_", " ")}).`;
|
|
1162
1173
|
const blocker = code === "adapter_unavailable"
|
|
1163
1174
|
|| code === "browser_bridge_unavailable"
|
|
1164
1175
|
|| code === "target_evidence_unavailable"
|
|
1165
|
-
|| code === "backend_unavailable"
|
|
1176
|
+
|| code === "backend_unavailable"
|
|
1177
|
+
|| runtimeUnavailable || serviceUnavailable;
|
|
1166
1178
|
const data = parentHandle === undefined
|
|
1167
1179
|
? {
|
|
1168
1180
|
operationId,
|
|
@@ -1177,11 +1189,21 @@ function transactionalWorkError(operationId, error, controlActionId, parentHandl
|
|
|
1177
1189
|
};
|
|
1178
1190
|
return {
|
|
1179
1191
|
ok: false,
|
|
1180
|
-
status: blocker ? "blocked" : "error",
|
|
1192
|
+
status: indeterminate ? "partial" : blocker ? "blocked" : "error",
|
|
1181
1193
|
data,
|
|
1182
1194
|
warnings: [],
|
|
1183
|
-
...(blocker ? { blocker: {
|
|
1184
|
-
|
|
1195
|
+
...(blocker ? { blocker: {
|
|
1196
|
+
kind: transactionalBlockerKind(code), code, message, resumable: !runtimeUnavailable && !serviceUnavailable,
|
|
1197
|
+
...(runtimeUnavailable ? { remediation: [{
|
|
1198
|
+
label: "Use a supported transactional host",
|
|
1199
|
+
instruction: "Start the packaged journal service in a supported Node host and configure operations.journalService.descriptorPath in the browser client.",
|
|
1200
|
+
userActionRequired: false
|
|
1201
|
+
}] } : serviceUnavailable ? { remediation: [{
|
|
1202
|
+
...journalServiceRemediation(code),
|
|
1203
|
+
userActionRequired: false
|
|
1204
|
+
}] } : {})
|
|
1205
|
+
} } : {}),
|
|
1206
|
+
error: { name: "OperationError", message, recoverable: blocker && !runtimeUnavailable && !serviceUnavailable },
|
|
1185
1207
|
context: { timestamp: new Date().toISOString(), experience: "work" }
|
|
1186
1208
|
};
|
|
1187
1209
|
}
|
|
@@ -1619,17 +1641,52 @@ function transactionalBlockerKind(code) {
|
|
|
1619
1641
|
return "upload_failed";
|
|
1620
1642
|
return "unknown";
|
|
1621
1643
|
}
|
|
1644
|
+
const JOURNAL_SERVICE_FAILURE_CODES = new Set([
|
|
1645
|
+
"journal_rpc_unavailable", "journal_rpc_authentication_failed", "journal_rpc_protocol_error",
|
|
1646
|
+
"journal_rpc_limit_exceeded", "journal_rpc_outcome_indeterminate", "journal_rpc_request_rejected",
|
|
1647
|
+
"journal_rpc_unsupported_platform"
|
|
1648
|
+
]);
|
|
1649
|
+
function journalServiceFailureMessage(code) {
|
|
1650
|
+
if (code === "journal_rpc_unsupported_platform") {
|
|
1651
|
+
return "The private-file journal service requires POSIX file ownership and permissions and is unavailable on Windows.";
|
|
1652
|
+
}
|
|
1653
|
+
return code === "journal_rpc_outcome_indeterminate"
|
|
1654
|
+
? "The journal service connection ended without confirming a durable write. Preserve the operation identity and reconcile its state before any further browser action."
|
|
1655
|
+
: "The configured journal service could not provide authenticated operation state. Restore its private connection before continuing.";
|
|
1656
|
+
}
|
|
1657
|
+
function journalServiceRemediation(code) {
|
|
1658
|
+
return code === "journal_rpc_unsupported_platform" ? {
|
|
1659
|
+
label: "Use a supported journal host",
|
|
1660
|
+
instruction: "On Windows, use the local journal in an ordinary Node browser host without operations.journalService. The private-file journal service requires a POSIX host. Preserve any existing operation identity."
|
|
1661
|
+
} : {
|
|
1662
|
+
label: "Reconnect the same journal",
|
|
1663
|
+
instruction: "Restore the private journal-service connection, then inspect or reconcile the same operation identity. Never create a new operation ID to retry an uncertain Send."
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1622
1666
|
function transactionalAskError(operationId, error) {
|
|
1623
1667
|
const code = safeOwnErrorCode(error) ?? "operation_error";
|
|
1624
|
-
const
|
|
1625
|
-
const
|
|
1668
|
+
const runtimeUnavailable = code === "journal_runtime_unavailable";
|
|
1669
|
+
const serviceUnavailable = JOURNAL_SERVICE_FAILURE_CODES.has(code);
|
|
1670
|
+
const indeterminate = code === "journal_rpc_outcome_indeterminate";
|
|
1671
|
+
const blocker = runtimeUnavailable || serviceUnavailable || code === "adapter_unavailable" || code === "browser_bridge_unavailable" || code === "target_evidence_unavailable";
|
|
1672
|
+
const message = runtimeUnavailable ? JOURNAL_RUNTIME_UNAVAILABLE_MESSAGE : serviceUnavailable ? journalServiceFailureMessage(code) : `Transactional operation failed (${code.replaceAll("_", " ")}).`;
|
|
1626
1673
|
return {
|
|
1627
1674
|
ok: false,
|
|
1628
|
-
status: blocker ? "blocked" : "error",
|
|
1675
|
+
status: indeterminate ? "partial" : blocker ? "blocked" : "error",
|
|
1629
1676
|
data: { operationId },
|
|
1630
1677
|
warnings: [],
|
|
1631
|
-
...(blocker ? { blocker: {
|
|
1632
|
-
|
|
1678
|
+
...(blocker ? { blocker: {
|
|
1679
|
+
kind: transactionalBlockerKind(code), code, message, resumable: !runtimeUnavailable && !serviceUnavailable,
|
|
1680
|
+
...(runtimeUnavailable ? { remediation: [{
|
|
1681
|
+
label: "Use a supported transactional host",
|
|
1682
|
+
instruction: "Start the packaged journal service in a supported Node host and configure operations.journalService.descriptorPath in the browser client.",
|
|
1683
|
+
userActionRequired: false
|
|
1684
|
+
}] } : serviceUnavailable ? { remediation: [{
|
|
1685
|
+
...journalServiceRemediation(code),
|
|
1686
|
+
userActionRequired: false
|
|
1687
|
+
}] } : {})
|
|
1688
|
+
} } : {}),
|
|
1689
|
+
error: { name: "OperationError", message, recoverable: blocker && !runtimeUnavailable && !serviceUnavailable },
|
|
1633
1690
|
context: { timestamp: new Date().toISOString() }
|
|
1634
1691
|
};
|
|
1635
1692
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
|
-
import { waitForDownloadFromClick } from "../browser/downloads.js";
|
|
3
|
+
import { isTerminalDownloadCode, isTerminalDownloadError, waitForDownloadFromClick } from "../browser/downloads.js";
|
|
4
4
|
import { readPageState } from "../browser/page-state.js";
|
|
5
5
|
import { listPageArtifacts, readLatestImageDataUrl } from "../dom/artifacts.js";
|
|
6
6
|
import { cssSelectors, requiredLocator } from "../dom/selectors.js";
|
|
7
7
|
import { localeLabels } from "../dom/locale-labels.js";
|
|
8
|
-
import { resultOk } from "../errors.js";
|
|
8
|
+
import { resultError, resultOk } from "../errors.js";
|
|
9
9
|
import { contextFromPage } from "./context.js";
|
|
10
10
|
import { ensurePage } from "./session.js";
|
|
11
11
|
import { localGuardTimeout, withTimeout } from "./timeouts.js";
|
|
@@ -112,7 +112,7 @@ export async function downloadLatestArtifact(env, args) {
|
|
|
112
112
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
113
113
|
if (args.prefer !== "visible_image_source") {
|
|
114
114
|
const byDownload = await tryDownloadControl(page, args, timeoutMs);
|
|
115
|
-
if (byDownload.ok || args.prefer === "download_control") {
|
|
115
|
+
if (byDownload.ok || isTerminalDownloadCode(byDownload.blocker?.code) || args.prefer === "download_control") {
|
|
116
116
|
return byDownload;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -156,7 +156,7 @@ async function tryDownloadControl(page, args, timeoutMs) {
|
|
|
156
156
|
return resultOk(downloaded, await contextFromPage(page));
|
|
157
157
|
}
|
|
158
158
|
catch (error) {
|
|
159
|
-
return artifactDownloadBlocker(error, await contextFromPage(page));
|
|
159
|
+
return artifactDownloadBlocker(error, await contextFromPage(page, {}, { minimal: isTerminalDownloadError(error) }));
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
async function saveLatestVisibleImageSource(page, destDir, timeoutMs) {
|
|
@@ -395,6 +395,8 @@ function artifactSelectorBlocker(error, context) {
|
|
|
395
395
|
};
|
|
396
396
|
}
|
|
397
397
|
function artifactDownloadBlocker(error, context) {
|
|
398
|
+
if (isTerminalDownloadError(error))
|
|
399
|
+
return resultError(error, context);
|
|
398
400
|
return {
|
|
399
401
|
ok: false,
|
|
400
402
|
status: "unsupported",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PageLike } from "../types.js";
|
|
2
|
+
export type ChatPopoverSnapshot = {
|
|
3
|
+
view: "simple" | "advanced";
|
|
4
|
+
rootIndex: number;
|
|
5
|
+
triggerId?: string;
|
|
6
|
+
toggleIndex: number;
|
|
7
|
+
modelOptions: Array<{
|
|
8
|
+
label: string;
|
|
9
|
+
checked: boolean;
|
|
10
|
+
index: number;
|
|
11
|
+
}>;
|
|
12
|
+
activeModel?: string;
|
|
13
|
+
effort?: string;
|
|
14
|
+
speed?: "Standard" | "Fast";
|
|
15
|
+
speedIndex?: number;
|
|
16
|
+
slider?: {
|
|
17
|
+
index: number;
|
|
18
|
+
minimum: number;
|
|
19
|
+
maximum: number;
|
|
20
|
+
current: number;
|
|
21
|
+
valueText?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/** Inactive model radios retain geometry. Require an owned, active, non-inert view. */
|
|
25
|
+
export declare function readChatPopover(page: PageLike): Promise<{
|
|
26
|
+
snapshot?: ChatPopoverSnapshot;
|
|
27
|
+
acted: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function closeChatPopover(page: PageLike, before?: ChatPopoverSnapshot): Promise<boolean>;
|
|
30
|
+
export declare function setChatPopoverView(page: PageLike, view: ChatPopoverSnapshot["view"]): Promise<ChatPopoverSnapshot | undefined>;
|
|
31
|
+
/** Opens the model view for evidence; does not probe slider values during inspection. */
|
|
32
|
+
export declare function inspectChatPopover(page: PageLike): Promise<ChatPopoverSnapshot | undefined>;
|
|
33
|
+
export declare function selectChatPopoverModel(page: PageLike, labels: string[]): Promise<string | undefined>;
|
|
34
|
+
/** Search the observed bounded range during mutation, restoring an unmatched search. */
|
|
35
|
+
export declare function selectChatPopoverEffort(page: PageLike, labels: string[]): Promise<string | undefined>;
|
|
36
|
+
/** The optional Work fast-mode checkbox is independent of model and effort. */
|
|
37
|
+
export declare function selectChatPopoverSpeed(page: PageLike, labels: string[]): Promise<string | undefined>;
|