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.
Files changed (99) hide show
  1. package/config/roles/developer/prompt.md +3 -1
  2. package/config/skills/registry.json +125 -1
  3. package/dist/backend/backend/src/constants.d.ts +130 -2
  4. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  5. package/dist/backend/backend/src/constants.js +141 -3
  6. package/dist/backend/backend/src/constants.js.map +1 -1
  7. package/dist/backend/backend/src/index.d.ts +37 -0
  8. package/dist/backend/backend/src/index.d.ts.map +1 -1
  9. package/dist/backend/backend/src/index.js +103 -10
  10. package/dist/backend/backend/src/index.js.map +1 -1
  11. package/dist/backend/backend/src/services/agent/boot-pressure.d.ts +147 -0
  12. package/dist/backend/backend/src/services/agent/boot-pressure.d.ts.map +1 -0
  13. package/dist/backend/backend/src/services/agent/boot-pressure.js +212 -0
  14. package/dist/backend/backend/src/services/agent/boot-pressure.js.map +1 -0
  15. package/dist/backend/backend/src/services/agent/restore-queue.d.ts +50 -1
  16. package/dist/backend/backend/src/services/agent/restore-queue.d.ts.map +1 -1
  17. package/dist/backend/backend/src/services/agent/restore-queue.js +100 -5
  18. package/dist/backend/backend/src/services/agent/restore-queue.js.map +1 -1
  19. package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.d.ts +11 -0
  20. package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.d.ts.map +1 -1
  21. package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.js +23 -2
  22. package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.js.map +1 -1
  23. package/dist/backend/backend/src/services/messaging/deferred-start-retry.d.ts +8 -0
  24. package/dist/backend/backend/src/services/messaging/deferred-start-retry.d.ts.map +1 -1
  25. package/dist/backend/backend/src/services/messaging/deferred-start-retry.js +8 -0
  26. package/dist/backend/backend/src/services/messaging/deferred-start-retry.js.map +1 -1
  27. package/dist/backend/backend/src/services/wiki/wiki-log-entries.d.ts +7 -0
  28. package/dist/backend/backend/src/services/wiki/wiki-log-entries.d.ts.map +1 -1
  29. package/dist/backend/backend/src/services/wiki/wiki-log-entries.js +10 -0
  30. package/dist/backend/backend/src/services/wiki/wiki-log-entries.js.map +1 -1
  31. package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.d.ts +24 -0
  32. package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.d.ts.map +1 -1
  33. package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.js +75 -9
  34. package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.js.map +1 -1
  35. package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.d.ts +19 -0
  36. package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.d.ts.map +1 -1
  37. package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.js +23 -2
  38. package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.js.map +1 -1
  39. package/dist/backend/backend/src/services/wiki/work-record.service.d.ts +23 -1
  40. package/dist/backend/backend/src/services/wiki/work-record.service.d.ts.map +1 -1
  41. package/dist/backend/backend/src/services/wiki/work-record.service.js +33 -5
  42. package/dist/backend/backend/src/services/wiki/work-record.service.js.map +1 -1
  43. package/dist/backend/backend/src/services/worktree/low-disk-guard.d.ts +92 -8
  44. package/dist/backend/backend/src/services/worktree/low-disk-guard.d.ts.map +1 -1
  45. package/dist/backend/backend/src/services/worktree/low-disk-guard.js +98 -14
  46. package/dist/backend/backend/src/services/worktree/low-disk-guard.js.map +1 -1
  47. package/dist/backend/backend/src/services/worktree/tmp-janitor.d.ts +138 -0
  48. package/dist/backend/backend/src/services/worktree/tmp-janitor.d.ts.map +1 -0
  49. package/dist/backend/backend/src/services/worktree/tmp-janitor.js +199 -0
  50. package/dist/backend/backend/src/services/worktree/tmp-janitor.js.map +1 -0
  51. package/dist/backend/backend/src/services/worktree/transcript-janitor.d.ts +154 -0
  52. package/dist/backend/backend/src/services/worktree/transcript-janitor.d.ts.map +1 -0
  53. package/dist/backend/backend/src/services/worktree/transcript-janitor.js +357 -0
  54. package/dist/backend/backend/src/services/worktree/transcript-janitor.js.map +1 -0
  55. package/dist/backend/backend/src/services/worktree/workitem-worktree.service.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/worktree/workitem-worktree.service.js +7 -2
  57. package/dist/backend/backend/src/services/worktree/workitem-worktree.service.js.map +1 -1
  58. package/dist/backend/backend/src/services/worktree/worktree-dirty.d.ts +16 -4
  59. package/dist/backend/backend/src/services/worktree/worktree-dirty.d.ts.map +1 -1
  60. package/dist/backend/backend/src/services/worktree/worktree-dirty.js +21 -5
  61. package/dist/backend/backend/src/services/worktree/worktree-dirty.js.map +1 -1
  62. package/dist/backend/backend/src/services/worktree/worktree-git.d.ts +18 -0
  63. package/dist/backend/backend/src/services/worktree/worktree-git.d.ts.map +1 -1
  64. package/dist/backend/backend/src/services/worktree/worktree-git.js +60 -0
  65. package/dist/backend/backend/src/services/worktree/worktree-git.js.map +1 -1
  66. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts +164 -25
  67. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts.map +1 -1
  68. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js +494 -55
  69. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js.map +1 -1
  70. package/dist/backend/backend/src/services/worktree/worktree-tiers.d.ts +186 -0
  71. package/dist/backend/backend/src/services/worktree/worktree-tiers.d.ts.map +1 -0
  72. package/dist/backend/backend/src/services/worktree/worktree-tiers.js +279 -0
  73. package/dist/backend/backend/src/services/worktree/worktree-tiers.js.map +1 -0
  74. package/dist/backend/build-info.json +2 -2
  75. package/dist/cli/backend/src/constants.d.ts +130 -2
  76. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  77. package/dist/cli/backend/src/constants.js +141 -3
  78. package/dist/cli/backend/src/constants.js.map +1 -1
  79. package/dist/cli/backend/src/services/agent/boot-pressure.d.ts +147 -0
  80. package/dist/cli/backend/src/services/agent/boot-pressure.d.ts.map +1 -0
  81. package/dist/cli/backend/src/services/agent/boot-pressure.js +212 -0
  82. package/dist/cli/backend/src/services/agent/boot-pressure.js.map +1 -0
  83. package/dist/cli/backend/src/services/agent/restore-queue.d.ts +50 -1
  84. package/dist/cli/backend/src/services/agent/restore-queue.d.ts.map +1 -1
  85. package/dist/cli/backend/src/services/agent/restore-queue.js +100 -5
  86. package/dist/cli/backend/src/services/agent/restore-queue.js.map +1 -1
  87. package/dist/cli/backend/src/services/wiki/wiki-log-entries.d.ts +7 -0
  88. package/dist/cli/backend/src/services/wiki/wiki-log-entries.d.ts.map +1 -1
  89. package/dist/cli/backend/src/services/wiki/wiki-log-entries.js +10 -0
  90. package/dist/cli/backend/src/services/wiki/wiki-log-entries.js.map +1 -1
  91. package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts +92 -8
  92. package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts.map +1 -1
  93. package/dist/cli/backend/src/services/worktree/low-disk-guard.js +98 -14
  94. package/dist/cli/backend/src/services/worktree/low-disk-guard.js.map +1 -1
  95. package/dist/cli/backend/src/services/worktree/worktree-git.d.ts +18 -0
  96. package/dist/cli/backend/src/services/worktree/worktree-git.d.ts.map +1 -1
  97. package/dist/cli/backend/src/services/worktree/worktree-git.js +60 -0
  98. package/dist/cli/backend/src/services/worktree/worktree-git.js.map +1 -1
  99. 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
- constructor(concurrency?: number, timeoutMs?: number);
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;CAC9B;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,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;IASvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAT3B,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,QAAQ,CAAoD;IACpE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,IAAI,CAAoC;IAChD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;gBAGvB,WAAW,GAAE,MAAoC,EACjD,SAAS,GAAE,MAAoG;IAKjI;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBtD,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;YAQN,OAAO;CA2CrB;AAID,sCAAsC;AACtC,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,qCAAqC;AACrC,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
1
+ {"version":3,"file":"restore-queue.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/agent/restore-queue.ts"],"names":[],"mappings":"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"}
@@ -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
- constructor(concurrency = restoreConcurrencyFromEnv(), timeoutMs = Number(process.env.CREWLY_RESTORE_TIMEOUT_MS) || RESTORE_QUEUE_CONSTANTS.DEFAULT_TIMEOUT_MS) {
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
- order: this.pending.map((s, i) => `${i + 1}. ${s.entry.name} (${s.entry.reason})`),
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
- await this.runSlot(slot);
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');