polygram 0.10.0-rc.40 → 0.10.0-rc.41

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "polygram",
4
- "version": "0.10.0-rc.40",
4
+ "version": "0.10.0-rc.41",
5
5
  "description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
6
6
  "keywords": [
7
7
  "telegram",
@@ -202,6 +202,14 @@ const DEFAULT_QUIESCE_MS = 500; // require READY for this long before decl
202
202
  // wedge-detection delay is bounded.
203
203
  const DEFAULT_HARD_BACKSTOP_MS = 4 * 60 * 60_000; // 4 hours
204
204
  const IDLE_POLL_INTERVAL_MS = 30_000; // 30 s
205
+ // 0.10.0 H4 — `Stop` hook as authoritative turn-done.
206
+ // The Stop hook fires when claude finishes responding — same
207
+ // semantic as the JSONL `result` event. Both should land within
208
+ // ms of each other; this grace gives JSONL a chance to win (full
209
+ // result data: subtype, stopReason, all the metadata) before the
210
+ // Stop hook synthesizes a fallback settle. If the JSONL stream is
211
+ // broken or stuck, Stop carries the turn to completion alone.
212
+ const DEFAULT_STOP_GRACE_MS = 2_000; // 2 s
205
213
 
206
214
  // B8 (slow-MCP readiness): how long the claude `--debug-file` log must
207
215
  // have had NO new bytes appended before the startup is considered
@@ -251,6 +259,7 @@ class TmuxProcess extends Process {
251
259
  readyTimeoutMs = DEFAULT_READY_TIMEOUT_MS,
252
260
  turnTimeoutMs = DEFAULT_TURN_TIMEOUT_MS,
253
261
  hardBackstopMs = DEFAULT_HARD_BACKSTOP_MS,
262
+ stopGraceMs = DEFAULT_STOP_GRACE_MS,
254
263
  pollMs = DEFAULT_POLL_MS,
255
264
  quiesceMs = DEFAULT_QUIESCE_MS,
256
265
  lateGraceMs = 1500,
@@ -295,6 +304,7 @@ class TmuxProcess extends Process {
295
304
  this.readyTimeoutMs = readyTimeoutMs;
296
305
  this.turnTimeoutMs = turnTimeoutMs;
297
306
  this.hardBackstopMs = hardBackstopMs;
307
+ this.stopGraceMs = stopGraceMs;
298
308
  this.pollMs = pollMs;
299
309
  this.quiesceMs = quiesceMs;
300
310
  this.readyDebugQuietMs = readyDebugQuietMs;
@@ -1601,7 +1611,7 @@ class TmuxProcess extends Process {
1601
1611
  }
1602
1612
 
1603
1613
  /**
1604
- * Hook-event handler. Three roles, layered over time:
1614
+ * Hook-event handler. Four roles, layered over time:
1605
1615
  *
1606
1616
  * H1 (rc.36) — emit `hook-event` so polygram persists each event
1607
1617
  * to the events DB; observer-only.
@@ -1615,24 +1625,63 @@ class TmuxProcess extends Process {
1615
1625
  * the structural fix for the msg-884 incident (49-min
1616
1626
  * SoundCloud subagent killed at the 30-min wall-clock while
1617
1627
  * demonstrably alive).
1618
- *
1619
- * H4 `Stop` as authoritative completion (still pending).
1628
+ * H4 (rc.41) — `Stop` hook is an authoritative turn-done signal.
1629
+ * If JSONL `result` doesn't fire within `stopGraceMs`,
1630
+ * synthesize a settle from the Stop payload so a broken or
1631
+ * stuck JSONL stream can't strand a finished turn. Promise-
1632
+ * resolve idempotency means JSONL still wins when both fire.
1620
1633
  *
1621
1634
  * Parse errors and unknown event shapes are intentionally still
1622
1635
  * forwarded — observer-only metrics for stream-reliability soak.
1623
1636
  */
1624
1637
  _handleHookEvent(ev) {
1625
1638
  // H3: every hook event (except the diagnostic types) is liveness
1626
- // evidence. Heartbeat every turn in the active group so the
1627
- // idle-ceiling poller resets. We don't differentiate by event
1639
+ // evidence. Heartbeat every turn we can identify as in-flight so
1640
+ // the idle-ceiling poller resets. We don't differentiate by event
1628
1641
  // type — even Notification or UserPromptSubmit prove claude is
1629
1642
  // active in this session.
1643
+ //
1644
+ // Two scopes are searched (deduped via Set): active group turns
1645
+ // (the steady state once `user-message` has landed) AND the
1646
+ // pendingQueue head (the PRE-active window between turn start
1647
+ // and the first `user-message`). Hook events can fire in either
1648
+ // window — e.g. `UserPromptSubmit` arrives just after claude
1649
+ // receives the paste but BEFORE the `user-message` is echoed
1650
+ // back into the JSONL. Without the pendingQueue fallback, that
1651
+ // window leaves the turn un-heartbeated and the idle poller
1652
+ // could fire on a turn that's actively starting up.
1630
1653
  if (ev?.type && ev.type !== 'parse-error' && ev.type !== 'unknown') {
1631
- const turns = this._activeGroup?.turns || [];
1654
+ const turns = new Set(this._activeGroup?.turns || []);
1655
+ const head = this.pendingQueue[0];
1656
+ if (head) turns.add(head);
1632
1657
  for (const t of turns) {
1633
1658
  this._heartbeat(t, `hook:${ev.type}`);
1634
1659
  }
1635
1660
  }
1661
+ // H4: Stop hook → synthesize a settle for the primary turn after
1662
+ // a grace, so JSONL `result` (which carries richer metadata)
1663
+ // wins when both fire. If JSONL never arrives — broken stream,
1664
+ // stuck parser — the Stop synth settles the turn instead of
1665
+ // stranding it. Idempotent: a later JSONL settleResult call is
1666
+ // a no-op once the promise has resolved.
1667
+ if (ev?.type === 'Stop') {
1668
+ const primary = (this._activeGroup?.turns || [])
1669
+ .find((t) => t.kind === 'primary');
1670
+ if (primary && typeof primary.settleResult === 'function') {
1671
+ const synth = {
1672
+ text: primary.text || ev.lastAssistantMessage || '',
1673
+ subtype: 'success',
1674
+ stopReason: 'stop_hook',
1675
+ sessionId: this.claudeSessionId,
1676
+ via: 'stop-hook',
1677
+ };
1678
+ const timer = setTimeout(
1679
+ () => primary.settleResult(synth),
1680
+ this.stopGraceMs,
1681
+ );
1682
+ timer.unref?.();
1683
+ }
1684
+ }
1636
1685
  this.emit('hook-event', ev);
1637
1686
  }
1638
1687
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.10.0-rc.40",
3
+ "version": "0.10.0-rc.41",
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": {