switchroom 0.14.65 → 0.14.66
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 -3
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +38 -17
- package/telegram-plugin/gateway/answer-thread-resolve.test.ts +85 -0
- package/telegram-plugin/gateway/answer-thread-resolve.ts +30 -4
- package/telegram-plugin/gateway/gateway.ts +91 -12
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +4 -2
package/dist/cli/switchroom.js
CHANGED
|
@@ -49452,8 +49452,8 @@ var {
|
|
|
49452
49452
|
} = import__.default;
|
|
49453
49453
|
|
|
49454
49454
|
// src/build-info.ts
|
|
49455
|
-
var VERSION = "0.14.
|
|
49456
|
-
var COMMIT_SHA = "
|
|
49455
|
+
var VERSION = "0.14.66";
|
|
49456
|
+
var COMMIT_SHA = "0f4f029d";
|
|
49457
49457
|
|
|
49458
49458
|
// src/cli/agent.ts
|
|
49459
49459
|
init_source();
|
|
@@ -52027,7 +52027,7 @@ function buildSettingsHooksBlock(p) {
|
|
|
52027
52027
|
|
|
52028
52028
|
` + 'Do NOT send a trailing confirmation after your answer \u2014 no "Done.", ' + '"Sent.", "Hope that helps." as a separate message once you have ' + "already replied. Your answer is the last thing the user should " + `see; a follow-up "Done." is dead-air clutter (and the user's ` + `device already pinged on the answer). Stop after the answer.
|
|
52029
52029
|
|
|
52030
|
-
` + 'CRITICAL: "answer" means a call to the reply tool ' + "(mcp__switchroom-telegram__reply, or stream_reply with done=true). " + "Your terminal/transcript text is NEVER delivered to Telegram \u2014 the " + "user sees only what you send through the reply tool. After a long " + "tool sequence (scheduling, multi-step research, sub-agent handback), " + "do not let your closing narration stand as the answer: end the turn " + "by passing that narration to the reply tool. No reply tool call = the " + "user got nothing, however much text you wrote.</turn-pacing>";
|
|
52030
|
+
` + 'CRITICAL: "answer" means a call to the reply tool ' + "(mcp__switchroom-telegram__reply, or stream_reply with done=true). " + "Your terminal/transcript text is NEVER delivered to Telegram \u2014 the " + "user sees only what you send through the reply tool. After a long " + "tool sequence (scheduling, multi-step research, sub-agent handback), " + "do not let your closing narration stand as the answer: end the turn " + "by passing that narration to the reply tool. No reply tool call = the " + "user got nothing, however much text you wrote. Call the reply tool as " + "your FIRST action when you have the answer \u2014 do not write it out as " + "transcript text first and call reply afterward: a framework backstop " + "flushes unsent text after a delay and then your real reply lands late " + "and out of order.</turn-pacing>";
|
|
52031
52031
|
const switchroomUserPromptSubmit = [
|
|
52032
52032
|
...useHotReloadStable ? [
|
|
52033
52033
|
{
|
package/package.json
CHANGED
|
@@ -47936,6 +47936,10 @@ function resolveAnswerThreadId(input) {
|
|
|
47936
47936
|
return input.explicitThreadId;
|
|
47937
47937
|
if (input.originResolved)
|
|
47938
47938
|
return input.originThreadId;
|
|
47939
|
+
if (input.liveThreadId != null)
|
|
47940
|
+
return input.liveThreadId;
|
|
47941
|
+
if (input.lastEndedResolvedForChat)
|
|
47942
|
+
return input.lastEndedThreadIdForChat;
|
|
47939
47943
|
return input.liveThreadId;
|
|
47940
47944
|
}
|
|
47941
47945
|
|
|
@@ -52759,11 +52763,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
52759
52763
|
}
|
|
52760
52764
|
|
|
52761
52765
|
// ../src/build-info.ts
|
|
52762
|
-
var VERSION = "0.14.
|
|
52763
|
-
var COMMIT_SHA = "
|
|
52764
|
-
var COMMIT_DATE = "2026-06-
|
|
52765
|
-
var LATEST_PR =
|
|
52766
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
52766
|
+
var VERSION = "0.14.66";
|
|
52767
|
+
var COMMIT_SHA = "0f4f029d";
|
|
52768
|
+
var COMMIT_DATE = "2026-06-05T07:05:45Z";
|
|
52769
|
+
var LATEST_PR = 2167;
|
|
52770
|
+
var COMMITS_AHEAD_OF_TAG = 2;
|
|
52767
52771
|
|
|
52768
52772
|
// gateway/boot-version.ts
|
|
52769
52773
|
function formatRelativeAgo(iso) {
|
|
@@ -54061,6 +54065,33 @@ function findTurnByOriginId(originTurnId) {
|
|
|
54061
54065
|
return currentTurn;
|
|
54062
54066
|
return recentTurnsById.get(originTurnId) ?? null;
|
|
54063
54067
|
}
|
|
54068
|
+
var LATE_REPLY_TOPIC_RECOVERY_ENABLED = process.env.SWITCHROOM_LATE_REPLY_TOPIC_RECOVERY !== "0";
|
|
54069
|
+
function findLatestEndedTurnForChat(chatId) {
|
|
54070
|
+
let latest = null;
|
|
54071
|
+
for (const t of recentTurnsById.values()) {
|
|
54072
|
+
if (t.sessionChatId === chatId)
|
|
54073
|
+
latest = t;
|
|
54074
|
+
}
|
|
54075
|
+
return latest;
|
|
54076
|
+
}
|
|
54077
|
+
function resolveAnswerThreadWithLog(chatId, explicitThreadId, originTurn, liveTurn, surface) {
|
|
54078
|
+
const recovered = LATE_REPLY_TOPIC_RECOVERY_ENABLED && explicitThreadId == null && originTurn == null && liveTurn?.sessionThreadId == null ? findLatestEndedTurnForChat(chatId) : null;
|
|
54079
|
+
const threadId = resolveAnswerThreadId({
|
|
54080
|
+
explicitThreadId,
|
|
54081
|
+
originResolved: originTurn != null,
|
|
54082
|
+
originThreadId: originTurn?.sessionThreadId,
|
|
54083
|
+
liveThreadId: liveTurn?.sessionThreadId,
|
|
54084
|
+
lastEndedResolvedForChat: recovered != null,
|
|
54085
|
+
lastEndedThreadIdForChat: recovered?.sessionThreadId
|
|
54086
|
+
});
|
|
54087
|
+
const via = explicitThreadId != null ? "explicit" : originTurn != null ? "origin" : liveTurn?.sessionThreadId != null ? "live" : recovered != null ? "recovered" : "none";
|
|
54088
|
+
const ownerTurn = originTurn ?? recovered ?? liveTurn;
|
|
54089
|
+
const isSupergroup = chatId.startsWith("-100");
|
|
54090
|
+
const unrouted = isSupergroup && threadId == null;
|
|
54091
|
+
process.stderr.write(`telegram gateway: reply-route surface=${surface} chat=${chatId} resolved_thread=${threadId ?? "-"} via=${via} late=${liveTurn == null} originTurn=${ownerTurn?.turnId ?? "-"} origin_thread=${ownerTurn?.sessionThreadId ?? "-"}` + (via === "recovered" ? " RECOVERED" : "") + (unrouted ? " UNROUTED(supergroup\u2192no-topic)" : "") + `
|
|
54092
|
+
`);
|
|
54093
|
+
return threadId;
|
|
54094
|
+
}
|
|
54064
54095
|
function closeObligationOnSubstantiveReply(args, liveTurn) {
|
|
54065
54096
|
if (!OBLIGATION_LEDGER_ENABLED)
|
|
54066
54097
|
return;
|
|
@@ -56291,12 +56322,7 @@ ${url}`;
|
|
|
56291
56322
|
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
56292
56323
|
const explicit = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
|
|
56293
56324
|
const originTurn = findTurnByOriginId(args.origin_turn_id);
|
|
56294
|
-
threadId =
|
|
56295
|
-
explicitThreadId: Number.isFinite(explicit) ? explicit : undefined,
|
|
56296
|
-
originResolved: originTurn != null,
|
|
56297
|
-
originThreadId: originTurn?.sessionThreadId,
|
|
56298
|
-
liveThreadId: turn?.sessionThreadId
|
|
56299
|
-
});
|
|
56325
|
+
threadId = resolveAnswerThreadWithLog(chat_id, Number.isFinite(explicit) ? explicit : undefined, originTurn, turn, "reply");
|
|
56300
56326
|
} else {
|
|
56301
56327
|
threadId = resolveThreadId(chat_id, args.message_thread_id ?? (turn?.sessionThreadId != null ? turn.sessionThreadId : undefined));
|
|
56302
56328
|
}
|
|
@@ -56656,12 +56682,7 @@ async function executeStreamReply(args) {
|
|
|
56656
56682
|
let injected;
|
|
56657
56683
|
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
56658
56684
|
const originTurn = findTurnByOriginId(args.origin_turn_id);
|
|
56659
|
-
injected =
|
|
56660
|
-
explicitThreadId: undefined,
|
|
56661
|
-
originResolved: originTurn != null,
|
|
56662
|
-
originThreadId: originTurn?.sessionThreadId,
|
|
56663
|
-
liveThreadId: turn?.sessionThreadId
|
|
56664
|
-
});
|
|
56685
|
+
injected = resolveAnswerThreadWithLog(String(args.chat_id), undefined, originTurn, turn, "stream_reply");
|
|
56665
56686
|
} else {
|
|
56666
56687
|
injected = turn?.sessionThreadId;
|
|
56667
56688
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { resolveAnswerThreadId } from './answer-thread-resolve.js'
|
|
3
|
+
|
|
4
|
+
describe('resolveAnswerThreadId — precedence', () => {
|
|
5
|
+
it('(1) explicit model thread wins over everything', () => {
|
|
6
|
+
expect(
|
|
7
|
+
resolveAnswerThreadId({
|
|
8
|
+
explicitThreadId: 7,
|
|
9
|
+
originResolved: true,
|
|
10
|
+
originThreadId: 3,
|
|
11
|
+
liveThreadId: 4,
|
|
12
|
+
lastEndedResolvedForChat: true,
|
|
13
|
+
lastEndedThreadIdForChat: 9,
|
|
14
|
+
}),
|
|
15
|
+
).toBe(7)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('(2) origin turn thread wins over the live turn (the Brevo→Meta fix)', () => {
|
|
19
|
+
expect(
|
|
20
|
+
resolveAnswerThreadId({ originResolved: true, originThreadId: 3, liveThreadId: 4 }),
|
|
21
|
+
).toBe(3)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('(2) a DM origin (resolved, thread undefined) pins to undefined, not the live thread', () => {
|
|
25
|
+
expect(
|
|
26
|
+
resolveAnswerThreadId({ originResolved: true, originThreadId: undefined, liveThreadId: 4 }),
|
|
27
|
+
).toBeUndefined()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('(3) no origin → falls back to the live turn thread (legacy #1664)', () => {
|
|
31
|
+
expect(
|
|
32
|
+
resolveAnswerThreadId({ originResolved: false, liveThreadId: 4 }),
|
|
33
|
+
).toBe(4)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// ── tier (4): late-reply topic recovery (2026-06-05) ──────────────────────
|
|
37
|
+
it('(4) no explicit, no origin, NO live turn → recovers the most-recent ended turn thread', () => {
|
|
38
|
+
// The marko bug: a reply that fired after the orphaned-reply backstop ended
|
|
39
|
+
// its turn. Pre-fix this returned undefined (General); now it recovers topic 3.
|
|
40
|
+
expect(
|
|
41
|
+
resolveAnswerThreadId({
|
|
42
|
+
originResolved: false,
|
|
43
|
+
liveThreadId: undefined,
|
|
44
|
+
lastEndedResolvedForChat: true,
|
|
45
|
+
lastEndedThreadIdForChat: 3,
|
|
46
|
+
}),
|
|
47
|
+
).toBe(3)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('(4) a recovered DM turn (ended, thread undefined) stays threadless', () => {
|
|
51
|
+
expect(
|
|
52
|
+
resolveAnswerThreadId({
|
|
53
|
+
originResolved: false,
|
|
54
|
+
liveThreadId: undefined,
|
|
55
|
+
lastEndedResolvedForChat: true,
|
|
56
|
+
lastEndedThreadIdForChat: undefined,
|
|
57
|
+
}),
|
|
58
|
+
).toBeUndefined()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('(4) recovery does NOT override a live turn — live thread still wins at tier 3', () => {
|
|
62
|
+
expect(
|
|
63
|
+
resolveAnswerThreadId({
|
|
64
|
+
originResolved: false,
|
|
65
|
+
liveThreadId: 4,
|
|
66
|
+
lastEndedResolvedForChat: true,
|
|
67
|
+
lastEndedThreadIdForChat: 3,
|
|
68
|
+
}),
|
|
69
|
+
).toBe(4)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('(4) no recovery candidate → legacy result (undefined), unchanged', () => {
|
|
73
|
+
expect(
|
|
74
|
+
resolveAnswerThreadId({
|
|
75
|
+
originResolved: false,
|
|
76
|
+
liveThreadId: undefined,
|
|
77
|
+
lastEndedResolvedForChat: false,
|
|
78
|
+
}),
|
|
79
|
+
).toBeUndefined()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('pure DM (every tier undefined) → undefined', () => {
|
|
83
|
+
expect(resolveAnswerThreadId({ originResolved: false })).toBeUndefined()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -26,10 +26,14 @@
|
|
|
26
26
|
* 3. Else the LIVE turn's thread — but ONLY when the live turn IS the
|
|
27
27
|
* origin turn (no flip happened) OR no origin turn could be resolved
|
|
28
28
|
* at all (origin id absent/unknown; legacy / pre-stamp path).
|
|
29
|
-
* 4. Else (
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* 4. Else (no explicit, no origin echoed, no live turn) — a LATE reply that
|
|
30
|
+
* fired after its turn already ended (the orphaned-reply backstop case) —
|
|
31
|
+
* recover the origin topic from the most-recently-ended turn for this
|
|
32
|
+
* chat. Without this, such a reply defaults to the main chat (General in a
|
|
33
|
+
* supergroup) and its answer vanishes from the topic the user is reading
|
|
34
|
+
* (the 2026-06-05 marko triage). Still NOT the `chatThreadMap` last-seen
|
|
35
|
+
* heuristic — the recovered turn is the chat's own most-recent turn, not
|
|
36
|
+
* whichever topic last received any message.
|
|
33
37
|
*
|
|
34
38
|
* The `chatThreadMap` last-seen fallback is preserved for NON-answer
|
|
35
39
|
* surfaces (`send_typing`, `forward_message`, `progress_update`) by NOT
|
|
@@ -53,6 +57,20 @@ export interface AnswerThreadInput {
|
|
|
53
57
|
* (no live turn, or a DM live turn). The legacy (#1664) fallback when
|
|
54
58
|
* no origin turn is resolvable. */
|
|
55
59
|
liveThreadId?: number | undefined
|
|
60
|
+
/**
|
|
61
|
+
* Late-reply topic recovery (2026-06-05). Thread of the most-recently-ended
|
|
62
|
+
* turn for THIS chat (from `recentTurnsById`), used as a deterministic
|
|
63
|
+
* fallback when the model echoed no `origin_turn_id` AND there is no live
|
|
64
|
+
* turn — the late-reply-after-turn-end case. Without it, a reply that fires
|
|
65
|
+
* after the orphaned-reply backstop closed its turn defaults to the main chat
|
|
66
|
+
* (General topic in a supergroup), so its answer vanishes from the topic the
|
|
67
|
+
* user is reading. Only consulted at tier (4); a DM origin yields undefined,
|
|
68
|
+
* which is correct.
|
|
69
|
+
*/
|
|
70
|
+
lastEndedThreadIdForChat?: number | undefined
|
|
71
|
+
/** Whether a recently-ended turn exists for this chat — distinguishes
|
|
72
|
+
* "ended turn exists, DM (thread undefined)" from "no ended turn at all". */
|
|
73
|
+
lastEndedResolvedForChat?: boolean
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
/**
|
|
@@ -75,5 +93,13 @@ export function resolveAnswerThreadId(input: AnswerThreadInput): number | undefi
|
|
|
75
93
|
if (input.originResolved) return input.originThreadId
|
|
76
94
|
// (3) no origin resolved (legacy / pre-stamp / evicted) → fall back to
|
|
77
95
|
// the live turn's thread, the existing turn-pinned behaviour (#1664).
|
|
96
|
+
if (input.liveThreadId != null) return input.liveThreadId
|
|
97
|
+
// (4) no explicit, no origin echoed, no live turn — a LATE reply that fired
|
|
98
|
+
// after its turn already ended (the orphaned-reply backstop case).
|
|
99
|
+
// Recover the origin topic from the most-recently-ended turn for this
|
|
100
|
+
// chat so the answer lands in the topic it belongs to instead of
|
|
101
|
+
// defaulting to the main chat (General). When no ended turn is known,
|
|
102
|
+
// fall through to liveThreadId (undefined) — the legacy result.
|
|
103
|
+
if (input.lastEndedResolvedForChat) return input.lastEndedThreadIdForChat
|
|
78
104
|
return input.liveThreadId
|
|
79
105
|
}
|
|
@@ -1888,6 +1888,83 @@ function findTurnByOriginId(originTurnId: string | null | undefined): CurrentTur
|
|
|
1888
1888
|
return recentTurnsById.get(originTurnId) ?? null
|
|
1889
1889
|
}
|
|
1890
1890
|
|
|
1891
|
+
// Late-reply topic recovery (2026-06-05 marko triage). Default ON; kill switch
|
|
1892
|
+
// SWITCHROOM_LATE_REPLY_TOPIC_RECOVERY=0 restores the legacy behaviour (a late
|
|
1893
|
+
// reply with no echoed origin and no live turn defaults to General).
|
|
1894
|
+
const LATE_REPLY_TOPIC_RECOVERY_ENABLED =
|
|
1895
|
+
process.env.SWITCHROOM_LATE_REPLY_TOPIC_RECOVERY !== '0'
|
|
1896
|
+
|
|
1897
|
+
/**
|
|
1898
|
+
* The most-recently-started turn for a chat from the bounded recently-ended
|
|
1899
|
+
* registry — the deterministic fallback for a LATE answer reply when the model
|
|
1900
|
+
* echoed no `origin_turn_id` and `currentTurn` has already cleared. Iterates in
|
|
1901
|
+
* insertion order so the last match is the most recent turn for that chat.
|
|
1902
|
+
* Returns null when the chat has no remembered turn (so the caller keeps the
|
|
1903
|
+
* legacy result). NB: this is the chat's own most-recent TURN, not the
|
|
1904
|
+
* `chatThreadMap` last-seen-any-message heuristic that caused the wrong-topic
|
|
1905
|
+
* bug — a late reply almost always belongs to the turn that just ended.
|
|
1906
|
+
*/
|
|
1907
|
+
function findLatestEndedTurnForChat(chatId: string): CurrentTurn | null {
|
|
1908
|
+
let latest: CurrentTurn | null = null
|
|
1909
|
+
for (const t of recentTurnsById.values()) {
|
|
1910
|
+
if (t.sessionChatId === chatId) latest = t
|
|
1911
|
+
}
|
|
1912
|
+
return latest
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/**
|
|
1916
|
+
* Resolve the answer-reply thread AND emit `reply-route` telemetry. The
|
|
1917
|
+
* 2026-06-05 triage showed reply routing was the blind spot: `reply: invoked`
|
|
1918
|
+
* logged only chat + char count, so a late reply landing in the wrong topic was
|
|
1919
|
+
* invisible without hand-correlating raw tg-post threads against turn-lifecycle
|
|
1920
|
+
* timestamps. This wrapper logs, per reply: which precedence tier won (`via`),
|
|
1921
|
+
* the resolved thread, the origin turn + its thread, and whether the reply was
|
|
1922
|
+
* late (turn already ended). `via=recovered` marks a late reply this fix saved
|
|
1923
|
+
* from General; `UNROUTED` flags a supergroup reply that still resolved to no
|
|
1924
|
+
* topic (the residual gap to watch).
|
|
1925
|
+
*/
|
|
1926
|
+
function resolveAnswerThreadWithLog(
|
|
1927
|
+
chatId: string,
|
|
1928
|
+
explicitThreadId: number | undefined,
|
|
1929
|
+
originTurn: CurrentTurn | null,
|
|
1930
|
+
liveTurn: CurrentTurn | null,
|
|
1931
|
+
surface: 'reply' | 'stream_reply',
|
|
1932
|
+
): number | undefined {
|
|
1933
|
+
const recovered =
|
|
1934
|
+
LATE_REPLY_TOPIC_RECOVERY_ENABLED &&
|
|
1935
|
+
explicitThreadId == null &&
|
|
1936
|
+
originTurn == null &&
|
|
1937
|
+
liveTurn?.sessionThreadId == null
|
|
1938
|
+
? findLatestEndedTurnForChat(chatId)
|
|
1939
|
+
: null
|
|
1940
|
+
const threadId = resolveAnswerThreadId({
|
|
1941
|
+
explicitThreadId,
|
|
1942
|
+
originResolved: originTurn != null,
|
|
1943
|
+
originThreadId: originTurn?.sessionThreadId,
|
|
1944
|
+
liveThreadId: liveTurn?.sessionThreadId,
|
|
1945
|
+
lastEndedResolvedForChat: recovered != null,
|
|
1946
|
+
lastEndedThreadIdForChat: recovered?.sessionThreadId,
|
|
1947
|
+
})
|
|
1948
|
+
const via =
|
|
1949
|
+
explicitThreadId != null ? 'explicit'
|
|
1950
|
+
: originTurn != null ? 'origin'
|
|
1951
|
+
: liveTurn?.sessionThreadId != null ? 'live'
|
|
1952
|
+
: recovered != null ? 'recovered'
|
|
1953
|
+
: 'none'
|
|
1954
|
+
const ownerTurn = originTurn ?? recovered ?? liveTurn
|
|
1955
|
+
const isSupergroup = chatId.startsWith('-100')
|
|
1956
|
+
const unrouted = isSupergroup && threadId == null
|
|
1957
|
+
process.stderr.write(
|
|
1958
|
+
`telegram gateway: reply-route surface=${surface} chat=${chatId} ` +
|
|
1959
|
+
`resolved_thread=${threadId ?? '-'} via=${via} late=${liveTurn == null} ` +
|
|
1960
|
+
`originTurn=${ownerTurn?.turnId ?? '-'} origin_thread=${ownerTurn?.sessionThreadId ?? '-'}` +
|
|
1961
|
+
(via === 'recovered' ? ' RECOVERED' : '') +
|
|
1962
|
+
(unrouted ? ' UNROUTED(supergroup→no-topic)' : '') +
|
|
1963
|
+
'\n',
|
|
1964
|
+
)
|
|
1965
|
+
return threadId
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1891
1968
|
/**
|
|
1892
1969
|
* PR2 obligation-ledger CLOSE. Called when a SUBSTANTIVE final answer lands
|
|
1893
1970
|
* (not a bare interim ack — using finalAnswerSubstantive, the #2141 signal): the
|
|
@@ -6522,12 +6599,13 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
6522
6599
|
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
6523
6600
|
const explicit = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
|
|
6524
6601
|
const originTurn = findTurnByOriginId(args.origin_turn_id as string | undefined)
|
|
6525
|
-
threadId =
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6602
|
+
threadId = resolveAnswerThreadWithLog(
|
|
6603
|
+
chat_id,
|
|
6604
|
+
Number.isFinite(explicit as number) ? (explicit as number) : undefined,
|
|
6605
|
+
originTurn,
|
|
6606
|
+
turn,
|
|
6607
|
+
'reply',
|
|
6608
|
+
)
|
|
6531
6609
|
} else {
|
|
6532
6610
|
threadId = resolveThreadId(
|
|
6533
6611
|
chat_id,
|
|
@@ -7178,12 +7256,13 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
|
|
|
7178
7256
|
let injected: number | undefined
|
|
7179
7257
|
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
7180
7258
|
const originTurn = findTurnByOriginId(args.origin_turn_id as string | undefined)
|
|
7181
|
-
injected =
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7259
|
+
injected = resolveAnswerThreadWithLog(
|
|
7260
|
+
String(args.chat_id),
|
|
7261
|
+
undefined,
|
|
7262
|
+
originTurn,
|
|
7263
|
+
turn,
|
|
7264
|
+
'stream_reply',
|
|
7265
|
+
)
|
|
7187
7266
|
} else {
|
|
7188
7267
|
injected = turn?.sessionThreadId
|
|
7189
7268
|
}
|
|
@@ -45,13 +45,15 @@ describe('component 3 — turn-origin reply routing', () => {
|
|
|
45
45
|
const fn = gatewaySrc.split('async function executeReply')[1]?.split('\nasync function ')[0] ?? ''
|
|
46
46
|
expect(fn).toMatch(/TURN_ORIGIN_ROUTING_ENABLED/)
|
|
47
47
|
expect(fn).toMatch(/findTurnByOriginId\(args\.origin_turn_id/)
|
|
48
|
-
|
|
48
|
+
// The resolution + reply-route telemetry go through resolveAnswerThreadWithLog,
|
|
49
|
+
// which calls the pure resolveAnswerThreadId internally (incl. tier-4 recovery).
|
|
50
|
+
expect(fn).toMatch(/resolveAnswerThread\w*\(/)
|
|
49
51
|
})
|
|
50
52
|
|
|
51
53
|
it('executeStreamReply resolves the answer thread via the origin turn too', () => {
|
|
52
54
|
const fn = gatewaySrc.split('async function executeStreamReply')[1]?.split('\nasync function ')[0] ?? ''
|
|
53
55
|
expect(fn).toMatch(/findTurnByOriginId\(args\.origin_turn_id/)
|
|
54
|
-
expect(fn).toMatch(/
|
|
56
|
+
expect(fn).toMatch(/resolveAnswerThread\w*\(/)
|
|
55
57
|
})
|
|
56
58
|
|
|
57
59
|
it('the reply + stream_reply tool schemas expose origin_turn_id to the model', () => {
|