codex-chatgpt-control 0.2.0-alpha.1 → 0.3.0-alpha.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.
Files changed (179) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/contracts/v1/fixtures/backend-capabilities.json +1 -0
  3. package/contracts/v1/fixtures/command-descriptors.json +33 -2
  4. package/contracts/v1/fixtures/doctor-scenario-preflight.json +12 -1
  5. package/contracts/v1/fixtures/help-root.json +1 -1
  6. package/contracts/v1/fixtures/reports-create-redacted.json +1 -1
  7. package/contracts/v1/parity-suite.json +8 -1
  8. package/dist/codex-chatgpt-control-backend.mjs +1199 -159
  9. package/dist/codex-chatgpt-control.bundle.mjs +1198 -156
  10. package/dist/src/backend/protocol.d.ts +1 -1
  11. package/dist/src/backend/protocol.js +1 -0
  12. package/dist/src/backend/session.js +2 -0
  13. package/dist/src/browser/clipboard.d.ts +11 -0
  14. package/dist/src/browser/clipboard.js +34 -7
  15. package/dist/src/browser/page-state.js +12 -2
  16. package/dist/src/client.d.ts +2 -1
  17. package/dist/src/client.js +3 -2
  18. package/dist/src/commands/context.js +3 -1
  19. package/dist/src/commands/conversation.d.ts +15 -0
  20. package/dist/src/commands/conversation.js +44 -0
  21. package/dist/src/commands/deadline.d.ts +8 -0
  22. package/dist/src/commands/deadline.js +14 -0
  23. package/dist/src/commands/doctor.js +9 -2
  24. package/dist/src/commands/files.js +62 -8
  25. package/dist/src/commands/messages.js +212 -72
  26. package/dist/src/commands/modes.d.ts +4 -1
  27. package/dist/src/commands/modes.js +215 -30
  28. package/dist/src/commands/probes.d.ts +15 -0
  29. package/dist/src/commands/probes.js +64 -0
  30. package/dist/src/commands/registry.js +34 -2
  31. package/dist/src/commands/reports.js +1 -1
  32. package/dist/src/commands/threads.js +2 -24
  33. package/dist/src/dom/generation-state.d.ts +7 -0
  34. package/dist/src/dom/generation-state.js +7 -1
  35. package/dist/src/dom/label-match.d.ts +3 -0
  36. package/dist/src/dom/label-match.js +25 -0
  37. package/dist/src/dom/locale/am.d.ts +6 -0
  38. package/dist/src/dom/locale/am.js +6 -0
  39. package/dist/src/dom/locale/ar.d.ts +7 -0
  40. package/dist/src/dom/locale/ar.js +7 -0
  41. package/dist/src/dom/locale/bg.d.ts +7 -0
  42. package/dist/src/dom/locale/bg.js +7 -0
  43. package/dist/src/dom/locale/bn.d.ts +7 -0
  44. package/dist/src/dom/locale/bn.js +7 -0
  45. package/dist/src/dom/locale/bs.d.ts +6 -0
  46. package/dist/src/dom/locale/bs.js +6 -0
  47. package/dist/src/dom/locale/ca.d.ts +6 -0
  48. package/dist/src/dom/locale/ca.js +6 -0
  49. package/dist/src/dom/locale/cs.d.ts +6 -0
  50. package/dist/src/dom/locale/cs.js +6 -0
  51. package/dist/src/dom/locale/da.d.ts +5 -0
  52. package/dist/src/dom/locale/da.js +5 -0
  53. package/dist/src/dom/locale/de.d.ts +6 -0
  54. package/dist/src/dom/locale/de.js +6 -0
  55. package/dist/src/dom/locale/el.d.ts +6 -0
  56. package/dist/src/dom/locale/el.js +6 -0
  57. package/dist/src/dom/locale/en.d.ts +14 -0
  58. package/dist/src/dom/locale/en.js +15 -0
  59. package/dist/src/dom/locale/es-419.d.ts +6 -0
  60. package/dist/src/dom/locale/es-419.js +6 -0
  61. package/dist/src/dom/locale/es-ES.d.ts +6 -0
  62. package/dist/src/dom/locale/es-ES.js +6 -0
  63. package/dist/src/dom/locale/et.d.ts +6 -0
  64. package/dist/src/dom/locale/et.js +6 -0
  65. package/dist/src/dom/locale/fa.d.ts +7 -0
  66. package/dist/src/dom/locale/fa.js +7 -0
  67. package/dist/src/dom/locale/fi.d.ts +6 -0
  68. package/dist/src/dom/locale/fi.js +6 -0
  69. package/dist/src/dom/locale/fr-CA.d.ts +6 -0
  70. package/dist/src/dom/locale/fr-CA.js +6 -0
  71. package/dist/src/dom/locale/fr-FR.d.ts +5 -0
  72. package/dist/src/dom/locale/fr-FR.js +5 -0
  73. package/dist/src/dom/locale/gu.d.ts +6 -0
  74. package/dist/src/dom/locale/gu.js +6 -0
  75. package/dist/src/dom/locale/hi.d.ts +6 -0
  76. package/dist/src/dom/locale/hi.js +6 -0
  77. package/dist/src/dom/locale/hr.d.ts +5 -0
  78. package/dist/src/dom/locale/hr.js +5 -0
  79. package/dist/src/dom/locale/hu.d.ts +6 -0
  80. package/dist/src/dom/locale/hu.js +6 -0
  81. package/dist/src/dom/locale/hy.d.ts +7 -0
  82. package/dist/src/dom/locale/hy.js +7 -0
  83. package/dist/src/dom/locale/id.d.ts +6 -0
  84. package/dist/src/dom/locale/id.js +6 -0
  85. package/dist/src/dom/locale/index.d.ts +5 -1
  86. package/dist/src/dom/locale/index.js +37 -0
  87. package/dist/src/dom/locale/is.d.ts +6 -0
  88. package/dist/src/dom/locale/is.js +6 -0
  89. package/dist/src/dom/locale/it.d.ts +6 -0
  90. package/dist/src/dom/locale/it.js +6 -0
  91. package/dist/src/dom/locale/ja.d.ts +6 -0
  92. package/dist/src/dom/locale/ja.js +6 -0
  93. package/dist/src/dom/locale/ka.d.ts +6 -0
  94. package/dist/src/dom/locale/ka.js +6 -0
  95. package/dist/src/dom/locale/kk.d.ts +6 -0
  96. package/dist/src/dom/locale/kk.js +6 -0
  97. package/dist/src/dom/locale/kn.d.ts +7 -0
  98. package/dist/src/dom/locale/kn.js +7 -0
  99. package/dist/src/dom/locale/ko.d.ts +6 -0
  100. package/dist/src/dom/locale/ko.js +6 -0
  101. package/dist/src/dom/locale/lt.d.ts +7 -0
  102. package/dist/src/dom/locale/lt.js +7 -0
  103. package/dist/src/dom/locale/lv.d.ts +6 -0
  104. package/dist/src/dom/locale/lv.js +6 -0
  105. package/dist/src/dom/locale/mk.d.ts +5 -0
  106. package/dist/src/dom/locale/mk.js +5 -0
  107. package/dist/src/dom/locale/ml.d.ts +7 -0
  108. package/dist/src/dom/locale/ml.js +7 -0
  109. package/dist/src/dom/locale/mn.d.ts +7 -0
  110. package/dist/src/dom/locale/mn.js +7 -0
  111. package/dist/src/dom/locale/mr.d.ts +7 -0
  112. package/dist/src/dom/locale/mr.js +7 -0
  113. package/dist/src/dom/locale/ms.d.ts +6 -0
  114. package/dist/src/dom/locale/ms.js +6 -0
  115. package/dist/src/dom/locale/my.d.ts +6 -0
  116. package/dist/src/dom/locale/my.js +6 -0
  117. package/dist/src/dom/locale/nb.d.ts +6 -0
  118. package/dist/src/dom/locale/nb.js +6 -0
  119. package/dist/src/dom/locale/nl.d.ts +6 -0
  120. package/dist/src/dom/locale/nl.js +6 -0
  121. package/dist/src/dom/locale/pa.d.ts +7 -0
  122. package/dist/src/dom/locale/pa.js +7 -0
  123. package/dist/src/dom/locale/pl.d.ts +6 -0
  124. package/dist/src/dom/locale/pl.js +6 -0
  125. package/dist/src/dom/locale/pt-BR.d.ts +6 -0
  126. package/dist/src/dom/locale/pt-BR.js +6 -0
  127. package/dist/src/dom/locale/pt-PT.d.ts +6 -0
  128. package/dist/src/dom/locale/pt-PT.js +6 -0
  129. package/dist/src/dom/locale/ro.d.ts +5 -0
  130. package/dist/src/dom/locale/ro.js +5 -0
  131. package/dist/src/dom/locale/ru.d.ts +3 -0
  132. package/dist/src/dom/locale/ru.js +3 -0
  133. package/dist/src/dom/locale/sk.d.ts +6 -0
  134. package/dist/src/dom/locale/sk.js +6 -0
  135. package/dist/src/dom/locale/sl.d.ts +6 -0
  136. package/dist/src/dom/locale/sl.js +6 -0
  137. package/dist/src/dom/locale/so.d.ts +6 -0
  138. package/dist/src/dom/locale/so.js +6 -0
  139. package/dist/src/dom/locale/sq.d.ts +6 -0
  140. package/dist/src/dom/locale/sq.js +6 -0
  141. package/dist/src/dom/locale/sr.d.ts +3 -0
  142. package/dist/src/dom/locale/sr.js +3 -0
  143. package/dist/src/dom/locale/sv.d.ts +6 -0
  144. package/dist/src/dom/locale/sv.js +6 -0
  145. package/dist/src/dom/locale/sw.d.ts +6 -0
  146. package/dist/src/dom/locale/sw.js +6 -0
  147. package/dist/src/dom/locale/ta.d.ts +7 -0
  148. package/dist/src/dom/locale/ta.js +7 -0
  149. package/dist/src/dom/locale/te.d.ts +7 -0
  150. package/dist/src/dom/locale/te.js +7 -0
  151. package/dist/src/dom/locale/th.d.ts +6 -0
  152. package/dist/src/dom/locale/th.js +6 -0
  153. package/dist/src/dom/locale/tr.d.ts +6 -0
  154. package/dist/src/dom/locale/tr.js +6 -0
  155. package/dist/src/dom/locale/types.d.ts +8 -1
  156. package/dist/src/dom/locale/uk.d.ts +6 -0
  157. package/dist/src/dom/locale/uk.js +6 -0
  158. package/dist/src/dom/locale/ur.d.ts +6 -0
  159. package/dist/src/dom/locale/ur.js +6 -0
  160. package/dist/src/dom/locale/vi.d.ts +6 -0
  161. package/dist/src/dom/locale/vi.js +6 -0
  162. package/dist/src/dom/locale/zh-HK.d.ts +7 -0
  163. package/dist/src/dom/locale/zh-HK.js +7 -0
  164. package/dist/src/dom/locale/zh-Hans.d.ts +7 -0
  165. package/dist/src/dom/locale/zh-Hans.js +7 -0
  166. package/dist/src/dom/locale/zh-TW.d.ts +7 -0
  167. package/dist/src/dom/locale/zh-TW.js +7 -0
  168. package/dist/src/dom/menus.js +13 -2
  169. package/dist/src/dom/wait-snapshot.d.ts +43 -0
  170. package/dist/src/dom/wait-snapshot.js +127 -0
  171. package/dist/src/safety/risk.d.ts +1 -0
  172. package/dist/src/safety/risk.js +1 -0
  173. package/dist/src/scripts/apply-intelligence-locale-captures.js +99 -13
  174. package/dist/src/types.d.ts +28 -0
  175. package/package.json +2 -2
  176. package/references/backend-protocol.md +6 -4
  177. package/references/localization.md +14 -4
  178. package/references/python-parity.md +5 -3
  179. package/references/troubleshooting.md +28 -5
