@bitkyc08/opencodex 2.7.41 → 2.7.42
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/README.md +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
- package/gui/dist/assets/index-DfVGuN88.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/kiro-constants.ts +6 -2
- package/src/adapters/kiro-retry.ts +175 -10
- package/src/adapters/kiro.ts +172 -85
- package/src/adapters/mimo-free.ts +1 -0
- package/src/adapters/openai-chat.ts +30 -4
- package/src/adapters/openai-responses.ts +90 -12
- package/src/bridge.ts +91 -43
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +29 -2
- package/src/cli/access.ts +108 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account.ts +9 -1
- package/src/cli/agent.ts +184 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +20 -8
- package/src/cli/doctor.ts +45 -8
- package/src/cli/help.ts +65 -13
- package/src/cli/index.ts +108 -7
- package/src/cli/integrations.ts +142 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +9 -10
- package/src/cli/observe.ts +117 -0
- package/src/cli/provider-runtime.ts +152 -0
- package/src/cli/provider.ts +23 -1
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +3 -3
- package/src/cli/status.ts +17 -0
- package/src/cli/system-command.ts +112 -0
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/aggregation.ts +113 -18
- package/src/codex/catalog/provider-fetch.ts +24 -13
- package/src/codex/catalog/sync.ts +20 -8
- package/src/codex/catalog.ts +2 -1
- package/src/codex/refresh.ts +10 -3
- package/src/codex/routing.ts +21 -32
- package/src/codex/sync.ts +17 -0
- package/src/config.ts +48 -0
- package/src/generated/jawcode-model-metadata.ts +2 -1
- package/src/grok/inject.ts +184 -4
- package/src/grok/status.ts +33 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/windows-elevation.ts +627 -0
- package/src/providers/openai-sidecar.ts +46 -2
- package/src/providers/registry.ts +52 -0
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/claude-messages.ts +20 -1
- package/src/server/images.ts +14 -7
- package/src/server/management/agent-settings-routes.ts +10 -4
- package/src/server/management/combo-routes.ts +0 -1
- package/src/server/management/config-routes.ts +0 -1
- package/src/server/management/logs-usage-routes.ts +94 -0
- package/src/server/management/model-routes.ts +0 -1
- package/src/server/management/oauth-account-routes.ts +0 -1
- package/src/server/management/provider-routes.ts +0 -1
- package/src/server/management/shared.ts +0 -1
- package/src/server/management/system-routes.ts +27 -15
- package/src/server/management-api.ts +0 -1
- package/src/server/memory-watchdog.ts +54 -10
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +122 -2
- package/src/server/responses/core.ts +76 -13
- package/src/server/responses/passthrough-error.ts +38 -13
- package/src/server/startup-action-control.ts +266 -15
- package/src/service.ts +512 -3
- package/src/storage/cleanup.ts +1538 -0
- package/src/storage/scanner.ts +4 -1
- package/src/types.ts +16 -0
- package/src/update/job.ts +229 -25
- package/src/usage/log.ts +39 -0
- package/src/web-search/loop.ts +8 -1
- package/gui/dist/assets/index-B2J4t3te.css +0 -1
- package/gui/dist/assets/index-BmvM6wRb.js +0 -65
|
@@ -219,6 +219,28 @@ function stripDisabledReasoningSummaries(
|
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Normalize only the delivery enum Codex already emitted. Do not inject a field into callers that
|
|
224
|
+
* did not request summaries, and leave every unconfigured provider/model byte-for-byte unchanged.
|
|
225
|
+
*/
|
|
226
|
+
function normalizeConfiguredReasoningSummaryDelivery(
|
|
227
|
+
body: unknown,
|
|
228
|
+
provider: OcxProviderConfig,
|
|
229
|
+
modelId: string,
|
|
230
|
+
): unknown {
|
|
231
|
+
const delivery = modelRecordValue(provider.modelReasoningSummaryDelivery, modelId);
|
|
232
|
+
if (delivery === undefined || !isPlainObject(body) || !isPlainObject(body.stream_options)) return body;
|
|
233
|
+
if (!Object.hasOwn(body.stream_options, "reasoning_summary_delivery")) return body;
|
|
234
|
+
if (body.stream_options.reasoning_summary_delivery === delivery) return body;
|
|
235
|
+
return {
|
|
236
|
+
...body,
|
|
237
|
+
stream_options: {
|
|
238
|
+
...body.stream_options,
|
|
239
|
+
reasoning_summary_delivery: delivery,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
222
244
|
/**
|
|
223
245
|
* Comprehensive Spark compatibility layer. codex-rs emits five tool types (function,
|
|
224
246
|
* namespace, tool_search, web_search, custom) plus extensions (defer_loading,
|
|
@@ -496,26 +518,81 @@ const HOSTED_TOOL_NAME_CONFLICTS: ReadonlyArray<{ hostedType: string; namePrefix
|
|
|
496
518
|
* HOSTED_TOOL_NAME_CONFLICTS). Only applies on the API-key platform path: the ChatGPT backend
|
|
497
519
|
* ("forward" mode) accepts the pair, and stripping there would disable native imagegen. No-op
|
|
498
520
|
* (returns the original reference) when nothing matches.
|
|
521
|
+
*
|
|
522
|
+
* Codex Desktop Responses Lite requests do not always use top-level `body.tools`; they may place
|
|
523
|
+
* the same declarations in `body.input[]` entries of type `additional_tools`. The platform validates
|
|
524
|
+
* these containers as one tool namespace, so conflict detection must span all of them. The function
|
|
525
|
+
* uses copy-on-write semantics and preserves the original request reference when nothing is removed.
|
|
499
526
|
*/
|
|
500
527
|
function stripConflictingHostedTools(body: unknown): unknown {
|
|
501
|
-
if (!isPlainObject(body)
|
|
502
|
-
|
|
528
|
+
if (!isPlainObject(body)) return body;
|
|
529
|
+
|
|
530
|
+
// Collect every tool container in the request. Traditional HTTP/SSE requests use top-level
|
|
531
|
+
// `tools`, while Responses Lite may carry the same declarations in `additional_tools` entries.
|
|
532
|
+
const toolGroups: unknown[][] = [];
|
|
533
|
+
if (Array.isArray(body.tools)) toolGroups.push(body.tools);
|
|
534
|
+
if (Array.isArray(body.input)) {
|
|
535
|
+
for (const item of body.input) {
|
|
536
|
+
if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
|
|
537
|
+
toolGroups.push(item.tools);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (toolGroups.length === 0) return body;
|
|
503
542
|
|
|
504
|
-
|
|
505
|
-
|
|
543
|
+
// Detect conflicts across containers because the client namespace and the hosted tool may be
|
|
544
|
+
// declared in different groups while still sharing one platform-validated namespace.
|
|
545
|
+
const conflicting = HOSTED_TOOL_NAME_CONFLICTS.filter(c => toolGroups.some(group =>
|
|
546
|
+
group.some(t => {
|
|
506
547
|
if (!isPlainObject(t) || typeof t.name !== "string") return false;
|
|
507
548
|
if (t.type === "namespace") return t.name === c.namePrefix;
|
|
508
549
|
return t.name === c.namePrefix || t.name.startsWith(`${c.namePrefix}.`);
|
|
509
550
|
}),
|
|
510
|
-
);
|
|
551
|
+
));
|
|
511
552
|
if (conflicting.length === 0) return body;
|
|
512
553
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
554
|
+
// Allocate a replacement only when a hosted tool is removed. Preserve malformed entries,
|
|
555
|
+
// unrelated tools, and future tool types so this compatibility rule remains narrowly scoped.
|
|
556
|
+
const stripGroup = (tools: unknown[]): unknown[] => {
|
|
557
|
+
const filtered = tools.filter(t => {
|
|
558
|
+
const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
|
|
559
|
+
if (!type) return true;
|
|
560
|
+
return !conflicting.some(c => c.hostedType === type);
|
|
561
|
+
});
|
|
562
|
+
return filtered.length === tools.length ? tools : filtered;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
let changed = false;
|
|
566
|
+
let tools = body.tools;
|
|
567
|
+
if (Array.isArray(body.tools)) {
|
|
568
|
+
tools = stripGroup(body.tools);
|
|
569
|
+
changed ||= tools !== body.tools;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
let input = body.input;
|
|
573
|
+
if (Array.isArray(body.input)) {
|
|
574
|
+
let nestedChanged = false;
|
|
575
|
+
const mappedInput = body.input.map(item => {
|
|
576
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) {
|
|
577
|
+
return item;
|
|
578
|
+
}
|
|
579
|
+
const nestedTools = stripGroup(item.tools);
|
|
580
|
+
if (nestedTools === item.tools) return item;
|
|
581
|
+
nestedChanged = true;
|
|
582
|
+
return { ...item, tools: nestedTools };
|
|
583
|
+
});
|
|
584
|
+
if (nestedChanged) {
|
|
585
|
+
input = mappedInput;
|
|
586
|
+
changed = true;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (!changed) return body;
|
|
591
|
+
return {
|
|
592
|
+
...body,
|
|
593
|
+
...(Array.isArray(body.tools) ? { tools } : {}),
|
|
594
|
+
...(Array.isArray(body.input) ? { input } : {}),
|
|
595
|
+
};
|
|
519
596
|
}
|
|
520
597
|
|
|
521
598
|
/**
|
|
@@ -674,12 +751,13 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
674
751
|
if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
|
|
675
752
|
outBody = buildRoutedCompactionBody(outBody);
|
|
676
753
|
}
|
|
754
|
+
const sanitizedBody = stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody)))))));
|
|
677
755
|
return {
|
|
678
756
|
url,
|
|
679
757
|
method: "POST",
|
|
680
758
|
headers,
|
|
681
759
|
body: JSON.stringify(stripDisabledReasoningSummaries(
|
|
682
|
-
|
|
760
|
+
normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
|
|
683
761
|
provider,
|
|
684
762
|
parsed.modelId,
|
|
685
763
|
)),
|
package/src/bridge.ts
CHANGED
|
@@ -133,8 +133,18 @@ export function bridgeToResponsesSSE(
|
|
|
133
133
|
* from this callback instead of re-parsing the bridged SSE.
|
|
134
134
|
*/
|
|
135
135
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
136
|
+
/**
|
|
137
|
+
* Test seam for the wire/stall beat loop. Production omits this and uses the
|
|
138
|
+
* global timers; injecting here must not change scheduling semantics.
|
|
139
|
+
*/
|
|
140
|
+
timers?: {
|
|
141
|
+
setInterval: (handler: () => void, ms: number) => unknown;
|
|
142
|
+
clearInterval: (id: unknown) => void;
|
|
143
|
+
};
|
|
136
144
|
},
|
|
137
145
|
): ReadableStream<Uint8Array> {
|
|
146
|
+
const setBeatInterval = options?.timers?.setInterval ?? ((handler: () => void, ms: number) => setInterval(handler, ms));
|
|
147
|
+
const clearBeatInterval = options?.timers?.clearInterval ?? ((id: unknown) => clearInterval(id as ReturnType<typeof setInterval>));
|
|
138
148
|
// Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
|
|
139
149
|
// function with `{input:string}`, so unwrap it here when relaying back as a custom_tool_call.
|
|
140
150
|
const freeformInput = (args: string): string => {
|
|
@@ -189,17 +199,19 @@ export function bridgeToResponsesSSE(
|
|
|
189
199
|
};
|
|
190
200
|
// RC3 keep-alive: Codex's idle timer is timeout(idle_timeout, stream.next()) over an
|
|
191
201
|
// eventsource_stream; ANY received event re-arms it, while an unknown type is ignored
|
|
192
|
-
// (responses.rs `_ => Ok(None)`).
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
let
|
|
202
|
+
// (responses.rs `_ => Ok(None)`). Emit a parser-ignored `response.heartbeat` whenever the
|
|
203
|
+
// *wire* has been silent, even if invisible adapter heartbeats are still flowing (web-search
|
|
204
|
+
// buffering + raw-byte progress). Upstream activity only resets the stall watchdog.
|
|
205
|
+
let upstreamActivity = false;
|
|
206
|
+
let wireActivity = false;
|
|
207
|
+
let beat: unknown;
|
|
196
208
|
let controller: ReadableStreamDefaultController<Uint8Array>;
|
|
197
209
|
let emittedFrames = 0;
|
|
198
210
|
let gated = false;
|
|
199
211
|
let stepping = false;
|
|
200
212
|
const emit = (name: string, data: Record<string, unknown>) => {
|
|
201
213
|
if (closed) return;
|
|
202
|
-
|
|
214
|
+
wireActivity = true;
|
|
203
215
|
try {
|
|
204
216
|
controller.enqueue(encoder.encode(sseEvent(name, { type: name, sequence_number: seq++, ...data })));
|
|
205
217
|
emittedFrames++;
|
|
@@ -302,8 +314,12 @@ export function bridgeToResponsesSSE(
|
|
|
302
314
|
return anns;
|
|
303
315
|
};
|
|
304
316
|
|
|
305
|
-
const closeCurrentMessage = () => {
|
|
317
|
+
const closeCurrentMessage = (inferredPhase?: OcxMessagePhase) => {
|
|
306
318
|
if (!currentMsg) return;
|
|
319
|
+
// Chat Completions has no message-phase field. Keep its live item provisional, then
|
|
320
|
+
// classify it only when the next adapter event proves whether this text led into more
|
|
321
|
+
// work or completed the turn. Explicit adapter phases always outrank this inference.
|
|
322
|
+
const phase = currentMsg.phase ?? inferredPhase;
|
|
307
323
|
// Bind any pending web-search citations to this assistant message (then they clear).
|
|
308
324
|
const annotations = takeWebAnnotations();
|
|
309
325
|
// Finalize the text part (Responses protocol). Without these .done events Codex never
|
|
@@ -318,7 +334,7 @@ export function bridgeToResponsesSSE(
|
|
|
318
334
|
const item = {
|
|
319
335
|
type: "message", id: currentMsg.itemId, status: "completed", role: "assistant",
|
|
320
336
|
content: [{ type: "output_text", text: currentMsg.text, annotations }],
|
|
321
|
-
...(
|
|
337
|
+
...(phase ? { phase } : {}),
|
|
322
338
|
};
|
|
323
339
|
emit("response.output_item.done", { output_index: currentMsg.outputIndex, item });
|
|
324
340
|
finishedItems.push(item as OutputItem);
|
|
@@ -478,7 +494,9 @@ export function bridgeToResponsesSSE(
|
|
|
478
494
|
if (next.done) { upstreamDone = true; break; }
|
|
479
495
|
const event = next.value;
|
|
480
496
|
let terminalEvent = false;
|
|
481
|
-
|
|
497
|
+
// Invisible adapter heartbeats (and buffered web-search progress) count as upstream
|
|
498
|
+
// liveness only — they must not suppress wire keepalives that re-arm Codex idle timers.
|
|
499
|
+
upstreamActivity = true;
|
|
482
500
|
stallTicks = 0;
|
|
483
501
|
reportFirstOutput(event);
|
|
484
502
|
// Compaction turns emit ONLY the synthetic compaction item + response.completed. The
|
|
@@ -494,7 +512,7 @@ export function bridgeToResponsesSSE(
|
|
|
494
512
|
case "assistant_boundary": {
|
|
495
513
|
// A guarded continuation starts a fresh assistant output item while keeping the
|
|
496
514
|
// intermediate, suspicious text in the same Responses turn.
|
|
497
|
-
if (currentMsg) closeCurrentMessage();
|
|
515
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
498
516
|
if (currentReasoning) closeCurrentReasoning();
|
|
499
517
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
500
518
|
flushHiddenRawReasoning();
|
|
@@ -507,7 +525,11 @@ export function bridgeToResponsesSSE(
|
|
|
507
525
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
508
526
|
flushHiddenRawReasoning();
|
|
509
527
|
if (currentToolCall) closeCurrentToolCall();
|
|
510
|
-
|
|
528
|
+
// Only flush on an explicit phase change. A later delta that omits `phase` must
|
|
529
|
+
// keep appending to the current message rather than wiping the earlier phase.
|
|
530
|
+
if (currentMsg && event.phase !== undefined && currentMsg.phase !== event.phase) {
|
|
531
|
+
closeCurrentMessage("commentary");
|
|
532
|
+
}
|
|
511
533
|
if (!currentMsg) {
|
|
512
534
|
const itemId = `msg_${uuid()}`;
|
|
513
535
|
const item = {
|
|
@@ -531,7 +553,7 @@ export function bridgeToResponsesSSE(
|
|
|
531
553
|
}
|
|
532
554
|
case "thinking_delta": {
|
|
533
555
|
if (options?.hideThinkingSummary) { hiddenThinkingText += event.thinking; break; }
|
|
534
|
-
if (currentMsg) closeCurrentMessage();
|
|
556
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
535
557
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
536
558
|
flushHiddenRawReasoning();
|
|
537
559
|
if (currentToolCall) closeCurrentToolCall();
|
|
@@ -566,7 +588,7 @@ export function bridgeToResponsesSSE(
|
|
|
566
588
|
}
|
|
567
589
|
case "reasoning_raw_delta": {
|
|
568
590
|
if (options?.hideThinkingSummary) { hiddenRawReasoningText += event.text; break; }
|
|
569
|
-
if (currentMsg) closeCurrentMessage();
|
|
591
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
570
592
|
if (currentReasoning) closeCurrentReasoning();
|
|
571
593
|
if (currentToolCall) closeCurrentToolCall();
|
|
572
594
|
if (!currentRawReasoning) {
|
|
@@ -583,7 +605,7 @@ export function bridgeToResponsesSSE(
|
|
|
583
605
|
break;
|
|
584
606
|
}
|
|
585
607
|
case "tool_call_start": {
|
|
586
|
-
if (currentMsg) closeCurrentMessage();
|
|
608
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
587
609
|
if (currentReasoning) closeCurrentReasoning();
|
|
588
610
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
589
611
|
flushHiddenRawReasoning();
|
|
@@ -638,7 +660,7 @@ export function bridgeToResponsesSSE(
|
|
|
638
660
|
// Open the native search cell so Codex shows the "Searching the web" spinner WHILE the
|
|
639
661
|
// sidecar runs. Close any other open item first, allocate this item's output index, and
|
|
640
662
|
// hold it open until the matching `web_search_call_end` (or a terminal close).
|
|
641
|
-
if (currentMsg) closeCurrentMessage();
|
|
663
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
642
664
|
if (currentReasoning) closeCurrentReasoning();
|
|
643
665
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
644
666
|
flushHiddenRawReasoning();
|
|
@@ -675,7 +697,7 @@ export function bridgeToResponsesSSE(
|
|
|
675
697
|
break;
|
|
676
698
|
}
|
|
677
699
|
case "done": {
|
|
678
|
-
if (currentMsg) closeCurrentMessage();
|
|
700
|
+
if (currentMsg) closeCurrentMessage(event.stopReason ? undefined : "final_answer");
|
|
679
701
|
if (currentReasoning) closeCurrentReasoning();
|
|
680
702
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
681
703
|
flushHiddenRawReasoning();
|
|
@@ -801,7 +823,7 @@ export function bridgeToResponsesSSE(
|
|
|
801
823
|
stepping = false;
|
|
802
824
|
return;
|
|
803
825
|
}
|
|
804
|
-
if (beat) {
|
|
826
|
+
if (beat !== undefined) { clearBeatInterval(beat); beat = undefined; }
|
|
805
827
|
|
|
806
828
|
if (!terminated) {
|
|
807
829
|
// The adapter generator ended without an explicit done/error event. Mark as incomplete
|
|
@@ -840,10 +862,12 @@ export function bridgeToResponsesSSE(
|
|
|
840
862
|
// The default ReadableStream strategy has HWM=1. Once one event's frames fill that
|
|
841
863
|
// queue, pull stepping pauses; no custom FIFO or queuing strategy is layered on top.
|
|
842
864
|
gated = true;
|
|
843
|
-
beat =
|
|
865
|
+
beat = setBeatInterval(() => {
|
|
844
866
|
if (closed || gated) return;
|
|
845
|
-
if (
|
|
846
|
-
|
|
867
|
+
if (upstreamActivity) {
|
|
868
|
+
upstreamActivity = false;
|
|
869
|
+
stallTicks = 0;
|
|
870
|
+
} else if (++stallTicks >= maxStallTicks) {
|
|
847
871
|
if (currentMsg) closeCurrentMessage();
|
|
848
872
|
if (currentReasoning) closeCurrentReasoning();
|
|
849
873
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
@@ -861,12 +885,17 @@ export function bridgeToResponsesSSE(
|
|
|
861
885
|
terminated = true;
|
|
862
886
|
returnIterator();
|
|
863
887
|
emitDone();
|
|
864
|
-
if (beat)
|
|
888
|
+
if (beat !== undefined) clearBeatInterval(beat);
|
|
865
889
|
beat = undefined;
|
|
866
890
|
try { controller.close(); } catch { /* already closed */ }
|
|
867
891
|
closed = true;
|
|
868
892
|
return;
|
|
869
893
|
}
|
|
894
|
+
// Wire silence is independent of upstream adapter heartbeats.
|
|
895
|
+
if (wireActivity) {
|
|
896
|
+
wireActivity = false;
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
870
899
|
try {
|
|
871
900
|
controller.enqueue(heartbeatFrame);
|
|
872
901
|
emittedFrames++;
|
|
@@ -887,14 +916,14 @@ export function bridgeToResponsesSSE(
|
|
|
887
916
|
cancel() {
|
|
888
917
|
// Client (Codex) disconnected. Stop emitting and let the caller abort the upstream fetch so a
|
|
889
918
|
// cancelled turn does not leak the upstream stream or keep draining tokens (RC2).
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
}
|
|
919
|
+
clientCancelled = true;
|
|
920
|
+
closed = true;
|
|
921
|
+
if (beat !== undefined) clearBeatInterval(beat);
|
|
922
|
+
onCancel?.();
|
|
923
|
+
returnIterator();
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
}
|
|
898
927
|
|
|
899
928
|
export function buildResponseJSON(
|
|
900
929
|
events: AdapterEvent[],
|
|
@@ -918,6 +947,7 @@ export function buildResponseJSON(
|
|
|
918
947
|
let incompleteEvent: Extract<AdapterEvent, { type: "incomplete" }> | undefined;
|
|
919
948
|
let endTurn: boolean | undefined;
|
|
920
949
|
let stopReason: string | undefined;
|
|
950
|
+
let cleanDone = false;
|
|
921
951
|
let compactionText = "";
|
|
922
952
|
|
|
923
953
|
let currentText = "";
|
|
@@ -941,8 +971,9 @@ export function buildResponseJSON(
|
|
|
941
971
|
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
942
972
|
};
|
|
943
973
|
|
|
944
|
-
const flushText = () => {
|
|
974
|
+
const flushText = (inferredPhase?: OcxMessagePhase) => {
|
|
945
975
|
if (!currentText) return;
|
|
976
|
+
const phase = currentTextPhase ?? inferredPhase;
|
|
946
977
|
const annotations = pendingWebSources.map(s => ({
|
|
947
978
|
type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
|
|
948
979
|
}));
|
|
@@ -950,7 +981,7 @@ export function buildResponseJSON(
|
|
|
950
981
|
output.push({
|
|
951
982
|
type: "message", id: `msg_${uuid()}`, role: "assistant", status: "completed",
|
|
952
983
|
content: [{ type: "output_text", text: currentText, annotations }],
|
|
953
|
-
...(
|
|
984
|
+
...(phase ? { phase } : {}),
|
|
954
985
|
});
|
|
955
986
|
currentText = "";
|
|
956
987
|
currentTextPhase = undefined;
|
|
@@ -1025,13 +1056,15 @@ export function buildResponseJSON(
|
|
|
1025
1056
|
for (const e of events) {
|
|
1026
1057
|
switch (e.type) {
|
|
1027
1058
|
case "assistant_boundary":
|
|
1028
|
-
flushText();
|
|
1059
|
+
flushText("commentary");
|
|
1029
1060
|
flushSummaryReasoning();
|
|
1030
1061
|
flushRawReasoning();
|
|
1031
1062
|
flushToolCall();
|
|
1032
1063
|
break;
|
|
1033
1064
|
case "text_delta":
|
|
1034
|
-
|
|
1065
|
+
// Only flush on an explicit phase change. A later delta that omits `phase` must keep
|
|
1066
|
+
// appending under the previously established phase.
|
|
1067
|
+
if (currentText && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
|
|
1035
1068
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1036
1069
|
if (currentRawReasoning) flushRawReasoning();
|
|
1037
1070
|
if (currentToolCallId) flushToolCall();
|
|
@@ -1039,12 +1072,12 @@ export function buildResponseJSON(
|
|
|
1039
1072
|
// bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
|
|
1040
1073
|
if (options?.compaction) compactionText += e.text;
|
|
1041
1074
|
else {
|
|
1042
|
-
currentTextPhase = e.phase;
|
|
1075
|
+
if (e.phase !== undefined) currentTextPhase = e.phase;
|
|
1043
1076
|
currentText += e.text;
|
|
1044
1077
|
}
|
|
1045
1078
|
break;
|
|
1046
1079
|
case "thinking_delta":
|
|
1047
|
-
if (currentText) flushText();
|
|
1080
|
+
if (currentText) flushText("commentary");
|
|
1048
1081
|
if (currentRawReasoning) flushRawReasoning();
|
|
1049
1082
|
if (currentToolCallId) flushToolCall();
|
|
1050
1083
|
currentSummaryReasoning += e.thinking;
|
|
@@ -1059,13 +1092,13 @@ export function buildResponseJSON(
|
|
|
1059
1092
|
batchRedacted.push(e.data);
|
|
1060
1093
|
break;
|
|
1061
1094
|
case "reasoning_raw_delta":
|
|
1062
|
-
if (currentText) flushText();
|
|
1095
|
+
if (currentText) flushText("commentary");
|
|
1063
1096
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1064
1097
|
if (currentToolCallId) flushToolCall();
|
|
1065
1098
|
currentRawReasoning += e.text;
|
|
1066
1099
|
break;
|
|
1067
1100
|
case "tool_call_start":
|
|
1068
|
-
if (currentText) flushText();
|
|
1101
|
+
if (currentText) flushText("commentary");
|
|
1069
1102
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1070
1103
|
if (currentRawReasoning) flushRawReasoning();
|
|
1071
1104
|
flushToolCall();
|
|
@@ -1084,7 +1117,7 @@ export function buildResponseJSON(
|
|
|
1084
1117
|
// single finalized item, emitted on `end`. Begin is a no-op here.
|
|
1085
1118
|
break;
|
|
1086
1119
|
case "web_search_call_end":
|
|
1087
|
-
if (currentText) flushText();
|
|
1120
|
+
if (currentText) flushText("commentary");
|
|
1088
1121
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1089
1122
|
if (currentRawReasoning) flushRawReasoning();
|
|
1090
1123
|
flushToolCall();
|
|
@@ -1112,25 +1145,33 @@ export function buildResponseJSON(
|
|
|
1112
1145
|
case "done":
|
|
1113
1146
|
usage = e.usage;
|
|
1114
1147
|
endTurn = e.endTurn;
|
|
1148
|
+
cleanDone = e.stopReason === undefined;
|
|
1115
1149
|
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
1116
|
-
|
|
1150
|
+
// Match streaming: max_tokens and content_filter both terminate as incomplete.
|
|
1151
|
+
if (e.stopReason === "max_tokens" || e.stopReason === "content_filter") stopReason = e.stopReason;
|
|
1117
1152
|
break;
|
|
1118
1153
|
}
|
|
1119
1154
|
}
|
|
1120
|
-
flushText();
|
|
1155
|
+
flushText(cleanDone && !errorEvent && !incompleteEvent ? "final_answer" : undefined);
|
|
1121
1156
|
flushSummaryReasoning();
|
|
1122
1157
|
flushRawReasoning();
|
|
1123
1158
|
flushToolCall();
|
|
1124
1159
|
// A truncated turn must never be installed as replacement history: emit the
|
|
1125
1160
|
// compaction item only when the turn actually completed (#422).
|
|
1126
|
-
if (
|
|
1161
|
+
if (
|
|
1162
|
+
options?.compaction
|
|
1163
|
+
&& !errorEvent
|
|
1164
|
+
&& !incompleteEvent
|
|
1165
|
+
&& stopReason !== "max_tokens"
|
|
1166
|
+
&& stopReason !== "content_filter"
|
|
1167
|
+
) {
|
|
1127
1168
|
output.push({ type: "compaction", id: `cmp_${uuid()}`, encrypted_content: encodeCompactionSummary(compactionText) });
|
|
1128
1169
|
}
|
|
1129
1170
|
|
|
1130
1171
|
const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;
|
|
1131
1172
|
const status = errorEvent
|
|
1132
1173
|
? "failed"
|
|
1133
|
-
: incompleteEvent || stopReason === "max_tokens"
|
|
1174
|
+
: incompleteEvent || stopReason === "max_tokens" || stopReason === "content_filter"
|
|
1134
1175
|
? "incomplete"
|
|
1135
1176
|
: "completed";
|
|
1136
1177
|
options?.onUsage?.(incompleteEvent?.usage ?? usage);
|
|
@@ -1150,6 +1191,8 @@ export function buildResponseJSON(
|
|
|
1150
1191
|
},
|
|
1151
1192
|
} : stopReason === "max_tokens" ? {
|
|
1152
1193
|
incomplete_details: { reason: "max_output_tokens" },
|
|
1194
|
+
} : stopReason === "content_filter" ? {
|
|
1195
|
+
incomplete_details: { reason: "content_filter" },
|
|
1153
1196
|
} : {}),
|
|
1154
1197
|
usage: responsesUsage(incompleteEvent?.usage ?? usage),
|
|
1155
1198
|
};
|
|
@@ -1159,7 +1202,7 @@ export function formatErrorResponse(
|
|
|
1159
1202
|
status: number,
|
|
1160
1203
|
type: string,
|
|
1161
1204
|
message: string,
|
|
1162
|
-
options?: { code?: string | null },
|
|
1205
|
+
options?: { code?: string | null; retryAfter?: string | null },
|
|
1163
1206
|
): Response {
|
|
1164
1207
|
const error = classifyError(status, type, message);
|
|
1165
1208
|
if (isCyberPolicyCode(options?.code)) {
|
|
@@ -1167,8 +1210,13 @@ export function formatErrorResponse(
|
|
|
1167
1210
|
error.type = "invalid_request_error";
|
|
1168
1211
|
}
|
|
1169
1212
|
const finalStatus = error.code === CYBER_POLICY_ERROR_CODE ? 400 : status;
|
|
1213
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1214
|
+
const retryAfter = options?.retryAfter?.trim();
|
|
1215
|
+
if (retryAfter && retryAfter.length > 0 && retryAfter.length <= 128) {
|
|
1216
|
+
headers.set("Retry-After", retryAfter);
|
|
1217
|
+
}
|
|
1170
1218
|
return new Response(JSON.stringify({ error }), {
|
|
1171
1219
|
status: finalStatus,
|
|
1172
|
-
headers
|
|
1220
|
+
headers,
|
|
1173
1221
|
});
|
|
1174
1222
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Claude Desktop's own configLibrary location, ported from the shipped app bundle.
|
|
6
|
+
*
|
|
7
|
+
* Desktop assembles the directory name at runtime, which is why searching the
|
|
8
|
+
* bundle for the literal "Claude-3p" finds almost nothing (app.asar, v1.18286.0):
|
|
9
|
+
*
|
|
10
|
+
* const Bu = "-3p", zW = "Claude", ND = `${zW}${Bu}`;
|
|
11
|
+
* function GE(){
|
|
12
|
+
* if (process.env.CLAUDE_USER_DATA_DIR) return app.getPath("userData");
|
|
13
|
+
* if (process.platform === "win32" && process.env.LOCALAPPDATA) return join(process.env.LOCALAPPDATA, ND);
|
|
14
|
+
* const t = app.getPath("userData");
|
|
15
|
+
* return t.endsWith(Bu) ? t : `${t}${Bu}`;
|
|
16
|
+
* }
|
|
17
|
+
* function mD(){ return join(GE(), "configLibrary") }
|
|
18
|
+
*
|
|
19
|
+
* So `Claude-3p` is Desktop's correct default, not a mistake to "fix" — but the
|
|
20
|
+
* first two branches were missing here, which is what GitHub #539 actually hit.
|
|
21
|
+
*
|
|
22
|
+
* Resolution is a pure function of (env, platform, home) so tests can exercise the
|
|
23
|
+
* win32 branch on any host: stubbing `process.platform` does NOT propagate to
|
|
24
|
+
* `os.platform()` under Bun, so a platform-sniffing implementation would be
|
|
25
|
+
* untestable in this repo.
|
|
26
|
+
*/
|
|
27
|
+
const SUFFIX = "-3p";
|
|
28
|
+
const APP_DIR = `Claude${SUFFIX}`;
|
|
29
|
+
|
|
30
|
+
function joinForPlatform(platform: NodeJS.Platform, ...parts: string[]): string {
|
|
31
|
+
return platform === "win32" ? win32.join(...parts) : posix.join(...parts);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DesktopPathInputs {
|
|
35
|
+
env: Record<string, string | undefined>;
|
|
36
|
+
platform: NodeJS.Platform;
|
|
37
|
+
home: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Electron `app.getPath("userData")`: per-platform appData + productName ("Claude"). */
|
|
41
|
+
export function resolveElectronUserData(inputs: DesktopPathInputs): string {
|
|
42
|
+
const { env, platform, home } = inputs;
|
|
43
|
+
if (platform === "win32") {
|
|
44
|
+
const appData = env.APPDATA?.trim();
|
|
45
|
+
return appData ? win32.join(appData, "Claude") : win32.join(home, "AppData", "Roaming", "Claude");
|
|
46
|
+
}
|
|
47
|
+
if (platform === "darwin") return posix.join(home, "Library", "Application Support", "Claude");
|
|
48
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
49
|
+
return xdg ? posix.join(xdg, "Claude") : posix.join(home, ".config", "Claude");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Desktop's userData root, mirroring `GE()` branch for branch. */
|
|
53
|
+
export function resolveUserDataDir(inputs: DesktopPathInputs): string {
|
|
54
|
+
const explicit = inputs.env.CLAUDE_USER_DATA_DIR?.trim();
|
|
55
|
+
// Desktop calls app.setPath("userData", CLAUDE_USER_DATA_DIR) at startup, so this
|
|
56
|
+
// path is used verbatim — notably WITHOUT the "-3p" suffix.
|
|
57
|
+
if (explicit) return explicit;
|
|
58
|
+
|
|
59
|
+
if (inputs.platform === "win32") {
|
|
60
|
+
const localAppData = inputs.env.LOCALAPPDATA?.trim();
|
|
61
|
+
if (localAppData) return win32.join(localAppData, APP_DIR);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const root = resolveElectronUserData(inputs);
|
|
65
|
+
// Concatenated, not joined: "Claude-3p" is one path component.
|
|
66
|
+
return root.endsWith(SUFFIX) ? root : `${root}${SUFFIX}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The directory opencodex writes its 3P profile into.
|
|
71
|
+
*
|
|
72
|
+
* OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR wins over every other branch and is returned
|
|
73
|
+
* verbatim (no `join`), because tests point it straight at a temp directory.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveConfigLibraryDir(inputs: DesktopPathInputs): string {
|
|
76
|
+
const override = inputs.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR?.trim();
|
|
77
|
+
if (override) return override;
|
|
78
|
+
return joinForPlatform(inputs.platform, resolveUserDataDir(inputs), "configLibrary");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Runtime wrapper. Keep it thin: all branching lives in the pure functions above. */
|
|
82
|
+
export function claudeDesktopConfigLibraryDir(): string {
|
|
83
|
+
return resolveConfigLibraryDir({ env: process.env, platform: process.platform, home: homedir() });
|
|
84
|
+
}
|
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { atomicWriteFile } from "../config";
|
|
6
6
|
import type { OcxClaudeDesktopProfile } from "../types";
|
|
7
|
+
import { claudeDesktopConfigLibraryDir, resolveConfigLibraryDir } from "./desktop-3p-paths";
|
|
7
8
|
import {
|
|
8
9
|
reconcileDesktopProfile,
|
|
9
10
|
renderDesktopProfile,
|
|
@@ -49,6 +50,33 @@ export interface Desktop3pRoutedModel {
|
|
|
49
50
|
*/
|
|
50
51
|
export const DESKTOP_SUPPORTS_1M_THRESHOLD = 1_000_000;
|
|
51
52
|
|
|
53
|
+
export interface Desktop3pConfigLibraryOptions {
|
|
54
|
+
env?: NodeJS.ProcessEnv;
|
|
55
|
+
platform?: NodeJS.Platform;
|
|
56
|
+
homeDir?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the config library from the same user-data root Claude Desktop uses. Keeping this in one
|
|
61
|
+
* helper prevents the writer and dashboard status probe from agreeing on a path Desktop never reads.
|
|
62
|
+
*
|
|
63
|
+
* The resolution itself lives in `./desktop-3p-paths`, which ports Claude Desktop's own `GE()`
|
|
64
|
+
* branch for branch — including the `-3p` suffix the app appends to its userData root. Dropping
|
|
65
|
+
* that suffix points us at a directory Desktop never reads (GitHub #539).
|
|
66
|
+
*/
|
|
67
|
+
export function resolveDesktop3pConfigLibraryPath(
|
|
68
|
+
options: Desktop3pConfigLibraryOptions = {},
|
|
69
|
+
): string {
|
|
70
|
+
if (options.env === undefined && options.platform === undefined && options.homeDir === undefined) {
|
|
71
|
+
return claudeDesktopConfigLibraryDir();
|
|
72
|
+
}
|
|
73
|
+
return resolveConfigLibraryDir({
|
|
74
|
+
env: options.env ?? process.env,
|
|
75
|
+
platform: options.platform ?? process.platform,
|
|
76
|
+
home: options.homeDir ?? homedir(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
/** CLI arg parsing for `ocx claude desktop` mode flags (mutually exclusive). */
|
|
53
81
|
export function parseDesktop3pModeArgs(flags: string[]): { mode: Desktop3pConfigMode } | { error: string } {
|
|
54
82
|
const known = new Map<string, Desktop3pConfigMode>([
|
|
@@ -308,8 +336,7 @@ export function writeDesktop3pConfig(
|
|
|
308
336
|
mode: Desktop3pConfigMode = "static",
|
|
309
337
|
profile?: OcxClaudeDesktopProfile,
|
|
310
338
|
): { written: boolean; path: string; reason?: string; fingerprint?: string } {
|
|
311
|
-
const libraryPath =
|
|
312
|
-
|| join(homedir(), "Library", "Application Support", "Claude-3p", "configLibrary");
|
|
339
|
+
const libraryPath = resolveDesktop3pConfigLibraryPath();
|
|
313
340
|
const metadataPath = join(libraryPath, "_meta.json");
|
|
314
341
|
let configPath = libraryPath;
|
|
315
342
|
|