@coseung2/opencodex 2.8.0-cs.1 → 2.8.0-cs.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/bin/ocx-notch.mjs +27 -1
- package/bin/ocx.mjs +0 -0
- package/gui/dist/assets/{index-CAnnes06.js → index-BYDeFyGN.js} +3 -3
- package/gui/dist/assets/{index-OY43ubAq.css → index-CxisOo-q.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/packages/ocx-notch/README.md +7 -3
- package/src/adapters/kiro-constants.ts +1 -1
- package/src/adapters/kiro.ts +29 -6
- package/src/cli/index.ts +67 -29
- package/src/cli/status.ts +4 -3
- package/src/config.ts +8 -3
- package/src/oauth/account-pause.ts +31 -0
- package/src/oauth/index.ts +11 -5
- package/src/oauth/kiro-credentials.ts +18 -1
- package/src/oauth/store.ts +25 -2
- package/src/providers/quota.ts +312 -18
- package/src/providers/registry.ts +1 -0
- package/src/responses/parser.ts +9 -1
- package/src/responses/state.ts +19 -2
- package/src/server/lifecycle.ts +23 -0
- package/src/server/management/oauth-account-routes.ts +51 -2
- package/src/server/memory-watchdog.ts +72 -7
- package/src/server/proxy-liveness.ts +5 -1
- package/src/service.ts +14 -7
- package/src/types.ts +9 -0
- package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory watchdog (#314 WP3 / #509) —
|
|
3
|
-
* native-memory
|
|
2
|
+
* Memory watchdog (#314 WP3 / #509) — observability plus a bounded Windows
|
|
3
|
+
* native-memory reclamation attempt for the Bun fetch-buffer retention shape.
|
|
4
4
|
*
|
|
5
5
|
* Samples process.memoryUsage() on an unref'd interval into a bounded ring and
|
|
6
|
-
* logs ONE rate-limited warning when observed memory crosses the threshold.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* instance is a module-level singleton so
|
|
10
|
-
* snapshot without threading server state
|
|
6
|
+
* logs ONE rate-limited warning when observed memory crosses the threshold. On
|
|
7
|
+
* Windows, a supported Bun.gc(true) is attempted once per pressure episode,
|
|
8
|
+
* only after observed memory reaches the reclamation threshold and the active
|
|
9
|
+
* turn registry is idle. The active instance is a module-level singleton so
|
|
10
|
+
* the management API can expose the snapshot without threading server state
|
|
11
|
+
* through route contexts.
|
|
11
12
|
*
|
|
12
13
|
* Privacy: samples are scalar numbers only; the warn line never interpolates
|
|
13
14
|
* paths, hostnames, or tokens.
|
|
14
15
|
*/
|
|
15
16
|
|
|
17
|
+
import { getActiveTurnCount, onActiveTurnsIdle } from "./lifecycle";
|
|
18
|
+
|
|
16
19
|
export type MemorySampleBase = {
|
|
17
20
|
/** Epoch ms. */
|
|
18
21
|
at: number;
|
|
@@ -52,6 +55,7 @@ export type MemoryWatchdog = {
|
|
|
52
55
|
|
|
53
56
|
const DEFAULT_INTERVAL_MS = 60_000;
|
|
54
57
|
const DEFAULT_WARN_THRESHOLD_BYTES = 4 * 1024 ** 3; // 4 GiB
|
|
58
|
+
const DEFAULT_RECLAIM_THRESHOLD_BYTES = 2 * 1024 ** 3; // 2 GiB, based on Windows reproduction
|
|
55
59
|
const DEFAULT_RING_SIZE = 360; // ≈6h at 60s
|
|
56
60
|
const WARN_INTERVAL_MS = 30 * 60_000;
|
|
57
61
|
const DOCS_URL = "https://opencodex.me/troubleshooting/windows-memory/";
|
|
@@ -71,6 +75,24 @@ export function observedMemoryCounter(sample: Pick<MemorySampleBase, "rss" | "ex
|
|
|
71
75
|
return { observedBytes: best.bytes, observedMetric: best.metric };
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
export type MemoryReclamationPolicyInput = {
|
|
79
|
+
observedBytes: number;
|
|
80
|
+
pressureThresholdBytes: number;
|
|
81
|
+
activeTurns: number;
|
|
82
|
+
platform: NodeJS.Platform;
|
|
83
|
+
gcSupported: boolean;
|
|
84
|
+
pressureLatched: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Pure policy gate for the pressure-triggered, idle-only reclamation path. */
|
|
88
|
+
export function shouldAttemptMemoryReclamation(input: MemoryReclamationPolicyInput): boolean {
|
|
89
|
+
return input.platform === "win32"
|
|
90
|
+
&& input.gcSupported
|
|
91
|
+
&& input.activeTurns === 0
|
|
92
|
+
&& input.observedBytes >= input.pressureThresholdBytes
|
|
93
|
+
&& !input.pressureLatched;
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
/** The running watchdog, if any — read by /api/system/memory. */
|
|
75
97
|
export function getActiveMemoryWatchdog(): MemoryWatchdog | null {
|
|
76
98
|
return active;
|
|
@@ -93,6 +115,12 @@ function normalizeSample(sample: MemorySampleBase): MemorySample {
|
|
|
93
115
|
return { ...sample, ...observedMemoryCounter(sample) };
|
|
94
116
|
}
|
|
95
117
|
|
|
118
|
+
function defaultGarbageCollector(platform: NodeJS.Platform): (() => void) | null {
|
|
119
|
+
if (platform !== "win32") return null;
|
|
120
|
+
if (typeof Bun === "undefined" || typeof Bun.gc !== "function") return null;
|
|
121
|
+
return () => Bun.gc(true);
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
/**
|
|
97
125
|
* Start (or replace) the process-wide memory watchdog. Idempotent: a previous
|
|
98
126
|
* active instance is stopped first, so repeated startServer() calls in tests
|
|
@@ -102,23 +130,57 @@ function normalizeSample(sample: MemorySampleBase): MemorySample {
|
|
|
102
130
|
export function startMemoryWatchdog(opts?: {
|
|
103
131
|
intervalMs?: number;
|
|
104
132
|
warnThresholdBytes?: number;
|
|
133
|
+
pressureThresholdBytes?: number;
|
|
105
134
|
ringSize?: number;
|
|
106
135
|
now?: () => number;
|
|
107
136
|
sample?: () => MemorySampleBase;
|
|
108
137
|
warn?: (msg: string) => void;
|
|
138
|
+
platform?: NodeJS.Platform;
|
|
139
|
+
activeTurnCount?: () => number;
|
|
140
|
+
gc?: (() => void) | null;
|
|
109
141
|
}): MemoryWatchdog {
|
|
110
142
|
active?.stop();
|
|
111
143
|
const intervalMs = opts?.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
112
144
|
const warnThresholdBytes = opts?.warnThresholdBytes ?? DEFAULT_WARN_THRESHOLD_BYTES;
|
|
145
|
+
const pressureThresholdBytes = opts?.pressureThresholdBytes ?? DEFAULT_RECLAIM_THRESHOLD_BYTES;
|
|
113
146
|
const ringSize = opts?.ringSize ?? DEFAULT_RING_SIZE;
|
|
114
147
|
const now = opts?.now ?? Date.now;
|
|
115
148
|
const sample = opts?.sample ?? (() => defaultSample(now));
|
|
116
149
|
const warn = opts?.warn ?? ((msg: string) => console.warn(msg));
|
|
150
|
+
const platform = opts?.platform ?? process.platform;
|
|
151
|
+
const activeTurnCount = opts?.activeTurnCount ?? getActiveTurnCount;
|
|
152
|
+
const collectGarbage = opts !== undefined && "gc" in opts
|
|
153
|
+
? opts.gc ?? null
|
|
154
|
+
: defaultGarbageCollector(platform);
|
|
117
155
|
|
|
118
156
|
const samples: MemorySample[] = [];
|
|
119
157
|
let lastWarnAt: number | null = null;
|
|
120
158
|
let observedBytes = 0;
|
|
121
159
|
let observedMetric: MemoryMetric = "rss";
|
|
160
|
+
let pressureLatched = false;
|
|
161
|
+
|
|
162
|
+
const reclaimIfIdle = (): void => {
|
|
163
|
+
if (!shouldAttemptMemoryReclamation({
|
|
164
|
+
observedBytes,
|
|
165
|
+
pressureThresholdBytes,
|
|
166
|
+
activeTurns: activeTurnCount(),
|
|
167
|
+
platform,
|
|
168
|
+
gcSupported: collectGarbage !== null,
|
|
169
|
+
pressureLatched,
|
|
170
|
+
})) return;
|
|
171
|
+
// Latch before calling Bun.gc so a sustained high sample cannot turn into
|
|
172
|
+
// a GC on every timer tick or every later turn completion.
|
|
173
|
+
pressureLatched = true;
|
|
174
|
+
try {
|
|
175
|
+
collectGarbage?.();
|
|
176
|
+
} catch {
|
|
177
|
+
// Reclamation is a best-effort runtime hint. A collector failure must not
|
|
178
|
+
// escape the watchdog timer and terminate the proxy.
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const removeIdleListener = platform === "win32" && collectGarbage !== null
|
|
182
|
+
? onActiveTurnsIdle(reclaimIfIdle)
|
|
183
|
+
: () => {};
|
|
122
184
|
|
|
123
185
|
const tick = () => {
|
|
124
186
|
let s: MemorySample;
|
|
@@ -131,6 +193,8 @@ export function startMemoryWatchdog(opts?: {
|
|
|
131
193
|
if (samples.length > ringSize) samples.splice(0, samples.length - ringSize);
|
|
132
194
|
observedBytes = s.observedBytes;
|
|
133
195
|
observedMetric = s.observedMetric;
|
|
196
|
+
if (observedBytes < pressureThresholdBytes) pressureLatched = false;
|
|
197
|
+
reclaimIfIdle();
|
|
134
198
|
if (s.observedBytes >= warnThresholdBytes && (lastWarnAt === null || now() - lastWarnAt >= WARN_INTERVAL_MS)) {
|
|
135
199
|
lastWarnAt = now();
|
|
136
200
|
const observedMb = Math.round(s.observedBytes / (1024 * 1024));
|
|
@@ -145,6 +209,7 @@ export function startMemoryWatchdog(opts?: {
|
|
|
145
209
|
const instance: MemoryWatchdog = {
|
|
146
210
|
stop() {
|
|
147
211
|
clearInterval(timer);
|
|
212
|
+
removeIdleListener();
|
|
148
213
|
if (active === instance) active = null;
|
|
149
214
|
},
|
|
150
215
|
snapshot() {
|
|
@@ -68,7 +68,11 @@ export interface LiveProxy {
|
|
|
68
68
|
*/
|
|
69
69
|
export function probeHostname(hostname: string | undefined): string {
|
|
70
70
|
const trimmed = (hostname ?? "").trim();
|
|
71
|
-
if (!trimmed || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]")
|
|
71
|
+
if (!trimmed || /^localhost$/i.test(trimmed) || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]") {
|
|
72
|
+
// startServer canonicalizes localhost to IPv4 loopback because Codex's injected URL does
|
|
73
|
+
// the same. Keep liveness probes on that exact address (Windows otherwise prefers ::1).
|
|
74
|
+
return "127.0.0.1";
|
|
75
|
+
}
|
|
72
76
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
|
|
73
77
|
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
74
78
|
}
|
package/src/service.ts
CHANGED
|
@@ -2326,8 +2326,8 @@ export function diagnoseService(): ServiceDiagnostic {
|
|
|
2326
2326
|
return { supported: false, installed: false, enabled: false, running: false, viable: false, startable: false, stale: false, conflict: false, backend: null, summary: `unsupported on ${process.platform}` };
|
|
2327
2327
|
}
|
|
2328
2328
|
|
|
2329
|
-
export function serviceStatusSummary(): string {
|
|
2330
|
-
return
|
|
2329
|
+
export function serviceStatusSummary(service: ServiceDiagnostic = diagnoseService()): string {
|
|
2330
|
+
return `${service.summary}; log: ${serviceLogPath()}`;
|
|
2331
2331
|
}
|
|
2332
2332
|
|
|
2333
2333
|
/**
|
|
@@ -2394,6 +2394,16 @@ export interface ParsedServiceArgs {
|
|
|
2394
2394
|
invalid: string[];
|
|
2395
2395
|
}
|
|
2396
2396
|
|
|
2397
|
+
const SERVICE_SUBCOMMANDS = new Set(["install", "repair", "start", "stop", "status", "uninstall", "remove"]);
|
|
2398
|
+
|
|
2399
|
+
function exitServiceUsage(): never {
|
|
2400
|
+
console.error("Usage: ocx service [install|repair|start|stop|status|uninstall|remove] [--native|--scheduler]");
|
|
2401
|
+
console.error(" With no subcommand, installs/updates and starts the background service.");
|
|
2402
|
+
console.error(" repair: refresh assets and restart an already-installed service (no admin re-prompt).");
|
|
2403
|
+
console.error(" --native (Windows only): register a real SCM service via WinSW instead of Task Scheduler.");
|
|
2404
|
+
process.exit(1);
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2397
2407
|
/**
|
|
2398
2408
|
* `ocx service [sub] [--native|--scheduler]`. The first non-flag token is the
|
|
2399
2409
|
* subcommand; backend flags are only meaningful for `install` (validated by the caller).
|
|
@@ -2421,6 +2431,7 @@ export function parseServiceArgs(args: string[]): ParsedServiceArgs {
|
|
|
2421
2431
|
export async function serviceCommand(...args: (string | undefined)[]): Promise<void> {
|
|
2422
2432
|
const parsed = parseServiceArgs(args.filter((a): a is string => Boolean(a)));
|
|
2423
2433
|
const command = parsed.sub;
|
|
2434
|
+
if (!SERVICE_SUBCOMMANDS.has(command)) exitServiceUsage();
|
|
2424
2435
|
if (parsed.invalid.length > 0) {
|
|
2425
2436
|
console.error(`Unknown service option: ${parsed.invalid.join(" ")}`);
|
|
2426
2437
|
process.exit(1);
|
|
@@ -2545,10 +2556,6 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
2545
2556
|
console.log("✅ service uninstalled.");
|
|
2546
2557
|
break;
|
|
2547
2558
|
default:
|
|
2548
|
-
|
|
2549
|
-
console.error(" With no subcommand, installs/updates and starts the background service.");
|
|
2550
|
-
console.error(" repair: refresh assets and restart an already-installed service (no admin re-prompt).");
|
|
2551
|
-
console.error(" --native (Windows only): register a real SCM service via WinSW instead of Task Scheduler.");
|
|
2552
|
-
process.exit(1);
|
|
2559
|
+
exitServiceUsage();
|
|
2553
2560
|
}
|
|
2554
2561
|
}
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface OcxParsedRequest {
|
|
|
9
9
|
_rawBody?: unknown;
|
|
10
10
|
/** Number of leading raw input items restored from local previous_response_id state. */
|
|
11
11
|
_replayPrefixLen?: number;
|
|
12
|
+
/** Number of parsed messages produced by that restored raw-input prefix. */
|
|
13
|
+
_replayMessagePrefixLen?: number;
|
|
12
14
|
/** True when the proxy expanded a previous_response_id request into a full input replay. */
|
|
13
15
|
_previousResponseInputExpanded?: boolean;
|
|
14
16
|
/** Provider-private stable Cursor conversation id resolved from the Responses previous_response_id chain. */
|
|
@@ -722,6 +724,13 @@ export interface OcxConfig {
|
|
|
722
724
|
codexAccounts?: CodexAccount[];
|
|
723
725
|
/** Account ids administratively excluded from future pool selection until resumed. */
|
|
724
726
|
pausedCodexAccountIds?: string[];
|
|
727
|
+
/**
|
|
728
|
+
* OAuth provider account ids administratively excluded from pool selection until
|
|
729
|
+
* resumed, keyed by provider name (for example `kiro`, `anthropic`, `xai`).
|
|
730
|
+
* A paused account cannot be activated; pausing the active account promotes the
|
|
731
|
+
* first non-paused account (or clears the active selection when none remain).
|
|
732
|
+
*/
|
|
733
|
+
pausedOauthAccountIds?: Record<string, string[]>;
|
|
725
734
|
/**
|
|
726
735
|
* Public model-selector namespaces bound to one Codex account. Values are stored account ids;
|
|
727
736
|
* `"@main"` selects the Codex Desktop/main auth.json account. Account display aliases
|
|
Binary file
|