polygram 0.17.1 → 0.17.3

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.
@@ -38,7 +38,7 @@
38
38
  * `let x = null; wired in main()` pattern, made explicit.
39
39
  */
40
40
 
41
- const ADMIN_CMD_RE = /^\/(model|effort|config|pair-code|pairings|unpair|new|reset|context|compact)(\s|$)/;
41
+ const ADMIN_CMD_RE = /^\/(model|effort|config|pair-code|pairings|unpair|new|reset|clear|context|compact)(\s|$)/;
42
42
  const PAIR_CLAIM_RE = /^\/pair\s+\S+/;
43
43
 
44
44
  function createGateInbound({
@@ -203,7 +203,7 @@ function createQuestionHandlers({
203
203
  // every group member's message for the whole question lifetime).
204
204
  if (!row.awaiting_other) return { consumed: false };
205
205
  // /stop, /new and other commands are never consumed as a free-text answer.
206
- if (/^\/(stop|new|reset|cancel|abort)\b/i.test(String(text || '').trim())) return { consumed: false };
206
+ if (/^\/(stop|new|reset|clear|cancel|abort)\b/i.test(String(text || '').trim())) return { consumed: false };
207
207
  // Identity: only the claimed owner supplies the free-text answer.
208
208
  const auth = questions.claimOrCheck(row.id, fromId);
209
209
  if (!auth.ok) {
@@ -157,8 +157,8 @@ function createSlashCommands({
157
157
  return true;
158
158
  }
159
159
 
160
- // /new + /reset — fresh session
161
- if (botAllowsCommands && (text === '/new' || text === '/reset')) {
160
+ // /new + /reset + /clear — fresh session (all synonyms)
161
+ if (botAllowsCommands && (text === '/new' || text === '/reset' || text === '/clear')) {
162
162
  let drained = 0;
163
163
  try {
164
164
  const r = await pm.resetSession(sessionKey, { reason: text.slice(1) });
@@ -1839,18 +1839,43 @@ class CliProcess extends Process {
1839
1839
  clearTimeout(pending._activityQuietTimer);
1840
1840
  pending._activityQuietTimer = null;
1841
1841
  }
1842
- pending._stopGraceTimer = setTimeout(() => {
1842
+ const fire = () => {
1843
1843
  pending._stopGraceTimer = null;
1844
+ // Don't finalize a turn while a sub-agent is provably still in flight — a Stop
1845
+ // that fired at a sub-agent boundary (or during a quiet sub-agent stretch)
1846
+ // would otherwise CLEAR THE REACTION and end the turn mid-work, with the result
1847
+ // arriving later as a detached cycle. Defer: keep the turn (and its 👾 reaction,
1848
+ // held by B3) alive and re-check. Single-pending only — _pendingSubagentStarts
1849
+ // is proc-wide, so don't cross-attribute. The idle/absolute ceilings are
1850
+ // untouched (we don't reset them), so a lost SubagentStop can't hang — the
1851
+ // ceiling backstops it. docs/progress-is-not-turn-end-spec.md
1852
+ if (this.pendingTurns.has(turnId)
1853
+ && this.pendingTurns.size === 1
1854
+ && (this._pendingSubagentStarts?.length || 0) > 0) {
1855
+ if (!pending._stopGraceDeferred) {
1856
+ pending._stopGraceDeferred = true;
1857
+ this._logEvent('cli-stop-grace-deferred-subagent', {
1858
+ turn_id: turnId, in_flight: this._pendingSubagentStarts.length,
1859
+ session_id: this.claudeSessionId,
1860
+ });
1861
+ }
1862
+ pending._stopGracePending = true;
1863
+ pending._stopGraceTimer = setTimeout(fire, this.stopGraceMs);
1864
+ pending._stopGraceTimer.unref?.();
1865
+ return;
1866
+ }
1844
1867
  pending._stopGracePending = false;
1845
1868
  this._logEvent('cli-turn-resolved-by-stop', {
1846
1869
  turn_id: turnId,
1847
1870
  reply_count: pending.replies?.length || 0,
1848
1871
  via_text_fallback: (pending.replies?.length || 0) === 0,
1849
1872
  attributed: pending.seen === true ? 'seen' : 'reply-bound',
1873
+ deferred_for_subagent: pending._stopGraceDeferred === true,
1850
1874
  session_id: this.claudeSessionId,
1851
1875
  });
1852
1876
  this._finalizeTurn(turnId);
1853
- }, this.stopGraceMs);
1877
+ };
1878
+ pending._stopGraceTimer = setTimeout(fire, this.stopGraceMs);
1854
1879
  pending._stopGraceTimer.unref?.();
1855
1880
  }
1856
1881
 
@@ -3013,6 +3038,12 @@ class CliProcess extends Process {
3013
3038
  session_id: this.claudeSessionId,
3014
3039
  });
3015
3040
  }
3041
+ } else if (p._stopGraceDeferred === true) {
3042
+ // A Stop landed while we're deferring finalize for an in-flight
3043
+ // sub-agent: refresh the captured last_assistant_message so the
3044
+ // eventual finalize delivers the LATEST produced answer (claude's real
3045
+ // end-of-work text), not the boundary Stop's stale/partial text.
3046
+ p._stopHookData = info;
3016
3047
  }
3017
3048
  } else if (this.pendingTurns.size > 1) {
3018
3049
  // Can't attribute Stop to one of several concurrent turns — surface
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.17.1",
3
+ "version": "0.17.3",
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": {