polygram 0.12.0-rc.21 → 0.12.0-rc.22
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 +111 -1
- package/package.json +1 -1
|
@@ -113,6 +113,20 @@ const MID_TURN_PROMPTS = [
|
|
|
113
113
|
// hook process.
|
|
114
114
|
const STREAMING_HINT_RE = /esc to interrupt/i;
|
|
115
115
|
|
|
116
|
+
// 0.12.0 background-work lifecycle: claude's TUI mode line shows a live
|
|
117
|
+
// background-shell COUNT while a `run_in_background:true` Bash outlives its turn,
|
|
118
|
+
// e.g. `⏵⏵ auto mode on · 1 shell · ← for agents · ↓ to manage`. Confirmed on
|
|
119
|
+
// claude 2.1.158 (P0 spike — docs/0.12.0-background-work-lifecycle-plan.md): the
|
|
120
|
+
// count is always-present in the viewport mode line while shells run and clears
|
|
121
|
+
// IN-PLACE within ~3s when they exit (no stale scrollback). Anchored to the
|
|
122
|
+
// `auto mode on` line and matched only against the captured TAIL so a scrolled-off
|
|
123
|
+
// history line never trips it. R1: re-validate on each pinned-claude bump.
|
|
124
|
+
const BACKGROUND_SHELL_RE = /auto mode on[^\n]*·\s*(\d+)\s+shells?\b/i;
|
|
125
|
+
// How long a detached background shell may run AFTER its turn resolved (claude
|
|
126
|
+
// idle) before the stall-watchdog fires one read-only self-check. Override via
|
|
127
|
+
// the constructor (tests use a small value).
|
|
128
|
+
const DEFAULT_BG_WORK_STALL_MS = 600_000; // 10 min
|
|
129
|
+
|
|
116
130
|
// 0.12 Phase 3.3 (Q1 resolution): heuristic for "looks like an unknown
|
|
117
131
|
// interactive prompt." Match common prompt shapes that don't appear in
|
|
118
132
|
// MID_TURN_PROMPTS — operator gets a telemetry event so they can decide
|
|
@@ -172,6 +186,7 @@ class CliProcess extends Process {
|
|
|
172
186
|
turnQuietMs = DEFAULT_TURN_QUIET_MS,
|
|
173
187
|
turnTimeoutMs = DEFAULT_TURN_TIMEOUT_MS,
|
|
174
188
|
turnAbsoluteMs = DEFAULT_TURN_ABSOLUTE_MS,
|
|
189
|
+
bgWorkStallMs = DEFAULT_BG_WORK_STALL_MS,
|
|
175
190
|
interruptGraceMs = DEFAULT_INTERRUPT_GRACE_MS,
|
|
176
191
|
maxRepliesPerTurn = DEFAULT_MAX_REPLIES_PER_TURN,
|
|
177
192
|
queueCap = DEFAULT_QUEUE_CAP, // Parity P2
|
|
@@ -203,6 +218,7 @@ class CliProcess extends Process {
|
|
|
203
218
|
this.turnQuietMs = turnQuietMs;
|
|
204
219
|
this.turnTimeoutMs = turnTimeoutMs;
|
|
205
220
|
this.turnAbsoluteMs = turnAbsoluteMs;
|
|
221
|
+
this.bgWorkStallMs = bgWorkStallMs;
|
|
206
222
|
this.interruptGraceMs = interruptGraceMs;
|
|
207
223
|
this.maxRepliesPerTurn = maxRepliesPerTurn;
|
|
208
224
|
this.queueCap = queueCap;
|
|
@@ -226,6 +242,12 @@ class CliProcess extends Process {
|
|
|
226
242
|
// interval fires bridge-disconnected if too much time elapses.
|
|
227
243
|
this.lastPongAt = 0;
|
|
228
244
|
this.pongWatchdog = null;
|
|
245
|
+
// 0.12.0 background-work stall-watchdog state. `_bgWorkSince` = when a live
|
|
246
|
+
// background shell was first observed while idle (null = none); reset only
|
|
247
|
+
// when the shell count returns to 0. `_bgWorkEscalations` caps the watchdog
|
|
248
|
+
// at one read-only self-check per continuous background-work window.
|
|
249
|
+
this._bgWorkSince = null;
|
|
250
|
+
this._bgWorkEscalations = 0;
|
|
229
251
|
// Review P2 ADV-6: token-bucket rate limit on Claude's reply tool calls.
|
|
230
252
|
// Without this, a prompt-injected or runaway Claude can fire reply() 1000×
|
|
231
253
|
// in a tight loop, flooding TG + saturating the daemon event loop.
|
|
@@ -1531,7 +1553,7 @@ class CliProcess extends Process {
|
|
|
1531
1553
|
*/
|
|
1532
1554
|
async probeBusyState() {
|
|
1533
1555
|
const base = {
|
|
1534
|
-
busy: false, streaming: false,
|
|
1556
|
+
busy: false, streaming: false, backgroundShell: false, shellCount: 0,
|
|
1535
1557
|
inFlight: this.inFlight, pendingTurns: this.pendingTurns.size,
|
|
1536
1558
|
captured: false, paneTail: null,
|
|
1537
1559
|
};
|
|
@@ -1547,10 +1569,23 @@ class CliProcess extends Process {
|
|
|
1547
1569
|
}
|
|
1548
1570
|
if (!pane) return base;
|
|
1549
1571
|
const streaming = STREAMING_HINT_RE.test(pane);
|
|
1572
|
+
// Background-shell count from the TUI mode line. Match only the captured
|
|
1573
|
+
// TAIL (the mode line lives at the bottom of the viewport) so a `· N shell ·`
|
|
1574
|
+
// string scrolled off into history can't trip a stale false-positive — see
|
|
1575
|
+
// BACKGROUND_SHELL_RE. A detached `run_in_background` Bash that outlived its
|
|
1576
|
+
// turn shows here even while claude is idle and not streaming.
|
|
1577
|
+
const m = pane.slice(-400).match(BACKGROUND_SHELL_RE);
|
|
1578
|
+
const shellCount = m ? parseInt(m[1], 10) : 0;
|
|
1579
|
+
const backgroundShell = shellCount > 0;
|
|
1550
1580
|
return {
|
|
1551
1581
|
...base,
|
|
1582
|
+
// `busy` stays streaming-only — it is the abort path's "is claude working a
|
|
1583
|
+
// turn" signal and must not change behaviour. Background-shell liveness is a
|
|
1584
|
+
// separate axis the stall-watchdog reads via `backgroundShell`/`shellCount`.
|
|
1552
1585
|
busy: streaming,
|
|
1553
1586
|
streaming,
|
|
1587
|
+
backgroundShell,
|
|
1588
|
+
shellCount,
|
|
1554
1589
|
captured: true,
|
|
1555
1590
|
paneTail: pane.slice(-200),
|
|
1556
1591
|
};
|
|
@@ -1562,6 +1597,76 @@ class CliProcess extends Process {
|
|
|
1562
1597
|
return busy;
|
|
1563
1598
|
}
|
|
1564
1599
|
|
|
1600
|
+
/**
|
|
1601
|
+
* Does this session have a detached background shell running RIGHT NOW — a
|
|
1602
|
+
* `run_in_background` Bash that may have outlived its turn? Thin probe over
|
|
1603
|
+
* probeBusyState's background-shell signal; the stall-watchdog's input.
|
|
1604
|
+
* @returns {Promise<{live:boolean, count:number}>}
|
|
1605
|
+
*/
|
|
1606
|
+
async hasLiveBackgroundWork() {
|
|
1607
|
+
const { backgroundShell, shellCount } = await this.probeBusyState();
|
|
1608
|
+
return { live: backgroundShell, count: shellCount };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* Stall-watchdog for detached background work (0.12.0 background-work
|
|
1613
|
+
* lifecycle, shumorobot Music 7h frozen-Chrome download). Runs on the
|
|
1614
|
+
* pongWatchdog 5s tick but ONLY while the session is IDLE (pendingTurns===0) —
|
|
1615
|
+
* the mirror of _pollMidTurnDialogs, which only runs DURING turns. When a
|
|
1616
|
+
* `run_in_background` Bash outlives its turn and keeps running while claude is
|
|
1617
|
+
* idle for > bgWorkStallMs, nothing tells the agent or user whether it's
|
|
1618
|
+
* progressing or stuck. One read-only self-check re-invokes the agent to
|
|
1619
|
+
* diagnose — via `fireUserMessage`, NOT `injectUserMessage` (which no-ops when
|
|
1620
|
+
* !inFlight, the exact idle state here). Read-only framing matters: the agent
|
|
1621
|
+
* runs bypassPermissions, so an open-ended "fix it" could background another
|
|
1622
|
+
* hung shell unattended.
|
|
1623
|
+
*
|
|
1624
|
+
* Exactly one self-check per continuous background-work window (capped by
|
|
1625
|
+
* `_bgWorkEscalations`); the window resets only when the shell count returns to
|
|
1626
|
+
* 0. Never throws — swallows its own errors so the pong watchdog stays clean.
|
|
1627
|
+
*/
|
|
1628
|
+
async _pollBackgroundWork() {
|
|
1629
|
+
if (this.closed || !this.bridgeReady) return;
|
|
1630
|
+
// Only watch while idle. An active turn means the agent is engaged
|
|
1631
|
+
// (_pollMidTurnDialogs owns that path). Crucially we do NOT reset the clock
|
|
1632
|
+
// here — the same shell is still running, so the window persists across a
|
|
1633
|
+
// brief self-check turn rather than restarting and re-pinging every window.
|
|
1634
|
+
if (this.pendingTurns.size > 0) return;
|
|
1635
|
+
let live = false;
|
|
1636
|
+
let count = 0;
|
|
1637
|
+
try {
|
|
1638
|
+
({ live, count } = await this.hasLiveBackgroundWork());
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
this.logger.warn?.(`[${this.label}] channels: bg-work probe failed: ${err.message}`);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
if (!live) {
|
|
1644
|
+
if (this._bgWorkSince !== null) {
|
|
1645
|
+
this._logEvent('cli-bg-work-cleared', { idle_ms: Date.now() - this._bgWorkSince });
|
|
1646
|
+
}
|
|
1647
|
+
this._bgWorkSince = null;
|
|
1648
|
+
this._bgWorkEscalations = 0;
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
if (this._bgWorkSince === null) {
|
|
1652
|
+
// First idle observation of a live background shell — start the clock.
|
|
1653
|
+
this._bgWorkSince = Date.now();
|
|
1654
|
+
this._bgWorkEscalations = 0;
|
|
1655
|
+
this._logEvent('cli-bg-work-detected', { shell_count: count });
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const idleMs = Date.now() - this._bgWorkSince;
|
|
1659
|
+
if (idleMs < this.bgWorkStallMs || this._bgWorkEscalations >= 1) return;
|
|
1660
|
+
const mins = Math.max(1, Math.round(idleMs / 60000));
|
|
1661
|
+
const prompt =
|
|
1662
|
+
`⏳ A background job has been running ~${mins} min with no update. `
|
|
1663
|
+
+ `Check its status and report whether it's progressing or stuck. `
|
|
1664
|
+
+ `Do NOT start new work, re-run it, or kill anything — report only.`;
|
|
1665
|
+
const fired = this.fireUserMessage(prompt);
|
|
1666
|
+
this._bgWorkEscalations = 1;
|
|
1667
|
+
this._logEvent('cli-bg-work-stall-selfcheck', { idle_ms: idleMs, shell_count: count, fired });
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1565
1670
|
async kill(reason = 'kill') {
|
|
1566
1671
|
if (this.closed) return;
|
|
1567
1672
|
// Parity P19: re-entry guard for concurrent kill() calls. Mirrors
|
|
@@ -2397,6 +2502,11 @@ class CliProcess extends Process {
|
|
|
2397
2502
|
this._pollMidTurnDialogs().catch((err) => {
|
|
2398
2503
|
this.logger.warn?.(`[${this.label}] channels: mid-turn poll failed: ${err.message}`);
|
|
2399
2504
|
});
|
|
2505
|
+
// 0.12.0 background-work lifecycle: idle-side stall-watchdog, the mirror of
|
|
2506
|
+
// _pollMidTurnDialogs (which only runs during turns). Fire-and-forget.
|
|
2507
|
+
this._pollBackgroundWork().catch((err) => {
|
|
2508
|
+
this.logger.warn?.(`[${this.label}] channels: bg-work poll failed: ${err.message}`);
|
|
2509
|
+
});
|
|
2400
2510
|
}, PONG_CHECK_INTERVAL_MS);
|
|
2401
2511
|
this.pongWatchdog.unref?.();
|
|
2402
2512
|
}
|
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.22",
|
|
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": {
|