polygram 0.12.0 → 0.12.2
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.
|
@@ -322,7 +322,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
322
322
|
// (P0 spike Q-B: claude echoes only the trigger id) — this array can.
|
|
323
323
|
consumed_turn_ids: {
|
|
324
324
|
type: 'array', items: { type: 'string' },
|
|
325
|
-
description: 'turn_id of EVERY <channel> message this reply answers or has absorbed since your last reply
|
|
325
|
+
description: 'turn_id of EVERY <channel> message this reply answers or has absorbed since your last reply, including mid-turn follow-ups. Set it on EVERY reply, even short one-line ones; if you answered two messages in one reply, list BOTH turn_ids. Omitting a folded follow-up makes polygram treat it as dropped.',
|
|
326
326
|
},
|
|
327
327
|
},
|
|
328
328
|
required: ['chat_id', 'text'],
|
|
@@ -754,11 +754,14 @@ class CliProcess extends Process {
|
|
|
754
754
|
'as normal — only the FINAL user-visible message needs to go through',
|
|
755
755
|
'the reply tool.',
|
|
756
756
|
'',
|
|
757
|
-
'When you call `reply`, ALWAYS set `consumed_turn_ids` to the turn_id',
|
|
758
|
-
'
|
|
759
|
-
'since your last reply
|
|
760
|
-
'
|
|
761
|
-
'
|
|
757
|
+
'When you call `reply`, ALWAYS set `consumed_turn_ids` to the turn_id of',
|
|
758
|
+
'EVERY <channel> message this reply answers or folds in — every mid-turn',
|
|
759
|
+
'follow-up you absorbed since your last reply. This applies to EVERY reply,',
|
|
760
|
+
'including SHORT one-line ones: if two messages arrived and you answered both',
|
|
761
|
+
'in one reply, list BOTH turn_ids (e.g. consumed_turn_ids: ["<original-id>",',
|
|
762
|
+
'"<follow-up-id>"]). Omitting a folded follow-up makes polygram read it as',
|
|
763
|
+
'DROPPED — it gets re-sent to you or flagged as a lost message. When unsure,',
|
|
764
|
+
'include the id.',
|
|
762
765
|
'',
|
|
763
766
|
'### Staying responsive on a long task',
|
|
764
767
|
'',
|
|
@@ -1159,7 +1162,10 @@ class CliProcess extends Process {
|
|
|
1159
1162
|
// happens at the chat_id guard below.
|
|
1160
1163
|
const chatIdMatches = this.chatId == null || String(args.chat_id) === String(this.chatId);
|
|
1161
1164
|
if (chatIdMatches && Array.isArray(args.consumed_turn_ids) && args.consumed_turn_ids.length) {
|
|
1162
|
-
this._ledgerAckConsumed(
|
|
1165
|
+
this._ledgerAckConsumed(
|
|
1166
|
+
args.consumed_turn_ids.filter((x) => typeof x === 'string'),
|
|
1167
|
+
typeof args.text === 'string' ? args.text : '',
|
|
1168
|
+
);
|
|
1163
1169
|
} else if (chatIdMatches && msg.name === 'reply' && 'consumed_turn_ids' in args) {
|
|
1164
1170
|
this._lastAckFieldAt = Date.now(); // field present but empty — contract observed
|
|
1165
1171
|
}
|
|
@@ -1534,8 +1540,16 @@ class CliProcess extends Process {
|
|
|
1534
1540
|
if (e._watchdogTimer) { clearTimeout(e._watchdogTimer); e._watchdogTimer = null; }
|
|
1535
1541
|
}
|
|
1536
1542
|
|
|
1537
|
-
/**
|
|
1538
|
-
|
|
1543
|
+
/**
|
|
1544
|
+
* Tier 2C: a reply carried consumed_turn_ids — acknowledge every known id.
|
|
1545
|
+
* `consumingText` is the text the consuming reply actually delivered; it is
|
|
1546
|
+
* recorded on each consumed pending so _finalizeTurn can tell a genuine fold
|
|
1547
|
+
* (the answer rode the sibling reply) from an early ack that DIDN'T carry the
|
|
1548
|
+
* answer (prod 2026-06-13: a 294-char "Researching now…" ack, then the real
|
|
1549
|
+
* answer arrived as Stop-fallback text — which must NOT be suppressed).
|
|
1550
|
+
* See docs/0.13-consumed-ack-stop-fallback-drop-spec.md.
|
|
1551
|
+
*/
|
|
1552
|
+
_ledgerAckConsumed(ids, consumingText = '') {
|
|
1539
1553
|
this._lastAckFieldAt = Date.now();
|
|
1540
1554
|
for (const id of ids) {
|
|
1541
1555
|
const e = this.inputLedger.get(id);
|
|
@@ -1552,6 +1566,14 @@ class CliProcess extends Process {
|
|
|
1552
1566
|
const pending = this.pendingTurns.get(id);
|
|
1553
1567
|
if (pending) {
|
|
1554
1568
|
pending._consumedAcked = true;
|
|
1569
|
+
// Remember WHAT the consuming reply delivered for this turn. The last
|
|
1570
|
+
// ack wins; a longer subsequent ack is the safer record (more likely to
|
|
1571
|
+
// actually contain the answer). _finalizeTurn only suppresses a
|
|
1572
|
+
// Stop-fallback finalize when its text matches this.
|
|
1573
|
+
if (typeof consumingText === 'string'
|
|
1574
|
+
&& consumingText.length > (pending._consumedByText?.length || 0)) {
|
|
1575
|
+
pending._consumedByText = consumingText;
|
|
1576
|
+
}
|
|
1555
1577
|
// The ack itself flips rung-2 eligibility on — arm now. (The turn's
|
|
1556
1578
|
// last _noteActivity ran BEFORE this flag was set, so without this
|
|
1557
1579
|
// a quiet tail would never re-arm and the turn would sit until a
|
|
@@ -1890,10 +1912,33 @@ class CliProcess extends Process {
|
|
|
1890
1912
|
// the turn without calling mcp__polygram-bridge__reply), use the Stop
|
|
1891
1913
|
// hook's last_assistant_message as the text. Same rescue pattern rc.41
|
|
1892
1914
|
// H4 uses on tmux backend when JSONL stream is broken.
|
|
1893
|
-
|
|
1915
|
+
const usedStopFallback = !text && !!pending._stopHookData?.lastAssistantMessage;
|
|
1916
|
+
if (usedStopFallback) {
|
|
1894
1917
|
text = pending._stopHookData.lastAssistantMessage;
|
|
1895
1918
|
this.logger.warn?.(`[${this.label}] cli: turn finalized via stop-hook fallback (no reply tool call); text_len=${text.length}`);
|
|
1896
1919
|
}
|
|
1920
|
+
// A _consumedAcked turn is "already delivered" ONLY when the consuming
|
|
1921
|
+
// sibling reply actually carried THIS text — not merely an ack. Prod
|
|
1922
|
+
// 2026-06-13 (Shumabit@UMI/37): the consuming reply was a 294-char
|
|
1923
|
+
// "Researching now…" ack, then the real answer arrived as Stop-fallback
|
|
1924
|
+
// text. Suppressing it (alreadyDelivered=true → polygram.js short-circuit)
|
|
1925
|
+
// dropped the answer silently for 5h20m. Only suppress when the rescued
|
|
1926
|
+
// text matches what the sibling delivered.
|
|
1927
|
+
// docs/0.13-consumed-ack-stop-fallback-drop-spec.md
|
|
1928
|
+
const norm = (s) => (s || '').trim();
|
|
1929
|
+
const consumedCoversFallback = !usedStopFallback || norm(text) === norm(pending._consumedByText);
|
|
1930
|
+
const alreadyDelivered = hadReplyToolCalls
|
|
1931
|
+
|| (pending._consumedAcked === true && consumedCoversFallback);
|
|
1932
|
+
if (usedStopFallback && pending._consumedAcked === true && !consumedCoversFallback) {
|
|
1933
|
+
this.logger.warn?.(`[${this.label}] cli: consumed-ack did NOT cover the Stop-fallback answer — delivering rescued text (len=${text.length})`);
|
|
1934
|
+
this._logEvent('cli-consumed-ack-fallback-rescued', {
|
|
1935
|
+
turn_id: turnId,
|
|
1936
|
+
session_key: this.sessionKey,
|
|
1937
|
+
backend: this.backend,
|
|
1938
|
+
rescued_len: text.length,
|
|
1939
|
+
ack_len: norm(pending._consumedByText).length,
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1897
1942
|
const duration = Date.now() - pending.startedAt;
|
|
1898
1943
|
// Review AC4: cost=null + metrics-tokens=null signal "unmeasured-subscription"
|
|
1899
1944
|
// (channels protocol doesn't expose per-turn cost or token breakdowns).
|
|
@@ -1910,9 +1955,11 @@ class CliProcess extends Process {
|
|
|
1910
1955
|
// would resolve the turn silently and the user still sees nothing. So
|
|
1911
1956
|
// only claim already-delivered when reply tool calls actually fired —
|
|
1912
1957
|
// or when claude ACKED consuming this turn in a sibling reply
|
|
1913
|
-
// (consumed_turn_ids; the fold-id-echo case)
|
|
1914
|
-
//
|
|
1915
|
-
|
|
1958
|
+
// (consumed_turn_ids; the fold-id-echo case) AND that sibling actually
|
|
1959
|
+
// delivered this text — re-sending the Stop fallback there would
|
|
1960
|
+
// duplicate. A consumed-ack whose ack did NOT carry the Stop-fallback
|
|
1961
|
+
// answer must still deliver (see consumedCoversFallback above).
|
|
1962
|
+
alreadyDelivered,
|
|
1916
1963
|
sessionId: this.claudeSessionId,
|
|
1917
1964
|
cost: null, // Channels protocol doesn't expose per-turn cost
|
|
1918
1965
|
duration,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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": {
|