mixdog 0.9.83 → 0.9.84
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/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Periodic idle-session + tombstone sweep extracted verbatim from manager.mjs.
|
|
3
3
|
// Drives sweepStaleSessions on an unref'd interval; closeSession is imported
|
|
4
4
|
// from session-close.mjs (one-way dependency, no cycle).
|
|
5
|
-
import { sweepStaleSessions, evictIdleLiveSessions } from '../store.mjs';
|
|
5
|
+
import { sweepStaleSessions, sweepStaleSessionsCooperative, evictIdleLiveSessions } from '../store.mjs';
|
|
6
6
|
import { sweepOrphanedPendingMessages } from './pending-messages.mjs';
|
|
7
7
|
import {
|
|
8
8
|
_getRuntimeEntry,
|
|
@@ -23,6 +23,7 @@ const CLEANUP_SLOW_LOG_MS = nonNegativeIntEnv('MIXDOG_SESSION_CLEANUP_SLOW_LOG_M
|
|
|
23
23
|
const TOMBSTONE_MAX_AGE_MS = 60 * 60 * 1000; // 1h
|
|
24
24
|
let _cleanupTimer = null;
|
|
25
25
|
let _cleanupInitialTimer = null;
|
|
26
|
+
let _cleanupRun = null;
|
|
26
27
|
|
|
27
28
|
// A session is "live" when it still owns a non-closed runtime entry. Passed to
|
|
28
29
|
// the retention cap so the active/current and any in-flight session is never
|
|
@@ -65,10 +66,10 @@ const _sweepLog = (line) => {
|
|
|
65
66
|
if (process.env.MIXDOG_DEBUG_SESSION_LOG) process.stderr.write(line);
|
|
66
67
|
};
|
|
67
68
|
|
|
68
|
-
function sweepIdleSessions({ includeTombstones = true, sweepIdle = true } = {}) {
|
|
69
|
+
async function sweepIdleSessions({ includeTombstones = true, sweepIdle = true } = {}) {
|
|
69
70
|
const startedAt = Date.now();
|
|
70
71
|
try {
|
|
71
|
-
const result =
|
|
72
|
+
const result = await sweepStaleSessionsCooperative({
|
|
72
73
|
sweepIdle,
|
|
73
74
|
tombstoneMaxAgeMs: includeTombstones ? TOMBSTONE_MAX_AGE_MS : 0,
|
|
74
75
|
isSessionLive: _isSessionLive,
|
|
@@ -144,33 +145,43 @@ export function sweepTombstones() {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
export function _runCleanupCycle() {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
if (_cleanupRun) return _cleanupRun;
|
|
149
|
+
const run = (async () => {
|
|
150
|
+
// Drain every settled runtime entry on each pass, not just the one or two
|
|
151
|
+
// sessions whose on-disk idle TTL happened to expire in this interval.
|
|
152
|
+
_sweepTerminalSessionRuntimes();
|
|
153
|
+
sweepOrphanedPendingMessages();
|
|
154
|
+
await sweepIdleSessions({ includeTombstones: true });
|
|
155
|
+
// Reclaim same-process session snapshots whose state is durable on disk
|
|
156
|
+
// (memory-leak guard: _liveSessions used to grow for process lifetime).
|
|
157
|
+
try { evictIdleLiveSessions({ isSessionLive: _isSessionLive }); } catch { /* best-effort */ }
|
|
158
|
+
})().catch((error) => {
|
|
159
|
+
try { process.stderr.write(`[agent-session] cleanup cycle failed: ${error?.message || error}\n`); } catch {}
|
|
160
|
+
});
|
|
161
|
+
const tracked = run.finally(() => {
|
|
162
|
+
if (_cleanupRun === tracked) _cleanupRun = null;
|
|
163
|
+
});
|
|
164
|
+
_cleanupRun = tracked;
|
|
165
|
+
return tracked;
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
function _startCleanupInterval() {
|
|
158
169
|
if (_cleanupTimer) return;
|
|
159
170
|
if (CLEANUP_INTERVAL_MS <= 0) return;
|
|
160
|
-
_cleanupTimer = setInterval(_runCleanupCycle, CLEANUP_INTERVAL_MS);
|
|
171
|
+
_cleanupTimer = setInterval(() => { void _runCleanupCycle(); }, CLEANUP_INTERVAL_MS);
|
|
161
172
|
if (_cleanupTimer.unref) _cleanupTimer.unref(); // don't block process exit
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
export function startIdleCleanup() {
|
|
165
176
|
if (_cleanupTimer || _cleanupInitialTimer) return;
|
|
166
177
|
if (CLEANUP_INITIAL_DELAY_MS <= 0) {
|
|
167
|
-
_runCleanupCycle();
|
|
178
|
+
void _runCleanupCycle();
|
|
168
179
|
_startCleanupInterval();
|
|
169
180
|
return;
|
|
170
181
|
}
|
|
171
182
|
_cleanupInitialTimer = setTimeout(() => {
|
|
172
183
|
_cleanupInitialTimer = null;
|
|
173
|
-
_runCleanupCycle();
|
|
184
|
+
void _runCleanupCycle();
|
|
174
185
|
_startCleanupInterval();
|
|
175
186
|
}, CLEANUP_INITIAL_DELAY_MS);
|
|
176
187
|
if (_cleanupInitialTimer.unref) _cleanupInitialTimer.unref();
|
|
@@ -244,7 +244,7 @@ export function getStoredSessionsRaw() {
|
|
|
244
244
|
* Background sweep: delete session files idle longer than ttlMs.
|
|
245
245
|
* Returns { cleaned, remaining, details } for logging.
|
|
246
246
|
*/
|
|
247
|
-
|
|
247
|
+
function* sweepStaleSessionSteps(ttlMs, options = {}) {
|
|
248
248
|
if (ttlMs && typeof ttlMs === 'object') {
|
|
249
249
|
options = ttlMs;
|
|
250
250
|
ttlMs = options.ttlMs;
|
|
@@ -302,6 +302,9 @@ export function sweepStaleSessions(ttlMs, options = {}) {
|
|
|
302
302
|
let openPruned = 0;
|
|
303
303
|
const openPrunedDetails = [];
|
|
304
304
|
for (const row of summaries) {
|
|
305
|
+
// Cooperative callers pause between records so large stores never hold
|
|
306
|
+
// an interactive host's event loop for the full directory scan.
|
|
307
|
+
yield undefined;
|
|
305
308
|
try {
|
|
306
309
|
if (!row?.id) continue;
|
|
307
310
|
const jsonPath = sessionPath(row.id);
|
|
@@ -593,6 +596,7 @@ export function sweepStaleSessions(ttlMs, options = {}) {
|
|
|
593
596
|
// session mid-create whose .json write has not landed yet.
|
|
594
597
|
try {
|
|
595
598
|
for (const h of readdirSync(dir).filter(f => f.endsWith('.hb') || f.endsWith('.own'))) {
|
|
599
|
+
yield undefined;
|
|
596
600
|
if (existsSync(join(dir, h.replace(/\.(hb|own)$/, '.json')))) continue;
|
|
597
601
|
let hbMtime = 0;
|
|
598
602
|
try { hbMtime = statSync(join(dir, h)).mtimeMs; } catch { continue; }
|
|
@@ -613,3 +617,36 @@ export function sweepStaleSessions(ttlMs, options = {}) {
|
|
|
613
617
|
}
|
|
614
618
|
return { cleaned, remaining, details, tombstonesCleaned, tombstoneDetails, tombstoneErrors, openPruned, openPrunedDetails };
|
|
615
619
|
}
|
|
620
|
+
|
|
621
|
+
/** Synchronous compatibility surface for explicit maintenance commands/tests. */
|
|
622
|
+
export function sweepStaleSessions(ttlMs, options = {}) {
|
|
623
|
+
const steps = sweepStaleSessionSteps(ttlMs, options);
|
|
624
|
+
let next = steps.next();
|
|
625
|
+
while (!next.done) next = steps.next();
|
|
626
|
+
return next.value;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Interactive-host sweep: preserve the exact synchronous lifecycle decisions
|
|
631
|
+
* while yielding between records. A single large session remains atomic, but a
|
|
632
|
+
* directory worth of reads/parses can no longer become one multi-second task.
|
|
633
|
+
*/
|
|
634
|
+
export async function sweepStaleSessionsCooperative(ttlMs, options = {}) {
|
|
635
|
+
const cooperativeOptions = ttlMs && typeof ttlMs === 'object' ? ttlMs : options;
|
|
636
|
+
const configuredSliceMs = Number(cooperativeOptions?.cooperativeSliceMs);
|
|
637
|
+
const sliceMs = Number.isFinite(configuredSliceMs)
|
|
638
|
+
? Math.min(50, Math.max(0, configuredSliceMs))
|
|
639
|
+
: 8;
|
|
640
|
+
const steps = sweepStaleSessionSteps(ttlMs, options);
|
|
641
|
+
let next = steps.next();
|
|
642
|
+
while (!next.done) {
|
|
643
|
+
const sliceStartedAt = performance.now();
|
|
644
|
+
do {
|
|
645
|
+
next = steps.next();
|
|
646
|
+
} while (!next.done && performance.now() - sliceStartedAt < sliceMs);
|
|
647
|
+
if (!next.done) {
|
|
648
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return next.value;
|
|
652
|
+
}
|