crewly 1.20.359 → 1.20.360
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/config/roles/developer/prompt.md +3 -1
- package/config/skills/registry.json +125 -1
- package/dist/backend/backend/src/constants.d.ts +130 -2
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +141 -3
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts +37 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +103 -10
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/boot-pressure.d.ts +147 -0
- package/dist/backend/backend/src/services/agent/boot-pressure.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/boot-pressure.js +212 -0
- package/dist/backend/backend/src/services/agent/boot-pressure.js.map +1 -0
- package/dist/backend/backend/src/services/agent/restore-queue.d.ts +50 -1
- package/dist/backend/backend/src/services/agent/restore-queue.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/restore-queue.js +100 -5
- package/dist/backend/backend/src/services/agent/restore-queue.js.map +1 -1
- package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.d.ts +11 -0
- package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.js +23 -2
- package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/deferred-start-retry.d.ts +8 -0
- package/dist/backend/backend/src/services/messaging/deferred-start-retry.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/deferred-start-retry.js +8 -0
- package/dist/backend/backend/src/services/messaging/deferred-start-retry.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-log-entries.d.ts +7 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-entries.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-log-entries.js +10 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-entries.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.d.ts +24 -0
- package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.js +75 -9
- package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.d.ts +19 -0
- package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.js +23 -2
- package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/work-record.service.d.ts +23 -1
- package/dist/backend/backend/src/services/wiki/work-record.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/work-record.service.js +33 -5
- package/dist/backend/backend/src/services/wiki/work-record.service.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/low-disk-guard.d.ts +92 -8
- package/dist/backend/backend/src/services/worktree/low-disk-guard.d.ts.map +1 -1
- package/dist/backend/backend/src/services/worktree/low-disk-guard.js +98 -14
- package/dist/backend/backend/src/services/worktree/low-disk-guard.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/tmp-janitor.d.ts +138 -0
- package/dist/backend/backend/src/services/worktree/tmp-janitor.d.ts.map +1 -0
- package/dist/backend/backend/src/services/worktree/tmp-janitor.js +199 -0
- package/dist/backend/backend/src/services/worktree/tmp-janitor.js.map +1 -0
- package/dist/backend/backend/src/services/worktree/transcript-janitor.d.ts +154 -0
- package/dist/backend/backend/src/services/worktree/transcript-janitor.d.ts.map +1 -0
- package/dist/backend/backend/src/services/worktree/transcript-janitor.js +357 -0
- package/dist/backend/backend/src/services/worktree/transcript-janitor.js.map +1 -0
- package/dist/backend/backend/src/services/worktree/workitem-worktree.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/worktree/workitem-worktree.service.js +7 -2
- package/dist/backend/backend/src/services/worktree/workitem-worktree.service.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-dirty.d.ts +16 -4
- package/dist/backend/backend/src/services/worktree/worktree-dirty.d.ts.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-dirty.js +21 -5
- package/dist/backend/backend/src/services/worktree/worktree-dirty.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-git.d.ts +18 -0
- package/dist/backend/backend/src/services/worktree/worktree-git.d.ts.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-git.js +60 -0
- package/dist/backend/backend/src/services/worktree/worktree-git.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts +164 -25
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js +494 -55
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-tiers.d.ts +186 -0
- package/dist/backend/backend/src/services/worktree/worktree-tiers.d.ts.map +1 -0
- package/dist/backend/backend/src/services/worktree/worktree-tiers.js +279 -0
- package/dist/backend/backend/src/services/worktree/worktree-tiers.js.map +1 -0
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +130 -2
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +141 -3
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/boot-pressure.d.ts +147 -0
- package/dist/cli/backend/src/services/agent/boot-pressure.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/boot-pressure.js +212 -0
- package/dist/cli/backend/src/services/agent/boot-pressure.js.map +1 -0
- package/dist/cli/backend/src/services/agent/restore-queue.d.ts +50 -1
- package/dist/cli/backend/src/services/agent/restore-queue.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/restore-queue.js +100 -5
- package/dist/cli/backend/src/services/agent/restore-queue.js.map +1 -1
- package/dist/cli/backend/src/services/wiki/wiki-log-entries.d.ts +7 -0
- package/dist/cli/backend/src/services/wiki/wiki-log-entries.d.ts.map +1 -1
- package/dist/cli/backend/src/services/wiki/wiki-log-entries.js +10 -0
- package/dist/cli/backend/src/services/wiki/wiki-log-entries.js.map +1 -1
- package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts +92 -8
- package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts.map +1 -1
- package/dist/cli/backend/src/services/worktree/low-disk-guard.js +98 -14
- package/dist/cli/backend/src/services/worktree/low-disk-guard.js.map +1 -1
- package/dist/cli/backend/src/services/worktree/worktree-git.d.ts +18 -0
- package/dist/cli/backend/src/services/worktree/worktree-git.d.ts.map +1 -1
- package/dist/cli/backend/src/services/worktree/worktree-git.js +60 -0
- package/dist/cli/backend/src/services/worktree/worktree-git.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-aware boot (crewly#1353): the snapshot the boot restore and the boot
|
|
3
|
+
* maintenance passes read, the gate that holds a maintenance pass until the
|
|
4
|
+
* restore queue is drained and the machine is calm, the pacer that spaces
|
|
5
|
+
* launches across the restore queue and the deferred-start retry, and the
|
|
6
|
+
* one-line owner note when boot restores were held back for long.
|
|
7
|
+
*
|
|
8
|
+
* Pressure mode (ResourceModeService) needs two tight 30 s samples before it
|
|
9
|
+
* engages; the first minute after a boot is exactly when everything starts at
|
|
10
|
+
* once, so the boot reads the raw memory reading as well.
|
|
11
|
+
*
|
|
12
|
+
* @module services/agent/boot-pressure
|
|
13
|
+
*/
|
|
14
|
+
/** What the boot looks at before a launch or a maintenance pass. */
|
|
15
|
+
export interface BootPressureSnapshot {
|
|
16
|
+
usedPercent: number;
|
|
17
|
+
freeMB: number;
|
|
18
|
+
swapUsedPercent: number;
|
|
19
|
+
osPressure: boolean;
|
|
20
|
+
/** ResourceModeService is in pressure mode */
|
|
21
|
+
pressureMode: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Thresholds, with the optional env overrides applied. */
|
|
24
|
+
export interface BootPressureThresholds {
|
|
25
|
+
swapUsedPercent: number;
|
|
26
|
+
minFreeMB: number;
|
|
27
|
+
launchSpacingMs: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Thresholds from the constants block, with env overrides.
|
|
31
|
+
*
|
|
32
|
+
* @param env - Environment (tests inject one)
|
|
33
|
+
* @returns Effective thresholds
|
|
34
|
+
*/
|
|
35
|
+
export declare function bootPressureThresholds(env?: NodeJS.ProcessEnv): BootPressureThresholds;
|
|
36
|
+
/**
|
|
37
|
+
* Why the snapshot is tight for the boot, or null when it is calm. Tight:
|
|
38
|
+
* pressure mode, swap at or above the threshold, or free memory under the
|
|
39
|
+
* floor. An unreadable snapshot (null) is calm: a boot must not stall on a
|
|
40
|
+
* reading that failed.
|
|
41
|
+
*
|
|
42
|
+
* @param snapshot - Reading, or null when unavailable
|
|
43
|
+
* @param thresholds - Effective thresholds
|
|
44
|
+
* @returns One-line reason, or null
|
|
45
|
+
*/
|
|
46
|
+
export declare function bootPressureReason(snapshot: BootPressureSnapshot | null, thresholds?: BootPressureThresholds): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* The live snapshot: ResourceModeService's memory reading and mode. Null when
|
|
49
|
+
* the reading throws or memory is unknown (totalMB 0).
|
|
50
|
+
*
|
|
51
|
+
* @returns Snapshot, or null
|
|
52
|
+
*/
|
|
53
|
+
export declare function readBootPressureSnapshot(): BootPressureSnapshot | null;
|
|
54
|
+
/**
|
|
55
|
+
* Remembers when the last boot launch began, so the restore queue and the
|
|
56
|
+
* deferred-start retry keep the launch spacing between them.
|
|
57
|
+
*/
|
|
58
|
+
export declare class BootLaunchPacer {
|
|
59
|
+
private readonly now;
|
|
60
|
+
private lastAt;
|
|
61
|
+
constructor(now?: () => number);
|
|
62
|
+
/** A launch began now. */
|
|
63
|
+
note(): void;
|
|
64
|
+
/** Milliseconds since the last launch began (Infinity when none yet). */
|
|
65
|
+
msSinceLast(): number;
|
|
66
|
+
}
|
|
67
|
+
/** The process-wide pacer. */
|
|
68
|
+
export declare function getBootLaunchPacer(): BootLaunchPacer;
|
|
69
|
+
/** Test hook: drop the pacer. */
|
|
70
|
+
export declare function resetBootLaunchPacer(): void;
|
|
71
|
+
/** What {@link bootLaunchHoldReason} looks at. */
|
|
72
|
+
export interface BootLaunchHoldDeps {
|
|
73
|
+
snapshot: () => BootPressureSnapshot | null;
|
|
74
|
+
/** The restore queue is still launching agents */
|
|
75
|
+
restoreRunning: () => boolean;
|
|
76
|
+
pacer: BootLaunchPacer;
|
|
77
|
+
/** Epoch ms of the boot */
|
|
78
|
+
bootAt: number;
|
|
79
|
+
thresholds?: BootPressureThresholds;
|
|
80
|
+
now?: () => number;
|
|
81
|
+
holdMaxMs?: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Why a boot launch outside the restore queue (the deferred-start retry) must
|
|
85
|
+
* wait this round: memory is tight (only within `holdMaxMs` of the boot), the
|
|
86
|
+
* restore queue is still launching, or another boot launch began less than
|
|
87
|
+
* the launch spacing ago. Null = go.
|
|
88
|
+
*
|
|
89
|
+
* @param deps - Snapshot, restore state, pacer, boot time
|
|
90
|
+
* @returns Reason to hold, or null
|
|
91
|
+
*/
|
|
92
|
+
export declare function bootLaunchHoldReason(deps: BootLaunchHoldDeps): string | null;
|
|
93
|
+
/** What {@link waitForBootQuiet} needs. */
|
|
94
|
+
export interface BootQuietGateDeps {
|
|
95
|
+
/** The boot restore is over (queue drained, or there was nothing to restore). */
|
|
96
|
+
restoreDone: () => boolean;
|
|
97
|
+
/** Current reading (default: the live one). */
|
|
98
|
+
snapshot?: () => BootPressureSnapshot | null;
|
|
99
|
+
thresholds?: BootPressureThresholds;
|
|
100
|
+
sleep?: (ms: number) => Promise<void>;
|
|
101
|
+
now?: () => number;
|
|
102
|
+
pollMs?: number;
|
|
103
|
+
maxWaitMs?: number;
|
|
104
|
+
}
|
|
105
|
+
/** Outcome of the gate. */
|
|
106
|
+
export interface BootQuietGateResult {
|
|
107
|
+
waitedMs: number;
|
|
108
|
+
/** The gate gave up waiting; the pass should run reduced. */
|
|
109
|
+
reduced: boolean;
|
|
110
|
+
/** Why it was still waiting when it gave up */
|
|
111
|
+
reason?: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Hold a boot maintenance pass until the restore queue is drained and the
|
|
115
|
+
* machine is not tight, polling every `pollMs`. After `maxWaitMs` it gives
|
|
116
|
+
* up and returns `reduced: true` so the pass runs with a smaller batch.
|
|
117
|
+
*
|
|
118
|
+
* @param deps - Restore state, snapshot, clock
|
|
119
|
+
* @returns How long it waited and whether the pass should run reduced
|
|
120
|
+
*/
|
|
121
|
+
export declare function waitForBootQuiet(deps: BootQuietGateDeps): Promise<BootQuietGateResult>;
|
|
122
|
+
/**
|
|
123
|
+
* Names as a sentence fragment: "Ella", "Ella and Pia", "Ella, Pia and Owen".
|
|
124
|
+
*
|
|
125
|
+
* @param names - Agent names
|
|
126
|
+
* @returns Joined names
|
|
127
|
+
*/
|
|
128
|
+
export declare function joinNames(names: readonly string[]): string;
|
|
129
|
+
/** What {@link scheduleHeldBackNotice} needs. */
|
|
130
|
+
export interface HeldBackNoticeDeps {
|
|
131
|
+
/** Agents held back by the boot that are still not running */
|
|
132
|
+
stillHeld: () => Promise<string[]>;
|
|
133
|
+
/** Sends one line to the owner; false when nothing is connected */
|
|
134
|
+
notify: (line: string) => Promise<boolean>;
|
|
135
|
+
afterMs?: number;
|
|
136
|
+
setTimer?: (fn: () => void, ms: number) => void;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* One line to the owner, once, if agents the boot held back for memory are
|
|
140
|
+
* still down `afterMs` later. Nothing is sent when they came back in time.
|
|
141
|
+
* The line names the agents and says they will return; no mechanics.
|
|
142
|
+
*
|
|
143
|
+
* @param deps - Held-back check and notifier
|
|
144
|
+
* @returns Resolves when the check ran (tests); callers do not await it
|
|
145
|
+
*/
|
|
146
|
+
export declare function scheduleHeldBackNotice(deps: HeldBackNoticeDeps): Promise<void>;
|
|
147
|
+
//# sourceMappingURL=boot-pressure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boot-pressure.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/agent/boot-pressure.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-aware boot (crewly#1353): the snapshot the boot restore and the boot
|
|
3
|
+
* maintenance passes read, the gate that holds a maintenance pass until the
|
|
4
|
+
* restore queue is drained and the machine is calm, the pacer that spaces
|
|
5
|
+
* launches across the restore queue and the deferred-start retry, and the
|
|
6
|
+
* one-line owner note when boot restores were held back for long.
|
|
7
|
+
*
|
|
8
|
+
* Pressure mode (ResourceModeService) needs two tight 30 s samples before it
|
|
9
|
+
* engages; the first minute after a boot is exactly when everything starts at
|
|
10
|
+
* once, so the boot reads the raw memory reading as well.
|
|
11
|
+
*
|
|
12
|
+
* @module services/agent/boot-pressure
|
|
13
|
+
*/
|
|
14
|
+
import { BOOT_PRESSURE_CONSTANTS } from '../../constants.js';
|
|
15
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
16
|
+
import { ResourceModeService } from './resource-mode.service.js';
|
|
17
|
+
/**
|
|
18
|
+
* Positive integer from an env var, or the fallback.
|
|
19
|
+
*
|
|
20
|
+
* @param raw - Env value
|
|
21
|
+
* @param fallback - Default
|
|
22
|
+
* @returns The parsed value or the fallback
|
|
23
|
+
*/
|
|
24
|
+
function envInt(raw, fallback) {
|
|
25
|
+
const n = Number.parseInt(raw ?? '', 10);
|
|
26
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Thresholds from the constants block, with env overrides.
|
|
30
|
+
*
|
|
31
|
+
* @param env - Environment (tests inject one)
|
|
32
|
+
* @returns Effective thresholds
|
|
33
|
+
*/
|
|
34
|
+
export function bootPressureThresholds(env = process.env) {
|
|
35
|
+
return {
|
|
36
|
+
swapUsedPercent: envInt(env[BOOT_PRESSURE_CONSTANTS.ENV_SWAP_PERCENT], BOOT_PRESSURE_CONSTANTS.SWAP_USED_PERCENT),
|
|
37
|
+
minFreeMB: envInt(env[BOOT_PRESSURE_CONSTANTS.ENV_MIN_FREE_MB], BOOT_PRESSURE_CONSTANTS.MIN_FREE_MB),
|
|
38
|
+
launchSpacingMs: envInt(env[BOOT_PRESSURE_CONSTANTS.ENV_LAUNCH_SPACING_MS], BOOT_PRESSURE_CONSTANTS.MIN_LAUNCH_SPACING_MS),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Why the snapshot is tight for the boot, or null when it is calm. Tight:
|
|
43
|
+
* pressure mode, swap at or above the threshold, or free memory under the
|
|
44
|
+
* floor. An unreadable snapshot (null) is calm: a boot must not stall on a
|
|
45
|
+
* reading that failed.
|
|
46
|
+
*
|
|
47
|
+
* @param snapshot - Reading, or null when unavailable
|
|
48
|
+
* @param thresholds - Effective thresholds
|
|
49
|
+
* @returns One-line reason, or null
|
|
50
|
+
*/
|
|
51
|
+
export function bootPressureReason(snapshot, thresholds = bootPressureThresholds()) {
|
|
52
|
+
if (!snapshot)
|
|
53
|
+
return null;
|
|
54
|
+
const why = [];
|
|
55
|
+
if (snapshot.pressureMode)
|
|
56
|
+
why.push('pressure mode');
|
|
57
|
+
if (snapshot.swapUsedPercent >= thresholds.swapUsedPercent)
|
|
58
|
+
why.push(`swap ${snapshot.swapUsedPercent}% >= ${thresholds.swapUsedPercent}%`);
|
|
59
|
+
if (snapshot.freeMB < thresholds.minFreeMB)
|
|
60
|
+
why.push(`free ${snapshot.freeMB}MB < ${thresholds.minFreeMB}MB`);
|
|
61
|
+
if (why.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
return `${why.join(', ')} (used=${snapshot.usedPercent}% free=${snapshot.freeMB}MB swap=${snapshot.swapUsedPercent}% osPressure=${snapshot.osPressure})`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The live snapshot: ResourceModeService's memory reading and mode. Null when
|
|
67
|
+
* the reading throws or memory is unknown (totalMB 0).
|
|
68
|
+
*
|
|
69
|
+
* @returns Snapshot, or null
|
|
70
|
+
*/
|
|
71
|
+
export function readBootPressureSnapshot() {
|
|
72
|
+
try {
|
|
73
|
+
const rm = ResourceModeService.getInstance();
|
|
74
|
+
const mem = rm.memoryStats();
|
|
75
|
+
if (!mem.totalMB)
|
|
76
|
+
return null;
|
|
77
|
+
return {
|
|
78
|
+
usedPercent: mem.usedPercent,
|
|
79
|
+
freeMB: mem.freeMB,
|
|
80
|
+
swapUsedPercent: mem.swapUsedPercent ?? 0,
|
|
81
|
+
osPressure: mem.pressureElevated === true,
|
|
82
|
+
pressureMode: rm.getMode() === 'pressure',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Remembers when the last boot launch began, so the restore queue and the
|
|
91
|
+
* deferred-start retry keep the launch spacing between them.
|
|
92
|
+
*/
|
|
93
|
+
export class BootLaunchPacer {
|
|
94
|
+
now;
|
|
95
|
+
lastAt = null;
|
|
96
|
+
constructor(now = Date.now) {
|
|
97
|
+
this.now = now;
|
|
98
|
+
}
|
|
99
|
+
/** A launch began now. */
|
|
100
|
+
note() {
|
|
101
|
+
this.lastAt = this.now();
|
|
102
|
+
}
|
|
103
|
+
/** Milliseconds since the last launch began (Infinity when none yet). */
|
|
104
|
+
msSinceLast() {
|
|
105
|
+
return this.lastAt === null ? Infinity : this.now() - this.lastAt;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
let pacer = null;
|
|
109
|
+
/** The process-wide pacer. */
|
|
110
|
+
export function getBootLaunchPacer() {
|
|
111
|
+
return (pacer ??= new BootLaunchPacer());
|
|
112
|
+
}
|
|
113
|
+
/** Test hook: drop the pacer. */
|
|
114
|
+
export function resetBootLaunchPacer() {
|
|
115
|
+
pacer = null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Why a boot launch outside the restore queue (the deferred-start retry) must
|
|
119
|
+
* wait this round: memory is tight (only within `holdMaxMs` of the boot), the
|
|
120
|
+
* restore queue is still launching, or another boot launch began less than
|
|
121
|
+
* the launch spacing ago. Null = go.
|
|
122
|
+
*
|
|
123
|
+
* @param deps - Snapshot, restore state, pacer, boot time
|
|
124
|
+
* @returns Reason to hold, or null
|
|
125
|
+
*/
|
|
126
|
+
export function bootLaunchHoldReason(deps) {
|
|
127
|
+
const now = deps.now ?? Date.now;
|
|
128
|
+
const thresholds = deps.thresholds ?? bootPressureThresholds();
|
|
129
|
+
const holdMaxMs = deps.holdMaxMs ?? BOOT_PRESSURE_CONSTANTS.RETRY_HOLD_MAX_MS;
|
|
130
|
+
if (now() - deps.bootAt < holdMaxMs) {
|
|
131
|
+
const tight = bootPressureReason(deps.snapshot(), thresholds);
|
|
132
|
+
if (tight)
|
|
133
|
+
return `memory tight: ${tight}`;
|
|
134
|
+
}
|
|
135
|
+
if (deps.restoreRunning())
|
|
136
|
+
return 'boot restore queue still launching';
|
|
137
|
+
const since = deps.pacer.msSinceLast();
|
|
138
|
+
if (since < thresholds.launchSpacingMs)
|
|
139
|
+
return `a boot launch began ${Math.round(since / 1000)} s ago`;
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const defaultSleep = (ms) => new Promise((resolve) => { const t = setTimeout(resolve, ms); t.unref?.(); });
|
|
143
|
+
/**
|
|
144
|
+
* Hold a boot maintenance pass until the restore queue is drained and the
|
|
145
|
+
* machine is not tight, polling every `pollMs`. After `maxWaitMs` it gives
|
|
146
|
+
* up and returns `reduced: true` so the pass runs with a smaller batch.
|
|
147
|
+
*
|
|
148
|
+
* @param deps - Restore state, snapshot, clock
|
|
149
|
+
* @returns How long it waited and whether the pass should run reduced
|
|
150
|
+
*/
|
|
151
|
+
export async function waitForBootQuiet(deps) {
|
|
152
|
+
const snapshot = deps.snapshot ?? readBootPressureSnapshot;
|
|
153
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
154
|
+
const now = deps.now ?? Date.now;
|
|
155
|
+
const pollMs = deps.pollMs ?? BOOT_PRESSURE_CONSTANTS.MAINTENANCE_POLL_MS;
|
|
156
|
+
const maxWaitMs = deps.maxWaitMs ?? BOOT_PRESSURE_CONSTANTS.MAINTENANCE_WAIT_MAX_MS;
|
|
157
|
+
const started = now();
|
|
158
|
+
for (;;) {
|
|
159
|
+
const tight = bootPressureReason(snapshot(), deps.thresholds);
|
|
160
|
+
const reason = !deps.restoreDone() ? `restore queue still running${tight ? `; ${tight}` : ''}` : tight;
|
|
161
|
+
if (!reason)
|
|
162
|
+
return { waitedMs: now() - started, reduced: false };
|
|
163
|
+
if (now() - started >= maxWaitMs)
|
|
164
|
+
return { waitedMs: now() - started, reduced: true, reason };
|
|
165
|
+
await sleep(pollMs);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Names as a sentence fragment: "Ella", "Ella and Pia", "Ella, Pia and Owen".
|
|
170
|
+
*
|
|
171
|
+
* @param names - Agent names
|
|
172
|
+
* @returns Joined names
|
|
173
|
+
*/
|
|
174
|
+
export function joinNames(names) {
|
|
175
|
+
if (names.length <= 1)
|
|
176
|
+
return names[0] ?? '';
|
|
177
|
+
return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* One line to the owner, once, if agents the boot held back for memory are
|
|
181
|
+
* still down `afterMs` later. Nothing is sent when they came back in time.
|
|
182
|
+
* The line names the agents and says they will return; no mechanics.
|
|
183
|
+
*
|
|
184
|
+
* @param deps - Held-back check and notifier
|
|
185
|
+
* @returns Resolves when the check ran (tests); callers do not await it
|
|
186
|
+
*/
|
|
187
|
+
export function scheduleHeldBackNotice(deps) {
|
|
188
|
+
const logger = LoggerService.getInstance().createComponentLogger('BootPressure');
|
|
189
|
+
const afterMs = deps.afterMs ?? BOOT_PRESSURE_CONSTANTS.OWNER_NOTE_AFTER_MS;
|
|
190
|
+
const setTimer = deps.setTimer ?? ((fn, ms) => { const t = setTimeout(fn, ms); t.unref?.(); });
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
setTimer(() => {
|
|
193
|
+
void (async () => {
|
|
194
|
+
try {
|
|
195
|
+
const held = await deps.stillHeld();
|
|
196
|
+
if (held.length === 0)
|
|
197
|
+
return;
|
|
198
|
+
const line = `Memory is tight on this machine, so ${joinNames(held)} ${held.length === 1 ? 'has' : 'have'} not been brought back yet; ${held.length === 1 ? 'it' : 'they'} will start as soon as it eases.`;
|
|
199
|
+
const sent = await deps.notify(line);
|
|
200
|
+
logger.info('Told the owner about boot restores held back for memory', { held, sent });
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
logger.warn('Could not tell the owner about held-back boot restores', { error: err instanceof Error ? err.message : String(err) });
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
resolve();
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
209
|
+
}, afterMs);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=boot-pressure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boot-pressure.js","sourceRoot":"","sources":["../../../../../../backend/src/services/agent/boot-pressure.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module services/agent/restore-queue
|
|
8
8
|
*/
|
|
9
|
+
import { type BootPressureSnapshot, type BootPressureThresholds } from './boot-pressure.js';
|
|
9
10
|
/** Defaults for the restore queue. */
|
|
10
11
|
export declare const RESTORE_QUEUE_CONSTANTS: {
|
|
11
12
|
readonly DEFAULT_CONCURRENCY: 1;
|
|
@@ -36,6 +37,28 @@ export interface RestoreEntry {
|
|
|
36
37
|
}>;
|
|
37
38
|
/** Called right after the agent registered */
|
|
38
39
|
onReady?: () => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Launched even while memory is tight (crewly#1353): the owner waits on it
|
|
42
|
+
* (owner message, owed promise, owner thread) or it is the orchestrator.
|
|
43
|
+
* Everything else is deferred to the retry when the machine is tight.
|
|
44
|
+
*/
|
|
45
|
+
essential?: boolean;
|
|
46
|
+
/** The queue handed the agent to the deferred-start retry instead of launching it (tight memory) */
|
|
47
|
+
onDeferred?: (snapshot: BootPressureSnapshot | null, reason: string) => void;
|
|
48
|
+
}
|
|
49
|
+
/** Memory awareness and pacing of the queue (tests inject fakes). */
|
|
50
|
+
export interface RestoreQueueOptions {
|
|
51
|
+
/** Current memory reading (default: the live one; null = unknown, treated as calm) */
|
|
52
|
+
snapshot?: () => BootPressureSnapshot | null;
|
|
53
|
+
thresholds?: BootPressureThresholds;
|
|
54
|
+
now?: () => number;
|
|
55
|
+
sleep?: (ms: number) => Promise<void>;
|
|
56
|
+
/** A launch began (default: the process-wide pacer) */
|
|
57
|
+
onLaunch?: () => void;
|
|
58
|
+
settleMs?: number;
|
|
59
|
+
pollMs?: number;
|
|
60
|
+
holdMaxMs?: number;
|
|
61
|
+
pressureConcurrency?: number;
|
|
39
62
|
}
|
|
40
63
|
/** Progress counters exposed in /health. */
|
|
41
64
|
export interface RestoreQueueStats {
|
|
@@ -45,6 +68,8 @@ export interface RestoreQueueStats {
|
|
|
45
68
|
ready: number;
|
|
46
69
|
failed: number;
|
|
47
70
|
timedOut: number;
|
|
71
|
+
/** Handed to the deferred-start retry because memory was tight */
|
|
72
|
+
deferred: number;
|
|
48
73
|
order: string[];
|
|
49
74
|
}
|
|
50
75
|
/**
|
|
@@ -71,7 +96,18 @@ export declare class RestoreQueue {
|
|
|
71
96
|
private running;
|
|
72
97
|
private done;
|
|
73
98
|
private readonly logger;
|
|
74
|
-
|
|
99
|
+
private readonly snapshot;
|
|
100
|
+
private readonly thresholds;
|
|
101
|
+
private readonly now;
|
|
102
|
+
private readonly sleep;
|
|
103
|
+
private readonly onLaunch;
|
|
104
|
+
private readonly settleMs;
|
|
105
|
+
private readonly pollMs;
|
|
106
|
+
private readonly holdMaxMs;
|
|
107
|
+
private readonly pressureConcurrency;
|
|
108
|
+
/** When the last launch settled (registered, failed or timed out); spacing counts from here */
|
|
109
|
+
private lastSettledAt;
|
|
110
|
+
constructor(concurrency?: number, timeoutMs?: number, opts?: RestoreQueueOptions);
|
|
75
111
|
/**
|
|
76
112
|
* Plan and run the restore. Resolves when every entry is ready, failed or
|
|
77
113
|
* out of retries. Returns the same promise while already running.
|
|
@@ -96,6 +132,19 @@ export declare class RestoreQueue {
|
|
|
96
132
|
/** Progress for /health. */
|
|
97
133
|
stats(): RestoreQueueStats;
|
|
98
134
|
private worker;
|
|
135
|
+
/**
|
|
136
|
+
* Memory is tight and nobody waits on this agent: hand it to the
|
|
137
|
+
* deferred-start retry (it is owed something, so it is not dropped) and
|
|
138
|
+
* log the reading it was deferred on.
|
|
139
|
+
*/
|
|
140
|
+
private defer;
|
|
141
|
+
/**
|
|
142
|
+
* Wait before a launch: always the launch spacing since the previous one
|
|
143
|
+
* settled (RSS growth is visible before the next); under pressure also the
|
|
144
|
+
* settle time, the pressure concurrency, and the snapshot clearing (at most
|
|
145
|
+
* `holdMaxMs`, then the owner-waiting agent launches anyway).
|
|
146
|
+
*/
|
|
147
|
+
private paceBeforeLaunch;
|
|
99
148
|
private runSlot;
|
|
100
149
|
}
|
|
101
150
|
/** The process-wide restore queue. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"restore-queue.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/agent/restore-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,sCAAsC;AACtC,eAAO,MAAM,uBAAuB;;;IAGnC,sEAAsE;;IAEtE,6EAA6E;;CAEpE,CAAC;AAEX,4CAA4C;AAC5C,eAAO,MAAM,YAAY;;;;;CAA4D,CAAC;AAEtF,4BAA4B;AAC5B,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACxD,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"restore-queue.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/agent/restore-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAKN,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,MAAM,oBAAoB,CAAC;AAE5B,sCAAsC;AACtC,eAAO,MAAM,uBAAuB;;;IAGnC,sEAAsE;;IAEtE,6EAA6E;;CAEpE,CAAC;AAEX,4CAA4C;AAC5C,eAAO,MAAM,YAAY;;;;;CAA4D,CAAC;AAEtF,4BAA4B;AAC5B,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACxD,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oGAAoG;IACpG,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7E;AAED,qEAAqE;AACrE,MAAM,WAAW,mBAAmB;IACnC,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,oBAAoB,GAAG,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,GAAE,MAAM,GAAG,SAAkD,GAAG,MAAM,CAIlH;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,YAAY,EAAE,CAQpF;AAID,wBAAwB;AACxB,qBAAa,YAAY;IAoBvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IApB3B,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,QAAQ,CAAiE;IACjF,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,IAAI,CAAoC;IAChD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IACpD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,+FAA+F;IAC/F,OAAO,CAAC,aAAa,CAAuB;gBAG1B,WAAW,GAAE,MAAoC,EACjD,SAAS,GAAE,MAAoG,EAChI,IAAI,GAAE,mBAAwB;IAc/B;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBtD,yEAAyE;IACzE,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIhC,kFAAkF;IAClF,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIlC;;;;;;;OAOG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAG,OAAO;IAepD,4BAA4B;IAC5B,KAAK,IAAI,iBAAiB;YAIZ,MAAM;IAsBpB;;;;OAIG;IACH,OAAO,CAAC,KAAK;IAYb;;;;;OAKG;YACW,gBAAgB;YA6BhB,OAAO;CA4CrB;AAID,sCAAsC;AACtC,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,qCAAqC;AACrC,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* @module services/agent/restore-queue
|
|
8
8
|
*/
|
|
9
9
|
import { LoggerService } from '../core/logger.service.js';
|
|
10
|
+
import { BOOT_PRESSURE_CONSTANTS } from '../../constants.js';
|
|
11
|
+
import { bootPressureReason, bootPressureThresholds, getBootLaunchPacer, readBootPressureSnapshot, } from './boot-pressure.js';
|
|
10
12
|
/** Defaults for the restore queue. */
|
|
11
13
|
export const RESTORE_QUEUE_CONSTANTS = {
|
|
12
14
|
DEFAULT_CONCURRENCY: 1,
|
|
@@ -50,14 +52,34 @@ export class RestoreQueue {
|
|
|
50
52
|
timeoutMs;
|
|
51
53
|
pending = [];
|
|
52
54
|
active = new Set();
|
|
53
|
-
counters = { started: 0, ready: 0, failed: 0, timedOut: 0 };
|
|
55
|
+
counters = { started: 0, ready: 0, failed: 0, timedOut: 0, deferred: 0 };
|
|
54
56
|
running = false;
|
|
55
57
|
done = Promise.resolve();
|
|
56
58
|
logger;
|
|
57
|
-
|
|
59
|
+
snapshot;
|
|
60
|
+
thresholds;
|
|
61
|
+
now;
|
|
62
|
+
sleep;
|
|
63
|
+
onLaunch;
|
|
64
|
+
settleMs;
|
|
65
|
+
pollMs;
|
|
66
|
+
holdMaxMs;
|
|
67
|
+
pressureConcurrency;
|
|
68
|
+
/** When the last launch settled (registered, failed or timed out); spacing counts from here */
|
|
69
|
+
lastSettledAt = null;
|
|
70
|
+
constructor(concurrency = restoreConcurrencyFromEnv(), timeoutMs = Number(process.env.CREWLY_RESTORE_TIMEOUT_MS) || RESTORE_QUEUE_CONSTANTS.DEFAULT_TIMEOUT_MS, opts = {}) {
|
|
58
71
|
this.concurrency = concurrency;
|
|
59
72
|
this.timeoutMs = timeoutMs;
|
|
60
73
|
this.logger = LoggerService.getInstance().createComponentLogger('RestoreQueue');
|
|
74
|
+
this.snapshot = opts.snapshot ?? readBootPressureSnapshot;
|
|
75
|
+
this.thresholds = opts.thresholds ?? bootPressureThresholds();
|
|
76
|
+
this.now = opts.now ?? Date.now;
|
|
77
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => { const t = setTimeout(resolve, ms); t.unref?.(); }));
|
|
78
|
+
this.onLaunch = opts.onLaunch ?? (() => getBootLaunchPacer().note());
|
|
79
|
+
this.settleMs = opts.settleMs ?? BOOT_PRESSURE_CONSTANTS.SETTLE_AFTER_READY_MS;
|
|
80
|
+
this.pollMs = opts.pollMs ?? BOOT_PRESSURE_CONSTANTS.PRESSURE_POLL_MS;
|
|
81
|
+
this.holdMaxMs = opts.holdMaxMs ?? BOOT_PRESSURE_CONSTANTS.PRESSURE_HOLD_MAX_MS;
|
|
82
|
+
this.pressureConcurrency = opts.pressureConcurrency ?? BOOT_PRESSURE_CONSTANTS.PRESSURE_MAX_CONCURRENCY;
|
|
61
83
|
}
|
|
62
84
|
/**
|
|
63
85
|
* Plan and run the restore. Resolves when every entry is ready, failed or
|
|
@@ -70,11 +92,13 @@ export class RestoreQueue {
|
|
|
70
92
|
if (this.running)
|
|
71
93
|
return this.done;
|
|
72
94
|
this.pending = orderRestoreEntries(entries).map((entry) => ({ entry, attempt: 0, promoted: false }));
|
|
73
|
-
this.counters = { started: 0, ready: 0, failed: 0, timedOut: 0 };
|
|
95
|
+
this.counters = { started: 0, ready: 0, failed: 0, timedOut: 0, deferred: 0 };
|
|
74
96
|
this.running = true;
|
|
97
|
+
this.lastSettledAt = null;
|
|
75
98
|
this.logger.info('Restore plan', {
|
|
76
99
|
concurrency: this.concurrency,
|
|
77
|
-
|
|
100
|
+
launchSpacingMs: this.thresholds.launchSpacingMs,
|
|
101
|
+
order: this.pending.map((s, i) => `${i + 1}. ${s.entry.name} (${s.entry.reason}${s.entry.essential ? '; launched even under memory pressure' : ''})`),
|
|
78
102
|
});
|
|
79
103
|
const workers = Array.from({ length: this.concurrency }, () => this.worker());
|
|
80
104
|
this.done = Promise.all(workers).then(() => {
|
|
@@ -124,13 +148,84 @@ export class RestoreQueue {
|
|
|
124
148
|
const slot = this.pending.shift();
|
|
125
149
|
if (!slot)
|
|
126
150
|
return;
|
|
127
|
-
|
|
151
|
+
const snap = this.snapshot();
|
|
152
|
+
const tight = bootPressureReason(snap, this.thresholds);
|
|
153
|
+
if (tight && !slot.entry.essential) {
|
|
154
|
+
this.defer(slot, snap, tight);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// In flight from here (isRestoring, the pressure concurrency), pacing included.
|
|
158
|
+
this.active.add(slot.entry.name);
|
|
159
|
+
try {
|
|
160
|
+
await this.paceBeforeLaunch(slot, tight);
|
|
161
|
+
await this.runSlot(slot);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
this.active.delete(slot.entry.name);
|
|
165
|
+
this.lastSettledAt = this.now();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Memory is tight and nobody waits on this agent: hand it to the
|
|
171
|
+
* deferred-start retry (it is owed something, so it is not dropped) and
|
|
172
|
+
* log the reading it was deferred on.
|
|
173
|
+
*/
|
|
174
|
+
defer(slot, snapshot, reason) {
|
|
175
|
+
this.counters.deferred++;
|
|
176
|
+
this.logger.warn('Deferring boot restore: memory is tight and the owner is not waiting on this agent', {
|
|
177
|
+
name: slot.entry.name, reason: slot.entry.reason, memory: reason, remaining: this.pending.length,
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
slot.entry.onDeferred?.(snapshot, reason);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
this.logger.warn('Deferral hook failed', { name: slot.entry.name, error: err instanceof Error ? err.message : String(err) });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Wait before a launch: always the launch spacing since the previous one
|
|
188
|
+
* settled (RSS growth is visible before the next); under pressure also the
|
|
189
|
+
* settle time, the pressure concurrency, and the snapshot clearing (at most
|
|
190
|
+
* `holdMaxMs`, then the owner-waiting agent launches anyway).
|
|
191
|
+
*/
|
|
192
|
+
async paceBeforeLaunch(slot, tight) {
|
|
193
|
+
const { name } = slot.entry;
|
|
194
|
+
const first = this.lastSettledAt === null;
|
|
195
|
+
if (!first) {
|
|
196
|
+
const gap = tight ? Math.max(this.thresholds.launchSpacingMs, this.settleMs) : this.thresholds.launchSpacingMs;
|
|
197
|
+
const wait = gap - (this.now() - this.lastSettledAt);
|
|
198
|
+
if (wait > 0)
|
|
199
|
+
await this.sleep(wait);
|
|
200
|
+
}
|
|
201
|
+
if (!tight)
|
|
202
|
+
return;
|
|
203
|
+
// The first launches under pressure are the owner's agents: nothing has
|
|
204
|
+
// settled yet, so they go now (within the pressure concurrency; this
|
|
205
|
+
// launch itself is already counted in `active`).
|
|
206
|
+
if (first && this.active.size <= this.pressureConcurrency)
|
|
207
|
+
return;
|
|
208
|
+
const heldSince = this.now();
|
|
209
|
+
for (let poll = 0;; poll++) {
|
|
210
|
+
const reason = bootPressureReason(this.snapshot(), this.thresholds);
|
|
211
|
+
const crowded = this.active.size > this.pressureConcurrency;
|
|
212
|
+
if (!reason && !crowded)
|
|
213
|
+
return;
|
|
214
|
+
if (this.now() - heldSince >= this.holdMaxMs) {
|
|
215
|
+
this.logger.warn('Launching an owner-waiting agent although memory is still tight: held long enough', { name, memory: reason, heldMs: this.now() - heldSince });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (poll % BOOT_PRESSURE_CONSTANTS.PRESSURE_HOLD_LOG_EVERY_POLLS === 0) {
|
|
219
|
+
this.logger.info('Holding an owner-waiting boot restore until memory eases', { name, memory: reason, crowded, heldMs: this.now() - heldSince });
|
|
220
|
+
}
|
|
221
|
+
await this.sleep(this.pollMs);
|
|
128
222
|
}
|
|
129
223
|
}
|
|
130
224
|
async runSlot({ entry, attempt, promoted }) {
|
|
131
225
|
const { name } = entry;
|
|
132
226
|
this.active.add(name);
|
|
133
227
|
this.counters.started++;
|
|
228
|
+
this.onLaunch();
|
|
134
229
|
this.logger.info('Restoring agent', { name, attempt, reason: entry.reason, remaining: this.pending.length });
|
|
135
230
|
let timer;
|
|
136
231
|
const TIMEOUT = Symbol('timeout');
|