@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.
- package/gui/dist/assets/{App-BOUa-ek3.js → App-MbDVfUeC.js} +3 -3
- package/gui/dist/assets/{Tray-DUtzB4zF.js → Tray-CzkxK82A.js} +1 -1
- package/gui/dist/assets/{index-DwHNDF4s.js → index-UJt6npXk.js} +2 -2
- package/gui/dist/assets/{quota-summary-BuL4WIBo.js → quota-summary-DU3sD6Z-.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/openai-chat/messages.ts +3 -1
- package/src/claude/inbound.ts +9 -0
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev.ts +3 -1
- package/src/generated/compatibility-version.json +35 -23
- package/src/server/index.ts +12 -14
- package/src/server/messages-native-scope.ts +4 -1
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/core-combo.ts +5 -7
- package/src/service/desktop-startup.ts +100 -3
- package/src/web-search/header-deadline.ts +67 -0
- 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(
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
-
|
|
186
|
-
|
|
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(
|
|
190
|
-
|
|
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
|
+
}
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -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)
|
package/src/adapters/devin.ts
CHANGED
|
@@ -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")
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
package/src/claude/inbound.ts
CHANGED
|
@@ -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 {
|
|
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 ??
|
|
150
|
+
const desktop = diagnostics.desktop ?? diagnoseDesktopStartup();
|
|
151
151
|
const routingAdoption = diagnostics.routingAdoption
|
|
152
152
|
?? (routingKind === "opencodex-local" ? collectRoutingAdoption({ routingKind }) : undefined);
|
|
153
153
|
return deriveStartupHealth({
|
package/src/codex/journal.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
}
|