switchroom 0.19.25 → 0.19.27

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.
Files changed (84) hide show
  1. package/bin/git-agent-attribution-hook.sh +144 -0
  2. package/dist/agent-scheduler/index.js +61 -2
  3. package/dist/auth-broker/index.js +125 -8
  4. package/dist/cli/notion-write-pretool.mjs +61 -2
  5. package/dist/cli/switchroom.js +2347 -1104
  6. package/dist/host-control/main.js +126 -9
  7. package/dist/vault/approvals/kernel-server.js +124 -8
  8. package/dist/vault/broker/server.js +124 -8
  9. package/package.json +6 -2
  10. package/profiles/_base/cron-session.sh.hbs +14 -0
  11. package/profiles/_base/start.sh.hbs +145 -4
  12. package/telegram-plugin/card-layout.ts +328 -0
  13. package/telegram-plugin/dist/bridge/bridge.js +93 -1
  14. package/telegram-plugin/dist/gateway/gateway.js +2213 -1204
  15. package/telegram-plugin/dist/server.js +96 -1
  16. package/telegram-plugin/edit-flood-fuse.ts +637 -56
  17. package/telegram-plugin/flood-429-ledger.ts +526 -0
  18. package/telegram-plugin/flood-circuit-breaker.ts +18 -0
  19. package/telegram-plugin/gateway/flood-reply-queue.ts +168 -0
  20. package/telegram-plugin/gateway/gateway.ts +103 -112
  21. package/telegram-plugin/gateway/narrative-lane.ts +14 -0
  22. package/telegram-plugin/gateway/outbound-send-path.ts +36 -0
  23. package/telegram-plugin/gateway/outbox-sweep.ts +183 -6
  24. package/telegram-plugin/gateway/periodic-sweep-guard.ts +86 -0
  25. package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
  26. package/telegram-plugin/gateway/status-pin-retarget.ts +180 -0
  27. package/telegram-plugin/gateway/status-pin-store.ts +58 -9
  28. package/telegram-plugin/gateway/worker-pin-reaper.ts +56 -7
  29. package/telegram-plugin/llm-error-present.ts +61 -2
  30. package/telegram-plugin/model-unavailable.ts +8 -0
  31. package/telegram-plugin/operator-events.ts +72 -5
  32. package/telegram-plugin/outbound-class.ts +81 -0
  33. package/telegram-plugin/provider-credit.ts +237 -0
  34. package/telegram-plugin/scripts/bun-test-ci.sh +36 -6
  35. package/telegram-plugin/send-gate.ts +24 -2
  36. package/telegram-plugin/status-no-truncate.ts +11 -0
  37. package/telegram-plugin/status-pin-driver.ts +33 -17
  38. package/telegram-plugin/status-pin.ts +51 -5
  39. package/telegram-plugin/tests/card-golden.test.ts +69 -0
  40. package/telegram-plugin/tests/card-lifecycle-render.test.ts +362 -0
  41. package/telegram-plugin/tests/card-type-distinguishability.test.ts +291 -0
  42. package/telegram-plugin/tests/card-variants.golden.txt +211 -0
  43. package/telegram-plugin/tests/card-variants.ts +366 -0
  44. package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +316 -0
  45. package/telegram-plugin/tests/edit-flood-fuse-default-deny.test.ts +319 -0
  46. package/telegram-plugin/tests/edit-flood-fuse.test.ts +11 -2
  47. package/telegram-plugin/tests/feed-edit-rate-ceiling.test.ts +462 -0
  48. package/telegram-plugin/tests/fixtures/real-429-stream.ts +220 -0
  49. package/telegram-plugin/tests/flood-429-ledger.test.ts +278 -0
  50. package/telegram-plugin/tests/flood-429-recorder-wiring.test.ts +128 -0
  51. package/telegram-plugin/tests/flood-reply-queue.test.ts +418 -0
  52. package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +221 -0
  53. package/telegram-plugin/tests/periodic-sweep-guard.test.ts +151 -0
  54. package/telegram-plugin/tests/pinned-card-collapse.test.ts +24 -18
  55. package/telegram-plugin/tests/pinned-message-handler.test.ts +15 -15
  56. package/telegram-plugin/tests/provider-credit-402.test.ts +243 -0
  57. package/telegram-plugin/tests/status-pin-api.test.ts +11 -11
  58. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +36 -37
  59. package/telegram-plugin/tests/status-pin-lifecycle.test.ts +602 -0
  60. package/telegram-plugin/tests/status-pin-retarget.test.ts +244 -0
  61. package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
  62. package/telegram-plugin/tests/status-pin-shutdown-wiring.test.ts +94 -0
  63. package/telegram-plugin/tests/status-pin-store.test.ts +179 -64
  64. package/telegram-plugin/tests/status-pin.test.ts +184 -7
  65. package/telegram-plugin/tests/test-runner-coverage.test.ts +133 -0
  66. package/telegram-plugin/tests/worker-activity-feed.test.ts +12 -10
  67. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +29 -19
  68. package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +56 -59
  69. package/telegram-plugin/tests/worker-feed-terminal-edit-class.test.ts +335 -0
  70. package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
  71. package/telegram-plugin/tier-downgrade.ts +3 -2
  72. package/telegram-plugin/tool-activity-summary.ts +239 -322
  73. package/telegram-plugin/uat/assertions.ts +33 -3
  74. package/telegram-plugin/uat/feed-matcher.test.ts +36 -0
  75. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +9 -2
  76. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +9 -2
  77. package/telegram-plugin/worker-activity-feed.ts +109 -30
  78. package/vendor/hindsight-memory/CLAUDE.md +45 -0
  79. package/vendor/hindsight-memory/scripts/lib/config.py +33 -0
  80. package/vendor/hindsight-memory/scripts/recall.py +176 -7
  81. package/vendor/hindsight-memory/scripts/tests/test_config_recall_passthrough_env.py +170 -0
  82. package/vendor/hindsight-memory/scripts/tests/test_recall_min_score.py +464 -0
  83. package/vendor/hindsight-memory/scripts/tests/test_recall_request_timeout.py +241 -0
  84. package/vendor/hindsight-memory/settings.json +1 -1
