polygram 0.10.0-rc.26 → 0.10.0-rc.28
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.
|
|
4
|
+
"version": "0.10.0-rc.28",
|
|
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",
|
|
@@ -174,6 +174,20 @@ const DEFAULT_TURN_TIMEOUT_MS = 5 * 60_000;
|
|
|
174
174
|
const DEFAULT_POLL_MS = 250;
|
|
175
175
|
const DEFAULT_QUIESCE_MS = 500; // require READY for this long before declaring done
|
|
176
176
|
|
|
177
|
+
// B8 (slow-MCP readiness): how long the claude `--debug-file` log must
|
|
178
|
+
// have had NO new bytes appended before the startup is considered
|
|
179
|
+
// quiescent. During MCP cold-start the debug log is DENSELY written —
|
|
180
|
+
// the production log shows ~33 s of `MCP server "X": connecting/…
|
|
181
|
+
// connected` lines, then total silence once the TUI is idle. A genuine
|
|
182
|
+
// idle TUI's debug log is quiet for minutes. 1 s is comfortably longer
|
|
183
|
+
// than the gap between two consecutive MCP-startup log writes (verified
|
|
184
|
+
// against the production debug log) yet short enough to add only ~1 s
|
|
185
|
+
// to a clean startup. Used ONLY by `_waitForReady`, scoped to the
|
|
186
|
+
// startup wait — never reused mid-turn (the debug log keeps being
|
|
187
|
+
// written during a turn; quiescence-of-the-whole-log would wrongly
|
|
188
|
+
// block, but `_waitForReady` runs only at startup before any turn).
|
|
189
|
+
const DEFAULT_READY_DEBUG_QUIET_MS = 1000;
|
|
190
|
+
|
|
177
191
|
// R7: sentinel returned by _awaitTurnComplete when its poll loop is
|
|
178
192
|
// stopped by the caller's absolute-deadline abort (rather than by a
|
|
179
193
|
// real READY quiescence or its own internal timeout). _runTurn maps
|
|
@@ -197,6 +211,9 @@ class TmuxProcess extends Process {
|
|
|
197
211
|
* @param {number} [opts.turnTimeoutMs]
|
|
198
212
|
* @param {number} [opts.pollMs]
|
|
199
213
|
* @param {number} [opts.quiesceMs]
|
|
214
|
+
* @param {number} [opts.readyDebugQuietMs] — B8: `_waitForReady`
|
|
215
|
+
* requires the claude `--debug-file` log to have had no new bytes
|
|
216
|
+
* for this long (in addition to pane stability + ready hint).
|
|
200
217
|
*/
|
|
201
218
|
constructor({
|
|
202
219
|
sessionKey, chatId, threadId, label,
|
|
@@ -216,6 +233,14 @@ class TmuxProcess extends Process {
|
|
|
216
233
|
// presses before giving up loud.
|
|
217
234
|
submitConfirmMs = 1500,
|
|
218
235
|
submitConfirmRetries = 4,
|
|
236
|
+
// B8: `_waitForReady` gates startup on the claude `--debug-file`
|
|
237
|
+
// log going quiet (no new bytes for this long) — the signal that
|
|
238
|
+
// is NOT fooled by a byte-stable-but-still-loading TUI pane.
|
|
239
|
+
readyDebugQuietMs = DEFAULT_READY_DEBUG_QUIET_MS,
|
|
240
|
+
// Test seam: a fake `fs` forwarded to the readiness debug-log tail
|
|
241
|
+
// so a unit test can drive debug-log writes deterministically
|
|
242
|
+
// without touching the real filesystem.
|
|
243
|
+
fs: fsOverride = null,
|
|
219
244
|
} = {}) {
|
|
220
245
|
super({ sessionKey, chatId, threadId, label });
|
|
221
246
|
if (!runner) throw new TypeError('TmuxProcess: runner required');
|
|
@@ -241,6 +266,8 @@ class TmuxProcess extends Process {
|
|
|
241
266
|
this.turnTimeoutMs = turnTimeoutMs;
|
|
242
267
|
this.pollMs = pollMs;
|
|
243
268
|
this.quiesceMs = quiesceMs;
|
|
269
|
+
this.readyDebugQuietMs = readyDebugQuietMs;
|
|
270
|
+
this._fsOverride = fsOverride;
|
|
244
271
|
this.lateGraceMs = lateGraceMs;
|
|
245
272
|
this.queueCap = queueCap;
|
|
246
273
|
// Optional shared poll scheduler. When provided, the polling
|
|
@@ -743,6 +770,12 @@ class TmuxProcess extends Process {
|
|
|
743
770
|
// The interrupt signal still wins here too — Bug 3: an
|
|
744
771
|
// interrupted tool turn writes no terminal JSONL `result`, so
|
|
745
772
|
// without this racer it would hang to `turnTimeoutMs`.
|
|
773
|
+
//
|
|
774
|
+
// B10: an outstanding `Agent` subagent counts as "tool in
|
|
775
|
+
// flight" exactly like a foreground `Bash` — its `tool-use`
|
|
776
|
+
// already set `toolUsedThisTurn`, so this branch catches the
|
|
777
|
+
// common case. The race where capture wins BEFORE the `Agent`
|
|
778
|
+
// tool_use line is tailed is handled by the §6 re-check below.
|
|
746
779
|
if (winner.kind === 'capture' && turn.toolUsedThisTurn) {
|
|
747
780
|
winner = await Promise.race([
|
|
748
781
|
turn.resultPromise.then((ev) => ({ kind: 'jsonl', ev })),
|
|
@@ -808,11 +841,41 @@ class TmuxProcess extends Process {
|
|
|
808
841
|
text = turn.text;
|
|
809
842
|
} else {
|
|
810
843
|
const lateGraceMs = this.lateGraceMs ?? 1500;
|
|
811
|
-
|
|
844
|
+
let late = await Promise.race([
|
|
812
845
|
turn.resultPromise.then((ev) => ({ kind: 'jsonl-late', ev })),
|
|
813
846
|
new Promise((r) => setTimeout(() => r({ kind: 'no-jsonl' }), lateGraceMs)),
|
|
814
847
|
]);
|
|
815
|
-
|
|
848
|
+
// B10 (shumorobot Music topic, 2026-05-20): the main agent
|
|
849
|
+
// delegated to an `Agent` subagent within ~7 s, then the main
|
|
850
|
+
// pane went quiescent for MINUTES while the subagent ran in
|
|
851
|
+
// its own sidechain. capture-pane read that quiescence as
|
|
852
|
+
// "done"; the main agent had emitted only the `Agent` call so
|
|
853
|
+
// no JSONL reply text existed yet, and the §6 fail-loud below
|
|
854
|
+
// fired ~grace-window in — closing a turn that was genuinely
|
|
855
|
+
// in flight. A subagent is still running iff its `Agent`
|
|
856
|
+
// tool_use has no matching `tool-result` yet. While one is
|
|
857
|
+
// outstanding, capture-pane quiescence of the MAIN pane is
|
|
858
|
+
// meaningless — the turn completes only when the subagent
|
|
859
|
+
// returns and the main agent emits its real terminal reply.
|
|
860
|
+
// Wait for that JSONL `result`, bounded by the absolute turn
|
|
861
|
+
// deadline so a genuinely wedged turn still fails loud.
|
|
862
|
+
if (late.kind === 'no-jsonl' && turn.outstandingSubagents.size > 0) {
|
|
863
|
+
this.emit('subagent-wait', {
|
|
864
|
+
outstanding: turn.outstandingSubagents.size,
|
|
865
|
+
turnId: turn.turnId,
|
|
866
|
+
});
|
|
867
|
+
late = await Promise.race([
|
|
868
|
+
turn.resultPromise.then((ev) => ({ kind: 'jsonl-late', ev })),
|
|
869
|
+
turnDeadlineP,
|
|
870
|
+
turn.interruptP.then(() => ({ kind: 'interrupt' })),
|
|
871
|
+
]);
|
|
872
|
+
}
|
|
873
|
+
if (late.kind === 'interrupt') {
|
|
874
|
+
turn.interrupted = true;
|
|
875
|
+
text = turn.text || '';
|
|
876
|
+
resultSubtype = 'interrupted';
|
|
877
|
+
stopReason = 'interrupted';
|
|
878
|
+
} else if (late.kind === 'jsonl-late') {
|
|
816
879
|
resolvedVia = 'jsonl-late';
|
|
817
880
|
text = turn.text || late.ev.text || '';
|
|
818
881
|
resultSubtype = late.ev.subtype || 'success';
|
|
@@ -941,6 +1004,13 @@ class TmuxProcess extends Process {
|
|
|
941
1004
|
text: '',
|
|
942
1005
|
toolUses: 0,
|
|
943
1006
|
toolUsedThisTurn: false,
|
|
1007
|
+
// B10: outstanding `Agent` (subagent/Task) tool_use ids — a
|
|
1008
|
+
// tool_use with no matching tool_result yet. A non-empty set
|
|
1009
|
+
// means a subagent is running in its own sidechain context: the
|
|
1010
|
+
// main pane goes quiescent for MINUTES while it works, and that
|
|
1011
|
+
// quiescence must NOT be read as turn completion. Cleared when
|
|
1012
|
+
// the matching `tool-result` arrives.
|
|
1013
|
+
outstandingSubagents: new Set(),
|
|
944
1014
|
stopReason: null,
|
|
945
1015
|
resultEvent: null,
|
|
946
1016
|
via: null, // autosteer: 'fold' | 'new-turn'
|
|
@@ -1118,8 +1188,25 @@ class TmuxProcess extends Process {
|
|
|
1118
1188
|
// the flag here so a transient capture-pane "ready" between
|
|
1119
1189
|
// tool calls cannot resolve a still-working turn.
|
|
1120
1190
|
t.toolUsedThisTurn = true;
|
|
1191
|
+
// B10: an `Agent` (subagent/Task) tool_use spawns a subagent
|
|
1192
|
+
// that runs for MINUTES in its own sidechain context while the
|
|
1193
|
+
// main pane sits quiescent. Track its id as outstanding until
|
|
1194
|
+
// the matching `tool-result` returns — `_runTurn` treats an
|
|
1195
|
+
// outstanding subagent as "turn still in flight" so the main
|
|
1196
|
+
// pane's quiescence cannot trip the §6 fail-loud.
|
|
1197
|
+
if (ev.name === 'Agent' && typeof ev.id === 'string') {
|
|
1198
|
+
t.outstandingSubagents.add(ev.id);
|
|
1199
|
+
}
|
|
1121
1200
|
}
|
|
1122
1201
|
this.emit('tool-use', ev.name);
|
|
1202
|
+
} else if (ev.type === 'tool-result') {
|
|
1203
|
+
// B10: a subagent returned. Clear the outstanding `Agent` call
|
|
1204
|
+
// it answers across every turn in the active group. A
|
|
1205
|
+
// tool-result for a non-Agent tool (or an id we never tracked)
|
|
1206
|
+
// is a harmless no-op — the set only ever held `Agent` ids.
|
|
1207
|
+
for (const t of this._activeGroup.turns) {
|
|
1208
|
+
t.outstandingSubagents.delete(ev.toolUseId);
|
|
1209
|
+
}
|
|
1123
1210
|
} else if (ev.type === 'usage') {
|
|
1124
1211
|
// Token-usage snapshot from JSONL. Cache for getContextUsage().
|
|
1125
1212
|
// Each assistant message carries the cumulative usage; latest
|
|
@@ -1573,43 +1660,95 @@ class TmuxProcess extends Process {
|
|
|
1573
1660
|
return this._sleep(this.pollMs);
|
|
1574
1661
|
}
|
|
1575
1662
|
|
|
1663
|
+
/**
|
|
1664
|
+
* Synchronously probe the byte-size of the claude `--debug-file` log.
|
|
1665
|
+
* Returns the size in bytes, or `null` if the log does not exist yet
|
|
1666
|
+
* (claude takes ~100 ms to create it after spawn) or cannot be
|
|
1667
|
+
* stat'd. Never throws — a readiness gate must not hard-fail on a
|
|
1668
|
+
* missing debug-log signal.
|
|
1669
|
+
*
|
|
1670
|
+
* B8: this is the size-delta channel `_waitForReady` uses to detect
|
|
1671
|
+
* debug-log quiescence — the signal a byte-stable-but-still-loading
|
|
1672
|
+
* TUI pane cannot fool. During MCP cold-start claude DENSELY appends
|
|
1673
|
+
* to this log (`MCP server "X": connecting…` / `…connected in
|
|
1674
|
+
* NNNNms`); once the TUI is genuinely idle the log goes silent. This
|
|
1675
|
+
* mirrors `LogTail`'s approach (stat-based size-delta detection,
|
|
1676
|
+
* ENOENT-tolerant) without arming a second async tailer: the
|
|
1677
|
+
* readiness loop already polls on a timer, so a synchronous
|
|
1678
|
+
* `statSync` per poll is the simplest, fully-deterministic fit and
|
|
1679
|
+
* needs no fs.watch.
|
|
1680
|
+
*
|
|
1681
|
+
* `this._fsOverride` is a test seam — a fake `fs` lets a unit test
|
|
1682
|
+
* drive debug-log growth by hand.
|
|
1683
|
+
*/
|
|
1684
|
+
_probeDebugLogSize() {
|
|
1685
|
+
const fsImpl = this._fsOverride || require('fs');
|
|
1686
|
+
try {
|
|
1687
|
+
return fsImpl.statSync(this.debugLogPath).size;
|
|
1688
|
+
} catch {
|
|
1689
|
+
return null; // ENOENT (not created yet) / any stat error
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1576
1693
|
async _waitForReady() {
|
|
1577
1694
|
const deadline = this._now() + this.readyTimeoutMs;
|
|
1578
1695
|
let lastBuf = '';
|
|
1579
1696
|
// B6 (shumorobot 2026-05-18, Music topic, twice): a slow
|
|
1580
1697
|
// custom-agent spawn (`music-curation:music-curator` loading
|
|
1581
1698
|
// several MCP servers) leaves the claude TUI mid-startup for
|
|
1582
|
-
// SECONDS
|
|
1583
|
-
//
|
|
1584
|
-
//
|
|
1585
|
-
//
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1699
|
+
// SECONDS. Throughout that window the TUI ALREADY renders its
|
|
1700
|
+
// ready hint (`? for shortcuts` / `bypass permissions on`) at the
|
|
1701
|
+
// bottom of its startup banner. The old `_waitForReady` returned
|
|
1702
|
+
// the INSTANT `READY_HINTS_RE` matched — on the first poll, while
|
|
1703
|
+
// MCP servers were still loading — so the first `send()` pasted
|
|
1704
|
+
// into a not-yet-ready TUI and the submitted Enter was dropped.
|
|
1705
|
+
//
|
|
1706
|
+
// B6's fix gated on pane QUIESCENCE: ready ⇔ the hint is present
|
|
1707
|
+
// AND the `capture-pane` is byte-stable across consecutive polls.
|
|
1588
1708
|
//
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1709
|
+
// B8 (slow-MCP-startup, 2026-05-19): pane quiescence is NOT
|
|
1710
|
+
// enough. The production debug log for the Music topic shows MCP
|
|
1711
|
+
// cold-start spanning ~33 s (`plugin:serena:serena` 27.5 s,
|
|
1712
|
+
// peekaboo 9.3 s, …) — yet across that whole window the claude
|
|
1713
|
+
// pane is BYTE-STABLE: the REPL mounts and paints its ready hint
|
|
1714
|
+
// immediately, then MCP servers load entirely off-screen with the
|
|
1715
|
+
// pane unchanged. B6 reads "stable = ready", `start()` resolves
|
|
1716
|
+
// mid-MCP-load, the paste lands in a TUI that is not yet
|
|
1717
|
+
// interactive, and the Enter is dropped (the Music-topic break,
|
|
1718
|
+
// 5+ times). `isolateUserConfig` (rc.26) removes MCP servers for
|
|
1719
|
+
// the Music topic specifically, but the gate is still wrong for
|
|
1720
|
+
// any non-isolated topic that legitimately loads MCP servers.
|
|
1597
1721
|
//
|
|
1598
|
-
// The
|
|
1599
|
-
//
|
|
1600
|
-
//
|
|
1601
|
-
//
|
|
1602
|
-
// `
|
|
1603
|
-
// ready hint)
|
|
1722
|
+
// The pane is fooled; the claude `--debug-file` log is NOT. During
|
|
1723
|
+
// MCP startup that log is ACTIVELY written; a genuinely-ready idle
|
|
1724
|
+
// TUI's debug log is quiet (verified against the production log —
|
|
1725
|
+
// dense writes for ~33 s, then silence for over an hour). So
|
|
1726
|
+
// `_waitForReady` now ALSO gates on debug-log quiescence:
|
|
1727
|
+
// ready ⇔ (ready hint present)
|
|
1728
|
+
// AND (pane byte-stable across consecutive polls)
|
|
1729
|
+
// AND (the --debug-file log has had no new bytes for
|
|
1730
|
+
// `readyDebugQuietMs`).
|
|
1731
|
+
// During MCP load the debug log is active → not ready. After load
|
|
1732
|
+
// → quiet → ready. Still bounded by `readyTimeoutMs`, so a
|
|
1733
|
+
// genuinely wedged spawn still throws TMUX_READY_TIMEOUT.
|
|
1604
1734
|
//
|
|
1605
|
-
//
|
|
1606
|
-
//
|
|
1607
|
-
//
|
|
1608
|
-
//
|
|
1609
|
-
//
|
|
1610
|
-
//
|
|
1735
|
+
// Scope: the debug log keeps being written DURING normal turns, so
|
|
1736
|
+
// whole-log quiescence would wrongly block mid-turn — but
|
|
1737
|
+
// `_waitForReady` runs ONLY at startup, before any turn, so
|
|
1738
|
+
// startup-phase quiescence is exactly the right window. The
|
|
1739
|
+
// `readyDebugQuietMs` clock lives entirely inside this method and
|
|
1740
|
+
// is never consulted by `_awaitTurnComplete`.
|
|
1611
1741
|
let readySinceAt = null; // when the (hint + stable-pane) state began
|
|
1612
1742
|
let prevBuf = null; // last poll's capture, for the stability compare
|
|
1743
|
+
// B8 debug-log quiescence tracking. `prevDebugSize` is the
|
|
1744
|
+
// `--debug-file` byte-size seen on the PREVIOUS poll; `lastGrowthAt`
|
|
1745
|
+
// is when the size last increased. The debug log is "quiet" once it
|
|
1746
|
+
// has not grown for `readyDebugQuietMs`. The FIRST non-null size is
|
|
1747
|
+
// a baseline (no growth recorded for it) — claude actively writing
|
|
1748
|
+
// its MCP-startup burst makes the size keep climbing across the
|
|
1749
|
+
// next polls, which is what resets the clock.
|
|
1750
|
+
let prevDebugSize = null;
|
|
1751
|
+
let lastGrowthAt = null;
|
|
1613
1752
|
if (this.pollScheduler) this.pollScheduler.acquire();
|
|
1614
1753
|
try {
|
|
1615
1754
|
while (this._now() < deadline) {
|
|
@@ -1624,11 +1763,28 @@ class TmuxProcess extends Process {
|
|
|
1624
1763
|
// least two matching captures, which is the point.
|
|
1625
1764
|
const hintPresent = READY_HINTS_RE.test(lastBuf);
|
|
1626
1765
|
const paneStable = prevBuf !== null && lastBuf === prevBuf;
|
|
1627
|
-
|
|
1766
|
+
// B8: the --debug-file log must have stopped growing for
|
|
1767
|
+
// `readyDebugQuietMs`. A debug-log size increase since the last
|
|
1768
|
+
// poll = claude is still writing (MCP servers connecting) → not
|
|
1769
|
+
// quiet. The log never appearing at all (null size for the whole
|
|
1770
|
+
// wait — no MCP startup observed) reads as quiet, so the B6 pane
|
|
1771
|
+
// check still gates a no-agent / fast spawn.
|
|
1772
|
+
const debugSize = this._probeDebugLogSize();
|
|
1773
|
+
if (debugSize !== null && prevDebugSize !== null
|
|
1774
|
+
&& debugSize > prevDebugSize) {
|
|
1775
|
+
lastGrowthAt = this._now(); // log grew → claude still writing
|
|
1776
|
+
}
|
|
1777
|
+
if (debugSize !== null) prevDebugSize = debugSize;
|
|
1778
|
+
const debugQuiet = lastGrowthAt === null
|
|
1779
|
+
|| (this._now() - lastGrowthAt) >= this.readyDebugQuietMs;
|
|
1780
|
+
if (hintPresent && paneStable && debugQuiet) {
|
|
1628
1781
|
if (readySinceAt == null) readySinceAt = this._now();
|
|
1629
1782
|
if (this._now() - readySinceAt >= this.quiesceMs) return;
|
|
1630
1783
|
} else {
|
|
1631
|
-
|
|
1784
|
+
// pane moved / hint gone / debug log still being written →
|
|
1785
|
+
// reset the clock. A debug-log write during the quiesce
|
|
1786
|
+
// window means MCP startup is not finished.
|
|
1787
|
+
readySinceAt = null;
|
|
1632
1788
|
}
|
|
1633
1789
|
prevBuf = lastBuf;
|
|
1634
1790
|
await this._waitForNextTick();
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
* (ONCE per message.id, on finalize)
|
|
51
51
|
* - last-prompt → 'last-prompt' (fallback complete signal)
|
|
52
52
|
* - user (top-level string) → 'user-message' { text, parentUuid, promptId }
|
|
53
|
+
* - user tool_result block → 'tool-result' { toolUseId, isError }
|
|
53
54
|
* - queue-operation → 'queue-operation' { operation, content }
|
|
54
55
|
*
|
|
55
56
|
* Robust against malformed lines: skips them.
|
|
@@ -128,6 +129,31 @@ function extractContentBlocks(content) {
|
|
|
128
129
|
return { textParts, toolUses };
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Pull `tool_result` blocks out of a user message's `content` array.
|
|
134
|
+
* A user message with array content carries API-shaped tool feedback
|
|
135
|
+
* (NOT a user prompt). Each `tool_result` block names the `tool_use`
|
|
136
|
+
* it answers via `tool_use_id` — the matcher polygram's turn ledger
|
|
137
|
+
* uses to clear an outstanding `Agent`/subagent call.
|
|
138
|
+
*
|
|
139
|
+
* @returns {object[]} `tool-result` events, possibly empty.
|
|
140
|
+
*/
|
|
141
|
+
function extractToolResults(content) {
|
|
142
|
+
const out = [];
|
|
143
|
+
if (!Array.isArray(content)) return out;
|
|
144
|
+
for (const block of content) {
|
|
145
|
+
if (!block || typeof block !== 'object') continue;
|
|
146
|
+
if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') {
|
|
147
|
+
out.push({
|
|
148
|
+
type: 'tool-result',
|
|
149
|
+
toolUseId: block.tool_use_id,
|
|
150
|
+
isError: block.is_error === true,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
/**
|
|
132
158
|
* Join assistant text blocks the way the SDK backend's
|
|
133
159
|
* `extractAssistantText` does (rc.8 cross-backend parity): blocks
|
|
@@ -204,12 +230,15 @@ function parseLine(line) {
|
|
|
204
230
|
} else if (obj.type === 'last-prompt') {
|
|
205
231
|
out.push({ type: 'last-prompt', text: obj.lastPrompt ?? '' });
|
|
206
232
|
} else if (obj.type === 'user' && obj.message) {
|
|
207
|
-
// Top-level user message
|
|
208
|
-
//
|
|
209
|
-
//
|
|
233
|
+
// Top-level user message. String content is a user prompt. Array
|
|
234
|
+
// content carries API-shaped `tool_result` blocks (tool feedback,
|
|
235
|
+
// NOT a prompt) — those surface as `tool-result` events so the
|
|
236
|
+
// turn ledger can clear an outstanding `Agent`/subagent call.
|
|
210
237
|
const content = obj.message.content;
|
|
211
238
|
if (typeof content === 'string' && content.length > 0) {
|
|
212
239
|
out.push({ type: 'user-message', text: content });
|
|
240
|
+
} else {
|
|
241
|
+
out.push(...extractToolResults(content));
|
|
213
242
|
}
|
|
214
243
|
} else if (obj.type === 'attachment' && obj.attachment) {
|
|
215
244
|
const a = obj.attachment;
|
|
@@ -321,6 +350,13 @@ class SessionEventAggregator {
|
|
|
321
350
|
parentUuid: obj.parentUuid ?? null,
|
|
322
351
|
promptId: obj.promptId ?? null,
|
|
323
352
|
});
|
|
353
|
+
} else {
|
|
354
|
+
// Array content — API-shaped `tool_result` blocks. A subagent
|
|
355
|
+
// (`Agent` tool) returning to the main agent surfaces here;
|
|
356
|
+
// the turn ledger keys on `toolUseId` to clear the outstanding
|
|
357
|
+
// subagent call so capture-pane quiescence of the main pane is
|
|
358
|
+
// not mistaken for turn completion while the subagent runs.
|
|
359
|
+
out.push(...extractToolResults(content));
|
|
324
360
|
}
|
|
325
361
|
} else if (obj.type === 'last-prompt') {
|
|
326
362
|
out.push({ type: 'last-prompt', text: obj.lastPrompt ?? '' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.10.0-rc.
|
|
3
|
+
"version": "0.10.0-rc.28",
|
|
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": {
|