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.
Files changed (76) hide show
  1. package/dist/agent-scheduler/index.js +2 -1
  2. package/dist/auth-broker/index.js +121 -1
  3. package/dist/cli/notion-write-pretool.mjs +2 -1
  4. package/dist/cli/switchroom.js +2995 -1583
  5. package/dist/host-control/main.js +122 -2
  6. package/dist/vault/approvals/kernel-server.js +124 -4
  7. package/dist/vault/broker/server.js +124 -4
  8. package/package.json +7 -4
  9. package/profiles/_base/start.sh.hbs +101 -0
  10. package/profiles/_shared/agent-self-service.md.hbs +64 -109
  11. package/profiles/_shared/delegation-golden-rule.md.hbs +5 -5
  12. package/profiles/_shared/dev-protocol.md.hbs +13 -42
  13. package/profiles/_shared/execution-discipline.md.hbs +7 -14
  14. package/profiles/coding/CLAUDE.md.hbs +0 -6
  15. package/profiles/default/CLAUDE.md.hbs +21 -50
  16. package/skills/dev-protocol/SKILL.md +90 -107
  17. package/skills/switchroom-release/SKILL.md +103 -20
  18. package/telegram-plugin/bunfig.toml +10 -0
  19. package/telegram-plugin/card-format.ts +92 -3
  20. package/telegram-plugin/dist/gateway/gateway.js +873 -184
  21. package/telegram-plugin/edit-flood-fuse.ts +477 -0
  22. package/telegram-plugin/format.ts +19 -7
  23. package/telegram-plugin/gateway/backstop-delivery.ts +97 -16
  24. package/telegram-plugin/gateway/boot-sweep-gate.ts +164 -0
  25. package/telegram-plugin/gateway/callback-query-handlers.ts +454 -81
  26. package/telegram-plugin/gateway/captured-answer-resume.ts +46 -17
  27. package/telegram-plugin/gateway/gateway.ts +75 -63
  28. package/telegram-plugin/gateway/inbound-interceptors.ts +27 -4
  29. package/telegram-plugin/gateway/narrative-lane.ts +49 -3
  30. package/telegram-plugin/gateway/outbound-send-path.ts +8 -1
  31. package/telegram-plugin/gateway/status-pin-api.ts +145 -0
  32. package/telegram-plugin/gateway/stream-render.ts +6 -0
  33. package/telegram-plugin/gateway/turn-record-status.ts +19 -0
  34. package/telegram-plugin/gateway/turns-jsonl-rotate.ts +65 -0
  35. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +325 -45
  36. package/telegram-plugin/retry-api-call.ts +15 -2
  37. package/telegram-plugin/send-gate.ts +1 -1
  38. package/telegram-plugin/status-no-truncate.ts +64 -1
  39. package/telegram-plugin/status-pin-driver.ts +50 -27
  40. package/telegram-plugin/status-pin.ts +43 -5
  41. package/telegram-plugin/tests/activity-card-send-gate.test.ts +275 -0
  42. package/telegram-plugin/tests/activity-card-wiring.test.ts +16 -7
  43. package/telegram-plugin/tests/agent-state-dir-preload.test.ts +33 -0
  44. package/telegram-plugin/tests/backstop-delivery.test.ts +204 -7
  45. package/telegram-plugin/tests/backstop-readback-probe.test.ts +12 -0
  46. package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +101 -0
  47. package/telegram-plugin/tests/boot-sweep-gate.test.ts +293 -0
  48. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  49. package/telegram-plugin/tests/captured-answer-resume.test.ts +104 -0
  50. package/telegram-plugin/tests/edit-flood-fuse.test.ts +431 -0
  51. package/telegram-plugin/tests/pinned-card-collapse.test.ts +356 -0
  52. package/telegram-plugin/tests/status-pin-api.test.ts +178 -0
  53. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +94 -11
  54. package/telegram-plugin/tests/status-pin.test.ts +106 -5
  55. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +631 -1
  56. package/telegram-plugin/tests/tool-activity-summary.test.ts +19 -10
  57. package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +92 -1
  58. package/telegram-plugin/tests/vault-approval-posture.test.ts +6 -1
  59. package/telegram-plugin/tests/vault-passphrase-retry.test.ts +666 -0
  60. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +42 -21
  61. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +233 -1
  62. package/telegram-plugin/tool-activity-summary.ts +85 -13
  63. package/telegram-plugin/worker-activity-feed.ts +5 -1
  64. package/vendor/hindsight-memory/scripts/drain_pending.py +304 -34
  65. package/vendor/hindsight-memory/scripts/lib/pending.py +886 -70
  66. package/vendor/hindsight-memory/scripts/lib/retain_split.py +71 -13
  67. package/vendor/hindsight-memory/scripts/recall.py +74 -5
  68. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +1602 -14
  69. package/vendor/hindsight-memory/scripts/tests/test_pending_failure_class.py +105 -0
  70. package/vendor/hindsight-memory/scripts/tests/test_pending_wedge.py +300 -0
  71. package/vendor/hindsight-memory/scripts/tests/test_recall_degraded_notice.py +365 -0
  72. package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +12 -4
  73. package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +27 -2
  74. package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +93 -13
  75. package/vendor/hindsight-memory/tests/test_drain_pending.py +44 -3
  76. package/vendor/hindsight-memory/tests/test_pending.py +12 -4
@@ -1,5 +1,6 @@
1
1
  """Tests for drain_pending.drain() (#1071)."""
2
2
 
3
+ import io
3
4
  import json
4
5
  import os
5
6
  import sys
