@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.
Files changed (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,13 +1,15 @@
1
- import chalk from 'chalk';
2
- import kleur from 'kleur';
3
- import { renderMarkdownAnsi } from './markdownRenderer.js';
4
- import { formatFinishLine } from '../oracle/finishLine.js';
5
- import { sessionStore, wait } from '../sessionStore.js';
6
- import { formatTokenCount, formatTokenValue } from '../oracle/runUtils.js';
7
- import { resumeBrowserSession } from '../browser/reattach.js';
8
- import { estimateTokenCount } from '../browser/utils.js';
9
- import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost } from './sessionTable.js';
10
- import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from './sessionLineage.js';
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 === 'ESRCH' || code === 'EINVAL') {
25
+ if (code === "ESRCH" || code === "EINVAL") {
24
26
  return false;
25
27
  }
26
- if (code === 'EPERM') {
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, { hours, includeAll, limit });
36
- const filteredEntries = modelFilter ? entries.filter((entry) => matchesModel(entry, modelFilter)) : entries;
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('Recent Sessions'));
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 === 'browser' && metadata.status === 'running' && !metadata.browser?.runtime) {
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 === 'chrome-disconnected';
96
- const statusAllowsReattach = metadata.status === 'running' || (metadata.status === 'error' && hasChromeDisconnect);
97
- const hasFallbackSessionInfo = Boolean(runtime?.chromePort || runtime?.tabUrl || runtime?.conversationId);
98
- const canReattach = statusAllowsReattach &&
99
- metadata.mode === 'browser' &&
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 || (runtime?.controllerPid && !controllerAlive));
179
+ (hasChromeDisconnect ||
180
+ hasIncompleteCapture ||
181
+ completedDeepResearchPlaceholder ||
182
+ (runtime?.controllerPid && !controllerAlive));
102
183
  if (canReattach) {
103
- const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : 'unknown port';
104
- const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : 'url=unknown';
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 logWriter = sessionStore.createLogWriter(sessionId);
114
- logWriter.logLine('[reattach] captured assistant response from existing Chrome tab');
115
- logWriter.logLine('Answer:');
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: 'completed',
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: 'completed',
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
- response: { status: 'completed' },
223
+ artifacts,
224
+ response: { status: "completed" },
144
225
  error: undefined,
145
226
  transport: undefined,
146
227
  });
147
- console.log(chalk.green('Reattach succeeded; session marked completed.'));
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('Models:');
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 === 'completed' || initialStatus === 'error';
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('Prompt:'));
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, 'utf8');
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 = 'Render requested but stdout is not a TTY; showing raw text.';
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('Render will apply after completion; streaming raw text meanwhile...'));
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
- ? { pending: '', inFence: false, inTable: false, renderedBytes: 0, fallback: false, noticedFallback: false }
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, 'utf8');
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, 'utf8');
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, 'utf8');
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 === 'completed' || latest.status === 'error') {
425
+ if (latest.status === "completed" || latest.status === "error") {
308
426
  await printNew();
309
427
  flushRemainder();
310
428
  if (!options?.suppressMetadata) {
311
- if (latest.status === 'error' && latest.errorMessage) {
312
- console.log('\nResult:');
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 === 'completed' && latest.usage) {
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(' | ') : null;
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
- 'client-timeout': 'client timeout (deadline exceeded)',
357
- 'connection-lost': 'connection lost before completion',
358
- 'client-abort': 'request aborted locally',
359
- unknown: 'unknown transport failure',
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] ?? 'transport error';
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(' | ') : null;
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 === 'running') {
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 = 'Answer:';
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('Usage Examples'));
446
- console.log(`${chalk.bold(' oracle status --hours 72 --limit 50')}`);
447
- console.log(dim(' Show 72h of history capped at 50 entries.'));
448
- console.log(`${chalk.bold(' oracle status --clear --hours 168')}`);
449
- console.log(dim(' Delete sessions older than 7 days (use --all to wipe everything).'));
450
- console.log(`${chalk.bold(' oracle session <session-id>')}`);
451
- console.log(dim(' Attach to a specific running/completed session to stream its output.'));
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()) ?? (entry.model ? [entry.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 ? '' : ' ')).join('')}${nodeBranch}`;
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('\n\n');
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 === '\n') {
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('|') && trimmed.includes('|')) {
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 === 'browser' ? `${metadata.model ?? 'n/a'}[browser]` : metadata.model ?? 'n/a';
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 === 'string' ? record.responseId : typeof record.id === 'string' ? record.id : null;
5
- if (!candidate || !candidate.startsWith('resp_')) {
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;