@@ -11,7 +11,7 @@
11
11
  * We cannot import gateway.ts directly (its module top-level runs the whole boot
12
12
  * IIFE incl. the startup mutex + bot.start). Instead we model the gateway's
13
13
  * status-pin subsystem faithfully with the SAME functions gateway.ts calls
14
- * (reconcileAndPersistStatusPin, reconcilePin, runStatusPinBootCleanup) over an
14
+ * (runStatusPinReconcile, executePinLeg, runStatusPinBootCleanup) over an
15
15
  * in-memory fs + a fake Telegram whose pin set we can inspect — so a regression
16
16
  * in the ordering / cleanup contract reds here.
17
17
  *
@@ -25,18 +25,28 @@ import { readFileSync } from "node:fs";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import {
27
27
  loadStatusPins,
28
+ type PersistedStatusPin,
28
29
  persistStatusPins,
29
30
  reconcileAndPersistStatusPin,
30
31
  runStatusPinBootCleanup,
31
32
  type StatusPinStoreFsSeam,
32
33
  } from "../gateway/status-pin-store.js";
33
- import { reconcilePin } from "../status-pin-driver.js";
34
- import type { PinState, DesiredPin } from "../status-pin.js";
35
- import { decidePinAction } from "../status-pin.js";
34
+ import { executePinLeg } from "../status-pin-driver.js";
35
+ import type { DesiredPin, PinState } from "../status-pin.js";
36
+ import {
37
+ runStatusPinReconcile,
38
+ type StatusPinClaim,
39
+ } from "../gateway/status-pin-retarget.js";
36
40
  import { makeFloodWaitActiveError } from "../retry-api-call.js";
37
41
 
38
42
  const PATH = "/state/agent/telegram/status-pins.json";
39
43
 