@@ -14,6 +15,21 @@ if SCRIPTS_DIR not in sys.path:
14
15
  sys.path.insert(0, SCRIPTS_DIR)
15
16
 
16
17
 
18
+ def _dead_marker(path: str) -> str:
19
+ """Where `mark_dead` puts the marker for the queue entry at `path`.
20
+
21
+ NOT `<path>.dead` any more: a marker is the only remaining copy of its
22
+ memory, so it leaves the live queue directory for the `pending-dead/`
23
+ sibling rather than sitting in the directory external janitors glob
24
+ (switchroom #3697). Asserting the old spelling made this suite red on a
25
+ deliberate change -- invisibly, because CI discovers only
26
+ `scripts/tests/`.
27
+ """
28
+ from lib.pending import dead_dir
29
+
30
+ return os.path.join(dead_dir(), os.path.basename(path) + ".dead")
31
+
32
+
17
33
  class FakeOk:
18
34
  """Minimal urlopen() context-manager stand-in for a 200 OK."""
19
35
 
@@ -212,20 +228,45 @@ class DrainPendingTest(unittest.TestCase):
212
228
  self.assertEqual(entry["attempt_count"], 2)
213
229
  self.assertIn("last_attempt_at", entry)
214
230
 
215
- def test_drain_max_attempts_marks_dead(self):
231
+ def test_drain_max_attempts_marks_dead_on_a_permanent_failure(self):
232
+ """Exhausting the budget on a 4xx still retires the entry."""
216
233
  from lib.pending import MAX_ATTEMPTS
217
234
 
218
235
  path = _seed_entry(self._pending, attempt=MAX_ATTEMPTS)
219
236
  import drain_pending
220
237
 
221
238
  def boom(*a, **kw):
222
- raise urllib.error.URLError("still down")
239
+ raise urllib.error.HTTPError(
240
+ "http://h/v1/x", 400, "Bad Request", {}, io.BytesIO(b"bad payload")
241
+ )
223
242
 
224
243
  with patch("urllib.request.urlopen", side_effect=boom):
225
244
  summary = drain_pending.drain({})
226
245
  self.assertEqual(summary["dead"], 1)
227
246
  self.assertFalse(os.path.exists(path))
228
- self.assertTrue(os.path.exists(path + ".dead"))
247
+ self.assertTrue(os.path.exists(_dead_marker(path)))
248
+
249
+ def test_drain_max_attempts_keeps_the_memory_on_a_transient_failure(self):
250
+ """A dead upstream must never retire a memory, however many attempts.
251
+
252
+ ``URLError`` is what an unreachable daemon raises. Before the
253
+ permanence gate this retired the entry at MAX_ATTEMPTS — i.e. an
254
+ outage destroyed memory that was perfectly saveable.
255
+ """
256
+ from lib.pending import MAX_ATTEMPTS
257
+
258
+ path = _seed_entry(self._pending, attempt=MAX_ATTEMPTS)
259
+ import drain_pending
260
+
261
+ def boom(*a, **kw):
262
+ raise urllib.error.URLError("still down")
263
+
264
+ with patch("urllib.request.urlopen", side_effect=boom):
265
+ summary = drain_pending.drain({})
266
+ self.assertEqual(summary["dead"], 0)
267
+ self.assertEqual(summary["retried"], 1)
268
+ self.assertTrue(os.path.exists(path))
269
+ self.assertFalse(os.path.exists(_dead_marker(path)))
229
270
 
230
271
  def test_drain_stall_guard_stops_after_threshold(self):
231
272
  # Seed 10 entries; with a same-error-class stream, the stall
@@ -38,7 +38,10 @@ class PendingQueueTest(unittest.TestCase):
38
38
  "api_url": "http://fake:9077",
39
39
  "api_token": None,
40
40
  "bank_id": "test-bank",
41
- "content": "user: hello\nassistant: hi",
41
+ # Distinct per document: the queue's dedupe key is
42
+ # (bank_id, part_position, sha256(content)) since switchroom
43
+ # #3688, so a shared body would collapse these into one entry.
44
+ "content": f"user: hello\nassistant: hi ({document_id})",
42
45
  "document_id": document_id,
43
46
  "context": "claude-code",
44
47
  "metadata": {"session_id": "sess-1"},
@@ -59,7 +62,7 @@ class PendingQueueTest(unittest.TestCase):
59
62
  with open(path) as f:
60
63
  entry = json.load(f)
61
64
  self.assertEqual(entry["bank_id"], "test-bank")
62
- self.assertEqual(entry["content"], "user: hello\nassistant: hi")
65
+ self.assertEqual(entry["content"], self._sample_payload()["content"])
63
66
  self.assertEqual(entry["document_id"], "doc-1")
64
67
  self.assertEqual(entry["error_class"], "ValueError")
65
68
  self.assertEqual(entry["error_message"], "nope")
@@ -200,8 +203,13 @@ class PendingQueueTest(unittest.TestCase):
200
203
  self.assertNotIn(
201
204
  "dead_at", e, "a live queue entry must never carry dead_at"
202
205
  )
203
- # The .dead marker exists and carries dead_at.
204
- dead = path + ".dead"
206
+ # The .dead marker exists and carries dead_at. It lives in the
207
+ # `pending-dead/` SIBLING, not in the live queue directory: a marker
208
+ # is the only remaining copy of its memory, and the queue dir is the
209
+ # one external janitors glob (switchroom #3697).
210
+ dead = os.path.join(
211
+ pending_mod.dead_dir(), os.path.basename(path) + ".dead"
212
+ )
205
213
  self.assertTrue(os.path.isfile(dead))
206
214
  with open(dead) as f:
207
215
  self.assertIn("dead_at", json.load(f))