@gajae-code/coding-agent 0.11.9 → 0.11.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/CHANGELOG.md +56 -1
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +57 -2
- package/dist/types/config/settings.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +14 -9
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +11 -2
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
- package/dist/types/sdk/host/control/operations.d.ts +1 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +11 -11
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
- package/dist/types/session/session-manager.d.ts +34 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +39 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/resource-gc.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +48 -2
- package/src/commands/sdk.ts +91 -7
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +31 -7
- package/src/config/model-registry.ts +4 -2
- package/src/config/settings-schema.ts +61 -2
- package/src/config/settings.ts +8 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +14 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
- package/src/gjc-runtime/deep-interview-draft.ts +2 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
- package/src/gjc-runtime/deep-interview-state.ts +25 -3
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/ralplan-runtime.ts +252 -1
- package/src/gjc-runtime/state-runtime.ts +48 -11
- package/src/gjc-runtime/state-writer.ts +1 -1
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/modes/acp/acp-agent.ts +554 -60
- package/src/modes/acp/acp-event-mapper.ts +93 -6
- package/src/modes/components/model-selector.ts +15 -1
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +65 -26
- package/src/modes/runtime-init.ts +1 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +82 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +222 -27
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +1191 -164
- package/src/sdk/host/control/dispatch.ts +2 -0
- package/src/sdk/host/control/operations.ts +1 -0
- package/src/sdk/host/host.ts +2 -2
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +1 -0
- package/src/sdk/session.ts +24 -1
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +290 -132
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +173 -127
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/session-manager.ts +608 -25
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +152 -42
- package/src/task/index.ts +2 -0
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +25 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/resource-gc.ts +496 -120
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
package/src/tools/resource-gc.ts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { WindowsJobMemoryProbeResult } from "@gajae-code/natives";
|
|
5
|
+
|
|
6
|
+
function safeProbeWindowsJobMemory(): WindowsJobMemoryProbeResult {
|
|
7
|
+
try {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
9
|
+
const natives = require("@gajae-code/natives") as { probeWindowsJobMemory?: () => unknown };
|
|
10
|
+
if (typeof natives.probeWindowsJobMemory === "function") {
|
|
11
|
+
return natives.probeWindowsJobMemory() as WindowsJobMemoryProbeResult;
|
|
12
|
+
}
|
|
13
|
+
} catch {
|
|
14
|
+
// Native addon unbuilt or missing
|
|
15
|
+
}
|
|
16
|
+
return { kind: "unsupported_platform", platform: process.platform };
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
import { logger } from "@gajae-code/utils";
|
|
2
20
|
import type { Settings } from "../config/settings";
|
|
21
|
+
import { computeMemoryGuardDomain } from "../runtime/memory-domain";
|
|
22
|
+
import { chooseMemoryGuardAction, MemoryGuardHost, resolveMemoryGuardPolicy } from "../runtime/memory-guard";
|
|
23
|
+
import type { MemoryGuardPolicy } from "../runtime/memory-guard-contract";
|
|
24
|
+
import { resolveEffectiveMemoryLimit } from "../runtime/memory-limit";
|
|
3
25
|
import { listTabsForGc, releaseTabIfGcEligible, type TabGcSnapshot } from "./browser/tab-supervisor";
|
|
4
26
|
import { cleanupStaleScreenshotFallbackDirs, hasCreatedScreenshotFallbackDir } from "./computer-gc";
|
|
5
27
|
|
|
@@ -17,7 +39,6 @@ import { cleanupStaleScreenshotFallbackDirs, hasCreatedScreenshotFallbackDir } f
|
|
|
17
39
|
* eviction is best-effort and never force-evicts.
|
|
18
40
|
*/
|
|
19
41
|
|
|
20
|
-
const DEFAULT_SWEEP_INTERVAL_MS = 30_000;
|
|
21
42
|
const BYTES_PER_MB = 1024 * 1024;
|
|
22
43
|
|
|
23
44
|
export interface BrowserGcPolicy {
|
|
@@ -55,7 +76,10 @@ export function resolveSweepIntervalMs(settings: Settings): number {
|
|
|
55
76
|
/** Injectable seams so the controller is fully testable without real browsers/filesystem/RSS. */
|
|
56
77
|
export interface ResourceGcDeps {
|
|
57
78
|
now: () => number;
|
|
79
|
+
monotonicNow: () => number;
|
|
58
80
|
rssBytes: () => number;
|
|
81
|
+
memorySnapshot: () => Promise<MemoryPressureSnapshot>;
|
|
82
|
+
runGc: () => void;
|
|
59
83
|
logWarn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
60
84
|
listTabs: () => TabGcSnapshot[];
|
|
61
85
|
releaseTab: (name: string, policy: { now: () => number; idleMs: number }) => Promise<boolean>;
|
|
@@ -65,7 +89,10 @@ export interface ResourceGcDeps {
|
|
|
65
89
|
|
|
66
90
|
const defaultDeps: ResourceGcDeps = {
|
|
67
91
|
now: () => Date.now(),
|
|
92
|
+
monotonicNow: () => performance.now(),
|
|
68
93
|
rssBytes: () => process.memoryUsage().rss,
|
|
94
|
+
memorySnapshot: () => sampleMemoryPressure(),
|
|
95
|
+
runGc: () => Bun.gc(true),
|
|
69
96
|
logWarn: (msg, meta) => logger.warn(msg, meta),
|
|
70
97
|
listTabs: () => listTabsForGc(),
|
|
71
98
|
releaseTab: (name, policy) => releaseTabIfGcEligible(name, policy),
|
|
@@ -75,33 +102,18 @@ const defaultDeps: ResourceGcDeps = {
|
|
|
75
102
|
|
|
76
103
|
// ── Controller state (process-global; tabs/browsers are module-global too) ──────────────────
|
|
77
104
|
const activeSessions = new Map<string, Settings>();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
interface DeferredSchedule {
|
|
85
|
-
generation: number;
|
|
86
|
-
deadline: number;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface WorkOwner {
|
|
90
|
-
generation: number;
|
|
91
|
-
source: "timer" | "external";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let pendingTimer: NodeJS.Timeout | null = null;
|
|
95
|
-
let pendingDeadline: number | null = null;
|
|
96
|
-
let pendingOwner: ScheduleOwner | null = null;
|
|
97
|
-
let deferredSchedule: DeferredSchedule | null = null;
|
|
98
|
-
let inProgressOwner: WorkOwner | null = null;
|
|
99
|
-
let stopped = false;
|
|
100
|
-
let timerGeneration = 0;
|
|
101
|
-
let nextTimerToken = 0;
|
|
102
|
-
let schedulerNow = (): number => performance.now();
|
|
105
|
+
const scheduler = new MemoryGuardHost({
|
|
106
|
+
run: async () => {
|
|
107
|
+
await sweepOnce(deps);
|
|
108
|
+
},
|
|
109
|
+
logDebug: (message, meta) => logger.debug(message, meta),
|
|
110
|
+
});
|
|
103
111
|
let rssWarningActive = false;
|
|
104
112
|
let lastScreenshotScanAt = 0;
|
|
113
|
+
const memoryGuardGcActive = new Set<string>();
|
|
114
|
+
const memoryGuardLastEvaluatedAt = new Map<string, number>();
|
|
115
|
+
const memoryGuardRestartAboveSince = new Map<string, number>();
|
|
116
|
+
const memoryGuardRestartCooldownUntil = new Map<string, number>();
|
|
105
117
|
let deps: ResourceGcDeps = defaultDeps;
|
|
106
118
|
|
|
107
119
|
export interface ResourceGcRegistration {
|
|
@@ -109,120 +121,479 @@ export interface ResourceGcRegistration {
|
|
|
109
121
|
settings: Settings;
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
function resolveSessionSweepIntervalMs(settings: Settings): number {
|
|
125
|
+
const memoryPolicy = resolveMemoryGuardPolicy(settings);
|
|
126
|
+
return memoryPolicy.enabled
|
|
127
|
+
? Math.min(resolveSweepIntervalMs(settings), memoryPolicy.checkIntervalMs)
|
|
128
|
+
: resolveSweepIntervalMs(settings);
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
/**
|
|
113
132
|
* Register a session with the resource GC. Starts the single shared timer on the first
|
|
114
133
|
* registration. Returns an idempotent unregister function; the timer stops only when the last
|
|
115
134
|
* session unregisters.
|
|
116
135
|
*/
|
|
117
136
|
export function registerResourceGcSession(reg: ResourceGcRegistration): () => void {
|
|
118
|
-
const isNewSession = !activeSessions.has(reg.sessionId);
|
|
119
137
|
activeSessions.set(reg.sessionId, reg.settings);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
const unregisterSchedule = scheduler.register({
|
|
139
|
+
ownerId: reg.sessionId,
|
|
140
|
+
intervalMs: resolveSessionSweepIntervalMs(reg.settings),
|
|
141
|
+
});
|
|
142
|
+
const unregisterSettings = reg.settings.onChanged(path => {
|
|
123
143
|
if (
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
path === "memoryGuard.enabled" ||
|
|
145
|
+
path === "memoryGuard.checkIntervalMs" ||
|
|
146
|
+
path === "resourceGc.sweepIntervalMs"
|
|
126
147
|
) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
scheduler.updateInterval(reg.sessionId, resolveSessionSweepIntervalMs(reg.settings));
|
|
149
|
+
if (path === "memoryGuard.enabled" && !resolveMemoryGuardPolicy(reg.settings).enabled) {
|
|
150
|
+
memoryGuardGcActive.delete(reg.sessionId);
|
|
151
|
+
memoryGuardRestartAboveSince.delete(reg.sessionId);
|
|
152
|
+
memoryGuardRestartCooldownUntil.delete(reg.sessionId);
|
|
153
|
+
memoryGuardLastEvaluatedAt.delete(reg.sessionId);
|
|
154
|
+
}
|
|
130
155
|
}
|
|
131
|
-
}
|
|
156
|
+
});
|
|
132
157
|
let unregistered = false;
|
|
133
158
|
return () => {
|
|
134
159
|
if (unregistered) return;
|
|
135
160
|
unregistered = true;
|
|
136
161
|
activeSessions.delete(reg.sessionId);
|
|
137
|
-
|
|
162
|
+
memoryGuardLastEvaluatedAt.delete(reg.sessionId);
|
|
163
|
+
memoryGuardGcActive.delete(reg.sessionId);
|
|
164
|
+
memoryGuardRestartAboveSince.delete(reg.sessionId);
|
|
165
|
+
memoryGuardRestartCooldownUntil.delete(reg.sessionId);
|
|
166
|
+
unregisterSchedule();
|
|
167
|
+
unregisterSettings();
|
|
138
168
|
};
|
|
139
169
|
}
|
|
140
170
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
171
|
+
export async function sweepOnce(d: ResourceGcDeps = deps): Promise<void> {
|
|
172
|
+
if (activeSessions.size === 0) return;
|
|
173
|
+
try {
|
|
174
|
+
const memorySweep = sweepMemoryPressureGuard(d);
|
|
175
|
+
if (memorySweep) await memorySweep;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
d.logWarn("Memory guard: sweep failed; continuing with browser/screenshot cleanup", { error: String(error) });
|
|
178
|
+
}
|
|
179
|
+
await sweepBrowserTabs(d);
|
|
180
|
+
await sweepScreenshots(d);
|
|
145
181
|
}
|
|
146
182
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
183
|
+
export interface MemoryPressureDomain {
|
|
184
|
+
hardCapBytes: number;
|
|
185
|
+
totalUsageBytes: number;
|
|
186
|
+
source: MemoryPressureSnapshot["source"];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface MemoryPressureSnapshot {
|
|
190
|
+
hardCapBytes: number;
|
|
191
|
+
totalUsageBytes: number;
|
|
192
|
+
parentBytes: number;
|
|
193
|
+
source: "host" | "linux_cgroup_v2" | "linux_cgroup_v1" | "windows_job" | "windows_process_job_limit";
|
|
194
|
+
domains?: MemoryPressureDomain[];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function decodeMountInfoPath(value: string): string {
|
|
198
|
+
return value.replace(/\\([0-7]{3})/g, (_match, octal: string) => String.fromCharCode(Number.parseInt(octal, 8)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface CgroupDirectoryCandidate {
|
|
202
|
+
directory: string;
|
|
203
|
+
mountPoint: string;
|
|
204
|
+
fallback: boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function resolveCgroupDirectories(
|
|
208
|
+
mountInfo: string,
|
|
209
|
+
membershipPath: string,
|
|
210
|
+
fsType: "cgroup" | "cgroup2",
|
|
211
|
+
): CgroupDirectoryCandidate[] {
|
|
212
|
+
const contained: CgroupDirectoryCandidate[] = [];
|
|
213
|
+
const fallbacks: CgroupDirectoryCandidate[] = [];
|
|
214
|
+
const seen = new Set<string>();
|
|
215
|
+
for (const line of mountInfo.split("\n")) {
|
|
216
|
+
const [left, right] = line.split(" - ", 2);
|
|
217
|
+
if (!left || !right) continue;
|
|
218
|
+
const leftFields = left.split(" ");
|
|
219
|
+
const rightFields = right.split(" ");
|
|
220
|
+
if (leftFields.length < 5 || rightFields[0] !== fsType) continue;
|
|
221
|
+
if (fsType === "cgroup" && !rightFields.slice(2).join(",").split(",").includes("memory")) continue;
|
|
222
|
+
const mountRoot = decodeMountInfoPath(leftFields[3]!);
|
|
223
|
+
const mountPoint = decodeMountInfoPath(leftFields[4]!);
|
|
224
|
+
const relative = path.posix.relative(mountRoot, membershipPath);
|
|
225
|
+
const directory =
|
|
226
|
+
!relative.startsWith("..") && !path.posix.isAbsolute(relative)
|
|
227
|
+
? path.join(mountPoint, relative)
|
|
228
|
+
: path.join(mountPoint, membershipPath.replace(/^\/+/, ""));
|
|
229
|
+
const key = `${directory}\0${mountPoint}`;
|
|
230
|
+
if (seen.has(key)) continue;
|
|
231
|
+
seen.add(key);
|
|
232
|
+
const fallback = relative.startsWith("..") || path.posix.isAbsolute(relative);
|
|
233
|
+
const candidate = { directory, mountPoint, fallback };
|
|
234
|
+
if (!relative.startsWith("..") && !path.posix.isAbsolute(relative)) contained.push(candidate);
|
|
235
|
+
else fallbacks.push(candidate);
|
|
236
|
+
}
|
|
237
|
+
return [...contained, ...fallbacks];
|
|
152
238
|
}
|
|
153
239
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
void handleTimerCallback(owner, deadline);
|
|
164
|
-
},
|
|
165
|
-
Math.max(0, deadline - schedulerNow()),
|
|
166
|
-
);
|
|
167
|
-
pendingTimer.unref?.();
|
|
240
|
+
async function readMemoryCounter(file: string): Promise<number | null> {
|
|
241
|
+
try {
|
|
242
|
+
const value = (await fs.readFile(file, "utf8")).trim();
|
|
243
|
+
if (value === "max" || !/^\d+$/.test(value)) return null;
|
|
244
|
+
const parsed = Number(value);
|
|
245
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
168
249
|
}
|
|
169
250
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
251
|
+
type MemoryLimitCounter = { kind: "finite"; bytes: number } | { kind: "unlimited" };
|
|
252
|
+
|
|
253
|
+
async function readMemoryLimit(file: string, fsType: "cgroup" | "cgroup2"): Promise<MemoryLimitCounter | null> {
|
|
254
|
+
try {
|
|
255
|
+
const value = (await fs.readFile(file, "utf8")).trim();
|
|
256
|
+
if (value === "max") return { kind: "unlimited" };
|
|
257
|
+
if (!/^\d+$/.test(value)) return null;
|
|
258
|
+
const bytes = BigInt(value);
|
|
259
|
+
if (fsType === "cgroup" && bytes > BigInt(Number.MAX_SAFE_INTEGER)) return { kind: "unlimited" };
|
|
260
|
+
const numericBytes = Number(bytes);
|
|
261
|
+
return Number.isSafeInteger(numericBytes) ? { kind: "finite", bytes: numericBytes } : null;
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
175
264
|
}
|
|
176
|
-
deferredSchedule = { generation, deadline };
|
|
177
265
|
}
|
|
178
266
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
267
|
+
function parseCgroupEntry(line: string): [string, string, string] | null {
|
|
268
|
+
const first = line.indexOf(":");
|
|
269
|
+
const second = first < 0 ? -1 : line.indexOf(":", first + 1);
|
|
270
|
+
if (first < 0 || second < 0) return null;
|
|
271
|
+
return [line.slice(0, first), line.slice(first + 1, second), line.slice(second + 1)];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function sampleLinuxCgroupDirectory(
|
|
275
|
+
candidate: CgroupDirectoryCandidate,
|
|
276
|
+
fsType: "cgroup" | "cgroup2",
|
|
277
|
+
hostBytes: number,
|
|
278
|
+
): Promise<MemoryPressureDomain[]> {
|
|
279
|
+
const limitName = fsType === "cgroup2" ? "memory.max" : "memory.limit_in_bytes";
|
|
280
|
+
const usageName = fsType === "cgroup2" ? "memory.current" : "memory.usage_in_bytes";
|
|
281
|
+
const source = fsType === "cgroup2" ? "linux_cgroup_v2" : "linux_cgroup_v1";
|
|
282
|
+
const domains: MemoryPressureDomain[] = [];
|
|
283
|
+
let current = candidate.directory;
|
|
284
|
+
while (true) {
|
|
285
|
+
const [limit, usage] = await Promise.all([
|
|
286
|
+
readMemoryLimit(path.join(current, limitName), fsType),
|
|
287
|
+
readMemoryCounter(path.join(current, usageName)),
|
|
288
|
+
]);
|
|
289
|
+
if (limit !== null && usage !== null && (limit.kind === "unlimited" || limit.bytes > 0)) {
|
|
290
|
+
domains.push({
|
|
291
|
+
hardCapBytes: limit.kind === "unlimited" ? hostBytes : Math.min(hostBytes, limit.bytes),
|
|
292
|
+
totalUsageBytes: usage,
|
|
293
|
+
source,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (current === candidate.mountPoint) break;
|
|
297
|
+
const parent = path.dirname(current);
|
|
298
|
+
if (
|
|
299
|
+
parent === current ||
|
|
300
|
+
(parent !== candidate.mountPoint && !parent.startsWith(`${candidate.mountPoint}${path.sep}`))
|
|
301
|
+
) {
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
current = parent;
|
|
305
|
+
}
|
|
306
|
+
return domains;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function __sampleLinuxCgroupHierarchyForTest(
|
|
310
|
+
mountInfo: string,
|
|
311
|
+
membership: string,
|
|
312
|
+
fsType: "cgroup" | "cgroup2",
|
|
313
|
+
hostBytes: number,
|
|
314
|
+
parentBytes: number,
|
|
315
|
+
): Promise<MemoryPressureSnapshot | null> {
|
|
316
|
+
const containedDomains: MemoryPressureDomain[] = [];
|
|
317
|
+
const fallbackDomains: MemoryPressureDomain[] = [];
|
|
318
|
+
for (const candidate of resolveCgroupDirectories(mountInfo, membership, fsType)) {
|
|
319
|
+
const domains = await sampleLinuxCgroupDirectory(candidate, fsType, hostBytes);
|
|
320
|
+
if (candidate.fallback) fallbackDomains.push(...domains);
|
|
321
|
+
else containedDomains.push(...domains);
|
|
187
322
|
}
|
|
188
|
-
|
|
323
|
+
const domains = containedDomains.length > 0 ? containedDomains : fallbackDomains;
|
|
324
|
+
if (domains.length === 0) return null;
|
|
325
|
+
const selected = domains.reduce((current, domain) =>
|
|
326
|
+
domain.totalUsageBytes / Math.min(hostBytes, domain.hardCapBytes) >
|
|
327
|
+
current.totalUsageBytes / Math.min(hostBytes, current.hardCapBytes)
|
|
328
|
+
? domain
|
|
329
|
+
: current,
|
|
330
|
+
);
|
|
331
|
+
return {
|
|
332
|
+
...selected,
|
|
333
|
+
totalUsageBytes: Math.max(parentBytes, selected.totalUsageBytes),
|
|
334
|
+
parentBytes,
|
|
335
|
+
domains,
|
|
336
|
+
};
|
|
189
337
|
}
|
|
190
338
|
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
339
|
+
async function sampleLinuxCgroupMemory(hostBytes: number, parentBytes: number): Promise<MemoryPressureSnapshot | null> {
|
|
340
|
+
let cgroup: string;
|
|
341
|
+
let mountInfo: string;
|
|
342
|
+
try {
|
|
343
|
+
[cgroup, mountInfo] = await Promise.all([
|
|
344
|
+
fs.readFile("/proc/self/cgroup", "utf8"),
|
|
345
|
+
fs.readFile("/proc/self/mountinfo", "utf8"),
|
|
346
|
+
]);
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const entries = cgroup
|
|
352
|
+
.split("\n")
|
|
353
|
+
.map(parseCgroupEntry)
|
|
354
|
+
.filter((entry): entry is [string, string, string] => entry !== null);
|
|
355
|
+
const v2Membership = entries.find(parts => parts[0] === "0" && parts[1] === "")?.[2];
|
|
356
|
+
const v1Membership = entries.find(parts => parts[1].split(",").includes("memory"))?.[2];
|
|
357
|
+
if (v2Membership) {
|
|
358
|
+
const snapshot = await __sampleLinuxCgroupHierarchyForTest(
|
|
359
|
+
mountInfo,
|
|
360
|
+
v2Membership,
|
|
361
|
+
"cgroup2",
|
|
362
|
+
hostBytes,
|
|
363
|
+
parentBytes,
|
|
364
|
+
);
|
|
365
|
+
if (snapshot) return snapshot;
|
|
366
|
+
}
|
|
367
|
+
if (v1Membership) {
|
|
368
|
+
return __sampleLinuxCgroupHierarchyForTest(mountInfo, v1Membership, "cgroup", hostBytes, parentBytes);
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function __sampleWindowsJobMemoryForTest(
|
|
374
|
+
hostBytes: number,
|
|
375
|
+
parentBytes: number,
|
|
376
|
+
probeResult?: WindowsJobMemoryProbeResult,
|
|
377
|
+
): MemoryPressureSnapshot | null {
|
|
378
|
+
return sampleWindowsJobMemory(hostBytes, parentBytes, probeResult);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function sampleWindowsJobMemory(
|
|
382
|
+
hostBytes: number,
|
|
383
|
+
parentBytes: number,
|
|
384
|
+
result = safeProbeWindowsJobMemory(),
|
|
385
|
+
): MemoryPressureSnapshot | null {
|
|
386
|
+
if (result.kind !== "job_snapshot") return null;
|
|
387
|
+
const domains: MemoryPressureDomain[] = [];
|
|
388
|
+
const jobUsage = Number(result.jobMemoryUsedBytes);
|
|
389
|
+
const jobLimitRaw = result.jobMemoryLimitBytes;
|
|
390
|
+
const hasJobLimit = jobLimitRaw !== undefined && jobLimitRaw !== null;
|
|
391
|
+
const jobLimit = hasJobLimit ? Number(jobLimitRaw) : NaN;
|
|
392
|
+
if (Number.isSafeInteger(jobUsage) && jobUsage >= 0) {
|
|
393
|
+
if (hasJobLimit && Number.isSafeInteger(jobLimit) && jobLimit > 0) {
|
|
394
|
+
domains.push({
|
|
395
|
+
hardCapBytes: jobLimit,
|
|
396
|
+
totalUsageBytes: jobUsage,
|
|
397
|
+
source: "windows_job",
|
|
398
|
+
});
|
|
399
|
+
} else {
|
|
400
|
+
// Uncapped Job Object: usage participates against policy limit (no physical RAM clamp)
|
|
401
|
+
domains.push({
|
|
402
|
+
hardCapBytes: Number.MAX_SAFE_INTEGER,
|
|
403
|
+
totalUsageBytes: jobUsage,
|
|
404
|
+
source: "windows_job",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const processUsage = Number(result.processPrivateUsageBytes);
|
|
409
|
+
const processLimitRaw = result.processMemoryLimitBytes;
|
|
410
|
+
const hasProcessLimit = processLimitRaw !== undefined && processLimitRaw !== null;
|
|
411
|
+
const processLimit = hasProcessLimit ? Number(processLimitRaw) : NaN;
|
|
412
|
+
if (Number.isSafeInteger(processUsage) && processUsage >= 0) {
|
|
413
|
+
if (hasProcessLimit && Number.isSafeInteger(processLimit) && processLimit > 0) {
|
|
414
|
+
domains.push({
|
|
415
|
+
hardCapBytes: processLimit,
|
|
416
|
+
totalUsageBytes: processUsage,
|
|
417
|
+
source: "windows_process_job_limit",
|
|
418
|
+
});
|
|
419
|
+
} else {
|
|
420
|
+
domains.push({
|
|
421
|
+
hardCapBytes: Number.MAX_SAFE_INTEGER,
|
|
422
|
+
totalUsageBytes: processUsage,
|
|
423
|
+
source: "windows_process_job_limit",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const workingSetUsage = Number(result.processWorkingSetBytes);
|
|
428
|
+
if (Number.isSafeInteger(workingSetUsage) && workingSetUsage >= 0) {
|
|
429
|
+
domains.push({
|
|
430
|
+
hardCapBytes: hostBytes,
|
|
431
|
+
totalUsageBytes: Math.max(parentBytes, workingSetUsage),
|
|
432
|
+
source: "windows_process_job_limit",
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (domains.length === 0) return null;
|
|
436
|
+
const selected = domains.reduce((current, candidate) =>
|
|
437
|
+
candidate.totalUsageBytes / candidate.hardCapBytes > current.totalUsageBytes / current.hardCapBytes
|
|
438
|
+
? candidate
|
|
439
|
+
: current,
|
|
440
|
+
);
|
|
441
|
+
return {
|
|
442
|
+
...selected,
|
|
443
|
+
parentBytes,
|
|
444
|
+
domains,
|
|
445
|
+
};
|
|
197
446
|
}
|
|
198
447
|
|
|
199
|
-
function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
448
|
+
async function sampleMemoryPressure(): Promise<MemoryPressureSnapshot> {
|
|
449
|
+
const parentBytes = process.memoryUsage().rss;
|
|
450
|
+
const hostBytes = os.totalmem();
|
|
451
|
+
if (process.platform === "linux") {
|
|
452
|
+
const cgroup = await sampleLinuxCgroupMemory(hostBytes, parentBytes);
|
|
453
|
+
if (cgroup) return cgroup;
|
|
454
|
+
}
|
|
455
|
+
if (process.platform === "win32") {
|
|
456
|
+
const job = sampleWindowsJobMemory(hostBytes, parentBytes);
|
|
457
|
+
if (job) return job;
|
|
458
|
+
}
|
|
459
|
+
return { hardCapBytes: hostBytes, totalUsageBytes: parentBytes, parentBytes, source: "host" };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function __selectMemoryPressureDomainForTest(
|
|
463
|
+
snapshot: MemoryPressureSnapshot,
|
|
464
|
+
policyLimitBytes: number | null,
|
|
465
|
+
): MemoryPressureSnapshot {
|
|
466
|
+
const domains = snapshot.domains;
|
|
467
|
+
if (!domains || domains.length === 0) return snapshot;
|
|
468
|
+
const selected = domains.reduce((current, domain) => {
|
|
469
|
+
const currentLimit = Math.min(current.hardCapBytes, policyLimitBytes ?? current.hardCapBytes);
|
|
470
|
+
const domainLimit = Math.min(domain.hardCapBytes, policyLimitBytes ?? domain.hardCapBytes);
|
|
471
|
+
return domain.totalUsageBytes / domainLimit > current.totalUsageBytes / currentLimit ? domain : current;
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
...snapshot,
|
|
475
|
+
...selected,
|
|
476
|
+
totalUsageBytes: Math.max(snapshot.parentBytes, selected.totalUsageBytes),
|
|
477
|
+
};
|
|
204
478
|
}
|
|
205
479
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// generation demand (which may have deferred behind it) can be reconciled independently.
|
|
218
|
-
reconcileCurrentSchedule();
|
|
480
|
+
function sweepMemoryPressureGuard(d: ResourceGcDeps): Promise<void> | undefined {
|
|
481
|
+
let enabled = false;
|
|
482
|
+
for (const [sessionId, settings] of activeSessions) {
|
|
483
|
+
if (resolveMemoryGuardPolicy(settings).enabled) {
|
|
484
|
+
enabled = true;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
memoryGuardGcActive.delete(sessionId);
|
|
488
|
+
memoryGuardRestartAboveSince.delete(sessionId);
|
|
489
|
+
memoryGuardRestartCooldownUntil.delete(sessionId);
|
|
490
|
+
memoryGuardLastEvaluatedAt.delete(sessionId);
|
|
219
491
|
}
|
|
492
|
+
if (!enabled) return undefined;
|
|
493
|
+
return sweepEnabledMemoryPressureGuard(d);
|
|
220
494
|
}
|
|
221
495
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
496
|
+
async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void> {
|
|
497
|
+
const now = d.monotonicNow();
|
|
498
|
+
const dueSessions: Array<{ sessionId: string; policy: MemoryGuardPolicy }> = [];
|
|
499
|
+
for (const [sessionId, settings] of activeSessions) {
|
|
500
|
+
const policy = resolveMemoryGuardPolicy(settings);
|
|
501
|
+
if (!policy.enabled) {
|
|
502
|
+
memoryGuardGcActive.delete(sessionId);
|
|
503
|
+
memoryGuardRestartAboveSince.delete(sessionId);
|
|
504
|
+
memoryGuardRestartCooldownUntil.delete(sessionId);
|
|
505
|
+
memoryGuardLastEvaluatedAt.delete(sessionId);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const lastEvaluated = memoryGuardLastEvaluatedAt.get(sessionId);
|
|
509
|
+
if (lastEvaluated !== undefined && now - lastEvaluated < policy.checkIntervalMs) continue;
|
|
510
|
+
memoryGuardLastEvaluatedAt.set(sessionId, now);
|
|
511
|
+
dueSessions.push({ sessionId, policy });
|
|
512
|
+
}
|
|
513
|
+
if (dueSessions.length === 0) return;
|
|
514
|
+
|
|
515
|
+
const snapshot = await d.memorySnapshot();
|
|
516
|
+
let gcRequested = false;
|
|
517
|
+
const gcTelemetry: Array<{ sessionId: string } & Record<string, unknown>> = [];
|
|
518
|
+
for (const { sessionId, policy } of dueSessions) {
|
|
519
|
+
const pressure = __selectMemoryPressureDomainForTest(snapshot, policy.policyLimitBytes);
|
|
520
|
+
const limit = resolveEffectiveMemoryLimit({
|
|
521
|
+
hardCapBytes: pressure.hardCapBytes,
|
|
522
|
+
policyLimitBytes: policy.policyLimitBytes,
|
|
523
|
+
});
|
|
524
|
+
if (limit.effectiveBytes === null) {
|
|
525
|
+
memoryGuardGcActive.delete(sessionId);
|
|
526
|
+
memoryGuardRestartAboveSince.delete(sessionId);
|
|
527
|
+
memoryGuardRestartCooldownUntil.delete(sessionId);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const domain = computeMemoryGuardDomain({
|
|
531
|
+
effectiveLimitBytes: limit.effectiveBytes,
|
|
532
|
+
totalUsageBytes: pressure.totalUsageBytes,
|
|
533
|
+
parentBytes: pressure.parentBytes,
|
|
534
|
+
parentReserveBytes: policy.parentReserveBytes,
|
|
535
|
+
workers: [],
|
|
536
|
+
});
|
|
537
|
+
const decision = chooseMemoryGuardAction({
|
|
538
|
+
domain,
|
|
539
|
+
hostSupported: false,
|
|
540
|
+
workerSupported: () => false,
|
|
541
|
+
});
|
|
542
|
+
const usageRatio = pressure.totalUsageBytes / limit.effectiveBytes;
|
|
543
|
+
if (usageRatio >= policy.gcThresholdRatio) {
|
|
544
|
+
if (!memoryGuardGcActive.has(sessionId)) {
|
|
545
|
+
gcRequested = true;
|
|
546
|
+
gcTelemetry.push({
|
|
547
|
+
sessionId,
|
|
548
|
+
parentBytes: pressure.parentBytes,
|
|
549
|
+
totalUsageBytes: pressure.totalUsageBytes,
|
|
550
|
+
effectiveLimitBytes: limit.effectiveBytes,
|
|
551
|
+
domainSource: pressure.source,
|
|
552
|
+
limitSource: limit.source,
|
|
553
|
+
usageRatio,
|
|
554
|
+
decision: decision.kind,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
memoryGuardGcActive.delete(sessionId);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (usageRatio < policy.restartThresholdRatio) {
|
|
562
|
+
memoryGuardRestartAboveSince.delete(sessionId);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const aboveSince = memoryGuardRestartAboveSince.get(sessionId);
|
|
566
|
+
if (aboveSince === undefined) {
|
|
567
|
+
memoryGuardRestartAboveSince.set(sessionId, now);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const cooldownUntil = memoryGuardRestartCooldownUntil.get(sessionId) ?? 0;
|
|
571
|
+
if (now - aboveSince < policy.restartThresholdWindowMs || now < cooldownUntil) continue;
|
|
572
|
+
memoryGuardRestartCooldownUntil.set(sessionId, now + policy.cooldownMs);
|
|
573
|
+
d.logWarn("Memory guard: restart threshold sustained; restart remains advisory-only", {
|
|
574
|
+
sessionId,
|
|
575
|
+
parentBytes: pressure.parentBytes,
|
|
576
|
+
totalUsageBytes: pressure.totalUsageBytes,
|
|
577
|
+
effectiveLimitBytes: limit.effectiveBytes,
|
|
578
|
+
domainSource: pressure.source,
|
|
579
|
+
limitSource: limit.source,
|
|
580
|
+
usageRatio,
|
|
581
|
+
windowMs: policy.restartThresholdWindowMs,
|
|
582
|
+
cooldownMs: policy.cooldownMs,
|
|
583
|
+
decision: decision.kind,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (gcRequested) {
|
|
587
|
+
try {
|
|
588
|
+
d.runGc();
|
|
589
|
+
for (const { sessionId, ...telemetry } of gcTelemetry) {
|
|
590
|
+
memoryGuardGcActive.add(sessionId);
|
|
591
|
+
d.logWarn("Memory guard: GC threshold reached", { sessionId, ...telemetry });
|
|
592
|
+
}
|
|
593
|
+
} catch (error) {
|
|
594
|
+
d.logWarn("Memory guard: GC invocation failed; latch not set", { error: String(error) });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
226
597
|
}
|
|
227
598
|
|
|
228
599
|
function ownerBrowserPolicy(snapshot: TabGcSnapshot): BrowserGcPolicy | null {
|
|
@@ -323,22 +694,26 @@ async function sweepScreenshots(d: ResourceGcDeps): Promise<void> {
|
|
|
323
694
|
|
|
324
695
|
// ── Test-only seams ─────────────────────────────────────────────────────────────────────────
|
|
325
696
|
export function __setResourceGcDepsForTest(overrides: Partial<ResourceGcDeps>): void {
|
|
326
|
-
deps = {
|
|
697
|
+
deps = {
|
|
698
|
+
...defaultDeps,
|
|
699
|
+
...overrides,
|
|
700
|
+
monotonicNow: overrides.monotonicNow ?? overrides.now ?? defaultDeps.monotonicNow,
|
|
701
|
+
};
|
|
327
702
|
}
|
|
328
703
|
|
|
329
704
|
export function __setResourceGcSchedulerNowForTest(now: () => number): void {
|
|
330
|
-
|
|
705
|
+
scheduler.setSchedulerNowForTest(now);
|
|
331
706
|
}
|
|
332
707
|
|
|
333
708
|
export async function __runResourceGcTickForTest(): Promise<void> {
|
|
334
|
-
await runTick();
|
|
709
|
+
await scheduler.runTick();
|
|
335
710
|
}
|
|
336
711
|
|
|
337
712
|
export async function __runResourceGcTimerCallbackForTest(
|
|
338
713
|
owner: { generation: number; token: number },
|
|
339
714
|
deadline: number,
|
|
340
715
|
): Promise<void> {
|
|
341
|
-
await
|
|
716
|
+
await scheduler.runTimerCallbackForTest(owner, deadline);
|
|
342
717
|
}
|
|
343
718
|
|
|
344
719
|
export function __getResourceGcStateForTest(): {
|
|
@@ -353,28 +728,29 @@ export function __getResourceGcStateForTest(): {
|
|
|
353
728
|
deferredGeneration: number | null;
|
|
354
729
|
activeGeneration: number | null;
|
|
355
730
|
} {
|
|
731
|
+
const state = scheduler.getStateForTest();
|
|
356
732
|
return {
|
|
357
|
-
timerActive:
|
|
733
|
+
timerActive: state.timerActive,
|
|
358
734
|
sessionCount: activeSessions.size,
|
|
359
735
|
rssWarningActive,
|
|
360
|
-
inProgress:
|
|
361
|
-
generation:
|
|
362
|
-
pendingDeadline,
|
|
363
|
-
pendingOwner: pendingOwner
|
|
364
|
-
deferredDeadline:
|
|
365
|
-
deferredGeneration:
|
|
366
|
-
activeGeneration:
|
|
736
|
+
inProgress: state.inProgress,
|
|
737
|
+
generation: state.generation,
|
|
738
|
+
pendingDeadline: state.pendingDeadline,
|
|
739
|
+
pendingOwner: state.pendingOwner,
|
|
740
|
+
deferredDeadline: state.deferredDeadline,
|
|
741
|
+
deferredGeneration: state.deferredGeneration,
|
|
742
|
+
activeGeneration: state.activeGeneration,
|
|
367
743
|
};
|
|
368
744
|
}
|
|
369
745
|
|
|
370
746
|
export function __resetResourceGcForTest(): void {
|
|
371
|
-
|
|
747
|
+
scheduler.resetForTest();
|
|
372
748
|
activeSessions.clear();
|
|
373
|
-
inProgressOwner = null;
|
|
374
|
-
deferredSchedule = null;
|
|
375
749
|
rssWarningActive = false;
|
|
750
|
+
memoryGuardGcActive.clear();
|
|
751
|
+
memoryGuardRestartAboveSince.clear();
|
|
752
|
+
memoryGuardRestartCooldownUntil.clear();
|
|
753
|
+
memoryGuardLastEvaluatedAt.clear();
|
|
376
754
|
lastScreenshotScanAt = 0;
|
|
377
|
-
nextTimerToken = 0;
|
|
378
|
-
schedulerNow = (): number => performance.now();
|
|
379
755
|
deps = defaultDeps;
|
|
380
756
|
}
|