@bitkyc08/opencodex 2.79.0 → 2.80.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 (31) hide show
  1. package/gui/dist/assets/{App-BOUa-ek3.js → App-MbDVfUeC.js} +3 -3
  2. package/gui/dist/assets/{Tray-DUtzB4zF.js → Tray-CzkxK82A.js} +1 -1
  3. package/gui/dist/assets/{index-DwHNDF4s.js → index-UJt6npXk.js} +2 -2
  4. package/gui/dist/assets/{quota-summary-BuL4WIBo.js → quota-summary-DU3sD6Z-.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/cursor/live-transport.ts +5 -3
  8. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  9. package/src/adapters/cursor/native-exec.ts +13 -5
  10. package/src/adapters/cursor/protobuf-request.ts +13 -7
  11. package/src/adapters/cursor/request-builder.ts +23 -10
  12. package/src/adapters/cursor/tool-definitions.ts +9 -6
  13. package/src/adapters/cursor/tool-guidance.ts +29 -8
  14. package/src/adapters/cursor/tool-naming.ts +40 -7
  15. package/src/adapters/cursor/tool-wording.ts +46 -0
  16. package/src/adapters/cursor.ts +3 -0
  17. package/src/adapters/devin.ts +25 -11
  18. package/src/adapters/openai-chat/messages.ts +3 -1
  19. package/src/claude/inbound.ts +9 -0
  20. package/src/codex/autostart-health.ts +3 -3
  21. package/src/codex/journal.ts +36 -5
  22. package/src/codex/shim-overlay.ts +9 -1
  23. package/src/combos/jev.ts +3 -1
  24. package/src/generated/compatibility-version.json +35 -23
  25. package/src/server/index.ts +12 -14
  26. package/src/server/messages-native-scope.ts +4 -1
  27. package/src/server/responses/combo-requested-effort.ts +14 -0
  28. package/src/server/responses/core-combo.ts +5 -7
  29. package/src/service/desktop-startup.ts +100 -3
  30. package/src/web-search/header-deadline.ts +67 -0
  31. package/src/web-search/loop.ts +5 -5
@@ -1,6 +1,7 @@
1
1
  import type { OcxRequestOptions, OcxTool } from "../../types";
2
+ import { CURSOR_TOOL_CALL_CONTINUATION, cursorUsesPlainToolWording } from "./tool-wording";
2
3
  import { CODE_MODE_HOST_CONTRACT_SENTENCE, CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize";
3
- import { CODEX_SHELL_BRIDGE_TOOL_NAMES, CODEX_TOOL_SEARCH_TOOL, CODEX_UNIFIED_EXEC_TOOL, clientSemanticToolNameFromCursorWire, cursorRequestAdvertisesApplyPatch, cursorRequestHasExecutionPath, cursorRequestHasShellAlias, cursorRequestUsesCodeMode, cursorToolAllowedByChoice, cursorToolWireName, isCodexShellBridgeToolName, isCursorExecutionPathTool, isCursorStructuredEditToolName } from "./tool-naming";
4
+ import { CODEX_SHELL_BRIDGE_TOOL_NAMES, CODEX_TOOL_SEARCH_TOOL, CODEX_UNIFIED_EXEC_TOOL, clientSemanticToolNameFromCursorWire, cursorRequestAdvertisesApplyPatch, cursorRequestHasExecutionPath, cursorRequestHasShellAlias, cursorRequestUsesCodeMode, cursorToolAllowedByChoice, cursorToolWireName, isBareCodexShellBridgeTool, isClaudeClientBareToolName, isCodexShellBridgeToolName, isCursorExecutionPathTool, isCursorStructuredEditToolName } from "./tool-naming";
4
5
 
5
6
  export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
6
7
  'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell.';
@@ -129,7 +130,15 @@ function advertisedCoversNeighbor(wireNames: readonly string[], neighbor: (typeo
129
130
  return NEIGHBOR_AGENT_TOOL_ALIASES[neighbor].some(alias => advertised.has(alias.toLowerCase()));
130
131
  }
131
132
 
132
- function unavailableNeighborAgentToolNames(wireNames: readonly string[]): string[] {
133
+ function unavailableNeighborAgentToolNames(
134
+ wireNames: readonly string[],
135
+ catalog?: readonly Pick<OcxTool, "namespace" | "name">[],
136
+ ): string[] {
137
+ if (
138
+ catalog
139
+ && !catalog.some(isBareCodexShellBridgeTool)
140
+ && catalog.some(tool => !tool.namespace && isClaudeClientBareToolName(tool.name))
141
+ ) return [];
133
142
  return NEIGHBOR_AGENT_TOOL_NAMES.filter(name => !advertisedCoversNeighbor(wireNames, name));
134
143
  }
135
144
 
@@ -144,12 +153,14 @@ function discoveryToolLabel(wireNames: readonly string[]): string | undefined {
144
153
  export function buildCursorToolGuidanceSystemNote(
145
154
  tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
146
155
  toolChoice?: OcxRequestOptions["toolChoice"],
156
+ modelId?: string,
147
157
  ): string | undefined {
148
158
  if (!tools?.length) return undefined;
159
+ const plainToolWording = cursorUsesPlainToolWording(modelId);
149
160
  const wireNames = [...new Set(
150
161
  tools
151
162
  .filter(tool => cursorToolAllowedByChoice(tool, toolChoice, tools))
152
- .map(tool => cursorToolWireName(tool)),
163
+ .map(tool => cursorToolWireName(tool, tools)),
153
164
  )];
154
165
  if (wireNames.length === 0) return undefined;
155
166
 
@@ -170,7 +181,7 @@ export function buildCursorToolGuidanceSystemNote(
170
181
  ?.filter(tool => !tool.namespace && isCursorStructuredEditToolName(tool.name))
171
182
  .map(tool => tool.name) ?? [];
172
183
  const discoveryTools = discoveryToolLabel(wireNames);
173
- const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames);
184
+ const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames, tools);
174
185
  // Host-shell-neutral: the Codex client executes bridge commands, and may differ from
175
186
  // the OpenCodex proxy OS (LAN/SSH remote-proxy). Always cover PowerShell 5.1 pitfalls.
176
187
  const hostShellNote = hasBareExec
@@ -179,6 +190,9 @@ export function buildCursorToolGuidanceSystemNote(
179
190
  const notes = [
180
191
  `Cursor tool calls: available tool names are exactly ${listedNames}.`,
181
192
  "Use the current tool catalog as ground truth and call only those exact names with their listed argument keys.",
193
+ plainToolWording && !codeMode && !hasBareExec
194
+ ? "Cursor-native Read/Glob/Grep/LS/Shell/Write/Fetch are not available in this request. Use the listed tool that fits the operation."
195
+ : undefined,
182
196
  unavailableNeighborNames.length > 0
183
197
  ? `This turn does not expose neighboring-agent tool names ${quotedNames(unavailableNeighborNames)}; do not call or suggest them unless the catalog lists them.`
184
198
  : undefined,
@@ -191,18 +205,24 @@ export function buildCursorToolGuidanceSystemNote(
191
205
  ? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers. " + CODE_MODE_HOST_CONTRACT_SENTENCE
192
206
  : undefined,
193
207
  codeMode
194
- ? "NEVER attempt Cursor-native Shell, Read, Grep, List, Write, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces."
208
+ ? plainToolWording
209
+ ? "Cursor-native Shell, Read, Grep, List, and Write are not available in this request. The exec code cell and its nested helpers provide the execution path; separately listed tools remain callable at the top level."
210
+ : "NEVER attempt Cursor-native Shell, Read, Grep, List, Write, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces."
195
211
  : undefined,
196
212
  hasBareExec
197
213
  ? `${shellBridgeLabel} is the Codex Responses shell bridge for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool. \`shell_command\` and \`exec_command\` are aliases of the same bridge.`
198
214
  : undefined,
199
215
  hasBareExec
200
- ? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user."
216
+ ? plainToolWording
217
+ ? "Your tool list may display the same shell bridge under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; call the name listed in the catalog."
218
+ : "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user."
201
219
  : undefined,
202
220
  hasBareExec
203
- ? `NEVER attempt Cursor-native Shell, Read, Grep, List, Write, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.`
221
+ ? plainToolWording
222
+ ? `Cursor-native Shell, Read, Grep, List, and Write are not available in this request. ${shellBridgeLabel} provides shell execution through the Codex client host.`
223
+ : `NEVER attempt Cursor-native Shell, Read, Grep, List, Write, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.`
204
224
  : undefined,
205
- hasBareExec
225
+ hasBareExec && !plainToolWording
206
226
  ? "Tool-selection commentary is forbidden: for any shell, read, grep, list, or file operation, your FIRST visible action is the bridge call itself — never a sentence about which tool you will use, which tool was redirected, or switching surfaces. Words like 차단/전환/blocked/switching must not appear in your output for tool-routing reasons."
207
227
  : undefined,
208
228
  hostShellNote,
@@ -233,5 +253,6 @@ export function buildCursorToolGuidanceSystemNote(
233
253
  ? `For every file read, directory listing, grep, or shell operation use ${shellBridgeLabel} directly with host-shell-safe commands (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.`
234
254
  : undefined,
235
255
  ].filter((note): note is string => typeof note === "string");
256
+ if (plainToolWording) notes.push(CURSOR_TOOL_CALL_CONTINUATION);
236
257
  return notes.join(" ");
237
258
  }
@@ -67,11 +67,11 @@ function cursorToolChoiceMatches(
67
67
  if (catalogHasBareCodexShellBridge(catalog)) {
68
68
  return isBareCodexShellBridgeTool(tool);
69
69
  }
70
- return tool.name === choiceName || cursorToolWireName(tool) === choiceName;
70
+ return tool.name === choiceName || cursorToolWireName(tool, catalog) === choiceName;
71
71
  }
72
72
  if (tool.name === choiceName) return true;
73
73
  if (cursorToolChoiceAliases(tool).includes(choiceName)) return true;
74
- return cursorToolWireName(tool) === choiceName
74
+ return cursorToolWireName(tool, catalog) === choiceName
75
75
  && !catalog.some(candidate => candidate.name === choiceName);
76
76
  }
77
77
 
@@ -178,16 +178,49 @@ const CURSOR_PROXY_OWNED_BARE_TOOL_NAMES = new Set([
178
178
  CODEX_TOOL_SEARCH_TOOL,
179
179
  ]);
180
180
 
181
+ export const CLAUDE_CLIENT_BARE_TOOL_NAMES = new Set([
182
+ "Bash",
183
+ "Read",
184
+ "Write",
185
+ "Edit",
186
+ "Grep",
187
+ "Glob",
188
+ "Task",
189
+ "GetDynamicTools",
190
+ "EnterMode",
191
+ "ExitMode",
192
+ "Todo",
193
+ "AddTodo",
194
+ "UpdateTodo",
195
+ "ViewTodo",
196
+ "WebSearch",
197
+ "FetchUrl",
198
+ "KillProcess",
199
+ "ListProcesses",
200
+ "ViewProcessOutput",
201
+ ]);
202
+
203
+ export function isClaudeClientBareToolName(name: string): boolean {
204
+ return CLAUDE_CLIENT_BARE_TOOL_NAMES.has(name);
205
+ }
206
+
181
207
  /** Avoid collisions with Cursor's private bare-tool namespace. */
182
- function isCursorBareClientToolWireAliased(
208
+ export function isCursorBareClientToolWireAliased(
183
209
  tool: Pick<OcxTool, "namespace" | "name">,
210
+ catalog?: readonly Pick<OcxTool, "namespace" | "name">[],
184
211
  ): boolean {
185
- return !tool.namespace
186
- && !CURSOR_PROXY_OWNED_BARE_TOOL_NAMES.has(tool.name);
212
+ if (tool.namespace || CURSOR_PROXY_OWNED_BARE_TOOL_NAMES.has(tool.name)) return false;
213
+ if (isClaudeClientBareToolName(tool.name)) {
214
+ return catalog ? catalogHasBareCodexShellBridge(catalog) : false;
215
+ }
216
+ return true;
187
217
  }
188
218
 
189
- export function cursorToolWireName(tool: Pick<OcxTool, "namespace" | "name">): string {
190
- if (isCursorBareClientToolWireAliased(tool)) {
219
+ export function cursorToolWireName(
220
+ tool: Pick<OcxTool, "namespace" | "name">,
221
+ catalog?: readonly Pick<OcxTool, "namespace" | "name">[],
222
+ ): string {
223
+ if (isCursorBareClientToolWireAliased(tool, catalog)) {
191
224
  return `${CURSOR_CLIENT_TOOL_WIRE_PREFIX}${tool.name}`;
192
225
  }
193
226
  return namespacedToolName(tool.namespace, tool.name);
@@ -0,0 +1,46 @@
1
+ import type { OcxTool } from "../../types";
2
+ import type { McpToolDefinition } from "./gen/agent_pb";
3
+ import { CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize";
4
+ import { OCX_RESPONSES_TOOL_PROVIDER, cursorRequestHasExecutionPath, cursorRequestHasShellAlias, cursorRequestUsesCodeMode, cursorToolWireName } from "./tool-naming";
5
+
6
+ /** Cursor may expose Claude with version-first, family-first, or effort-suffixed ids. */
7
+ export function cursorUsesPlainToolWording(modelId?: string): boolean {
8
+ return /^(?:cursor\/)?claude-/i.test(modelId?.trim() ?? "");
9
+ }
10
+
11
+ export const CURSOR_TOOL_CALL_CONTINUATION = "Continue the task with that tool call.";
12
+ const CURSOR_NO_CLIENT_TOOLS = "Cursor-native filesystem, shell, and fetch tools are not available through this execution channel. No client tools are available in this request. Answer without tools using the information already provided, or report that the requested operation cannot be completed without tools.";
13
+
14
+ /** Factual catalog wording; legacy model wording is owned by native-exec.ts unchanged. */
15
+ export function cursorPlainNativeExecRedirectHint(
16
+ tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
17
+ mcpToolDefs: readonly Pick<McpToolDefinition, "name" | "providerIdentifier">[],
18
+ ): string | undefined {
19
+ const clientTools = tools ?? [];
20
+ const names = [...new Set([
21
+ ...clientTools.map(tool => cursorToolWireName(tool, clientTools)),
22
+ ...mcpToolDefs.map(def => `mcp_${def.providerIdentifier}_${def.name}`),
23
+ ])];
24
+ if (names.length === 0) return CURSOR_NO_CLIENT_TOOLS;
25
+ const shown = names.slice(0, 16).map(name => `\`${name}\``).join(", ");
26
+ const more = names.length > 16 ? ` (+${names.length - 16} more)` : "";
27
+ const catalog = `Available tools in this request's catalog: ${shown}${more}. `
28
+ + `The harness displays a \`${OCX_RESPONSES_TOOL_PROVIDER}\` entry as \`mcp_${OCX_RESPONSES_TOOL_PROVIDER}_<name>\`; that is the same tool. `
29
+ + "Cursor-native Read/Glob/Grep/LS/Shell/Write/Fetch are not available in this request. ";
30
+ if (!cursorRequestHasShellAlias(clientTools) && cursorRequestUsesCodeMode(clientTools)) {
31
+ return catalog + "`exec` takes a JavaScript body evaluated in a V8 isolate, not a shell command. Shell, file, search, and fetch are nested helpers called inside it as `await tools.<name>(...)`, for example `text(await tools.exec_command({cmd: \"ls\"}))`. "
32
+ + "`shell_command` and `exec_command` are nested helpers, not top-level tools here. Every other listed tool remains callable at the top level. "
33
+ + CODE_MODE_RESULT_ECHO_SENTENCE + " " + CURSOR_TOOL_CALL_CONTINUATION;
34
+ }
35
+ const operation = cursorRequestHasExecutionPath(clientTools)
36
+ ? "Use the listed execution tool with its advertised argument schema and the Codex client host's shell syntax; use a listed edit tool for file edits. "
37
+ : "Use the listed tool that fits the operation: a file, search, or fetch tool when provided, or a listed worker-agent delegation tool. ";
38
+ return catalog + operation + CURSOR_TOOL_CALL_CONTINUATION;
39
+ }
40
+
41
+ /** Fallback uses advertised dispatcher definitions, never assumed shell/edit tools. */
42
+ export function cursorPlainNativeExecFallback(
43
+ toolDefs: readonly Pick<McpToolDefinition, "name" | "providerIdentifier">[] = [],
44
+ ): string {
45
+ return cursorPlainNativeExecRedirectHint([], toolDefs)!;
46
+ }
@@ -65,6 +65,7 @@ import {
65
65
  type CursorTransportFactory,
66
66
  } from "./cursor/transport";
67
67
  import { cursorLiveRosterScope } from "./cursor/catalog";
68
+ import { cursorUsesPlainToolWording } from "./cursor/tool-wording";
68
69
 
69
70
  export const CURSOR_API_URL = "https://api2.cursor.sh";
70
71
 
@@ -313,8 +314,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
313
314
  const armMidstreamEcho = cursorNeedsExternalToolContinuation(activeRequest.modelId) && replaysToolResult;
314
315
  const midstreamObserver = armMidstreamEcho ? new CursorMidstreamEchoObserver() : undefined;
315
316
  const midstreamFilter = armMidstreamEcho ? new ToolEnvelopeEchoFilter() : undefined;
317
+ // Claude's factual redirects allow truthful routing prose; it is not a failure claim.
316
318
  const armRoutingCommentarySniffer =
317
319
  isCursorExternalWireModel(activeRequest.modelId)
320
+ && !cursorUsesPlainToolWording(activeRequest.modelId)
318
321
  && (
319
322
  cursorRequestUsesCodeMode(activeRequest.tools, activeRequest.toolChoice)
320
323
  || cursorRequestHasShellAlias(activeRequest.tools)
@@ -524,30 +524,44 @@ export function mapOcxMessagesToDevin(
524
524
  if (system) items.push({ role: "system", content: system });
525
525
 
526
526
  let previousToolResult: ChatHistoryItem | undefined;
527
+ let textChunks: string[] | undefined;
528
+ const flushText = () => {
529
+ if (previousToolResult && textChunks) previousToolResult.content = textChunks.join("\n\n");
530
+ textChunks = undefined;
531
+ };
527
532
  for (const message of parsed.context.messages) {
528
533
  // Original-message adjacency matters even when an intervening message maps to nothing.
529
- if (message.role !== "toolResult") previousToolResult = undefined;
534
+ if (message.role !== "toolResult") {
535
+ flushText();
536
+ previousToolResult = undefined;
537
+ }
530
538
  const mapped = mapOneMessage(message, parsed.modelId, options);
531
539
  if (!mapped) continue;
532
540
  if (mapped.role === "tool") {
533
541
  if (previousToolResult && previousToolResult.tool_call_id === mapped.tool_call_id) {
534
542
  const previous = previousToolResult;
535
- previous.content = typeof previous.content === "string" && typeof mapped.content === "string"
536
- ? `${previous.content}\n\n${mapped.content}`
537
- : [
538
- ...(typeof previous.content === "string"
539
- ? [{ type: "text" as const, text: previous.content }] : previous.content),
540
- { type: "text", text: "\n\n" },
541
- ...(typeof mapped.content === "string"
542
- ? [{ type: "text" as const, text: mapped.content }] : mapped.content),
543
- ];
543
+ if (textChunks && typeof mapped.content === "string") {
544
+ textChunks.push(mapped.content);
545
+ } else {
546
+ flushText();
547
+ // mapOneMessage owns these wire arrays; append without recopying their prefix.
548
+ const parts: ContentPart[] = typeof previous.content === "string"
549
+ ? [{ type: "text", text: previous.content }] : previous.content;
550
+ parts.push({ type: "text", text: "\n\n" });
551
+ if (typeof mapped.content === "string") parts.push({ type: "text", text: mapped.content });
552
+ else for (const part of mapped.content) parts.push(part);
553
+ previous.content = parts;
554
+ }
544
555
  if (mapped.is_error) previous.is_error = true;
545
556
  continue;
546
557
  }
558
+ flushText();
547
559
  previousToolResult = mapped;
560
+ textChunks = typeof mapped.content === "string" ? [mapped.content] : undefined;
548
561
  }
549
562
  items.push(mapped);
550
563
  }
564
+ flushText();
551
565
  return items;
552
566
  }
553
567
 
@@ -795,7 +809,7 @@ export function createDevinAdapter(
795
809
  // An admitted HTTP turn owns globally shared capacity until this call
796
810
  // emits. Without an explicit wait allowance, preserve the typed reset
797
811
  // delay in generated diagnostic wording and return immediately.
798
- const signedMessages = mapOcxMessagesToDevin(parsed);
812
+ const signedMessages = messages; // Reuse the history already mapped above.
799
813
  // A Claude signature is replayed because it is what carries the reasoning into this
800
814
  // turn, but Cognition streams Claude's thinking as a summary the signature does not
801
815
  // cover, and some replays are refused with invalid_argument before any output. That
@@ -144,7 +144,9 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
144
144
  const nativeOpenAI = isNativeOpenAIChatTarget(provider);
145
145
  // Qwen3.8-27B's pinned chat template rejects non-leading system and every developer role;
146
146
  // a later user is rendered in place. Keep this exception specific to that template family.
147
- const qwen38LeadingSystemTemplate = !nativeOpenAI && /^(?:Qwen\/)?(?:Qwen3\.8-27B|qwen3-8-27b(?:-(?:fp8|lora))?)$/i.test(parsed.modelId);
147
+ // Gateways serve it under namespaced ids (LiteLLM's openai/Qwen3.8-27B, #6674), so any number
148
+ // of leading path segments is accepted; the final segment still has to name the checkpoint.
149
+ const qwen38LeadingSystemTemplate = !nativeOpenAI && /^(?:[^/]+\/)*(?:Qwen3\.8-27B|qwen3-8-27b(?:-(?:fp8|lora))?)$/i.test(parsed.modelId);
148
150
  // Which role a developer message carries, and why the unrecorded state folds, is stated once
149
151
  // in ./developer-role.ts and read from there by the native passthrough as well. Either way the
150
152
  // message keeps the slot it arrived in — only the role changes, never the position.
@@ -72,6 +72,15 @@ function toolResultOutput(block: Rec): string | Rec[] {
72
72
  if (!isRec(item)) continue;
73
73
  if (item.type === "text" && typeof item.text === "string") {
74
74
  out.push({ type: "input_text", text: item.text });
75
+ } else if (item.type === "tool_reference" && typeof item.tool_name === "string" && item.tool_name.length > 0) {
76
+ // Responses has no tool_reference result carrier. Preserve ToolSearch's loaded-name
77
+ // evidence as result text so a reference-only reply does not become empty (#6647).
78
+ // This describes client output; it must not create tools or widen authorization.
79
+ // Text-only tool output is later joined without separators, so the marker carries its
80
+ // own line breaks to keep each loaded name distinct from its neighbours.
81
+ const prev = out[out.length - 1];
82
+ const lead = prev?.type === "input_text" && typeof prev.text === "string" && !prev.text.endsWith("\n") ? "\n" : "";
83
+ out.push({ type: "input_text", text: `${lead}Tool loaded: ${item.tool_name}\n` });
75
84
  } else if (item.type === "image") {
76
85
  const img = imageBlockToInputImage(item);
77
86
  if (img) out.push(img);
@@ -4,7 +4,7 @@ import type { OcxConfig } from "../types";
4
4
  import { getCodexRoutingKind, type CodexRoutingKind } from "./inject";
5
5
  import { collectRoutingAdoption, type RoutingAdoptionEvidence } from "./routing-adoption";
6
6
  import { diagnoseCodexShim, type CodexShimDiagnostic } from "./shim";
7
- import { diagnoseMacDesktopStartup, type DesktopStartupDiagnostic } from "../service/desktop-startup";
7
+ import { diagnoseDesktopStartup, type DesktopStartupDiagnostic } from "../service/desktop-startup";
8
8
 
9
9
  export type StartupProtection = "service" | "desktop" | "shim" | "none";
10
10
  export type StartupHealthStatus = "native" | "protected" | "at-risk";
@@ -90,7 +90,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
90
90
  // We can only credit an opencodex service/shim for routing that opencodex owns.
91
91
  // An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
92
92
  const ownsLocalRouting = inputs.routingKind === "opencodex-local";
93
- const desktopEffective = inputs.platform === "darwin" && !inputs.diagnosticStale
93
+ const desktopEffective = (inputs.platform === "darwin" || inputs.platform === "linux") && !inputs.diagnosticStale
94
94
  && inputs.desktop?.owned === true && inputs.desktop.loginEnabled
95
95
  && inputs.desktop.running && inputs.desktop.viable;
96
96
  const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
@@ -147,7 +147,7 @@ export function collectStartupHealth(
147
147
  const shim = diagnostics.shim ?? diagnoseCodexShim();
148
148
  const service = diagnostics.service ?? diagnoseService();
149
149
  const routingKind = diagnostics.routingKind ?? getCodexRoutingKind();
150
- const desktop = diagnostics.desktop ?? diagnoseMacDesktopStartup();
150
+ const desktop = diagnostics.desktop ?? diagnoseDesktopStartup();
151
151
  const routingAdoption = diagnostics.routingAdoption
152
152
  ?? (routingKind === "opencodex-local" ? collectRoutingAdoption({ routingKind }) : undefined);
153
153
  return deriveStartupHealth({
@@ -85,6 +85,9 @@ export interface RestoreJournalResult {
85
85
  ownershipRefusal?: CodexHomeOwnerRefusalReason;
86
86
  configRestored: boolean;
87
87
  profileRestored: boolean;
88
+ /** True only when restore rewrote or removed a file (not when state was already original). */
89
+ configRewritten: boolean;
90
+ profileRewritten: boolean;
88
91
  configChanged: boolean;
89
92
  profileChanged: boolean;
90
93
  complete: boolean;
@@ -299,12 +302,13 @@ export function restoreJournalState(): RestoreJournalResult {
299
302
  try { assertCodexHomeOwner(CODEX_HOME); }
300
303
  catch (error) {
301
304
  if (!(error instanceof CodexHomeOwnerRefusal)) throw error;
302
- return { configRestored: false, profileRestored: false, configChanged: false, profileChanged: false,
303
- complete: false, unverified: true, ownershipRefusal: error.reason };
305
+ return { configRestored: false, profileRestored: false, configRewritten: false, profileRewritten: false,
306
+ configChanged: false, profileChanged: false, complete: false, unverified: true, ownershipRefusal: error.reason };
304
307
  }
305
308
  const journal = readJournal();
306
309
  if (!journal) {
307
- return { configRestored: false, profileRestored: false, configChanged: false, profileChanged: false, complete: false, unverified: false };
310
+ return { configRestored: false, profileRestored: false, configRewritten: false, profileRewritten: false,
311
+ configChanged: false, profileChanged: false, complete: false, unverified: false };
308
312
  }
309
313
  const currentConfig = existsSync(CODEX_CONFIG_PATH) ? readFileSync(CODEX_CONFIG_PATH, "utf-8") : null;
310
314
  const currentProfile = existsSync(CODEX_PROFILE_PATH) ? readFileSync(CODEX_PROFILE_PATH, "utf-8") : null;
@@ -316,6 +320,8 @@ export function restoreJournalState(): RestoreJournalResult {
316
320
  return {
317
321
  configRestored: comparison.configAlreadyOriginal,
318
322
  profileRestored: comparison.profileAlreadyOriginal,
323
+ configRewritten: false,
324
+ profileRewritten: false,
319
325
  configChanged: !configUnchanged,
320
326
  profileChanged: !profileUnchanged,
321
327
  complete: false,
@@ -326,14 +332,18 @@ export function restoreJournalState(): RestoreJournalResult {
326
332
  assertCodexHomeOwner(CODEX_HOME);
327
333
  let configRestored = comparison.configAlreadyOriginal;
328
334
  let profileRestored = comparison.profileAlreadyOriginal;
335
+ let configRewritten = false;
336
+ let profileRewritten = false;
329
337
  if (configUnchanged && !configRestored) {
330
338
  atomicWriteFile(CODEX_CONFIG_PATH, comparison.originalConfig);
331
339
  configRestored = true;
340
+ configRewritten = true;
332
341
  }
333
342
  if (profileUnchanged && !profileRestored) {
334
343
  if (comparison.originalProfile !== null) {
335
344
  atomicWriteFile(CODEX_PROFILE_PATH, comparison.originalProfile);
336
345
  profileRestored = true;
346
+ profileRewritten = true;
337
347
  } else if (existsSync(CODEX_PROFILE_PATH)) {
338
348
  // "There was no profile before, so remove the one we generated." Claiming success
339
349
  // without checking is how a caller ends up deleting the journal, reporting a clean
@@ -343,6 +353,7 @@ export function restoreJournalState(): RestoreJournalResult {
343
353
  try {
344
354
  unlinkSync(CODEX_PROFILE_PATH);
345
355
  profileRestored = true;
356
+ profileRewritten = true;
346
357
  } catch (error) {
347
358
  profileRestored = (error as NodeJS.ErrnoException).code === "ENOENT";
348
359
  }
@@ -355,6 +366,8 @@ export function restoreJournalState(): RestoreJournalResult {
355
366
  return {
356
367
  configRestored,
357
368
  profileRestored,
369
+ configRewritten,
370
+ profileRewritten,
358
371
  configChanged: !configUnchanged,
359
372
  profileChanged: !profileUnchanged,
360
373
  complete,
@@ -367,6 +380,11 @@ export function restoreJournal(): boolean {
367
380
  return restoreJournalState().complete;
368
381
  }
369
382
 
383
+ /** A no-rewrite recovery is quiet only when the stale journal is actually gone. */
384
+ function warnIfJournalRetained(): void {
385
+ if (existsSync(JOURNAL_PATH)) console.error("⚠️ Codex journal recovery found nothing to restore, but the journal could not be removed; it will be retried on the next start.");
386
+ }
387
+
370
388
  export interface ReconcileJournalOptions {
371
389
  activeClientApiKeyId?: string;
372
390
  }
@@ -388,7 +406,13 @@ export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean
388
406
  console.error("⚠️ Codex journal recovery was not verified; current configuration files and the journal were preserved.");
389
407
  return false;
390
408
  }
391
- if (!restored.configRestored && !restored.profileRestored) return false;
409
+ if (!restored.complete) {
410
+ console.error("⚠️ Codex journal recovery was incomplete; the journal was preserved — check the Codex files.");
411
+ return false;
412
+ }
413
+ // Warn only when restore actually rewrote state; an already-original
414
+ // snapshot still cleans itself up but has nothing to report.
415
+ if (!restored.configRewritten && !restored.profileRewritten) { warnIfJournalRetained(); return false; }
392
416
  console.error(`⚠️ Uncommitted or mismatched client routing (${owner.apiKeyId}) was restored from the Codex journal.`);
393
417
  return true;
394
418
  }
@@ -406,7 +430,14 @@ export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean
406
430
  console.error("⚠️ Codex journal recovery was not verified; current configuration files and the journal were preserved.");
407
431
  return false;
408
432
  }
409
- if (!restored.configRestored && !restored.profileRestored) return false;
433
+ if (!restored.complete) {
434
+ console.error("⚠️ Codex journal recovery was incomplete; the journal was preserved — check the Codex files.");
435
+ return false;
436
+ }
437
+ // Warn only when restore actually rewrote state. A stale journal whose
438
+ // artifacts are already original still cleans itself up (complete removes
439
+ // it) but has nothing to report — claiming a restore then cries wolf.
440
+ if (!restored.configRewritten && !restored.profileRewritten) { warnIfJournalRetained(); return false; }
410
441
  console.error(`⚠️ Previous session (PID ${pid}) did not shut down cleanly. Codex state restored from journal.`);
411
442
  return true;
412
443
  }
@@ -48,11 +48,19 @@ function lexical(path: string): Stats | null {
48
48
  try { return lstatSync(path); }
49
49
  catch (error) { if (fileErrorCode(error) === "ENOENT") return null; throw error; }
50
50
  }
51
+ /** Name the path and the failed check; a group/world-writable owned file gets the chmod that fixes it. */
52
+ function untrustedArtifact(path: string, stat: Stats): Error {
53
+ const prefix = `Private Codex artifact is not an owned regular file: ${path}`;
54
+ if (stat.isSymbolicLink() || !stat.isFile()) return new Error(`${prefix} is not a regular file`);
55
+ if (stat.uid !== process.getuid?.()) return new Error(`${prefix} is owned by uid ${stat.uid}`);
56
+ const mode = (stat.mode & 0o777).toString(8).padStart(4, "0");
57
+ return new Error(`${prefix} is group- or world-writable (mode ${mode}); run chmod 600 ${shQuote(path)} and retry`);
58
+ }
51
59
  function snapshot(path: string): Snapshot | null {
52
60
  const before = lexical(path);
53
61
  if (!before) return null;
54
62
  if (!before.isFile() || before.isSymbolicLink() || before.uid !== process.getuid?.() || (before.mode & 0o022)) {
55
- throw new Error("Private Codex artifact is not an owned regular file");
63
+ throw untrustedArtifact(path, before);
56
64
  }
57
65
  const bounded = readBoundedRegularFile(path, PRIVATE_FILE_MAX_BYTES);
58
66
  if (!bounded || "warning" in bounded) throw new Error("Private Codex artifact is unreadable or changed");
package/src/combos/jev.ts CHANGED
@@ -47,6 +47,7 @@ const TOOL_NAME_CHARS = 160;
47
47
  const VISIBLE_TEXT_CHUNK_CHARS = 16_384;
48
48
 
49
49
  const ENVELOPE_TAGS = [
50
+ "system-reminder",
50
51
  "codex_internal_context",
51
52
  "recommended_plugins",
52
53
  "environment_context",
@@ -302,7 +303,8 @@ function taskWithoutProtectedEnvelopes(text: string): string {
302
303
  }
303
304
  } else {
304
305
  if (stack.length === 0) appendTrimmedRange(visible, "\n", 0, 1);
305
- if (!completedGoal && !goal && tag === "codex_internal_context") {
306
+ // A Codex goal quoted inside a Claude reminder is context, never a fallback task.
307
+ if (!completedGoal && !goal && tag === "codex_internal_context" && !stack.includes("system-reminder")) {
306
308
  goal = { sample: emptyTextSample(), pendingWhitespace: emptyTextSample() };
307
309
  goalDepth = stack.length;
308
310
  }