@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -26,13 +26,11 @@
|
|
|
26
26
|
* CODEX_HOME is resolved at CALL time (the `features.ts:58-67` pattern) so tests
|
|
27
27
|
* can point fixtures via env or an explicit path.
|
|
28
28
|
*/
|
|
29
|
-
import { existsSync,
|
|
29
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
30
30
|
import { dirname, join, resolve } from "node:path";
|
|
31
|
-
import { createHash, randomBytes
|
|
31
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
32
32
|
import { expandUserPath } from "../config";
|
|
33
|
-
import { CODEX_CONFIG_PATH } from "./paths";
|
|
34
33
|
import { resolveCodexHomeDir } from "./home";
|
|
35
|
-
import { OCX_SECTION_MARKER } from "./injected-marker";
|
|
36
34
|
import {
|
|
37
35
|
durableWrite,
|
|
38
36
|
durableWriteExclusive,
|
|
@@ -141,46 +139,19 @@ export function isToggleId(value: string): value is ToggleId {
|
|
|
141
139
|
return Object.prototype.hasOwnProperty.call(TOGGLE_KEYS, value);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
export
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
baseVariantDir?: string;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function activeCodexHome(): string {
|
|
155
|
-
const raw = process.env.CODEX_HOME?.trim();
|
|
156
|
-
if (!raw) return CODEX_CONFIG_PATH.slice(0, -"/config.toml".length);
|
|
157
|
-
const path = resolve(expandUserPath(raw));
|
|
158
|
-
try {
|
|
159
|
-
return realpathSync.native(path);
|
|
160
|
-
} catch {
|
|
161
|
-
return path;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function activeConfigPath(opts?: Paths): string {
|
|
166
|
-
return opts?.configPath ?? join(activeCodexHome(), "config.toml");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function activeStorePath(opts?: Paths): string {
|
|
170
|
-
return opts?.storePath ?? join(activeCodexHome(), "opencodex-prompt.json");
|
|
171
|
-
}
|
|
142
|
+
export { activeConfigPath, activeStorePath, activeBaseVariantDir } from "./prompt-layers/paths";
|
|
143
|
+
export type { Paths } from "./prompt-layers/paths";
|
|
144
|
+
export { computeRevision, readFileBytes } from "./prompt-layers/revision";
|
|
145
|
+
export { normalizeBody, findInvalidCharacter, encodeBasicString, decodeBasicString } from "./prompt-layers/encoding";
|
|
146
|
+
export type { CharacterFinding } from "./prompt-layers/encoding";
|
|
147
|
+
export { inspectOwnership } from "./prompt-layers/toml-read";
|
|
148
|
+
export type { Ownership } from "./prompt-layers/toml-read";
|
|
172
149
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
* bodies in `opencodex-prompt.json` would mean materialising a temp file at selection
|
|
179
|
-
* time, which is a second write path for no gain.
|
|
180
|
-
*/
|
|
181
|
-
export function activeBaseVariantDir(opts?: Paths): string {
|
|
182
|
-
return opts?.baseVariantDir ?? join(activeCodexHome(), "opencodex-prompt-base");
|
|
183
|
-
}
|
|
150
|
+
import { activeConfigPath, activeStorePath, activeBaseVariantDir, journalPathFor, lockPathFor, type Paths } from "./prompt-layers/paths";
|
|
151
|
+
import { readFileOrNull, computeRevision, updateFingerprintField } from "./prompt-layers/revision";
|
|
152
|
+
import { normalizeBody, findInvalidCharacter, decodeBasicString } from "./prompt-layers/encoding";
|
|
153
|
+
import { rootArrayEntries, hasRootKey, rootLines, tableLines, boolInLines, inspectOwnership } from "./prompt-layers/toml-read";
|
|
154
|
+
import { setRootBool, setRootString, setTableBool, setProjection, removeUnownedProjection } from "./prompt-layers/toml-edit";
|
|
184
155
|
|
|
185
156
|
/**
|
|
186
157
|
* Instruction documents the prompt probe renders out of CODEX_HOME, in the
|
|
@@ -212,88 +183,6 @@ function probeInstructionFilenames(configBytes: string | null): string[] {
|
|
|
212
183
|
return names;
|
|
213
184
|
}
|
|
214
185
|
|
|
215
|
-
/**
|
|
216
|
-
* Decoded string entries of a root-scope TOML array.
|
|
217
|
-
*
|
|
218
|
-
* Parsed, not pattern-matched. Three successive review rounds each found another
|
|
219
|
-
* valid spelling a hand-rolled reader missed — multi-line arrays, a comment after the
|
|
220
|
-
* opening bracket, a quoted key — and every miss was a rendered document whose edits
|
|
221
|
-
* moved no admission key. The pattern was the defect: TOML is not a line format, so
|
|
222
|
-
* no regex over lines can enumerate what a parser accepts.
|
|
223
|
-
*
|
|
224
|
-
* The module header's warning about JS TOML parsers does apply here, and a review
|
|
225
|
-
* round proved it against an earlier version of this comment that claimed otherwise.
|
|
226
|
-
* Bun rejects an entire document containing an integer outside JavaScript's safe
|
|
227
|
-
* range, such as `model_context_window = 9223372036854775807`, which Rust accepts as
|
|
228
|
-
* an ordinary `i64`. A whole-document parse turned that into BOTH arrays disappearing
|
|
229
|
-
* — a worse failure than any single missed spelling, and one the old regex did not
|
|
230
|
-
* have.
|
|
231
|
-
*
|
|
232
|
-
* So the parse is the preferred reader, not the only one. When it fails, the scan
|
|
233
|
-
* below runs, and it is deliberately loose: it accepts any spelling it recognises and
|
|
234
|
-
* over-reports rather than under-reports, because an extra hashed filename costs one
|
|
235
|
-
* redundant probe while a missing one costs stale text.
|
|
236
|
-
*/
|
|
237
|
-
function rootArrayEntries(configBytes: string | null, key: string): string[] {
|
|
238
|
-
const value = rootValue(configBytes, key);
|
|
239
|
-
if (value === PARSE_FAILED) return scanRootArrayEntries(configBytes, key);
|
|
240
|
-
if (!Array.isArray(value)) return [];
|
|
241
|
-
return value.filter((entry): entry is string => typeof entry === "string");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Distinguishes "the parser could not read this file" from "the key is absent".
|
|
246
|
-
* Collapsing the two is what made an unrelated large integer silently empty the
|
|
247
|
-
* project-document set.
|
|
248
|
-
*/
|
|
249
|
-
const PARSE_FAILED = Symbol("toml-parse-failed");
|
|
250
|
-
|
|
251
|
-
/** A root-scope value, `undefined` when the key is absent, `PARSE_FAILED` when the file will not parse. */
|
|
252
|
-
function rootValue(configBytes: string | null, key: string): unknown {
|
|
253
|
-
if (configBytes === null) return undefined;
|
|
254
|
-
let parsed: unknown;
|
|
255
|
-
try {
|
|
256
|
-
parsed = Bun.TOML.parse(configBytes);
|
|
257
|
-
} catch {
|
|
258
|
-
return PARSE_FAILED;
|
|
259
|
-
}
|
|
260
|
-
if (typeof parsed !== "object" || parsed === null) return PARSE_FAILED;
|
|
261
|
-
return (parsed as Record<string, unknown>)[key];
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Fallback reader for a config this parser will not accept but Codex will.
|
|
266
|
-
*
|
|
267
|
-
* Not a second attempt at being a TOML parser — that approach failed three review
|
|
268
|
-
* rounds. It is a deliberately over-eager scan: it takes the first bracketed group for
|
|
269
|
-
* the key under either spelling, spans lines, strips comments, and keeps anything that
|
|
270
|
-
* decodes. Over-reporting is the safe direction here.
|
|
271
|
-
*/
|
|
272
|
-
function scanRootArrayEntries(configBytes: string | null, key: string): string[] {
|
|
273
|
-
const lines = rootLines(configBytes ?? "");
|
|
274
|
-
const opener = new RegExp(`^\\s*"?${key}"?\\s*=\\s*\\[(.*)$`);
|
|
275
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
276
|
-
const m = opener.exec(lines[i]!);
|
|
277
|
-
if (!m) continue;
|
|
278
|
-
let body = m[1]!.replace(/#.*$/, "");
|
|
279
|
-
for (let j = i; !body.includes("]"); ) {
|
|
280
|
-
j += 1;
|
|
281
|
-
if (j >= lines.length) return [];
|
|
282
|
-
body += lines[j]!.replace(/#.*$/, "");
|
|
283
|
-
}
|
|
284
|
-
const out: string[] = [];
|
|
285
|
-
for (const raw of body.slice(0, body.indexOf("]")).split(",")) {
|
|
286
|
-
const trimmed = raw.trim();
|
|
287
|
-
if (trimmed === "") continue;
|
|
288
|
-
const decoded = trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2
|
|
289
|
-
? trimmed.slice(1, -1)
|
|
290
|
-
: decodeBasicString(trimmed);
|
|
291
|
-
if (decoded !== null) out.push(decoded);
|
|
292
|
-
}
|
|
293
|
-
return out;
|
|
294
|
-
}
|
|
295
|
-
return [];
|
|
296
|
-
}
|
|
297
186
|
|
|
298
187
|
/**
|
|
299
188
|
* The directories Codex would look in for a project document, given the home the
|
|
@@ -344,242 +233,6 @@ function projectRootMarkers(configBytes: string | null): string[] {
|
|
|
344
233
|
return rootArrayEntries(configBytes, "project_root_markers").filter(m => m !== "");
|
|
345
234
|
}
|
|
346
235
|
|
|
347
|
-
/**
|
|
348
|
-
* Whether a root-scope key is present at all, regardless of what it holds.
|
|
349
|
-
*
|
|
350
|
-
* A parse failure is not an answer, so it falls through to the scan rather than
|
|
351
|
-
* counting as present: reading `PARSE_FAILED` as "present" would report an empty
|
|
352
|
-
* marker list and disable root detection on a config Codex reads fine.
|
|
353
|
-
*/
|
|
354
|
-
function hasRootKey(configBytes: string | null, key: string): boolean {
|
|
355
|
-
const value = rootValue(configBytes, key);
|
|
356
|
-
if (value === PARSE_FAILED) return scanHasRootKey(configBytes, key);
|
|
357
|
-
return value !== undefined;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/** Textual presence check, used only when the parser cannot read the file. */
|
|
361
|
-
function scanHasRootKey(configBytes: string | null, key: string): boolean {
|
|
362
|
-
const probe = new RegExp(`^\\s*"?${key}"?\\s*=`);
|
|
363
|
-
return rootLines(configBytes ?? "").some(line => probe.test(line));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Feed one named field into a fingerprint, framed so that no two distinct states
|
|
368
|
-
* can produce the same digest.
|
|
369
|
-
*
|
|
370
|
-
* Framing is the whole point. Concatenating `name + ":" + contents` is ambiguous:
|
|
371
|
-
* an adversarial review of the first version of this function showed that
|
|
372
|
-
* `{override: "left", agents: "right\nAGENTS.md:tail"}` and
|
|
373
|
-
* `{override: "left\nAGENTS.md:right", agents: "tail"}` hashed identically, because
|
|
374
|
-
* a file's own bytes can imitate the separator that follows it. That is exactly a
|
|
375
|
-
* missed invalidation: the fingerprint is the probe's admission key, so two
|
|
376
|
-
* different prompt states sharing a digest means one caller is served the other's
|
|
377
|
-
* stale text.
|
|
378
|
-
*
|
|
379
|
-
* A byte length cannot be forged by content, so each field carries one. Absence is
|
|
380
|
-
* a length of -1 rather than a sentinel string, because a sentinel is just more
|
|
381
|
-
* content: the same review found that `null` collided with a file whose bytes were
|
|
382
|
-
* literally NUL + "absent".
|
|
383
|
-
*/
|
|
384
|
-
function updateFingerprintField(hash: Hash, name: string, contents: string | null): void {
|
|
385
|
-
const bytes = contents === null ? -1 : Buffer.byteLength(contents, "utf8");
|
|
386
|
-
hash.update(`\n${name}:${bytes}:`);
|
|
387
|
-
if (contents !== null) hash.update(contents);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function journalPathFor(storePath: string): string {
|
|
391
|
-
return `${storePath.replace(/\.json$/, "")}.journal`;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function lockPathFor(storePath: string): string {
|
|
395
|
-
return `${storePath.replace(/\.json$/, "")}.lock`;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// ---------------------------------------------------------------------------
|
|
399
|
-
// Character policy — see the header. Defined over Unicode SCALAR VALUES, not
|
|
400
|
-
// UTF-16 code units, because a lone surrogate is not a scalar value and UTF-8
|
|
401
|
-
// encoding would silently substitute U+FFFD.
|
|
402
|
-
// ---------------------------------------------------------------------------
|
|
403
|
-
|
|
404
|
-
export interface CharacterFinding {
|
|
405
|
-
/** code-point index, consistent across module, route and editor */
|
|
406
|
-
position: number;
|
|
407
|
-
reason: "control" | "unpaired-surrogate";
|
|
408
|
-
codePoint: number;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** Tab to four spaces, CRLF and lone CR to LF. Applied BEFORE validation. */
|
|
412
|
-
export function normalizeBody(body: string): string {
|
|
413
|
-
return body.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/** First offending scalar, or null. Run AFTER normalizeBody. */
|
|
417
|
-
export function findInvalidCharacter(body: string): CharacterFinding | null {
|
|
418
|
-
let position = 0;
|
|
419
|
-
for (let i = 0; i < body.length; ) {
|
|
420
|
-
const code = body.codePointAt(i)!;
|
|
421
|
-
const unit = body.charCodeAt(i);
|
|
422
|
-
const isHighSurrogate = unit >= 0xd800 && unit <= 0xdbff;
|
|
423
|
-
const isLowSurrogate = unit >= 0xdc00 && unit <= 0xdfff;
|
|
424
|
-
// codePointAt only combines a well-formed pair, so a surviving surrogate
|
|
425
|
-
// code point here is unpaired by construction.
|
|
426
|
-
if ((isHighSurrogate || isLowSurrogate) && code === unit) {
|
|
427
|
-
return { position, reason: "unpaired-surrogate", codePoint: code };
|
|
428
|
-
}
|
|
429
|
-
const isNewline = code === 0x0a;
|
|
430
|
-
const isC0 = code < 0x20 && !isNewline;
|
|
431
|
-
const isDel = code === 0x7f;
|
|
432
|
-
const isC1 = code >= 0x80 && code <= 0x9f;
|
|
433
|
-
if (isC0 || isDel || isC1) {
|
|
434
|
-
return { position, reason: "control", codePoint: code };
|
|
435
|
-
}
|
|
436
|
-
i += code > 0xffff ? 2 : 1;
|
|
437
|
-
position += 1;
|
|
438
|
-
}
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* TOML basic-string encoding, total over the accepted set: three rules, none of
|
|
444
|
-
* them in the range where `Bun.TOML.parse` misbehaves. `\r` cannot appear
|
|
445
|
-
* because normalizeBody removed it; control characters cannot appear because
|
|
446
|
-
* findInvalidCharacter rejected them.
|
|
447
|
-
*/
|
|
448
|
-
export function encodeBasicString(body: string): string {
|
|
449
|
-
return `"${body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Inverse of `encodeBasicString`, deliberately narrow: it accepts ONLY the three
|
|
454
|
-
* escapes we emit. `\t`, `\f`, `\b`, `\r` and `\uXXXX` are refused rather than
|
|
455
|
-
* guessed — decoding them correctly is exactly the ambiguity the restricted set
|
|
456
|
-
* exists to avoid.
|
|
457
|
-
*/
|
|
458
|
-
export function decodeBasicString(literal: string): string | null {
|
|
459
|
-
if (literal.length < 2 || !literal.startsWith('"') || !literal.endsWith('"')) return null;
|
|
460
|
-
const inner = literal.slice(1, -1);
|
|
461
|
-
let out = "";
|
|
462
|
-
for (let i = 0; i < inner.length; i += 1) {
|
|
463
|
-
const ch = inner[i]!;
|
|
464
|
-
if (ch !== "\\") {
|
|
465
|
-
if (ch === '"') return null; // unescaped quote: not a single literal
|
|
466
|
-
out += ch;
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
const next = inner[i + 1];
|
|
470
|
-
if (next === "\\") out += "\\";
|
|
471
|
-
else if (next === '"') out += '"';
|
|
472
|
-
else if (next === "n") out += "\n";
|
|
473
|
-
else return null; // any other escape is outside what we will decode
|
|
474
|
-
i += 1;
|
|
475
|
-
}
|
|
476
|
-
return out;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// ---------------------------------------------------------------------------
|
|
480
|
-
// Byte-level hashing. The revision covers COMPLETE file bytes plus existence,
|
|
481
|
-
// so removing the marker while leaving the value intact still changes it.
|
|
482
|
-
// ---------------------------------------------------------------------------
|
|
483
|
-
|
|
484
|
-
function readFileOrNull(path: string): string | null {
|
|
485
|
-
try {
|
|
486
|
-
if (!existsSync(path)) return null;
|
|
487
|
-
return readFileSync(path, "utf8");
|
|
488
|
-
} catch {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export function computeRevision(configBytes: string | null, storeBytes: string | null): string {
|
|
494
|
-
const hash = createHash("sha256");
|
|
495
|
-
// Length-framed for the reason given on updateFingerprintField: with a bare
|
|
496
|
-
// separator, config bytes ending in "\nstore:" shift the boundary and two
|
|
497
|
-
// different pairs hash alike. That matters twice over — this value is both the
|
|
498
|
-
// probe's admission input and the optimistic-concurrency token compared in
|
|
499
|
-
// commit(), where a collision would let a write built on stale bytes through.
|
|
500
|
-
updateFingerprintField(hash, "cfg", configBytes);
|
|
501
|
-
updateFingerprintField(hash, "store", storeBytes);
|
|
502
|
-
return `sha256:${hash.digest("hex")}`;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export { readFileOrNull as readFileBytes };
|
|
506
|
-
|
|
507
|
-
// ---------------------------------------------------------------------------
|
|
508
|
-
// Scoped TOML scanning. Line-based like `features.ts:80-93`: booleans need no
|
|
509
|
-
// escaping, and line editing preserves the user's comments and formatting
|
|
510
|
-
// exactly where a re-serialize would not.
|
|
511
|
-
// ---------------------------------------------------------------------------
|
|
512
|
-
|
|
513
|
-
const TABLE_HEADER = /^\s*\[/;
|
|
514
|
-
|
|
515
|
-
/** Lines of the root scope: everything before the first `[table]` header. */
|
|
516
|
-
function rootLines(content: string): string[] {
|
|
517
|
-
const lines = content.split("\n");
|
|
518
|
-
const first = lines.findIndex(l => TABLE_HEADER.test(l));
|
|
519
|
-
return first === -1 ? lines : lines.slice(0, first);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/** Lines of `[header]`'s body, up to the next table header. */
|
|
523
|
-
function tableLines(content: string, header: string): string[] | null {
|
|
524
|
-
const lines = content.split("\n");
|
|
525
|
-
const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
526
|
-
const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
|
|
527
|
-
if (start === -1) return null;
|
|
528
|
-
const rest = lines.slice(start + 1);
|
|
529
|
-
const end = rest.findIndex(l => TABLE_HEADER.test(l));
|
|
530
|
-
return end === -1 ? rest : rest.slice(0, end);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function boolInLines(lines: string[], key: string): boolean | null {
|
|
534
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
535
|
-
const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*(true|false)\\s*(?:#.*)?$`);
|
|
536
|
-
for (const line of lines) {
|
|
537
|
-
const m = pattern.exec(line);
|
|
538
|
-
if (m) return m[1] === "true";
|
|
539
|
-
}
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// ---------------------------------------------------------------------------
|
|
544
|
-
// Ownership of the generated projection.
|
|
545
|
-
//
|
|
546
|
-
// Canonical physical form, always exactly two lines at the top of the document:
|
|
547
|
-
//
|
|
548
|
-
// # Auto-injected by opencodex
|
|
549
|
-
// developer_instructions = "<single-line basic string>"
|
|
550
|
-
//
|
|
551
|
-
// Replacement is "find the marker, replace the next line" — never a span search.
|
|
552
|
-
// Adjacency mirrors `injected-marker.ts:53-60`, tightened by a shape check.
|
|
553
|
-
// ---------------------------------------------------------------------------
|
|
554
|
-
|
|
555
|
-
const DEV_INSTRUCTIONS_KEY = "developer_instructions";
|
|
556
|
-
const CANONICAL_LINE = /^developer_instructions = "(?:[^"\\]|\\.)*"$/;
|
|
557
|
-
const ANY_DEV_INSTRUCTIONS = /^\s*(?:developer_instructions|"developer_instructions"|'developer_instructions')\s*=/;
|
|
558
|
-
|
|
559
|
-
export type Ownership =
|
|
560
|
-
/** no such key anywhere in the root scope */
|
|
561
|
-
| { state: "absent" }
|
|
562
|
-
/** marker-adjacent and canonically shaped: ours to rewrite */
|
|
563
|
-
| { state: "owned"; line: number; literal: string }
|
|
564
|
-
/** marker-adjacent but reshaped: refuse, offer repair */
|
|
565
|
-
| { state: "owned-malformed"; line: number; raw: string }
|
|
566
|
-
/** no marker: externally authored, refuse and offer adoption */
|
|
567
|
-
| { state: "external"; line: number; raw: string };
|
|
568
|
-
|
|
569
|
-
export function inspectOwnership(configBytes: string | null): Ownership {
|
|
570
|
-
if (configBytes === null) return { state: "absent" };
|
|
571
|
-
const lines = rootLines(configBytes);
|
|
572
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
573
|
-
const raw = lines[i]!;
|
|
574
|
-
if (!ANY_DEV_INSTRUCTIONS.test(raw)) continue;
|
|
575
|
-
const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER);
|
|
576
|
-
if (!marked) return { state: "external", line: i + 1, raw };
|
|
577
|
-
if (!CANONICAL_LINE.test(raw)) return { state: "owned-malformed", line: i + 1, raw };
|
|
578
|
-
const literal = raw.slice(`${DEV_INSTRUCTIONS_KEY} = `.length);
|
|
579
|
-
return { state: "owned", line: i + 1, literal };
|
|
580
|
-
}
|
|
581
|
-
return { state: "absent" };
|
|
582
|
-
}
|
|
583
236
|
|
|
584
237
|
// ---------------------------------------------------------------------------
|
|
585
238
|
// Store — the single source of truth for custom layers.
|
|
@@ -996,151 +649,6 @@ export type WriteResult =
|
|
|
996
649
|
| { ok: true; changed: boolean; snapshot: PromptLayerSnapshot }
|
|
997
650
|
| { ok: false; error: WriteError; detail?: string };
|
|
998
651
|
|
|
999
|
-
/** Line editing, not re-serialization: the user's comments and layout survive. */
|
|
1000
|
-
function dominantEol(content: string): "\r\n" | "\n" {
|
|
1001
|
-
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
1002
|
-
if (crlf === 0) return "\n";
|
|
1003
|
-
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
1004
|
-
return crlf >= bareLf ? "\r\n" : "\n";
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
function splitLines(content: string): string[] {
|
|
1008
|
-
return content.replace(/\r\n/g, "\n").split("\n");
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
/**
|
|
1012
|
-
* A leading UTF-8 BOM, split off so line editing never steps over it.
|
|
1013
|
-
*
|
|
1014
|
-
* Codex reads config.toml with Rust `toml_edit`, which accepts a BOM at byte 0 and
|
|
1015
|
-
* nowhere else. Inserting the generated block at line index 0 pushed the BOM down
|
|
1016
|
-
* to byte 58, the write reported success because our own byte comparison matched
|
|
1017
|
-
* what we intended to write, and the next parse failed with
|
|
1018
|
-
* "Expected a key but found (0xEF)" — a config file the user could no longer load,
|
|
1019
|
-
* produced by a write that told them it worked.
|
|
1020
|
-
*
|
|
1021
|
-
* Editors on Windows write this byte routinely, so the file is not exotic.
|
|
1022
|
-
*/
|
|
1023
|
-
function splitBom(content: string): { bom: string; body: string } {
|
|
1024
|
-
return content.startsWith("\ufeff")
|
|
1025
|
-
? { bom: "\ufeff", body: content.slice(1) }
|
|
1026
|
-
: { bom: "", body: content };
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function joinLines(lines: string[], eol: "\r\n" | "\n"): string {
|
|
1030
|
-
const text = lines.join("\n");
|
|
1031
|
-
return eol === "\n" ? text : text.replace(/\n/g, "\r\n");
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
function firstTableIndex(lines: string[]): number {
|
|
1035
|
-
const idx = lines.findIndex(l => TABLE_HEADER.test(l));
|
|
1036
|
-
return idx === -1 ? lines.length : idx;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
/** Set a root-scope boolean, inserting above the first table when absent. */
|
|
1040
|
-
function setRootBool(content: string, key: string, value: boolean): string {
|
|
1041
|
-
const eol = dominantEol(content);
|
|
1042
|
-
const { bom, body } = splitBom(content);
|
|
1043
|
-
const lines = splitLines(body);
|
|
1044
|
-
const limit = firstTableIndex(lines);
|
|
1045
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1046
|
-
const pattern = new RegExp(`^(\\s*${escaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
1047
|
-
for (let i = 0; i < limit; i += 1) {
|
|
1048
|
-
const m = pattern.exec(lines[i]!);
|
|
1049
|
-
if (m) {
|
|
1050
|
-
lines[i] = `${m[1]}${value}${m[2]}`;
|
|
1051
|
-
return bom + joinLines(lines, eol);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
lines.splice(limit, 0, `${key} = ${value}`);
|
|
1055
|
-
return bom + joinLines(lines, eol);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
/**
|
|
1059
|
-
* Set or REMOVE a root-scope basic string. `null` removes the key.
|
|
1060
|
-
*
|
|
1061
|
-
* Removal is what selecting the default variant does, and it has to be a real deletion
|
|
1062
|
-
* rather than an empty string: `model_instructions_file = ""` is a path Codex would try
|
|
1063
|
-
* to read, not an absent setting.
|
|
1064
|
-
*/
|
|
1065
|
-
function setRootString(content: string, key: string, value: string | null): string {
|
|
1066
|
-
const eol = dominantEol(content);
|
|
1067
|
-
const { bom, body } = splitBom(content);
|
|
1068
|
-
const lines = splitLines(body);
|
|
1069
|
-
const limit = firstTableIndex(lines);
|
|
1070
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1071
|
-
const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*"[^"]*"\\s*(?:#.*)?$`);
|
|
1072
|
-
for (let i = 0; i < limit; i += 1) {
|
|
1073
|
-
if (!pattern.test(lines[i]!)) continue;
|
|
1074
|
-
if (value === null) lines.splice(i, 1);
|
|
1075
|
-
else lines[i] = `${key} = ${encodeBasicString(value)}`;
|
|
1076
|
-
return bom + joinLines(lines, eol);
|
|
1077
|
-
}
|
|
1078
|
-
if (value === null) return bom + joinLines(lines, eol);
|
|
1079
|
-
lines.splice(limit, 0, `${key} = ${encodeBasicString(value)}`);
|
|
1080
|
-
return bom + joinLines(lines, eol);
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/** Set a boolean inside `[table]`, appending the table when absent. */
|
|
1084
|
-
function setTableBool(content: string, table: string, key: string, value: boolean): string {
|
|
1085
|
-
const eol = dominantEol(content);
|
|
1086
|
-
const { bom, body } = splitBom(content);
|
|
1087
|
-
const lines = splitLines(body);
|
|
1088
|
-
const escaped = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1089
|
-
const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
|
|
1090
|
-
if (start === -1) {
|
|
1091
|
-
const tail = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
1092
|
-
lines.splice(tail, 0, `[${table}]`, `${key} = ${value}`);
|
|
1093
|
-
return bom + joinLines(lines, eol);
|
|
1094
|
-
}
|
|
1095
|
-
const keyEscaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1096
|
-
const pattern = new RegExp(`^(\\s*${keyEscaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
1097
|
-
let end = start + 1;
|
|
1098
|
-
while (end < lines.length && !TABLE_HEADER.test(lines[end]!)) end += 1;
|
|
1099
|
-
for (let i = start + 1; i < end; i += 1) {
|
|
1100
|
-
const m = pattern.exec(lines[i]!);
|
|
1101
|
-
if (m) {
|
|
1102
|
-
lines[i] = `${m[1]}${value}${m[2]}`;
|
|
1103
|
-
return bom + joinLines(lines, eol);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
lines.splice(end, 0, `${key} = ${value}`);
|
|
1107
|
-
return bom + joinLines(lines, eol);
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
/**
|
|
1111
|
-
* Replace, insert, or remove the generated two-line block. Canonical form is
|
|
1112
|
-
* marker + assignment at the top of the document; replacement is "find the
|
|
1113
|
-
* marker, replace the next line" rather than a span search.
|
|
1114
|
-
*/
|
|
1115
|
-
function setProjection(content: string | null, projection: string | null): string {
|
|
1116
|
-
const base = content ?? "";
|
|
1117
|
-
const eol = dominantEol(base);
|
|
1118
|
-
// The BOM is held aside for the whole edit. This is the function that produced
|
|
1119
|
-
// the corruption: the insert below is at index 0, which put the marker line
|
|
1120
|
-
// ahead of a byte that is only legal at byte 0.
|
|
1121
|
-
const { bom, body } = splitBom(base);
|
|
1122
|
-
const lines = splitLines(body);
|
|
1123
|
-
const limit = firstTableIndex(lines);
|
|
1124
|
-
|
|
1125
|
-
let markerAt = -1;
|
|
1126
|
-
for (let i = 0; i < limit; i += 1) {
|
|
1127
|
-
if (i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER) && ANY_DEV_INSTRUCTIONS.test(lines[i]!)) {
|
|
1128
|
-
markerAt = i - 1;
|
|
1129
|
-
break;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
if (markerAt !== -1) {
|
|
1134
|
-
if (projection === null) lines.splice(markerAt, 2);
|
|
1135
|
-
else lines[markerAt + 1] = `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`;
|
|
1136
|
-
return bom + joinLines(lines, eol);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
if (projection === null) return bom + joinLines(lines, eol);
|
|
1140
|
-
lines.splice(0, 0, OCX_SECTION_MARKER, `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`);
|
|
1141
|
-
return bom + joinLines(lines, eol);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
652
|
function serializeStore(layers: readonly CustomLayer[]): string {
|
|
1145
653
|
return `${JSON.stringify({ layers }, null, 2)}\n`;
|
|
1146
654
|
}
|
|
@@ -1564,20 +1072,6 @@ export function adoptDeveloperInstructions(revision: string, opts?: Paths): Writ
|
|
|
1564
1072
|
});
|
|
1565
1073
|
}
|
|
1566
1074
|
|
|
1567
|
-
/** Remove an unowned or reshaped `developer_instructions` from the root scope. */
|
|
1568
|
-
function removeUnownedProjection(content: string): string {
|
|
1569
|
-
const eol = dominantEol(content);
|
|
1570
|
-
const lines = splitLines(content);
|
|
1571
|
-
const limit = firstTableIndex(lines);
|
|
1572
|
-
for (let i = 0; i < limit; i += 1) {
|
|
1573
|
-
if (!ANY_DEV_INSTRUCTIONS.test(lines[i]!)) continue;
|
|
1574
|
-
const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER);
|
|
1575
|
-
lines.splice(marked ? i - 1 : i, marked ? 2 : 1);
|
|
1576
|
-
return joinLines(lines, eol);
|
|
1577
|
-
}
|
|
1578
|
-
return joinLines(lines, eol);
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
1075
|
// ---------------------------------------------------------------------------
|
|
1582
1076
|
// Salvage — the store is gone while a live projection remains.
|
|
1583
1077
|
//
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Shared bookkeeping leaf; lifecycle cleanup must not load warmup/credential owners. */
|
|
2
|
+
/** Completed/due markers use epoch milliseconds; persisted legacy markers may use seconds. */
|
|
3
|
+
export type CodexQuotaAutoRefreshWindows = { fiveHour?: number; weekly?: number };
|
|
4
|
+
|
|
5
|
+
export const completedByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
|
|
6
|
+
export const retryAfterByAccount = new Map<string, number>();
|
|
7
|
+
|
|
8
|
+
export function forgetCodexQuotaAutoRefreshAccount(accountId: string): void {
|
|
9
|
+
completedByAccount.delete(accountId);
|
|
10
|
+
retryAfterByAccount.delete(accountId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resetCodexQuotaAutoRefreshStateForTests(): void {
|
|
14
|
+
completedByAccount.clear();
|
|
15
|
+
retryAfterByAccount.clear();
|
|
16
|
+
}
|