polygram 0.12.0-rc.22 → 0.12.0-rc.24
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/handlers/abort.js +21 -8
- package/lib/process/cli-process.js +12 -1
- package/lib/process-manager.js +6 -0
- package/lib/sdk/callbacks.js +58 -0
- package/package.json +1 -1
package/lib/handlers/abort.js
CHANGED
|
@@ -96,15 +96,28 @@ function createHandleAbort({
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// drainQueue() rejects every queued pending with
|
|
103
|
-
// err.code='INTERRUPTED' so the abort-grace classifier
|
|
104
|
-
// suppresses error replies.
|
|
105
|
-
await pm.interrupt(sessionKey).catch((err) =>
|
|
106
|
-
logger.error?.(`[${botName}] interrupt failed: ${err.message}`));
|
|
99
|
+
// Reject queued pendings first (err.code='INTERRUPTED' → the abort-grace
|
|
100
|
+
// classifier suppresses their error replies AND each turn's finally clears
|
|
101
|
+
// its reactor + typing), THEN stop the live work.
|
|
107
102
|
pm.drainQueue(sessionKey, 'INTERRUPTED');
|
|
103
|
+
if (hadActive && proc && proc.backend === 'cli') {
|
|
104
|
+
// Channels HARD stop (user decision 2026-06-04: "/stop should stop
|
|
105
|
+
// everything including background, like the SDK backend"). A soft C-c
|
|
106
|
+
// interrupt leaves detached background shells + subagents running and
|
|
107
|
+
// can't clear a ghost (no-pending-turn) busy state — the symptom was
|
|
108
|
+
// "Stopped." with the reaction + typing still going. Kill the session: the
|
|
109
|
+
// whole process tree (claude + every subagent + all background shells)
|
|
110
|
+
// dies at once, the close drains the in-flight turn (clearing its
|
|
111
|
+
// reactor/typing), and the next message respawns fresh (--resume restores
|
|
112
|
+
// the conversation). This is what makes channels /stop "stop everything".
|
|
113
|
+
await pm.kill(sessionKey, 'abort').catch((err) =>
|
|
114
|
+
logger.error?.(`[${botName}] abort kill failed: ${err.message}`));
|
|
115
|
+
} else {
|
|
116
|
+
// SDK (or nothing active): non-destructive interrupt cancels the in-flight
|
|
117
|
+
// Query turn WITHOUT tearing down the Query (cheap to reuse next message).
|
|
118
|
+
await pm.interrupt(sessionKey).catch((err) =>
|
|
119
|
+
logger.error?.(`[${botName}] interrupt failed: ${err.message}`));
|
|
120
|
+
}
|
|
108
121
|
|
|
109
122
|
clearAutosteeredReactions(sessionKey).catch(() => {});
|
|
110
123
|
logEvent('abort-requested', {
|
|
@@ -248,6 +248,9 @@ class CliProcess extends Process {
|
|
|
248
248
|
// at one read-only self-check per continuous background-work window.
|
|
249
249
|
this._bgWorkSince = null;
|
|
250
250
|
this._bgWorkEscalations = 0;
|
|
251
|
+
// Visibility (Use 3): whether a "⏳ working in background" status message is
|
|
252
|
+
// currently shown, so we emit exactly one running→cleared pair per window.
|
|
253
|
+
this._bgWorkStatusShown = false;
|
|
251
254
|
// Review P2 ADV-6: token-bucket rate limit on Claude's reply tool calls.
|
|
252
255
|
// Without this, a prompt-injected or runaway Claude can fire reply() 1000×
|
|
253
256
|
// in a tight loop, flooding TG + saturating the daemon event loop.
|
|
@@ -1643,16 +1646,24 @@ class CliProcess extends Process {
|
|
|
1643
1646
|
if (!live) {
|
|
1644
1647
|
if (this._bgWorkSince !== null) {
|
|
1645
1648
|
this._logEvent('cli-bg-work-cleared', { idle_ms: Date.now() - this._bgWorkSince });
|
|
1649
|
+
// Visibility: tear down the status indicator once work clears.
|
|
1650
|
+
if (this._bgWorkStatusShown) {
|
|
1651
|
+
this.emit('bg-work-status', { state: 'cleared' });
|
|
1652
|
+
this._bgWorkStatusShown = false;
|
|
1653
|
+
}
|
|
1646
1654
|
}
|
|
1647
1655
|
this._bgWorkSince = null;
|
|
1648
1656
|
this._bgWorkEscalations = 0;
|
|
1649
1657
|
return;
|
|
1650
1658
|
}
|
|
1651
1659
|
if (this._bgWorkSince === null) {
|
|
1652
|
-
// First idle observation of a live background shell — start the clock
|
|
1660
|
+
// First idle observation of a live background shell — start the clock AND
|
|
1661
|
+
// raise the visibility indicator so a long job reads as working, not stuck.
|
|
1653
1662
|
this._bgWorkSince = Date.now();
|
|
1654
1663
|
this._bgWorkEscalations = 0;
|
|
1655
1664
|
this._logEvent('cli-bg-work-detected', { shell_count: count });
|
|
1665
|
+
this.emit('bg-work-status', { state: 'running', count });
|
|
1666
|
+
this._bgWorkStatusShown = true;
|
|
1656
1667
|
return;
|
|
1657
1668
|
}
|
|
1658
1669
|
const idleMs = Date.now() - this._bgWorkSince;
|
package/lib/process-manager.js
CHANGED
|
@@ -47,6 +47,12 @@ const CALLBACK_TO_EVENT = {
|
|
|
47
47
|
// auto-compacting now. The callback posts a chat message proposing /compact
|
|
48
48
|
// — opt-in per chat. See docs/0.12.0-file-send.md / lib/compaction-warn.js.
|
|
49
49
|
onCompactionWarn: 'compaction-warn',
|
|
50
|
+
// 0.12.0 background-work visibility (Use 3). CliProcess emits 'bg-work-status'
|
|
51
|
+
// {state:'running'|'cleared', count?} when a detached background shell is first
|
|
52
|
+
// observed running idle past its turn, and again when it clears. The callback
|
|
53
|
+
// posts/edits a "⏳ working in background" status message so a long job reads as
|
|
54
|
+
// working, not stuck. See docs/0.12.0-background-work-lifecycle-plan.md.
|
|
55
|
+
onBgWorkStatus: 'bg-work-status',
|
|
50
56
|
onQueueDrop: 'queue-drop',
|
|
51
57
|
onThinking: 'thinking',
|
|
52
58
|
// Tmux backend: TUI shows in-pane approval prompt. SDK backend
|
package/lib/sdk/callbacks.js
CHANGED
|
@@ -60,6 +60,10 @@ function createSdkCallbacks({
|
|
|
60
60
|
// because the TUI's queue is FIFO and we only watch one extra turn
|
|
61
61
|
// at a time per session.
|
|
62
62
|
const extraTurnTracker = new Map(); // sessionKey → { msgId, intervalHandle, chatId }
|
|
63
|
+
// 0.12.0 background-work visibility (Use 3): sessionKey → message_id of the live
|
|
64
|
+
// "⏳ working in background" status message, so the cleared/close paths can edit
|
|
65
|
+
// it to a final state instead of leaving it dangling as "working".
|
|
66
|
+
const bgStatusMsgIds = new Map();
|
|
63
67
|
|
|
64
68
|
function startExtraTurnVisuals(sessionKey, msgId) {
|
|
65
69
|
if (!bot) return;
|
|
@@ -152,6 +156,17 @@ function createSdkCallbacks({
|
|
|
152
156
|
// visuals so we don't leak the interval and aren't stuck
|
|
153
157
|
// showing "writing…" on a dead session.
|
|
154
158
|
stopExtraTurnVisuals(sessionKey, null);
|
|
159
|
+
// 0.12.0 bg-work visibility: if a "⏳ working in background" status is still
|
|
160
|
+
// up when the session closes, its shell died with the session — edit to a
|
|
161
|
+
// final state so it doesn't dangle as "working" forever.
|
|
162
|
+
const bgMid = bgStatusMsgIds.get(sessionKey);
|
|
163
|
+
if (bgMid != null && bot) {
|
|
164
|
+
bgStatusMsgIds.delete(sessionKey);
|
|
165
|
+
tg(bot, 'editMessageText', {
|
|
166
|
+
chat_id: entry.chatId, message_id: bgMid,
|
|
167
|
+
text: '⏹ Background work ended (session restarted).',
|
|
168
|
+
}, { source: 'bg-work-status', botName }).catch(() => {});
|
|
169
|
+
}
|
|
155
170
|
},
|
|
156
171
|
|
|
157
172
|
onStreamChunk: (sessionKey, partial, entry) => {
|
|
@@ -324,6 +339,49 @@ function createSdkCallbacks({
|
|
|
324
339
|
}
|
|
325
340
|
},
|
|
326
341
|
|
|
342
|
+
// 0.12.0 background-work visibility (Use 3). CliProcess emits this when a
|
|
343
|
+
// detached `run_in_background` shell is first observed running idle past its
|
|
344
|
+
// turn ('running') and again when it clears ('cleared'). We post ONE bot
|
|
345
|
+
// status message and edit it to done — so a long job reads as working, not
|
|
346
|
+
// stuck. Direct tg send (NOT via claude — this is a bot status indicator),
|
|
347
|
+
// keyed by sessionKey so the cleared/close paths can find it to edit.
|
|
348
|
+
onBgWorkStatus: async (sessionKey, payload) => {
|
|
349
|
+
try {
|
|
350
|
+
if (!bot) return;
|
|
351
|
+
const chatId = getChatIdFromKey(sessionKey);
|
|
352
|
+
const threadIdRaw = getThreadIdFromKey(sessionKey);
|
|
353
|
+
const threadId = threadIdRaw ? parseInt(threadIdRaw, 10) : null;
|
|
354
|
+
const state = payload?.state;
|
|
355
|
+
if (state === 'running') {
|
|
356
|
+
if (bgStatusMsgIds.has(sessionKey)) return; // already showing one
|
|
357
|
+
const res = await tg(bot, 'sendMessage', {
|
|
358
|
+
chat_id: chatId,
|
|
359
|
+
text: '⏳ Working in the background — I\'ll keep an eye on it and report when it\'s done.',
|
|
360
|
+
...(Number.isInteger(threadId) && { message_thread_id: threadId }),
|
|
361
|
+
}, { source: 'bg-work-status', botName });
|
|
362
|
+
const mid = res?.message_id ?? res?.result?.message_id ?? null;
|
|
363
|
+
if (mid != null) bgStatusMsgIds.set(sessionKey, mid);
|
|
364
|
+
logEvent('bg-work-status', {
|
|
365
|
+
chat_id: chatId, session_key: sessionKey, thread_id: threadIdRaw,
|
|
366
|
+
state: 'running', message_id: mid,
|
|
367
|
+
});
|
|
368
|
+
} else if (state === 'cleared') {
|
|
369
|
+
const mid = bgStatusMsgIds.get(sessionKey);
|
|
370
|
+
bgStatusMsgIds.delete(sessionKey);
|
|
371
|
+
if (mid == null) return;
|
|
372
|
+
await tg(bot, 'editMessageText', {
|
|
373
|
+
chat_id: chatId, message_id: mid, text: '✅ Background work finished.',
|
|
374
|
+
}, { source: 'bg-work-status', botName }).catch(() => {});
|
|
375
|
+
logEvent('bg-work-status', {
|
|
376
|
+
chat_id: chatId, session_key: sessionKey, thread_id: threadIdRaw,
|
|
377
|
+
state: 'cleared', message_id: mid,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
} catch (err) {
|
|
381
|
+
logger.error?.(`[${botName}] bg-work-status handler: ${err.message}`);
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
|
|
327
385
|
// rc.9: pair-of with onExtraTurnReply. Fires the moment
|
|
328
386
|
// TmuxProcess sees the dequeued user-message in JSONL → turn 2
|
|
329
387
|
// is starting. Re-engages typing indicator + ✍ on the
|
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.24",
|
|
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": {
|