libretto 0.6.10 → 0.6.12
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/README.md +4 -0
- package/README.template.md +4 -0
- package/dist/cli/cli.js +4 -3
- package/dist/cli/commands/ai.js +3 -2
- package/dist/cli/commands/browser.js +17 -17
- package/dist/cli/commands/execution.js +254 -234
- package/dist/cli/commands/experiments.js +100 -0
- package/dist/cli/commands/setup.js +20 -34
- package/dist/cli/commands/shared.js +10 -0
- package/dist/cli/commands/snapshot.js +81 -9
- package/dist/cli/commands/status.js +5 -4
- package/dist/cli/core/ai-model.js +6 -3
- package/dist/cli/core/browser.js +300 -121
- package/dist/cli/core/config.js +4 -2
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/config.js +0 -6
- package/dist/cli/core/daemon/daemon.js +535 -89
- package/dist/cli/core/daemon/ipc.js +170 -129
- package/dist/cli/core/daemon/snapshot.js +72 -6
- package/dist/cli/core/experiments.js +66 -0
- package/dist/cli/core/session.js +5 -4
- package/dist/cli/core/skill-version.js +2 -1
- package/dist/cli/core/snapshot-analyzer.js +4 -3
- package/dist/cli/core/workflow-runner/runner.js +147 -0
- package/dist/cli/core/workflow-runtime.js +60 -0
- package/dist/cli/router.js +4 -1
- package/dist/shared/debug/pause-handler.d.ts +9 -0
- package/dist/shared/debug/pause-handler.js +15 -0
- package/dist/shared/debug/pause.d.ts +1 -2
- package/dist/shared/debug/pause.js +13 -36
- package/dist/shared/ipc/child-process-transport.d.ts +7 -0
- package/dist/shared/ipc/child-process-transport.js +60 -0
- package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/child-process-transport.spec.js +68 -0
- package/dist/shared/ipc/ipc.d.ts +46 -0
- package/dist/shared/ipc/ipc.js +165 -0
- package/dist/shared/ipc/ipc.spec.d.ts +2 -0
- package/dist/shared/ipc/ipc.spec.js +114 -0
- package/dist/shared/ipc/socket-transport.d.ts +9 -0
- package/dist/shared/ipc/socket-transport.js +143 -0
- package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/socket-transport.spec.js +117 -0
- package/dist/shared/package-manager.d.ts +7 -0
- package/dist/shared/package-manager.js +60 -0
- package/dist/shared/paths/paths.d.ts +1 -8
- package/dist/shared/paths/paths.js +1 -49
- package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
- package/dist/shared/snapshot/capture-snapshot.js +463 -0
- package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
- package/dist/shared/snapshot/diff-snapshots.js +358 -0
- package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
- package/dist/shared/snapshot/render-snapshot.js +651 -0
- package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
- package/dist/shared/snapshot/snapshot.spec.js +333 -0
- package/dist/shared/snapshot/types.d.ts +40 -0
- package/dist/shared/snapshot/types.js +0 -0
- package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
- package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +1 -0
- package/docs/experiments.md +67 -0
- package/package.json +4 -2
- package/skills/libretto/SKILL.md +3 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/AGENTS.md +7 -0
- package/src/cli/cli.ts +4 -3
- package/src/cli/commands/ai.ts +3 -2
- package/src/cli/commands/browser.ts +13 -11
- package/src/cli/commands/execution.ts +303 -271
- package/src/cli/commands/experiments.ts +120 -0
- package/src/cli/commands/setup.ts +18 -36
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/snapshot.ts +99 -11
- package/src/cli/commands/status.ts +5 -4
- package/src/cli/core/ai-model.ts +6 -3
- package/src/cli/core/browser.ts +369 -147
- package/src/cli/core/config.ts +3 -1
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/config.ts +35 -19
- package/src/cli/core/daemon/daemon.ts +686 -106
- package/src/cli/core/daemon/ipc.ts +330 -214
- package/src/cli/core/daemon/snapshot.ts +106 -8
- package/src/cli/core/experiments.ts +85 -0
- package/src/cli/core/session.ts +5 -4
- package/src/cli/core/skill-version.ts +2 -1
- package/src/cli/core/snapshot-analyzer.ts +4 -3
- package/src/cli/core/workflow-runner/runner.ts +237 -0
- package/src/cli/core/workflow-runtime.ts +85 -0
- package/src/cli/router.ts +4 -1
- package/src/shared/debug/pause-handler.ts +20 -0
- package/src/shared/debug/pause.ts +14 -48
- package/src/shared/ipc/AGENTS.md +24 -0
- package/src/shared/ipc/child-process-transport.spec.ts +86 -0
- package/src/shared/ipc/child-process-transport.ts +96 -0
- package/src/shared/ipc/ipc.spec.ts +161 -0
- package/src/shared/ipc/ipc.ts +288 -0
- package/src/shared/ipc/socket-transport.spec.ts +141 -0
- package/src/shared/ipc/socket-transport.ts +189 -0
- package/src/shared/package-manager.ts +76 -0
- package/src/shared/paths/paths.ts +0 -72
- package/src/shared/snapshot/capture-snapshot.ts +615 -0
- package/src/shared/snapshot/diff-snapshots.ts +579 -0
- package/src/shared/snapshot/render-snapshot.ts +962 -0
- package/src/shared/snapshot/snapshot.spec.ts +388 -0
- package/src/shared/snapshot/types.ts +43 -0
- package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/daemon/index.js +0 -16
- package/dist/cli/core/daemon/spawn.js +0 -90
- package/dist/cli/core/pause-signals.js +0 -29
- package/dist/cli/workers/run-integration-runtime.js +0 -235
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
- package/dist/cli/workers/run-integration-worker.js +0 -64
- package/src/cli/core/daemon/index.ts +0 -24
- package/src/cli/core/daemon/spawn.ts +0 -171
- package/src/cli/core/pause-signals.ts +0 -35
- package/src/cli/workers/run-integration-runtime.ts +0 -326
- package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
- package/src/cli/workers/run-integration-worker.ts +0 -72
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
4
|
+
const DEFAULT_MUTATION_IDLE_MS = 400;
|
|
5
|
+
const DEFAULT_MINIMUM_WAIT_MS = 800;
|
|
6
|
+
const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
7
|
+
|
|
8
|
+
type LoadState = "domcontentloaded" | "load";
|
|
9
|
+
|
|
10
|
+
export type PageStabilityWaitOptions = {
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
mutationIdleMs?: number;
|
|
13
|
+
minimumWaitMs?: number;
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type PageStabilityWaitResult = {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
diagnostics: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type PageStabilityWaitArgs = Required<PageStabilityWaitOptions>;
|
|
23
|
+
|
|
24
|
+
type BrowserWaiterApi = {
|
|
25
|
+
waitForStability(args: PageStabilityWaitArgs): Promise<string | null>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function preparePageStabilityWait(
|
|
29
|
+
page: Page,
|
|
30
|
+
options: Pick<PageStabilityWaitOptions, "timeoutMs"> = {},
|
|
31
|
+
): Promise<PageStabilityWaitResult> {
|
|
32
|
+
const diagnostic = await installBrowserStabilityWaiterOnPage(
|
|
33
|
+
page,
|
|
34
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
35
|
+
);
|
|
36
|
+
return {
|
|
37
|
+
ok: diagnostic === null,
|
|
38
|
+
diagnostics: diagnostic === null ? [] : [diagnostic],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function waitForPageStable(
|
|
43
|
+
page: Page,
|
|
44
|
+
options: PageStabilityWaitOptions = {},
|
|
45
|
+
): Promise<PageStabilityWaitResult> {
|
|
46
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
47
|
+
const mutationIdleMs = options.mutationIdleMs ?? DEFAULT_MUTATION_IDLE_MS;
|
|
48
|
+
const minimumWaitMs = options.minimumWaitMs ?? DEFAULT_MINIMUM_WAIT_MS;
|
|
49
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
50
|
+
const deadline = Date.now() + timeoutMs;
|
|
51
|
+
|
|
52
|
+
const loadDiagnostics = await Promise.all([
|
|
53
|
+
waitForLoadState(page, "domcontentloaded", deadline),
|
|
54
|
+
waitForLoadState(page, "load", deadline),
|
|
55
|
+
]);
|
|
56
|
+
const browserDiagnostic = await waitForBrowserStability(page, {
|
|
57
|
+
timeoutMs: Math.max(0, deadline - Date.now()),
|
|
58
|
+
mutationIdleMs,
|
|
59
|
+
minimumWaitMs,
|
|
60
|
+
pollIntervalMs,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const diagnostics = [...loadDiagnostics, browserDiagnostic].filter(
|
|
64
|
+
(diagnostic): diagnostic is string => diagnostic !== null,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function waitForLoadState(
|
|
71
|
+
page: Page,
|
|
72
|
+
state: LoadState,
|
|
73
|
+
deadline: number,
|
|
74
|
+
): Promise<string | null> {
|
|
75
|
+
const timeout = Math.max(0, deadline - Date.now());
|
|
76
|
+
if (timeout === 0) return `Timed out waiting for ${state}`;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await page.waitForLoadState(state, { timeout });
|
|
80
|
+
return null;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return `Failed to wait for ${state}: ${errorMessage(error)}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function waitForBrowserStability(
|
|
87
|
+
page: Page,
|
|
88
|
+
args: PageStabilityWaitArgs,
|
|
89
|
+
): Promise<string | null> {
|
|
90
|
+
const deadline = Date.now() + args.timeoutMs;
|
|
91
|
+
let lastError: string | null = null;
|
|
92
|
+
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
const installDiagnostic = await installBrowserStabilityWaiterOnPage(
|
|
95
|
+
page,
|
|
96
|
+
Math.max(0, deadline - Date.now()),
|
|
97
|
+
);
|
|
98
|
+
if (installDiagnostic) return installDiagnostic;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
return await page.evaluate(runBrowserStabilityWait, {
|
|
102
|
+
...args,
|
|
103
|
+
timeoutMs: Math.max(0, deadline - Date.now()),
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
lastError = errorMessage(error);
|
|
107
|
+
if (!isRetryableExecutionContextError(lastError)) {
|
|
108
|
+
return `Failed to wait for page stability: ${lastError}`;
|
|
109
|
+
}
|
|
110
|
+
await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return lastError
|
|
115
|
+
? `Failed to wait for page stability: ${lastError}`
|
|
116
|
+
: "Timed out waiting for page stability";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function installBrowserStabilityWaiterOnPage(
|
|
120
|
+
page: Page,
|
|
121
|
+
timeoutMs: number,
|
|
122
|
+
): Promise<string | null> {
|
|
123
|
+
const deadline = Date.now() + timeoutMs;
|
|
124
|
+
let lastError: string | null = null;
|
|
125
|
+
|
|
126
|
+
while (Date.now() < deadline) {
|
|
127
|
+
try {
|
|
128
|
+
await page.evaluate(installPageStabilityWaiter);
|
|
129
|
+
return null;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
lastError = errorMessage(error);
|
|
132
|
+
if (!isRetryableExecutionContextError(lastError)) {
|
|
133
|
+
return `Failed to install page stability waiter: ${lastError}`;
|
|
134
|
+
}
|
|
135
|
+
await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return lastError
|
|
140
|
+
? `Failed to install page stability waiter: ${lastError}`
|
|
141
|
+
: "Timed out installing page stability waiter";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function installPageStabilityWaiter(): void {
|
|
145
|
+
const symbol = Symbol.for("libretto.pageStabilityWaiter");
|
|
146
|
+
const windowWithWaiter = window as Window & {
|
|
147
|
+
[symbol]?: BrowserWaiterApi;
|
|
148
|
+
};
|
|
149
|
+
if (windowWithWaiter[symbol]) return;
|
|
150
|
+
|
|
151
|
+
type WaiterState = {
|
|
152
|
+
pendingRequests: number;
|
|
153
|
+
pendingUrls: Set<string>;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
type ResourceSnapshot = {
|
|
157
|
+
pendingResources: number;
|
|
158
|
+
pendingResourceLabels: string[];
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const state: WaiterState = {
|
|
162
|
+
pendingRequests: 0,
|
|
163
|
+
pendingUrls: new Set<string>(),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const requestStarted = (url: string): void => {
|
|
167
|
+
state.pendingRequests += 1;
|
|
168
|
+
state.pendingUrls.add(url);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const requestFinished = (url: string): void => {
|
|
172
|
+
state.pendingRequests = Math.max(0, state.pendingRequests - 1);
|
|
173
|
+
state.pendingUrls.delete(url);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const originalFetch = window.fetch;
|
|
177
|
+
if (typeof originalFetch === "function") {
|
|
178
|
+
window.fetch = function trackedFetch(
|
|
179
|
+
this: Window,
|
|
180
|
+
input: RequestInfo | URL,
|
|
181
|
+
init?: RequestInit,
|
|
182
|
+
): Promise<Response> {
|
|
183
|
+
const url = requestUrl(input);
|
|
184
|
+
requestStarted(url);
|
|
185
|
+
return originalFetch.call(this, input, init).finally(() => {
|
|
186
|
+
requestFinished(url);
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
192
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
193
|
+
const requestUrls = new WeakMap<XMLHttpRequest, string>();
|
|
194
|
+
const startedRequests = new WeakSet<XMLHttpRequest>();
|
|
195
|
+
|
|
196
|
+
XMLHttpRequest.prototype.open = function trackedOpen(
|
|
197
|
+
this: XMLHttpRequest,
|
|
198
|
+
method: string,
|
|
199
|
+
url: string | URL,
|
|
200
|
+
async?: boolean,
|
|
201
|
+
username?: string | null,
|
|
202
|
+
password?: string | null,
|
|
203
|
+
): void {
|
|
204
|
+
requestUrls.set(this, String(url));
|
|
205
|
+
return originalOpen.call(
|
|
206
|
+
this,
|
|
207
|
+
method,
|
|
208
|
+
url,
|
|
209
|
+
async ?? true,
|
|
210
|
+
username,
|
|
211
|
+
password,
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
XMLHttpRequest.prototype.send = function trackedSend(
|
|
216
|
+
this: XMLHttpRequest,
|
|
217
|
+
body?: Document | XMLHttpRequestBodyInit | null,
|
|
218
|
+
): void {
|
|
219
|
+
const url = requestUrls.get(this) ?? "XMLHttpRequest";
|
|
220
|
+
requestStarted(url);
|
|
221
|
+
startedRequests.add(this);
|
|
222
|
+
|
|
223
|
+
const finish = (): void => {
|
|
224
|
+
if (!startedRequests.has(this)) return;
|
|
225
|
+
startedRequests.delete(this);
|
|
226
|
+
requestFinished(url);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
this.addEventListener("loadend", finish, { once: true });
|
|
230
|
+
try {
|
|
231
|
+
return originalSend.call(this, body);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
finish();
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const waitForStability = async (
|
|
239
|
+
args: PageStabilityWaitArgs,
|
|
240
|
+
): Promise<string | null> => {
|
|
241
|
+
const sleepInPage = (ms: number): Promise<void> =>
|
|
242
|
+
new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
243
|
+
|
|
244
|
+
const startedAt = Date.now();
|
|
245
|
+
let lastActivityAt = Date.now();
|
|
246
|
+
let lastResources: ResourceSnapshot = {
|
|
247
|
+
pendingResources: 0,
|
|
248
|
+
pendingResourceLabels: [],
|
|
249
|
+
};
|
|
250
|
+
let lastPendingRequests = state.pendingRequests;
|
|
251
|
+
let lastPendingUrls = [...state.pendingUrls];
|
|
252
|
+
|
|
253
|
+
const markActivity = (): void => {
|
|
254
|
+
lastActivityAt = Date.now();
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const observer = new MutationObserver(markActivity);
|
|
258
|
+
const root = document.documentElement ?? document.body;
|
|
259
|
+
if (root) {
|
|
260
|
+
observer.observe(root, {
|
|
261
|
+
attributes: true,
|
|
262
|
+
childList: true,
|
|
263
|
+
characterData: true,
|
|
264
|
+
subtree: true,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
while (Date.now() - startedAt < args.timeoutMs) {
|
|
270
|
+
lastResources = countPendingResourceElements();
|
|
271
|
+
lastPendingRequests = state.pendingRequests;
|
|
272
|
+
lastPendingUrls = [...state.pendingUrls];
|
|
273
|
+
const pageLoaded = document.readyState === "complete";
|
|
274
|
+
const mutationIdle = Date.now() - lastActivityAt >= args.mutationIdleMs;
|
|
275
|
+
const waitedLongEnough = Date.now() - startedAt >= args.minimumWaitMs;
|
|
276
|
+
const requestIdle = lastPendingRequests === 0;
|
|
277
|
+
const resourceIdle = lastResources.pendingResources === 0;
|
|
278
|
+
|
|
279
|
+
if (
|
|
280
|
+
pageLoaded &&
|
|
281
|
+
mutationIdle &&
|
|
282
|
+
waitedLongEnough &&
|
|
283
|
+
requestIdle &&
|
|
284
|
+
resourceIdle
|
|
285
|
+
) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await sleepInPage(args.pollIntervalMs);
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
observer.disconnect();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return formatStabilityTimeout({
|
|
296
|
+
timeoutMs: args.timeoutMs,
|
|
297
|
+
readyState: document.readyState,
|
|
298
|
+
pendingRequests: lastPendingRequests,
|
|
299
|
+
pendingUrls: lastPendingUrls,
|
|
300
|
+
pendingResources: lastResources.pendingResources,
|
|
301
|
+
pendingResourceLabels: lastResources.pendingResourceLabels,
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
Object.defineProperty(windowWithWaiter, symbol, {
|
|
306
|
+
value: { waitForStability } satisfies BrowserWaiterApi,
|
|
307
|
+
configurable: false,
|
|
308
|
+
enumerable: false,
|
|
309
|
+
writable: false,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
function countPendingResourceElements(): ResourceSnapshot {
|
|
313
|
+
const elements = Array.from(
|
|
314
|
+
document.querySelectorAll(
|
|
315
|
+
'img,video,audio,embed,object,iframe[src],link[rel="stylesheet"][href]',
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
let pendingResources = 0;
|
|
319
|
+
const pendingResourceLabels: string[] = [];
|
|
320
|
+
|
|
321
|
+
const markPending = (element: Element): void => {
|
|
322
|
+
pendingResources += 1;
|
|
323
|
+
if (pendingResourceLabels.length < 5) {
|
|
324
|
+
pendingResourceLabels.push(resourceLabel(element));
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
for (const element of elements) {
|
|
329
|
+
const tagName = element.tagName.toLowerCase();
|
|
330
|
+
if (tagName === "img") {
|
|
331
|
+
const image = element as HTMLImageElement;
|
|
332
|
+
if (image.loading !== "lazy" && !image.complete) markPending(element);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (tagName === "video" || tagName === "audio") {
|
|
337
|
+
const media = element as HTMLMediaElement;
|
|
338
|
+
if (media.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
339
|
+
markPending(element);
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (tagName === "iframe") {
|
|
345
|
+
const iframe = element as HTMLIFrameElement;
|
|
346
|
+
try {
|
|
347
|
+
if (
|
|
348
|
+
iframe.contentDocument &&
|
|
349
|
+
iframe.contentDocument.readyState !== "complete"
|
|
350
|
+
) {
|
|
351
|
+
markPending(element);
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
// Cross-origin iframes cannot be inspected; treat them as settled.
|
|
355
|
+
}
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (tagName === "link") {
|
|
360
|
+
const link = element as HTMLLinkElement;
|
|
361
|
+
if (!link.sheet) markPending(element);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { pendingResources, pendingResourceLabels };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function requestUrl(input: RequestInfo | URL): string {
|
|
369
|
+
if (typeof input === "string") return input;
|
|
370
|
+
if (input instanceof URL) return input.href;
|
|
371
|
+
return input.url;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function resourceLabel(element: Element): string {
|
|
375
|
+
const tagName = element.tagName.toLowerCase();
|
|
376
|
+
const source =
|
|
377
|
+
element.getAttribute("src") ?? element.getAttribute("href") ?? "";
|
|
378
|
+
return source ? `${tagName}:${source}` : tagName;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function formatStabilityTimeout(args: {
|
|
382
|
+
timeoutMs: number;
|
|
383
|
+
readyState: DocumentReadyState;
|
|
384
|
+
pendingRequests: number;
|
|
385
|
+
pendingUrls: string[];
|
|
386
|
+
pendingResources: number;
|
|
387
|
+
pendingResourceLabels: string[];
|
|
388
|
+
}): string {
|
|
389
|
+
const urls = args.pendingUrls.slice(0, 5).join(", ");
|
|
390
|
+
const resources = args.pendingResourceLabels.join(", ");
|
|
391
|
+
return (
|
|
392
|
+
`Timed out waiting for page stability after ${args.timeoutMs}ms ` +
|
|
393
|
+
`(readyState=${args.readyState}, pendingRequests=${args.pendingRequests}` +
|
|
394
|
+
`${urls ? `, pendingUrls=${urls}` : ""}, pendingResources=${args.pendingResources}` +
|
|
395
|
+
`${resources ? `, pendingResourceLabels=${resources}` : ""})`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function runBrowserStabilityWait(
|
|
401
|
+
args: PageStabilityWaitArgs,
|
|
402
|
+
): Promise<string | null> {
|
|
403
|
+
const symbol = Symbol.for("libretto.pageStabilityWaiter");
|
|
404
|
+
const waiter = (
|
|
405
|
+
window as unknown as Window & Record<symbol, BrowserWaiterApi | undefined>
|
|
406
|
+
)[symbol];
|
|
407
|
+
if (!waiter) return "Page stability waiter was not installed.";
|
|
408
|
+
return waiter.waitForStability(args);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function sleep(ms: number): Promise<void> {
|
|
412
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function errorMessage(error: unknown): string {
|
|
416
|
+
return error instanceof Error ? error.message : String(error);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isRetryableExecutionContextError(message: string): boolean {
|
|
420
|
+
return (
|
|
421
|
+
message.includes("Execution context was destroyed") ||
|
|
422
|
+
message.includes("Cannot find context with specified id") ||
|
|
423
|
+
message.includes("Most likely the page has been closed")
|
|
424
|
+
);
|
|
425
|
+
}
|
|
@@ -31,6 +31,7 @@ export const SessionStateFileSchema = z.object({
|
|
|
31
31
|
status: SessionStatusSchema.optional(),
|
|
32
32
|
mode: SessionAccessModeSchema.default("write-access"),
|
|
33
33
|
viewport: SessionViewportSchema.optional(),
|
|
34
|
+
stayOpenOnSuccess: z.boolean().optional(),
|
|
34
35
|
provider: ProviderStateSchema.optional(),
|
|
35
36
|
daemonSocketPath: z.string().optional(),
|
|
36
37
|
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DaemonServer,
|
|
3
|
-
DaemonClient,
|
|
4
|
-
DaemonClientError,
|
|
5
|
-
getDaemonSocketPath
|
|
6
|
-
} from "./ipc.js";
|
|
7
|
-
import {
|
|
8
|
-
spawnSessionDaemon
|
|
9
|
-
} from "./spawn.js";
|
|
10
|
-
export {
|
|
11
|
-
DaemonClient,
|
|
12
|
-
DaemonClientError,
|
|
13
|
-
DaemonServer,
|
|
14
|
-
getDaemonSocketPath,
|
|
15
|
-
spawnSessionDaemon
|
|
16
|
-
};
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { openSync, closeSync } from "node:fs";
|
|
2
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { getDaemonSocketPath } from "./ipc.js";
|
|
6
|
-
import { DaemonClient } from "./ipc.js";
|
|
7
|
-
const DEFAULT_IPC_TIMEOUT_MS = 1e4;
|
|
8
|
-
const IPC_POLL_INTERVAL_MS = 250;
|
|
9
|
-
async function spawnSessionDaemon(options) {
|
|
10
|
-
const {
|
|
11
|
-
config,
|
|
12
|
-
session,
|
|
13
|
-
logger,
|
|
14
|
-
logPath,
|
|
15
|
-
ipcTimeoutMs = DEFAULT_IPC_TIMEOUT_MS,
|
|
16
|
-
onFailure
|
|
17
|
-
} = options;
|
|
18
|
-
const daemonEntryPath = fileURLToPath(
|
|
19
|
-
new URL("./daemon.js", import.meta.url)
|
|
20
|
-
);
|
|
21
|
-
const require2 = createRequire(import.meta.url);
|
|
22
|
-
const tsxImportPath = pathToFileURL(require2.resolve("tsx/esm")).href;
|
|
23
|
-
const childStderrFd = openSync(logPath, "a");
|
|
24
|
-
const child = spawn(
|
|
25
|
-
process.execPath,
|
|
26
|
-
["--import", tsxImportPath, daemonEntryPath, JSON.stringify(config)],
|
|
27
|
-
{
|
|
28
|
-
detached: true,
|
|
29
|
-
stdio: ["ignore", "ignore", childStderrFd]
|
|
30
|
-
}
|
|
31
|
-
);
|
|
32
|
-
child.unref();
|
|
33
|
-
closeSync(childStderrFd);
|
|
34
|
-
const pid = child.pid;
|
|
35
|
-
logger.info("daemon-spawned", { pid, session });
|
|
36
|
-
let childSpawnError = null;
|
|
37
|
-
let childEarlyExit = null;
|
|
38
|
-
child.on("error", (err) => {
|
|
39
|
-
childSpawnError = err;
|
|
40
|
-
logger.error("daemon-spawn-error", { error: err, session });
|
|
41
|
-
});
|
|
42
|
-
child.on("exit", (code, signal) => {
|
|
43
|
-
childEarlyExit = { code, signal };
|
|
44
|
-
logger.warn("daemon-early-exit", { code, signal, session, pid });
|
|
45
|
-
});
|
|
46
|
-
const socketPath = getDaemonSocketPath(session);
|
|
47
|
-
const client = new DaemonClient(socketPath);
|
|
48
|
-
const maxAttempts = Math.ceil(ipcTimeoutMs / IPC_POLL_INTERVAL_MS);
|
|
49
|
-
let ipcReady = false;
|
|
50
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
51
|
-
const spawnError = childSpawnError;
|
|
52
|
-
if (spawnError !== null) {
|
|
53
|
-
await onFailure?.();
|
|
54
|
-
const errWithCode = spawnError;
|
|
55
|
-
const hint = errWithCode.code === "ENOENT" ? " Ensure Node.js is available in PATH for child processes." : "";
|
|
56
|
-
throw new Error(
|
|
57
|
-
`Failed to spawn daemon: ${spawnError.message}.${hint} Check logs: ${logPath}`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
const earlyExit = childEarlyExit;
|
|
61
|
-
if (earlyExit !== null) {
|
|
62
|
-
await onFailure?.();
|
|
63
|
-
const status = earlyExit.code ?? earlyExit.signal ?? "unknown";
|
|
64
|
-
throw new Error(
|
|
65
|
-
`Daemon exited before startup (status: ${status}). Check logs: ${logPath}`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
await new Promise((r) => setTimeout(r, IPC_POLL_INTERVAL_MS));
|
|
69
|
-
ipcReady = await client.ping();
|
|
70
|
-
if (ipcReady) break;
|
|
71
|
-
if (i > 0 && i % 10 === 0) {
|
|
72
|
-
logger.info("daemon-waiting-for-ipc", { attempt: i, session });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (!ipcReady) {
|
|
76
|
-
try {
|
|
77
|
-
process.kill(pid, "SIGTERM");
|
|
78
|
-
} catch {
|
|
79
|
-
}
|
|
80
|
-
await onFailure?.();
|
|
81
|
-
throw new Error(
|
|
82
|
-
`Daemon failed to start within ${Math.ceil(ipcTimeoutMs / 1e3)}s. Check logs: ${logPath}`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
logger.info("daemon-ipc-ready", { session, socketPath });
|
|
86
|
-
return { pid, socketPath, client };
|
|
87
|
-
}
|
|
88
|
-
export {
|
|
89
|
-
spawnSessionDaemon
|
|
90
|
-
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { unlink } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { getSessionDir } from "./context.js";
|
|
5
|
-
function getPauseSignalPaths(session) {
|
|
6
|
-
const sessionDir = getSessionDir(session);
|
|
7
|
-
return {
|
|
8
|
-
pausedSignalPath: join(sessionDir, `${session}.paused`),
|
|
9
|
-
resumeSignalPath: join(sessionDir, `${session}.resume`),
|
|
10
|
-
completedSignalPath: join(sessionDir, `${session}.completed`),
|
|
11
|
-
failedSignalPath: join(sessionDir, `${session}.failed`),
|
|
12
|
-
outputSignalPath: join(sessionDir, `${session}.output`)
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
async function removeSignalIfExists(path) {
|
|
16
|
-
if (!existsSync(path)) return;
|
|
17
|
-
try {
|
|
18
|
-
await unlink(path);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
const code = error.code;
|
|
21
|
-
if (code !== "ENOENT") {
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
export {
|
|
27
|
-
getPauseSignalPaths,
|
|
28
|
-
removeSignalIfExists
|
|
29
|
-
};
|