@bitkyc08/opencodex 2.16.0 → 2.17.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-CZwbOse7.js → index-DOKr6RBR.js} +10 -10
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/cli/export-command.ts +19 -7
- package/src/cli/help.ts +1 -1
- package/src/cli/registry.ts +2 -2
- package/src/clients/config-export.ts +165 -3
- package/src/generated/compatibility-version.json +17 -13
- package/src/integrations/omp-yaml-source.ts +232 -104
- package/src/integrations/registry.ts +14 -0
- package/src/integrations/state.ts +4 -3
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +141 -16
- package/src/lib/shadow-call.ts +6 -14
- package/src/server/management/integration-routes.ts +21 -14
- package/src/server/responses/core.ts +0 -1
- package/src/types.ts +5 -5
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Design of record: devlog/_fin/260802_client_toggle_api/030 and 031.
|
|
11
11
|
*/
|
|
12
|
+
import { homedir } from "node:os";
|
|
12
13
|
import { dirname } from "node:path";
|
|
13
14
|
import { EXPORT_CLIENTS, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
14
15
|
import { isLoopbackHostname } from "../codex/inject";
|
|
@@ -23,7 +24,8 @@ import { serializeDocument, UnserializableValueError } from "./serialize";
|
|
|
23
24
|
import { ClientPathError } from "../clients/config-export";
|
|
24
25
|
import { matchesOperationResult, newOpId, type JournalEntry } from "./journal";
|
|
25
26
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
26
|
-
import {
|
|
27
|
+
import { patchYamlFragmentSource, sourcePrunableYamlContainers } from "./omp-yaml-source";
|
|
28
|
+
import { withIntegrationWriterLock, type IntegrationWriterLockSeams } from "./writer-lock";
|
|
27
29
|
|
|
28
30
|
export type RefusalReason =
|
|
29
31
|
| "not_installed"
|
|
@@ -66,6 +68,8 @@ export interface IntegrationWriteInput {
|
|
|
66
68
|
home?: string;
|
|
67
69
|
store?: IntegrationStateStore;
|
|
68
70
|
io?: IntegrationIO;
|
|
71
|
+
/** Frozen once by the async coordinator; synchronous callers may omit it. */
|
|
72
|
+
resolvedPaths?: { configPath: string; detectDir: string };
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
export interface IntegrationRestoreInput extends IntegrationWriteInput {
|
|
@@ -169,11 +173,13 @@ function snapshotAbsPath(store: IntegrationStateStore, entry: JournalEntry): str
|
|
|
169
173
|
return snapshot.kind === "stored" ? snapshot.path : undefined;
|
|
170
174
|
}
|
|
171
175
|
|
|
172
|
-
function
|
|
176
|
+
function sourcePreservingFragmentValue(
|
|
177
|
+
contribution: ManagedContribution,
|
|
178
|
+
path: readonly string[],
|
|
179
|
+
): unknown | undefined {
|
|
173
180
|
const fragment = contribution.fragments.find(item => (
|
|
174
|
-
item.path.length ===
|
|
175
|
-
&& item.path
|
|
176
|
-
&& item.path[1] === "opencodex"
|
|
181
|
+
item.path.length === path.length
|
|
182
|
+
&& item.path.every((key, index) => key === path[index])
|
|
177
183
|
));
|
|
178
184
|
return fragment?.value;
|
|
179
185
|
}
|
|
@@ -194,7 +200,7 @@ function preflight(input: IntegrationWriteInput) {
|
|
|
194
200
|
*/
|
|
195
201
|
let configPath: string;
|
|
196
202
|
try {
|
|
197
|
-
configPath = spec.configPath(input.env, input.home);
|
|
203
|
+
configPath = input.resolvedPaths?.configPath ?? spec.configPath(input.env, input.home);
|
|
198
204
|
} catch (error) {
|
|
199
205
|
if (!(error instanceof ClientPathError)) throw error;
|
|
200
206
|
return { failed: refuse(clientId, "unsafe", "unsafe", error.message) } as const;
|
|
@@ -239,7 +245,7 @@ export function applyIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
239
245
|
if (pre.failed) return pre.failed;
|
|
240
246
|
const { store, io, clientId, spec, exportSpec, configPath, before, parsed, contribution, record, classified } = pre;
|
|
241
247
|
|
|
242
|
-
if (io.statKind(spec.detectDir(input.env, input.home)) !== "dir") {
|
|
248
|
+
if (io.statKind(input.resolvedPaths?.detectDir ?? spec.detectDir(input.env, input.home)) !== "dir") {
|
|
243
249
|
return refuse(clientId, "not_installed", "absent", `${clientId} is not installed`);
|
|
244
250
|
}
|
|
245
251
|
if (isLoopbackOnly(clientId) && !isLoopbackHostname(input.config.hostname)) {
|
|
@@ -294,11 +300,16 @@ export function applyIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
294
300
|
const nextDocument = mergeContribution(base, contribution);
|
|
295
301
|
let text: string;
|
|
296
302
|
try {
|
|
297
|
-
if (
|
|
298
|
-
const value =
|
|
303
|
+
if (spec.sourcePreservingYaml && before !== null) {
|
|
304
|
+
const value = sourcePreservingFragmentValue(contribution, spec.sourcePreservingYaml.path);
|
|
299
305
|
const patched = value === undefined
|
|
300
306
|
? null
|
|
301
|
-
:
|
|
307
|
+
: patchYamlFragmentSource(
|
|
308
|
+
before,
|
|
309
|
+
spec.sourcePreservingYaml.path,
|
|
310
|
+
{ kind: "upsert", value },
|
|
311
|
+
nextDocument,
|
|
312
|
+
);
|
|
302
313
|
if (patched === null) {
|
|
303
314
|
return refuse(clientId, "unsafe", "unsafe",
|
|
304
315
|
`${configPath} uses YAML source opencodex cannot patch without risking unrelated comments or formatting, so it was left alone`);
|
|
@@ -344,7 +355,7 @@ export function applyIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
344
355
|
export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
345
356
|
const pre = preflight(input);
|
|
346
357
|
if (pre.failed) return pre.failed;
|
|
347
|
-
const { store, io, clientId, exportSpec, configPath, before, parsed, record, classified } = pre;
|
|
358
|
+
const { store, io, clientId, spec, exportSpec, configPath, before, parsed, record, classified } = pre;
|
|
348
359
|
|
|
349
360
|
if (classified.state === "absent") {
|
|
350
361
|
return { ok: true, changed: false, state: "absent", clientId, message: "not applied" };
|
|
@@ -377,21 +388,32 @@ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
377
388
|
* json client classifies a sibling edit as stale (#1631) — which is why the
|
|
378
389
|
* removal runs against the document as parsed NOW, and the re-serialize is
|
|
379
390
|
* value-safe because non-round-tripping numbers were refused at parse time.
|
|
391
|
+
* Source-preserving YAML clients additionally compute which recorded
|
|
392
|
+
* containers are still source-empty before pruning, so a later sibling or
|
|
393
|
+
* comment makes its ancestor user-owned without protecting our leaf.
|
|
380
394
|
*/
|
|
395
|
+
const recordedCreated = record!.createdContainers ?? [];
|
|
396
|
+
const prunableCreated = spec.sourcePreservingYaml && before !== null
|
|
397
|
+
? sourcePrunableYamlContainers(before, spec.sourcePreservingYaml.path, recordedCreated)
|
|
398
|
+
: recordedCreated;
|
|
399
|
+
if (prunableCreated === null) {
|
|
400
|
+
return refuse(clientId, "unsafe", "unsafe",
|
|
401
|
+
`${configPath} uses YAML source opencodex cannot patch without risking unrelated comments or formatting, so nothing was removed`);
|
|
402
|
+
}
|
|
381
403
|
const { doc, removed } = removeFragments(
|
|
382
404
|
parsed,
|
|
383
405
|
record!.fragmentPaths,
|
|
384
|
-
new Set(
|
|
406
|
+
new Set(prunableCreated),
|
|
385
407
|
);
|
|
386
408
|
if (!removed) {
|
|
387
409
|
return { ok: true, changed: false, state: "absent", clientId, message: "nothing to remove" };
|
|
388
410
|
}
|
|
389
411
|
let text: string;
|
|
390
412
|
try {
|
|
391
|
-
if (
|
|
392
|
-
const patched =
|
|
413
|
+
if (spec.sourcePreservingYaml && before !== null) {
|
|
414
|
+
const patched = patchYamlFragmentSource(before, spec.sourcePreservingYaml.path, {
|
|
393
415
|
kind: "remove",
|
|
394
|
-
|
|
416
|
+
createdContainers: prunableCreated,
|
|
395
417
|
}, doc);
|
|
396
418
|
if (patched === null) {
|
|
397
419
|
return refuse(clientId, "unsafe", "unsafe",
|
|
@@ -435,7 +457,8 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
435
457
|
if (entry.clientId !== input.clientId) throw new Error("restore input names a different client than the operation");
|
|
436
458
|
|
|
437
459
|
const clientId = entry.clientId;
|
|
438
|
-
const resolvedPath =
|
|
460
|
+
const resolvedPath = input.resolvedPaths?.configPath
|
|
461
|
+
?? INTEGRATION_CLIENTS[clientId].configPath(input.env, input.home);
|
|
439
462
|
// Restore acts on the path the operation was journaled against. Resolving a
|
|
440
463
|
// different path here would let an operation recorded for one home delete a
|
|
441
464
|
// file in another.
|
|
@@ -533,3 +556,105 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
533
556
|
snapshotPath: snapshotAbsPath(store, restoreEntry),
|
|
534
557
|
});
|
|
535
558
|
}
|
|
559
|
+
|
|
560
|
+
export interface CoordinatedIntegrationOptions {
|
|
561
|
+
lockSeams?: IntegrationWriterLockSeams;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Freeze all mutable resolution seams before the first lock await. */
|
|
565
|
+
type FrozenIntegrationInput = IntegrationWriteInput & {
|
|
566
|
+
store: IntegrationStateStore;
|
|
567
|
+
io: IntegrationIO;
|
|
568
|
+
env: NodeJS.ProcessEnv;
|
|
569
|
+
home: string;
|
|
570
|
+
resolvedPaths: { configPath: string; detectDir: string };
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegrationInput {
|
|
574
|
+
const env = { ...(input.env ?? process.env) };
|
|
575
|
+
const home = input.home ?? homedir();
|
|
576
|
+
const store = input.store ?? createIntegrationStateStore();
|
|
577
|
+
const io = input.io ?? defaultIntegrationIO(store);
|
|
578
|
+
const spec = INTEGRATION_CLIENTS[input.clientId];
|
|
579
|
+
const resolvedPaths = {
|
|
580
|
+
configPath: spec.configPath(env, home),
|
|
581
|
+
detectDir: spec.detectDir(env, home),
|
|
582
|
+
};
|
|
583
|
+
return { ...input, env, home, store, io, resolvedPaths };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function tryFreezeIntegrationInput(input: IntegrationWriteInput):
|
|
587
|
+
| { ok: true; value: FrozenIntegrationInput }
|
|
588
|
+
| { ok: false; refusal: WriteRefused } {
|
|
589
|
+
try {
|
|
590
|
+
return { ok: true, value: freezeIntegrationInput(input) };
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (!(error instanceof ClientPathError)) throw error;
|
|
593
|
+
return {
|
|
594
|
+
ok: false,
|
|
595
|
+
refusal: refuse(input.clientId, "unsafe", "unsafe", error.message),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function coordinatedWrite(
|
|
601
|
+
input: IntegrationWriteInput,
|
|
602
|
+
operation: (frozen: IntegrationWriteInput) => WriteOutcome,
|
|
603
|
+
options?: CoordinatedIntegrationOptions,
|
|
604
|
+
): Promise<WriteOutcome> {
|
|
605
|
+
const prepared = tryFreezeIntegrationInput(input);
|
|
606
|
+
if (!prepared.ok) return prepared.refusal;
|
|
607
|
+
const frozen = prepared.value;
|
|
608
|
+
const spec = INTEGRATION_CLIENTS[frozen.clientId];
|
|
609
|
+
if (!spec.writerLock) return operation(frozen);
|
|
610
|
+
|
|
611
|
+
// An absent client home is not created merely to acquire a sibling lock.
|
|
612
|
+
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
|
|
613
|
+
return operation(frozen);
|
|
614
|
+
}
|
|
615
|
+
return withIntegrationWriterLock(
|
|
616
|
+
frozen.resolvedPaths.configPath,
|
|
617
|
+
async () => operation(frozen),
|
|
618
|
+
options?.lockSeams,
|
|
619
|
+
spec.writerLock.suffix,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function applyIntegrationCoordinated(
|
|
624
|
+
input: IntegrationWriteInput,
|
|
625
|
+
options?: CoordinatedIntegrationOptions,
|
|
626
|
+
): Promise<WriteOutcome> {
|
|
627
|
+
return coordinatedWrite(input, applyIntegration, options);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export function disableIntegrationCoordinated(
|
|
631
|
+
input: IntegrationWriteInput,
|
|
632
|
+
options?: CoordinatedIntegrationOptions,
|
|
633
|
+
): Promise<WriteOutcome> {
|
|
634
|
+
return coordinatedWrite(input, disableIntegration, options);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export async function restoreIntegrationCoordinated(
|
|
638
|
+
input: IntegrationRestoreInput,
|
|
639
|
+
options?: CoordinatedIntegrationOptions,
|
|
640
|
+
): Promise<WriteOutcome> {
|
|
641
|
+
const prepared = tryFreezeIntegrationInput(input);
|
|
642
|
+
if (!prepared.ok) return prepared.refusal;
|
|
643
|
+
const frozen = prepared.value;
|
|
644
|
+
const spec = INTEGRATION_CLIENTS[frozen.clientId];
|
|
645
|
+
if (!spec.writerLock) return restoreIntegration({ ...frozen, opId: input.opId, confirmDrift: input.confirmDrift });
|
|
646
|
+
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
|
|
647
|
+
return refuse(
|
|
648
|
+
frozen.clientId,
|
|
649
|
+
"unsafe",
|
|
650
|
+
"unsafe",
|
|
651
|
+
`${frozen.resolvedPaths.detectDir} is missing; restore will not create the client home`,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
return withIntegrationWriterLock(
|
|
655
|
+
frozen.resolvedPaths.configPath,
|
|
656
|
+
async () => restoreIntegration({ ...frozen, opId: input.opId, confirmDrift: input.confirmDrift }),
|
|
657
|
+
options?.lockSeams,
|
|
658
|
+
spec.writerLock.suffix,
|
|
659
|
+
);
|
|
660
|
+
}
|
package/src/lib/shadow-call.ts
CHANGED
|
@@ -32,23 +32,15 @@ export function isShadowSourceModel(modelId: string, configured?: unknown): bool
|
|
|
32
32
|
/**
|
|
33
33
|
* Decide whether a matching source model should use the opt-in intercept.
|
|
34
34
|
*
|
|
35
|
-
* Codex 0.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Before Codex 0.147.0 this checked x-codex-turn-metadata and exempted
|
|
36
|
+
* request_kind "turn". Codex 0.147.0 can label background helper calls as
|
|
37
|
+
* "turn", causing them to bypass the intercept (#1684). The fix is to
|
|
38
|
+
* intercept every configured shadow source model unconditionally — the model
|
|
39
|
+
* slug alone is a sufficient signal.
|
|
38
40
|
*/
|
|
39
41
|
export function shouldInterceptShadowCall(
|
|
40
42
|
modelId: string,
|
|
41
43
|
configured: unknown,
|
|
42
|
-
headers: Headers,
|
|
43
44
|
): boolean {
|
|
44
|
-
|
|
45
|
-
const rawMetadata = headers.get("x-codex-turn-metadata");
|
|
46
|
-
if (rawMetadata === null) return true;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const parsed = JSON.parse(rawMetadata) as { request_kind?: unknown };
|
|
50
|
-
return parsed?.request_kind !== "turn";
|
|
51
|
-
} catch {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
45
|
+
return isShadowSourceModel(modelId, configured);
|
|
54
46
|
}
|
|
@@ -19,13 +19,14 @@ import {
|
|
|
19
19
|
import { readIntegrationState } from "../../integrations/state";
|
|
20
20
|
import { createIntegrationStateStore, type IntegrationStateStore } from "../../integrations/store";
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
applyIntegrationCoordinated,
|
|
23
|
+
disableIntegrationCoordinated,
|
|
24
|
+
restoreIntegrationCoordinated,
|
|
25
25
|
type IntegrationRestoreInput,
|
|
26
26
|
type IntegrationWriteInput,
|
|
27
27
|
type WriteRefused,
|
|
28
28
|
} from "../../integrations/writer";
|
|
29
|
+
import { IntegrationWriterLockBusyError, type IntegrationWriterLockSeams } from "../../integrations/writer-lock";
|
|
29
30
|
import { jsonResponse } from "../auth-cors";
|
|
30
31
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
31
32
|
import type { ManagementContext } from "./context";
|
|
@@ -37,9 +38,9 @@ const INTEGRATION_MUTATION_JOIN_MS = 120_000;
|
|
|
37
38
|
export const INTEGRATION_MUTATION_TERMINAL_MS = 10 * 60_000;
|
|
38
39
|
|
|
39
40
|
type IntegrationStateRecord = Awaited<ReturnType<typeof readIntegrationState>>;
|
|
40
|
-
type ApplyResult = Awaited<ReturnType<typeof
|
|
41
|
-
type DisableResult = Awaited<ReturnType<typeof
|
|
42
|
-
type RestoreResult = Awaited<ReturnType<typeof
|
|
41
|
+
type ApplyResult = Awaited<ReturnType<typeof applyIntegrationCoordinated>>;
|
|
42
|
+
type DisableResult = Awaited<ReturnType<typeof disableIntegrationCoordinated>>;
|
|
43
|
+
type RestoreResult = Awaited<ReturnType<typeof restoreIntegrationCoordinated>>;
|
|
43
44
|
|
|
44
45
|
export type IntegrationStateEnvelope = {
|
|
45
46
|
clientId: IntegrationClientId;
|
|
@@ -95,6 +96,7 @@ class IntegrationMutationBusyError extends Error {
|
|
|
95
96
|
const integrationMutationFlights = new Map<IntegrationClientId, IntegrationMutationFlight>();
|
|
96
97
|
let integrationMutationTestHooks: {
|
|
97
98
|
io?: IntegrationIO;
|
|
99
|
+
lockSeams?: IntegrationWriterLockSeams;
|
|
98
100
|
/**
|
|
99
101
|
* Bind every read and write in the request to one store. Without this a
|
|
100
102
|
* route test could isolate the writer but not the journal listing or the
|
|
@@ -185,6 +187,7 @@ function runIntegrationMutationFlight<T>(
|
|
|
185
187
|
export function setIntegrationMutationFlightTestHooks(
|
|
186
188
|
hooks: {
|
|
187
189
|
io?: IntegrationIO;
|
|
190
|
+
lockSeams?: IntegrationWriterLockSeams;
|
|
188
191
|
/** Binds the WHOLE request — reads, writes and journal — to one store. */
|
|
189
192
|
store?: IntegrationStateStore;
|
|
190
193
|
run?: (operation: () => Promise<unknown>) => Promise<unknown>;
|
|
@@ -433,6 +436,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
433
436
|
|
|
434
437
|
const opId = parsed.opId.trim();
|
|
435
438
|
const confirmDrift = parsed.confirmDrift ?? false;
|
|
439
|
+
let restoreClientId: IntegrationClientId | undefined;
|
|
436
440
|
try {
|
|
437
441
|
const store = integrationStore();
|
|
438
442
|
const operation = store.findOperation(opId);
|
|
@@ -443,6 +447,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
443
447
|
opId,
|
|
444
448
|
}, 404, req, ctx.config);
|
|
445
449
|
}
|
|
450
|
+
restoreClientId = operation.clientId;
|
|
446
451
|
const snapshot = store.readSnapshot(operation);
|
|
447
452
|
if (snapshot.kind === "expired") {
|
|
448
453
|
return jsonResponse({
|
|
@@ -462,7 +467,9 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
462
467
|
operation.clientId,
|
|
463
468
|
`restore:${opId}:${confirmDrift}`,
|
|
464
469
|
writeInput.io?.now ?? Date.now,
|
|
465
|
-
() =>
|
|
470
|
+
() => restoreIntegrationCoordinated(restoreInput, {
|
|
471
|
+
lockSeams: integrationMutationTestHooks?.lockSeams,
|
|
472
|
+
}),
|
|
466
473
|
);
|
|
467
474
|
if (!result.ok) {
|
|
468
475
|
/*
|
|
@@ -478,11 +485,11 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
478
485
|
}
|
|
479
486
|
return jsonResponse(result satisfies IntegrationRestoreEnvelope, 200, req, ctx.config);
|
|
480
487
|
} catch (error) {
|
|
481
|
-
if (error instanceof IntegrationMutationBusyError) {
|
|
488
|
+
if (error instanceof IntegrationMutationBusyError || error instanceof IntegrationWriterLockBusyError) {
|
|
482
489
|
return jsonResponse({
|
|
483
490
|
error: "integration mutation busy",
|
|
484
491
|
code: "integration_mutation_busy",
|
|
485
|
-
clientId: error.clientId,
|
|
492
|
+
clientId: restoreClientId ?? (error instanceof IntegrationMutationBusyError ? error.clientId : undefined),
|
|
486
493
|
}, 409, req, ctx.config);
|
|
487
494
|
}
|
|
488
495
|
return internalErrorResponse(error, ctx);
|
|
@@ -519,18 +526,18 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
519
526
|
requestedClient,
|
|
520
527
|
parsed.enabled ? "apply" : "disable",
|
|
521
528
|
input.io?.now ?? Date.now,
|
|
522
|
-
() =>
|
|
523
|
-
?
|
|
524
|
-
:
|
|
529
|
+
() => parsed.enabled
|
|
530
|
+
? applyIntegrationCoordinated(input, { lockSeams: integrationMutationTestHooks?.lockSeams })
|
|
531
|
+
: disableIntegrationCoordinated(input, { lockSeams: integrationMutationTestHooks?.lockSeams }),
|
|
525
532
|
);
|
|
526
533
|
if (!result.ok) return writerFailureResponse(requestedClient, result, ctx);
|
|
527
534
|
return jsonResponse(result satisfies IntegrationToggleEnvelope, 200, req, ctx.config);
|
|
528
535
|
} catch (error) {
|
|
529
|
-
if (error instanceof IntegrationMutationBusyError) {
|
|
536
|
+
if (error instanceof IntegrationMutationBusyError || error instanceof IntegrationWriterLockBusyError) {
|
|
530
537
|
return jsonResponse({
|
|
531
538
|
error: "integration mutation busy",
|
|
532
539
|
code: "integration_mutation_busy",
|
|
533
|
-
clientId:
|
|
540
|
+
clientId: requestedClient,
|
|
534
541
|
}, 409, req, ctx.config);
|
|
535
542
|
}
|
|
536
543
|
return internalErrorResponse(error, ctx);
|
|
@@ -1582,7 +1582,6 @@ async function handleResponsesInner(
|
|
|
1582
1582
|
if (_sci?.enabled && _sci.model && shouldInterceptShadowCall(
|
|
1583
1583
|
parsed.modelId,
|
|
1584
1584
|
_sci.sourceModels,
|
|
1585
|
-
req.headers,
|
|
1586
1585
|
)) {
|
|
1587
1586
|
const _sciOriginal = parsed.modelId;
|
|
1588
1587
|
parsed.modelId = _sci.model;
|
package/src/types.ts
CHANGED
|
@@ -756,11 +756,11 @@ export interface OcxConfig {
|
|
|
756
756
|
*/
|
|
757
757
|
customModelCatalogMigration?: unknown;
|
|
758
758
|
/**
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
*
|
|
759
|
+
* Shadow call intercept: redirect Codex's hard-coded helper calls (title generation,
|
|
760
|
+
* commit messages, skill orchestration) to a user-chosen model. Default intercepted
|
|
761
|
+
* source models: gpt-5.4-mini (older clients) and gpt-5.6-luna (Codex 0.145.0+).
|
|
762
|
+
* Opt-in; disabled by default. Matching maintenance/helper requests are forced to low.
|
|
763
|
+
* All requests for configured shadow source models are intercepted unconditionally.
|
|
764
764
|
*/
|
|
765
765
|
shadowCallIntercept?: {
|
|
766
766
|
/** When true, requests for known shadow/helper source models are rewritten to the configured model. */
|