@@ -0,0 +1,43 @@
1
+ import type { PageLike } from "../types.js";
2
+ /**
3
+ * One-evaluate DOM snapshot for the messages.wait polling loop.
4
+ *
5
+ * The previous loop ran four separate DOM probes per poll and transferred the entire
6
+ * latest assistant text across the browser bridge every iteration, even though the loop
7
+ * only needs change detection until completion. This snapshot returns fixed-size text
8
+ * metadata (normalized length + hash + transient flag) plus generation state and
9
+ * response-action evidence in a single round trip, sampled atomically from the same DOM
10
+ * instant. The full text is fetched once, at loop exit, by the caller.
11
+ */
12
+ export type WaitDomSnapshot = {
13
+ turnCount: number;
14
+ assistantTurnCount: number;
15
+ latestAssistantTurnIndex?: number;
16
+ text: WaitTextMetadata;
17
+ generation: {
18
+ active: boolean;
19
+ stopped: boolean;
20
+ signals: string[];
21
+ };
22
+ /** undefined means no conversation-turn markers were found; callers fall back to the copy-button locator. */
23
+ hasResponseActions?: boolean;
24
+ };
25
+ export type WaitTextMetadata = {
26
+ /** Length of the whitespace-normalized latest assistant text. */
27
+ length: number;
28
+ /** FNV-1a 32-bit hash (hex) of the whitespace-normalized latest assistant text. */
29
+ hash: string;
30
+ /** Whether the text is a transient placeholder such as "Thinking". */
31
+ transient: boolean;
32
+ };
33
+ /**
34
+ * SDK-side twin of the in-page metadata computation. The transient check delegates to
35
+ * dom/messages.ts isTransientAssistantText — the ground truth used by isResponseComplete —
36
+ * so only the in-page copy below is a true duplicate. The evaluate callback inlines the
37
+ * same normalization, hash, and transient rules because serialized callbacks cannot close
38
+ * over imports; `wait-snapshot.test.ts` pins the in-page copy to this helper (and thereby,
39
+ * transitively, to the ground truth).
40
+ */
41
+ export declare function waitTextMetadata(rawText: string | undefined): WaitTextMetadata;
42
+ export declare function fnv1a32Hex(text: string): string;
43
+ export declare function readWaitDomSnapshot(page: PageLike): Promise<WaitDomSnapshot | undefined>;
@@ -0,0 +1,127 @@
1
+ import { localeLabels } from "./locale-labels.js";
2
+ import { isTransientAssistantText } from "./messages.js";
3
+ import { normalizeWhitespace } from "./visible-text.js";
4
+ /**
5
+ * SDK-side twin of the in-page metadata computation. The transient check delegates to
6
+ * dom/messages.ts isTransientAssistantText — the ground truth used by isResponseComplete —
7
+ * so only the in-page copy below is a true duplicate. The evaluate callback inlines the
8
+ * same normalization, hash, and transient rules because serialized callbacks cannot close
9
+ * over imports; `wait-snapshot.test.ts` pins the in-page copy to this helper (and thereby,
10
+ * transitively, to the ground truth).
11
+ */
12
+ export function waitTextMetadata(rawText) {
13
+ const normalized = normalizeWhitespace(rawText ?? "");
14
+ return {
15
+ length: normalized.length,
16
+ hash: fnv1a32Hex(normalized),
17
+ transient: isTransientAssistantText(normalized)
18
+ };
19
+ }
20
+ export function fnv1a32Hex(text) {
21
+ let hash = 0x811c9dc5;
22
+ for (let index = 0; index < text.length; index += 1) {
23
+ hash ^= text.charCodeAt(index);
24
+ hash = Math.imul(hash, 0x01000193);
25
+ }
26
+ return (hash >>> 0).toString(16).padStart(8, "0");
27
+ }
28
+ export async function readWaitDomSnapshot(page) {
29
+ if (typeof page.evaluate !== "function") {
30
+ return undefined;
31
+ }
32
+ return page.evaluate((args) => {
33
+ const __combinedWaitSnapshot = true;
34
+ void __combinedWaitSnapshot;
35
+ const normalizeWs = (value) => value.replace(/\s+/g, " ").trim();
36
+ const normalizeLower = (value) => (value ?? "").trim().toLowerCase();
37
+ // --- Progress: turn counts and latest assistant text metadata (no text transfer) ---
38
+ const nodes = Array.from(document.querySelectorAll("[data-message-author-role]"));
39
+ const assistantNodes = nodes.filter(node => node.getAttribute("data-message-author-role") === "assistant");
40
+ const latestAssistant = assistantNodes.at(-1);
41
+ const latestAssistantTurnIndex = latestAssistant === undefined ? undefined : nodes.indexOf(latestAssistant) + 1;
42
+ const normalizedText = normalizeWs(latestAssistant?.innerText ?? latestAssistant?.textContent ?? "");
43
+ let hash = 0x811c9dc5;
44
+ for (let index = 0; index < normalizedText.length; index += 1) {
45
+ hash ^= normalizedText.charCodeAt(index);
46
+ hash = Math.imul(hash, 0x01000193);
47
+ }
48
+ const textHash = (hash >>> 0).toString(16).padStart(8, "0");
49
+ const trimmedForTransient = normalizedText.replace(/[.。…]+$/g, "").trim().toLowerCase();
50
+ const transient = args.transient.some(phrase => trimmedForTransient === phrase.toLowerCase())
51
+ || /^analyzing (?:the )?images?$/.test(trimmedForTransient)
52
+ || /^processing (?:the )?images?$/.test(trimmedForTransient)
53
+ || /^reading (?:the )?images?$/.test(trimmedForTransient);
54
+ // --- Generation state: mirrors dom/generation-state.ts readAssistantGenerationState ---
55
+ const isVisible = (element) => {
56
+ const style = window.getComputedStyle(element);
57
+ return style.display !== "none"
58
+ && style.visibility !== "hidden"
59
+ && style.opacity !== "0"
60
+ && element.getAttribute("aria-hidden") !== "true";
61
+ };
62
+ const visibleButtons = Array.from(document.querySelectorAll("button"))
63
+ .filter((button) => isVisible(button)
64
+ && button.disabled !== true
65
+ && button.getAttribute("aria-disabled") !== "true");
66
+ const buttonTexts = visibleButtons
67
+ .map(button => [
68
+ button.innerText,
69
+ button.textContent,
70
+ button.getAttribute("aria-label"),
71
+ button.getAttribute("title")
72
+ ].map(normalizeLower).filter(Boolean).join(" "))
73
+ .filter(Boolean);
74
+ const bodyText = normalizeLower(document.body?.innerText);
75
+ const haystacks = [bodyText, ...buttonTexts];
76
+ const matchingSignals = (phrases) => haystacks.flatMap(text => phrases
77
+ .map(phrase => phrase.toLowerCase())
78
+ .filter(phrase => text.includes(phrase)));
79
+ const activeSignals = matchingSignals(args.stop);
80
+ const stoppedSignals = matchingSignals(args.stopped);
81
+ const generation = {
82
+ active: activeSignals.length > 0,
83
+ stopped: stoppedSignals.length > 0,
84
+ signals: [...new Set([...activeSignals, ...stoppedSignals, ...buttonTexts.filter(text => /stop|cancel|stopped|answering|thinking/i.test(text))])].slice(0, 5)
85
+ };
86
+ // --- Response actions: mirrors dom/generation-state.ts latestAssistantTurnHasResponseActions ---
87
+ const turns = Array.from(document.querySelectorAll("[data-testid^='conversation-turn']"));
88
+ let hasResponseActions;
89
+ if (turns.length === 0) {
90
+ hasResponseActions = undefined;
91
+ }
92
+ else {
93
+ const latestTurn = [...turns].reverse().find(turn => turn.querySelector("[data-message-author-role='assistant']") !== null);
94
+ if (latestTurn === undefined) {
95
+ hasResponseActions = false;
96
+ }
97
+ else {
98
+ const actionText = Array.from(latestTurn.querySelectorAll("button"))
99
+ .map(button => [
100
+ button.innerText,
101
+ button.textContent,
102
+ button.getAttribute("aria-label"),
103
+ button.getAttribute("title")
104
+ ].filter(Boolean).join(" "))
105
+ .join(" ")
106
+ .toLowerCase();
107
+ hasResponseActions = args.actions.some(phrase => actionText.includes(phrase.toLowerCase()));
108
+ }
109
+ }
110
+ const snapshot = {
111
+ turnCount: nodes.length,
112
+ assistantTurnCount: assistantNodes.length,
113
+ text: { length: normalizedText.length, hash: textHash, transient },
114
+ generation
115
+ };
116
+ if (latestAssistantTurnIndex !== undefined)
117
+ snapshot.latestAssistantTurnIndex = latestAssistantTurnIndex;
118
+ if (hasResponseActions !== undefined)
119
+ snapshot.hasResponseActions = hasResponseActions;
120
+ return snapshot;
121
+ }, {
122
+ transient: [...localeLabels.transientAssistant],
123
+ stop: [...localeLabels.stopControl],
124
+ stopped: [...localeLabels.stoppedAssistant],
125
+ actions: [...localeLabels.responseActions]
126
+ });
127
+ }
@@ -21,6 +21,7 @@ export declare const commandRisk: {
21
21
  readonly "projects.sources.add": "medium";
22
22
  readonly "response.copy": "medium";
23
23
  readonly "modes.set": "medium";
24
+ readonly "modes.get": "low";
24
25
  readonly "tools.select": "medium";
25
26
  readonly "threads.delete": "high";
26
27
  readonly "threads.archive": "high";
@@ -20,6 +20,7 @@ export const commandRisk = {
20
20
  "projects.sources.add": "medium",
21
21
  "response.copy": "medium",
22
22
  "modes.set": "medium",
23
+ "modes.get": "low",
23
24
  "tools.select": "medium",
24
25
  "threads.delete": "high",
25
26
  "threads.archive": "high",
@@ -3,6 +3,14 @@ import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { readLanguageCoverage } from "./locale-capture/language-coverage.js";
5
5
  const ENGLISH_MODE_LABELS = new Set(["Latest", "Instant", "Thinking", "Extended", "Medium", "High", "Extra High", "Pro"]);
6
+ const INTELLIGENCE_MODE_OPTION_IDS = ["instant", "medium", "high", "extraHigh", "pro"];
7
+ const ENGLISH_INTELLIGENCE_MODE_OPTIONS = {
8
+ instant: "Instant",
9
+ medium: "Medium",
10
+ high: "High",
11
+ extraHigh: "Extra High",
12
+ pro: "Pro",
13
+ };
6
14
  const UPDATE_NOTE = " * Intelligence picker labels updated 2026-06-10 from a visible ChatGPT Pro session.";
7
15
  class ApplyUsageError extends Error {
8
16
  exitCode;
@@ -45,12 +53,14 @@ export async function main(argv = process.argv.slice(2)) {
45
53
  throw new ApplyUsageError(`Missing successful capture for ${language.bcp47}.`, 1);
46
54
  }
47
55
  const labels = observedNonEnglishLabels(record);
48
- if (labels.length === 0)
56
+ const modeOptions = observedNonEnglishModeOptions(record);
57
+ if (labels.length === 0 && Object.keys(modeOptions).length === 0)
49
58
  continue;
50
59
  planned.push({
51
60
  locale: language.bcp47,
52
61
  file: resolve(root, "src/dom/locale", `${language.bcp47}.ts`),
53
62
  labels,
63
+ modeOptions,
54
64
  });
55
65
  }
56
66
  if (!options.reviewed) {
@@ -62,10 +72,10 @@ export async function main(argv = process.argv.slice(2)) {
62
72
  }
63
73
  for (const change of planned) {
64
74
  const before = await readFile(change.file, "utf8");
65
- const after = mergeModeLabels(before, change.labels);
75
+ const after = mergeModeCapture(before, change.labels, change.modeOptions);
66
76
  if (after !== before) {
67
77
  await writeFile(change.file, after, "utf8");
68
- console.log(`updated ${change.locale} labels=${change.labels.length}`);
78
+ console.log(`updated ${change.locale} labels=${change.labels.length} modeOptions=${Object.keys(change.modeOptions).length}`);
69
79
  }
70
80
  }
71
81
  return 0;
@@ -93,18 +103,38 @@ function observedNonEnglishLabels(record) {
93
103
  }
94
104
  return labels;
95
105
  }
96
- function mergeModeLabels(source, labels) {
97
- let text = updateComment(source);
98
- const existing = parseExistingModeLabels(text);
99
- const merged = dedupe([...existing, ...labels]);
100
- const line = ` modeLabels: [${merged.map(label => JSON.stringify(label)).join(", ")}],`;
101
- if (/^\s*modeLabels:\s*\[[^\]]*\],/m.test(text)) {
102
- return text.replace(/^\s*modeLabels:\s*\[[^\]]*\],/m, line);
106
+ function observedNonEnglishModeOptions(record) {
107
+ const labels = record.intelligenceLabels ?? [];
108
+ if (labels.length !== INTELLIGENCE_MODE_OPTION_IDS.length) {
109
+ throw new ApplyUsageError(`${record.requestedLocale} has ${labels.length} Intelligence labels; expected ${INTELLIGENCE_MODE_OPTION_IDS.length}.`, 1);
103
110
  }
104
- if (/^\s*copyResponse:\s*.*,\n/m.test(text)) {
105
- return text.replace(/^(\s*copyResponse:\s*.*,\n)/m, `$1${line}\n`);
111
+ const modeOptions = {};
112
+ for (let index = 0; index < INTELLIGENCE_MODE_OPTION_IDS.length; index += 1) {
113
+ const id = INTELLIGENCE_MODE_OPTION_IDS[index];
114
+ const label = labels[index];
115
+ if (label !== ENGLISH_INTELLIGENCE_MODE_OPTIONS[id]) {
116
+ modeOptions[id] = [label];
117
+ }
106
118
  }
107
- return text.replace(/^export const \w+ = \{\n/m, match => `${match}${line}\n`);
119
+ return modeOptions;
120
+ }
121
+ function mergeModeCapture(source, labels, modeOptions) {
122
+ let text = updateComment(source);
123
+ if (labels.length > 0) {
124
+ const existing = parseExistingModeLabels(text);
125
+ const merged = dedupe([...existing, ...labels]);
126
+ const line = ` modeLabels: [${merged.map(label => JSON.stringify(label)).join(", ")}],`;
127
+ if (/^\s*modeLabels:\s*\[[^\]]*\],/m.test(text)) {
128
+ text = text.replace(/^\s*modeLabels:\s*\[[^\]]*\],/m, line);
129
+ }
130
+ else if (/^\s*copyResponse:\s*.*,\n/m.test(text)) {
131
+ text = text.replace(/^(\s*copyResponse:\s*.*,\n)/m, `$1${line}\n`);
132
+ }
133
+ else {
134
+ text = text.replace(/^export const \w+ = \{\n/m, match => `${match}${line}\n`);
135
+ }
136
+ }
137
+ return mergeModeOptions(text, modeOptions);
108
138
  }
109
139
  function parseExistingModeLabels(source) {
110
140
  const match = /^\s*modeLabels:\s*\[(?<body>[^\]]*)\],/m.exec(source);
@@ -117,6 +147,62 @@ function parseExistingModeLabels(source) {
117
147
  }
118
148
  return labels;
119
149
  }
150
+ function mergeModeOptions(source, modeOptions) {
151
+ const existing = parseExistingModeOptions(source);
152
+ const merged = {};
153
+ for (const id of INTELLIGENCE_MODE_OPTION_IDS) {
154
+ const values = dedupe([...(existing[id] ?? []), ...(modeOptions[id] ?? [])]);
155
+ if (values.length > 0) {
156
+ merged[id] = values;
157
+ }
158
+ }
159
+ const block = formatModeOptions(merged);
160
+ if (block === undefined) {
161
+ return source;
162
+ }
163
+ if (/^\s*modeOptions:\s*\{[\s\S]*?^\s*\},\n/m.test(source)) {
164
+ return source.replace(/^\s*modeOptions:\s*\{[\s\S]*?^\s*\},\n/m, `${block}\n`);
165
+ }
166
+ if (/^\s*modeLabels:\s*\[[^\]]*\],\n/m.test(source)) {
167
+ return source.replace(/^(\s*modeLabels:\s*\[[^\]]*\],\n)/m, `$1${block}\n`);
168
+ }
169
+ return source.replace(/^export const \w+ = \{\n/m, match => `${match}${block}\n`);
170
+ }
171
+ function parseExistingModeOptions(source) {
172
+ const options = {};
173
+ const blockMatch = /^\s*modeOptions:\s*\{(?<body>[\s\S]*?)^\s*\},/m.exec(source);
174
+ const body = blockMatch?.groups?.body;
175
+ if (body === undefined)
176
+ return options;
177
+ for (const id of INTELLIGENCE_MODE_OPTION_IDS) {
178
+ const lineMatch = new RegExp(`^\\s*${id}:\\s*\\[(?<body>[^\\]]*)\\],`, "m").exec(body);
179
+ const lineBody = lineMatch?.groups?.body;
180
+ if (lineBody === undefined)
181
+ continue;
182
+ const values = [];
183
+ for (const stringMatch of lineBody.matchAll(/"((?:\\"|[^"])*)"/g)) {
184
+ values.push(JSON.parse(`"${stringMatch[1]}"`));
185
+ }
186
+ if (values.length > 0) {
187
+ options[id] = values;
188
+ }
189
+ }
190
+ return options;
191
+ }
192
+ function formatModeOptions(modeOptions) {
193
+ const lines = INTELLIGENCE_MODE_OPTION_IDS
194
+ .map(id => {
195
+ const values = modeOptions[id];
196
+ return values === undefined || values.length === 0
197
+ ? undefined
198
+ : ` ${id}: [${values.map(value => JSON.stringify(value)).join(", ")}],`;
199
+ })
200
+ .filter((line) => line !== undefined);
201
+ if (lines.length === 0) {
202
+ return undefined;
203
+ }
204
+ return [" modeOptions: {", ...lines, " },"].join("\n");
205
+ }
120
206
  function updateComment(source) {
121
207
  let text = source.replace(/\n \* Omitted because they match English case-insensitively: `modeLabels`[\s\S]*?blocker copy\.\n/g, "\n * Some non-Intelligence surfaces may still fall back to English + `selector_drift`.\n");
122
208
  if (!text.includes(UPDATE_NOTE)) {
@@ -183,10 +183,14 @@ export type WaitArgs = {
183
183
  stableMs?: number;
184
184
  pollMs?: number;
185
185
  mode?: "normal" | "deep_research";
186
+ responseContent?: "include" | "metadata";
186
187
  };
187
188
  export type WaitData = {
188
189
  complete: boolean;
189
190
  responseText?: string;
191
+ responseChars?: number;
192
+ responseSha256?: string;
193
+ responseContent?: "include" | "metadata";
190
194
  assistantTurnCount: number;
191
195
  elapsedMs: number;
192
196
  };
@@ -291,6 +295,8 @@ export type AskReadData = {
291
295
  export type AttachFilesArgs = {
292
296
  paths: string[];
293
297
  timeoutMs?: number;
298
+ includeDiagnostics?: boolean;
299
+ includeHashes?: boolean;
294
300
  };
295
301
  export type AttachedFile = {
296
302
  path: string;
@@ -302,11 +308,13 @@ export type FilePreflightArgs = {
302
308
  paths: string[];
303
309
  maxBytesPerFile?: number;
304
310
  maxTotalBytes?: number;
311
+ includeHashes?: boolean;
305
312
  };
306
313
  export type FilePreflightFile = AttachedFile & {
307
314
  extension: string;
308
315
  mimeType: string;
309
316
  category: FileCategory;
317
+ sha256?: string;
310
318
  };
311
319
  export type FilePreflightData = {
312
320
  files: FilePreflightFile[];
@@ -314,6 +322,20 @@ export type FilePreflightData = {
314
322
  };
315
323
  export type AttachFilesData = {
316
324
  files: AttachedFile[];
325
+ diagnostics?: FileUploadDiagnostics;
326
+ };
327
+ export type BrowserInputFileDiagnostic = {
328
+ name: string;
329
+ size: number;
330
+ type?: string;
331
+ lastModified?: number;
332
+ };
333
+ export type BrowserInputDiagnostic = {
334
+ files: BrowserInputFileDiagnostic[];
335
+ };
336
+ export type FileUploadDiagnostics = {
337
+ preflight: FilePreflightData;
338
+ browserInput?: BrowserInputDiagnostic;
317
339
  };
318
340
  export type ProjectSourceStatus = "ready" | "processing" | "failed" | "unknown";
319
341
  export type ProjectSource = {
@@ -460,6 +482,12 @@ export type SetModeArgs = {
460
482
  version?: string;
461
483
  timeoutMs?: number;
462
484
  };
485
+ export type GetModeArgs = {
486
+ timeoutMs?: number;
487
+ };
488
+ export type GetModeData = {
489
+ modes: string[];
490
+ };
463
491
  export type SelectToolArgs = {
464
492
  tool: "web_search" | "deep_research" | "create_image" | string;
465
493
  timeoutMs?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-chatgpt-control",
3
- "version": "0.2.0-alpha.1",
3
+ "version": "0.3.0-alpha.1",
4
4
  "description": "Unofficial SDK for Codex agents controlling visible ChatGPT web sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -74,7 +74,7 @@
74
74
  "devDependencies": {
75
75
  "@types/node": "^24.13.1",
76
76
  "ajv": "^8.17.1",
77
- "esbuild": "^0.28.0",
77
+ "esbuild": "^0.28.1",
78
78
  "tsx": "^4.20.0",
79
79
  "typescript": "^5.8.0",
80
80
  "vitest": "^4.1.8"
@@ -67,7 +67,7 @@ Current protocol error codes are:
67
67
 
68
68
  Browser-control blockers are not protocol errors. They are normal command or runner results with `status: "blocked"`, `status: "partial"`, or `status: "needs_confirmation"` plus blocker/interruption details.
69
69
 
70
- `status: "partial"` is the required result for incomplete response capture. A partial result may still include `output_text` and `data.responseText`, but consumers must treat that text as incomplete until a later wait confirms completion. Common causes are wait timeout after partial assistant text, active generation controls such as `Stop answering`, stopped-generation markers such as `Stopped thinking`, or a read fallback after the wait step could not confirm completion. Intentional capture clipping uses `data.captureLimit` plus warnings; it is separate from ChatGPT generation length.
70
+ `status: "partial"` is the required result for incomplete response capture. A partial result may still include `output_text` and `data.responseText`, but consumers must treat that text as incomplete until a later wait confirms completion. Common causes are wait timeout after partial assistant text, active generation controls such as `Stop answering`, stopped-generation markers such as `Stopped thinking`, or a read fallback after the wait step could not confirm completion. For status-only polling, `messages.wait` accepts `responseContent: "metadata"`; partial and completed wait results then omit assistant text and instead return compact metadata such as `data.responseChars` and `data.responseSha256`. Intentional capture clipping uses `data.captureLimit` plus warnings; it is separate from ChatGPT generation length.
71
71
 
72
72
  ## Streaming
73
73
 
@@ -112,7 +112,7 @@ The backend must support:
112
112
  - diagnostics: `doctor`
113
113
  - reports: `createReport`, `reports.create`, `reports.redact`, `reports.summarize`
114
114
  - command discovery: `commands`, `describe`, `help`
115
- - primitives: `session.bootstrap`, `threads.*`, `messages.*`, `artifacts.*`, `files.preflight`, `files.attach`, `files.downloadLatest`, `projects.sources.list`, `projects.sources.planAdd`, `projects.sources.add`, `modes.set`, `tools.select`, `response.copy`
115
+ - primitives: `session.bootstrap`, `threads.*`, `messages.*`, `artifacts.*`, `files.preflight`, `files.attach`, `files.downloadLatest`, `projects.sources.list`, `projects.sources.planAdd`, `projects.sources.add`, `modes.set`, `modes.get`, `tools.select`, `response.copy`
116
116
 
117
117
  `doctor` returns a normal `CommandResult` whose `data.checks` map is extensible. Scenario checks such as `existing_tab`, `artifacts`, `file_preflight`, `localization`, and `reports` may add optional `code`, `blockerKind`, `nextCommand`, and JSON `details` fields to individual check entries while preserving the existing `status`, `message`, and `remediation` fields.
118
118
 
@@ -120,7 +120,9 @@ The backend must support:
120
120
 
121
121
  Attachment paths are interpreted on the machine running the Node backend. Use an absolute path in that host operating system's native form. On macOS/Linux/WSL, use paths such as `/example/user/file.pdf`, `/home/you/file.pdf`, or `/mnt/c/example/user/file.pdf`. On Windows backend hosts, use fully qualified paths such as `C:\Users\you\file.pdf` or UNC paths such as `\\server\share\file.pdf`. Drive-relative paths like `C:Users\you\file.pdf`, root-relative paths like `\tmp\file.pdf`, and Windows-looking paths sent to a POSIX backend are rejected before filesystem access.
122
122
 
123
- Use `files.preflight` for non-mutating local validation before browser upload workflows. It validates absolute paths, existence, readability, file-vs-directory status, configurable per-file and total byte limits, duplicate basenames, duplicate resolved paths, zero-byte files, and extension-based MIME/category guesses. It does not open ChatGPT, perform a live upload, or read file contents for MIME detection. `askWithFiles` and `files.attach` run the same preflight before upload attempts so obvious local file failures stop before browser interaction.
123
+ Use `files.preflight` for non-mutating local validation before browser upload workflows. It validates absolute paths, existence, readability, file-vs-directory status, configurable per-file and total byte limits, duplicate basenames, duplicate resolved paths, zero-byte files, and extension-based MIME/category guesses. Zero-byte files are blocked before browser interaction because ChatGPT rejects empty attachments. By default the command does not open ChatGPT, perform a live upload, read file contents for MIME detection, or return file-content fingerprints. Callers may pass `includeHashes: true` to include SHA-256 metadata for local diagnostics; file contents are never returned. `askWithFiles` and `files.attach` run the same preflight before upload attempts so obvious local file failures stop before browser interaction.
124
+
125
+ `files.attach` accepts `includeDiagnostics: true` to return metadata-only upload diagnostics in `data.diagnostics`: the preflight result plus the browser input's selected file names and sizes when the DOM exposes them. Pair `includeDiagnostics: true` with `includeHashes: true` when diagnosing whether a non-empty local file became an empty browser-side `File`; do not persist these diagnostics in public reports unless the user has approved content fingerprint metadata.
124
126
 
125
127
  ## Project Sources
126
128
 
@@ -213,7 +215,7 @@ Important: in Codex, `globalThis.agent` is not present until the Chrome plugin r
213
215
  The live Chrome bootstrap is:
214
216
 
215
217
  ```js
216
- const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/26.602.40724/scripts/browser-client.mjs");
218
+ const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/latest/scripts/browser-client.mjs");
217
219
  await setupBrowserRuntime({ globals: globalThis });
218
220
  globalThis.browser = await agent.browsers.get("extension");
219
221
  ```
@@ -77,7 +77,8 @@ Localized — lives in `src/dom/locale/en.ts` (English) and per-locale files, sa
77
77
  | `projectSourcesTab` / `projectSourcesAddSource` / `projectSourcesUploadFiles` | Project Sources tab and append-add flow | visible tab/button/menu text |
78
78
  | `copyResponse` | copy-response button | `aria-label` |
79
79
  | `download` / `downloadImage` / `imageContainerHint` | download affordances | `aria-label` / container hint |
80
- | `modeLabels` / `modeOpenerExtra` | model/effort switcher | visible button + menu text |
80
+ | `modeLabels` / `modeOpenerExtra` | model/effort switcher recognition and openers | visible button + menu text |
81
+ | `modeOptions.<semantic-id>` | selectable model/intelligence mode options | visible picker rows, keyed by stable ids such as `high`, `extraHigh`, and `pro` |
81
82
  | `tools.web_search` / `tools.deep_research` / `tools.create_image` | tool menu items | visible menu text |
82
83
  | `signedInMarkers` | signed-in detection | sidebar/shell words |
83
84
  | `transientAssistant` | streaming placeholder filter | assistant streaming text ("Thinking", etc.) |
@@ -141,7 +142,13 @@ import type { LocaleContribution } from "./types.js";
141
142
 
142
143
  export const de = {
143
144
  sendButton: ["Nachricht senden"],
144
- modeLabels: ["Neueste", "Schnell", "Denken", "Erweitert", "Pro"],
145
+ modeLabels: ["Sofort", "Mittel", "Hoch", "Extra hoch"],
146
+ modeOptions: {
147
+ instant: ["Sofort"],
148
+ medium: ["Mittel"],
149
+ high: ["Hoch"],
150
+ extraHigh: ["Extra hoch"],
151
+ },
145
152
  tools: {
146
153
  web_search: ["Websuche"],
147
154
  },
@@ -150,11 +157,14 @@ export const de = {
150
157
  ```
151
158
 
152
159
  Leave the canonical English first (it comes from `en.ts`), and leave the API keys
153
- (`web_search`, the `effort` values) unchanged.
160
+ (`web_search`, `modeOptions.pro`, `modeOptions.high`, and the other semantic ids)
161
+ unchanged.
154
162
 
155
163
  Newer ChatGPT rollouts may expose `Medium`, `High`, `Extra High`, and `Pro`
156
164
  under an `Intelligence` picker. Add localized equivalents only after observing
157
- those exact labels in the target locale.
165
+ those exact labels in the target locale. Put broad picker/opening labels in
166
+ `modeLabels`, but put selectable labels under `modeOptions.<semantic-id>` so
167
+ short labels such as `Pro` cannot match unrelated rows like `Move to project`.
158
168
 
159
169
  Then open [`src/dom/locale/index.ts`](../src/dom/locale/index.ts) and register the new
160
170
  locale:
@@ -29,6 +29,8 @@ Wire fields stay TypeScript-compatible. Python exposes idiomatic aliases:
29
29
 
30
30
  Incomplete response capture is also shared contract behavior. Python must preserve `status == "partial"`, `output_text`, warnings, and any nested `data.captureLimit` dictionaries exactly as the TypeScript backend returns them. `partial` is not a protocol error: callers should inspect `data.complete` and run another wait/read on the same thread when they need final output.
31
31
 
32
+ For long-answer polling, Python forwards `response_content="metadata"` to the shared wire field `responseContent: "metadata"` on `messages.wait`. The TypeScript backend then omits assistant text from wait results and returns compact metadata such as `data.responseChars` and `data.responseSha256`; Python must preserve those fields without trying to reconstruct omitted content.
33
+
32
34
  Generated-image behavior stays owned by the TypeScript runtime. Python exposes
33
35
  the same backend commands through `chatgpt.artifacts.list_latest(...)`,
34
36
  `chatgpt.artifacts.wait(...)`, and `chatgpt.artifacts.download_latest(...)`.
@@ -49,7 +51,7 @@ the shared `blocker-explanation-profiles.json` and
49
51
 
50
52
  Python does not reinterpret attachment paths. It sends the path string to the Node backend, and the backend validates the path against its own host operating system. Attachment paths must be absolute on the backend host. On macOS/Linux/WSL backends, use POSIX paths such as `/example/user/file.pdf` or `/mnt/c/example/user/file.pdf`. On Windows backends, use fully qualified paths such as `C:\Users\you\file.pdf` or UNC paths such as `\\server\share\file.pdf`. Drive-relative paths, root-relative paths, and Windows-looking paths sent to a POSIX backend are rejected before filesystem access.
51
53
 
52
- Python exposes the backend-visible `files.preflight` command as `chatgpt.files.preflight(...)`. It returns the same `CommandResult` as TypeScript and can be decoded with `FilePreflightData` when callers want typed metadata. The command validates paths, readability, file-vs-directory status, size limits, duplicate basenames, duplicate resolved paths, zero-byte files, and extension-based MIME/category guesses without opening ChatGPT or reading file contents for MIME detection.
54
+ Python exposes the backend-visible `files.preflight` command as `chatgpt.files.preflight(...)`. It returns the same `CommandResult` as TypeScript and can be decoded with `FilePreflightData` when callers want typed metadata. The command validates paths, readability, file-vs-directory status, size limits, duplicate basenames, duplicate resolved paths, zero-byte files, and extension-based MIME/category guesses without opening ChatGPT or reading file contents for MIME detection. Zero-byte files are blocked before browser interaction. Optional `include_hashes=True` / wire `includeHashes: true` adds SHA-256 metadata to `FilePreflightFile.sha256` for local diagnostics; file contents are never returned.
53
55
 
54
56
  ## Project Sources
55
57
 
@@ -161,7 +163,7 @@ Python is a native SDK facade over the local backend protocol. The initial brows
161
163
 
162
164
  - `dist/codex-chatgpt-control-backend.mjs` is the stdio backend bundle.
163
165
  - `BackendClient` and `StdioBackendTransport` keep Python backend calls long-lived.
164
- - `NodeSidecarTransport.run(...)` remains as a compatibility wrapper over backend `runner.run`.
166
+ - `NodeSidecarTransport.run(...)` remains as a compatibility wrapper over backend `runner.run`. By default each call spawns and tears down its own backend subprocess; use it as a context manager (or call `open()`/`close()`) to reuse one persistent backend process across calls in multi-command workflows. Transport-level failures close the persistent session; protocol-level errors keep it open.
165
167
  - Ordinary-shell smoke passes when browser-required calls return structured `browser_bridge_unavailable`.
166
168
  - Browser-bridge runtime smoke remains explicitly gated because it can operate a real ChatGPT session.
167
169
 
@@ -199,7 +201,7 @@ python scripts/live_smoke.py --mode browser-bridge
199
201
  When the live backend is hosted inside the Codex Chrome plugin runtime, do not test bridge availability from a normal shell or an unbootstrapped Node REPL. First initialize the Chrome runtime:
200
202
 
201
203
  ```js
202
- const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/26.602.40724/scripts/browser-client.mjs");
204
+ const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/latest/scripts/browser-client.mjs");
203
205
  await setupBrowserRuntime({ globals: globalThis });
204
206
  globalThis.browser = await agent.browsers.get("extension");
205
207
  ```
@@ -33,7 +33,7 @@ Use `chatgpt.explainBlocker(result)` or Python `explain_blocker(result)` when re
33
33
  Do not conclude that Chrome or the extension is broken from a plain shell result, or from checking `globalThis.agent` before the Chrome plugin runtime is initialized. For a true Codex Chrome-plugin live run, bootstrap the runtime first:
34
34
 
35
35
  ```js
36
- const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/26.602.40724/scripts/browser-client.mjs");
36
+ const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/latest/scripts/browser-client.mjs");
37
37
  await setupBrowserRuntime({ globals: globalThis });
38
38
  globalThis.browser = await agent.browsers.get("extension");
39
39
  ```
@@ -90,9 +90,24 @@ Agent-facing remediation text should name both settings:
90
90
 
91
91
  Attachment paths are validated against the backend host's operating system. If a Windows-looking path is rejected on macOS/Linux, do not retry with the same string. Convert it to the backend host's real path, for example `/mnt/c/example/user/file.pdf` for a WSL/Linux backend. Drive-relative paths like `C:Users\you\file.pdf`, root-relative paths like `\tmp\file.pdf`, and empty or relative paths are always rejected.
92
92
 
93
+ ## Empty Or Stripped Attachments
94
+
95
+ Zero-byte files are blocked by `files.preflight` before browser upload because
96
+ ChatGPT rejects empty attachments with a generic help-center error. If a user
97
+ reports that manual upload works but automated upload fails, compare local and
98
+ browser-side metadata instead of guessing:
99
+
100
+ 1. Run `files.preflight({ paths, includeHashes: true })` and inspect `bytes`
101
+ plus `sha256` for the backend-visible file.
102
+ 2. Run `files.attach({ paths, includeDiagnostics: true, includeHashes: true })`
103
+ and inspect `data.diagnostics.browserInput.files[].size` when available.
104
+ 3. If preflight `bytes` is zero, investigate the source path, generation race,
105
+ cloud placeholder, or host/container path mapping. If preflight bytes are
106
+ nonzero but browser-side size is zero, investigate the Chrome handoff.
107
+
93
108
  ## Clipboard Unavailable
94
109
 
95
- `response.copy` falls back to DOM text extraction when the macOS system clipboard does not change.
110
+ `response.copy` falls back to DOM text extraction when the system clipboard does not change. Clipboard reads use `pbpaste` on macOS, PowerShell `Get-Clipboard` on Windows, and `xclip`/`xsel`/`wl-paste` on Linux; hosts without any of these tools always use the DOM fallback.
96
111
 
97
112
  ## Flattened Or Unreadable Response Capture
98
113
 
@@ -100,12 +115,20 @@ Use `readLatest({ format: "markdown" })`, `copyLatest()`, or the default SDK `re
100
115
 
101
116
  ## Long Responses Return `partial`
102
117
 
103
- Long Pro, Thinking, Deep Research, or file-backed answers can take longer than the default wait window. Treat `status: "partial"` and `data.complete: false` as an incomplete capture even when `output_text` is non-empty. Re-run `messages.wait(...)` on the same thread with a larger timeout, then call `readLatest(...)` or `copyLatest(...)` only after completion is confirmed.
118
+ Long Pro, Thinking, Deep Research, or file-backed answers can take longer than the default wait window. Treat `status: "partial"` and `data.complete: false` as an incomplete capture even when `output_text` is non-empty. For repeated polling, prefer `messages.wait({ responseContent: "metadata", ... })` so Codex receives compact status metadata instead of the same growing partial answer body. Re-run `messages.wait(...)` on the same thread until completion is confirmed, then call `readLatest(...)` or `copyLatest(...)` once.
104
119
 
105
120
  Recommended long-answer wait:
106
121
 
107
122
  ```ts
108
- wait: { timeoutMs: 600000, stableMs: 8000, pollMs: 1000 }
123
+ await chatgpt.messages.wait({
124
+ timeoutMs: 45_000,
125
+ stableMs: 2_000,
126
+ pollMs: 1_000,
127
+ mode: "deep_research",
128
+ responseContent: "metadata"
129
+ });
130
+
131
+ const final = await chatgpt.messages.readLatest({ format: "markdown" });
109
132
  ```
110
133
 
111
134
  Active generation may appear as a visible or accessible-name control such as `Stop answering`, `Stop generating`, or `Stop streaming`. Stopped generation may appear as `Stopped thinking`. Treat all of those as incomplete states.
@@ -144,6 +167,6 @@ Doctor also supports opt-in scenario checks:
144
167
 
145
168
  - `existing_tab`: claims only the requested already-open tab target by default and reports `existing_tab_not_found` / `existing_tab_ambiguous` diagnostics without opening a replacement tab unless `existingTab.ifMissing` explicitly allows that.
146
169
  - `artifacts`: verifies current-page artifact selector/download/asset support without requesting generation.
147
- - `file_preflight`: validates supplied local file paths without opening ChatGPT or attempting upload. It reports path count, total bytes, duplicate/zero-byte warnings, and extension-based MIME/category metadata; fatal local file problems map to the same structured blockers as `files.preflight`.
170
+ - `file_preflight`: validates supplied local file paths without opening ChatGPT or attempting upload. It reports path count, total bytes, duplicate warnings, zero-byte blockers, and extension-based MIME/category metadata; fatal local file problems map to the same structured blockers as `files.preflight`.
148
171
  - `localization`: checks locale-label registry readiness and English canonical labels without changing the ChatGPT account language; it is not yet proof of full localized selector coverage.
149
172
  - `reports`: checks redacted-report policy and existing destination writability when possible without writing a report.