@steipete/oracle 0.8.5 → 0.8.6
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 +23 -1
- package/dist/bin/oracle-cli.js +189 -7
- package/dist/src/browser/actions/assistantResponse.js +72 -37
- package/dist/src/browser/actions/promptComposer.js +141 -32
- package/dist/src/browser/chromeLifecycle.js +25 -9
- package/dist/src/browser/config.js +14 -0
- package/dist/src/browser/index.js +341 -24
- package/dist/src/browser/profileState.js +93 -0
- package/dist/src/cli/browserConfig.js +21 -0
- package/dist/src/cli/browserDefaults.js +21 -0
- package/dist/src/cli/sessionRunner.js +149 -0
- package/dist/src/cli/tui/index.js +1 -0
- package/dist/src/mcp/tools/consult.js +1 -0
- package/dist/src/oracle/modelResolver.js +33 -1
- package/dist/src/sessionManager.js +9 -2
- package/dist/src/sessionStore.js +2 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +19 -19
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ npx -y @steipete/oracle --engine browser --model gemini-3-pro --prompt "a cute r
|
|
|
42
42
|
# Sessions (list and replay)
|
|
43
43
|
npx -y @steipete/oracle status --hours 72
|
|
44
44
|
npx -y @steipete/oracle session <id> --render
|
|
45
|
+
npx -y @steipete/oracle restart <id>
|
|
45
46
|
|
|
46
47
|
# TUI (interactive, only for humans)
|
|
47
48
|
npx -y @steipete/oracle tui
|
|
@@ -100,6 +101,24 @@ npx -y @steipete/oracle oracle-mcp
|
|
|
100
101
|
- Sessions you can replay (`oracle status`, `oracle session <id> --render`).
|
|
101
102
|
- Session logs and bundles live in `~/.oracle/sessions` (override with `ORACLE_HOME_DIR`).
|
|
102
103
|
|
|
104
|
+
## Browser auto-reattach (long Pro runs)
|
|
105
|
+
|
|
106
|
+
When browser runs time out (common with long GPT‑5.x Pro responses), Oracle can keep polling the existing ChatGPT tab and capture the final answer without manual `oracle session <id>` commands.
|
|
107
|
+
|
|
108
|
+
Enable auto-reattach by setting a non-zero interval:
|
|
109
|
+
- `--browser-auto-reattach-delay` — wait before the first retry (e.g. `30s`)
|
|
110
|
+
- `--browser-auto-reattach-interval` — how often to retry (e.g. `2m`)
|
|
111
|
+
- `--browser-auto-reattach-timeout` — per-attempt budget (default `2m`)
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
oracle --engine browser \
|
|
115
|
+
--browser-timeout 6m \
|
|
116
|
+
--browser-auto-reattach-delay 30s \
|
|
117
|
+
--browser-auto-reattach-interval 2m \
|
|
118
|
+
--browser-auto-reattach-timeout 2m \
|
|
119
|
+
-p "Run the long UI audit" --file "src/**/*.ts"
|
|
120
|
+
```
|
|
121
|
+
|
|
103
122
|
## Flags you’ll actually use
|
|
104
123
|
|
|
105
124
|
| Flag | Purpose |
|
|
@@ -117,6 +136,9 @@ npx -y @steipete/oracle oracle-mcp
|
|
|
117
136
|
| `--browser-port <port>` | Pin the Chrome DevTools port (WSL/Windows firewall helper). |
|
|
118
137
|
| `--browser-inline-cookies[(-file)] <payload|path>` | Supply cookies without Chrome/Keychain (browser). |
|
|
119
138
|
| `--browser-timeout`, `--browser-input-timeout` | Control overall/browser input timeouts (supports h/m/s/ms). |
|
|
139
|
+
| `--browser-recheck-delay`, `--browser-recheck-timeout` | Delayed recheck for long Pro runs: wait then retry capture after timeout (supports h/m/s/ms). |
|
|
140
|
+
| `--browser-reuse-wait` | Wait for a shared Chrome profile before launching (parallel browser runs). |
|
|
141
|
+
| `--browser-profile-lock-timeout` | Wait for the shared manual-login profile lock before sending (serializes parallel runs). |
|
|
120
142
|
| `--render`, `--copy` | Print and/or copy the assembled markdown bundle. |
|
|
121
143
|
| `--wait` | Block for background API runs (e.g., GPT‑5.1 Pro) instead of detaching. |
|
|
122
144
|
| `--timeout <seconds\|auto>` | Overall API deadline (auto = 60m for pro, 120s otherwise). |
|
|
@@ -154,7 +176,7 @@ Advanced flags
|
|
|
154
176
|
|
|
155
177
|
| Area | Flags |
|
|
156
178
|
| --- | --- |
|
|
157
|
-
| Browser | `--browser-manual-login`, `--browser-thinking-time`, `--browser-timeout`, `--browser-input-timeout`, `--browser-cookie-wait`, `--browser-inline-cookies[(-file)]`, `--browser-attachments`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
|
|
179
|
+
| Browser | `--browser-manual-login`, `--browser-thinking-time`, `--browser-timeout`, `--browser-input-timeout`, `--browser-recheck-delay`, `--browser-recheck-timeout`, `--browser-reuse-wait`, `--browser-profile-lock-timeout`, `--browser-auto-reattach-delay`, `--browser-auto-reattach-interval`, `--browser-auto-reattach-timeout`, `--browser-cookie-wait`, `--browser-inline-cookies[(-file)]`, `--browser-attachments`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
|
|
158
180
|
| Run control | `--background`, `--no-background`, `--http-timeout`, `--zombie-timeout`, `--zombie-last-activity` |
|
|
159
181
|
| Azure/OpenAI | `--azure-endpoint`, `--azure-deployment`, `--azure-api-version`, `--base-url` |
|
|
160
182
|
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -183,7 +183,14 @@ program
|
|
|
183
183
|
.addOption(new Option('--chatgpt-url <url>', `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`))
|
|
184
184
|
.addOption(new Option('--browser-url <url>', `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
|
|
185
185
|
.addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 1200s / 20m).').hideHelp())
|
|
186
|
-
.addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default
|
|
186
|
+
.addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 60s).').hideHelp())
|
|
187
|
+
.addOption(new Option('--browser-recheck-delay <ms|s|m|h>', 'After an assistant timeout, wait this long then revisit the conversation to retry capture.').hideHelp())
|
|
188
|
+
.addOption(new Option('--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt (default 120s).').hideHelp())
|
|
189
|
+
.addOption(new Option('--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile to appear before launching a new one (helps parallel runs).').hideHelp())
|
|
190
|
+
.addOption(new Option('--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the shared manual-login profile lock before sending (serializes parallel runs).').hideHelp())
|
|
191
|
+
.addOption(new Option('--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before starting periodic auto-reattach attempts after a timeout.').hideHelp())
|
|
192
|
+
.addOption(new Option('--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).').hideHelp())
|
|
193
|
+
.addOption(new Option('--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt (default 120s).').hideHelp())
|
|
187
194
|
.addOption(new Option('--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.').hideHelp())
|
|
188
195
|
.addOption(new Option('--browser-port <port>', 'Use a fixed Chrome DevTools port (helpful on WSL firewalls).')
|
|
189
196
|
.argParser(parseIntOption))
|
|
@@ -379,6 +386,17 @@ const statusCommand = program
|
|
|
379
386
|
showExamples,
|
|
380
387
|
});
|
|
381
388
|
});
|
|
389
|
+
program
|
|
390
|
+
.command('restart <id>')
|
|
391
|
+
.description('Re-run a stored session as a new session (clones options).')
|
|
392
|
+
.addOption(new Option('--wait').default(undefined))
|
|
393
|
+
.addOption(new Option('--no-wait').default(undefined).hideHelp())
|
|
394
|
+
.option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.')
|
|
395
|
+
.option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.')
|
|
396
|
+
.action(async (sessionId, _options, cmd) => {
|
|
397
|
+
const restartOptions = cmd.opts();
|
|
398
|
+
await restartSession(sessionId, restartOptions);
|
|
399
|
+
});
|
|
382
400
|
function buildRunOptions(options, overrides = {}) {
|
|
383
401
|
if (!options.prompt) {
|
|
384
402
|
throw new Error('Prompt is required.');
|
|
@@ -665,11 +683,10 @@ async function runRootCommand(options) {
|
|
|
665
683
|
// - otherwise block for fast models (gpt-5.1, browser) and detach by default for pro API runs
|
|
666
684
|
let waitPreference = resolveWaitFlag({
|
|
667
685
|
waitFlag: options.wait,
|
|
668
|
-
noWaitFlag: options.noWait,
|
|
669
686
|
model: resolvedModel,
|
|
670
687
|
engine,
|
|
671
688
|
});
|
|
672
|
-
if (remoteHost &&
|
|
689
|
+
if (remoteHost && waitPreference === false) {
|
|
673
690
|
console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
|
|
674
691
|
waitPreference = true;
|
|
675
692
|
}
|
|
@@ -857,6 +874,13 @@ async function runRootCommand(options) {
|
|
|
857
874
|
...baseRunOptions,
|
|
858
875
|
mode: sessionMode,
|
|
859
876
|
browserConfig,
|
|
877
|
+
waitPreference,
|
|
878
|
+
youtube: options.youtube,
|
|
879
|
+
generateImage: options.generateImage,
|
|
880
|
+
editImage: options.editImage,
|
|
881
|
+
outputPath: options.output,
|
|
882
|
+
aspectRatio: options.aspect,
|
|
883
|
+
geminiShowThoughts: options.geminiShowThoughts,
|
|
860
884
|
}, process.cwd(), notifications);
|
|
861
885
|
const liveRunOptions = {
|
|
862
886
|
...baseRunOptions,
|
|
@@ -898,7 +922,7 @@ async function runRootCommand(options) {
|
|
|
898
922
|
await attachSession(sessionMeta.id, { suppressMetadata: true });
|
|
899
923
|
}
|
|
900
924
|
}
|
|
901
|
-
async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps) {
|
|
925
|
+
async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps, cwd = process.cwd()) {
|
|
902
926
|
const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
|
|
903
927
|
let headerAugmented = false;
|
|
904
928
|
const combinedLog = (message = '') => {
|
|
@@ -927,7 +951,7 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
|
|
|
927
951
|
runOptions,
|
|
928
952
|
mode,
|
|
929
953
|
browserConfig,
|
|
930
|
-
cwd
|
|
954
|
+
cwd,
|
|
931
955
|
log: combinedLog,
|
|
932
956
|
write: combinedWrite,
|
|
933
957
|
version: VERSION,
|
|
@@ -970,6 +994,140 @@ async function launchDetachedSession(sessionId) {
|
|
|
970
994
|
}
|
|
971
995
|
});
|
|
972
996
|
}
|
|
997
|
+
async function restartSession(sessionId, options) {
|
|
998
|
+
const metadata = await sessionStore.readSession(sessionId);
|
|
999
|
+
if (!metadata) {
|
|
1000
|
+
console.error(chalk.red(`No session found with ID ${sessionId}`));
|
|
1001
|
+
process.exitCode = 1;
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const runOptions = buildRunOptionsFromMetadata(metadata);
|
|
1005
|
+
if (!runOptions.prompt) {
|
|
1006
|
+
console.error(chalk.red(`Session ${sessionId} has no stored prompt; cannot restart.`));
|
|
1007
|
+
process.exitCode = 1;
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const sessionMode = getSessionMode(metadata);
|
|
1011
|
+
const engine = sessionMode === 'browser' ? 'browser' : 'api';
|
|
1012
|
+
const browserConfig = getBrowserConfigFromMetadata(metadata);
|
|
1013
|
+
if (sessionMode === 'browser' && !browserConfig) {
|
|
1014
|
+
console.error(chalk.red(`Session ${sessionId} is missing browser config; cannot restart.`));
|
|
1015
|
+
process.exitCode = 1;
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const userConfig = (await loadUserConfig()).config;
|
|
1019
|
+
const cwd = metadata.cwd ?? process.cwd();
|
|
1020
|
+
const storedOptions = metadata.options ?? {};
|
|
1021
|
+
if (runOptions.file && runOptions.file.length > 0) {
|
|
1022
|
+
const isBrowserMode = engine === 'browser';
|
|
1023
|
+
const filesToValidate = isBrowserMode ? runOptions.file.filter((f) => !isMediaFile(f)) : runOptions.file;
|
|
1024
|
+
if (filesToValidate.length > 0) {
|
|
1025
|
+
await readFiles(filesToValidate, { cwd });
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
enforceBrowserSearchFlag(runOptions, sessionMode, console.log);
|
|
1029
|
+
let waitPreference = resolveRestartWaitPreference({
|
|
1030
|
+
waitFlag: options.wait,
|
|
1031
|
+
storedPreference: storedOptions.waitPreference,
|
|
1032
|
+
model: runOptions.model,
|
|
1033
|
+
engine,
|
|
1034
|
+
});
|
|
1035
|
+
const remoteConfig = resolveRemoteServiceConfig({
|
|
1036
|
+
cliHost: options.remoteHost,
|
|
1037
|
+
cliToken: options.remoteToken,
|
|
1038
|
+
userConfig,
|
|
1039
|
+
env: process.env,
|
|
1040
|
+
});
|
|
1041
|
+
const remoteHost = remoteConfig.host;
|
|
1042
|
+
const remoteToken = remoteConfig.token;
|
|
1043
|
+
if (remoteHost && engine !== 'browser') {
|
|
1044
|
+
throw new Error('--remote-host requires a browser session.');
|
|
1045
|
+
}
|
|
1046
|
+
if (remoteHost) {
|
|
1047
|
+
console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
|
|
1048
|
+
}
|
|
1049
|
+
if (remoteHost && waitPreference === false) {
|
|
1050
|
+
console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
|
|
1051
|
+
waitPreference = true;
|
|
1052
|
+
}
|
|
1053
|
+
let browserDeps;
|
|
1054
|
+
if (browserConfig && remoteHost) {
|
|
1055
|
+
browserDeps = {
|
|
1056
|
+
executeBrowser: createRemoteBrowserExecutor({ host: remoteHost, token: remoteToken }),
|
|
1057
|
+
};
|
|
1058
|
+
console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
|
|
1059
|
+
}
|
|
1060
|
+
else if (browserConfig && runOptions.model.startsWith('gemini')) {
|
|
1061
|
+
browserDeps = {
|
|
1062
|
+
executeBrowser: createGeminiWebExecutor({
|
|
1063
|
+
youtube: storedOptions.youtube,
|
|
1064
|
+
generateImage: storedOptions.generateImage,
|
|
1065
|
+
editImage: storedOptions.editImage,
|
|
1066
|
+
outputPath: storedOptions.outputPath,
|
|
1067
|
+
aspectRatio: storedOptions.aspectRatio,
|
|
1068
|
+
showThoughts: storedOptions.geminiShowThoughts,
|
|
1069
|
+
}),
|
|
1070
|
+
};
|
|
1071
|
+
console.log(chalk.dim('Using Gemini web client for browser automation'));
|
|
1072
|
+
if (browserConfig.modelStrategy && browserConfig.modelStrategy !== 'select') {
|
|
1073
|
+
console.log(chalk.dim('Browser model strategy is ignored for Gemini web runs.'));
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const remoteExecutionActive = Boolean(browserDeps);
|
|
1077
|
+
await sessionStore.ensureStorage();
|
|
1078
|
+
const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
|
|
1079
|
+
const sessionMeta = await sessionStore.createSession({
|
|
1080
|
+
...runOptions,
|
|
1081
|
+
mode: sessionMode,
|
|
1082
|
+
browserConfig,
|
|
1083
|
+
waitPreference,
|
|
1084
|
+
youtube: storedOptions.youtube,
|
|
1085
|
+
generateImage: storedOptions.generateImage,
|
|
1086
|
+
editImage: storedOptions.editImage,
|
|
1087
|
+
outputPath: storedOptions.outputPath,
|
|
1088
|
+
aspectRatio: storedOptions.aspectRatio,
|
|
1089
|
+
geminiShowThoughts: storedOptions.geminiShowThoughts,
|
|
1090
|
+
}, cwd, notifications, sessionId);
|
|
1091
|
+
const liveRunOptions = {
|
|
1092
|
+
...runOptions,
|
|
1093
|
+
sessionId: sessionMeta.id,
|
|
1094
|
+
effectiveModelId: resolveEffectiveModelIdForRun(runOptions.model, runOptions.effectiveModelId),
|
|
1095
|
+
};
|
|
1096
|
+
const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
|
|
1097
|
+
const detachAllowed = remoteExecutionActive
|
|
1098
|
+
? false
|
|
1099
|
+
: shouldDetachSession({
|
|
1100
|
+
engine,
|
|
1101
|
+
model: runOptions.model,
|
|
1102
|
+
waitPreference,
|
|
1103
|
+
disableDetachEnv,
|
|
1104
|
+
});
|
|
1105
|
+
const detached = !detachAllowed
|
|
1106
|
+
? false
|
|
1107
|
+
: await launchDetachedSession(sessionMeta.id).catch((error) => {
|
|
1108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1109
|
+
console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
|
|
1110
|
+
return false;
|
|
1111
|
+
});
|
|
1112
|
+
if (!waitPreference) {
|
|
1113
|
+
if (!detached) {
|
|
1114
|
+
console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
|
|
1115
|
+
process.exitCode = 1;
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
|
|
1119
|
+
console.log(chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'));
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (detached === false) {
|
|
1123
|
+
await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps, cwd);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (detached) {
|
|
1127
|
+
console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
|
|
1128
|
+
await attachSession(sessionMeta.id, { suppressMetadata: true });
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
973
1131
|
async function executeSession(sessionId) {
|
|
974
1132
|
const metadata = await sessionStore.readSession(sessionId);
|
|
975
1133
|
if (!metadata) {
|
|
@@ -1020,6 +1178,13 @@ function printDebugHelp(cliName) {
|
|
|
1020
1178
|
['--browser-url <url>', 'Alias for --chatgpt-url.'],
|
|
1021
1179
|
['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
|
|
1022
1180
|
['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
|
|
1181
|
+
['--browser-recheck-delay <ms|s|m|h>', 'After timeout, wait then revisit the conversation to retry capture.'],
|
|
1182
|
+
['--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt.'],
|
|
1183
|
+
['--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile before launching (parallel runs).'],
|
|
1184
|
+
['--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the manual-login profile lock before sending.'],
|
|
1185
|
+
['--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before periodic auto-reattach attempts after a timeout.'],
|
|
1186
|
+
['--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).'],
|
|
1187
|
+
['--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt.'],
|
|
1023
1188
|
['--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.'],
|
|
1024
1189
|
['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
|
|
1025
1190
|
['--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and log in manually.'],
|
|
@@ -1037,13 +1202,30 @@ function printDebugOptionGroup(entries) {
|
|
|
1037
1202
|
console.log(` ${label}${description}`);
|
|
1038
1203
|
});
|
|
1039
1204
|
}
|
|
1040
|
-
function resolveWaitFlag({ waitFlag,
|
|
1205
|
+
function resolveWaitFlag({ waitFlag, model, engine, }) {
|
|
1041
1206
|
if (waitFlag === true)
|
|
1042
1207
|
return true;
|
|
1043
|
-
if (
|
|
1208
|
+
if (waitFlag === false)
|
|
1044
1209
|
return false;
|
|
1045
1210
|
return defaultWaitPreference(model, engine);
|
|
1046
1211
|
}
|
|
1212
|
+
function resolveRestartWaitPreference({ waitFlag, storedPreference, model, engine, }) {
|
|
1213
|
+
if (waitFlag === true)
|
|
1214
|
+
return true;
|
|
1215
|
+
if (waitFlag === false)
|
|
1216
|
+
return false;
|
|
1217
|
+
if (typeof storedPreference === 'boolean')
|
|
1218
|
+
return storedPreference;
|
|
1219
|
+
return defaultWaitPreference(model, engine);
|
|
1220
|
+
}
|
|
1221
|
+
function resolveEffectiveModelIdForRun(model, stored) {
|
|
1222
|
+
if (stored)
|
|
1223
|
+
return stored;
|
|
1224
|
+
if (model.startsWith('gemini')) {
|
|
1225
|
+
return resolveGeminiModelId(model);
|
|
1226
|
+
}
|
|
1227
|
+
return isKnownModel(model) ? MODEL_CONFIGS[model].apiModel ?? model : model;
|
|
1228
|
+
}
|
|
1047
1229
|
program.action(async function () {
|
|
1048
1230
|
const options = this.optsWithGlobals();
|
|
1049
1231
|
await runRootCommand(options);
|
|
@@ -27,23 +27,26 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
27
27
|
const raceReadyEvaluation = evaluationPromise.then((value) => ({ kind: 'evaluation', value }), (error) => {
|
|
28
28
|
throw { source: 'evaluation', error };
|
|
29
29
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return { kind: 'poll', value };
|
|
35
|
-
}, (error) => {
|
|
30
|
+
// Use AbortController to stop the poller when the evaluation wins the race,
|
|
31
|
+
// preventing abandoned polling loops from consuming resources.
|
|
32
|
+
const pollerAbort = new AbortController();
|
|
33
|
+
const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, pollerAbort.signal).then((value) => ({ kind: 'poll', value }), (error) => {
|
|
36
34
|
throw { source: 'poll', error };
|
|
37
35
|
});
|
|
38
36
|
let evaluation = null;
|
|
39
37
|
try {
|
|
40
38
|
const winner = await Promise.race([raceReadyEvaluation, pollerPromise]);
|
|
41
39
|
if (winner.kind === 'poll') {
|
|
40
|
+
if (!winner.value) {
|
|
41
|
+
throw { source: 'poll', error: new Error(ASSISTANT_POLL_TIMEOUT_ERROR) };
|
|
42
|
+
}
|
|
42
43
|
logger('Captured assistant response via snapshot watchdog');
|
|
43
44
|
evaluationPromise.catch(() => undefined);
|
|
44
45
|
await terminateRuntimeExecution(Runtime);
|
|
45
46
|
return winner.value;
|
|
46
47
|
}
|
|
48
|
+
// Evaluation won - abort the poller to prevent it from running until timeout
|
|
49
|
+
pollerAbort.abort();
|
|
47
50
|
evaluation = winner.value;
|
|
48
51
|
}
|
|
49
52
|
catch (wrappedError) {
|
|
@@ -86,7 +89,7 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
86
89
|
pollerPromise.catch(() => null),
|
|
87
90
|
delay(remainingMs).then(() => null),
|
|
88
91
|
]);
|
|
89
|
-
if (polled && polled.kind === 'poll') {
|
|
92
|
+
if (polled && polled.kind === 'poll' && polled.value) {
|
|
90
93
|
return polled.value;
|
|
91
94
|
}
|
|
92
95
|
}
|
|
@@ -263,12 +266,16 @@ async function terminateRuntimeExecution(Runtime) {
|
|
|
263
266
|
// ignore termination failures
|
|
264
267
|
}
|
|
265
268
|
}
|
|
266
|
-
async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex) {
|
|
269
|
+
async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, abortSignal) {
|
|
267
270
|
const watchdogDeadline = Date.now() + timeoutMs;
|
|
268
271
|
let previousLength = 0;
|
|
269
272
|
let stableCycles = 0;
|
|
270
273
|
let lastChangeAt = Date.now();
|
|
271
274
|
while (Date.now() < watchdogDeadline) {
|
|
275
|
+
// Check abort signal to stop polling when another path won the race
|
|
276
|
+
if (abortSignal?.aborted) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
272
279
|
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
|
|
273
280
|
const normalized = normalizeAssistantSnapshot(snapshot);
|
|
274
281
|
if (normalized) {
|
|
@@ -483,33 +490,63 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
|
483
490
|
new Promise((resolve, reject) => {
|
|
484
491
|
const deadline = Date.now() + ${timeoutMs};
|
|
485
492
|
let stopInterval = null;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
493
|
+
let timeoutId = null;
|
|
494
|
+
let cleanedUp = false;
|
|
495
|
+
let observer = null;
|
|
496
|
+
|
|
497
|
+
// Centralized cleanup to prevent resource leaks
|
|
498
|
+
const cleanup = () => {
|
|
499
|
+
if (cleanedUp) return;
|
|
500
|
+
cleanedUp = true;
|
|
501
|
+
if (stopInterval) {
|
|
502
|
+
clearInterval(stopInterval);
|
|
503
|
+
stopInterval = null;
|
|
504
|
+
}
|
|
505
|
+
if (timeoutId) {
|
|
506
|
+
clearTimeout(timeoutId);
|
|
507
|
+
timeoutId = null;
|
|
508
|
+
}
|
|
509
|
+
if (observer) {
|
|
510
|
+
try {
|
|
511
|
+
observer.disconnect();
|
|
512
|
+
} catch {
|
|
513
|
+
// ignore disconnect errors
|
|
514
|
+
}
|
|
515
|
+
observer = null;
|
|
496
516
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const observerCallback = () => {
|
|
520
|
+
if (cleanedUp) return;
|
|
521
|
+
try {
|
|
522
|
+
const extractedRaw = extractFromTurns();
|
|
523
|
+
const extractedCandidate =
|
|
524
|
+
extractedRaw && !isAnswerNowPlaceholder(extractedRaw) ? extractedRaw : null;
|
|
525
|
+
let extracted = acceptSnapshot(extractedCandidate);
|
|
526
|
+
if (!extracted) {
|
|
527
|
+
const fallbackRaw = extractFromMarkdownFallback();
|
|
528
|
+
const fallbackCandidate =
|
|
529
|
+
fallbackRaw && !isAnswerNowPlaceholder(fallbackRaw) ? fallbackRaw : null;
|
|
530
|
+
extracted = acceptSnapshot(fallbackCandidate);
|
|
501
531
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
|
|
532
|
+
if (extracted) {
|
|
533
|
+
cleanup();
|
|
534
|
+
resolve(extracted);
|
|
535
|
+
} else if (Date.now() > deadline) {
|
|
536
|
+
cleanup();
|
|
537
|
+
reject(new Error('Response timeout'));
|
|
507
538
|
}
|
|
508
|
-
|
|
539
|
+
} catch (error) {
|
|
540
|
+
cleanup();
|
|
541
|
+
reject(error);
|
|
509
542
|
}
|
|
510
|
-
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
observer = new MutationObserver(observerCallback);
|
|
511
546
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
547
|
+
|
|
512
548
|
stopInterval = setInterval(() => {
|
|
549
|
+
if (cleanedUp) return;
|
|
513
550
|
const stop = document.querySelector(STOP_SELECTOR);
|
|
514
551
|
if (!stop) {
|
|
515
552
|
return;
|
|
@@ -521,11 +558,9 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
|
521
558
|
}
|
|
522
559
|
dispatchClickSequence(stop);
|
|
523
560
|
}, 500);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
observer.disconnect();
|
|
561
|
+
|
|
562
|
+
timeoutId = setTimeout(() => {
|
|
563
|
+
cleanup();
|
|
529
564
|
reject(new Error('Response timeout'));
|
|
530
565
|
}, ${timeoutMs});
|
|
531
566
|
});
|
|
@@ -686,7 +721,7 @@ function buildAssistantExtractor(functionName) {
|
|
|
686
721
|
function buildMarkdownFallbackExtractor(minTurnLiteral) {
|
|
687
722
|
const turnIndexValue = minTurnLiteral ? `(${minTurnLiteral} >= 0 ? ${minTurnLiteral} : null)` : 'null';
|
|
688
723
|
return `(() => {
|
|
689
|
-
const
|
|
724
|
+
const __minTurn = ${turnIndexValue};
|
|
690
725
|
const roots = [
|
|
691
726
|
document.querySelector('section[data-testid="screen-threadFlyOut"]'),
|
|
692
727
|
document.querySelector('[data-testid="chat-thread"]'),
|
|
@@ -728,10 +763,10 @@ function buildMarkdownFallbackExtractor(minTurnLiteral) {
|
|
|
728
763
|
return idx >= 0 ? idx : null;
|
|
729
764
|
};
|
|
730
765
|
const isAfterMinTurn = (node) => {
|
|
731
|
-
if (
|
|
766
|
+
if (__minTurn === null) return true;
|
|
732
767
|
if (!hasTurns) return true;
|
|
733
768
|
const idx = resolveTurnIndex(node);
|
|
734
|
-
return idx !== null && idx >=
|
|
769
|
+
return idx !== null && idx >= __minTurn;
|
|
735
770
|
};
|
|
736
771
|
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
737
772
|
const collectUserText = (scope) => {
|