switchroom 0.18.21 → 0.18.23
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.
- package/dist/cli/switchroom.js +3 -1
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +227 -59
- package/telegram-plugin/flushed-turn-supersede.ts +230 -0
- package/telegram-plugin/gateway/gateway.ts +69 -0
- package/telegram-plugin/tests/flushed-turn-supersede.test.ts +206 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +71 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +13 -8
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +196 -0
- package/telegram-plugin/turn-flush-safety.ts +74 -1
- package/telegram-plugin/worker-activity-feed.ts +155 -45
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.18.
|
|
2123
|
+
var VERSION = "0.18.23", COMMIT_SHA = "a90cf518";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -23982,6 +23982,8 @@ function isOwnedStaleLink(target, poolDir) {
|
|
|
23982
23982
|
return true;
|
|
23983
23983
|
if (target.startsWith("/opt/skills/"))
|
|
23984
23984
|
return true;
|
|
23985
|
+
if (target.includes("/switchroom-ai/skills/"))
|
|
23986
|
+
return true;
|
|
23985
23987
|
return false;
|
|
23986
23988
|
}
|
|
23987
23989
|
function reconcileAgentDefaultSkills(agentDir, optOuts = {}, defaults = getBuiltinDefaultSkillEntries(), poolDir = getBundledSkillsPoolDir()) {
|
|
@@ -26605,7 +26605,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
|
|
|
26605
26605
|
import { dirname as dirname4, join as join7 } from "node:path";
|
|
26606
26606
|
|
|
26607
26607
|
// src/build-info.ts
|
|
26608
|
-
var VERSION = "0.18.
|
|
26608
|
+
var VERSION = "0.18.23";
|
|
26609
26609
|
|
|
26610
26610
|
// src/cli/resolve-version.ts
|
|
26611
26611
|
function readPackageVersion() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.23",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -39283,6 +39283,92 @@ function normalizeForDedup(text) {
|
|
|
39283
39283
|
return text.replace(/<\/?[a-zA-Z][^>]*>/g, "").replace(/&[a-zA-Z]+;|&#\d+;/g, " ").replace(/(\*\*|__|`)+/g, "").replace(/^[#>\-*+]\s+/gm, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
39284
39284
|
}
|
|
39285
39285
|
|
|
39286
|
+
// flushed-turn-supersede.ts
|
|
39287
|
+
var DEFAULT_SUPERSEDE_TTL_MS = 60000;
|
|
39288
|
+
function decideSupersede(record, args) {
|
|
39289
|
+
const ttlMs = args.ttlMs ?? DEFAULT_SUPERSEDE_TTL_MS;
|
|
39290
|
+
if (record == null)
|
|
39291
|
+
return { supersede: false, deleteMessageIds: [], reason: "no-record" };
|
|
39292
|
+
if (args.now - record.ts > ttlMs) {
|
|
39293
|
+
return { supersede: false, deleteMessageIds: [], reason: "expired" };
|
|
39294
|
+
}
|
|
39295
|
+
const sameTurn = record.turnId != null ? record.turnId === args.liveTurnId : args.liveTurnId == null;
|
|
39296
|
+
if (!sameTurn) {
|
|
39297
|
+
return { supersede: false, deleteMessageIds: [], reason: "different-turn" };
|
|
39298
|
+
}
|
|
39299
|
+
return { supersede: true, deleteMessageIds: [...record.messageIds], reason: "supersede" };
|
|
39300
|
+
}
|
|
39301
|
+
var NULL_TURN_KEY = "<<null-turn>>";
|
|
39302
|
+
function turnKey(turnId) {
|
|
39303
|
+
return turnId == null ? NULL_TURN_KEY : turnId;
|
|
39304
|
+
}
|
|
39305
|
+
|
|
39306
|
+
class FlushedTurnSupersedeRegistry {
|
|
39307
|
+
entries = new Map;
|
|
39308
|
+
ttlMs;
|
|
39309
|
+
constructor(opts = {}) {
|
|
39310
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_SUPERSEDE_TTL_MS;
|
|
39311
|
+
}
|
|
39312
|
+
record(chatId, threadId, rec, now) {
|
|
39313
|
+
if (rec.messageIds.length === 0)
|
|
39314
|
+
return;
|
|
39315
|
+
this.sweep(now);
|
|
39316
|
+
const lane = makeKey2(chatId, threadId);
|
|
39317
|
+
let laneMap = this.entries.get(lane);
|
|
39318
|
+
if (laneMap == null) {
|
|
39319
|
+
laneMap = new Map;
|
|
39320
|
+
this.entries.set(lane, laneMap);
|
|
39321
|
+
}
|
|
39322
|
+
laneMap.set(turnKey(rec.turnId), {
|
|
39323
|
+
turnId: rec.turnId,
|
|
39324
|
+
messageIds: [...rec.messageIds],
|
|
39325
|
+
text: rec.text,
|
|
39326
|
+
ts: now
|
|
39327
|
+
});
|
|
39328
|
+
}
|
|
39329
|
+
peek(chatId, threadId, args) {
|
|
39330
|
+
const rec = this.entries.get(makeKey2(chatId, threadId))?.get(turnKey(args.liveTurnId));
|
|
39331
|
+
return decideSupersede(rec, { liveTurnId: args.liveTurnId, now: args.now, ttlMs: this.ttlMs });
|
|
39332
|
+
}
|
|
39333
|
+
take(chatId, threadId, args) {
|
|
39334
|
+
const lane = makeKey2(chatId, threadId);
|
|
39335
|
+
const decision = this.peek(chatId, threadId, args);
|
|
39336
|
+
if (decision.supersede) {
|
|
39337
|
+
const laneMap = this.entries.get(lane);
|
|
39338
|
+
laneMap?.delete(turnKey(args.liveTurnId));
|
|
39339
|
+
if (laneMap != null && laneMap.size === 0)
|
|
39340
|
+
this.entries.delete(lane);
|
|
39341
|
+
}
|
|
39342
|
+
return decision;
|
|
39343
|
+
}
|
|
39344
|
+
forget(chatId, threadId) {
|
|
39345
|
+
this.entries.delete(makeKey2(chatId, threadId));
|
|
39346
|
+
}
|
|
39347
|
+
clear() {
|
|
39348
|
+
this.entries.clear();
|
|
39349
|
+
}
|
|
39350
|
+
sweep(now) {
|
|
39351
|
+
for (const [lane, laneMap] of this.entries) {
|
|
39352
|
+
for (const [tk, rec] of laneMap) {
|
|
39353
|
+
if (now - rec.ts > this.ttlMs)
|
|
39354
|
+
laneMap.delete(tk);
|
|
39355
|
+
}
|
|
39356
|
+
if (laneMap.size === 0)
|
|
39357
|
+
this.entries.delete(lane);
|
|
39358
|
+
}
|
|
39359
|
+
}
|
|
39360
|
+
size(now) {
|
|
39361
|
+
this.sweep(now);
|
|
39362
|
+
let total = 0;
|
|
39363
|
+
for (const laneMap of this.entries.values())
|
|
39364
|
+
total += laneMap.size;
|
|
39365
|
+
return total;
|
|
39366
|
+
}
|
|
39367
|
+
}
|
|
39368
|
+
function makeKey2(chatId, threadId) {
|
|
39369
|
+
return threadId == null ? chatId : `${chatId}|${threadId}`;
|
|
39370
|
+
}
|
|
39371
|
+
|
|
39286
39372
|
// gateway/inbound-coalesce.ts
|
|
39287
39373
|
function createInboundCoalescer(opts) {
|
|
39288
39374
|
const buffer = new Map;
|
|
@@ -40593,44 +40679,63 @@ function createWorkerActivityFeed(opts) {
|
|
|
40593
40679
|
function removeWorker(g, agentId) {
|
|
40594
40680
|
g.workers.delete(agentId);
|
|
40595
40681
|
agentIndex.delete(agentId);
|
|
40596
|
-
|
|
40682
|
+
maybeDeleteGroup(g);
|
|
40683
|
+
}
|
|
40684
|
+
function maybeDeleteGroup(g) {
|
|
40685
|
+
if (g.workers.size === 0 && g.pendingFinalize.size === 0)
|
|
40597
40686
|
groups.delete(g.feedKey);
|
|
40598
40687
|
}
|
|
40688
|
+
function hasLiveWorker(g) {
|
|
40689
|
+
for (const w of g.workers.values())
|
|
40690
|
+
if (!w.finished)
|
|
40691
|
+
return true;
|
|
40692
|
+
return false;
|
|
40693
|
+
}
|
|
40599
40694
|
function syncPin(g) {
|
|
40600
|
-
const messageId = g.messageId != null && g
|
|
40695
|
+
const messageId = g.messageId != null && hasLiveWorker(g) ? g.messageId : null;
|
|
40601
40696
|
reconcilePinFn({ feedKey: g.feedKey, chatId: g.chatId, threadId: g.threadId, messageId });
|
|
40602
40697
|
}
|
|
40603
40698
|
async function doRender(g, opts2 = {}) {
|
|
40604
40699
|
const now = nowFn();
|
|
40605
40700
|
const isTerminal = opts2.terminalRecap != null;
|
|
40606
|
-
const
|
|
40607
|
-
|
|
40608
|
-
if (opts2.
|
|
40609
|
-
|
|
40701
|
+
const finishingAgentId = opts2.finishingAgentId;
|
|
40702
|
+
const stageRecap = () => {
|
|
40703
|
+
if (isTerminal && finishingAgentId != null && opts2.terminalRecap != null) {
|
|
40704
|
+
g.pendingFinalize.set(finishingAgentId, opts2.terminalRecap);
|
|
40705
|
+
}
|
|
40706
|
+
};
|
|
40707
|
+
const clearStaged = () => {
|
|
40708
|
+
if (finishingAgentId != null)
|
|
40709
|
+
g.pendingFinalize.delete(finishingAgentId);
|
|
40710
|
+
maybeDeleteGroup(g);
|
|
40610
40711
|
syncPin(g);
|
|
40611
40712
|
};
|
|
40713
|
+
if (isTerminal) {
|
|
40714
|
+
stageRecap();
|
|
40715
|
+
if (finishingAgentId != null && g.workers.has(finishingAgentId)) {
|
|
40716
|
+
removeWorker(g, finishingAgentId);
|
|
40717
|
+
}
|
|
40718
|
+
g.terminalPainted = !hasLiveWorker(g);
|
|
40719
|
+
syncPin(g);
|
|
40720
|
+
}
|
|
40612
40721
|
if (now < g.cooldownUntil) {
|
|
40613
|
-
if (isTerminal && opts2.terminalRecap != null)
|
|
40614
|
-
g.pendingFinalize = opts2.terminalRecap;
|
|
40615
40722
|
return;
|
|
40616
40723
|
}
|
|
40617
40724
|
if (parkIfFloodWindowOpen(g)) {
|
|
40618
|
-
if (isTerminal && opts2.terminalRecap != null)
|
|
40619
|
-
g.pendingFinalize = opts2.terminalRecap;
|
|
40620
40725
|
return;
|
|
40621
40726
|
}
|
|
40622
40727
|
const body = renderGroupBody(g, now, opts2.terminalRecap ?? null, opts2.heartbeat ?? false);
|
|
40623
40728
|
if (body == null) {
|
|
40624
40729
|
if (isTerminal)
|
|
40625
|
-
|
|
40730
|
+
clearStaged();
|
|
40626
40731
|
return;
|
|
40627
40732
|
}
|
|
40628
40733
|
if (g.messageId == null) {
|
|
40629
|
-
const maxElapsed = Math.max(0, ...runningRows(g).map((r) => liveElapsed(r, now)));
|
|
40630
40734
|
if (isTerminal) {
|
|
40631
|
-
|
|
40735
|
+
clearStaged();
|
|
40632
40736
|
return;
|
|
40633
40737
|
}
|
|
40738
|
+
const maxElapsed = Math.max(0, ...runningRows(g).map((r) => liveElapsed(r, now)));
|
|
40634
40739
|
if (maxElapsed < firstPaintMin)
|
|
40635
40740
|
return;
|
|
40636
40741
|
try {
|
|
@@ -40643,6 +40748,7 @@ function createWorkerActivityFeed(opts) {
|
|
|
40643
40748
|
g.messageId = sent.message_id;
|
|
40644
40749
|
g.lastBody = body;
|
|
40645
40750
|
g.lastEditAt = now;
|
|
40751
|
+
g.terminalPainted = false;
|
|
40646
40752
|
syncPin(g);
|
|
40647
40753
|
log(`worker-feed: paint feed=${g.feedKey} chat=${g.chatId} ` + `thread=${g.threadId ?? "-"} msgId=${g.messageId} workers=${g.workers.size} bytes=${body.length}`);
|
|
40648
40754
|
} catch (err) {
|
|
@@ -40653,7 +40759,7 @@ function createWorkerActivityFeed(opts) {
|
|
|
40653
40759
|
}
|
|
40654
40760
|
if (body === g.lastBody) {
|
|
40655
40761
|
if (isTerminal)
|
|
40656
|
-
|
|
40762
|
+
clearStaged();
|
|
40657
40763
|
return;
|
|
40658
40764
|
}
|
|
40659
40765
|
if (!opts2.force && now - g.lastEditAt < minEditInterval)
|
|
@@ -40662,45 +40768,39 @@ function createWorkerActivityFeed(opts) {
|
|
|
40662
40768
|
const res = await opts.bot.editMessageText(g.chatId, g.messageId, body, sendOptsFor(g));
|
|
40663
40769
|
if (isSendGateShed(res)) {
|
|
40664
40770
|
parkIfFloodWindowOpen(g);
|
|
40665
|
-
if (isTerminal && opts2.terminalRecap != null)
|
|
40666
|
-
g.pendingFinalize = opts2.terminalRecap;
|
|
40667
40771
|
return;
|
|
40668
40772
|
}
|
|
40669
40773
|
g.lastBody = body;
|
|
40670
40774
|
g.lastEditAt = now;
|
|
40671
40775
|
if (isTerminal) {
|
|
40672
|
-
log(`worker-feed: finish feed=${g.feedKey} chat=${g.chatId} thread=${g.threadId ?? "-"} ` + `msgId=${g.messageId} agent=${
|
|
40776
|
+
log(`worker-feed: finish feed=${g.feedKey} chat=${g.chatId} thread=${g.threadId ?? "-"} ` + `msgId=${g.messageId} agent=${finishingAgentId ?? "-"} ` + `state=${opts2.terminalRecap?.state ?? "done"} bytes=${body.length}`);
|
|
40673
40777
|
} else {
|
|
40674
40778
|
log(`worker-feed: edit feed=${g.feedKey} chat=${g.chatId} ` + `thread=${g.threadId ?? "-"} msgId=${g.messageId} workers=${g.workers.size} bytes=${body.length}`);
|
|
40675
40779
|
}
|
|
40676
40780
|
if (isTerminal)
|
|
40677
|
-
|
|
40781
|
+
clearStaged();
|
|
40678
40782
|
} catch (err) {
|
|
40679
40783
|
const outcome = classifyEditError(err);
|
|
40680
40784
|
if (outcome === "rate_limited") {
|
|
40681
40785
|
noteRateLimited(g, err, isTerminal ? "finish" : "edit");
|
|
40682
|
-
if (isTerminal && opts2.terminalRecap != null)
|
|
40683
|
-
g.pendingFinalize = opts2.terminalRecap;
|
|
40684
40786
|
return;
|
|
40685
40787
|
}
|
|
40686
40788
|
if (outcome === "not_modified") {
|
|
40687
40789
|
g.lastBody = body;
|
|
40688
40790
|
g.lastEditAt = now;
|
|
40689
40791
|
if (isTerminal)
|
|
40690
|
-
|
|
40792
|
+
clearStaged();
|
|
40691
40793
|
return;
|
|
40692
40794
|
}
|
|
40693
40795
|
if (outcome === "gone") {
|
|
40694
40796
|
g.messageId = null;
|
|
40695
40797
|
g.lastBody = null;
|
|
40696
40798
|
if (isTerminal)
|
|
40697
|
-
|
|
40799
|
+
clearStaged();
|
|
40698
40800
|
else
|
|
40699
40801
|
syncPin(g);
|
|
40700
40802
|
return;
|
|
40701
40803
|
}
|
|
40702
|
-
if (isTerminal && opts2.terminalRecap != null)
|
|
40703
|
-
g.pendingFinalize = opts2.terminalRecap;
|
|
40704
40804
|
log(`worker-feed: edit transient error feed=${g.feedKey}: ${err.message}`);
|
|
40705
40805
|
}
|
|
40706
40806
|
}
|
|
@@ -40747,24 +40847,34 @@ function createWorkerActivityFeed(opts) {
|
|
|
40747
40847
|
function heartbeatTick() {
|
|
40748
40848
|
const now = nowFn();
|
|
40749
40849
|
const staleAgentIds = [];
|
|
40850
|
+
const staleFinished = [];
|
|
40750
40851
|
for (const g of groups.values()) {
|
|
40751
40852
|
for (const row of g.workers.values()) {
|
|
40752
|
-
if (
|
|
40753
|
-
|
|
40853
|
+
if (now - row.lastUpdateAt >= staleWorkerTtlMs) {
|
|
40854
|
+
if (row.finished)
|
|
40855
|
+
staleFinished.push({ g, agentId: row.agentId });
|
|
40856
|
+
else
|
|
40857
|
+
staleAgentIds.push(row.agentId);
|
|
40754
40858
|
}
|
|
40755
40859
|
}
|
|
40756
40860
|
}
|
|
40861
|
+
for (const { g, agentId } of staleFinished) {
|
|
40862
|
+
log(`worker-feed: TTL GC finished row agent=${agentId} feed=${g.feedKey} \u2014 reaping leaked finished row`);
|
|
40863
|
+
g.pendingFinalize.delete(agentId);
|
|
40864
|
+
removeWorker(g, agentId);
|
|
40865
|
+
syncPin(g);
|
|
40866
|
+
}
|
|
40757
40867
|
for (const agentId of staleAgentIds) {
|
|
40758
40868
|
log(`worker-feed: TTL reap agent=${agentId} \u2014 no update in ${Math.floor((now - (groupOfAgent(agentId)?.workers.get(agentId)?.lastUpdateAt ?? now)) / 1000)}s (>= ${Math.floor(staleWorkerTtlMs / 1000)}s); force-terminating leaked row`);
|
|
40759
40869
|
terminateWorker(agentId);
|
|
40760
40870
|
}
|
|
40761
40871
|
for (const g of [...groups.values()]) {
|
|
40762
|
-
if (g.pendingFinalize
|
|
40763
|
-
const recap
|
|
40764
|
-
|
|
40765
|
-
|
|
40766
|
-
|
|
40767
|
-
}
|
|
40872
|
+
if (g.pendingFinalize.size > 0 && now >= g.cooldownUntil) {
|
|
40873
|
+
for (const [agentId, recap] of [...g.pendingFinalize]) {
|
|
40874
|
+
g.chain = g.chain.then(() => doRender(g, { force: true, terminalRecap: recap, finishingAgentId: agentId })).catch((err) => {
|
|
40875
|
+
log(`worker-feed: heartbeat finalize re-drive error feed=${g.feedKey}: ${err.message}`);
|
|
40876
|
+
});
|
|
40877
|
+
}
|
|
40768
40878
|
continue;
|
|
40769
40879
|
}
|
|
40770
40880
|
if (now < g.cooldownUntil)
|
|
@@ -40802,7 +40912,7 @@ function createWorkerActivityFeed(opts) {
|
|
|
40802
40912
|
},
|
|
40803
40913
|
hasRunningInFeed(feedKey) {
|
|
40804
40914
|
const g = groups.get(feedKey);
|
|
40805
|
-
return g != null && g
|
|
40915
|
+
return g != null && hasLiveWorker(g);
|
|
40806
40916
|
},
|
|
40807
40917
|
get size() {
|
|
40808
40918
|
let n = 0;
|
|
@@ -40831,10 +40941,18 @@ function createWorkerActivityFeed(opts) {
|
|
|
40831
40941
|
cooldownUntil: 0,
|
|
40832
40942
|
chain: Promise.resolve(),
|
|
40833
40943
|
workers: new Map,
|
|
40834
|
-
pendingFinalize:
|
|
40944
|
+
pendingFinalize: new Map,
|
|
40945
|
+
terminalPainted: false
|
|
40835
40946
|
};
|
|
40836
40947
|
groups.set(feedKey, g);
|
|
40837
40948
|
}
|
|
40949
|
+
if (!g.workers.has(agentId) && g.terminalPainted && !hasLiveWorker(g)) {
|
|
40950
|
+
g.messageId = null;
|
|
40951
|
+
g.lastBody = null;
|
|
40952
|
+
g.pendingFinalize.clear();
|
|
40953
|
+
g.terminalPainted = false;
|
|
40954
|
+
syncPin(g);
|
|
40955
|
+
}
|
|
40838
40956
|
let row = g.workers.get(agentId);
|
|
40839
40957
|
if (row == null) {
|
|
40840
40958
|
row = {
|
|
@@ -62236,16 +62354,16 @@ function writeSilentEndState(args, deps) {
|
|
|
62236
62354
|
`);
|
|
62237
62355
|
}
|
|
62238
62356
|
}
|
|
62239
|
-
function clearSilentEndState(
|
|
62357
|
+
function clearSilentEndState(turnKey2, deps) {
|
|
62240
62358
|
const statePath = resolveStatePath2(deps);
|
|
62241
62359
|
if (!existsSync14(statePath))
|
|
62242
62360
|
return;
|
|
62243
62361
|
try {
|
|
62244
62362
|
const prev = JSON.parse(readFileSync16(statePath, "utf8"));
|
|
62245
|
-
if (prev.turnKey != null && prev.turnKey !==
|
|
62363
|
+
if (prev.turnKey != null && prev.turnKey !== turnKey2)
|
|
62246
62364
|
return;
|
|
62247
62365
|
unlinkSync9(statePath);
|
|
62248
|
-
emitLog(deps, `silent-end: cleared state file turnKey=${
|
|
62366
|
+
emitLog(deps, `silent-end: cleared state file turnKey=${turnKey2}
|
|
62249
62367
|
`);
|
|
62250
62368
|
} catch {}
|
|
62251
62369
|
}
|
|
@@ -62407,6 +62525,23 @@ function endsWithSilentMarker(text4) {
|
|
|
62407
62525
|
return false;
|
|
62408
62526
|
return isSilentFlushMarker(lines[lines.length - 1]);
|
|
62409
62527
|
}
|
|
62528
|
+
function selectFlushDeliveryText(blocks) {
|
|
62529
|
+
const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
|
|
62530
|
+
if (candidates.length === 0)
|
|
62531
|
+
return "";
|
|
62532
|
+
if (candidates.length === 1)
|
|
62533
|
+
return candidates[0];
|
|
62534
|
+
const answer = candidates[candidates.length - 1];
|
|
62535
|
+
const preceding = candidates.slice(0, -1);
|
|
62536
|
+
const allNarration = preceding.every(isNarrationBlock);
|
|
62537
|
+
return allNarration ? answer : candidates.join(`
|
|
62538
|
+
|
|
62539
|
+
`);
|
|
62540
|
+
}
|
|
62541
|
+
var NARRATION_OPENER = /^(let me\b|lemme\b|i'?ll\b|i will\b|i am going to\b|i'?m going to\b|i'?m about to\b|going to\b|first,?\s+(?:let me|i'?ll|i will)\b|now,?\s+(?:let me|i'?ll|i will)\b|next,?\s+(?:let me|i'?ll|i will)\b|let'?s\b)/i;
|
|
62542
|
+
function isNarrationBlock(block) {
|
|
62543
|
+
return NARRATION_OPENER.test(block.trimStart());
|
|
62544
|
+
}
|
|
62410
62545
|
function decideTurnFlush(input) {
|
|
62411
62546
|
const flushEnabled = input.flushEnabled !== false;
|
|
62412
62547
|
if (!flushEnabled)
|
|
@@ -62426,7 +62561,7 @@ function decideTurnFlush(input) {
|
|
|
62426
62561
|
return { kind: "skip", reason: "silent-marker" };
|
|
62427
62562
|
if (endsWithSilentMarker(joined))
|
|
62428
62563
|
return { kind: "skip", reason: "silent-marker" };
|
|
62429
|
-
return { kind: "flush", text:
|
|
62564
|
+
return { kind: "flush", text: selectFlushDeliveryText(input.capturedText) };
|
|
62430
62565
|
}
|
|
62431
62566
|
|
|
62432
62567
|
// answer-stream.ts
|
|
@@ -68030,6 +68165,23 @@ function endsWithSilentMarker2(text4) {
|
|
|
68030
68165
|
return false;
|
|
68031
68166
|
return isSilentFlushMarker2(lines[lines.length - 1]);
|
|
68032
68167
|
}
|
|
68168
|
+
function selectFlushDeliveryText2(blocks) {
|
|
68169
|
+
const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
|
|
68170
|
+
if (candidates.length === 0)
|
|
68171
|
+
return "";
|
|
68172
|
+
if (candidates.length === 1)
|
|
68173
|
+
return candidates[0];
|
|
68174
|
+
const answer = candidates[candidates.length - 1];
|
|
68175
|
+
const preceding = candidates.slice(0, -1);
|
|
68176
|
+
const allNarration = preceding.every(isNarrationBlock2);
|
|
68177
|
+
return allNarration ? answer : candidates.join(`
|
|
68178
|
+
|
|
68179
|
+
`);
|
|
68180
|
+
}
|
|
68181
|
+
var NARRATION_OPENER2 = /^(let me\b|lemme\b|i'?ll\b|i will\b|i am going to\b|i'?m going to\b|i'?m about to\b|going to\b|first,?\s+(?:let me|i'?ll|i will)\b|now,?\s+(?:let me|i'?ll|i will)\b|next,?\s+(?:let me|i'?ll|i will)\b|let'?s\b)/i;
|
|
68182
|
+
function isNarrationBlock2(block) {
|
|
68183
|
+
return NARRATION_OPENER2.test(block.trimStart());
|
|
68184
|
+
}
|
|
68033
68185
|
function decideTurnFlush2(input) {
|
|
68034
68186
|
const flushEnabled = input.flushEnabled !== false;
|
|
68035
68187
|
if (!flushEnabled)
|
|
@@ -68049,7 +68201,7 @@ function decideTurnFlush2(input) {
|
|
|
68049
68201
|
return { kind: "skip", reason: "silent-marker" };
|
|
68050
68202
|
if (endsWithSilentMarker2(joined))
|
|
68051
68203
|
return { kind: "skip", reason: "silent-marker" };
|
|
68052
|
-
return { kind: "flush", text:
|
|
68204
|
+
return { kind: "flush", text: selectFlushDeliveryText2(input.capturedText) };
|
|
68053
68205
|
}
|
|
68054
68206
|
function isTurnFlushSafetyEnabled(env = process.env) {
|
|
68055
68207
|
const raw = env.SWITCHROOM_TG_TURN_FLUSH_SAFETY;
|
|
@@ -75253,11 +75405,11 @@ function writeActivityCardRecord(path2, fs2, record, log = (l) => process.stderr
|
|
|
75253
75405
|
const others = current.filter((c) => c.turnKey !== record.turnKey);
|
|
75254
75406
|
persistActivityCards(path2, fs2, [...others, record], log);
|
|
75255
75407
|
}
|
|
75256
|
-
function clearActivityCardRecord(path2, fs2,
|
|
75408
|
+
function clearActivityCardRecord(path2, fs2, turnKey2, activityMessageId, log = (l) => process.stderr.write(l)) {
|
|
75257
75409
|
const current = loadActivityCards(path2, fs2);
|
|
75258
75410
|
if (current.length === 0)
|
|
75259
75411
|
return;
|
|
75260
|
-
const next = current.filter((c) => c.turnKey !==
|
|
75412
|
+
const next = current.filter((c) => c.turnKey !== turnKey2 || activityMessageId !== undefined && c.activityMessageId !== activityMessageId);
|
|
75261
75413
|
if (next.length === current.length)
|
|
75262
75414
|
return;
|
|
75263
75415
|
persistActivityCards(path2, fs2, next, log);
|
|
@@ -82758,10 +82910,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
82758
82910
|
}
|
|
82759
82911
|
|
|
82760
82912
|
// ../src/build-info.ts
|
|
82761
|
-
var VERSION = "0.18.
|
|
82762
|
-
var COMMIT_SHA = "
|
|
82763
|
-
var COMMIT_DATE = "2026-07-
|
|
82764
|
-
var LATEST_PR =
|
|
82913
|
+
var VERSION = "0.18.23";
|
|
82914
|
+
var COMMIT_SHA = "a90cf518";
|
|
82915
|
+
var COMMIT_DATE = "2026-07-14T09:32:46+10:00";
|
|
82916
|
+
var LATEST_PR = 3238;
|
|
82765
82917
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
82766
82918
|
|
|
82767
82919
|
// gateway/boot-version.ts
|
|
@@ -83853,8 +84005,8 @@ function recordTurnEnd(db2, args) {
|
|
|
83853
84005
|
WHERE turn_key = ?
|
|
83854
84006
|
`).run(now, args.endedVia, args.lastAssistantMsgId ?? null, args.lastAssistantDone !== undefined ? args.lastAssistantDone ? 1 : 0 : null, args.assistantReplyPreview ?? null, args.toolCallCount !== undefined ? args.toolCallCount : null, now, args.turnKey);
|
|
83855
84007
|
}
|
|
83856
|
-
function getTurnByKey(db2,
|
|
83857
|
-
const row = db2.prepare(`SELECT * FROM turns WHERE turn_key = ?`).get(
|
|
84008
|
+
function getTurnByKey(db2, turnKey2) {
|
|
84009
|
+
const row = db2.prepare(`SELECT * FROM turns WHERE turn_key = ?`).get(turnKey2);
|
|
83858
84010
|
return row ? mapRow(row) : null;
|
|
83859
84011
|
}
|
|
83860
84012
|
function markOrphanedWithTimeoutClassification(db2, opts) {
|
|
@@ -83912,13 +84064,13 @@ var INTERRUPTED_VIA = new Set([
|
|
|
83912
84064
|
"timeout",
|
|
83913
84065
|
"unknown"
|
|
83914
84066
|
]);
|
|
83915
|
-
function markTurnResumed(db2,
|
|
84067
|
+
function markTurnResumed(db2, turnKey2, now = Date.now()) {
|
|
83916
84068
|
db2.prepare(`
|
|
83917
84069
|
UPDATE turns
|
|
83918
84070
|
SET resumed_at = ?,
|
|
83919
84071
|
updated_at = ?
|
|
83920
84072
|
WHERE turn_key = ? AND resumed_at IS NULL
|
|
83921
|
-
`).run(now, now,
|
|
84073
|
+
`).run(now, now, turnKey2);
|
|
83922
84074
|
}
|
|
83923
84075
|
function findLatestTurnIfInterrupted(db2) {
|
|
83924
84076
|
const row = db2.prepare(`
|
|
@@ -85275,6 +85427,7 @@ var deferredDoneReactions = new DeferredDoneReactions({
|
|
|
85275
85427
|
purge: (key) => purgeReactionTracking(key)
|
|
85276
85428
|
});
|
|
85277
85429
|
var outboundDedup = new OutboundDedupCache;
|
|
85430
|
+
var flushedTurnSupersede = new FlushedTurnSupersedeRegistry;
|
|
85278
85431
|
var chatAvailableReactions = new Map;
|
|
85279
85432
|
var chatProbesInFlight = new Set;
|
|
85280
85433
|
var activeTurnStartedAt = new Map;
|
|
@@ -86956,10 +87109,10 @@ function summariseMarkerPayload(payload) {
|
|
|
86956
87109
|
return "payload=missing";
|
|
86957
87110
|
try {
|
|
86958
87111
|
const parsed = JSON.parse(payload);
|
|
86959
|
-
const
|
|
87112
|
+
const turnKey2 = typeof parsed.turnKey === "string" ? parsed.turnKey : "unknown";
|
|
86960
87113
|
const chatId = typeof parsed.chatId === "string" ? parsed.chatId : "unknown";
|
|
86961
87114
|
const startedAt = typeof parsed.startedAt === "number" ? parsed.startedAt : 0;
|
|
86962
|
-
return `turnKey=${
|
|
87115
|
+
return `turnKey=${turnKey2} chat=${chatId} started=${new Date(startedAt).toISOString()}`;
|
|
86963
87116
|
} catch {
|
|
86964
87117
|
return "payload=unparseable";
|
|
86965
87118
|
}
|
|
@@ -89889,6 +90042,18 @@ async function executeReply(args) {
|
|
|
89889
90042
|
return { content: [{ type: "text", text: "sent (deduped \u2014 same content sent via earlier path)" }] };
|
|
89890
90043
|
}
|
|
89891
90044
|
}
|
|
90045
|
+
{
|
|
90046
|
+
const replyThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
|
|
90047
|
+
const resolvedTurnId = turn?.turnId ?? findTurnByOriginId(args.origin_turn_id)?.turnId ?? null;
|
|
90048
|
+
const decision = flushedTurnSupersede.take(chat_id, replyThreadId, { liveTurnId: resolvedTurnId, now: Date.now() });
|
|
90049
|
+
if (decision.supersede) {
|
|
90050
|
+
process.stderr.write(`telegram gateway: reply: superseding flushed turn message(s) chatId=${chat_id} ids=${JSON.stringify(decision.deleteMessageIds)}
|
|
90051
|
+
`);
|
|
90052
|
+
for (const id of decision.deleteMessageIds) {
|
|
90053
|
+
await swallowingApiCall(() => lockedBot.api.deleteMessage(chat_id, id), { chat_id, verb: "reply.supersedeFlushed" });
|
|
90054
|
+
}
|
|
90055
|
+
}
|
|
90056
|
+
}
|
|
89892
90057
|
const files = args.files ?? [];
|
|
89893
90058
|
const quoteOptIn = args.quote !== false;
|
|
89894
90059
|
let reply_to = args.reply_to != null ? Number(args.reply_to) : undefined;
|
|
@@ -91501,8 +91666,8 @@ function resetOrphanedReplyTimeout() {
|
|
|
91501
91666
|
replyCalled: t.replyCalled,
|
|
91502
91667
|
progressCardActive: false
|
|
91503
91668
|
})) {
|
|
91504
|
-
const
|
|
91505
|
-
const working = isLegitimatelyWorking(
|
|
91669
|
+
const turnKey2 = statusKey(t.sessionChatId, t.sessionThreadId);
|
|
91670
|
+
const working = isLegitimatelyWorking(turnKey2);
|
|
91506
91671
|
const humanWaiting = (() => {
|
|
91507
91672
|
for (const entry of pendingAskUser.values()) {
|
|
91508
91673
|
if (entry.chatId === t.sessionChatId)
|
|
@@ -91520,7 +91685,7 @@ function resetOrphanedReplyTimeout() {
|
|
|
91520
91685
|
maxRearms: ORPHANED_REPLY_MAX_REARMS
|
|
91521
91686
|
});
|
|
91522
91687
|
if (decision.rearm) {
|
|
91523
|
-
process.stderr.write(`telegram gateway: orphaned-reply fuse expired \u2014 re-arming` + ` (rearm ${t.liveness.orphanedReplyRearmCount}/${ORPHANED_REPLY_MAX_REARMS}, in_flight=${toolFlightTracker.inFlightCount()}, human_wait=${humanWaiting}, recently_streaming=${recentlyStreaming}, bg_work=${hasPendingAsyncDispatch(
|
|
91688
|
+
process.stderr.write(`telegram gateway: orphaned-reply fuse expired \u2014 re-arming` + ` (rearm ${t.liveness.orphanedReplyRearmCount}/${ORPHANED_REPLY_MAX_REARMS}, in_flight=${toolFlightTracker.inFlightCount()}, human_wait=${humanWaiting}, recently_streaming=${recentlyStreaming}, bg_work=${hasPendingAsyncDispatch(turnKey2)})
|
|
91524
91689
|
`);
|
|
91525
91690
|
resetOrphanedReplyTimeout();
|
|
91526
91691
|
return;
|
|
@@ -92084,23 +92249,23 @@ function handleSessionEvent(ev) {
|
|
|
92084
92249
|
clearSilentEndState(statusKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null));
|
|
92085
92250
|
if (turnsDb != null) {
|
|
92086
92251
|
const evThreadIdNum = ev.threadId != null ? Number(ev.threadId) : null;
|
|
92087
|
-
const
|
|
92088
|
-
next.registryKey =
|
|
92252
|
+
const turnKey2 = chatKeyWithSuffix2(ev.chatId, evThreadIdNum, String(startedAt));
|
|
92253
|
+
next.registryKey = turnKey2;
|
|
92089
92254
|
const userPromptPreview = extractUserPromptPreview(ev.rawContent);
|
|
92090
92255
|
try {
|
|
92091
92256
|
recordTurnStart(turnsDb, {
|
|
92092
|
-
turnKey,
|
|
92257
|
+
turnKey: turnKey2,
|
|
92093
92258
|
chatId: String(ev.chatId),
|
|
92094
92259
|
threadId: ev.threadId != null ? String(ev.threadId) : null,
|
|
92095
92260
|
lastUserMsgId: ev.messageId != null ? String(ev.messageId) : null,
|
|
92096
92261
|
userPromptPreview
|
|
92097
92262
|
});
|
|
92098
92263
|
} catch (err) {
|
|
92099
|
-
process.stderr.write(`telegram gateway: recordTurnStart failed turnKey=${
|
|
92264
|
+
process.stderr.write(`telegram gateway: recordTurnStart failed turnKey=${turnKey2}: ${err.message}
|
|
92100
92265
|
`);
|
|
92101
92266
|
}
|
|
92102
92267
|
writeTurnActiveMarker(STATE_DIR, {
|
|
92103
|
-
turnKey,
|
|
92268
|
+
turnKey: turnKey2,
|
|
92104
92269
|
chatId: String(ev.chatId),
|
|
92105
92270
|
threadId: ev.threadId != null ? String(ev.threadId) : null,
|
|
92106
92271
|
startedAt
|
|
@@ -92606,6 +92771,9 @@ function handleSessionEvent(ev) {
|
|
|
92606
92771
|
} catch {}
|
|
92607
92772
|
}
|
|
92608
92773
|
outboundDedup.record(backstopChatId, backstopThreadId, capturedText, Date.now(), currentTurn?.registryKey ?? null);
|
|
92774
|
+
if (sentIds.length > 0) {
|
|
92775
|
+
flushedTurnSupersede.record(backstopChatId, backstopThreadId, { turnId: turn.turnId, messageIds: sentIds, text: capturedText }, Date.now());
|
|
92776
|
+
}
|
|
92609
92777
|
if (backstopCtrl)
|
|
92610
92778
|
backstopCtrl.finalize("done");
|
|
92611
92779
|
if (backstopCardTurnKey != null) {
|