switchroom 0.21.5 → 0.21.7
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/bin/autoaccept.exp +21 -1
- package/dist/cli/autoaccept-poll.js +95 -7
- package/dist/cli/switchroom.js +14 -10
- package/dist/host-control/main.js +1 -1
- package/package.json +3 -2
- package/telegram-plugin/bunfig.toml +12 -4
- package/telegram-plugin/dist/gateway/gateway.js +4 -4
- package/telegram-plugin/tests/framework-fallback-drains-parked.test.ts +7 -1
- package/telegram-plugin/tests/parked-turn-start-preload.test.ts +65 -0
- package/telegram-plugin/tests/queued-card-surface.test.ts +9 -1
- package/telegram-plugin/tests/stream-render-golden.test.ts +10 -1
- package/telegram-plugin/tests/turn-mint-defers-until-dequeue.test.ts +7 -1
- package/telegram-plugin/tests/turn-supersede-finalizes-prior-card.test.ts +7 -1
package/bin/autoaccept.exp
CHANGED
|
@@ -86,8 +86,28 @@ expect {
|
|
|
86
86
|
send "\r"
|
|
87
87
|
exp_continue
|
|
88
88
|
}
|
|
89
|
+
-re {Continue with Fable} {
|
|
90
|
+
# Fable-5 usage-credits consent modal:
|
|
91
|
+
# Fable 5 runs on usage credits, purchased separately from your plan.
|
|
92
|
+
# 1. Continue with Fable 5
|
|
93
|
+
# ❯ 2. Switch to Sonnet 5 and continue
|
|
94
|
+
# Enter to confirm · Esc to cancel
|
|
95
|
+
# MUST precede the generic "Enter to confirm" branch below: that
|
|
96
|
+
# footer matches this modal too, and the CLI focuses option 2 by
|
|
97
|
+
# default (`defaultFocusValue:"switch"`), so a bare Return silently
|
|
98
|
+
# declines Fable and downgrades the agent to Sonnet. Select option 1
|
|
99
|
+
# by digit instead. Kept in sync with the `fable-usage-credits-consent`
|
|
100
|
+
# rule in src/agents/autoaccept.ts.
|
|
101
|
+
sleep 0.5
|
|
102
|
+
send "1\r"
|
|
103
|
+
exp_continue
|
|
104
|
+
}
|
|
89
105
|
-re {Enter.{1,30}confirm} {
|
|
90
|
-
# Generic "Enter to confirm" prompt (dev channels, trust dialog)
|
|
106
|
+
# Generic "Enter to confirm" prompt (dev channels, trust dialog).
|
|
107
|
+
# NOTE: the TS poller additionally guards this catch-all against
|
|
108
|
+
# numbered selectors (NUMBERED_CHOICE_SIGNATURE). expect matches a
|
|
109
|
+
# STREAM in branch order, so here the equivalent protection is
|
|
110
|
+
# ordering: every option-selecting branch must sit above this one.
|
|
91
111
|
sleep 0.8
|
|
92
112
|
send "\r"
|
|
93
113
|
exp_continue
|
|
@@ -20,6 +20,29 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
20
20
|
|
|
21
21
|
// src/agents/autoaccept.ts
|
|
22
22
|
import { execFileSync } from "node:child_process";
|
|
23
|
+
var NUMBERED_CHOICE_SIGNATURE = /(?=[\s\S]*^[^\S\n]*(?:\u276f[^\S\n]*)?1\.[^\S\n]+\S)(?=[\s\S]*^[^\S\n]*(?:\u276f[^\S\n]*)?2\.[^\S\n]+\S)/m;
|
|
24
|
+
var FABLE_CONSENT_SIGNATURE = /(?=[\s\S]*Continue with Fable)(?=[\s\S]*usage credits)/i;
|
|
25
|
+
function fableConsentNav(text) {
|
|
26
|
+
for (const line of text.split(`
|
|
27
|
+
`)) {
|
|
28
|
+
const m = line.match(/^\s*(\u276f)?\s*(\d+)\.\s+(.*\S)\s*$/);
|
|
29
|
+
if (!m)
|
|
30
|
+
continue;
|
|
31
|
+
if (/Continue with Fable/i.test(m[3]))
|
|
32
|
+
return [m[2], "Enter"];
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function ruleMatches(rule, text) {
|
|
37
|
+
if (!rule.match.test(text))
|
|
38
|
+
return false;
|
|
39
|
+
if (rule.notMatch && rule.notMatch.test(text))
|
|
40
|
+
return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function resolveRuleKeys(rule, text) {
|
|
44
|
+
return rule.keysFor?.(text) ?? rule.keys;
|
|
45
|
+
}
|
|
23
46
|
var PROMPTS = [
|
|
24
47
|
{
|
|
25
48
|
name: "dev-channels-loading",
|
|
@@ -57,9 +80,16 @@ var PROMPTS = [
|
|
|
57
80
|
keys: ["Escape"],
|
|
58
81
|
maxFires: 5
|
|
59
82
|
},
|
|
83
|
+
{
|
|
84
|
+
name: "fable-usage-credits-consent",
|
|
85
|
+
match: FABLE_CONSENT_SIGNATURE,
|
|
86
|
+
keysFor: fableConsentNav,
|
|
87
|
+
keys: ["1", "Enter"]
|
|
88
|
+
},
|
|
60
89
|
{
|
|
61
90
|
name: "enter-to-confirm",
|
|
62
91
|
match: /Enter.{1,30}confirm/,
|
|
92
|
+
notMatch: NUMBERED_CHOICE_SIGNATURE,
|
|
63
93
|
keys: ["Enter"]
|
|
64
94
|
}
|
|
65
95
|
];
|
|
@@ -117,12 +147,13 @@ async function runAutoaccept(opts) {
|
|
|
117
147
|
for (const entry of rules) {
|
|
118
148
|
if (entry.fired >= entry.cap)
|
|
119
149
|
continue;
|
|
120
|
-
if (entry.rule
|
|
150
|
+
if (ruleMatches(entry.rule, text)) {
|
|
151
|
+
const keys = resolveRuleKeys(entry.rule, text);
|
|
121
152
|
entry.fired++;
|
|
122
153
|
matchedThisPoll = true;
|
|
123
154
|
fired.push(entry.rule.name);
|
|
124
|
-
console.error(`[autoaccept] ${opts.agentName}: fired ${entry.rule.name} (${
|
|
125
|
-
sendKeys(opts.agentName,
|
|
155
|
+
console.error(`[autoaccept] ${opts.agentName}: fired ${entry.rule.name} (${keys.join("+")})`);
|
|
156
|
+
sendKeys(opts.agentName, keys);
|
|
126
157
|
}
|
|
127
158
|
}
|
|
128
159
|
}
|
|
@@ -141,6 +172,26 @@ async function runAutoaccept(opts) {
|
|
|
141
172
|
|
|
142
173
|
// src/agents/autoaccept.ts
|
|
143
174
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
175
|
+
var NUMBERED_CHOICE_SIGNATURE2 = /(?=[\s\S]*^[^\S\n]*(?:\u276f[^\S\n]*)?1\.[^\S\n]+\S)(?=[\s\S]*^[^\S\n]*(?:\u276f[^\S\n]*)?2\.[^\S\n]+\S)/m;
|
|
176
|
+
var FABLE_CONSENT_SIGNATURE2 = /(?=[\s\S]*Continue with Fable)(?=[\s\S]*usage credits)/i;
|
|
177
|
+
function fableConsentNav2(text) {
|
|
178
|
+
for (const line of text.split(`
|
|
179
|
+
`)) {
|
|
180
|
+
const m = line.match(/^\s*(\u276f)?\s*(\d+)\.\s+(.*\S)\s*$/);
|
|
181
|
+
if (!m)
|
|
182
|
+
continue;
|
|
183
|
+
if (/Continue with Fable/i.test(m[3]))
|
|
184
|
+
return [m[2], "Enter"];
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function ruleMatches2(rule, text) {
|
|
189
|
+
if (!rule.match.test(text))
|
|
190
|
+
return false;
|
|
191
|
+
if (rule.notMatch && rule.notMatch.test(text))
|
|
192
|
+
return false;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
144
195
|
var PROMPTS2 = [
|
|
145
196
|
{
|
|
146
197
|
name: "dev-channels-loading",
|
|
@@ -178,9 +229,16 @@ var PROMPTS2 = [
|
|
|
178
229
|
keys: ["Escape"],
|
|
179
230
|
maxFires: 5
|
|
180
231
|
},
|
|
232
|
+
{
|
|
233
|
+
name: "fable-usage-credits-consent",
|
|
234
|
+
match: FABLE_CONSENT_SIGNATURE2,
|
|
235
|
+
keysFor: fableConsentNav2,
|
|
236
|
+
keys: ["1", "Enter"]
|
|
237
|
+
},
|
|
181
238
|
{
|
|
182
239
|
name: "enter-to-confirm",
|
|
183
240
|
match: /Enter.{1,30}confirm/,
|
|
241
|
+
notMatch: NUMBERED_CHOICE_SIGNATURE2,
|
|
184
242
|
keys: ["Enter"]
|
|
185
243
|
}
|
|
186
244
|
];
|
|
@@ -613,6 +671,7 @@ var DEFAULT_STABILITY_THRESHOLD = 3;
|
|
|
613
671
|
var DEFAULT_COOLDOWN_MS = 60000;
|
|
614
672
|
var DEFAULT_CONFIRM_MODAL_POLLS = 3;
|
|
615
673
|
var DEFAULT_MANIFEST_STALL_POLLS = 60;
|
|
674
|
+
var DEFAULT_FABLE_CONSENT_POLLS = 3;
|
|
616
675
|
var DEFAULT_PERMISSION_PROMPT_POLLS = 3;
|
|
617
676
|
var DEFAULT_PERMISSION_CARDLESS_POLLS = 24;
|
|
618
677
|
var DEFAULT_PERMISSION_FLOOD_MAX_POLLS = 120;
|
|
@@ -643,6 +702,8 @@ async function runWedgeWatchdog(opts) {
|
|
|
643
702
|
const parseReset = opts.parseReset ?? parseWeeklyReset;
|
|
644
703
|
const confirmModalSignature = opts.confirmModalSignature === null ? null : opts.confirmModalSignature ?? CONFIRM_MODAL_SIGNATURE;
|
|
645
704
|
const confirmModalPolls = opts.confirmModalPolls ?? envInt("SWITCHROOM_WEDGE_CONFIRM_POLLS", DEFAULT_CONFIRM_MODAL_POLLS);
|
|
705
|
+
const fableConsentSignature = opts.fableConsentSignature === null ? null : opts.fableConsentSignature ?? FABLE_CONSENT_SIGNATURE2;
|
|
706
|
+
const fableConsentPolls = opts.fableConsentPolls ?? envInt("SWITCHROOM_WEDGE_FABLE_POLLS", DEFAULT_FABLE_CONSENT_POLLS);
|
|
646
707
|
const permissionPromptSignature = opts.permissionPromptSignature === null ? null : opts.permissionPromptSignature ?? PERMISSION_PROMPT_SIGNATURE;
|
|
647
708
|
const permissionPromptPolls = opts.permissionPromptPolls ?? envInt("SWITCHROOM_WEDGE_PERMISSION_POLLS", DEFAULT_PERMISSION_PROMPT_POLLS);
|
|
648
709
|
const permissionCardlessPolls = Math.max(permissionPromptPolls, opts.permissionCardlessPolls ?? envInt("SWITCHROOM_WEDGE_PERMISSION_CARDLESS_POLLS", DEFAULT_PERMISSION_CARDLESS_POLLS));
|
|
@@ -664,6 +725,7 @@ async function runWedgeWatchdog(opts) {
|
|
|
664
725
|
let rateLimitFires = 0;
|
|
665
726
|
let overageCreditSelections = 0;
|
|
666
727
|
let confirmModalFires = 0;
|
|
728
|
+
let fableConsentFires = 0;
|
|
667
729
|
let permissionPromptFires = 0;
|
|
668
730
|
let permissionPromptDeferrals = 0;
|
|
669
731
|
let permissionPromptFloodHolds = 0;
|
|
@@ -671,10 +733,12 @@ async function runWedgeWatchdog(opts) {
|
|
|
671
733
|
let restartEscalations = 0;
|
|
672
734
|
let polls = 0;
|
|
673
735
|
let confirmModalPresent = 0;
|
|
736
|
+
let fableConsentPresent = 0;
|
|
674
737
|
let permissionPromptPresent = 0;
|
|
675
738
|
let manifestStallPresent = 0;
|
|
676
739
|
let lastManifestKey = null;
|
|
677
740
|
let confirmCooldownUntil = 0;
|
|
741
|
+
let fableCooldownUntil = 0;
|
|
678
742
|
let permissionCooldownUntil = 0;
|
|
679
743
|
let lastPermissionHoldLogAt = Number.NEGATIVE_INFINITY;
|
|
680
744
|
while (polls < maxPolls) {
|
|
@@ -687,9 +751,10 @@ async function runWedgeWatchdog(opts) {
|
|
|
687
751
|
text = "";
|
|
688
752
|
}
|
|
689
753
|
const isRateLimitMenu = !!text && rateLimitSignature !== null && rateLimitSignature.test(text);
|
|
690
|
-
const
|
|
691
|
-
const
|
|
692
|
-
const
|
|
754
|
+
const isFableConsent = !isRateLimitMenu && !!text && fableConsentSignature !== null && fableConsentSignature.test(text);
|
|
755
|
+
const isPermissionPrompt = !isRateLimitMenu && !isFableConsent && !!text && permissionPromptSignature !== null && permissionPromptSignature.test(text);
|
|
756
|
+
const isConfirmModal = !isRateLimitMenu && !isFableConsent && !isPermissionPrompt && !!text && confirmModalSignature !== null && confirmModalSignature.test(text);
|
|
757
|
+
const isBlockingModal = !isRateLimitMenu && !isFableConsent && !isPermissionPrompt && !isConfirmModal && !!text && signature.test(text) && !deferToPrompts.some((p) => ruleMatches2(p, text));
|
|
693
758
|
const manifestSignatureHit = !!text && manifestStallSignature !== null && manifestStallSignature.test(text);
|
|
694
759
|
const manifestKey = manifestSignatureHit ? stabilityKey(text) : null;
|
|
695
760
|
const stopHookPresent = !!text && STOP_HOOK_ERROR_SIGNATURE.test(text);
|
|
@@ -767,10 +832,30 @@ async function runWedgeWatchdog(opts) {
|
|
|
767
832
|
stableCount = 0;
|
|
768
833
|
lastKey = null;
|
|
769
834
|
}
|
|
835
|
+
} else if (isFableConsent) {
|
|
836
|
+
fableConsentPresent++;
|
|
837
|
+
stableCount = 0;
|
|
838
|
+
lastKey = null;
|
|
839
|
+
permissionPromptPresent = 0;
|
|
840
|
+
confirmModalPresent = 0;
|
|
841
|
+
if (fableConsentPresent >= fableConsentPolls && now() >= fableCooldownUntil) {
|
|
842
|
+
const nav = fableConsentNav2(text) ?? ["1", "Enter"];
|
|
843
|
+
console.error(`[wedge-watchdog] ${opts.agentName}: Fable-5 usage-credits consent modal present ` + `${fableConsentPresent} polls ` + `(~${Math.round(fableConsentPresent * pollIntervalMs / 1000)}s) \u2014 ` + `selecting "Continue with Fable 5" (${nav.join(" ")}); the session is on fable ` + `because the operator configured it, and Enter/Esc here would silently decline it`);
|
|
844
|
+
try {
|
|
845
|
+
send(opts.agentName, nav);
|
|
846
|
+
} catch (err) {
|
|
847
|
+
console.error(`[wedge-watchdog] ${opts.agentName}: send threw: ${err.message}`);
|
|
848
|
+
}
|
|
849
|
+
fires++;
|
|
850
|
+
fableConsentFires++;
|
|
851
|
+
fableCooldownUntil = now() + cooldownMs;
|
|
852
|
+
fableConsentPresent = 0;
|
|
853
|
+
}
|
|
770
854
|
} else if (isPermissionPrompt) {
|
|
771
855
|
permissionPromptPresent++;
|
|
772
856
|
stableCount = 0;
|
|
773
857
|
lastKey = null;
|
|
858
|
+
fableConsentPresent = 0;
|
|
774
859
|
if (permissionPromptPresent >= permissionPromptPolls && now() >= permissionCooldownUntil) {
|
|
775
860
|
let status = null;
|
|
776
861
|
if (queryPendingPermission2) {
|
|
@@ -824,6 +909,7 @@ async function runWedgeWatchdog(opts) {
|
|
|
824
909
|
stableCount = 0;
|
|
825
910
|
lastKey = null;
|
|
826
911
|
permissionPromptPresent = 0;
|
|
912
|
+
fableConsentPresent = 0;
|
|
827
913
|
if (confirmModalPresent >= confirmModalPolls && now() >= confirmCooldownUntil) {
|
|
828
914
|
console.error(`[wedge-watchdog] ${opts.agentName}: dismissing stuck confirmation modal ` + `(Esc == "No, go back") after ${confirmModalPresent} polls present ` + `(~${Math.round(confirmModalPresent * pollIntervalMs / 1000)}s, flicker-immune) ` + `\u2014 no human to answer it`);
|
|
829
915
|
try {
|
|
@@ -861,6 +947,7 @@ async function runWedgeWatchdog(opts) {
|
|
|
861
947
|
lastKey = null;
|
|
862
948
|
confirmModalPresent = 0;
|
|
863
949
|
permissionPromptPresent = 0;
|
|
950
|
+
fableConsentPresent = 0;
|
|
864
951
|
}
|
|
865
952
|
await sleep(pollIntervalMs);
|
|
866
953
|
}
|
|
@@ -869,6 +956,7 @@ async function runWedgeWatchdog(opts) {
|
|
|
869
956
|
rateLimitFires,
|
|
870
957
|
overageCreditSelections,
|
|
871
958
|
confirmModalFires,
|
|
959
|
+
fableConsentFires,
|
|
872
960
|
permissionPromptFires,
|
|
873
961
|
permissionPromptDeferrals,
|
|
874
962
|
permissionPromptFloodHolds,
|
|
@@ -5628,7 +5716,7 @@ async function main() {
|
|
|
5628
5716
|
queryPendingPermission: permissionCardAware ? undefined : null,
|
|
5629
5717
|
floodPressure: permissionFloodAware ? undefined : null
|
|
5630
5718
|
});
|
|
5631
|
-
console.error(`[autoaccept-poll] ${agentName}: wedge-watchdog returned reason=${res.reason} fires=${res.fires} rateLimitFires=${res.rateLimitFires} overageCreditSelections=${res.overageCreditSelections} confirmModalFires=${res.confirmModalFires} permissionPromptFires=${res.permissionPromptFires} permissionPromptDeferrals=${res.permissionPromptDeferrals} permissionPromptFloodHolds=${res.permissionPromptFloodHolds} permissionPromptCardlessHolds=${res.permissionPromptCardlessHolds} restartEscalations=${res.restartEscalations}`);
|
|
5719
|
+
console.error(`[autoaccept-poll] ${agentName}: wedge-watchdog returned reason=${res.reason} fires=${res.fires} rateLimitFires=${res.rateLimitFires} overageCreditSelections=${res.overageCreditSelections} confirmModalFires=${res.confirmModalFires} fableConsentFires=${res.fableConsentFires} permissionPromptFires=${res.permissionPromptFires} permissionPromptDeferrals=${res.permissionPromptDeferrals} permissionPromptFloodHolds=${res.permissionPromptFloodHolds} permissionPromptCardlessHolds=${res.permissionPromptCardlessHolds} restartEscalations=${res.restartEscalations}`);
|
|
5632
5720
|
} catch (err) {
|
|
5633
5721
|
console.error(`[autoaccept-poll] ${agentName}: wedge-watchdog unexpected throw: ${err.message}`);
|
|
5634
5722
|
}
|
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.21.
|
|
2123
|
+
var VERSION = "0.21.7", COMMIT_SHA = "e4e50f84";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -31796,7 +31796,7 @@ ${baseAppend}` : TELEGRAM_FORMATTING_FLOOR_CARD;
|
|
|
31796
31796
|
}
|
|
31797
31797
|
}
|
|
31798
31798
|
const settingsPath = join12(agentDir, ".claude", "settings.json");
|
|
31799
|
-
if (existsSync18(settingsPath)) {
|
|
31799
|
+
if (existsSync18(settingsPath) && !options.skipNonCronWrites) {
|
|
31800
31800
|
const before = readFileSync16(settingsPath, "utf-8");
|
|
31801
31801
|
const settings = JSON.parse(before);
|
|
31802
31802
|
settings.permissions = settings.permissions ?? {};
|
|
@@ -31955,8 +31955,10 @@ ${body}
|
|
|
31955
31955
|
}
|
|
31956
31956
|
}
|
|
31957
31957
|
}
|
|
31958
|
-
|
|
31959
|
-
|
|
31958
|
+
if (!options.skipNonCronWrites) {
|
|
31959
|
+
refreshTelegramBotTokenEnv(agentDir, resolvedBotToken, changes);
|
|
31960
|
+
}
|
|
31961
|
+
if (agentConfig.skills && !options.skipNonCronWrites) {
|
|
31960
31962
|
syncGlobalSkills(agentDir, agentConfig.skills, switchroomConfig.switchroom.skills_dir);
|
|
31961
31963
|
}
|
|
31962
31964
|
if (!options.skipProfileTemplates) {
|
|
@@ -32027,14 +32029,16 @@ ${body}
|
|
|
32027
32029
|
const after = JSON.stringify(mcpJson, null, 2) + `
|
|
32028
32030
|
`;
|
|
32029
32031
|
const before = existsSync18(mcpJsonPath) ? readFileSync16(mcpJsonPath, "utf-8") : "";
|
|
32030
|
-
if (after !== before) {
|
|
32032
|
+
if (after !== before && !options.skipNonCronWrites) {
|
|
32031
32033
|
atomicWriteFileSync2(mcpJsonPath, after, 384);
|
|
32032
32034
|
changes.push(mcpJsonPath);
|
|
32033
32035
|
}
|
|
32034
32036
|
const cronMcp = maybeWriteCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
|
|
32035
32037
|
if (cronMcp)
|
|
32036
32038
|
changes.push(cronMcp);
|
|
32037
|
-
|
|
32039
|
+
if (!options.skipNonCronWrites) {
|
|
32040
|
+
ensureMcpServersTrusted(agentDir, Object.keys(mcpServers));
|
|
32041
|
+
}
|
|
32038
32042
|
}
|
|
32039
32043
|
const reconcileWorkspaceDir = join12(agentDir, "workspace");
|
|
32040
32044
|
if (!options.skipProfileTemplates) {
|
|
@@ -32093,7 +32097,7 @@ ${body}
|
|
|
32093
32097
|
}
|
|
32094
32098
|
const agentSoulPath = join12(agentDir, "SOUL.md");
|
|
32095
32099
|
const workspaceSoulPath = join12(agentDir, "workspace", "SOUL.md");
|
|
32096
|
-
if (existsSync18(workspaceSoulPath)) {
|
|
32100
|
+
if (existsSync18(workspaceSoulPath) && !options.skipNonCronWrites) {
|
|
32097
32101
|
if (existsSync18(agentSoulPath)) {
|
|
32098
32102
|
const stat = lstatSync4(agentSoulPath);
|
|
32099
32103
|
if (stat.isSymbolicLink()) {
|
|
@@ -34980,7 +34984,7 @@ function getAgentLogs(name, follow, tail, timestamps) {
|
|
|
34980
34984
|
});
|
|
34981
34985
|
}
|
|
34982
34986
|
function classifyChangeKind(path2) {
|
|
34983
|
-
if (/\/telegram\/cron-(?:\d+|[0-9a-f]{12})\.sh$/.test(path2))
|
|
34987
|
+
if (/\/telegram\/cron-(?:\d+|[0-9a-f]{12})\.(?:sh|source)$/.test(path2))
|
|
34984
34988
|
return "cron";
|
|
34985
34989
|
if (path2.includes("/.claude-cron/"))
|
|
34986
34990
|
return "cron";
|
|
@@ -124771,13 +124775,13 @@ function reconcileAgentCronOnly(agent) {
|
|
|
124771
124775
|
return { ok: false, error: `agent "${agent}" not in switchroom.yaml` };
|
|
124772
124776
|
}
|
|
124773
124777
|
const agentsDir = resolveAgentsDir(config);
|
|
124774
|
-
const result = reconcileAgent(agent, agentConfig, agentsDir, config.telegram, config, undefined, { skipProfileTemplates: true });
|
|
124778
|
+
const result = reconcileAgent(agent, agentConfig, agentsDir, config.telegram, config, undefined, { skipProfileTemplates: true, skipNonCronWrites: true });
|
|
124775
124779
|
const changes = [...result.changes];
|
|
124776
124780
|
const nonCron = changes.filter((p) => classifyChangeKind(p) !== "cron");
|
|
124777
124781
|
if (nonCron.length > 0) {
|
|
124778
124782
|
return {
|
|
124779
124783
|
ok: false,
|
|
124780
|
-
error: `non-cron changes surfaced during cron-only reconcile: ${nonCron.join(", ")}`
|
|
124784
|
+
error: `non-cron changes surfaced during cron-only reconcile ` + `(ungated writer \u2014 see ReconcileOptions.skipNonCronWrites): ${nonCron.join(", ")}`
|
|
124781
124785
|
};
|
|
124782
124786
|
}
|
|
124783
124787
|
const r = applyCronChangesHot(agent, changes);
|
|
@@ -21565,7 +21565,7 @@ function allocateAgentUid(name) {
|
|
|
21565
21565
|
}
|
|
21566
21566
|
|
|
21567
21567
|
// src/build-info.ts
|
|
21568
|
-
var VERSION = "0.21.
|
|
21568
|
+
var VERSION = "0.21.7";
|
|
21569
21569
|
|
|
21570
21570
|
// src/setup/hindsight-recall-passthrough.ts
|
|
21571
21571
|
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
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.21.
|
|
4
|
+
"version": "0.21.7",
|
|
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": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test:vitest": "vitest run",
|
|
29
29
|
"test:bun": "bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/hindsight-bank-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/self-approval-bypass.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/boot-briefing-builder.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/progress-fallback-cap.test.ts telegram-plugin/tests/progress-cap.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/worker-origin-gap-dispatch.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
30
30
|
"test:watch": "vitest",
|
|
31
|
-
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-bench-baseline-anonymised.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-hindsight-bank-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-ctx-send-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-foreign-db-readonly.mjs && node scripts/check-changelog-entry.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts && bun scripts/check-hostd-template-guard.ts",
|
|
31
|
+
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-bench-baseline-anonymised.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-hindsight-bank-hermeticity.mjs && node scripts/check-parked-turn-start-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-ctx-send-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-foreign-db-readonly.mjs && node scripts/check-changelog-entry.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts && bun scripts/check-hostd-template-guard.ts",
|
|
32
32
|
"lint:tsc": "tsc --noEmit",
|
|
33
33
|
"lint:hindsight-write-redaction": "node scripts/check-hindsight-write-redaction.mjs",
|
|
34
34
|
"lint:secret-pattern-parity": "bun scripts/check-secret-pattern-parity.ts",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"lint:auth-test-hermeticity": "node scripts/check-auth-test-hermeticity.mjs",
|
|
42
42
|
"lint:agent-state-dir-hermeticity": "node scripts/check-agent-state-dir-hermeticity.mjs",
|
|
43
43
|
"lint:hindsight-bank-hermeticity": "node scripts/check-hindsight-bank-hermeticity.mjs",
|
|
44
|
+
"lint:parked-turn-start-hermeticity": "node scripts/check-parked-turn-start-hermeticity.mjs",
|
|
44
45
|
"lint:web-subscription-honest": "node scripts/check-web-subscription-honest.mjs",
|
|
45
46
|
"lint:no-broadcast-delivery": "node scripts/check-no-broadcast-delivery.mjs",
|
|
46
47
|
"lint:gateway-line-ratchet": "node scripts/check-gateway-line-ratchet.mjs",
|
|
@@ -3,12 +3,20 @@
|
|
|
3
3
|
#
|
|
4
4
|
# bun reads the bunfig.toml in its CWD only, so this file exists purely to load
|
|
5
5
|
# the same shared-state hermeticity preloads as the repo-root bunfig.toml. Keep
|
|
6
|
-
# the two in sync; `npm run lint:agent-state-dir-hermeticity
|
|
7
|
-
# `npm run lint:hindsight-bank-hermeticity`
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# the two in sync; `npm run lint:agent-state-dir-hermeticity`,
|
|
7
|
+
# `npm run lint:hindsight-bank-hermeticity` and
|
|
8
|
+
# `npm run lint:parked-turn-start-hermeticity` fail if either bunfig stops
|
|
9
|
+
# loading any of the three guards. Rationale lives in
|
|
10
|
+
# tests/vitest-setup/agent-state-dir-guard.mjs,
|
|
11
|
+
# tests/vitest-setup/hindsight-bank-guard.mjs and
|
|
12
|
+
# tests/vitest-setup/parked-turn-start-guard.mjs.
|
|
13
|
+
#
|
|
14
|
+
# This is the bunfig CI's full plugin sweep reads, so the parked-turn-start
|
|
15
|
+
# guard's blast radius is exactly the ~657-file one-process run that #4611
|
|
16
|
+
# poisoned.
|
|
10
17
|
[test]
|
|
11
18
|
preload = [
|
|
12
19
|
"../tests/vitest-setup/agent-state-dir-guard.mjs",
|
|
13
20
|
"../tests/vitest-setup/hindsight-bank-guard.mjs",
|
|
21
|
+
"../tests/vitest-setup/parked-turn-start-guard.mjs",
|
|
14
22
|
]
|
|
@@ -104467,10 +104467,10 @@ function startOutboxSweep(deps) {
|
|
|
104467
104467
|
}
|
|
104468
104468
|
|
|
104469
104469
|
// ../src/build-info.ts
|
|
104470
|
-
var VERSION2 = "0.21.
|
|
104471
|
-
var COMMIT_SHA = "
|
|
104472
|
-
var COMMIT_DATE = "2026-08-
|
|
104473
|
-
var LATEST_PR =
|
|
104470
|
+
var VERSION2 = "0.21.7";
|
|
104471
|
+
var COMMIT_SHA = "e4e50f84";
|
|
104472
|
+
var COMMIT_DATE = "2026-08-11T23:08:27Z";
|
|
104473
|
+
var LATEST_PR = 4613;
|
|
104474
104474
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
104475
104475
|
|
|
104476
104476
|
// gateway/boot-version.ts
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* parked store is emptied AND the parked message was handed back into turn
|
|
24
24
|
* processing (its turn begun, its card opened) rather than silently dropped.
|
|
25
25
|
*/
|
|
26
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
26
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
27
27
|
import {
|
|
28
28
|
handleSessionEvent,
|
|
29
29
|
drainParkedTurnStartsForChat,
|
|
@@ -34,9 +34,15 @@ import { buildSilencePokeOptions } from '../gateway/liveness-wiring.js'
|
|
|
34
34
|
import { makeLivenessFixture, makeTurn, statusKeyForTests } from './helpers/liveness-wiring-fixture.js'
|
|
35
35
|
import { CHAT, enqueue, makeHarness } from './turn-mint-harness.js'
|
|
36
36
|
|
|
37
|
+
// Module-scope store + one-process `bun test` sweep: resetting on ENTRY alone
|
|
38
|
+
// leaves a mid-park case's entry behind for every later FILE, which makes the
|
|
39
|
+
// obligation sweep read the session as busy for the rest of the run (#4611).
|
|
37
40
|
beforeEach(() => {
|
|
38
41
|
__resetParkedTurnStartsForTest()
|
|
39
42
|
})
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
__resetParkedTurnStartsForTest()
|
|
45
|
+
})
|
|
40
46
|
|
|
41
47
|
describe('silence-fallback drains parkedTurnStarts so a hung REPL cannot wedge the park gate', () => {
|
|
42
48
|
it('empties the parked store AND redelivers the parked message into turn processing', async () => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
handleSessionEvent,
|
|
4
|
+
__parkedTurnStartCountForTest,
|
|
5
|
+
__resetParkedTurnStartsForTest,
|
|
6
|
+
} from '../gateway/stream-render.js'
|
|
7
|
+
import { enqueue, makeHarness } from './turn-mint-harness.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runtime alarm for the parked-turn-start leak guard (#4611).
|
|
11
|
+
*
|
|
12
|
+
* `parkedTurnStarts` is module-scope by design (one CLI session, one queue) and
|
|
13
|
+
* `bun test` runs all ~657 telegram-plugin files in ONE process, so a file that
|
|
14
|
+
* exits mid-park changes global state for every file after it:
|
|
15
|
+
* `gateway/obligation-wiring.ts` folds the parked count into `sessionBusy`, and
|
|
16
|
+
* `tests/represent-guard.test.ts`'s idle case then asserts `toHaveLength(1)`
|
|
17
|
+
* against `0`. Two PRs were ejected from the merge queue that way, with
|
|
18
|
+
* byte-identical retries passing, because bun's file order is not stable.
|
|
19
|
+
*
|
|
20
|
+
* `npm run lint:parked-turn-start-hermeticity` pins the WIRING statically; this
|
|
21
|
+
* pins the EFFECT, so a bunfig that is present but no longer loading the guard
|
|
22
|
+
* (wrong relative path, bun config-discovery change) fails a test rather than
|
|
23
|
+
* silently un-protecting the runner.
|
|
24
|
+
*/
|
|
25
|
+
// This suite deliberately parks, so it follows the discipline the guard
|
|
26
|
+
// enforces for every other suite: reset on EXIT, not just on entry.
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
__resetParkedTurnStartsForTest()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('bun test runs with the parked-turn-start leak guard installed', () => {
|
|
32
|
+
it('the preload registered the global afterEach', () => {
|
|
33
|
+
const installed = (globalThis as { __switchroomParkedTurnStartGuard?: { hook: () => void } })
|
|
34
|
+
.__switchroomParkedTurnStartGuard
|
|
35
|
+
expect(
|
|
36
|
+
installed,
|
|
37
|
+
'guard absent — bunfig.toml `[test] preload` did not load parked-turn-start-guard.mjs',
|
|
38
|
+
).toBeTruthy()
|
|
39
|
+
expect(typeof installed!.hook).toBe('function')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('the hook throws on a leaked entry AND resets the store (one failure, not a cascade)', () => {
|
|
43
|
+
const { hook } = (globalThis as { __switchroomParkedTurnStartGuard: { hook: () => void } })
|
|
44
|
+
.__switchroomParkedTurnStartGuard
|
|
45
|
+
|
|
46
|
+
// Reproduce the #4611 leak shape: a mid-turn enqueue parks behind a live
|
|
47
|
+
// turn and is never dequeued — exactly queued-card-surface.test.ts:318.
|
|
48
|
+
const h = makeHarness()
|
|
49
|
+
handleSessionEvent(h.deps, enqueue('501'))
|
|
50
|
+
handleSessionEvent(h.deps, enqueue('502', 'real mid-turn message'))
|
|
51
|
+
expect(__parkedTurnStartCountForTest()).toBe(1)
|
|
52
|
+
|
|
53
|
+
expect(() => hook()).toThrow(/SWITCHROOM_PARKED_TURN_START_LEAK/)
|
|
54
|
+
// Reset-before-throw is what turns the ~1800-deep victim cascade into one
|
|
55
|
+
// attributable failure — and is why this test's own leak does not escape.
|
|
56
|
+
expect(__parkedTurnStartCountForTest()).toBe(0)
|
|
57
|
+
|
|
58
|
+
// A clean store is silent.
|
|
59
|
+
expect(() => hook()).not.toThrow()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('leaves the store clean for the next file', () => {
|
|
63
|
+
expect(__parkedTurnStartCountForTest()).toBe(0)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* • remove → the card is finalized as "folded into the current task".
|
|
15
15
|
* • TTL → the card is finalized as timed-out, never left frozen.
|
|
16
16
|
*/
|
|
17
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
17
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
18
18
|
import {
|
|
19
19
|
handleSessionEvent,
|
|
20
20
|
__resetParkedTurnStartsForTest,
|
|
@@ -58,9 +58,17 @@ function withRecordingBot(h: Harness) {
|
|
|
58
58
|
* stores the card id on the parked envelope) has run. */
|
|
59
59
|
const settle = () => new Promise((r) => setTimeout(r, 0))
|
|
60
60
|
|
|
61
|
+
// The parked store is module-scope and `bun test` runs all ~657 files in ONE
|
|
62
|
+
// process, so resetting on ENTRY alone is not enough: a case that ends mid-park
|
|
63
|
+
// leaves the entry behind for every later FILE. #4611 — this suite's last case
|
|
64
|
+
// parks msg 502 and never dequeues, and the leftover made the obligation sweep
|
|
65
|
+
// read the session as busy for the rest of the run, failing represent-guard.
|
|
61
66
|
beforeEach(() => {
|
|
62
67
|
__resetParkedTurnStartsForTest()
|
|
63
68
|
})
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
__resetParkedTurnStartsForTest()
|
|
71
|
+
})
|
|
64
72
|
|
|
65
73
|
describe('Part B — queued card is posted at park and adopted on dequeue', () => {
|
|
66
74
|
it('parks with a reply-anchored "Queued" card, then EDITS that same card in place on dequeue', async () => {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* the duplicate. This is the exact duplicate-reply class the shared-singleton
|
|
20
20
|
* injection (never a re-`new`) exists to kill.
|
|
21
21
|
*/
|
|
22
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
22
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
23
23
|
import { readFileSync } from 'node:fs'
|
|
24
24
|
import { tmpdir } from 'node:os'
|
|
25
25
|
import {
|
|
@@ -47,6 +47,15 @@ import {
|
|
|
47
47
|
import type { CurrentTurn } from '../gateway/gateway.js'
|
|
48
48
|
import type { ReplyOwnerTier } from '../reply-owner-resolve.js'
|
|
49
49
|
|
|
50
|
+
// The parked turn-start store is module-scope and `bun test` runs every file in
|
|
51
|
+
// ONE process, so the two describe-scoped `beforeEach` resets below clean ENTRY
|
|
52
|
+
// only — a case that ends mid-park still leaks into the next FILE, where the
|
|
53
|
+
// obligation sweep reads the leftover as a busy session (#4611). File-level so
|
|
54
|
+
// it covers every case here, not just the two blocks that reset on entry.
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
__resetParkedTurnStartsForTest()
|
|
57
|
+
})
|
|
58
|
+
|
|
50
59
|
/** The owner-resolution shape `resolveReplyOwnerTurn` returns, including the
|
|
51
60
|
* candidate set the content-gate bypass corroborates against. These fixtures
|
|
52
61
|
* never exercise the supersede path, so the candidates mirror the resolved turn
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* These drive the REAL `handleSessionEvent` (extracted-module golden-harness
|
|
21
21
|
* oracle, same standard as `stream-render-golden.test.ts`).
|
|
22
22
|
*/
|
|
23
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
23
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
24
24
|
import {
|
|
25
25
|
handleSessionEvent,
|
|
26
26
|
__resetParkedTurnStartsForTest,
|
|
@@ -29,9 +29,15 @@ import {
|
|
|
29
29
|
import { projectTranscriptLine } from '../session-tail.js'
|
|
30
30
|
import { CHAT, enqueue, inbound, makeHarness } from './turn-mint-harness.js'
|
|
31
31
|
|
|
32
|
+
// Module-scope store + one-process `bun test` sweep: resetting on ENTRY alone
|
|
33
|
+
// leaves a mid-park case's entry behind for every later FILE, which makes the
|
|
34
|
+
// obligation sweep read the session as busy for the rest of the run (#4611).
|
|
32
35
|
beforeEach(() => {
|
|
33
36
|
__resetParkedTurnStartsForTest()
|
|
34
37
|
})
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
__resetParkedTurnStartsForTest()
|
|
40
|
+
})
|
|
35
41
|
|
|
36
42
|
describe('#3927 FIX A — a mid-turn enqueue parks; the turn mints on dequeue', () => {
|
|
37
43
|
it('a second enqueue while turn A is live opens NO card, does not steal the slot, ' +
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* mints straight on top of it. That is the exact orphan carrie hit, and it is
|
|
26
26
|
* what FIX B finalizes.
|
|
27
27
|
*/
|
|
28
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
28
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
29
29
|
import {
|
|
30
30
|
handleSessionEvent,
|
|
31
31
|
__resetParkedTurnStartsForTest,
|
|
@@ -45,9 +45,15 @@ function syntheticEnqueue(chatId: string, text: string) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Module-scope store + one-process `bun test` sweep: resetting on ENTRY alone
|
|
49
|
+
// leaves a mid-park case's entry behind for every later FILE, which makes the
|
|
50
|
+
// obligation sweep read the session as busy for the rest of the run (#4611).
|
|
48
51
|
beforeEach(() => {
|
|
49
52
|
__resetParkedTurnStartsForTest()
|
|
50
53
|
})
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
__resetParkedTurnStartsForTest()
|
|
56
|
+
})
|
|
51
57
|
|
|
52
58
|
describe('#3927 FIX B — superseding a live turn finalizes its card', () => {
|
|
53
59
|
it('a dequeue-driven mint on top of a never-ended turn calls clearActivitySummary ' +
|