polygram 0.12.0-rc.31 → 0.12.0-rc.32

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.
@@ -1732,6 +1732,18 @@ class CliProcess extends Process {
1732
1732
  return { live: backgroundShell, count: shellCount };
1733
1733
  }
1734
1734
 
1735
+ /**
1736
+ * LRU eviction pin (0.12.0 spec). Cached read of `_bgWorkSince` — the idle bg-work
1737
+ * watchdog state maintained by `_pollBackgroundWork` on the ≤5s pong tick. Non-null ⟺ a
1738
+ * detached background shell has been observed while idle. No time cap: a job that runs for
1739
+ * hours stays pinned (elapsed time can't tell "slow-but-progressing" from "stuck"). Cheap,
1740
+ * sync — safe to call from `_evictLRU`.
1741
+ * @returns {boolean}
1742
+ */
1743
+ hasActiveBackgroundWork() {
1744
+ return this._bgWorkSince !== null;
1745
+ }
1746
+
1735
1747
  /**
1736
1748
  * Stall-watchdog for detached background work (0.12.0 background-work
1737
1749
  * lifecycle, shumorobot Music 7h frozen-Chrome download). Runs on the
@@ -160,6 +160,19 @@ class Process extends EventEmitter {
160
160
  return false;
161
161
  }
162
162
 
163
+ /**
164
+ * Does this session have a DETACHED background job running (a `run_in_background`
165
+ * shell that outlives the dispatch turn)? Used by ProcessManager._evictLRU to PIN
166
+ * the session — skip it for eviction the same way `inFlight` is skipped — so a live
167
+ * job isn't silently killed under budget pressure. Default: no signal → false.
168
+ * Backends that can detect detached shells (cli) override this. Must be cheap + sync.
169
+ *
170
+ * @returns {boolean}
171
+ */
172
+ hasActiveBackgroundWork() {
173
+ return false;
174
+ }
175
+
163
176
  /**
164
177
  * Push priority='now' style steer (rare; legacy of OpenClaw shape).
165
178
  * Hot-path-safe.
@@ -245,16 +245,36 @@ class ProcessManager {
245
245
  const newCost = newProc.cost;
246
246
 
247
247
  while (this.totalCost + newCost > this.budget) {
248
- const evicted = this._evictLRU();
249
- if (!evicted) {
250
- // All entries in-flightpark.
251
- await this._awaitLruSlot();
252
- if (this._shuttingDown) {
253
- try { await newProc.kill('shutdown'); } catch {}
254
- throw new Error('shutdown');
255
- }
256
- // Loop again — budget may have freed up.
248
+ const evicted = this._evictLRU(); // skips inFlight + background-job-pinned
249
+ if (evicted) continue;
250
+ // _evictLRU freed nothing. Policy C split by WHY:
251
+ if (this._hasPinnedSession()) {
252
+ // A DURABLE blocker (live background job) holds a slot. Don't park on it (could be
253
+ // ~an hour) and don't kill it. The budget caps RSS, not correctness — so treat it as
254
+ // SOFT: spawn over budget + warn; the operator reclaims by /reset-ing a chat.
255
+ const pinned = this._pinnedSessionKeys();
256
+ this._logEvent('lru-overflow-pinned', {
257
+ active: this.procs.size,
258
+ totalCost: this.totalCost,
259
+ budget: this.budget,
260
+ newCost,
261
+ pinned,
262
+ });
263
+ this.logger.warn?.(
264
+ `[pm] budget ${this.budget} exceeded (~${this.totalCost + newCost}): all free slots hold ` +
265
+ `live background jobs [${pinned.join(', ')}]. Spawning over limit — /reset one of those ` +
266
+ `chats to reclaim memory.`,
267
+ );
268
+ break; // soft overflow — spawn anyway
269
+ }
270
+ // No pin — the blockers are all in-flight TURNS (transient, finish in seconds). Keep the
271
+ // existing behavior: park briefly for a slot rather than needlessly overflow.
272
+ await this._awaitLruSlot();
273
+ if (this._shuttingDown) {
274
+ try { await newProc.kill('shutdown'); } catch {}
275
+ throw new Error('shutdown');
257
276
  }
277
+ // Loop again — budget may have freed up.
258
278
  }
259
279
 
260
280
  this._wireCallbacks(newProc);
@@ -278,8 +298,12 @@ class ProcessManager {
278
298
  _evictLRU() {
279
299
  let oldest = null;
280
300
  let oldestKey = null;
301
+ let pinnedSkipped = 0;
281
302
  for (const [k, p] of this.procs.entries()) {
282
303
  if (p.inFlight) continue;
304
+ // PIN: a session with a live detached background job is NOT evictable — killing it
305
+ // would silently drop the job (and its report-back wakeup). Skip like inFlight.
306
+ if (p.hasActiveBackgroundWork()) { pinnedSkipped++; continue; }
283
307
  if (!oldest || (p.lastUsedTs || 0) < (oldest.lastUsedTs || 0)) {
284
308
  oldest = p;
285
309
  oldestKey = k;
@@ -290,6 +314,7 @@ class ProcessManager {
290
314
  active: this.procs.size,
291
315
  totalCost: this.totalCost,
292
316
  budget: this.budget,
317
+ pinnedSkipped,
293
318
  });
294
319
  return false;
295
320
  }
@@ -297,12 +322,33 @@ class ProcessManager {
297
322
  session_key: oldestKey,
298
323
  cost: oldest.cost,
299
324
  backend: oldest.backend,
325
+ pinnedSkipped,
300
326
  });
301
327
  oldest.kill('evict').catch(() => {});
302
328
  this.procs.delete(oldestKey);
303
329
  return true;
304
330
  }
305
331
 
332
+ /**
333
+ * A DURABLE eviction blocker: a non-inFlight session holding a slot because it has a live
334
+ * background job (vs an inFlight TURN, which is transient and frees in seconds). Used to
335
+ * split park-vs-overflow when _evictLRU can free nothing.
336
+ */
337
+ _hasPinnedSession() {
338
+ for (const p of this.procs.values()) {
339
+ if (!p.inFlight && p.hasActiveBackgroundWork()) return true;
340
+ }
341
+ return false;
342
+ }
343
+
344
+ _pinnedSessionKeys() {
345
+ const keys = [];
346
+ for (const [k, p] of this.procs.entries()) {
347
+ if (!p.inFlight && p.hasActiveBackgroundWork()) keys.push(k);
348
+ }
349
+ return keys;
350
+ }
351
+
306
352
  async _awaitLruSlot() {
307
353
  return new Promise((resolve, reject) => {
308
354
  const timer = setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.12.0-rc.31",
3
+ "version": "0.12.0-rc.32",
4
4
  "description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
5
5
  "main": "lib/ipc/client.js",
6
6
  "bin": {