switchroom 0.13.59 → 0.13.61
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
|
@@ -49057,8 +49057,8 @@ var {
|
|
|
49057
49057
|
} = import__.default;
|
|
49058
49058
|
|
|
49059
49059
|
// src/build-info.ts
|
|
49060
|
-
var VERSION = "0.13.
|
|
49061
|
-
var COMMIT_SHA = "
|
|
49060
|
+
var VERSION = "0.13.61";
|
|
49061
|
+
var COMMIT_SHA = "43241ade";
|
|
49062
49062
|
|
|
49063
49063
|
// src/cli/agent.ts
|
|
49064
49064
|
init_source();
|
|
@@ -51271,7 +51271,18 @@ function buildSettingsHooksBlock(p) {
|
|
|
51271
51271
|
}
|
|
51272
51272
|
] : [];
|
|
51273
51273
|
const useHotReloadStable = agentConfig.channels?.telegram?.hotReloadStable === true;
|
|
51274
|
-
const turnPacingDirective = "<turn-pacing>You are messaging a human
|
|
51274
|
+
const turnPacingDirective = "<turn-pacing>You are messaging a human via Telegram. The framework " + "automatically shows the user a live preview in their compose area as " + 'you work \u2014 they see "Read a file", "Ran 2 commands", etc. as your ' + `tool_use events stream. You do NOT need to ack manually.
|
|
51275
|
+
|
|
51276
|
+
` + 'Do NOT call the reply tool with placeholder acks like "on it", ' + '"good question \u2014 one sec", "let me dig in", "checking now", etc. ' + "Those add chat clutter on top of the activity preview the user is " + "already seeing. The activity preview clears the moment you send a " + `real reply.
|
|
51277
|
+
|
|
51278
|
+
` + `Call reply only when you have something substantive to deliver:
|
|
51279
|
+
` + ` - The actual answer (any length \u2014 short or long)
|
|
51280
|
+
` + ` - A genuine question back to the user
|
|
51281
|
+
` + " - A real mid-work milestone or pivot that changes what the user " + 'should expect (e.g. "halfway through \u2014 found an unexpected issue, ' + `want me to continue?"). Not "still working".
|
|
51282
|
+
|
|
51283
|
+
` + "For trivial one-sentence answers: just reply with the answer. The " + `reply IS the answer, not an ack.
|
|
51284
|
+
|
|
51285
|
+
` + "For complex tool-driven work: go straight to the tools. The compose-" + "area preview is the ambient liveness signal. Reply once you have " + "the answer or a real reason to break in.</turn-pacing>";
|
|
51275
51286
|
const switchroomUserPromptSubmit = [
|
|
51276
51287
|
...useHotReloadStable ? [
|
|
51277
51288
|
{
|
package/package.json
CHANGED
|
@@ -49688,10 +49688,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
49688
49688
|
}
|
|
49689
49689
|
|
|
49690
49690
|
// ../src/build-info.ts
|
|
49691
|
-
var VERSION = "0.13.
|
|
49692
|
-
var COMMIT_SHA = "
|
|
49693
|
-
var COMMIT_DATE = "2026-05-
|
|
49694
|
-
var LATEST_PR =
|
|
49691
|
+
var VERSION = "0.13.61";
|
|
49692
|
+
var COMMIT_SHA = "43241ade";
|
|
49693
|
+
var COMMIT_DATE = "2026-05-28T01:27:29Z";
|
|
49694
|
+
var LATEST_PR = 1940;
|
|
49695
49695
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
49696
49696
|
|
|
49697
49697
|
// gateway/boot-version.ts
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// src/cli/ack-first-pretool.ts
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
var REPLY_TOOL_NAMES = new Set([
|
|
5
|
-
"mcp__switchroom-telegram__reply",
|
|
6
|
-
"mcp__switchroom-telegram__stream_reply"
|
|
7
|
-
]);
|
|
8
|
-
var ACK_SENT_MARKER = "ack-sent.flag";
|
|
9
|
-
function decide(input, stateDir) {
|
|
10
|
-
const toolName = input.tool_name ?? "";
|
|
11
|
-
if (toolName === "") {
|
|
12
|
-
return { decision: "allow" };
|
|
13
|
-
}
|
|
14
|
-
if (REPLY_TOOL_NAMES.has(toolName)) {
|
|
15
|
-
return { decision: "allow" };
|
|
16
|
-
}
|
|
17
|
-
if (!stateDir) {
|
|
18
|
-
return { decision: "allow" };
|
|
19
|
-
}
|
|
20
|
-
const markerPath = join(stateDir, ACK_SENT_MARKER);
|
|
21
|
-
let ackAlreadySent = false;
|
|
22
|
-
try {
|
|
23
|
-
ackAlreadySent = existsSync(markerPath);
|
|
24
|
-
} catch {
|
|
25
|
-
return { decision: "allow" };
|
|
26
|
-
}
|
|
27
|
-
if (ackAlreadySent) {
|
|
28
|
-
return { decision: "allow" };
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
decision: "block",
|
|
32
|
-
reason: "First call `mcp__switchroom-telegram__reply` with a brief one-line " + "acknowledgement in your persona's voice before using any other tool \u2014 " + 'e.g. "on it \u2014 checking", "good question \u2014 one sec", "let me dig in". ' + "This is the framework's enforcement of the human-feel design " + "(`reference/conversational-pacing.md` beat 1): a colleague answers " + "in a beat. The reply can be the entire short answer if you have one " + "ready (then no further work needed); otherwise it's a brief status " + "and you continue with the work after."
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
async function runHook() {
|
|
36
|
-
if (process.env.SWITCHROOM_DISABLE_ACK_FIRST_GATE === "1") {
|
|
37
|
-
process.exit(0);
|
|
38
|
-
}
|
|
39
|
-
const stdin = await readStdin();
|
|
40
|
-
let input;
|
|
41
|
-
try {
|
|
42
|
-
input = JSON.parse(stdin);
|
|
43
|
-
} catch {
|
|
44
|
-
process.exit(0);
|
|
45
|
-
}
|
|
46
|
-
const stateDir = process.env.TELEGRAM_STATE_DIR;
|
|
47
|
-
const decision = decide(input, stateDir);
|
|
48
|
-
if (decision.decision === "block") {
|
|
49
|
-
process.stdout.write(JSON.stringify({ decision: "block", reason: decision.reason }) + `
|
|
50
|
-
`);
|
|
51
|
-
}
|
|
52
|
-
process.exit(0);
|
|
53
|
-
}
|
|
54
|
-
async function readStdin() {
|
|
55
|
-
return new Promise((resolve, reject) => {
|
|
56
|
-
const chunks = [];
|
|
57
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
58
|
-
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
59
|
-
process.stdin.on("error", reject);
|
|
60
|
-
if (process.stdin.isTTY)
|
|
61
|
-
resolve("");
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
if (typeof process !== "undefined" && process.argv?.[1]?.endsWith("ack-first-pretool.mjs")) {
|
|
65
|
-
runHook().catch((err) => {
|
|
66
|
-
process.stderr.write(`ack-first-pretool: hook failed unexpectedly: ${err}
|
|
67
|
-
`);
|
|
68
|
-
process.exit(0);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
export {
|
|
72
|
-
runHook,
|
|
73
|
-
decide,
|
|
74
|
-
ACK_SENT_MARKER
|
|
75
|
-
};
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ack-first state flag — load-bearing for the ack-first-pretool hook
|
|
3
|
-
* (see `src/cli/ack-first-pretool.ts` and
|
|
4
|
-
* `reference/conversational-pacing.md` beat 1).
|
|
5
|
-
*
|
|
6
|
-
* The gateway is the source of truth for "has the model called reply
|
|
7
|
-
* yet this turn?". The PreToolUse hook runs as a short-lived child
|
|
8
|
-
* process so it can't read gateway in-memory state; the hand-off is
|
|
9
|
-
* a single file inside `$TELEGRAM_STATE_DIR`:
|
|
10
|
-
*
|
|
11
|
-
* - `markAckSent()` touches the file on the first reply per turn.
|
|
12
|
-
* - `clearAckSent()` removes it at turn_started.
|
|
13
|
-
* - The hook checks `existsSync(path)` → allow / block.
|
|
14
|
-
*
|
|
15
|
-
* Per-agent isolation is built-in: `$TELEGRAM_STATE_DIR` is the
|
|
16
|
-
* agent's per-container state dir (~/.switchroom/agents/<name>/telegram,
|
|
17
|
-
* bind-mounted into /state/agent/home/.switchroom/agents/<name>/telegram
|
|
18
|
-
* inside the container).
|
|
19
|
-
*
|
|
20
|
-
* All operations are best-effort: a write or unlink failure logs to
|
|
21
|
-
* stderr and returns; the gate is informational UX, not a safety
|
|
22
|
-
* primitive, so a broken state-dir must never wedge reply itself.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { closeSync, existsSync, openSync, unlinkSync } from "node:fs";
|
|
26
|
-
import { join } from "node:path";
|
|
27
|
-
|
|
28
|
-
export const ACK_SENT_MARKER = "ack-sent.flag";
|
|
29
|
-
|
|
30
|
-
function markerPath(): string | null {
|
|
31
|
-
const dir = process.env.TELEGRAM_STATE_DIR;
|
|
32
|
-
if (!dir) return null;
|
|
33
|
-
return join(dir, ACK_SENT_MARKER);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Create the ack-sent marker. Idempotent (no-op if it exists). */
|
|
37
|
-
export function markAckSent(): void {
|
|
38
|
-
const path = markerPath();
|
|
39
|
-
if (path == null) return;
|
|
40
|
-
if (existsSync(path)) return;
|
|
41
|
-
try {
|
|
42
|
-
const fd = openSync(path, "w");
|
|
43
|
-
closeSync(fd);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
process.stderr.write(
|
|
46
|
-
`ack-flag: markAckSent failed path=${path}: ${err}\n`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Remove the ack-sent marker. Idempotent. */
|
|
52
|
-
export function clearAckSent(): void {
|
|
53
|
-
const path = markerPath();
|
|
54
|
-
if (path == null) return;
|
|
55
|
-
try {
|
|
56
|
-
unlinkSync(path);
|
|
57
|
-
} catch (err: unknown) {
|
|
58
|
-
// ENOENT is the common case (turn started before any prior turn
|
|
59
|
-
// ran a reply); silently swallow.
|
|
60
|
-
const code = (err as { code?: string } | undefined)?.code;
|
|
61
|
-
if (code === "ENOENT") return;
|
|
62
|
-
process.stderr.write(
|
|
63
|
-
`ack-flag: clearAckSent failed path=${path}: ${err}\n`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { markAckSent, clearAckSent, ACK_SENT_MARKER } from "../ack-flag.js";
|
|
7
|
-
|
|
8
|
-
describe("ack-flag — gateway-side state file for ack-first hook", () => {
|
|
9
|
-
let stateDir: string;
|
|
10
|
-
let prevEnv: string | undefined;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
stateDir = mkdtempSync(join(tmpdir(), "ack-flag-"));
|
|
14
|
-
prevEnv = process.env.TELEGRAM_STATE_DIR;
|
|
15
|
-
process.env.TELEGRAM_STATE_DIR = stateDir;
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
if (prevEnv != null) process.env.TELEGRAM_STATE_DIR = prevEnv;
|
|
20
|
-
else delete process.env.TELEGRAM_STATE_DIR;
|
|
21
|
-
rmSync(stateDir, { recursive: true, force: true });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("markAckSent creates the marker file", () => {
|
|
25
|
-
expect(existsSync(join(stateDir, ACK_SENT_MARKER))).toBe(false);
|
|
26
|
-
markAckSent();
|
|
27
|
-
expect(existsSync(join(stateDir, ACK_SENT_MARKER))).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("markAckSent is idempotent (second call is a no-op)", () => {
|
|
31
|
-
markAckSent();
|
|
32
|
-
expect(() => markAckSent()).not.toThrow();
|
|
33
|
-
expect(existsSync(join(stateDir, ACK_SENT_MARKER))).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("clearAckSent removes the marker file", () => {
|
|
37
|
-
markAckSent();
|
|
38
|
-
expect(existsSync(join(stateDir, ACK_SENT_MARKER))).toBe(true);
|
|
39
|
-
clearAckSent();
|
|
40
|
-
expect(existsSync(join(stateDir, ACK_SENT_MARKER))).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("clearAckSent is idempotent (works when marker doesn't exist)", () => {
|
|
44
|
-
expect(existsSync(join(stateDir, ACK_SENT_MARKER))).toBe(false);
|
|
45
|
-
expect(() => clearAckSent()).not.toThrow();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("round-trip: mark → clear → mark cycles work across turns", () => {
|
|
49
|
-
// turn 1
|
|
50
|
-
markAckSent();
|
|
51
|
-
expect(existsSync(join(stateDir, ACK_SENT_MARKER))).toBe(true);
|
|
52
|
-
// turn end / next turn start
|
|
53
|
-
clearAckSent();
|
|
54
|
-
expect(existsSync(join(stateDir, ACK_SENT_MARKER))).toBe(false);
|
|
55
|
-
// turn 2 first reply
|
|
56
|
-
markAckSent();
|
|
57
|
-
expect(existsSync(join(stateDir, ACK_SENT_MARKER))).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("noop when TELEGRAM_STATE_DIR is unset (best-effort)", () => {
|
|
61
|
-
delete process.env.TELEGRAM_STATE_DIR;
|
|
62
|
-
expect(() => markAckSent()).not.toThrow();
|
|
63
|
-
expect(() => clearAckSent()).not.toThrow();
|
|
64
|
-
});
|
|
65
|
-
});
|