switchroom 0.19.19 → 0.19.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/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +121 -1
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +2995 -1583
- package/dist/host-control/main.js +122 -2
- package/dist/vault/approvals/kernel-server.js +124 -4
- package/dist/vault/broker/server.js +124 -4
- package/package.json +7 -4
- package/profiles/_base/start.sh.hbs +101 -0
- package/profiles/_shared/agent-self-service.md.hbs +64 -109
- package/profiles/_shared/delegation-golden-rule.md.hbs +5 -5
- package/profiles/_shared/dev-protocol.md.hbs +13 -42
- package/profiles/_shared/execution-discipline.md.hbs +7 -14
- package/profiles/coding/CLAUDE.md.hbs +0 -6
- package/profiles/default/CLAUDE.md.hbs +21 -50
- package/skills/dev-protocol/SKILL.md +90 -107
- package/skills/switchroom-release/SKILL.md +103 -20
- package/telegram-plugin/bunfig.toml +10 -0
- package/telegram-plugin/card-format.ts +92 -3
- package/telegram-plugin/dist/gateway/gateway.js +873 -184
- package/telegram-plugin/edit-flood-fuse.ts +477 -0
- package/telegram-plugin/format.ts +19 -7
- package/telegram-plugin/gateway/backstop-delivery.ts +97 -16
- package/telegram-plugin/gateway/boot-sweep-gate.ts +164 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +454 -81
- package/telegram-plugin/gateway/captured-answer-resume.ts +46 -17
- package/telegram-plugin/gateway/gateway.ts +75 -63
- package/telegram-plugin/gateway/inbound-interceptors.ts +27 -4
- package/telegram-plugin/gateway/narrative-lane.ts +49 -3
- package/telegram-plugin/gateway/outbound-send-path.ts +8 -1
- package/telegram-plugin/gateway/status-pin-api.ts +145 -0
- package/telegram-plugin/gateway/stream-render.ts +6 -0
- package/telegram-plugin/gateway/turn-record-status.ts +19 -0
- package/telegram-plugin/gateway/turns-jsonl-rotate.ts +65 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +325 -45
- package/telegram-plugin/retry-api-call.ts +15 -2
- package/telegram-plugin/send-gate.ts +1 -1
- package/telegram-plugin/status-no-truncate.ts +64 -1
- package/telegram-plugin/status-pin-driver.ts +50 -27
- package/telegram-plugin/status-pin.ts +43 -5
- package/telegram-plugin/tests/activity-card-send-gate.test.ts +275 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +16 -7
- package/telegram-plugin/tests/agent-state-dir-preload.test.ts +33 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +204 -7
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +12 -0
- package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +101 -0
- package/telegram-plugin/tests/boot-sweep-gate.test.ts +293 -0
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +104 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +431 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +356 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +178 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +94 -11
- package/telegram-plugin/tests/status-pin.test.ts +106 -5
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +631 -1
- package/telegram-plugin/tests/tool-activity-summary.test.ts +19 -10
- package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +92 -1
- package/telegram-plugin/tests/vault-approval-posture.test.ts +6 -1
- package/telegram-plugin/tests/vault-passphrase-retry.test.ts +666 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +42 -21
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +233 -1
- package/telegram-plugin/tool-activity-summary.ts +85 -13
- package/telegram-plugin/worker-activity-feed.ts +5 -1
- package/vendor/hindsight-memory/scripts/drain_pending.py +304 -34
- package/vendor/hindsight-memory/scripts/lib/pending.py +886 -70
- package/vendor/hindsight-memory/scripts/lib/retain_split.py +71 -13
- package/vendor/hindsight-memory/scripts/recall.py +74 -5
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +1602 -14
- package/vendor/hindsight-memory/scripts/tests/test_pending_failure_class.py +105 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_wedge.py +300 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_degraded_notice.py +365 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +12 -4
- package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +27 -2
- package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +93 -13
- package/vendor/hindsight-memory/tests/test_drain_pending.py +44 -3
- package/vendor/hindsight-memory/tests/test_pending.py +12 -4
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
type SessionActivityHeader,
|
|
16
16
|
} from "../tool-activity-summary.js";
|
|
17
17
|
import { STATUS_ROLLING_LINES, STATUS_LINE_MAX } from "../status-no-truncate.js";
|
|
18
|
+
import { COLLAPSE_SAFE_SEPARATOR } from "../card-format.js";
|
|
19
|
+
|
|
20
|
+
/** The pinned-card collapse separator (#3666) every status-card line now
|
|
21
|
+
* carries before its hard break. Aliased for readable goldens. */
|
|
22
|
+
const NB = COLLAPSE_SAFE_SEPARATOR;
|
|
18
23
|
|
|
19
24
|
describe("describeToolUse — friendly per-tool rendering (draft-mirror)", () => {
|
|
20
25
|
it("Bash uses the model-authored description verbatim, never the command", () => {
|
|
@@ -95,10 +100,10 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
95
100
|
"**→ Reading gateway.ts**",
|
|
96
101
|
);
|
|
97
102
|
expect(appendActivityLine(lines, "mcp__hindsight__reflect", { query: "x" })).toBe(
|
|
98
|
-
|
|
103
|
+
`~~_✓ Reading gateway.ts_~~${NB} \n**→ Searching memory**`,
|
|
99
104
|
);
|
|
100
105
|
expect(appendActivityLine(lines, "Bash", { command: "ls", description: "List workspace" })).toBe(
|
|
101
|
-
|
|
106
|
+
`~~_✓ Reading gateway.ts_~~${NB} \n~~_✓ Searching memory_~~${NB} \n**→ List workspace**`,
|
|
102
107
|
);
|
|
103
108
|
});
|
|
104
109
|
|
|
@@ -121,7 +126,7 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
121
126
|
const lines = Array.from({ length: total }, (_, i) => `Action ${i + 1}`);
|
|
122
127
|
const out = renderActivityFeed(lines)!;
|
|
123
128
|
const hidden = total - STATUS_ROLLING_LINES;
|
|
124
|
-
expect(out.startsWith(`_✓ +${hidden} earlier…_ \n`)).toBe(true);
|
|
129
|
+
expect(out.startsWith(`_✓ +${hidden} earlier…_${NB} \n`)).toBe(true);
|
|
125
130
|
// Only the last STATUS_ROLLING_LINES actions are shown; older ones collapsed.
|
|
126
131
|
const firstVisible = total - STATUS_ROLLING_LINES + 1;
|
|
127
132
|
expect(out).toContain(`~~_✓ Action ${firstVisible}_~~`);
|
|
@@ -156,7 +161,7 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
156
161
|
const lines = ["Reading a.ts", "Searching memory", "Running a command"];
|
|
157
162
|
const out = renderActivityFeed(lines, true)!;
|
|
158
163
|
expect(out).toBe(
|
|
159
|
-
|
|
164
|
+
`~~_✓ Reading a.ts_~~${NB} \n~~_✓ Searching memory_~~${NB} \n~~_✓ Running a command_~~`,
|
|
160
165
|
);
|
|
161
166
|
expect(out).not.toContain("→"); // no in-progress arrow anywhere
|
|
162
167
|
});
|
|
@@ -195,7 +200,7 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
195
200
|
describe("liveSuffix (heartbeat)", () => {
|
|
196
201
|
it("appends the suffix to the newest in-progress line only", () => {
|
|
197
202
|
expect(renderActivityFeed(["Reading a.ts", "Running a command"], false, " · 18s")).toBe(
|
|
198
|
-
|
|
203
|
+
`~~_✓ Reading a.ts_~~${NB} \n**→ Running a command · 18s**`,
|
|
199
204
|
);
|
|
200
205
|
});
|
|
201
206
|
it("single live line gets the suffix", () => {
|
|
@@ -207,7 +212,7 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
207
212
|
const out = renderActivityFeed(["Reading a.ts", "Running a command"], true, " · 18s")!;
|
|
208
213
|
expect(out).not.toContain("·");
|
|
209
214
|
expect(out).not.toContain("→");
|
|
210
|
-
expect(out).toBe(
|
|
215
|
+
expect(out).toBe(`~~_✓ Reading a.ts_~~${NB} \n~~_✓ Running a command_~~`);
|
|
211
216
|
});
|
|
212
217
|
it("default empty suffix is byte-identical to no suffix", () => {
|
|
213
218
|
expect(renderActivityFeed(["Reading a.ts"], false, "")).toBe(
|
|
@@ -222,7 +227,7 @@ describe("appendActivityLabel — precomputed label feed (tool_label path)", ()
|
|
|
222
227
|
const lines: string[] = [];
|
|
223
228
|
expect(appendActivityLabel(lines, "Searching memory")).toBe("**→ Searching memory**");
|
|
224
229
|
expect(appendActivityLabel(lines, "List workspace")).toBe(
|
|
225
|
-
|
|
230
|
+
`~~_✓ Searching memory_~~${NB} \n**→ List workspace**`,
|
|
226
231
|
);
|
|
227
232
|
// consecutive dup collapses
|
|
228
233
|
appendActivityLabel(lines, "List workspace");
|
|
@@ -417,7 +422,7 @@ describe("renderActivityFeedWithNested — foreground sub-agent nesting (Model A
|
|
|
417
422
|
it("stepCount footer appears on final=true with no children (delegates to flat render)", () => {
|
|
418
423
|
const out = renderActivityFeedWithNested(["Reading a.ts"], [], true, "", 3)!;
|
|
419
424
|
expect(out).toContain("_✓ 3 steps_");
|
|
420
|
-
expect(out).toBe(
|
|
425
|
+
expect(out).toBe(`~~_✓ Reading a.ts_~~${NB} \n_✓ 3 steps_`);
|
|
421
426
|
});
|
|
422
427
|
|
|
423
428
|
// Liveness-driven feed open (dark-turn fix): a turn that emits no tool_label
|
|
@@ -1052,7 +1057,11 @@ describe("status-card body stacks like a reply (#2669 rich-message renderer)", (
|
|
|
1052
1057
|
it("multiple step bullets stack on their own lines (no soft-break collapse)", () => {
|
|
1053
1058
|
const out = renderActivityFeed(["Reading a.ts", "Searching memory", "Running tests"], true)!;
|
|
1054
1059
|
const lines = out.split(" \n");
|
|
1055
|
-
expect(lines).toEqual([
|
|
1060
|
+
expect(lines).toEqual([
|
|
1061
|
+
`~~_✓ Reading a.ts_~~${NB}`,
|
|
1062
|
+
`~~_✓ Searching memory_~~${NB}`,
|
|
1063
|
+
"~~_✓ Running tests_~~",
|
|
1064
|
+
]);
|
|
1056
1065
|
// Not a single lone `\n` survives to collapse two bullets.
|
|
1057
1066
|
expect(out).not.toMatch(/(?<! {2})\n/);
|
|
1058
1067
|
});
|
|
@@ -1076,6 +1085,6 @@ describe("status-card body stacks like a reply (#2669 rich-message renderer)", (
|
|
|
1076
1085
|
// prose by escapeStepLine; the inline-markup-survives case is covered by
|
|
1077
1086
|
// the stackCardLines unit test, which operates on pre-rendered lines.)
|
|
1078
1087
|
const out = renderActivityFeed(["Reading a.ts", "Running tests"], false)!;
|
|
1079
|
-
expect(out).toBe(
|
|
1088
|
+
expect(out).toBe(`~~_✓ Reading a.ts_~~${NB} \n**→ Running tests**`);
|
|
1080
1089
|
});
|
|
1081
1090
|
});
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
1
5
|
import { describe, expect, it, vi } from 'vitest'
|
|
2
6
|
|
|
3
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
maybeRotate,
|
|
9
|
+
resolveAgentStateDir,
|
|
10
|
+
resolveTurnsJsonlPath,
|
|
11
|
+
DEFAULT_AGENT_STATE_DIR,
|
|
12
|
+
TURNS_JSONL_MAX_BYTES,
|
|
13
|
+
type RotateFs,
|
|
14
|
+
} from '../gateway/turns-jsonl-rotate.js'
|
|
15
|
+
|
|
16
|
+
const GATEWAY_SRC = readFileSync(
|
|
17
|
+
resolve(fileURLToPath(new URL('.', import.meta.url)), '..', 'gateway', 'gateway.ts'),
|
|
18
|
+
'utf-8',
|
|
19
|
+
)
|
|
4
20
|
|
|
5
21
|
describe('maybeRotate — turns.jsonl size cap', () => {
|
|
6
22
|
const mkFs = (size: number | undefined) => {
|
|
@@ -37,3 +53,78 @@ describe('maybeRotate — turns.jsonl size cap', () => {
|
|
|
37
53
|
expect(rename).toHaveBeenNthCalledWith(2, '/a/turns.jsonl', '/a/turns.jsonl.1')
|
|
38
54
|
})
|
|
39
55
|
})
|
|
56
|
+
|
|
57
|
+
describe('resolveTurnsJsonlPath — the turn record must follow the state dir', () => {
|
|
58
|
+
// Regression: the path was hard-coded to `/state/agent/turns.jsonl`, so a test
|
|
59
|
+
// that isolated TELEGRAM_STATE_DIR / SWITCHROOM_AGENT_STATE_DIR into a tmpdir
|
|
60
|
+
// still appended its synthetic turn rows into the PRODUCTION turn record of
|
|
61
|
+
// whichever agent container it ran in — which the fleet-health L0 sensor then
|
|
62
|
+
// scored as that agent's real production failures.
|
|
63
|
+
it('honours SWITCHROOM_AGENT_STATE_DIR', () => {
|
|
64
|
+
expect(resolveTurnsJsonlPath({ SWITCHROOM_AGENT_STATE_DIR: '/tmp/iso-123' })).toBe(
|
|
65
|
+
'/tmp/iso-123/turns.jsonl',
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('strips a trailing slash rather than doubling it', () => {
|
|
70
|
+
expect(resolveTurnsJsonlPath({ SWITCHROOM_AGENT_STATE_DIR: '/tmp/iso-123/' })).toBe(
|
|
71
|
+
'/tmp/iso-123/turns.jsonl',
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('falls back to the container default when unset or blank', () => {
|
|
76
|
+
expect(resolveTurnsJsonlPath({})).toBe(`${DEFAULT_AGENT_STATE_DIR}/turns.jsonl`)
|
|
77
|
+
expect(resolveTurnsJsonlPath({ SWITCHROOM_AGENT_STATE_DIR: ' ' })).toBe(
|
|
78
|
+
`${DEFAULT_AGENT_STATE_DIR}/turns.jsonl`,
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('resolveAgentStateDir — ONE normaliser for every writer in the state dir', () => {
|
|
84
|
+
// The gateway had two writers into this dir 26 lines apart reading the env
|
|
85
|
+
// var two different ways: the turn record via the normaliser, the
|
|
86
|
+
// context-occupancy snapshot via a bare `process.env.X ?? '/state/agent'`.
|
|
87
|
+
// For a value like `/x/ ` those resolve to different directories, so the two
|
|
88
|
+
// artifacts of the same turn land in two places.
|
|
89
|
+
it('trims, strips a trailing slash, and treats blank as unset', () => {
|
|
90
|
+
expect(resolveAgentStateDir({ SWITCHROOM_AGENT_STATE_DIR: '/x' })).toBe('/x')
|
|
91
|
+
expect(resolveAgentStateDir({ SWITCHROOM_AGENT_STATE_DIR: ' /x/ ' })).toBe('/x')
|
|
92
|
+
expect(resolveAgentStateDir({ SWITCHROOM_AGENT_STATE_DIR: '/x///' })).toBe('/x')
|
|
93
|
+
expect(resolveAgentStateDir({ SWITCHROOM_AGENT_STATE_DIR: ' ' })).toBe(DEFAULT_AGENT_STATE_DIR)
|
|
94
|
+
expect(resolveAgentStateDir({})).toBe(DEFAULT_AGENT_STATE_DIR)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('the turn record and the state dir agree for any messy value', () => {
|
|
98
|
+
const env = { SWITCHROOM_AGENT_STATE_DIR: ' /tmp/iso-9/ ' }
|
|
99
|
+
expect(resolveTurnsJsonlPath(env)).toBe(`${resolveAgentStateDir(env)}/turns.jsonl`)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('gateway.ts call sites — the defect site itself, not just the helper', () => {
|
|
104
|
+
// The bug was a hard-coded literal at the CALLSITE. A unit test of the pure
|
|
105
|
+
// helper passes with the literal re-inlined, so these read the real source.
|
|
106
|
+
// Source-text assertions are the repo's cheap deterministic pattern for
|
|
107
|
+
// pinning a callsite (see per-topic-current-turn.test.ts).
|
|
108
|
+
it('emitTurnRecord resolves the path — the hard-coded literal is gone', () => {
|
|
109
|
+
const body = GATEWAY_SRC.split('function emitTurnRecord(')[1]?.split('\n}')[0] ?? ''
|
|
110
|
+
expect(body.length).toBeGreaterThan(50)
|
|
111
|
+
expect(body).toMatch(/const turnsPath = resolveTurnsJsonlPath\(\)/)
|
|
112
|
+
// The append + the rotate both use the RESOLVED path, not a literal.
|
|
113
|
+
expect(body).toMatch(/appendFileSync\(turnsPath, /)
|
|
114
|
+
expect(body).toMatch(/maybeRotate\(turnsPath, /)
|
|
115
|
+
expect(body).not.toMatch(/turns\.jsonl/)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('no hard-coded turns.jsonl path survives anywhere in gateway.ts', () => {
|
|
119
|
+
const hits = GATEWAY_SRC.match(/['"`][^'"`\n]*\/turns\.jsonl['"`]/g) ?? []
|
|
120
|
+
expect(hits, `hard-coded turns.jsonl path(s): ${JSON.stringify(hits)}`).toEqual([])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('every state-dir read in gateway.ts goes through resolveAgentStateDir', () => {
|
|
124
|
+
// Consistency, not style: a second inline `process.env.X ?? '/state/agent'`
|
|
125
|
+
// silently opts that writer out of trim + trailing-slash normalisation.
|
|
126
|
+
const inline = GATEWAY_SRC.match(/process\.env\.SWITCHROOM_AGENT_STATE_DIR/g) ?? []
|
|
127
|
+
expect(inline, `inline state-dir read(s) in gateway.ts: ${inline.length}`).toEqual([])
|
|
128
|
+
expect(GATEWAY_SRC).toMatch(/resolveAgentStateDir\(\)/)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -101,7 +101,12 @@ describe('handleVaultRequestAccessCallback — posture branch', () => {
|
|
|
101
101
|
it('passphrase-mode branch unchanged: cache lookup + prompt still present', () => {
|
|
102
102
|
const approveBlock = sliceAccessApproveBlock()
|
|
103
103
|
expect(approveBlock).toMatch(/vaultPassphraseCache\.get\(pending\.chat_id\)/)
|
|
104
|
-
|
|
104
|
+
// #3627: the prompt COPY moved into the shared
|
|
105
|
+
// `buildAccessPassphrasePromptText` builder (so the first prompt and the
|
|
106
|
+
// wrong-passphrase re-prompt can't drift). The passphrase-mode branch
|
|
107
|
+
// must still route into it.
|
|
108
|
+
expect(approveBlock).toMatch(/sendAccessPassphrasePrompt\(/)
|
|
109
|
+
expect(gatewaySrc).toMatch(/Reply with your passphrase/i)
|
|
105
110
|
expect(approveBlock).toMatch(/passphrase-for-access-approve/)
|
|
106
111
|
// Pinned: the queued-drain path passes the typed passphrase via
|
|
107
112
|
// the new attestation shape `{ kind: 'passphrase', passphrase }`.
|