@bitkyc08/opencodex 2.57.0 → 2.58.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/{index-Cz7CLdif.js → index-BbrHOIY0.js} +2 -2
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-responses/passthrough.ts +30 -4
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/claude/outbound.ts +14 -4
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +44 -2
- package/src/cli/system-command.ts +70 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/codex/auth-context.ts +42 -8
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +3 -7
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/config/atomic-write.ts +83 -8
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +132 -68
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/providers/derive.ts +6 -0
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +50 -28
- package/src/providers/registry/model-seeds.ts +67 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +1 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +11 -0
- package/src/server/responses/passthrough-dispatch.ts +33 -1
- package/src/server/responses/request-prepare.ts +41 -0
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +13 -0
|
@@ -306,17 +306,37 @@ function buildNativeMessages(
|
|
|
306
306
|
// owned by this adapter/request lifecycle rather than process-global state.
|
|
307
307
|
reservedToolCallIds.clear();
|
|
308
308
|
let pending: PendingToolBatch | undefined;
|
|
309
|
+
// Codex records mid-turn injections (a PostToolUse hook verdict, a context notice) between an
|
|
310
|
+
// assistant tool call and that call's own tool result. Native Ollama needs the call and its
|
|
311
|
+
// results adjacent, so those conversational messages wait here instead of closing the batch
|
|
312
|
+
// early. The openai-chat adapter defers them the same way; refusing the replay killed the turn.
|
|
313
|
+
let deferred: OllamaNativeMessage[] = [];
|
|
314
|
+
|
|
315
|
+
const releaseDeferred = (): void => {
|
|
316
|
+
if (deferred.length === 0) return;
|
|
317
|
+
messages.push(...deferred);
|
|
318
|
+
deferred = [];
|
|
319
|
+
};
|
|
309
320
|
|
|
310
321
|
const flushPending = (): void => {
|
|
311
322
|
if (!pending) return;
|
|
312
323
|
for (const call of pending.calls) {
|
|
313
324
|
if (!call.result) {
|
|
314
|
-
|
|
325
|
+
// No result exists anywhere in the replayed history: the turn was interrupted, or the
|
|
326
|
+
// result never reached it. State exactly that instead of inventing an outcome, and keep
|
|
327
|
+
// the conversation replayable.
|
|
328
|
+
messages.push({
|
|
329
|
+
role: "tool",
|
|
330
|
+
tool_call_id: call.id,
|
|
331
|
+
tool_name: call.wireName,
|
|
332
|
+
// Same marker text as the chat adapter (openai-chat/messages.ts), so both adapters read
|
|
333
|
+
// the same in an operator's log. The name is this wire's flattened tool name, which is
|
|
334
|
+
// what the assistant turn above it carries.
|
|
335
|
+
content: `[ocx] no tool result was recorded for "${call.wireName}"; execution status unknown — do not treat this as success, failure, or user-provided input.`,
|
|
336
|
+
});
|
|
337
|
+
continue;
|
|
315
338
|
}
|
|
316
|
-
|
|
317
|
-
for (const call of pending.calls) {
|
|
318
|
-
const result = call.result!;
|
|
319
|
-
const translated = contentToNative(result.content, "tool result");
|
|
339
|
+
const translated = contentToNative(call.result.content, "tool result");
|
|
320
340
|
messages.push({
|
|
321
341
|
role: "tool",
|
|
322
342
|
tool_call_id: call.id,
|
|
@@ -326,6 +346,7 @@ function buildNativeMessages(
|
|
|
326
346
|
});
|
|
327
347
|
}
|
|
328
348
|
pending = undefined;
|
|
349
|
+
releaseDeferred();
|
|
329
350
|
};
|
|
330
351
|
|
|
331
352
|
for (const message of parsed.context.messages) {
|
|
@@ -347,9 +368,22 @@ function buildNativeMessages(
|
|
|
347
368
|
continue;
|
|
348
369
|
}
|
|
349
370
|
|
|
350
|
-
// Native Ollama requires the whole assistant tool-call turn followed by its tool results.
|
|
351
|
-
//
|
|
352
|
-
|
|
371
|
+
// Native Ollama requires the whole assistant tool-call turn followed by its tool results. A
|
|
372
|
+
// conversational message that arrives while the batch is still open is held aside instead of
|
|
373
|
+
// closing it, so the call keeps its results adjacent; it is released right after the batch
|
|
374
|
+
// flushes. Anything else (a new assistant turn) settles the batch first.
|
|
375
|
+
if (pending) {
|
|
376
|
+
if (message.role === "user" || message.role === "developer") {
|
|
377
|
+
const translated = message.role === "user"
|
|
378
|
+
? contentToNative(message.content, "user")
|
|
379
|
+
: contentToNative(message.content, "developer", false);
|
|
380
|
+
deferred.push(message.role === "user"
|
|
381
|
+
? { role: "user", content: translated.content, ...(translated.images ? { images: translated.images } : {}) }
|
|
382
|
+
: { role: "system", content: translated.content });
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
flushPending();
|
|
386
|
+
}
|
|
353
387
|
|
|
354
388
|
switch (message.role) {
|
|
355
389
|
case "user": {
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
createAdapterTierMetadata,
|
|
33
33
|
} from "../../providers/fastwire";
|
|
34
34
|
import { mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
|
|
35
|
-
import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
|
|
35
|
+
import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
|
|
36
36
|
import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache";
|
|
37
37
|
import { isPlainObject } from "./internal";
|
|
38
38
|
import { normalizeToolSchemas, promoteClientLoadedTools, stripUnsupportedHostedTools } from "./tool-schema";
|
|
@@ -270,19 +270,31 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
270
270
|
// tier write so a force-fast/default decision can never mutate parsed._rawBody.
|
|
271
271
|
outBody = applyTierDecisionToResponsesBody(outBody, parsed.options?.tierDecision);
|
|
272
272
|
const stateless = provider.statelessResponses === true;
|
|
273
|
+
const adjacentToolResults = provider.requiresAdjacentResponsesToolResults === true;
|
|
274
|
+
// Adjacency reorders items the upstream would accept in some order. Pairing synthesizes an
|
|
275
|
+
// item the client never sent, which is a larger claim about the conversation, so it is its
|
|
276
|
+
// own capability: Kimi carries the adjacency flag but accepts a dangling call (#4726) and
|
|
277
|
+
// must not start receiving placeholders it never needed.
|
|
278
|
+
const pairedToolResults = provider.requiresPairedResponsesToolResults === true;
|
|
273
279
|
if (stateless) outBody = stripStatefulResponsesParams(outBody);
|
|
274
280
|
// A replay miss can leave a function_call_output whose paired function_call sat
|
|
275
281
|
// in the prefix that was never expanded. A stateless upstream cannot resolve the
|
|
276
282
|
// pair from its own storage either, so it needs the same repair the forward
|
|
277
283
|
// backend gets — dropping previous_response_id is not much use if the body that
|
|
278
284
|
// reaches the wire is unparseable.
|
|
285
|
+
// A parser can also 400 on a function_call with no matching output at all. DeepSeek gets
|
|
286
|
+
// that repair through statelessResponses. xAI cannot be marked stateless: its Responses API
|
|
287
|
+
// stores conversations for 30 days and documents previous_response_id. So it carries the
|
|
288
|
+
// pairing capability instead, which reuses the orphan-call placeholder without touching
|
|
289
|
+
// store or previous_response_id.
|
|
279
290
|
if (provider.annotateEmptyToolOutputs === true) {
|
|
280
291
|
outBody = annotateEmptyResponsesToolOutputs(outBody, true);
|
|
281
292
|
}
|
|
282
|
-
|
|
283
|
-
|
|
293
|
+
const synthesizeMissingCallOutputs = !forward && (stateless || pairedToolResults);
|
|
294
|
+
if (forward || stateless || pairedToolResults) {
|
|
295
|
+
outBody = repairOrphanedInputItems(outBody, unexpandedMiss, synthesizeMissingCallOutputs);
|
|
284
296
|
}
|
|
285
|
-
if (
|
|
297
|
+
if (adjacentToolResults) {
|
|
286
298
|
outBody = normalizeResponsesToolResultAdjacency(outBody);
|
|
287
299
|
}
|
|
288
300
|
if (forward) {
|
|
@@ -316,6 +328,20 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
316
328
|
outBody = backfillWebSearchQueries(outBody);
|
|
317
329
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
318
330
|
outBody = stripInternalChatMessageMetadataPassthrough(outBody);
|
|
331
|
+
// The same class of private field, one level up, but keyed on the DESTINATION rather than
|
|
332
|
+
// on the canonical surface alone. `src/server/responses/compact.ts` spreads the caller's
|
|
333
|
+
// raw body into the native `/responses/compact` request without passing through this
|
|
334
|
+
// adapter, and that endpoint is offered only to OpenAI-operated destinations
|
|
335
|
+
// (supportsNativeResponsesCompactEndpoint). Stripping on the canonical predicate here
|
|
336
|
+
// would make the two paths disagree for `openai-apikey`; stripping on the destination
|
|
337
|
+
// keeps every OpenAI-operated route byte-identical and removes the field exactly where it
|
|
338
|
+
// is known to break, which is a gateway this proxy does not operate.
|
|
339
|
+
//
|
|
340
|
+
// Placed before the routed compaction body is built and before serialization, so the HTTP,
|
|
341
|
+
// routed-compaction and WebSocket outbounds are all covered by this one call.
|
|
342
|
+
if (!isOpenAiOperatedResponsesDestination(provider)) {
|
|
343
|
+
outBody = stripCanonicalOnlyTopLevelFields(outBody);
|
|
344
|
+
}
|
|
319
345
|
outBody = promoteClientLoadedTools(outBody);
|
|
320
346
|
}
|
|
321
347
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
@@ -120,6 +120,49 @@ export function stripInternalChatMessageMetadataPassthrough(body: unknown): unkn
|
|
|
120
120
|
return changed ? { ...body, input } : body;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* OpenAI-private TOP-LEVEL request keys, the sibling of `CANONICAL_ONLY_TOOL_FIELDS` one level up.
|
|
125
|
+
*
|
|
126
|
+
* Codex attaches these on the request body itself rather than on a tool or an input item, and gates
|
|
127
|
+
* them on its own auth rather than on the destination URL. Loopback injection keeps Codex pointed at
|
|
128
|
+
* its built-in `openai` provider, so the client still believes it is addressing the canonical
|
|
129
|
+
* ChatGPT backend and keeps the key no matter where this proxy routes the turn. A Responses gateway
|
|
130
|
+
* that validates its top-level schema then rejects the whole request before inference.
|
|
131
|
+
*
|
|
132
|
+
* Keep this a table, and keep it to keys a client is OBSERVED to send. It is not an unknown-field
|
|
133
|
+
* sanitizer: a top-level key nobody has traced to a client is forwarded untouched, because deleting
|
|
134
|
+
* it would silently drop a parameter some other caller means.
|
|
135
|
+
*/
|
|
136
|
+
const CANONICAL_ONLY_TOP_LEVEL_FIELDS: readonly string[] = [
|
|
137
|
+
// Cyber access program selector, new in Codex 0.155. codex-rs mints it from
|
|
138
|
+
// `cyber_access_program::for_auth`, which filters on ChatGPT auth alone and never on the
|
|
139
|
+
// destination base URL, and serializes it on the Responses request, the compaction input and the
|
|
140
|
+
// WebSocket `response.create` envelope. No public specification defines it, so a strict
|
|
141
|
+
// third-party gateway answers with an unknown-parameter error naming it, and every turn of that
|
|
142
|
+
// thread fails (#4853).
|
|
143
|
+
//
|
|
144
|
+
// `codex_output_schema` is deliberately NOT here. In codex-rs it is the `name` of the JSON-schema
|
|
145
|
+
// `text.format` object, not a top-level key, so listing it would delete a field this client never
|
|
146
|
+
// sends and discard it for any client that does send it meaningfully.
|
|
147
|
+
"access_programs",
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Remove the OpenAI-private top-level keys.
|
|
152
|
+
*
|
|
153
|
+
* The caller decides the boundary; see the call site in `passthrough.ts`, which applies this only
|
|
154
|
+
* to a destination OpenCodex does not operate. Returns the input unchanged when no listed key is
|
|
155
|
+
* present, so the common path allocates nothing and the caller-owned raw body is never mutated.
|
|
156
|
+
*/
|
|
157
|
+
export function stripCanonicalOnlyTopLevelFields(body: unknown): unknown {
|
|
158
|
+
if (!isPlainObject(body)) return body;
|
|
159
|
+
if (!CANONICAL_ONLY_TOP_LEVEL_FIELDS.some(field => Object.hasOwn(body, field))) return body;
|
|
160
|
+
|
|
161
|
+
const next = { ...body };
|
|
162
|
+
for (const field of CANONICAL_ONLY_TOP_LEVEL_FIELDS) delete next[field];
|
|
163
|
+
return next;
|
|
164
|
+
}
|
|
165
|
+
|
|
123
166
|
/**
|
|
124
167
|
* When `store` is false, the upstream API does not persist response items. Any item ID
|
|
125
168
|
* forwarded in `input` is then interpreted as a reference to a stored item that does not
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AdapterFetchContext } from "./base";
|
|
2
|
+
import type { SendClass } from "../lib/request-execution-budget";
|
|
3
|
+
import type { AttemptRecoveryKind } from "../usage/log";
|
|
4
|
+
import { abortError, SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
5
|
+
|
|
6
|
+
type PacedFetch = typeof globalThis.fetch & {
|
|
7
|
+
waitForPacing?: (signal?: AbortSignal) => Promise<void>;
|
|
8
|
+
unpacedFetch?: typeof globalThis.fetch;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** One ordinal sequence per adapter fetchResponse call, across all of its inference retries.
|
|
12
|
+
* Consumption starts at underlying executor invocation; its own later preflight may still fail. */
|
|
13
|
+
export function createAdapterPhysicalSend(ctx: AdapterFetchContext = {}, fallback = globalThis.fetch) {
|
|
14
|
+
const executor = (ctx.executor ?? fallback) as PacedFetch;
|
|
15
|
+
let ordinal = 0;
|
|
16
|
+
return async (options: {
|
|
17
|
+
url: string;
|
|
18
|
+
sendClass?: SendClass;
|
|
19
|
+
recovery?: AttemptRecoveryKind;
|
|
20
|
+
/** Runs only after admission, e.g. backoff and cancellation of a superseded response. */
|
|
21
|
+
beforeDispatch?: () => void | Promise<void>;
|
|
22
|
+
dispatch: (executor: typeof globalThis.fetch) => Promise<Response>;
|
|
23
|
+
}): Promise<Response> => {
|
|
24
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
25
|
+
const decision = ctx.sendBudget?.reserveDispatch({
|
|
26
|
+
sendClass: options.sendClass ?? "transient", targetKey: options.url,
|
|
27
|
+
});
|
|
28
|
+
if (decision && !decision.allowed) throw new SendBudgetExhaustedError(options.url);
|
|
29
|
+
const permit = decision?.allowed ? decision.permit : undefined;
|
|
30
|
+
let dispatched = false;
|
|
31
|
+
const physicalExecutor = (async (input, init) => {
|
|
32
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
33
|
+
if (init?.signal?.aborted) throw abortError(init.signal);
|
|
34
|
+
if (dispatched || (permit && !permit.use())) throw new SendBudgetExhaustedError(options.url);
|
|
35
|
+
dispatched = true;
|
|
36
|
+
ordinal += 1;
|
|
37
|
+
ctx.onPhysicalSend?.({ ordinal, ...(options.recovery ? { recovery: options.recovery } : {}) });
|
|
38
|
+
return (executor.unpacedFetch ?? executor)(input, init);
|
|
39
|
+
}) as typeof globalThis.fetch;
|
|
40
|
+
try {
|
|
41
|
+
await executor.waitForPacing?.(ctx.abortSignal);
|
|
42
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
43
|
+
await options.beforeDispatch?.();
|
|
44
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
45
|
+
return await options.dispatch(physicalExecutor);
|
|
46
|
+
} finally {
|
|
47
|
+
permit?.release();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -527,7 +527,7 @@ function buildResponseJSONWithBudget(
|
|
|
527
527
|
compactionEncryptedContent = e.compactionEncryptedContent;
|
|
528
528
|
sawTerminal = true;
|
|
529
529
|
endTurn = e.endTurn;
|
|
530
|
-
cleanDone = e.stopReason
|
|
530
|
+
cleanDone = !isTruncatedStopReason(e.stopReason);
|
|
531
531
|
rawStopReason = e.stopReason;
|
|
532
532
|
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
533
533
|
// Match streaming: max_tokens and content_filter both terminate as incomplete.
|
package/src/bridge/sse.ts
CHANGED
|
@@ -1171,7 +1171,7 @@ export function bridgeToResponsesSSE(
|
|
|
1171
1171
|
break;
|
|
1172
1172
|
}
|
|
1173
1173
|
case "done": {
|
|
1174
|
-
if (currentMsg) closeCurrentMessage(event.stopReason ? undefined : "final_answer");
|
|
1174
|
+
if (currentMsg) closeCurrentMessage(isTruncatedStopReason(event.stopReason) ? undefined : "final_answer");
|
|
1175
1175
|
if (currentReasoning) closeCurrentReasoning();
|
|
1176
1176
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
1177
1177
|
flushHiddenRawReasoning();
|
package/src/claude/outbound.ts
CHANGED
|
@@ -189,7 +189,7 @@ function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultConte
|
|
|
189
189
|
return { id, input, resultContent, completed };
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
function messageSnapshot(model: string): Rec {
|
|
192
|
+
function messageSnapshot(model: string, confirmedUsage?: Rec): Rec {
|
|
193
193
|
return {
|
|
194
194
|
id: `msg_${uuid()}`,
|
|
195
195
|
type: "message",
|
|
@@ -198,7 +198,7 @@ function messageSnapshot(model: string): Rec {
|
|
|
198
198
|
model,
|
|
199
199
|
stop_reason: null,
|
|
200
200
|
stop_sequence: null,
|
|
201
|
-
usage: { input_tokens: 0, output_tokens: 0 },
|
|
201
|
+
usage: confirmedUsage ?? { input_tokens: 0, output_tokens: 0 },
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -246,6 +246,7 @@ export function responsesSseToAnthropicSse(
|
|
|
246
246
|
let open: OpenBlock | null = null;
|
|
247
247
|
let sawToolUse = false;
|
|
248
248
|
let webSearchRequests = 0;
|
|
249
|
+
let earlyAnthropicUsage: Rec | undefined;
|
|
249
250
|
let pingTimer: ReturnType<typeof setInterval> | undefined;
|
|
250
251
|
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
251
252
|
const utf8SliceBytes = (value: string, start: number, end: number): number => {
|
|
@@ -282,7 +283,7 @@ export function responsesSseToAnthropicSse(
|
|
|
282
283
|
const ensureStarted = () => {
|
|
283
284
|
if (started) return;
|
|
284
285
|
started = true;
|
|
285
|
-
emit("message_start", { type: "message_start", message: messageSnapshot(model) });
|
|
286
|
+
emit("message_start", { type: "message_start", message: messageSnapshot(model, earlyAnthropicUsage) });
|
|
286
287
|
emit("ping", { type: "ping" });
|
|
287
288
|
};
|
|
288
289
|
// Keepalive pings protect remote deployments behind LB/NAT idle timeouts even
|
|
@@ -405,8 +406,17 @@ export function responsesSseToAnthropicSse(
|
|
|
405
406
|
const handleFrame = (eventName: string, data: Rec) => {
|
|
406
407
|
switch (eventName) {
|
|
407
408
|
case "response.created":
|
|
408
|
-
|
|
409
|
+
case "response.in_progress": {
|
|
410
|
+
// Lifecycle preludes do not start Anthropic framing, but some upstreams attach
|
|
411
|
+
// confirmed input usage before semantic output. Retain only its bounded Anthropic
|
|
412
|
+
// projection so message_start can report measurements that already arrived.
|
|
413
|
+
const response = isRec(data.response) ? data.response : {};
|
|
414
|
+
const usage = isRec(response.usage) ? response.usage : undefined;
|
|
415
|
+
if (!started && usage && typeof usage.input_tokens === "number") {
|
|
416
|
+
earlyAnthropicUsage = anthropicUsage(usage);
|
|
417
|
+
}
|
|
409
418
|
break;
|
|
419
|
+
}
|
|
410
420
|
case "response.heartbeat":
|
|
411
421
|
if ((controller.desiredSize ?? 0) > 0) emit("ping", { type: "ping" });
|
|
412
422
|
break;
|
|
@@ -4,7 +4,7 @@ import { getConfigPath, mutatePersistedConfig, readConfigDiagnostics, sanitizeMo
|
|
|
4
4
|
import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../reasoning-effort";
|
|
5
5
|
import type { OcxConfig } from "../types";
|
|
6
6
|
import { normalizeVisionReasoningForModel } from "../vision/reasoning";
|
|
7
|
-
import type {
|
|
7
|
+
import type { ServiceApiTokenState } from "../lib/service-secrets";
|
|
8
8
|
import { CliUsageError, printData, rejectArgs, runCliAction, takeFlag } from "./runtime-api";
|
|
9
9
|
|
|
10
10
|
const USAGE = `Usage:
|
|
@@ -38,9 +38,8 @@ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
|
38
38
|
* `client` block, which is the same defect in miniature: the presence of configuration is not
|
|
39
39
|
* evidence that the connection works, and a machine whose data-plane token was revoked, rotated
|
|
40
40
|
* away or deleted would have been labelled `connected: true` while it could not reach the hub at
|
|
41
|
-
* all.
|
|
42
|
-
*
|
|
43
|
-
* pure and testable.
|
|
41
|
+
* all. The read-only projection compares the bounded token file's fingerprint against the
|
|
42
|
+
* connection record, and passes that answer in so this formatter stays pure and testable.
|
|
44
43
|
*
|
|
45
44
|
* Synthetic and NOT persisted, for two reasons. `clientConnectionSchema` is `.strict()`, so a
|
|
46
45
|
* `client.note` field would not validate; and persisted prose drifts from the behaviour it
|
|
@@ -49,7 +48,7 @@ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
|
49
48
|
*/
|
|
50
49
|
export function remoteHubConfigNote(
|
|
51
50
|
config: OcxConfig,
|
|
52
|
-
readConnection: () =>
|
|
51
|
+
readConnection: () => RemoteHubConnectionObservation,
|
|
53
52
|
): { connected: boolean; origin: string; note: string } | null {
|
|
54
53
|
if (config.runtimeRole !== "client" || !config.client) return null;
|
|
55
54
|
// A thunk, so a standalone or hub install pays nothing: the guard above returns first and the
|
|
@@ -67,6 +66,36 @@ export function remoteHubConfigNote(
|
|
|
67
66
|
return { connected, origin: config.client.serverUrl, note };
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
export type RemoteHubConnectionObservation = {
|
|
70
|
+
state: "disconnected" | "connected" | "invalid" | "mismatched";
|
|
71
|
+
reason?: string;
|
|
72
|
+
token: "owned" | "missing" | "changed" | "unsafe";
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function remoteHubConnectionFromTokenState(
|
|
76
|
+
config: Pick<OcxConfig, "client">,
|
|
77
|
+
tokenState: ServiceApiTokenState,
|
|
78
|
+
): RemoteHubConnectionObservation {
|
|
79
|
+
const token = tokenState.kind === "absent"
|
|
80
|
+
? "missing"
|
|
81
|
+
: tokenState.kind === "unsafe"
|
|
82
|
+
? "unsafe"
|
|
83
|
+
: tokenState.fingerprint === config.client?.tokenFingerprint ? "owned" : "changed";
|
|
84
|
+
return { state: "connected", token };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readRemoteHubConfigNote(config: OcxConfig): Promise<ReturnType<typeof remoteHubConfigNote>> {
|
|
88
|
+
if (config.runtimeRole !== "client" || !config.client) return null;
|
|
89
|
+
// This display command needs only connection ownership, not lifecycle recovery, catalog
|
|
90
|
+
// readiness, or any write-capable connect machinery. Keep the read on the bounded token
|
|
91
|
+
// observer so a cold `config show` never imports the full connect command graph.
|
|
92
|
+
const { readServiceApiTokenState } = await import("../lib/service-secrets");
|
|
93
|
+
return remoteHubConfigNote(
|
|
94
|
+
config,
|
|
95
|
+
() => remoteHubConnectionFromTokenState(config, readServiceApiTokenState()),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
function redact(value: unknown, key = ""): unknown {
|
|
71
100
|
if (SECRET_KEYS.test(key) && typeof value === "string") return value ? "********" : value;
|
|
72
101
|
// `client.priorCatalog` is the base64 catalog snapshot connect took before overwriting the
|
|
@@ -168,19 +197,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
|
168
197
|
rejectArgs(args, USAGE);
|
|
169
198
|
const diagnostics = readConfigDiagnostics();
|
|
170
199
|
const redacted = redact(diagnostics.config);
|
|
171
|
-
|
|
172
|
-
// in, and `ocx config get/set` has no use for it.
|
|
173
|
-
const { collectClientConnectionStatus } = await import("./connect");
|
|
174
|
-
// The readiness probe is declined explicitly. `collectClientConnectionStatus` observes the
|
|
175
|
-
// local Codex ladder for a connected client, and observing it spawns `codex debug models`
|
|
176
|
-
// under a 45s budget. `ocx config show` reads only `state`, `reason` and `token` from the
|
|
177
|
-
// result, so paying for a subprocess here would buy nothing and would quietly turn a
|
|
178
|
-
// read-only config dump into a runtime probe. Returning no ladder resolves readiness to
|
|
179
|
-
// `unverified`, which is the honest answer for a caller that never asked.
|
|
180
|
-
const note = remoteHubConfigNote(
|
|
181
|
-
diagnostics.config,
|
|
182
|
-
() => collectClientConnectionStatus(undefined, undefined, { supportedEfforts: () => null }),
|
|
183
|
-
);
|
|
200
|
+
const note = await readRemoteHubConfigNote(diagnostics.config);
|
|
184
201
|
// First key, not last: it has to be read before the empty `providers` map that misled a
|
|
185
202
|
// reader into concluding nothing was configured anywhere.
|
|
186
203
|
const config = note && redacted && typeof redacted === "object" && !Array.isArray(redacted)
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "../codex/desired-state";
|
|
25
25
|
import { syncModelsToCodex } from "../codex/sync";
|
|
26
26
|
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
27
|
-
import { restoreNativeCodexAsync } from "../codex/inject";
|
|
27
|
+
import { restoreNativeCodexAsync, type CodexNativeRestoreResult } from "../codex/inject";
|
|
28
28
|
import { stripGrokConfig } from "../grok/inject";
|
|
29
29
|
import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } from "./restart-scope";
|
|
30
30
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
@@ -106,6 +106,7 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
106
106
|
restore: async deps => {
|
|
107
107
|
const restoreArgs = deps.args.slice(1);
|
|
108
108
|
const restoreJson = takeFlag(restoreArgs, "--json");
|
|
109
|
+
const removeProviderTable = takeFlag(restoreArgs, "--remove-codex-provider-table");
|
|
109
110
|
if (restoreArgs[0] === "back") {
|
|
110
111
|
// Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
|
|
111
112
|
// lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
|
|
@@ -146,6 +147,9 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
146
147
|
const target = collectOrcaCodexHomeDiagnostic();
|
|
147
148
|
return emitBack(true, `Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`, 0);
|
|
148
149
|
}
|
|
150
|
+
if (removeProviderTable && !restoreJson) {
|
|
151
|
+
console.log("⚠️ Removing [model_providers.opencodex] means conversations already tagged opencodex will stop opening.");
|
|
152
|
+
}
|
|
149
153
|
const desired = setIntegrationEnabled("codex", false);
|
|
150
154
|
if (!desired.ok) {
|
|
151
155
|
if (restoreJson) {
|
|
@@ -191,9 +195,9 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
191
195
|
return grokCode;
|
|
192
196
|
}
|
|
193
197
|
}
|
|
194
|
-
let r:
|
|
198
|
+
let r: CodexNativeRestoreResult | Pick<CodexNativeRestoreResult, "success" | "message">;
|
|
195
199
|
try {
|
|
196
|
-
r = await restoreNativeCodexAsync({ revalidateDesiredState: true });
|
|
200
|
+
r = await restoreNativeCodexAsync({ revalidateDesiredState: true, removeProviderTable });
|
|
197
201
|
} catch (err) {
|
|
198
202
|
r = { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
199
203
|
}
|
|
@@ -232,7 +236,16 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
232
236
|
code = 1;
|
|
233
237
|
}
|
|
234
238
|
if (r.success) {
|
|
235
|
-
|
|
239
|
+
const retained = "retainedCodexProviderTable" in r ? r.retainedCodexProviderTable : undefined;
|
|
240
|
+
if (retained) {
|
|
241
|
+
console.log("Codex integration is OFF and plain `codex` now runs natively.");
|
|
242
|
+
console.log("The following lines remain in $CODEX_HOME/config.toml because conversations already tagged opencodex resolve their provider only through this table:");
|
|
243
|
+
console.log(retained.lines.join("\n"));
|
|
244
|
+
console.log(`Follow-up: ${retained.followUp}`);
|
|
245
|
+
console.log("Switch back with: ocx restore back");
|
|
246
|
+
} else {
|
|
247
|
+
console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: ocx restore back");
|
|
248
|
+
}
|
|
236
249
|
console.log(`Note: ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`);
|
|
237
250
|
} else {
|
|
238
251
|
console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
|
package/src/cli/index.ts
CHANGED
|
@@ -13,7 +13,20 @@ try {
|
|
|
13
13
|
/* best-effort */
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
currentExternalCodexModelProvider,
|
|
18
|
+
restoreNativeCodex,
|
|
19
|
+
restoreNativeCodexAsync,
|
|
20
|
+
shouldInjectApiAuthHeader,
|
|
21
|
+
} from "../codex/inject";
|
|
22
|
+
// Straight from the owning modules rather than the facade: these are teardown-reporting
|
|
23
|
+
// helpers, not part of the injection surface, and `inject.ts` sits under a size cap that
|
|
24
|
+
// exists to stop it collecting exactly this kind of passthrough.
|
|
25
|
+
import { readOcxProviderTableBlock } from "../codex/inject/remove";
|
|
26
|
+
import {
|
|
27
|
+
describeRetainedCodexProviderTable,
|
|
28
|
+
type RetainedCodexProviderTable,
|
|
29
|
+
} from "../codex/inject/restore";
|
|
17
30
|
import { stripGrokConfig } from "../grok/inject";
|
|
18
31
|
import { STOP_HISTORY_DEFERRED_EXIT_CODE, STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../update/stop-contract.mjs";
|
|
19
32
|
import {
|
|
@@ -119,6 +132,12 @@ function reportShellHookFailure(result: { state: "installed" | "absent" | "faile
|
|
|
119
132
|
console.warn(" Check ~/.zshrc for the '# opencodex claude-env hook' block.");
|
|
120
133
|
}
|
|
121
134
|
|
|
135
|
+
function reportRetainedCodexProviderTable(retained: RetainedCodexProviderTable): void {
|
|
136
|
+
console.log(` ${describeRetainedCodexProviderTable(retained)}`);
|
|
137
|
+
console.log(" Retained config lines:");
|
|
138
|
+
for (const line of retained.lines) console.log(` ${line}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
122
141
|
async function refreshOwnedRaycastCatalog(
|
|
123
142
|
config: ReturnType<typeof loadConfig>,
|
|
124
143
|
port: number,
|
|
@@ -799,7 +818,12 @@ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boole
|
|
|
799
818
|
let other = false;
|
|
800
819
|
try {
|
|
801
820
|
const result = await restoreNativeCodexAsync();
|
|
802
|
-
if (result.success)
|
|
821
|
+
if (result.success) {
|
|
822
|
+
console.log(`↩️ ${result.message}`);
|
|
823
|
+
if (result.retainedCodexProviderTable) {
|
|
824
|
+
reportRetainedCodexProviderTable(result.retainedCodexProviderTable);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
803
827
|
else {
|
|
804
828
|
// Codex history is the one restore whose failure leaves the runtime consistent: the
|
|
805
829
|
// manifest is retained and the routed metadata is untouched. Config and catalog are
|
|
@@ -810,6 +834,9 @@ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boole
|
|
|
810
834
|
// attempted. Reading the states alone cannot tell that apart from an ownership
|
|
811
835
|
// refusal, so the structured reason carries it and the states are still required to
|
|
812
836
|
// agree — a refusal that somehow reports a failed artifact is not this case.
|
|
837
|
+
// A degraded restore has no refusal reason and reports config as partial, so it cannot
|
|
838
|
+
// enter this branch: its config obligation was discharged and the stop receipt must be
|
|
839
|
+
// released rather than preserved.
|
|
813
840
|
const preflightRefused = result.historyPreflightRefusal !== undefined
|
|
814
841
|
&& artifacts.config.state === "skipped"
|
|
815
842
|
&& artifacts.catalog.state === "skipped"
|
|
@@ -1357,6 +1384,9 @@ async function handleUninstall() {
|
|
|
1357
1384
|
await runStep("native Codex restored", async () => {
|
|
1358
1385
|
const r = await restoreNativeCodexAsync();
|
|
1359
1386
|
if (!r.success) throw new Error(r.message);
|
|
1387
|
+
if (r.retainedCodexProviderTable) {
|
|
1388
|
+
reportRetainedCodexProviderTable(r.retainedCodexProviderTable);
|
|
1389
|
+
}
|
|
1360
1390
|
});
|
|
1361
1391
|
|
|
1362
1392
|
await runStep("Grok Build config restored", () => {
|
|
@@ -1503,6 +1533,18 @@ async function handleStatus() {
|
|
|
1503
1533
|
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}${local}`);
|
|
1504
1534
|
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}${local}`);
|
|
1505
1535
|
console.log(` ${formatStartupRoutingDetail(status.json.startup)}${local}`);
|
|
1536
|
+
if (status.json.startup.routingKind === "native") {
|
|
1537
|
+
let retainedProviderTable = false;
|
|
1538
|
+
try {
|
|
1539
|
+
retainedProviderTable = readOcxProviderTableBlock() !== null;
|
|
1540
|
+
} catch {
|
|
1541
|
+
// The routing snapshot owns unreadable-config reporting. A later read race must not
|
|
1542
|
+
// turn this diagnostic command into a teardown failure.
|
|
1543
|
+
}
|
|
1544
|
+
if (retainedProviderTable) {
|
|
1545
|
+
console.log(` ⚠️ Codex provider table retained${local}: [model_providers.opencodex] remains while root routing is native. Remove with 'ocx restore --remove-codex-provider-table'; tagged conversations will stop opening.`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1506
1548
|
console.log(` Service: ${status.json.service.summary}${local}`);
|
|
1507
1549
|
console.log(` ${status.json.codexShim.summary}${local}`);
|
|
1508
1550
|
console.log(` Codex runtime: ${status.json.codexRuntime.path}${local}`);
|
|
@@ -42,6 +42,72 @@ async function status(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
42
42
|
printData(result, wantsJson, summaryLines(result));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
46
|
+
return value !== null && typeof value === "object" ? value as Record<string, unknown> : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function desktopSwitchInertReason(reason: unknown): string {
|
|
50
|
+
if (reason === "client_role") return "this proxy is running in the client role";
|
|
51
|
+
if (reason === "non_loopback_bind_requires_admission_token") {
|
|
52
|
+
return "a non-loopback bind requires an admission token, so this flag is inert";
|
|
53
|
+
}
|
|
54
|
+
return "the stored setting is not effective in the current runtime configuration";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function desktopSwitchApplyReason(reason: unknown): string {
|
|
58
|
+
if (reason === "not_requested") return "no desktop switch rewrite was requested";
|
|
59
|
+
if (reason === "proxy_not_running") return "the proxy is not running";
|
|
60
|
+
if (reason === "integration_disabled") return "Codex integration is disabled";
|
|
61
|
+
if (reason === "write_lock_busy") return "the Codex config write lock is busy";
|
|
62
|
+
if (reason === "injection_refused") return "Codex config injection was refused";
|
|
63
|
+
return "the rewrite could not be completed";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function settingsUpdateLines(
|
|
67
|
+
result: unknown,
|
|
68
|
+
changed: { desktopAuthless: boolean; clientCompaction: boolean },
|
|
69
|
+
): string[] {
|
|
70
|
+
if (!changed.desktopAuthless && !changed.clientCompaction) return ["System settings updated."];
|
|
71
|
+
const switches = recordValue(recordValue(result)?.codexDesktopSwitches);
|
|
72
|
+
if (!switches) return ["System settings updated."];
|
|
73
|
+
|
|
74
|
+
const lines: string[] = [];
|
|
75
|
+
const appendSwitch = (key: string, label: string): boolean => {
|
|
76
|
+
const state = recordValue(switches[key]);
|
|
77
|
+
if (!state || typeof state.stored !== "boolean" || typeof state.effective !== "boolean") return false;
|
|
78
|
+
lines.push(`${label}: stored ${state.stored ? "on" : "off"}.`);
|
|
79
|
+
// The effective value is always stated, even when it matches. Printing it only on a
|
|
80
|
+
// mismatch would make silence ambiguous — the reader could not tell "the stored value is
|
|
81
|
+
// in force" from "this build does not report effective state", and that ambiguity is a
|
|
82
|
+
// smaller version of the defect being fixed.
|
|
83
|
+
lines.push(state.effective === state.stored
|
|
84
|
+
? `${label}: effective ${state.effective ? "on" : "off"}.`
|
|
85
|
+
: `${label}: effective ${state.effective ? "on" : "off"} because ${desktopSwitchInertReason(state.inertReason)}.`);
|
|
86
|
+
return true;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (changed.desktopAuthless && !appendSwitch("codexDesktopAuthless", "Codex desktop authless")) {
|
|
90
|
+
return ["System settings updated."];
|
|
91
|
+
}
|
|
92
|
+
if (changed.clientCompaction && !appendSwitch("codexClientCompaction", "Codex client compaction")) {
|
|
93
|
+
return ["System settings updated."];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const apply = recordValue(switches.apply);
|
|
97
|
+
const authSource = recordValue(switches.authSource);
|
|
98
|
+
if (!apply || typeof apply.applied !== "boolean" || !authSource || typeof authSource.summary !== "string") {
|
|
99
|
+
return ["System settings updated."];
|
|
100
|
+
}
|
|
101
|
+
if (apply.applied) {
|
|
102
|
+
lines.push("Codex config: ~/.codex/config.toml was rewritten.");
|
|
103
|
+
} else {
|
|
104
|
+
const detail = typeof apply.detail === "string" && apply.detail.length > 0 ? ` Details: ${apply.detail}` : "";
|
|
105
|
+
lines.push(`Codex config: ~/.codex/config.toml was not rewritten because ${desktopSwitchApplyReason(apply.reason)}.${detail} Run 'ocx sync' to apply the stored settings.`);
|
|
106
|
+
}
|
|
107
|
+
lines.push(`Auth source: ${authSource.summary}`);
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
|
|
45
111
|
async function settings(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
46
112
|
const args = [...argv];
|
|
47
113
|
const wantsJson = takeFlag(args, "--json");
|
|
@@ -63,7 +129,10 @@ async function settings(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
63
129
|
...(clientCompaction !== undefined ? { codexClientCompaction: clientCompaction } : {}),
|
|
64
130
|
};
|
|
65
131
|
const result = await runtimeRequest("/api/settings", { method: "PUT", body: JSON.stringify(body) }, deps);
|
|
66
|
-
printData(result, wantsJson,
|
|
132
|
+
printData(result, wantsJson, settingsUpdateLines(result, {
|
|
133
|
+
desktopAuthless: desktopAuthless !== undefined,
|
|
134
|
+
clientCompaction: clientCompaction !== undefined,
|
|
135
|
+
}));
|
|
67
136
|
}
|
|
68
137
|
|
|
69
138
|
async function startup(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|