switchroom 0.13.27 → 0.13.29
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
CHANGED
|
@@ -47436,8 +47436,8 @@ var {
|
|
|
47436
47436
|
} = import__.default;
|
|
47437
47437
|
|
|
47438
47438
|
// src/build-info.ts
|
|
47439
|
-
var VERSION = "0.13.
|
|
47440
|
-
var COMMIT_SHA = "
|
|
47439
|
+
var VERSION = "0.13.29";
|
|
47440
|
+
var COMMIT_SHA = "927abe08";
|
|
47441
47441
|
|
|
47442
47442
|
// src/cli/agent.ts
|
|
47443
47443
|
init_source();
|
|
@@ -61793,30 +61793,44 @@ Push passphrase to broker for future requests? [Y/n]: `);
|
|
|
61793
61793
|
vault.command("list").description("List all secret key names in the vault").action(async () => {
|
|
61794
61794
|
try {
|
|
61795
61795
|
const parentOpts = program3.opts();
|
|
61796
|
-
|
|
61797
|
-
|
|
61798
|
-
|
|
61799
|
-
|
|
61800
|
-
|
|
61801
|
-
|
|
61802
|
-
|
|
61803
|
-
|
|
61804
|
-
|
|
61805
|
-
|
|
61806
|
-
|
|
61807
|
-
|
|
61808
|
-
|
|
61809
|
-
|
|
61796
|
+
const inSandbox = isSandboxContext();
|
|
61797
|
+
let brokerSocket;
|
|
61798
|
+
try {
|
|
61799
|
+
const config = loadConfig(parentOpts.config);
|
|
61800
|
+
brokerSocket = resolveBrokerSocketPath({
|
|
61801
|
+
vaultBrokerSocket: config.vault?.broker?.socket ? resolvePath(config.vault.broker.socket) : undefined
|
|
61802
|
+
});
|
|
61803
|
+
} catch {
|
|
61804
|
+
brokerSocket = resolveBrokerSocketPath();
|
|
61805
|
+
}
|
|
61806
|
+
const brokerOpts = { socket: brokerSocket };
|
|
61807
|
+
const status = inSandbox ? null : await statusViaBroker(brokerOpts);
|
|
61808
|
+
if (inSandbox || status !== null) {
|
|
61809
|
+
if (status !== null && !status.unlocked) {
|
|
61810
|
+
if (inSandbox) {
|
|
61811
|
+
process.stderr.write(`VAULT-BROKER-DENIED: broker locked.
|
|
61812
|
+
` + `${recoveryHint("locked")}
|
|
61810
61813
|
`);
|
|
61811
|
-
|
|
61812
|
-
|
|
61813
|
-
if (
|
|
61814
|
-
|
|
61815
|
-
|
|
61816
|
-
|
|
61817
|
-
|
|
61814
|
+
process.exit(3);
|
|
61815
|
+
}
|
|
61816
|
+
} else if (status !== null || inSandbox) {
|
|
61817
|
+
const keys2 = await listViaBroker(brokerOpts);
|
|
61818
|
+
if (keys2 === null) {
|
|
61819
|
+
if (inSandbox) {
|
|
61820
|
+
process.stderr.write(`VAULT-BROKER-UNREACHABLE: cannot reach vault broker; ` + `'switchroom vault list' from a sandbox requires a live broker.
|
|
61821
|
+
`);
|
|
61822
|
+
process.exit(VAULT_EXIT_BROKER_UNREACHABLE);
|
|
61823
|
+
}
|
|
61824
|
+
} else {
|
|
61825
|
+
if (keys2.length === 0) {
|
|
61826
|
+
console.log(source_default.dim("No secrets in vault"));
|
|
61827
|
+
} else {
|
|
61828
|
+
for (const key of keys2)
|
|
61829
|
+
console.log(key);
|
|
61830
|
+
}
|
|
61831
|
+
return;
|
|
61832
|
+
}
|
|
61818
61833
|
}
|
|
61819
|
-
return;
|
|
61820
61834
|
}
|
|
61821
61835
|
const vaultPath = getVaultPath4(parentOpts.config);
|
|
61822
61836
|
const passphrase = await getPassphrase();
|
package/package.json
CHANGED
|
@@ -48464,10 +48464,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
48464
48464
|
}
|
|
48465
48465
|
|
|
48466
48466
|
// ../src/build-info.ts
|
|
48467
|
-
var VERSION = "0.13.
|
|
48468
|
-
var COMMIT_SHA = "
|
|
48469
|
-
var COMMIT_DATE = "2026-05-
|
|
48470
|
-
var LATEST_PR =
|
|
48467
|
+
var VERSION = "0.13.29";
|
|
48468
|
+
var COMMIT_SHA = "927abe08";
|
|
48469
|
+
var COMMIT_DATE = "2026-05-24T12:14:05Z";
|
|
48470
|
+
var LATEST_PR = 1732;
|
|
48471
48471
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
48472
48472
|
|
|
48473
48473
|
// gateway/boot-version.ts
|
|
@@ -51326,6 +51326,7 @@ ${url}`;
|
|
|
51326
51326
|
noteSignal(statusKey(chat_id, threadId), Date.now());
|
|
51327
51327
|
if (turn != null && isFinalAnswerReply({ text: rawText, disableNotification })) {
|
|
51328
51328
|
turn.finalAnswerDelivered = true;
|
|
51329
|
+
finalizeStatusReaction(chat_id, threadId, "done");
|
|
51329
51330
|
}
|
|
51330
51331
|
}
|
|
51331
51332
|
process.stderr.write(`telegram channel: reply: finalized chatId=${chat_id} messageIds=[${sentIds.join(",")}] chunks=${chunks.length}
|
|
@@ -4931,26 +4931,49 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
4931
4931
|
} catch { /* best-effort signal */ }
|
|
4932
4932
|
// #203: fresh sendMessage from reply tool is a user-visible signal.
|
|
4933
4933
|
signalTracker.noteSignal(statusKey(chat_id, threadId), Date.now())
|
|
4934
|
-
// #1713: the reply tool is a NON-EVENT for the status reaction
|
|
4935
|
-
// The reaction reflects current turn
|
|
4936
|
-
//
|
|
4937
|
-
//
|
|
4938
|
-
//
|
|
4939
|
-
//
|
|
4940
|
-
//
|
|
4941
|
-
//
|
|
4942
|
-
//
|
|
4943
|
-
//
|
|
4944
|
-
//
|
|
4945
|
-
//
|
|
4946
|
-
//
|
|
4947
|
-
//
|
|
4948
|
-
//
|
|
4949
|
-
//
|
|
4950
|
-
//
|
|
4951
|
-
//
|
|
4934
|
+
// #1713: the reply tool is a NON-EVENT for the status reaction
|
|
4935
|
+
// WHEN IT'S AN INTERIM ACK. The reaction reflects current turn
|
|
4936
|
+
// activity, not delivery state — interim acks must not collapse
|
|
4937
|
+
// the working-state ladder to 👍.
|
|
4938
|
+
//
|
|
4939
|
+
// #1728 carve-out (2026-05-24): when this reply IS the final
|
|
4940
|
+
// answer (`isFinalAnswerReply` returns true — same classifier
|
|
4941
|
+
// #1664 uses for silent-end re-prompt gating), it IS effectively
|
|
4942
|
+
// turn-end and we MUST finalize here. Rationale: Claude Code's
|
|
4943
|
+
// `turn_duration` system event is unreliable for the trivial-
|
|
4944
|
+
// prompt happy path (driver sends "what's 2+2", model replies
|
|
4945
|
+
// "4", no `turn_duration` ever lands in the JSONL session tail).
|
|
4946
|
+
// Pre-#1718 this wedge was masked by the legacy
|
|
4947
|
+
// `endStatusReaction` shim running unconditionally on every
|
|
4948
|
+
// reply (outcome='done'); #1718 removed that call site
|
|
4949
|
+
// intending `turn_end` to be the sole terminal trigger. The
|
|
4950
|
+
// contract was right in spirit but `turn_end` doesn't fire 100%
|
|
4951
|
+
// of the time, so the buffer gate (activeTurnStartedAt) stays
|
|
4952
|
+
// set forever and every subsequent inbound gets `held mid-turn`
|
|
4953
|
+
// and never delivered. v0.13.27 shipped + reverted on this
|
|
4954
|
+
// failure mode (#1728).
|
|
4955
|
+
//
|
|
4956
|
+
// Net contract:
|
|
4957
|
+
// - interim ack reply (isFinalAnswerReply === false)
|
|
4958
|
+
// → non-event, no reaction finalize, buffer gate stays
|
|
4959
|
+
// - final-answer reply (isFinalAnswerReply === true)
|
|
4960
|
+
// → finalize reaction (debounced 👍) + release buffer
|
|
4961
|
+
// gate via purgeReactionTracking (called inside
|
|
4962
|
+
// finalizeStatusReaction). currentTurn stays alive so
|
|
4963
|
+
// a subsequent `turn_end` still cleans up its share
|
|
4964
|
+
// idempotently.
|
|
4965
|
+
//
|
|
4966
|
+
// #1664 — `turn.finalAnswerDelivered = true` keeps the silent-
|
|
4967
|
+
// end re-prompt from spuriously firing on a delivered final.
|
|
4952
4968
|
if (turn != null && isFinalAnswerReply({ text: rawText, disableNotification })) {
|
|
4953
4969
|
turn.finalAnswerDelivered = true
|
|
4970
|
+
// #1728: release the buffer gate + emit terminal 👍. Mid-turn
|
|
4971
|
+
// acks bypass this branch and remain non-events for the
|
|
4972
|
+
// reaction (preserves #1713). The full turn-state teardown
|
|
4973
|
+
// (nulling `currentTurn`, the per-turn cleanup) still runs in
|
|
4974
|
+
// the `turn_end` handler when it lands; this only fires the
|
|
4975
|
+
// observable side effects that #1718 deferred unconditionally.
|
|
4976
|
+
finalizeStatusReaction(chat_id, threadId, 'done')
|
|
4954
4977
|
}
|
|
4955
4978
|
}
|
|
4956
4979
|
|
|
@@ -1,39 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* #1713 —
|
|
2
|
+
* #1713 + #1728 — interim-ack `reply` tool is a non-event for the
|
|
3
|
+
* status reaction; final-answer `reply` finalizes.
|
|
3
4
|
*
|
|
4
|
-
* History.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* History.
|
|
6
|
+
* - PR #602 follow-up wired `executeReply` to fire the terminal 👍
|
|
7
|
+
* after at least one chunk landed.
|
|
8
|
+
* - #1713 (#1718) reverted that: reaction reflects current turn
|
|
9
|
+
* activity, not delivery state. Made `turn_end` the sole terminal
|
|
10
|
+
* trigger so mid-turn ACK replies don't collapse the working-state
|
|
11
|
+
* ladder to 👍.
|
|
12
|
+
* - #1728 (this fix) carve-out: Claude Code's `turn_duration` system
|
|
13
|
+
* event is unreliable for the trivial-prompt happy path, leaving
|
|
14
|
+
* `activeTurnStartedAt` set forever and every subsequent inbound
|
|
15
|
+
* stuck "held mid-turn" (the v0.13.27 wedge). When the reply IS
|
|
16
|
+
* the final answer (`isFinalAnswerReply` returns true), executeReply
|
|
17
|
+
* calls `finalizeStatusReaction` to release the buffer gate and
|
|
18
|
+
* emit the (debounced 3500ms) terminal 👍. Interim acks bypass this
|
|
19
|
+
* branch and remain non-events for the reaction.
|
|
10
20
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
21
|
+
* Net contract pinned here:
|
|
22
|
+
* - executeReply post-send block must NOT call the legacy
|
|
23
|
+
* `endStatusReaction('done')` (the pre-#1713 bug class).
|
|
24
|
+
* - executeReply MUST call `finalizeStatusReaction` gated on
|
|
25
|
+
* `isFinalAnswerReply` so the buffer gate releases on final answer.
|
|
15
26
|
*
|
|
16
27
|
* The gateway IIFE / executeReply body are too entangled to import
|
|
17
|
-
* directly, so we
|
|
18
|
-
* regresses (re-adds a terminal-reaction call), the inline review
|
|
19
|
-
* comment guarding `if (sentIds.length > 0)` and this test should both
|
|
20
|
-
* catch it.
|
|
28
|
+
* directly, so we do source-level assertions.
|
|
21
29
|
*/
|
|
22
30
|
import { describe, it, expect, vi } from 'vitest'
|
|
23
31
|
|
|
24
|
-
describe('#1713 —
|
|
25
|
-
it('executeReply post-send block does NOT call endStatusReaction', () => {
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
32
|
+
describe('#1713 + #1728 — reply tool reaction contract', () => {
|
|
33
|
+
it('executeReply post-send block does NOT call legacy endStatusReaction', () => {
|
|
34
|
+
// The pre-#1713 bug class was `endStatusReaction(chat_id, threadId,
|
|
35
|
+
// 'done')` firing 👍 unconditionally on every reply (including
|
|
36
|
+
// mid-turn acks). That call must stay removed.
|
|
29
37
|
//
|
|
30
38
|
// We do a coarse-grained source-level check rather than a unit
|
|
31
39
|
// test of a copied helper. If/when the executeReply body is
|
|
32
40
|
// extracted into its own function this can become a proper unit
|
|
33
41
|
// test; until then the source-level guard is the safest pin.
|
|
34
|
-
//
|
|
35
|
-
// The intent: a future commit that re-adds the call (regressing
|
|
36
|
-
// #1713) will trip this assertion.
|
|
37
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
43
|
const fs = require('node:fs') as typeof import('node:fs')
|
|
39
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -53,6 +58,44 @@ describe('#1713 — plain reply tool is a non-event for the reaction', () => {
|
|
|
53
58
|
expect(slice).not.toMatch(/endStatusReaction\([^)]*'done'\)/)
|
|
54
59
|
})
|
|
55
60
|
|
|
61
|
+
it('executeReply post-send block DOES call finalizeStatusReaction gated on isFinalAnswerReply (#1728 wedge fix)', () => {
|
|
62
|
+
// #1728 — the v0.13.27 wedge: `turn_duration` system events from
|
|
63
|
+
// Claude Code don't reliably land for trivial-prompt turns, so
|
|
64
|
+
// activeTurnStartedAt never clears and every subsequent inbound
|
|
65
|
+
// gets held mid-turn. The fix: when executeReply detects a final-
|
|
66
|
+
// answer reply (the same `isFinalAnswerReply` classifier #1664
|
|
67
|
+
// uses for silent-end re-prompt gating), trigger
|
|
68
|
+
// `finalizeStatusReaction` to release the buffer gate. Interim
|
|
69
|
+
// acks (isFinalAnswerReply === false) MUST bypass this branch and
|
|
70
|
+
// remain non-events for the reaction (preserves #1713).
|
|
71
|
+
//
|
|
72
|
+
// If a future commit removes the finalizeStatusReaction call or
|
|
73
|
+
// un-gates it from isFinalAnswerReply, the wedge returns.
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
75
|
+
const fs = require('node:fs') as typeof import('node:fs')
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
77
|
+
const path = require('node:path') as typeof import('node:path')
|
|
78
|
+
const src = fs.readFileSync(
|
|
79
|
+
path.resolve(__dirname, '../gateway/gateway.ts'),
|
|
80
|
+
'utf8',
|
|
81
|
+
)
|
|
82
|
+
const anchor = src.indexOf("fresh sendMessage from reply tool is a user-visible")
|
|
83
|
+
expect(anchor).toBeGreaterThan(-1)
|
|
84
|
+
const slice = src.slice(anchor, anchor + 3000)
|
|
85
|
+
// The finalize MUST appear in the post-send block.
|
|
86
|
+
expect(slice).toMatch(/finalizeStatusReaction\(/)
|
|
87
|
+
// It MUST be gated by isFinalAnswerReply (the classifier prevents
|
|
88
|
+
// interim acks from firing 👍, which would regress #1713).
|
|
89
|
+
expect(slice).toMatch(/isFinalAnswerReply\(/)
|
|
90
|
+
// Sanity: the finalize MUST appear AFTER the isFinalAnswerReply
|
|
91
|
+
// check (i.e. inside the gated branch), not as a sibling that
|
|
92
|
+
// fires unconditionally.
|
|
93
|
+
const gateIdx = slice.indexOf('isFinalAnswerReply(')
|
|
94
|
+
const finalizeIdx = slice.indexOf('finalizeStatusReaction(')
|
|
95
|
+
expect(gateIdx).toBeGreaterThan(-1)
|
|
96
|
+
expect(finalizeIdx).toBeGreaterThan(gateIdx)
|
|
97
|
+
})
|
|
98
|
+
|
|
56
99
|
it('reply tool deps no longer wire a status-reaction terminal callback', () => {
|
|
57
100
|
// Post-#1713 the stream-reply-handler has no call site for
|
|
58
101
|
// `deps.endStatusReaction`. Post follow-up cleanup, the dep itself
|