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.
- package/lib/process/cli-process.js +12 -0
- package/lib/process/process.js +13 -0
- package/lib/process-manager.js +55 -9
- package/package.json +1 -1
|
@@ -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
|
package/lib/process/process.js
CHANGED
|
@@ -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.
|
package/lib/process-manager.js
CHANGED
|
@@ -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 (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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.
|
|
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": {
|