@steipete/oracle 0.9.0 → 0.11.0
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/LICENSE +1 -1
- package/README.md +107 -49
- package/dist/bin/oracle-cli.js +551 -410
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/archiveConversation.js +224 -0
- package/dist/src/browser/actions/assistantResponse.js +175 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +342 -119
- package/dist/src/browser/actions/navigation.js +183 -137
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +152 -91
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +276 -63
- package/dist/src/browser/config.js +59 -16
- package/dist/src/browser/constants.js +25 -12
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +250 -77
- package/dist/src/browser/domDebug.js +50 -1
- package/dist/src/browser/index.js +1559 -692
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +127 -42
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +178 -73
- package/dist/src/browser/reattachHelpers.js +32 -27
- package/dist/src/browser/sessionRunner.js +89 -25
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +24 -20
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +102 -48
- package/dist/src/cli/browserDefaults.js +51 -26
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +62 -26
- package/dist/src/cli/duplicatePromptGuard.js +12 -4
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +131 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +82 -21
- package/dist/src/cli/sessionDisplay.js +213 -87
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +149 -95
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +18 -12
- package/dist/src/mcp/tools/consult.js +246 -67
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +12 -5
- package/dist/src/mcp/utils.js +21 -8
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +160 -135
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +78 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +67 -62
- package/vendor/oracle-notifier/README.md +2 -0
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
- /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import chalk from
|
|
2
|
-
import kleur from
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import kleur from "kleur";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { renderMarkdownAnsi } from "./markdownRenderer.js";
|
|
5
|
+
import { formatFinishLine } from "../oracle/finishLine.js";
|
|
6
|
+
import { sessionStore, wait } from "../sessionStore.js";
|
|
7
|
+
import { formatTokenCount, formatTokenValue } from "../oracle/runUtils.js";
|
|
8
|
+
import { resumeBrowserSession } from "../browser/reattach.js";
|
|
9
|
+
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "../browser/artifacts.js";
|
|
10
|
+
import { estimateTokenCount } from "../browser/utils.js";
|
|
11
|
+
import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
|
|
12
|
+
import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
|
|
11
13
|
const isTty = () => Boolean(process.stdout.isTTY);
|
|
12
14
|
const dim = (text) => (isTty() ? kleur.dim(text) : text);
|
|
13
15
|
export const MAX_RENDER_BYTES = 200_000;
|
|
@@ -20,20 +22,86 @@ function isProcessAlive(pid) {
|
|
|
20
22
|
}
|
|
21
23
|
catch (error) {
|
|
22
24
|
const code = error instanceof Error ? error.code : undefined;
|
|
23
|
-
if (code ===
|
|
25
|
+
if (code === "ESRCH" || code === "EINVAL") {
|
|
24
26
|
return false;
|
|
25
27
|
}
|
|
26
|
-
if (code ===
|
|
28
|
+
if (code === "EPERM") {
|
|
27
29
|
return true;
|
|
28
30
|
}
|
|
29
31
|
return true;
|
|
30
32
|
}
|
|
31
33
|
}
|
|
34
|
+
function formatBytes(bytes) {
|
|
35
|
+
if (bytes < 1024)
|
|
36
|
+
return `${bytes} B`;
|
|
37
|
+
if (bytes < 1024 * 1024)
|
|
38
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
39
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
40
|
+
}
|
|
41
|
+
function isDeepResearchBrowserSession(metadata) {
|
|
42
|
+
return metadata.mode === "browser" && metadata.browser?.config?.researchMode === "deep";
|
|
43
|
+
}
|
|
44
|
+
function isDeepResearchPlaceholderCapture(metadata, logText) {
|
|
45
|
+
const answer = trimBeforeFirstAnswer(logText)
|
|
46
|
+
.replace(/^Answer:\s*/i, "")
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/\s+/g, " ")
|
|
49
|
+
.trim();
|
|
50
|
+
const isToolOnly = answer === "called tool" ||
|
|
51
|
+
answer === "used tool" ||
|
|
52
|
+
answer === "użyto narzędzia" ||
|
|
53
|
+
answer === "narzędzie wywołane";
|
|
54
|
+
const modelUsage = metadata.models?.find((run) => run.model === metadata.model)?.usage;
|
|
55
|
+
const outputTokens = metadata.usage?.outputTokens ?? modelUsage?.outputTokens;
|
|
56
|
+
return isToolOnly && (outputTokens == null || outputTokens <= 8);
|
|
57
|
+
}
|
|
58
|
+
async function writeReattachAnswer(sessionId, result, replaceExistingLog) {
|
|
59
|
+
const body = result.answerMarkdown || result.answerText;
|
|
60
|
+
if (replaceExistingLog) {
|
|
61
|
+
const paths = await sessionStore.getPaths(sessionId);
|
|
62
|
+
await fs.writeFile(paths.log, `[reattach] replaced incomplete Deep Research capture from existing Chrome tab\nAnswer:\n${body}\n`, "utf8");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const logWriter = sessionStore.createLogWriter(sessionId);
|
|
66
|
+
logWriter.logLine("[reattach] captured assistant response from existing Chrome tab");
|
|
67
|
+
logWriter.logLine("Answer:");
|
|
68
|
+
logWriter.logLine(body);
|
|
69
|
+
logWriter.stream.end();
|
|
70
|
+
}
|
|
71
|
+
async function saveReattachBrowserArtifacts(sessionId, metadata, result) {
|
|
72
|
+
const body = result.answerMarkdown || result.answerText;
|
|
73
|
+
const conversationUrl = metadata.browser?.runtime?.tabUrl;
|
|
74
|
+
const logger = ((message) => console.log(dim(message)));
|
|
75
|
+
const reportArtifact = isDeepResearchBrowserSession(metadata)
|
|
76
|
+
? await saveDeepResearchReportArtifact({
|
|
77
|
+
sessionId,
|
|
78
|
+
reportMarkdown: body,
|
|
79
|
+
conversationUrl,
|
|
80
|
+
logger,
|
|
81
|
+
}).catch(() => null)
|
|
82
|
+
: null;
|
|
83
|
+
const prompt = (await readStoredPrompt(sessionId)) ?? metadata.promptPreview ?? "";
|
|
84
|
+
const transcriptArtifact = await saveBrowserTranscriptArtifact({
|
|
85
|
+
sessionId,
|
|
86
|
+
prompt,
|
|
87
|
+
answerMarkdown: body,
|
|
88
|
+
conversationUrl,
|
|
89
|
+
artifacts: appendArtifacts(undefined, [reportArtifact]),
|
|
90
|
+
logger,
|
|
91
|
+
}).catch(() => null);
|
|
92
|
+
return appendArtifacts(metadata.artifacts, [reportArtifact, transcriptArtifact]);
|
|
93
|
+
}
|
|
32
94
|
const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
|
|
33
95
|
export async function showStatus({ hours, includeAll, limit, showExamples = false, modelFilter, }) {
|
|
34
96
|
const metas = await sessionStore.listSessions();
|
|
35
|
-
const { entries, truncated, total } = sessionStore.filterSessions(metas, {
|
|
36
|
-
|
|
97
|
+
const { entries, truncated, total } = sessionStore.filterSessions(metas, {
|
|
98
|
+
hours,
|
|
99
|
+
includeAll,
|
|
100
|
+
limit,
|
|
101
|
+
});
|
|
102
|
+
const filteredEntries = modelFilter
|
|
103
|
+
? entries.filter((entry) => matchesModel(entry, modelFilter))
|
|
104
|
+
: entries;
|
|
37
105
|
const richTty = process.stdout.isTTY && chalk.level > 0;
|
|
38
106
|
const responseOwners = buildResponseOwnerIndex(metas);
|
|
39
107
|
if (!filteredEntries.length) {
|
|
@@ -43,7 +111,7 @@ export async function showStatus({ hours, includeAll, limit, showExamples = fals
|
|
|
43
111
|
}
|
|
44
112
|
return;
|
|
45
113
|
}
|
|
46
|
-
console.log(chalk.bold(
|
|
114
|
+
console.log(chalk.bold("Recent Sessions"));
|
|
47
115
|
console.log(formatSessionTableHeader(richTty));
|
|
48
116
|
const treeRows = buildStatusTreeRows(filteredEntries, responseOwners);
|
|
49
117
|
for (const row of treeRows) {
|
|
@@ -52,7 +120,7 @@ export async function showStatus({ hours, includeAll, limit, showExamples = fals
|
|
|
52
120
|
? richTty
|
|
53
121
|
? chalk.gray(` <- ${row.detachedParentLabel}`)
|
|
54
122
|
: ` <- ${row.detachedParentLabel}`
|
|
55
|
-
:
|
|
123
|
+
: "";
|
|
56
124
|
console.log(`${line}${detachedParent}`);
|
|
57
125
|
}
|
|
58
126
|
if (truncated) {
|
|
@@ -70,7 +138,7 @@ export async function attachSession(sessionId, options) {
|
|
|
70
138
|
process.exitCode = 1;
|
|
71
139
|
return;
|
|
72
140
|
}
|
|
73
|
-
if (metadata.mode ===
|
|
141
|
+
if (metadata.mode === "browser" && metadata.status === "running" && !metadata.browser?.runtime) {
|
|
74
142
|
await wait(250);
|
|
75
143
|
const refreshed = await sessionStore.readSession(sessionId);
|
|
76
144
|
if (refreshed) {
|
|
@@ -92,16 +160,29 @@ export async function attachSession(sessionId, options) {
|
|
|
92
160
|
const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
|
|
93
161
|
const runtime = metadata.browser?.runtime;
|
|
94
162
|
const controllerAlive = isProcessAlive(runtime?.controllerPid);
|
|
95
|
-
const hasChromeDisconnect = metadata.response?.incompleteReason ===
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
163
|
+
const hasChromeDisconnect = metadata.response?.incompleteReason === "chrome-disconnected";
|
|
164
|
+
const hasIncompleteCapture = metadata.response?.incompleteReason === "incomplete-capture";
|
|
165
|
+
const statusAllowsReattach = metadata.status === "running" ||
|
|
166
|
+
(metadata.status === "error" && (hasChromeDisconnect || hasIncompleteCapture));
|
|
167
|
+
const hasFallbackSessionInfo = Boolean(runtime?.chromePort ||
|
|
168
|
+
runtime?.chromeBrowserWSEndpoint ||
|
|
169
|
+
runtime?.chromeProfileRoot ||
|
|
170
|
+
runtime?.tabUrl ||
|
|
171
|
+
runtime?.conversationId);
|
|
172
|
+
const deepResearchPlaceholderCapture = isDeepResearchBrowserSession(metadata) &&
|
|
173
|
+
hasFallbackSessionInfo &&
|
|
174
|
+
isDeepResearchPlaceholderCapture(metadata, await sessionStore.readLog(sessionId).catch(() => ""));
|
|
175
|
+
const completedDeepResearchPlaceholder = metadata.status === "completed" && deepResearchPlaceholderCapture;
|
|
176
|
+
const canReattach = (statusAllowsReattach || completedDeepResearchPlaceholder) &&
|
|
177
|
+
metadata.mode === "browser" &&
|
|
100
178
|
hasFallbackSessionInfo &&
|
|
101
|
-
(hasChromeDisconnect ||
|
|
179
|
+
(hasChromeDisconnect ||
|
|
180
|
+
hasIncompleteCapture ||
|
|
181
|
+
completedDeepResearchPlaceholder ||
|
|
182
|
+
(runtime?.controllerPid && !controllerAlive));
|
|
102
183
|
if (canReattach) {
|
|
103
|
-
const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` :
|
|
104
|
-
const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` :
|
|
184
|
+
const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : "unknown port";
|
|
185
|
+
const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : "url=unknown";
|
|
105
186
|
console.log(chalk.yellow(`Attempting to reattach to the existing Chrome session (${portInfo}, ${urlInfo})...`));
|
|
106
187
|
try {
|
|
107
188
|
const result = await resumeBrowserSession(runtime, metadata.browser?.config, Object.assign(((message) => {
|
|
@@ -110,14 +191,12 @@ export async function attachSession(sessionId, options) {
|
|
|
110
191
|
}
|
|
111
192
|
}), { verbose: true }), { promptPreview: metadata.promptPreview });
|
|
112
193
|
const outputTokens = estimateTokenCount(result.answerMarkdown);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
logWriter.logLine(result.answerMarkdown || result.answerText);
|
|
117
|
-
logWriter.stream.end();
|
|
194
|
+
const artifacts = await saveReattachBrowserArtifacts(sessionId, metadata, result);
|
|
195
|
+
await writeReattachAnswer(sessionId, result, completedDeepResearchPlaceholder ||
|
|
196
|
+
(hasIncompleteCapture && deepResearchPlaceholderCapture));
|
|
118
197
|
if (metadata.model) {
|
|
119
198
|
await sessionStore.updateModelRun(metadata.id, metadata.model, {
|
|
120
|
-
status:
|
|
199
|
+
status: "completed",
|
|
121
200
|
usage: {
|
|
122
201
|
inputTokens: 0,
|
|
123
202
|
outputTokens,
|
|
@@ -128,7 +207,7 @@ export async function attachSession(sessionId, options) {
|
|
|
128
207
|
});
|
|
129
208
|
}
|
|
130
209
|
await sessionStore.updateSession(sessionId, {
|
|
131
|
-
status:
|
|
210
|
+
status: "completed",
|
|
132
211
|
completedAt: new Date().toISOString(),
|
|
133
212
|
usage: {
|
|
134
213
|
inputTokens: 0,
|
|
@@ -136,20 +215,44 @@ export async function attachSession(sessionId, options) {
|
|
|
136
215
|
reasoningTokens: 0,
|
|
137
216
|
totalTokens: outputTokens,
|
|
138
217
|
},
|
|
218
|
+
errorMessage: undefined,
|
|
139
219
|
browser: {
|
|
140
220
|
config: metadata.browser?.config,
|
|
141
221
|
runtime,
|
|
142
222
|
},
|
|
143
|
-
|
|
223
|
+
artifacts,
|
|
224
|
+
response: { status: "completed" },
|
|
144
225
|
error: undefined,
|
|
145
226
|
transport: undefined,
|
|
146
227
|
});
|
|
147
|
-
console.log(chalk.green(
|
|
228
|
+
console.log(chalk.green("Reattach succeeded; session marked completed."));
|
|
148
229
|
metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
|
|
149
230
|
}
|
|
150
231
|
catch (error) {
|
|
151
232
|
const message = error instanceof Error ? error.message : String(error);
|
|
152
233
|
console.log(chalk.red(`Reattach failed: ${message}`));
|
|
234
|
+
if (completedDeepResearchPlaceholder) {
|
|
235
|
+
if (metadata.model) {
|
|
236
|
+
await sessionStore.updateModelRun(metadata.id, metadata.model, {
|
|
237
|
+
status: "error",
|
|
238
|
+
response: { status: "incomplete", incompleteReason: "incomplete-capture" },
|
|
239
|
+
error: {
|
|
240
|
+
category: "browser-automation",
|
|
241
|
+
message: `Deep Research capture incomplete: ${message}`,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
await sessionStore.updateSession(sessionId, {
|
|
246
|
+
status: "error",
|
|
247
|
+
errorMessage: `Deep Research capture incomplete: ${message}`,
|
|
248
|
+
response: { status: "incomplete", incompleteReason: "incomplete-capture" },
|
|
249
|
+
error: {
|
|
250
|
+
category: "browser-automation",
|
|
251
|
+
message: `Deep Research capture incomplete: ${message}`,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
|
|
255
|
+
}
|
|
153
256
|
}
|
|
154
257
|
}
|
|
155
258
|
if (!options?.suppressMetadata) {
|
|
@@ -164,17 +267,25 @@ export async function attachSession(sessionId, options) {
|
|
|
164
267
|
console.log(`Created: ${metadata.createdAt}`);
|
|
165
268
|
console.log(`Status: ${metadata.status}`);
|
|
166
269
|
if (metadata.models && metadata.models.length > 0) {
|
|
167
|
-
console.log(
|
|
270
|
+
console.log("Models:");
|
|
168
271
|
for (const run of metadata.models) {
|
|
169
272
|
const usage = run.usage
|
|
170
273
|
? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
|
|
171
|
-
:
|
|
274
|
+
: "";
|
|
172
275
|
console.log(`- ${chalk.cyan(run.model)} — ${run.status}${usage}`);
|
|
173
276
|
}
|
|
174
277
|
}
|
|
175
278
|
else if (metadata.model) {
|
|
176
279
|
console.log(`Model: ${metadata.model}`);
|
|
177
280
|
}
|
|
281
|
+
if (metadata.artifacts && metadata.artifacts.length > 0) {
|
|
282
|
+
console.log("Artifacts:");
|
|
283
|
+
for (const artifact of metadata.artifacts) {
|
|
284
|
+
const label = artifact.label ?? artifact.kind;
|
|
285
|
+
const size = artifact.sizeBytes ? ` (${formatBytes(artifact.sizeBytes)})` : "";
|
|
286
|
+
console.log(`- ${chalk.cyan(label)} — ${artifact.path}${size}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
178
289
|
const responseSummary = formatResponseMetadata(metadata.response);
|
|
179
290
|
if (responseSummary) {
|
|
180
291
|
console.log(dim(`Response: ${responseSummary}`));
|
|
@@ -188,19 +299,19 @@ export async function attachSession(sessionId, options) {
|
|
|
188
299
|
console.log(dim(`User error: ${userErrorSummary}`));
|
|
189
300
|
}
|
|
190
301
|
}
|
|
191
|
-
const shouldTrimIntro = initialStatus ===
|
|
302
|
+
const shouldTrimIntro = initialStatus === "completed" || initialStatus === "error";
|
|
192
303
|
if (options?.renderPrompt !== false) {
|
|
193
304
|
const prompt = await readStoredPrompt(sessionId);
|
|
194
305
|
if (prompt) {
|
|
195
|
-
console.log(chalk.bold(
|
|
306
|
+
console.log(chalk.bold("Prompt:"));
|
|
196
307
|
console.log(renderMarkdownAnsi(prompt));
|
|
197
|
-
console.log(dim(
|
|
308
|
+
console.log(dim("---"));
|
|
198
309
|
}
|
|
199
310
|
}
|
|
200
311
|
if (shouldTrimIntro) {
|
|
201
312
|
const fullLog = await buildSessionLogForDisplay(sessionId, metadata, normalizedModelFilter);
|
|
202
313
|
const trimmed = trimBeforeFirstAnswer(fullLog);
|
|
203
|
-
const size = Buffer.byteLength(trimmed,
|
|
314
|
+
const size = Buffer.byteLength(trimmed, "utf8");
|
|
204
315
|
const canRender = wantsRender && isTty() && size <= MAX_RENDER_BYTES;
|
|
205
316
|
if (wantsRender && size > MAX_RENDER_BYTES) {
|
|
206
317
|
const msg = `Render skipped (log too large: ${size} bytes > ${MAX_RENDER_BYTES}). Showing raw text.`;
|
|
@@ -210,7 +321,7 @@ export async function attachSession(sessionId, options) {
|
|
|
210
321
|
}
|
|
211
322
|
}
|
|
212
323
|
else if (wantsRender && !isTty()) {
|
|
213
|
-
const msg =
|
|
324
|
+
const msg = "Render requested but stdout is not a TTY; showing raw text.";
|
|
214
325
|
console.log(dim(msg));
|
|
215
326
|
if (isVerbose) {
|
|
216
327
|
console.log(dim(`Verbose: renderMarkdown=true tty=${isTty()} size=${size}`));
|
|
@@ -232,13 +343,20 @@ export async function attachSession(sessionId, options) {
|
|
|
232
343
|
return;
|
|
233
344
|
}
|
|
234
345
|
if (wantsRender) {
|
|
235
|
-
console.log(dim(
|
|
346
|
+
console.log(dim("Render will apply after completion; streaming raw text meanwhile..."));
|
|
236
347
|
if (isVerbose) {
|
|
237
348
|
console.log(dim(`Verbose: streaming phase renderMarkdown=true tty=${isTty()}`));
|
|
238
349
|
}
|
|
239
350
|
}
|
|
240
351
|
const liveRenderState = wantsRender && isTty()
|
|
241
|
-
? {
|
|
352
|
+
? {
|
|
353
|
+
pending: "",
|
|
354
|
+
inFence: false,
|
|
355
|
+
inTable: false,
|
|
356
|
+
renderedBytes: 0,
|
|
357
|
+
fallback: false,
|
|
358
|
+
noticedFallback: false,
|
|
359
|
+
}
|
|
242
360
|
: null;
|
|
243
361
|
let lastLength = 0;
|
|
244
362
|
const renderLiveChunk = (chunk) => {
|
|
@@ -254,7 +372,7 @@ export async function attachSession(sessionId, options) {
|
|
|
254
372
|
const { chunks, remainder } = extractRenderableChunks(liveRenderState.pending, liveRenderState);
|
|
255
373
|
liveRenderState.pending = remainder;
|
|
256
374
|
for (const candidate of chunks) {
|
|
257
|
-
const projected = liveRenderState.renderedBytes + Buffer.byteLength(candidate,
|
|
375
|
+
const projected = liveRenderState.renderedBytes + Buffer.byteLength(candidate, "utf8");
|
|
258
376
|
if (projected > MAX_RENDER_BYTES) {
|
|
259
377
|
if (!liveRenderState.noticedFallback) {
|
|
260
378
|
console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
|
|
@@ -262,11 +380,11 @@ export async function attachSession(sessionId, options) {
|
|
|
262
380
|
}
|
|
263
381
|
liveRenderState.fallback = true;
|
|
264
382
|
process.stdout.write(candidate + liveRenderState.pending);
|
|
265
|
-
liveRenderState.pending =
|
|
383
|
+
liveRenderState.pending = "";
|
|
266
384
|
return;
|
|
267
385
|
}
|
|
268
386
|
process.stdout.write(renderMarkdownAnsi(candidate));
|
|
269
|
-
liveRenderState.renderedBytes += Buffer.byteLength(candidate,
|
|
387
|
+
liveRenderState.renderedBytes += Buffer.byteLength(candidate, "utf8");
|
|
270
388
|
}
|
|
271
389
|
};
|
|
272
390
|
const flushRemainder = () => {
|
|
@@ -277,8 +395,8 @@ export async function attachSession(sessionId, options) {
|
|
|
277
395
|
return;
|
|
278
396
|
}
|
|
279
397
|
const text = liveRenderState.pending;
|
|
280
|
-
liveRenderState.pending =
|
|
281
|
-
const projected = liveRenderState.renderedBytes + Buffer.byteLength(text,
|
|
398
|
+
liveRenderState.pending = "";
|
|
399
|
+
const projected = liveRenderState.renderedBytes + Buffer.byteLength(text, "utf8");
|
|
282
400
|
if (projected > MAX_RENDER_BYTES) {
|
|
283
401
|
if (!liveRenderState.noticedFallback) {
|
|
284
402
|
console.log(dim(`Render skipped (log too large: > ${MAX_RENDER_BYTES} bytes). Showing raw text.`));
|
|
@@ -304,15 +422,15 @@ export async function attachSession(sessionId, options) {
|
|
|
304
422
|
if (!latest) {
|
|
305
423
|
break;
|
|
306
424
|
}
|
|
307
|
-
if (latest.status ===
|
|
425
|
+
if (latest.status === "completed" || latest.status === "error") {
|
|
308
426
|
await printNew();
|
|
309
427
|
flushRemainder();
|
|
310
428
|
if (!options?.suppressMetadata) {
|
|
311
|
-
if (latest.status ===
|
|
312
|
-
console.log(
|
|
429
|
+
if (latest.status === "error" && latest.errorMessage) {
|
|
430
|
+
console.log("\nResult:");
|
|
313
431
|
console.log(`Session failed: ${latest.errorMessage}`);
|
|
314
432
|
}
|
|
315
|
-
if (latest.status ===
|
|
433
|
+
if (latest.status === "completed" && latest.usage) {
|
|
316
434
|
const summary = formatCompletionSummary(latest, { includeSlug: true });
|
|
317
435
|
if (summary) {
|
|
318
436
|
console.log(`\n${chalk.green.bold(summary)}`);
|
|
@@ -346,19 +464,19 @@ export function formatResponseMetadata(metadata) {
|
|
|
346
464
|
if (metadata.incompleteReason) {
|
|
347
465
|
parts.push(`incomplete=${metadata.incompleteReason}`);
|
|
348
466
|
}
|
|
349
|
-
return parts.length > 0 ? parts.join(
|
|
467
|
+
return parts.length > 0 ? parts.join(" | ") : null;
|
|
350
468
|
}
|
|
351
469
|
export function formatTransportMetadata(metadata) {
|
|
352
470
|
if (!metadata?.reason) {
|
|
353
471
|
return null;
|
|
354
472
|
}
|
|
355
473
|
const reasonLabels = {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
unknown:
|
|
474
|
+
"client-timeout": "client timeout (deadline exceeded)",
|
|
475
|
+
"connection-lost": "connection lost before completion",
|
|
476
|
+
"client-abort": "request aborted locally",
|
|
477
|
+
unknown: "unknown transport failure",
|
|
360
478
|
};
|
|
361
|
-
const label = reasonLabels[metadata.reason] ??
|
|
479
|
+
const label = reasonLabels[metadata.reason] ?? "transport error";
|
|
362
480
|
return `${metadata.reason} — ${label}`;
|
|
363
481
|
}
|
|
364
482
|
export function formatUserErrorMetadata(metadata) {
|
|
@@ -375,7 +493,7 @@ export function formatUserErrorMetadata(metadata) {
|
|
|
375
493
|
if (metadata.details && Object.keys(metadata.details).length > 0) {
|
|
376
494
|
parts.push(`details=${JSON.stringify(metadata.details)}`);
|
|
377
495
|
}
|
|
378
|
-
return parts.length > 0 ? parts.join(
|
|
496
|
+
return parts.length > 0 ? parts.join(" | ") : null;
|
|
379
497
|
}
|
|
380
498
|
export function buildReattachLine(metadata) {
|
|
381
499
|
if (!metadata.id) {
|
|
@@ -389,17 +507,24 @@ export function buildReattachLine(metadata) {
|
|
|
389
507
|
if (!elapsedLabel) {
|
|
390
508
|
return null;
|
|
391
509
|
}
|
|
392
|
-
if (metadata.status ===
|
|
510
|
+
if (metadata.status === "running") {
|
|
393
511
|
return `Session ${metadata.id} reattached, request started ${elapsedLabel} ago.`;
|
|
394
512
|
}
|
|
395
513
|
return null;
|
|
396
514
|
}
|
|
397
515
|
export function trimBeforeFirstAnswer(logText) {
|
|
398
|
-
const marker =
|
|
516
|
+
const marker = "Answer:";
|
|
399
517
|
const index = logText.indexOf(marker);
|
|
400
518
|
if (index === -1) {
|
|
401
519
|
return logText;
|
|
402
520
|
}
|
|
521
|
+
const fromFirstAnswer = logText.slice(index);
|
|
522
|
+
if (/^Answer:\s*(called tool|used tool|użyto narzędzia|narzędzie wywołane)\s*\n\[reattach\]/i.test(fromFirstAnswer)) {
|
|
523
|
+
const laterIndex = logText.lastIndexOf(marker);
|
|
524
|
+
if (laterIndex > index) {
|
|
525
|
+
return logText.slice(laterIndex);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
403
528
|
return logText.slice(index);
|
|
404
529
|
}
|
|
405
530
|
function formatRelativeDuration(referenceIso) {
|
|
@@ -427,7 +552,7 @@ function formatRelativeDuration(referenceIso) {
|
|
|
427
552
|
if (remainingMinutes > 0) {
|
|
428
553
|
parts.push(`${remainingMinutes}m`);
|
|
429
554
|
}
|
|
430
|
-
return parts.join(
|
|
555
|
+
return parts.join(" ");
|
|
431
556
|
}
|
|
432
557
|
const days = Math.floor(hours / 24);
|
|
433
558
|
const remainingHours = hours % 24;
|
|
@@ -438,17 +563,17 @@ function formatRelativeDuration(referenceIso) {
|
|
|
438
563
|
if (remainingMinutes > 0 && days === 0) {
|
|
439
564
|
parts.push(`${remainingMinutes}m`);
|
|
440
565
|
}
|
|
441
|
-
return parts.join(
|
|
566
|
+
return parts.join(" ");
|
|
442
567
|
}
|
|
443
568
|
function printStatusExamples() {
|
|
444
|
-
console.log(
|
|
445
|
-
console.log(chalk.bold(
|
|
446
|
-
console.log(`${chalk.bold(
|
|
447
|
-
console.log(dim(
|
|
448
|
-
console.log(`${chalk.bold(
|
|
449
|
-
console.log(dim(
|
|
450
|
-
console.log(`${chalk.bold(
|
|
451
|
-
console.log(dim(
|
|
569
|
+
console.log("");
|
|
570
|
+
console.log(chalk.bold("Usage Examples"));
|
|
571
|
+
console.log(`${chalk.bold(" oracle status --hours 72 --limit 50")}`);
|
|
572
|
+
console.log(dim(" Show 72h of history capped at 50 entries."));
|
|
573
|
+
console.log(`${chalk.bold(" oracle status --clear --hours 168")}`);
|
|
574
|
+
console.log(dim(" Delete sessions older than 7 days (use --all to wipe everything)."));
|
|
575
|
+
console.log(`${chalk.bold(" oracle session <session-id>")}`);
|
|
576
|
+
console.log(dim(" Attach to a specific running/completed session to stream its output."));
|
|
452
577
|
console.log(dim(CLEANUP_TIP));
|
|
453
578
|
}
|
|
454
579
|
function matchesModel(entry, filter) {
|
|
@@ -456,7 +581,8 @@ function matchesModel(entry, filter) {
|
|
|
456
581
|
if (!normalized) {
|
|
457
582
|
return true;
|
|
458
583
|
}
|
|
459
|
-
const models = entry.models?.map((model) => model.model.toLowerCase()) ??
|
|
584
|
+
const models = entry.models?.map((model) => model.model.toLowerCase()) ??
|
|
585
|
+
(entry.model ? [entry.model.toLowerCase()] : []);
|
|
460
586
|
return models.includes(normalized);
|
|
461
587
|
}
|
|
462
588
|
function buildStatusTreeRows(entries, responseOwners) {
|
|
@@ -485,8 +611,8 @@ function buildStatusTreeRows(entries, responseOwners) {
|
|
|
485
611
|
}
|
|
486
612
|
visited.add(entry.id);
|
|
487
613
|
const children = childMap.get(entry.id) ?? [];
|
|
488
|
-
const nodeBranch = isLast ?
|
|
489
|
-
const prefix = `${ancestorHasMore.map((hasMore) => (hasMore ?
|
|
614
|
+
const nodeBranch = isLast ? "└─ " : "├─ ";
|
|
615
|
+
const prefix = `${ancestorHasMore.map((hasMore) => (hasMore ? "│ " : " ")).join("")}${nodeBranch}`;
|
|
490
616
|
rows.push({ entry, displaySlug: `${prefix}${entry.id}` });
|
|
491
617
|
children.forEach((child, index) => {
|
|
492
618
|
walkChild(child, [...ancestorHasMore, !isLast], index === children.length - 1);
|
|
@@ -549,12 +675,12 @@ async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
|
|
|
549
675
|
? models.filter((model) => model.model.toLowerCase() === normalizedFilter)
|
|
550
676
|
: models;
|
|
551
677
|
if (candidates.length === 0) {
|
|
552
|
-
return
|
|
678
|
+
return "";
|
|
553
679
|
}
|
|
554
680
|
const sections = [];
|
|
555
681
|
let hasContent = false;
|
|
556
682
|
for (const model of candidates) {
|
|
557
|
-
const body = (await sessionStore.readModelLog(sessionId, model.model)) ??
|
|
683
|
+
const body = (await sessionStore.readModelLog(sessionId, model.model)) ?? "";
|
|
558
684
|
if (body.trim().length > 0) {
|
|
559
685
|
hasContent = true;
|
|
560
686
|
}
|
|
@@ -564,18 +690,18 @@ async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
|
|
|
564
690
|
// Fallback for runs that recorded output only in the session log (e.g., browser runs without per-model logs).
|
|
565
691
|
return await sessionStore.readLog(sessionId);
|
|
566
692
|
}
|
|
567
|
-
return sections.join(
|
|
693
|
+
return sections.join("\n\n");
|
|
568
694
|
}
|
|
569
695
|
function extractRenderableChunks(text, state) {
|
|
570
696
|
const chunks = [];
|
|
571
|
-
let buffer =
|
|
697
|
+
let buffer = "";
|
|
572
698
|
const lines = text.split(/(\n)/);
|
|
573
699
|
for (let i = 0; i < lines.length; i += 1) {
|
|
574
700
|
const segment = lines[i];
|
|
575
|
-
if (segment ===
|
|
701
|
+
if (segment === "\n") {
|
|
576
702
|
buffer += segment;
|
|
577
703
|
// Detect code fences
|
|
578
|
-
const prev = lines[i - 1] ??
|
|
704
|
+
const prev = lines[i - 1] ?? "";
|
|
579
705
|
const fenceMatch = prev.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
|
|
580
706
|
if (!state.inFence && fenceMatch) {
|
|
581
707
|
state.inFence = true;
|
|
@@ -587,17 +713,17 @@ function extractRenderableChunks(text, state) {
|
|
|
587
713
|
}
|
|
588
714
|
const trimmed = prev.trim();
|
|
589
715
|
if (!state.inFence) {
|
|
590
|
-
if (!state.inTable && trimmed.startsWith(
|
|
716
|
+
if (!state.inTable && trimmed.startsWith("|") && trimmed.includes("|")) {
|
|
591
717
|
state.inTable = true;
|
|
592
718
|
}
|
|
593
|
-
if (state.inTable && trimmed ===
|
|
719
|
+
if (state.inTable && trimmed === "") {
|
|
594
720
|
state.inTable = false;
|
|
595
721
|
}
|
|
596
722
|
}
|
|
597
|
-
const safeBreak = !state.inFence && !state.inTable && trimmed ===
|
|
723
|
+
const safeBreak = !state.inFence && !state.inTable && trimmed === "";
|
|
598
724
|
if (safeBreak) {
|
|
599
725
|
chunks.push(buffer);
|
|
600
|
-
buffer =
|
|
726
|
+
buffer = "";
|
|
601
727
|
}
|
|
602
728
|
continue;
|
|
603
729
|
}
|
|
@@ -609,7 +735,7 @@ export function formatCompletionSummary(metadata, options = {}) {
|
|
|
609
735
|
if (!metadata.usage || metadata.elapsedMs == null) {
|
|
610
736
|
return null;
|
|
611
737
|
}
|
|
612
|
-
const modeLabel = metadata.mode ===
|
|
738
|
+
const modeLabel = metadata.mode === "browser" ? `${metadata.model ?? "n/a"}[browser]` : (metadata.model ?? "n/a");
|
|
613
739
|
const usage = metadata.usage;
|
|
614
740
|
const cost = resolveSessionCost(metadata);
|
|
615
741
|
const tokensDisplay = [
|
|
@@ -624,9 +750,9 @@ export function formatCompletionSummary(metadata, options = {}) {
|
|
|
624
750
|
reasoning_tokens: usage.reasoningTokens,
|
|
625
751
|
total_tokens: usage.totalTokens,
|
|
626
752
|
}, index))
|
|
627
|
-
.join(
|
|
753
|
+
.join("/");
|
|
628
754
|
const tokensPart = (() => {
|
|
629
|
-
const parts = tokensDisplay.split(
|
|
755
|
+
const parts = tokensDisplay.split("/");
|
|
630
756
|
if (parts.length !== 4)
|
|
631
757
|
return tokensDisplay;
|
|
632
758
|
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
function readResponseId(record) {
|
|
2
2
|
if (!record)
|
|
3
3
|
return null;
|
|
4
|
-
const candidate = typeof record.responseId ===
|
|
5
|
-
|
|
4
|
+
const candidate = typeof record.responseId === "string"
|
|
5
|
+
? record.responseId
|
|
6
|
+
: typeof record.id === "string"
|
|
7
|
+
? record.id
|
|
8
|
+
: null;
|
|
9
|
+
if (!candidate || !candidate.startsWith("resp_")) {
|
|
6
10
|
return null;
|
|
7
11
|
}
|
|
8
12
|
return candidate;
|