@steipete/oracle 0.20.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +61 -16
- package/dist/src/browser/actions/thinkingTime.js +11 -5
- package/dist/src/browser/chromeLifecycle.js +23 -12
- package/dist/src/browser/index.js +33 -6
- package/dist/src/browser/liveTabs.js +8 -0
- package/dist/src/browser/profileState.js +6 -1
- package/dist/src/browser/promptFingerprint.js +54 -0
- package/dist/src/browser/reattach.js +157 -108
- package/dist/src/browser/recoveryTarget.js +31 -6
- package/dist/src/browser/sessionRunner.js +30 -7
- package/dist/src/browser/targetClaim.js +2 -2
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +35 -3
- package/dist/src/cli/detach.js +10 -1
- package/dist/src/cli/detachedSession.js +36 -0
- package/dist/src/cli/sessionDisplay.js +11 -3
- package/dist/src/cli/sessionRunner.js +150 -37
- package/dist/src/remote/server.js +1 -0
- package/dist/src/sessionManager.js +8 -1
- 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 +9 -9
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -15,8 +15,8 @@ import { collectPaths, collectModelList, collectTextValues, parseFloatOption, pa
|
|
|
15
15
|
import { copyToClipboard } from "../src/cli/clipboard.js";
|
|
16
16
|
import { isGpt6ProAlias } from "../src/cli/browserConfig.js";
|
|
17
17
|
import { buildMarkdownBundle } from "../src/cli/markdownBundle.js";
|
|
18
|
-
import { shouldDetachSession, stopDetachedWorker } from "../src/cli/detach.js";
|
|
19
|
-
import { launchDetachedSession } from "../src/cli/detachedSession.js";
|
|
18
|
+
import { detachedCancellationExitCode, shouldDetachSession, shouldExitAfterTopLevelSigint, stopDetachedWorker, } from "../src/cli/detach.js";
|
|
19
|
+
import { clearDetachedSessionCancellation, detachedSessionCancellationPath, launchDetachedSession, requestDetachedSessionCancellation, waitForDetachedSessionCancellation, } from "../src/cli/detachedSession.js";
|
|
20
20
|
import { applyHiddenAliases } from "../src/cli/hiddenAliases.js";
|
|
21
21
|
import { isMediaFile } from "../src/browser/prompt.js";
|
|
22
22
|
import { formatCompactNumber } from "../src/cli/format.js";
|
|
@@ -37,6 +37,7 @@ import { isAzureOpenAICandidateModel, validateProviderRouting, } from "../src/or
|
|
|
37
37
|
import { buildSessionLifecycle, formatSessionLifecycleBlock } from "../src/cli/sessionLifecycle.js";
|
|
38
38
|
import { buildDetachedPerfTraceEnv, createPerfTrace, isTraceValueFlag, } from "../src/cli/perfTrace.js";
|
|
39
39
|
import { resolveBrowserFollowupReference } from "../src/cli/followup.js";
|
|
40
|
+
import { BrowserRunCancelledError } from "../src/oracle/errors.js";
|
|
40
41
|
const VERSION = getCliVersion();
|
|
41
42
|
const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
|
|
42
43
|
const LEGACY_FLAG_ALIASES = new Map([
|
|
@@ -1673,18 +1674,28 @@ async function waitForDetachedStartGate() {
|
|
|
1673
1674
|
}
|
|
1674
1675
|
}
|
|
1675
1676
|
async function attachToDetachedSession(sessionId, workerPid) {
|
|
1677
|
+
const cancellationMarker = sessionStore
|
|
1678
|
+
.getPaths(sessionId)
|
|
1679
|
+
.then((paths) => detachedSessionCancellationPath(paths.dir, workerPid));
|
|
1676
1680
|
let cancelled = false;
|
|
1681
|
+
let cancellationRequest;
|
|
1677
1682
|
const cancelWorker = () => {
|
|
1678
1683
|
cancelled = true;
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
catch (error) {
|
|
1684
|
+
cancellationRequest ??= cancellationMarker
|
|
1685
|
+
.then((markerPath) => requestDetachedSessionCancellation(markerPath))
|
|
1686
|
+
.catch((error) => {
|
|
1683
1687
|
const message = error instanceof Error ? error.message : String(error);
|
|
1684
|
-
console.error(chalk.red(`Unable to
|
|
1685
|
-
|
|
1688
|
+
console.error(chalk.red(`Unable to request cancellation from worker ${workerPid}: ${message}`));
|
|
1689
|
+
try {
|
|
1690
|
+
stopDetachedWorker(workerPid);
|
|
1691
|
+
}
|
|
1692
|
+
catch (stopError) {
|
|
1693
|
+
const stopMessage = stopError instanceof Error ? stopError.message : String(stopError);
|
|
1694
|
+
console.error(chalk.red(`Unable to stop detached worker ${workerPid}: ${stopMessage}`));
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1686
1697
|
};
|
|
1687
|
-
process.
|
|
1698
|
+
process.on("SIGINT", cancelWorker);
|
|
1688
1699
|
try {
|
|
1689
1700
|
const { attachSession } = await import("../src/cli/sessionDisplay.js");
|
|
1690
1701
|
await attachSession(sessionId, {
|
|
@@ -1695,9 +1706,15 @@ async function attachToDetachedSession(sessionId, workerPid) {
|
|
|
1695
1706
|
}
|
|
1696
1707
|
finally {
|
|
1697
1708
|
process.off("SIGINT", cancelWorker);
|
|
1698
|
-
|
|
1699
|
-
|
|
1709
|
+
await cancellationRequest;
|
|
1710
|
+
const finalStatus = (await sessionStore.readSession(sessionId).catch(() => null))?.status;
|
|
1711
|
+
if (cancellationRequest &&
|
|
1712
|
+
finalStatus &&
|
|
1713
|
+
["completed", "partial", "cancelled", "error"].includes(finalStatus)) {
|
|
1714
|
+
// The parent may publish its request after the worker's final cleanup.
|
|
1715
|
+
await clearDetachedSessionCancellation(await cancellationMarker);
|
|
1700
1716
|
}
|
|
1717
|
+
process.exitCode = detachedCancellationExitCode(cancelled, finalStatus, process.exitCode);
|
|
1701
1718
|
}
|
|
1702
1719
|
}
|
|
1703
1720
|
async function restartSession(sessionId, options) {
|
|
@@ -1879,6 +1896,10 @@ async function restartSession(sessionId, options) {
|
|
|
1879
1896
|
async function executeSession(sessionId) {
|
|
1880
1897
|
let metadata = null;
|
|
1881
1898
|
let writer = null;
|
|
1899
|
+
const cancellation = new AbortController();
|
|
1900
|
+
const stopCancellationMonitor = new AbortController();
|
|
1901
|
+
let cancellationMarker;
|
|
1902
|
+
let cancellationMonitor;
|
|
1882
1903
|
try {
|
|
1883
1904
|
metadata = await sessionStore.readSession(sessionId);
|
|
1884
1905
|
if (!metadata) {
|
|
@@ -1892,6 +1913,18 @@ async function executeSession(sessionId) {
|
|
|
1892
1913
|
const sessionMode = getSessionMode(metadata);
|
|
1893
1914
|
const browserConfig = getBrowserConfigFromMetadata(metadata);
|
|
1894
1915
|
writer = sessionStore.createLogWriter(sessionId);
|
|
1916
|
+
if (sessionMode === "browser") {
|
|
1917
|
+
const paths = await sessionStore.getPaths(sessionId);
|
|
1918
|
+
cancellationMarker = detachedSessionCancellationPath(paths.dir, process.pid);
|
|
1919
|
+
cancellationMonitor = waitForDetachedSessionCancellation({
|
|
1920
|
+
markerPath: cancellationMarker,
|
|
1921
|
+
signal: stopCancellationMonitor.signal,
|
|
1922
|
+
}).then((requested) => {
|
|
1923
|
+
if (requested) {
|
|
1924
|
+
cancellation.abort(new BrowserRunCancelledError("Browser run cancelled by the user."));
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1895
1928
|
const userConfig = (await loadUserConfig()).config;
|
|
1896
1929
|
const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
|
|
1897
1930
|
const { performSessionRun } = await import("../src/cli/sessionRunner.js");
|
|
@@ -1905,18 +1938,22 @@ async function executeSession(sessionId) {
|
|
|
1905
1938
|
write: writer.writeChunk,
|
|
1906
1939
|
version: VERSION,
|
|
1907
1940
|
notifications,
|
|
1941
|
+
signal: sessionMode === "browser" ? cancellation.signal : undefined,
|
|
1908
1942
|
});
|
|
1909
1943
|
}
|
|
1910
1944
|
catch (error) {
|
|
1911
|
-
|
|
1945
|
+
const cancelled = error instanceof BrowserRunCancelledError;
|
|
1946
|
+
process.exitCode = cancelled ? 130 : 1;
|
|
1912
1947
|
const message = error instanceof Error ? error.message : String(error);
|
|
1913
1948
|
if (!metadata) {
|
|
1914
|
-
|
|
1949
|
+
if (!cancelled)
|
|
1950
|
+
console.error(chalk.red(message));
|
|
1915
1951
|
return;
|
|
1916
1952
|
}
|
|
1917
|
-
|
|
1953
|
+
if (!cancelled)
|
|
1954
|
+
writer?.logLine(`ERROR: Detached session worker failed: ${message}`);
|
|
1918
1955
|
const latest = await sessionStore.readSession(sessionId).catch(() => null);
|
|
1919
|
-
if (latest && !["completed", "partial", "error"].includes(latest.status)) {
|
|
1956
|
+
if (latest && !["completed", "partial", "error", "cancelled"].includes(latest.status)) {
|
|
1920
1957
|
await sessionStore.updateSession(sessionId, {
|
|
1921
1958
|
status: "error",
|
|
1922
1959
|
completedAt: new Date().toISOString(),
|
|
@@ -1930,6 +1967,14 @@ async function executeSession(sessionId) {
|
|
|
1930
1967
|
}
|
|
1931
1968
|
}
|
|
1932
1969
|
finally {
|
|
1970
|
+
stopCancellationMonitor.abort();
|
|
1971
|
+
await cancellationMonitor?.catch((error) => {
|
|
1972
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1973
|
+
writer?.logLine(`ERROR: Detached cancellation monitor failed: ${message}`);
|
|
1974
|
+
});
|
|
1975
|
+
if (cancellationMarker) {
|
|
1976
|
+
await clearDetachedSessionCancellation(cancellationMarker).catch(() => undefined);
|
|
1977
|
+
}
|
|
1933
1978
|
writer?.stream.end();
|
|
1934
1979
|
}
|
|
1935
1980
|
}
|
|
@@ -2048,7 +2093,7 @@ async function main() {
|
|
|
2048
2093
|
console.log(chalk.yellow("\nCancelled."));
|
|
2049
2094
|
process.exitCode = 130;
|
|
2050
2095
|
// Browser/serve modes install their own SIGINT cleanup after this top-level handler.
|
|
2051
|
-
if (process.listenerCount("SIGINT")
|
|
2096
|
+
if (shouldExitAfterTopLevelSigint(process.listenerCount("SIGINT"))) {
|
|
2052
2097
|
process.exit(130);
|
|
2053
2098
|
}
|
|
2054
2099
|
};
|
|
@@ -1030,11 +1030,12 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1030
1030
|
.filter(Boolean);
|
|
1031
1031
|
if (selections.length !== 1) return null;
|
|
1032
1032
|
const { label, index, level } = selections[0];
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1033
|
+
// Quota-limited accounts expose four tiers; the fourth remains Extra High.
|
|
1034
|
+
// Require an observed range and matching label/index before trusting either.
|
|
1035
|
+
const maximum = thumb.getAttribute('aria-valuemax');
|
|
1036
|
+
if (thumb.getAttribute('aria-valuemin') !== '0' || !['3', '4'].includes(maximum) ||
|
|
1037
|
+
index > Number(maximum) || thumb.getAttribute('aria-valuenow') !== String(index)) return null;
|
|
1038
|
+
return { control, label, index, level, maximum: Number(maximum) };
|
|
1038
1039
|
};
|
|
1039
1040
|
let current = resolve();
|
|
1040
1041
|
const finish = (result) => { closeOpenMenus(); return result; };
|
|
@@ -1054,6 +1055,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1054
1055
|
}
|
|
1055
1056
|
return finish(failure('option-not-found'));
|
|
1056
1057
|
}
|
|
1058
|
+
const unavailable = () => finish(failure('option-disabled', {
|
|
1059
|
+
label: 'Pro', notice: 'the available four-tier effort slider does not include Pro',
|
|
1060
|
+
}));
|
|
1061
|
+
if (targetIndex > current.maximum) return unavailable();
|
|
1057
1062
|
if (current.level === target) return finish({ status: 'already-selected', label: current.label });
|
|
1058
1063
|
const deadline = performance.now() + MAX_WAIT_MS;
|
|
1059
1064
|
for (let attempt = 0; attempt < 4 && performance.now() < deadline; attempt += 1) {
|
|
@@ -1069,6 +1074,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1069
1074
|
if (next && next.index !== previousIndex) { current = next; break; }
|
|
1070
1075
|
}
|
|
1071
1076
|
if (!current) return finish(failure('selection-unverified'));
|
|
1077
|
+
if (targetIndex > current.maximum) return unavailable();
|
|
1072
1078
|
if (current.level === target) return finish({ status: 'switched', label: current.label });
|
|
1073
1079
|
}
|
|
1074
1080
|
return finish(failure('selection-unverified'));
|
|
@@ -7,6 +7,7 @@ import { launch, Launcher, } from "chrome-launcher";
|
|
|
7
7
|
import { cleanupStaleProfileState } from "./profileState.js";
|
|
8
8
|
import { delay } from "./utils.js";
|
|
9
9
|
import { isWsl, resolveWslChromeLaunchRoute } from "./wslHost.js";
|
|
10
|
+
import { BrowserCancellation } from "./cancellation.js";
|
|
10
11
|
export async function launchChrome(config, userDataDir, logger) {
|
|
11
12
|
const { connectHost, debugBindAddress, usePatchedLauncher } = resolveWslChromeLaunchRoute();
|
|
12
13
|
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
@@ -388,21 +389,31 @@ export async function closeRemoteChromeTarget(host, port, targetId, logger) {
|
|
|
388
389
|
}
|
|
389
390
|
}
|
|
390
391
|
export async function listRemoteChromeTargets(options) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return targets;
|
|
394
|
-
}
|
|
395
|
-
const browser = await connectToBrowserWebSocket(options.host, options.port, options.browserWSEndpoint, options.logger ?? (() => { }), options.approvalWaitMs);
|
|
392
|
+
const logger = options.logger ?? (() => { });
|
|
393
|
+
const cancellation = new BrowserCancellation(options.signal, logger);
|
|
396
394
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
395
|
+
return await cancellation.run(async () => {
|
|
396
|
+
if (!options.browserWSEndpoint) {
|
|
397
|
+
const targets = await cancellation.call(() => CDP.List({ host: options.host, port: options.port }));
|
|
398
|
+
return targets;
|
|
399
|
+
}
|
|
400
|
+
const browser = await cancellation.acquire(() => connectToBrowserWebSocket(options.host, options.port, options.browserWSEndpoint, logger, options.approvalWaitMs), (lateBrowser) => lateBrowser.close());
|
|
401
|
+
try {
|
|
402
|
+
const client = cancellation.client(browser);
|
|
403
|
+
const result = await client.Target.getTargets();
|
|
404
|
+
return (result.targetInfos ?? []).map((target) => ({
|
|
405
|
+
targetId: target.targetId,
|
|
406
|
+
type: target.type,
|
|
407
|
+
url: target.url,
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
await browser.close().catch(() => undefined);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
403
414
|
}
|
|
404
415
|
finally {
|
|
405
|
-
|
|
416
|
+
cancellation.dispose();
|
|
406
417
|
}
|
|
407
418
|
}
|
|
408
419
|
export async function connectToRemoteChromeTarget(host, port, logger, options) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readSubmittedPromptFingerprint, readUserMessageIds } from "./promptFingerprint.js";
|
|
1
2
|
import { mkdtemp, rm, mkdir } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import os from "node:os";
|
|
@@ -708,6 +709,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
708
709
|
let lastTargetId;
|
|
709
710
|
let lastUrl;
|
|
710
711
|
let promptSubmitted = false;
|
|
712
|
+
let submittedPromptHash = null;
|
|
711
713
|
let ownedRecoveryTarget;
|
|
712
714
|
const targetClaimId = randomUUID();
|
|
713
715
|
let modelSelectionEvidence;
|
|
@@ -728,6 +730,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
728
730
|
tabUrl: lastUrl,
|
|
729
731
|
conversationId,
|
|
730
732
|
promptSubmitted,
|
|
733
|
+
submittedPromptHash,
|
|
731
734
|
ownedRecoveryTarget,
|
|
732
735
|
userDataDir,
|
|
733
736
|
controllerPid: process.pid,
|
|
@@ -748,10 +751,8 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
748
751
|
}
|
|
749
752
|
};
|
|
750
753
|
const markPromptSubmitted = async () => {
|
|
751
|
-
if (promptSubmitted) {
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
754
|
promptSubmitted = true;
|
|
755
|
+
submittedPromptHash = null;
|
|
755
756
|
await emitRuntimeHint();
|
|
756
757
|
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
757
758
|
};
|
|
@@ -986,6 +987,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
986
987
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
987
988
|
: undefined,
|
|
988
989
|
promptSubmitted,
|
|
990
|
+
submittedPromptHash,
|
|
989
991
|
ownedRecoveryTarget,
|
|
990
992
|
controllerPid: process.pid,
|
|
991
993
|
researchPlan,
|
|
@@ -1332,6 +1334,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1332
1334
|
const deepResearchTargetBaseline = deepResearch && client
|
|
1333
1335
|
? await captureDeepResearchTargetBaseline(client, logger)
|
|
1334
1336
|
: undefined;
|
|
1337
|
+
const previousUserMessageIds = await readUserMessageIds(Runtime, config.inputTimeoutMs);
|
|
1335
1338
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
1336
1339
|
prompt,
|
|
1337
1340
|
evaluate: async () => undefined,
|
|
@@ -1341,6 +1344,11 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1341
1344
|
});
|
|
1342
1345
|
await markPromptSubmitted();
|
|
1343
1346
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
1347
|
+
const renderedPromptHash = await readSubmittedPromptFingerprint(Runtime, previousUserMessageIds, config.inputTimeoutMs);
|
|
1348
|
+
if (renderedPromptHash) {
|
|
1349
|
+
submittedPromptHash = renderedPromptHash;
|
|
1350
|
+
await emitRuntimeHint();
|
|
1351
|
+
}
|
|
1344
1352
|
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
1345
1353
|
baselineTurns = providerBaselineTurns;
|
|
1346
1354
|
}
|
|
@@ -1463,6 +1471,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1463
1471
|
tabUrl: lastUrl,
|
|
1464
1472
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1465
1473
|
promptSubmitted,
|
|
1474
|
+
submittedPromptHash,
|
|
1466
1475
|
ownedRecoveryTarget,
|
|
1467
1476
|
controllerPid: process.pid,
|
|
1468
1477
|
researchPlan,
|
|
@@ -1555,6 +1564,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1555
1564
|
tabUrl: lastUrl,
|
|
1556
1565
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1557
1566
|
promptSubmitted,
|
|
1567
|
+
submittedPromptHash,
|
|
1558
1568
|
ownedRecoveryTarget,
|
|
1559
1569
|
controllerPid: process.pid,
|
|
1560
1570
|
},
|
|
@@ -1612,6 +1622,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1612
1622
|
tabUrl: lastUrl,
|
|
1613
1623
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1614
1624
|
promptSubmitted,
|
|
1625
|
+
submittedPromptHash,
|
|
1615
1626
|
ownedRecoveryTarget,
|
|
1616
1627
|
controllerPid: process.pid,
|
|
1617
1628
|
};
|
|
@@ -1838,6 +1849,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1838
1849
|
tabUrl: lastUrl,
|
|
1839
1850
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1840
1851
|
promptSubmitted,
|
|
1852
|
+
submittedPromptHash,
|
|
1841
1853
|
ownedRecoveryTarget,
|
|
1842
1854
|
controllerPid: process.pid,
|
|
1843
1855
|
},
|
|
@@ -1906,6 +1918,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1906
1918
|
tabUrl: lastUrl,
|
|
1907
1919
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1908
1920
|
promptSubmitted,
|
|
1921
|
+
submittedPromptHash,
|
|
1909
1922
|
ownedRecoveryTarget,
|
|
1910
1923
|
controllerPid: process.pid,
|
|
1911
1924
|
};
|
|
@@ -1933,6 +1946,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1933
1946
|
chromeTargetId: lastTargetId,
|
|
1934
1947
|
tabUrl: lastUrl,
|
|
1935
1948
|
promptSubmitted,
|
|
1949
|
+
submittedPromptHash,
|
|
1936
1950
|
ownedRecoveryTarget,
|
|
1937
1951
|
controllerPid: process.pid,
|
|
1938
1952
|
};
|
|
@@ -1997,6 +2011,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1997
2011
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
1998
2012
|
: undefined,
|
|
1999
2013
|
promptSubmitted,
|
|
2014
|
+
submittedPromptHash,
|
|
2000
2015
|
ownedRecoveryTarget,
|
|
2001
2016
|
controllerPid: process.pid,
|
|
2002
2017
|
researchPlan,
|
|
@@ -2397,6 +2412,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2397
2412
|
let tabLease = null;
|
|
2398
2413
|
let lastUrl;
|
|
2399
2414
|
let promptSubmitted = false;
|
|
2415
|
+
let submittedPromptHash = null;
|
|
2400
2416
|
let ownedRecoveryTarget;
|
|
2401
2417
|
const targetClaimId = randomUUID();
|
|
2402
2418
|
let modelSelectionEvidence;
|
|
@@ -2419,6 +2435,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2419
2435
|
tabUrl: lastUrl,
|
|
2420
2436
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2421
2437
|
promptSubmitted,
|
|
2438
|
+
submittedPromptHash,
|
|
2422
2439
|
ownedRecoveryTarget,
|
|
2423
2440
|
controllerPid: process.pid,
|
|
2424
2441
|
researchPlan,
|
|
@@ -2436,10 +2453,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2436
2453
|
}
|
|
2437
2454
|
};
|
|
2438
2455
|
const markPromptSubmitted = async () => {
|
|
2439
|
-
if (promptSubmitted) {
|
|
2440
|
-
return;
|
|
2441
|
-
}
|
|
2442
2456
|
promptSubmitted = true;
|
|
2457
|
+
submittedPromptHash = null;
|
|
2443
2458
|
await emitRuntimeHint();
|
|
2444
2459
|
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
2445
2460
|
};
|
|
@@ -2685,6 +2700,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2685
2700
|
const deepResearchTargetBaseline = deepResearch && client
|
|
2686
2701
|
? await captureDeepResearchTargetBaseline(client, logger)
|
|
2687
2702
|
: undefined;
|
|
2703
|
+
const previousUserMessageIds = await readUserMessageIds(Runtime, config.inputTimeoutMs);
|
|
2688
2704
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
2689
2705
|
prompt,
|
|
2690
2706
|
evaluate: async () => undefined,
|
|
@@ -2694,6 +2710,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2694
2710
|
});
|
|
2695
2711
|
await markPromptSubmitted();
|
|
2696
2712
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
2713
|
+
const renderedPromptHash = await readSubmittedPromptFingerprint(Runtime, previousUserMessageIds, config.inputTimeoutMs);
|
|
2714
|
+
if (renderedPromptHash) {
|
|
2715
|
+
submittedPromptHash = renderedPromptHash;
|
|
2716
|
+
await emitRuntimeHint();
|
|
2717
|
+
}
|
|
2697
2718
|
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
2698
2719
|
baselineTurns = providerBaselineTurns;
|
|
2699
2720
|
}
|
|
@@ -2790,6 +2811,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2790
2811
|
tabUrl: lastUrl,
|
|
2791
2812
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2792
2813
|
promptSubmitted,
|
|
2814
|
+
submittedPromptHash,
|
|
2793
2815
|
ownedRecoveryTarget,
|
|
2794
2816
|
controllerPid: process.pid,
|
|
2795
2817
|
researchPlan,
|
|
@@ -2881,6 +2903,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2881
2903
|
tabUrl: lastUrl,
|
|
2882
2904
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2883
2905
|
promptSubmitted,
|
|
2906
|
+
submittedPromptHash,
|
|
2884
2907
|
ownedRecoveryTarget,
|
|
2885
2908
|
controllerPid: process.pid,
|
|
2886
2909
|
},
|
|
@@ -2940,6 +2963,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2940
2963
|
tabUrl: lastUrl,
|
|
2941
2964
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2942
2965
|
promptSubmitted,
|
|
2966
|
+
submittedPromptHash,
|
|
2943
2967
|
ownedRecoveryTarget,
|
|
2944
2968
|
controllerPid: process.pid,
|
|
2945
2969
|
};
|
|
@@ -3124,6 +3148,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3124
3148
|
tabUrl: lastUrl,
|
|
3125
3149
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
3126
3150
|
promptSubmitted,
|
|
3151
|
+
submittedPromptHash,
|
|
3127
3152
|
ownedRecoveryTarget,
|
|
3128
3153
|
controllerPid: process.pid,
|
|
3129
3154
|
},
|
|
@@ -3187,6 +3212,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3187
3212
|
tabUrl: lastUrl,
|
|
3188
3213
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
3189
3214
|
promptSubmitted,
|
|
3215
|
+
submittedPromptHash,
|
|
3190
3216
|
ownedRecoveryTarget,
|
|
3191
3217
|
artifacts: savedArtifacts,
|
|
3192
3218
|
generatedImages: imageArtifacts.generatedImages,
|
|
@@ -3236,6 +3262,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3236
3262
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
3237
3263
|
: undefined,
|
|
3238
3264
|
promptSubmitted,
|
|
3265
|
+
submittedPromptHash,
|
|
3239
3266
|
ownedRecoveryTarget,
|
|
3240
3267
|
controllerPid: process.pid,
|
|
3241
3268
|
researchPlan,
|
|
@@ -162,6 +162,8 @@ function buildTabInspectionExpression() {
|
|
|
162
162
|
const assistantCount = new Set(assistantOwners).size;
|
|
163
163
|
const lastAssistantText = normalize(lastAssistantNode?.textContent);
|
|
164
164
|
const lastUserText = normalize(lastUserTurn?.textContent);
|
|
165
|
+
const lastUserMessage = lastUserTurn?.matches?.('[data-message-author-role="user"]')
|
|
166
|
+
? lastUserTurn : lastUserTurn?.querySelector?.('[data-message-author-role="user"]');
|
|
165
167
|
const authenticated = !loginButtonExists && (promptReady || sendExists || stopExists || assistantCount > 0);
|
|
166
168
|
return {
|
|
167
169
|
title: normalize(document.title),
|
|
@@ -178,6 +180,8 @@ function buildTabInspectionExpression() {
|
|
|
178
180
|
lastAssistantTurnIndex,
|
|
179
181
|
lastUserTurnIndex,
|
|
180
182
|
lastUserText,
|
|
183
|
+
lastUserTextRaw: lastUserMessage?.textContent,
|
|
184
|
+
lastUserMessageId: lastUserMessage?.getAttribute?.('data-message-id'),
|
|
181
185
|
visibilityState: document.visibilityState,
|
|
182
186
|
focused: Boolean(document.hasFocus?.()),
|
|
183
187
|
};
|
|
@@ -263,6 +267,8 @@ export async function inspectChatGptTab(options) {
|
|
|
263
267
|
: undefined,
|
|
264
268
|
lastAssistantSnippet: trimToSnippet(lastAssistantText),
|
|
265
269
|
lastUserText,
|
|
270
|
+
lastUserTextRaw: info.lastUserTextRaw,
|
|
271
|
+
lastUserMessageId: info.lastUserMessageId,
|
|
266
272
|
lastUserSnippet: trimToSnippet(lastUserText),
|
|
267
273
|
focused: Boolean(info.focused),
|
|
268
274
|
visibilityState: typeof info.visibilityState === "string" ? info.visibilityState : "",
|
|
@@ -461,6 +467,8 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
461
467
|
harvested.authenticated = followup.authenticated;
|
|
462
468
|
harvested.loginButtonExists = followup.loginButtonExists;
|
|
463
469
|
harvested.lastUserText = followup.lastUserText;
|
|
470
|
+
harvested.lastUserTextRaw = followup.lastUserTextRaw;
|
|
471
|
+
harvested.lastUserMessageId = followup.lastUserMessageId;
|
|
464
472
|
harvested.lastUserSnippet = followup.lastUserSnippet;
|
|
465
473
|
harvested.assistantFollowsLatestUser = followup.assistantFollowsLatestUser;
|
|
466
474
|
harvested.lastAssistantTurnIndex = followup.lastAssistantTurnIndex;
|
|
@@ -224,7 +224,12 @@ export async function readProcessStartTimeMs(pid) {
|
|
|
224
224
|
if (Math.trunc(pid) === process.pid) {
|
|
225
225
|
// Use the same OS identity as peer controllers, not wall time minus uptime.
|
|
226
226
|
// Cache our own PID only: it cannot be reused during this process's lifetime.
|
|
227
|
-
return (ownProcessStartTime ??= queryProcessStartTimeMs(process.pid))
|
|
227
|
+
return (ownProcessStartTime ??= queryProcessStartTimeMs(process.pid).then((startedAt) => {
|
|
228
|
+
// A transient probe failure is not a permanent process identity.
|
|
229
|
+
if (startedAt === null)
|
|
230
|
+
ownProcessStartTime = undefined;
|
|
231
|
+
return startedAt;
|
|
232
|
+
}));
|
|
228
233
|
}
|
|
229
234
|
return queryProcessStartTimeMs(pid);
|
|
230
235
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
3
|
+
export function browserPromptFingerprint(value, messageId) {
|
|
4
|
+
return createHash("sha256")
|
|
5
|
+
.update(JSON.stringify([messageId, String(value ?? "").replace(/\r\n?/g, "\n")]))
|
|
6
|
+
.digest("hex");
|
|
7
|
+
}
|
|
8
|
+
export function readUserMessageIds(runtime, timeoutMs = 0) {
|
|
9
|
+
return readDomUntil(runtime, `Array.from(document.querySelectorAll('[data-message-author-role="user"]'), user => user.getAttribute('data-message-id'))`, timeoutMs, (value) => Array.isArray(value) && value.every((id) => typeof id === "string" && id.trim())
|
|
10
|
+
? value
|
|
11
|
+
: undefined);
|
|
12
|
+
}
|
|
13
|
+
export async function readSubmittedPromptFingerprint(runtime, previousMessageIds, timeoutMs = 0) {
|
|
14
|
+
if (previousMessageIds === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
const previous = new Set(previousMessageIds);
|
|
17
|
+
return readDomUntil(runtime, `(() => {
|
|
18
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
19
|
+
for (let index = turns.length - 1; index >= 0; index--) {
|
|
20
|
+
const turn = turns[index];
|
|
21
|
+
const user = turn.matches('[data-message-author-role="user"]') ? turn : turn.querySelector('[data-message-author-role="user"]');
|
|
22
|
+
if (user) return { text: user.textContent, messageId: user.getAttribute('data-message-id') };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
})()`, timeoutMs, (value) => {
|
|
26
|
+
const turn = value;
|
|
27
|
+
if (typeof turn?.text === "string" &&
|
|
28
|
+
turn.text.trim() &&
|
|
29
|
+
typeof turn.messageId === "string" &&
|
|
30
|
+
turn.messageId.trim() &&
|
|
31
|
+
!previous.has(turn.messageId))
|
|
32
|
+
return browserPromptFingerprint(turn.text, turn.messageId);
|
|
33
|
+
return undefined;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function readDomUntil(runtime, expression, timeoutMs, select) {
|
|
37
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
38
|
+
for (;;) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await runtime.evaluate({ expression, returnByValue: true });
|
|
41
|
+
const selected = select(result.result?.value);
|
|
42
|
+
if (selected !== undefined)
|
|
43
|
+
return selected;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
if (!/Cannot find (?:default )?(?:execution )?context|Execution context (?:was destroyed|is not available)/i.test(message))
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (Date.now() >= deadline)
|
|
51
|
+
return undefined;
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
|
|
53
|
+
}
|
|
54
|
+
}
|