44
+ /** Drop the #3810 claim-age stamp so a row's identity fields can be compared
45
+ * literally. The age itself is asserted in status-pin-store.test.ts. */
46
+ function idOnly(rows: PersistedStatusPin[]): Omit<PersistedStatusPin, "pinnedAt">[] {
47
+ return rows.map(({ pinnedAt: _age, ...rest }) => rest);
48
+ }
49
+
40
50
  function memFs(seed: Record<string, string> = {}) {
41
51
  const files = new Map<string, string>(Object.entries(seed));
42
52
  const fs: StatusPinStoreFsSeam = {
@@ -78,33 +88,22 @@ function fakeTelegram() {
78
88
  * with injectable fs + api so a single test can span a "crash" and a fresh boot.
79
89
  */
80
90
  function makeGateway(fs: StatusPinStoreFsSeam, tg: ReturnType<typeof fakeTelegram>) {
81
- const statusPinState = new Map<string, PinState>();
82
- const statusPinChatIds = new Map<string, string>();
91
+ // ONE claim registry — the same single record the gateway keeps (#3809).
92
+ const statusPinClaims = new Map<string, StatusPinClaim>();
83
93
 
84
94
  async function reconcileStatusPin(pinKey: string, chatId: string, desired: DesiredPin) {
85
- const prev = statusPinState.get(pinKey) ?? null;
86
- const action = decidePinAction(prev, desired);
87
- const op =
88
- action.kind === "pin"
89
- ? ({ kind: "pin", messageId: action.messageId } as const)
90
- : ({ kind: "clear" } as const);
91
- const next = await reconcileAndPersistStatusPin({
92
- path: PATH,
93
- fs,
95
+ const claim = statusPinClaims.get(pinKey);
96
+ const prev: PinState | null = claim == null ? null : { messageId: claim.messageId };
97
+ await runStatusPinReconcile({
94
98
  pinKey,
95
99
  chatId,
96
- op,
97
- applyPin: () =>
98
- reconcilePin({ api: tg.api, chatId, prevState: prev, desired }),
99
- log: () => {},
100
+ prev,
101
+ desired,
102
+ persist: { path: PATH, fs },
103
+ runPin: (action, from) =>
104
+ executePinLeg({ api: tg.api, chatId, prevState: from, action }),
105
+ claims: statusPinClaims,
100
106
  });
101
- if (next == null) {
102
- statusPinState.delete(pinKey);
103
- statusPinChatIds.delete(pinKey);
104
- } else {
105
- statusPinState.set(pinKey, next);
106
- statusPinChatIds.set(pinKey, chatId);
107
- }
108
107
  }
109
108
 
110
109
  async function bootCleanup() {
@@ -116,7 +115,7 @@ function makeGateway(fs: StatusPinStoreFsSeam, tg: ReturnType<typeof fakeTelegra
116
115
  });
117
116
  }
118
117
 
119
- return { statusPinState, reconcileStatusPin, bootCleanup };
118
+ return { statusPinClaims, reconcileStatusPin, bootCleanup };
120
119
  }
121
120
 
122
121
  describe("status-pin boot recovery (gateway wiring)", () => {
@@ -130,7 +129,7 @@ describe("status-pin boot recovery (gateway wiring)", () => {
130
129
 
131
130
  // Pin is live in Telegram and recorded on disk (confirmed).
132
131
  expect(tg.pinned.has("-100123:715")).toBe(true);
133
- expect(loadStatusPins(PATH, fs)).toEqual([
132
+ expect(idOnly(loadStatusPins(PATH, fs))).toEqual([
134
133
  { pinKey: "fg:c:3", chatId: "-100123", messageId: 715 },
135
134
  ]);
136
135
 
@@ -144,7 +143,7 @@ describe("status-pin boot recovery (gateway wiring)", () => {
144
143
 
145
144
  expect(res).toEqual({ cleared: 1, retained: 0, kept: 0, total: 1 });
146
145
  expect(tg.pinned.has("-100123:715")).toBe(false); // orphan unpinned
147
- expect(loadStatusPins(PATH, fs)).toEqual([]); // store emptied
146
+ expect(idOnly(loadStatusPins(PATH, fs))).toEqual([]); // store emptied
148
147
  });
149
148
 
150
149
  it("(B) pending pin (crash inside the persist-after-pin window) → recovered next boot", async () => {
@@ -171,7 +170,7 @@ describe("status-pin boot recovery (gateway wiring)", () => {
171
170
 
172
171
  // Orphan is live in Telegram; a PENDING record is on disk.
173
172
  expect(tg.pinned.has("-100123:715")).toBe(true);
174
- expect(loadStatusPins(PATH, fs)).toEqual([
173
+ expect(idOnly(loadStatusPins(PATH, fs))).toEqual([
175
174
  { pinKey: "fg:c:3", chatId: "-100123", messageId: 715, pending: true },
176
175
  ]);
177
176
 
@@ -180,7 +179,7 @@ describe("status-pin boot recovery (gateway wiring)", () => {
180
179
  const res = await gw2.bootCleanup();
181
180
  expect(res).toEqual({ cleared: 1, retained: 0, kept: 0, total: 1 });
182
181
  expect(tg.pinned.has("-100123:715")).toBe(false);
183
- expect(loadStatusPins(PATH, fs)).toEqual([]);
182
+ expect(idOnly(loadStatusPins(PATH, fs))).toEqual([]);
184
183
  });
185
184
 
186
185
  it("(C) #3664: a never-confirmed unpin (FLOOD_WAIT_ACTIVE) KEEPS the claim + the durable row, and the next boot clears the pin", async () => {
@@ -216,9 +215,9 @@ describe("status-pin boot recovery (gateway wiring)", () => {
216
215
  // The pin is STILL UP …
217
216
  expect(pinned.has("-100123:715")).toBe(true);
218
217
  // … and BOTH records survive: the in-memory claim …
219
- expect(gw1.statusPinState.get("fg:c:3")).toEqual({ messageId: 715 });
218
+ expect(gw1.statusPinClaims.get("fg:c:3")?.messageId).toBe(715);
220
219
  // … and the durable row (what boot cleanup reads).
221
- expect(loadStatusPins(PATH, fs)).toEqual([
220
+ expect(idOnly(loadStatusPins(PATH, fs))).toEqual([
222
221
  { pinKey: "fg:c:3", chatId: "-100123", messageId: 715 },
223
222
  ]);
224
223
 
@@ -226,8 +225,8 @@ describe("status-pin boot recovery (gateway wiring)", () => {
226
225
  floodOpen = false;
227
226
  await gw1.reconcileStatusPin("fg:c:3", "-100123", { pinned: false });
228
227
  expect(pinned.has("-100123:715")).toBe(false);
229
- expect(gw1.statusPinState.has("fg:c:3")).toBe(false);
230
- expect(loadStatusPins(PATH, fs)).toEqual([]);
228
+ expect(gw1.statusPinClaims.has("fg:c:3")).toBe(false);
229
+ expect(idOnly(loadStatusPins(PATH, fs))).toEqual([]);
231
230
  });
232
231
 
233
232
  it("(D) #3664: if the process dies while the flood window is open, the retained row lets the NEXT boot clear the orphan", async () => {
@@ -257,7 +256,7 @@ describe("status-pin boot recovery (gateway wiring)", () => {
257
256
  const res = await gw2.bootCleanup();
258
257
  expect(res).toEqual({ cleared: 1, retained: 0, kept: 0, total: 1 });
259
258
  expect(pinned.has("-100123:715")).toBe(false);
260
- expect(loadStatusPins(PATH, fs)).toEqual([]);
259
+ expect(idOnly(loadStatusPins(PATH, fs))).toEqual([]);
261
260
  });
262
261
 
263
262
  it("clean shutdown (sweep DID run) leaves nothing for boot cleanup to do", async () => {
@@ -271,7 +270,7 @@ describe("status-pin boot recovery (gateway wiring)", () => {
271
270
  // Clean sweep: unpin the key.
272
271
  await gw1.reconcileStatusPin("fg:c:3", "-100123", { pinned: false });
273
272
  expect(tg.pinned.size).toBe(0);
274
- expect(loadStatusPins(PATH, fs)).toEqual([]);
273
+ expect(idOnly(loadStatusPins(PATH, fs))).toEqual([]);
275
274
 
276
275
  const gw2 = makeGateway(fs, tg);
277
276
  expect(await gw2.bootCleanup()).toEqual({ cleared: 0, retained: 0, kept: 0, total: 0 });