polygram 0.17.8 → 0.17.10

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.
@@ -350,6 +350,10 @@ class CliProcess extends Process {
350
350
  // Monotonic count of work hooks (all but the terminal Stop) — the rung-2
351
351
  // no-reply backstop snapshots it at Stop capture to detect a later resume.
352
352
  this._workHookSeq = 0;
353
+ // In-flight sub-agent starts (pushed on Agent PreToolUse, spliced on SubagentStop).
354
+ // Initialized here so a SubagentStop arriving before any Agent start (a lagged/orphan
355
+ // teardown on a fresh proc) reads a length safely instead of throwing.
356
+ this._pendingSubagentStarts = [];
353
357
  // 0.13 D2: the InputLedger — every user-shaped input written to the bridge
354
358
  // gets an observable lifecycle: written → seen → resolved | dropped |
355
359
  // superseded | fold-suspected. Pre-P3, injectUserMessage minted a turn_id
@@ -2950,15 +2954,26 @@ class CliProcess extends Process {
2950
2954
 
2951
2955
  // 0.13 D1: every hook event is same-session ACTIVITY for the finalizer
2952
2956
  // ladder (generalizes the 2026-06-08 WA-topic fix, which only extended on
2953
- // Pre/PostToolUse) — EXCEPT Stop, which is a terminal signal, not work:
2954
- // noting it as activity would cancel its own attribution grace. parse-error
2955
- // and unknown are excluded too (stream noise is not evidence of work).
2956
- if (ev.type === 'Stop') {
2957
+ // Pre/PostToolUse) — EXCEPT terminal signals, which are not work: noting them
2958
+ // as activity would cancel a live attribution grace. parse-error and unknown
2959
+ // are excluded too (stream noise is not evidence of work).
2960
+ //
2961
+ // Stop is always terminal. SubagentStop is terminal ONLY when it's an ORPHAN —
2962
+ // a late/lagged/foreign teardown hook with no matching in-flight sub-agent start.
2963
+ // Such an orphan trails the main Stop (or arrives from a prior cycle) and must not
2964
+ // bump the work-hook counter (the rung-2 no-reply backstop reads a bump as "claude
2965
+ // resumed", withdrawing the captured Stop's delivery) nor count as activity. A
2966
+ // MATCHED SubagentStop (a sub-agent this cycle actually started is finishing) IS
2967
+ // work-relevant — it keeps withdrawing rung-2 eligibility on a boundary Stop, incl.
2968
+ // a tool-less sub-agent whose only post-boundary signal is its SubagentStop.
2969
+ const orphanSubagentStop = ev.type === 'SubagentStop'
2970
+ && !(this._pendingSubagentStarts && this._pendingSubagentStarts.length);
2971
+ if (ev.type === 'Stop' || orphanSubagentStop) {
2957
2972
  this._lastHookEventAt = Date.now();
2958
2973
  } else if (ev.type && ev.type !== 'parse-error' && ev.type !== 'unknown') {
2959
2974
  this._lastHookEventAt = Date.now();
2960
- // Monotonic count of WORK hooks (everything but the terminal Stop). The rung-2
2961
- // no-reply backstop snapshots this at Stop capture; a later increment means
2975
+ // Monotonic count of WORK hooks (all but terminal Stop / orphan SubagentStop). The
2976
+ // rung-2 no-reply backstop snapshots this at Stop capture; a later increment means
2962
2977
  // claude resumed work, withdrawing the stale Stop's finalize eligibility.
2963
2978
  this._workHookSeq = (this._workHookSeq || 0) + 1;
2964
2979
  this._noteActivity(`hook:${ev.type}`);
@@ -128,7 +128,19 @@ function getConfigWriteScope(chatConfig, threadId) {
128
128
  // runs), and report threadId:null so the audit row reflects chat-level reach.
129
129
  if (tid && chatConfig?.isolateTopics === true) {
130
130
  chatConfig.topics = chatConfig.topics || {};
131
- chatConfig.topics[tid] = chatConfig.topics[tid] || {};
131
+ // A topic can be stored in the legacy / hand-edited form where the entry
132
+ // is the bare name string (topics["329"] = "Advertising") rather than the
133
+ // canonical object ({ name: "Advertising" }). getTopicConfig / getTopicName
134
+ // already tolerate that; the write path must too — assigning a property to
135
+ // a string throws under strict mode, which silently swallowed every /model
136
+ // + /effort button tap in such a topic. Normalise to the object form,
137
+ // preserving the name.
138
+ const existing = chatConfig.topics[tid];
139
+ if (typeof existing === 'string') {
140
+ chatConfig.topics[tid] = { name: existing };
141
+ } else if (!existing || typeof existing !== 'object') {
142
+ chatConfig.topics[tid] = {};
143
+ }
132
144
  return { scope: chatConfig.topics[tid], threadId: tid };
133
145
  }
134
146
  return { scope: chatConfig, threadId: null };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.17.8",
3
+ "version": "0.17.10",
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": {