@bitkyc08/opencodex 2.7.41 → 2.7.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
- package/gui/dist/assets/index-DfVGuN88.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/kiro-constants.ts +6 -2
- package/src/adapters/kiro-retry.ts +175 -10
- package/src/adapters/kiro.ts +172 -85
- package/src/adapters/mimo-free.ts +1 -0
- package/src/adapters/openai-chat.ts +30 -4
- package/src/adapters/openai-responses.ts +90 -12
- package/src/bridge.ts +91 -43
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +29 -2
- package/src/cli/access.ts +108 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account.ts +9 -1
- package/src/cli/agent.ts +184 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +20 -8
- package/src/cli/doctor.ts +45 -8
- package/src/cli/help.ts +65 -13
- package/src/cli/index.ts +108 -7
- package/src/cli/integrations.ts +142 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +9 -10
- package/src/cli/observe.ts +117 -0
- package/src/cli/provider-runtime.ts +152 -0
- package/src/cli/provider.ts +23 -1
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +3 -3
- package/src/cli/status.ts +17 -0
- package/src/cli/system-command.ts +112 -0
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/aggregation.ts +113 -18
- package/src/codex/catalog/provider-fetch.ts +24 -13
- package/src/codex/catalog/sync.ts +20 -8
- package/src/codex/catalog.ts +2 -1
- package/src/codex/refresh.ts +10 -3
- package/src/codex/routing.ts +21 -32
- package/src/codex/sync.ts +17 -0
- package/src/config.ts +48 -0
- package/src/generated/jawcode-model-metadata.ts +2 -1
- package/src/grok/inject.ts +184 -4
- package/src/grok/status.ts +33 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/windows-elevation.ts +627 -0
- package/src/providers/openai-sidecar.ts +46 -2
- package/src/providers/registry.ts +52 -0
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/claude-messages.ts +20 -1
- package/src/server/images.ts +14 -7
- package/src/server/management/agent-settings-routes.ts +10 -4
- package/src/server/management/combo-routes.ts +0 -1
- package/src/server/management/config-routes.ts +0 -1
- package/src/server/management/logs-usage-routes.ts +94 -0
- package/src/server/management/model-routes.ts +0 -1
- package/src/server/management/oauth-account-routes.ts +0 -1
- package/src/server/management/provider-routes.ts +0 -1
- package/src/server/management/shared.ts +0 -1
- package/src/server/management/system-routes.ts +27 -15
- package/src/server/management-api.ts +0 -1
- package/src/server/memory-watchdog.ts +54 -10
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +122 -2
- package/src/server/responses/core.ts +76 -13
- package/src/server/responses/passthrough-error.ts +38 -13
- package/src/server/startup-action-control.ts +266 -15
- package/src/service.ts +512 -3
- package/src/storage/cleanup.ts +1538 -0
- package/src/storage/scanner.ts +4 -1
- package/src/types.ts +16 -0
- package/src/update/job.ts +229 -25
- package/src/usage/log.ts +39 -0
- package/src/web-search/loop.ts +8 -1
- package/gui/dist/assets/index-B2J4t3te.css +0 -1
- package/gui/dist/assets/index-BmvM6wRb.js +0 -65
|
@@ -180,6 +180,19 @@ const THINKING_TOGGLE_MAP: Record<string, string> = {
|
|
|
180
180
|
const OPENCODE_GO_THINKING_TOGGLE_MODELS = [
|
|
181
181
|
"mimo-v2.5", "mimo-v2.5-pro", "mimo-v2-omni", "mimo-v2-pro", "glm-5", "glm-5.1",
|
|
182
182
|
];
|
|
183
|
+
/**
|
|
184
|
+
* Zhipu's domestic BigModel platform. Text families first, then the vision member: modalities are
|
|
185
|
+
* declared per model because `noVisionModels` means the opposite of "text only" here — it routes
|
|
186
|
+
* images through the proxy's vision sidecar (src/codex/catalog/provider-fetch.ts), a claim nobody
|
|
187
|
+
* has verified for BigModel-hosted GLM.
|
|
188
|
+
*/
|
|
189
|
+
const ZHIPU_BIGMODEL_TEXT_MODELS = ["glm-4.6", "glm-4.7", "glm-4.7-flash", "glm-5", "glm-5.1"];
|
|
190
|
+
const ZHIPU_BIGMODEL_MODELS = [...ZHIPU_BIGMODEL_TEXT_MODELS, "glm-4.6v"];
|
|
191
|
+
const ZHIPU_BIGMODEL_INPUT_MODALITIES: Record<string, string[]> = {
|
|
192
|
+
...Object.fromEntries(ZHIPU_BIGMODEL_TEXT_MODELS.map(id => [id, ["text"]])),
|
|
193
|
+
"glm-4.6v": ["text", "image"],
|
|
194
|
+
};
|
|
195
|
+
const ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS = ["glm-4.6", "glm-4.7", "glm-5", "glm-5.1"];
|
|
183
196
|
const THINKING_BUDGET_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
184
197
|
const THINKING_BUDGET_MODELS = [
|
|
185
198
|
"qwen3.5-397b", "qwen3.6-35b",
|
|
@@ -785,6 +798,45 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
785
798
|
modelReasoningEfforts: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_EFFORTS])),
|
|
786
799
|
preserveReasoningContentModels: ZAI_GLM_52_MODELS,
|
|
787
800
|
},
|
|
801
|
+
// Zhipu's domestic BigModel platform: OpenAI-compatible pay-as-you-go on open.bigmodel.cn — a
|
|
802
|
+
// different host and billing product from the `zai` coding-plan subscription above.
|
|
803
|
+
// The id is deliberately NOT `glm` or `glm-cn`: both are already bound in FREE_PROVIDER_DIRECTORY
|
|
804
|
+
// (to api.z.ai and to the BigModel *coding* path), and routedProviderConfig() canonicalizes a
|
|
805
|
+
// saved provider onto the registry baseUrl — reusing either id would silently retarget an
|
|
806
|
+
// existing config's endpoint and send its API key to another host.
|
|
807
|
+
// Evidence: docs.bigmodel.cn/api-reference (OpenAI-compatible chat completions),
|
|
808
|
+
// docs.bigmodel.cn/cn/guide/models/text/glm-4.6 (thinking: {type: enabled|disabled}).
|
|
809
|
+
// Originally proposed in #536 by @Lucinegogo.
|
|
810
|
+
{
|
|
811
|
+
id: "zhipu-bigmodel",
|
|
812
|
+
label: "Zhipu AI — BigModel",
|
|
813
|
+
baseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
814
|
+
adapter: "openai-chat",
|
|
815
|
+
authKind: "key",
|
|
816
|
+
dashboardUrl: "https://bigmodel.cn/console/usercenter/apikeys",
|
|
817
|
+
defaultModel: "glm-4.6",
|
|
818
|
+
models: ZHIPU_BIGMODEL_MODELS,
|
|
819
|
+
// The GLM families here are the same ones the `zai` metadata bundle already describes, so the
|
|
820
|
+
// bundle owns context windows and modalities for the whole list instead of a hand-copied table.
|
|
821
|
+
jawcodeBundle: "zai",
|
|
822
|
+
// Declared explicitly for the default model so its window survives a bundle-lookup miss:
|
|
823
|
+
// without it, catalog normalization falls back to a generic 128k and compacts ~76,800 early.
|
|
824
|
+
modelContextWindows: { "glm-4.6": 204_800 },
|
|
825
|
+
modelInputModalities: ZHIPU_BIGMODEL_INPUT_MODALITIES,
|
|
826
|
+
// GLM exposes a binary thinking knob, not an effort ladder: the adapter emits
|
|
827
|
+
// `thinking: {type}` for these ids and would otherwise send a rejected reasoning_effort.
|
|
828
|
+
thinkingToggleModels: ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS,
|
|
829
|
+
modelReasoningEfforts: Object.fromEntries(
|
|
830
|
+
ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_EFFORTS]),
|
|
831
|
+
),
|
|
832
|
+
modelReasoningEffortMap: Object.fromEntries(
|
|
833
|
+
ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP]),
|
|
834
|
+
),
|
|
835
|
+
preserveReasoningContentModels: ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS,
|
|
836
|
+
// No liveModels: GET /api/paas/v4/models has not been observed to answer on this host, and a
|
|
837
|
+
// false live claim yields an empty picker at runtime. Flip it on once someone verifies it.
|
|
838
|
+
note: "Domestic BigModel pay-as-you-go endpoint (open.bigmodel.cn)",
|
|
839
|
+
},
|
|
788
840
|
{ id: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://nano-gpt.com/api" },
|
|
789
841
|
{ id: "synthetic", label: "Synthetic", baseUrl: "https://api.synthetic.new/openai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://synthetic.new" },
|
|
790
842
|
// SiliconFlow publishes an OpenAI-compatible chat endpoint and a dynamic model catalog. Do not
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
positiveIntegerRecordConfigError,
|
|
9
9
|
providerBaseUrlConfigError,
|
|
10
10
|
providerHeadersConfigError,
|
|
11
|
+
reasoningSummaryDeliveryRecordConfigError,
|
|
11
12
|
} from "../config";
|
|
12
13
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
13
14
|
import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers/registry";
|
|
@@ -235,6 +236,11 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
235
236
|
if (maxInputError) return `provider ${name} ${maxInputError}`;
|
|
236
237
|
const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
|
|
237
238
|
if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
|
|
239
|
+
const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
|
|
240
|
+
raw.modelReasoningSummaryDelivery,
|
|
241
|
+
raw.modelSupportsReasoningSummaries,
|
|
242
|
+
);
|
|
243
|
+
if (reasoningSummaryDeliveryError) return `provider ${name} ${reasoningSummaryDeliveryError}`;
|
|
238
244
|
const modelAdaptersError = modelAdapterRecordConfigError(raw.modelAdapters, "modelAdapters", name, typed);
|
|
239
245
|
if (modelAdaptersError) return `provider ${name} ${modelAdaptersError}`;
|
|
240
246
|
const defaultMaxOutputError = positiveIntegerConfigError(raw.defaultMaxOutputTokens, "defaultMaxOutputTokens");
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "../chat/outbound";
|
|
17
17
|
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
18
18
|
import { redactSecretString } from "../lib/redact";
|
|
19
|
+
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
19
20
|
import { estimateTokens } from "../lib/token-estimate";
|
|
20
21
|
import { routeModel } from "../router";
|
|
21
22
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
@@ -180,7 +181,11 @@ export async function handleChatCompletions(
|
|
|
180
181
|
if (text) message = `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`;
|
|
181
182
|
}
|
|
182
183
|
} catch { /* keep fallback */ }
|
|
183
|
-
const retryAfter =
|
|
184
|
+
const retryAfter = resolveClientRetryAfter({
|
|
185
|
+
status: upstream.status,
|
|
186
|
+
message,
|
|
187
|
+
upstreamRetryAfter: upstream.headers.get("retry-after"),
|
|
188
|
+
});
|
|
184
189
|
const classified = classifyError(
|
|
185
190
|
upstream.status,
|
|
186
191
|
upstreamType
|
|
@@ -15,6 +15,7 @@ import { recordDesktopRequest } from "../claude/desktop-health";
|
|
|
15
15
|
import { stripOneMillionMarker } from "../claude/context-windows";
|
|
16
16
|
import { captureClaudeInbound } from "../claude/inbound-debug";
|
|
17
17
|
import { isTransientUpstreamStatus } from "../lib/upstream-retry";
|
|
18
|
+
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
18
19
|
import {
|
|
19
20
|
anthropicErrorBody,
|
|
20
21
|
anthropicErrorResponse,
|
|
@@ -29,6 +30,7 @@ import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
|
29
30
|
import type { OcxConfig } from "../types";
|
|
30
31
|
import { readJsonRequestBody } from "./request-decompress";
|
|
31
32
|
import { addFinalRequestLog, httpStatusForTerminalStatus, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log";
|
|
33
|
+
import { conversationIdFromClaudeMetadata } from "./request-log-conversation";
|
|
32
34
|
import { responseWithDeferredRequestLog } from "./relay";
|
|
33
35
|
import { handleResponses } from "./responses";
|
|
34
36
|
|
|
@@ -555,6 +557,13 @@ export async function handleClaudeMessages(
|
|
|
555
557
|
logCtx.surface = "claude-desktop";
|
|
556
558
|
recordDesktopRequest();
|
|
557
559
|
}
|
|
560
|
+
// Correlate before native passthrough so Anthropic-credential turns still filter/total (#330 / #522).
|
|
561
|
+
if (isRec(anthropicBody)) {
|
|
562
|
+
const claudeConversationId = conversationIdFromClaudeMetadata(
|
|
563
|
+
isRec(anthropicBody.metadata) ? anthropicBody.metadata : undefined,
|
|
564
|
+
);
|
|
565
|
+
if (claudeConversationId) logCtx.conversationId = claudeConversationId;
|
|
566
|
+
}
|
|
558
567
|
if (isRec(anthropicBody) && wantsNativePassthrough(req, config, anthropicBody.model)) {
|
|
559
568
|
return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
|
|
560
569
|
}
|
|
@@ -689,12 +698,22 @@ export async function handleClaudeMessages(
|
|
|
689
698
|
if (text) message = `upstream error (${response.status}): ${text.slice(0, 400)}`;
|
|
690
699
|
}
|
|
691
700
|
} catch { /* keep fallback message */ }
|
|
692
|
-
const
|
|
701
|
+
const upstreamRetryAfter = response.headers.get("retry-after");
|
|
702
|
+
const retryAfter = resolveClientRetryAfter({
|
|
703
|
+
status: response.status,
|
|
704
|
+
message,
|
|
705
|
+
upstreamRetryAfter,
|
|
706
|
+
})
|
|
707
|
+
// Instant-retry "0" is a valid client directive but rejected by cooldown parsers.
|
|
708
|
+
// Preserve it so it still wins over the transient "2" fallback (claude-529 mapping).
|
|
709
|
+
?? (upstreamRetryAfter?.trim() === "0" ? "0" : undefined);
|
|
693
710
|
// Transient upstream 5xx (already retried pre-stream, 010): reclassify as Anthropic
|
|
694
711
|
// 529 overloaded_error so the Claude Code client applies its built-in backoff retry
|
|
695
712
|
// instead of dying on a fatal api_error (260716 sol-builder incident). The request
|
|
696
713
|
// log keeps the upstream status (captured in the deferred-log closure before this
|
|
697
714
|
// rewrite): log = upstream truth, client = retry signal.
|
|
715
|
+
// Retryable 429s also get Retry-After (#507) so Codex-shaped clients and Claude Code
|
|
716
|
+
// share a backoff hint when the upstream omitted the header.
|
|
698
717
|
const transient = isTransientUpstreamStatus(response.status);
|
|
699
718
|
const outStatus = transient ? 529 : response.status;
|
|
700
719
|
const out = new Response(JSON.stringify(anthropicErrorBody(outStatus, message)), {
|
package/src/server/images.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* without a route the tool died on the /v1/* JSON-404 guard. Only an OpenAI-family upstream
|
|
8
8
|
* can serve these endpoints — routed providers (Cursor, Kiro, Gemini, …) have no image
|
|
9
9
|
* generation surface — so the handler relays the body verbatim to the ChatGPT forward
|
|
10
|
-
* provider
|
|
10
|
+
* provider, an OpenAI API-key provider, or an explicitly selected compatible custom provider and
|
|
11
|
+
* passes the response through untouched:
|
|
11
12
|
* codex's images client parses `{created, data:[{b64_json}]}` strictly and Debug-prints
|
|
12
13
|
* error bodies into the model-visible failure, so upstream errors must stay legible.
|
|
13
14
|
*/
|
|
@@ -23,7 +24,7 @@ import { formatCodexProviderForLog } from "../codex/routing";
|
|
|
23
24
|
import { signalWithTimeout } from "../lib/abort";
|
|
24
25
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
25
26
|
import type { OcxConfig } from "../types";
|
|
26
|
-
import { resolveFirstUsableOpenAiSidecar,
|
|
27
|
+
import { resolveFirstUsableOpenAiSidecar, selectImagesProvider } from "../providers/openai-sidecar";
|
|
27
28
|
import { readJsonRequestBody } from "./request-decompress";
|
|
28
29
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
29
30
|
import type { RequestLogContext } from "./request-log";
|
|
@@ -47,10 +48,17 @@ export async function handleImages(
|
|
|
47
48
|
endpoint: ImagesEndpoint,
|
|
48
49
|
logCtx: RequestLogContext,
|
|
49
50
|
): Promise<Response> {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const candidates = selectImagesProvider(config);
|
|
52
|
+
if (candidates.error) {
|
|
53
|
+
return formatErrorResponse(400, "invalid_request_error", candidates.error);
|
|
54
|
+
}
|
|
55
|
+
const explicitKeyedProvider = config.images?.provider !== undefined && candidates.keyed !== undefined;
|
|
56
|
+
if (!explicitKeyedProvider) {
|
|
57
|
+
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
54
62
|
}
|
|
55
63
|
let body: unknown;
|
|
56
64
|
try {
|
|
@@ -61,7 +69,6 @@ export async function handleImages(
|
|
|
61
69
|
const model = (body as { model?: unknown } | null)?.model;
|
|
62
70
|
if (typeof model === "string" && model) logCtx.model = model;
|
|
63
71
|
|
|
64
|
-
const candidates = selectOpenAiImagesProvider(config);
|
|
65
72
|
if (candidates.forwardCandidates.length === 0 && !candidates.keyed) {
|
|
66
73
|
// 400, not 5xx: codex retries every 5xx up to 5 total attempts, and this is a permanent
|
|
67
74
|
// configuration state that must surface on the first attempt.
|
|
@@ -33,7 +33,6 @@ import { clearThreadAccountMap } from "../../codex/routing";
|
|
|
33
33
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
35
35
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
36
|
-
import { scanStorage } from "../../storage/scanner";
|
|
37
36
|
import { readUsageEntries } from "../../usage/log";
|
|
38
37
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
39
38
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
@@ -543,16 +542,22 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
543
542
|
const { readFileSync: readFile, existsSync } = await import("node:fs");
|
|
544
543
|
const { createHash } = await import("node:crypto");
|
|
545
544
|
const { join } = await import("node:path");
|
|
546
|
-
const {
|
|
547
|
-
const libraryPath =
|
|
548
|
-
|| join(homedir(), "Library", "Application Support", "Claude-3p", "configLibrary");
|
|
545
|
+
const { resolveDesktop3pConfigLibraryPath } = await import("../../claude/desktop-3p");
|
|
546
|
+
const libraryPath = resolveDesktop3pConfigLibraryPath();
|
|
549
547
|
const metaPath = join(libraryPath, "_meta.json");
|
|
550
548
|
let onDiskFingerprint: string | null = null;
|
|
551
549
|
let configPath: string | null = null;
|
|
550
|
+
// Desktop serves ONLY the profile named by _meta.json's appliedId, so an
|
|
551
|
+
// opencodex entry that merely EXISTS does not mean Desktop is using it.
|
|
552
|
+
// null = undeterminable (no metadata / unreadable / no appliedId).
|
|
553
|
+
let activeProfile: boolean | null = null;
|
|
552
554
|
if (existsSync(metaPath)) {
|
|
553
555
|
try {
|
|
554
556
|
const meta = JSON.parse(readFile(metaPath, "utf8"));
|
|
555
557
|
const entry = Array.isArray(meta.entries) ? meta.entries.find((e: { name?: string }) => e?.name === "opencodex") : undefined;
|
|
558
|
+
const appliedId = typeof meta.appliedId === "string" ? meta.appliedId : null;
|
|
559
|
+
// A readable appliedId with no opencodex entry is a KNOWN false, not unknown.
|
|
560
|
+
activeProfile = appliedId === null ? null : (entry?.id ? appliedId === entry.id : false);
|
|
556
561
|
if (entry?.id) {
|
|
557
562
|
configPath = join(libraryPath, `${entry.id}.json`);
|
|
558
563
|
if (existsSync(configPath)) {
|
|
@@ -574,6 +579,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
574
579
|
onDiskFingerprint,
|
|
575
580
|
configPath,
|
|
576
581
|
stale,
|
|
582
|
+
activeProfile,
|
|
577
583
|
health,
|
|
578
584
|
});
|
|
579
585
|
} catch (error) {
|
|
@@ -33,7 +33,6 @@ import { clearThreadAccountMap } from "../../codex/routing";
|
|
|
33
33
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
35
35
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
36
|
-
import { scanStorage } from "../../storage/scanner";
|
|
37
36
|
import { readUsageEntries } from "../../usage/log";
|
|
38
37
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
39
38
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
@@ -34,7 +34,6 @@ import { clearThreadAccountMap } from "../../codex/routing";
|
|
|
34
34
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
35
35
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
36
36
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
37
|
-
import { scanStorage } from "../../storage/scanner";
|
|
38
37
|
import { readUsageEntries } from "../../usage/log";
|
|
39
38
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
40
39
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
@@ -34,6 +34,7 @@ import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
35
35
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
36
36
|
import { scanStorage } from "../../storage/scanner";
|
|
37
|
+
import { executeArchivedCleanup, pickWireCleanupTestHooks, previewArchivedCleanup, type CleanupMode } from "../../storage/cleanup";
|
|
37
38
|
import {
|
|
38
39
|
currentUsageLogRevision,
|
|
39
40
|
readUsageSnapshotForManagement,
|
|
@@ -232,5 +233,98 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
232
233
|
});
|
|
233
234
|
}
|
|
234
235
|
}
|
|
236
|
+
|
|
237
|
+
if (url.pathname === "/api/storage/cleanup/preview" && req.method === "POST") {
|
|
238
|
+
let body: { percent?: unknown };
|
|
239
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid_json" }, 400); }
|
|
240
|
+
const percent = typeof body?.percent === "number" ? body.percent : Number.NaN;
|
|
241
|
+
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
|
|
242
|
+
return jsonResponse({ error: "invalid_percent" }, 400);
|
|
243
|
+
}
|
|
244
|
+
const preview = previewArchivedCleanup(percent);
|
|
245
|
+
// Omit absolute host paths (codexHome / absPath) from the wire response.
|
|
246
|
+
return jsonResponse({
|
|
247
|
+
percent: preview.percent,
|
|
248
|
+
count: preview.count,
|
|
249
|
+
bytes: preview.bytes,
|
|
250
|
+
digest: preview.digest,
|
|
251
|
+
// Dashboard only lists a handful; count/bytes/digest already bind the full set.
|
|
252
|
+
candidates: preview.candidates.slice(0, 50).map(({ relPath, bytes, mtimeMs, physicalRelPaths }) => ({
|
|
253
|
+
relPath,
|
|
254
|
+
bytes,
|
|
255
|
+
mtimeMs,
|
|
256
|
+
physicalRelPaths,
|
|
257
|
+
})),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (url.pathname === "/api/storage/cleanup" && req.method === "POST") {
|
|
262
|
+
let body: { percent?: unknown; mode?: unknown; digest?: unknown; _test?: unknown };
|
|
263
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid_json" }, 400); }
|
|
264
|
+
const percent = typeof body?.percent === "number" ? body.percent : Number.NaN;
|
|
265
|
+
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
|
|
266
|
+
return jsonResponse({ error: "invalid_percent" }, 400);
|
|
267
|
+
}
|
|
268
|
+
const mode = body?.mode;
|
|
269
|
+
if (mode !== "quarantine" && mode !== "permanent") {
|
|
270
|
+
return jsonResponse({ error: "invalid_mode" }, 400);
|
|
271
|
+
}
|
|
272
|
+
const digest = typeof body?.digest === "string" ? body.digest : "";
|
|
273
|
+
const testHooks =
|
|
274
|
+
process.env.OPENCODEX_CLEANUP_TEST_HOOKS === "1" &&
|
|
275
|
+
body &&
|
|
276
|
+
typeof body === "object" &&
|
|
277
|
+
"_test" in body
|
|
278
|
+
? pickWireCleanupTestHooks(body._test)
|
|
279
|
+
: undefined;
|
|
280
|
+
try {
|
|
281
|
+
const result = executeArchivedCleanup({
|
|
282
|
+
percent,
|
|
283
|
+
mode: mode as CleanupMode,
|
|
284
|
+
digest,
|
|
285
|
+
...(testHooks ? { _test: testHooks } : {}),
|
|
286
|
+
});
|
|
287
|
+
if (!result.ok) {
|
|
288
|
+
const status =
|
|
289
|
+
result.error === "codex_busy" || result.error === "stale_preview" || result.error === "referenced_history"
|
|
290
|
+
? 409
|
|
291
|
+
: result.error === "invalid_mode" || result.error === "invalid_digest"
|
|
292
|
+
? 400
|
|
293
|
+
: 500;
|
|
294
|
+
const messages: Record<string, string> = {
|
|
295
|
+
codex_busy: "Codex is using state.sqlite — try again after quitting Codex.",
|
|
296
|
+
stale_preview: "Archived files changed since preview — run Preview again.",
|
|
297
|
+
referenced_history: "Selected archives are still referenced by forked or paginated history.",
|
|
298
|
+
invalid_digest: "Preview digest is missing or invalid.",
|
|
299
|
+
invalid_mode: "mode must be quarantine or permanent.",
|
|
300
|
+
fs_failed: "Filesystem cleanup failed. Some changes may already be applied — check CODEX_HOME/.trash and any recovery path in the response.",
|
|
301
|
+
db_reconcile_failed: "Could not update Codex state database.",
|
|
302
|
+
cleanup_failed: "Cleanup failed.",
|
|
303
|
+
};
|
|
304
|
+
return jsonResponse({
|
|
305
|
+
ok: false,
|
|
306
|
+
error: result.error ?? "cleanup_failed",
|
|
307
|
+
message: messages[result.error ?? ""] ?? messages.cleanup_failed,
|
|
308
|
+
...(result.trashDir ? { trashDir: result.trashDir } : {}),
|
|
309
|
+
}, status);
|
|
310
|
+
}
|
|
311
|
+
return jsonResponse({
|
|
312
|
+
ok: true,
|
|
313
|
+
mode: result.mode,
|
|
314
|
+
percent: result.percent,
|
|
315
|
+
count: result.count,
|
|
316
|
+
bytes: result.bytes,
|
|
317
|
+
...(result.trashDir ? { trashDir: result.trashDir } : {}),
|
|
318
|
+
removedPaths: result.removedPaths,
|
|
319
|
+
});
|
|
320
|
+
} catch {
|
|
321
|
+
return jsonResponse({
|
|
322
|
+
ok: false,
|
|
323
|
+
error: "cleanup_failed",
|
|
324
|
+
message: "Cleanup failed.",
|
|
325
|
+
}, 500);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
235
329
|
return null;
|
|
236
330
|
}
|
|
@@ -35,7 +35,6 @@ import { clearThreadAccountMap } from "../../codex/routing";
|
|
|
35
35
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
36
36
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
37
37
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
38
|
-
import { scanStorage } from "../../storage/scanner";
|
|
39
38
|
import { readUsageEntries } from "../../usage/log";
|
|
40
39
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
41
40
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
@@ -33,7 +33,6 @@ import { clearThreadAccountMap } from "../../codex/routing";
|
|
|
33
33
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
35
35
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
36
|
-
import { scanStorage } from "../../storage/scanner";
|
|
37
36
|
import { readUsageEntries } from "../../usage/log";
|
|
38
37
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
39
38
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
@@ -34,7 +34,6 @@ import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
|
34
34
|
import { getProviderDiscoveryStatus } from "../../codex/model-cache";
|
|
35
35
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
36
36
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
37
|
-
import { scanStorage } from "../../storage/scanner";
|
|
38
37
|
import { readUsageEntries } from "../../usage/log";
|
|
39
38
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
40
39
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
@@ -33,7 +33,6 @@ import { clearThreadAccountMap } from "../../codex/routing";
|
|
|
33
33
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
35
35
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
36
|
-
import { scanStorage } from "../../storage/scanner";
|
|
37
36
|
import { readUsageEntries } from "../../usage/log";
|
|
38
37
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
39
38
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
* unauthenticated /healthz surface.
|
|
8
8
|
*
|
|
9
9
|
* The payload is scalar-only (numbers, enum strings): no paths, no tokens, no
|
|
10
|
-
* account identifiers. `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* account identifiers. `external` and `arrayBuffers` keep Windows diagnostics
|
|
11
|
+
* honest when RSS/working-set counters under-report committed retention.
|
|
12
|
+
* `jscHeap` (bun:jsc heapStats) is useful context, but on Bun 1.3.14 it is not a
|
|
13
|
+
* standalone leak discriminator. `responseState` attributes growth further: it
|
|
14
|
+
* is the proxy's previous_response_id continuation store, so a growing
|
|
15
|
+
* responseState.totalBytes under rising observed memory points at conversation
|
|
16
|
+
* retention rather than the runtime allocator.
|
|
16
17
|
*/
|
|
17
18
|
import { decideEagerRelay } from "../../lib/bun-stream-caps";
|
|
18
|
-
import { getActiveMemoryWatchdog } from "../memory-watchdog";
|
|
19
|
+
import { getActiveMemoryWatchdog, observedMemoryCounter } from "../memory-watchdog";
|
|
19
20
|
import { responseStateMetrics } from "../../responses/state";
|
|
20
21
|
import { jsonResponse } from "../auth-cors";
|
|
21
22
|
import type { ManagementContext } from "./context";
|
|
@@ -38,15 +39,22 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
38
39
|
} catch {
|
|
39
40
|
/* non-Bun tooling or unavailable introspection — omit the discriminator */
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
const watchdogInstance = getActiveMemoryWatchdog();
|
|
43
|
+
const observed = observedMemoryCounter({
|
|
44
|
+
rss: usage.rss,
|
|
45
|
+
external: usage.external,
|
|
46
|
+
arrayBuffers: usage.arrayBuffers,
|
|
47
|
+
});
|
|
42
48
|
const watchdog = watchdogInstance
|
|
43
49
|
? (() => {
|
|
44
50
|
const snap = watchdogInstance.snapshot();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
return {
|
|
52
|
+
warnThresholdBytes: snap.warnThresholdBytes,
|
|
53
|
+
lastWarnAt: snap.lastWarnAt,
|
|
54
|
+
observedBytes: snap.observedBytes,
|
|
55
|
+
observedMetric: snap.observedMetric,
|
|
56
|
+
samples: snap.samples.slice(-ENDPOINT_SAMPLE_LIMIT),
|
|
57
|
+
};
|
|
50
58
|
})()
|
|
51
59
|
: null;
|
|
52
60
|
const streamMode = config.streamMode ?? "auto";
|
|
@@ -58,8 +66,12 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
58
66
|
uptimeSeconds: process.uptime(),
|
|
59
67
|
rss: usage.rss,
|
|
60
68
|
heapUsed: usage.heapUsed,
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
heapTotal: usage.heapTotal,
|
|
70
|
+
external: usage.external,
|
|
71
|
+
arrayBuffers: usage.arrayBuffers,
|
|
72
|
+
observedBytes: observed.observedBytes,
|
|
73
|
+
observedMetric: observed.observedMetric,
|
|
74
|
+
jscHeap,
|
|
63
75
|
responseState: responseStateMetrics(),
|
|
64
76
|
streamMode,
|
|
65
77
|
eagerRelay: process.platform === "win32" ? decideEagerRelay(streamMode) : null,
|
|
@@ -33,7 +33,6 @@ import { clearThreadAccountMap } from "../codex/routing";
|
|
|
33
33
|
import { primeCodexPoolQuotas } from "../codex/auth-api";
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../providers/context-cap";
|
|
35
35
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
36
|
-
import { scanStorage } from "../storage/scanner";
|
|
37
36
|
import { readUsageEntries } from "../usage/log";
|
|
38
37
|
import { getUsageDebugLogEntries } from "../usage/debug";
|
|
39
38
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../usage/summary";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Memory watchdog (#314 WP3 / #509) — warn-only observability for the Windows
|
|
3
3
|
* native-memory growth reported upstream (Bun fetch buffers / socket handles).
|
|
4
4
|
*
|
|
5
5
|
* Samples process.memoryUsage() on an unref'd interval into a bounded ring and
|
|
6
|
-
* logs ONE rate-limited warning when
|
|
6
|
+
* logs ONE rate-limited warning when observed memory crosses the threshold. It never
|
|
7
7
|
* restarts anything (threshold auto-restart is deliberately deferred; the
|
|
8
8
|
* service managers' crash-respawn already covers hard failures). The active
|
|
9
9
|
* instance is a module-level singleton so the management API can expose the
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* paths, hostnames, or tokens.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
export type
|
|
16
|
+
export type MemorySampleBase = {
|
|
17
17
|
/** Epoch ms. */
|
|
18
18
|
at: number;
|
|
19
19
|
/** Resident set size in bytes. */
|
|
@@ -22,12 +22,27 @@ export type MemorySample = {
|
|
|
22
22
|
heapUsed: number;
|
|
23
23
|
/** JS heap total in bytes. */
|
|
24
24
|
heapTotal: number;
|
|
25
|
+
/** External/native memory tracked by process.memoryUsage(). */
|
|
26
|
+
external: number;
|
|
27
|
+
/** ArrayBuffer memory tracked by process.memoryUsage(). */
|
|
28
|
+
arrayBuffers: number;
|
|
25
29
|
};
|
|
26
30
|
|
|
31
|
+
export type MemorySample = MemorySampleBase & {
|
|
32
|
+
/** Largest observed memory counter used for thresholding. */
|
|
33
|
+
observedBytes: number;
|
|
34
|
+
/** Counter that produced observedBytes. */
|
|
35
|
+
observedMetric: MemoryMetric;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type MemoryMetric = "rss" | "external" | "arrayBuffers";
|
|
39
|
+
|
|
27
40
|
export type MemoryWatchdogState = {
|
|
28
41
|
samples: MemorySample[];
|
|
29
42
|
warnThresholdBytes: number;
|
|
30
43
|
lastWarnAt: number | null;
|
|
44
|
+
observedBytes: number;
|
|
45
|
+
observedMetric: MemoryMetric;
|
|
31
46
|
};
|
|
32
47
|
|
|
33
48
|
export type MemoryWatchdog = {
|
|
@@ -43,6 +58,19 @@ const DOCS_URL = "https://opencodex.me/troubleshooting/windows-memory/";
|
|
|
43
58
|
|
|
44
59
|
let active: MemoryWatchdog | null = null;
|
|
45
60
|
|
|
61
|
+
export function observedMemoryCounter(sample: Pick<MemorySampleBase, "rss" | "external" | "arrayBuffers">): {
|
|
62
|
+
observedBytes: number;
|
|
63
|
+
observedMetric: MemoryMetric;
|
|
64
|
+
} {
|
|
65
|
+
const values: Array<{ metric: MemoryMetric; bytes: number }> = [
|
|
66
|
+
{ metric: "rss", bytes: sample.rss },
|
|
67
|
+
{ metric: "external", bytes: sample.external },
|
|
68
|
+
{ metric: "arrayBuffers", bytes: sample.arrayBuffers },
|
|
69
|
+
];
|
|
70
|
+
const best = values.reduce((current, next) => next.bytes > current.bytes ? next : current, values[0]);
|
|
71
|
+
return { observedBytes: best.bytes, observedMetric: best.metric };
|
|
72
|
+
}
|
|
73
|
+
|
|
46
74
|
/** The running watchdog, if any — read by /api/system/memory. */
|
|
47
75
|
export function getActiveMemoryWatchdog(): MemoryWatchdog | null {
|
|
48
76
|
return active;
|
|
@@ -50,7 +78,19 @@ export function getActiveMemoryWatchdog(): MemoryWatchdog | null {
|
|
|
50
78
|
|
|
51
79
|
function defaultSample(now: () => number): MemorySample {
|
|
52
80
|
const usage = process.memoryUsage();
|
|
53
|
-
|
|
81
|
+
const base = {
|
|
82
|
+
at: now(),
|
|
83
|
+
rss: usage.rss,
|
|
84
|
+
heapUsed: usage.heapUsed,
|
|
85
|
+
heapTotal: usage.heapTotal,
|
|
86
|
+
external: usage.external,
|
|
87
|
+
arrayBuffers: usage.arrayBuffers,
|
|
88
|
+
};
|
|
89
|
+
return { ...base, ...observedMemoryCounter(base) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeSample(sample: MemorySampleBase): MemorySample {
|
|
93
|
+
return { ...sample, ...observedMemoryCounter(sample) };
|
|
54
94
|
}
|
|
55
95
|
|
|
56
96
|
/**
|
|
@@ -64,7 +104,7 @@ export function startMemoryWatchdog(opts?: {
|
|
|
64
104
|
warnThresholdBytes?: number;
|
|
65
105
|
ringSize?: number;
|
|
66
106
|
now?: () => number;
|
|
67
|
-
sample?: () =>
|
|
107
|
+
sample?: () => MemorySampleBase;
|
|
68
108
|
warn?: (msg: string) => void;
|
|
69
109
|
}): MemoryWatchdog {
|
|
70
110
|
active?.stop();
|
|
@@ -77,21 +117,25 @@ export function startMemoryWatchdog(opts?: {
|
|
|
77
117
|
|
|
78
118
|
const samples: MemorySample[] = [];
|
|
79
119
|
let lastWarnAt: number | null = null;
|
|
120
|
+
let observedBytes = 0;
|
|
121
|
+
let observedMetric: MemoryMetric = "rss";
|
|
80
122
|
|
|
81
123
|
const tick = () => {
|
|
82
124
|
let s: MemorySample;
|
|
83
125
|
try {
|
|
84
|
-
s = sample();
|
|
126
|
+
s = normalizeSample(sample());
|
|
85
127
|
} catch {
|
|
86
128
|
return; // sampling must never break the server
|
|
87
129
|
}
|
|
88
130
|
samples.push(s);
|
|
89
131
|
if (samples.length > ringSize) samples.splice(0, samples.length - ringSize);
|
|
90
|
-
|
|
132
|
+
observedBytes = s.observedBytes;
|
|
133
|
+
observedMetric = s.observedMetric;
|
|
134
|
+
if (s.observedBytes >= warnThresholdBytes && (lastWarnAt === null || now() - lastWarnAt >= WARN_INTERVAL_MS)) {
|
|
91
135
|
lastWarnAt = now();
|
|
92
|
-
const
|
|
136
|
+
const observedMb = Math.round(s.observedBytes / (1024 * 1024));
|
|
93
137
|
const thresholdMb = Math.round(warnThresholdBytes / (1024 * 1024));
|
|
94
|
-
warn(`⚠️ opencodex
|
|
138
|
+
warn(`⚠️ opencodex observed memory ${observedMb}MB (${s.observedMetric}) exceeds the ${thresholdMb}MB watch threshold. On Windows this is usually the upstream Bun runtime memory issue — see ${DOCS_URL}`);
|
|
95
139
|
}
|
|
96
140
|
};
|
|
97
141
|
|
|
@@ -104,7 +148,7 @@ export function startMemoryWatchdog(opts?: {
|
|
|
104
148
|
if (active === instance) active = null;
|
|
105
149
|
},
|
|
106
150
|
snapshot() {
|
|
107
|
-
return { samples: [...samples], warnThresholdBytes, lastWarnAt };
|
|
151
|
+
return { samples: [...samples], warnThresholdBytes, lastWarnAt, observedBytes, observedMetric };
|
|
108
152
|
},
|
|
109
153
|
};
|
|
110
154
|
active = instance;
|