switchroom 0.17.6 → 0.18.3

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 (197) hide show
  1. package/bin/workspace-dynamic-hook.sh +12 -13
  2. package/dist/agent-scheduler/index.js +65 -5
  3. package/dist/auth-broker/index.js +6623 -514
  4. package/dist/cli/notion-write-pretool.mjs +64 -4
  5. package/dist/cli/switchroom.js +1888 -1162
  6. package/dist/host-control/main.js +6306 -162
  7. package/dist/vault/approvals/kernel-server.js +6014 -202
  8. package/dist/vault/broker/server.js +6741 -940
  9. package/package.json +1 -1
  10. package/profiles/_base/settings.json.hbs +2 -2
  11. package/profiles/_base/start.sh.hbs +218 -25
  12. package/profiles/coding/CLAUDE.md.hbs +1 -1
  13. package/profiles/default/CLAUDE.md +116 -0
  14. package/profiles/default/CLAUDE.md.hbs +2 -2
  15. package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
  16. package/profiles/health-coach/CLAUDE.md.hbs +1 -1
  17. package/skills/mental-model-curator/SKILL.md +162 -0
  18. package/telegram-plugin/auth-snapshot-format.ts +22 -24
  19. package/telegram-plugin/bridge/bridge.ts +80 -1
  20. package/telegram-plugin/bridge/ipc-client.ts +19 -0
  21. package/telegram-plugin/bridge/permission-ledger.ts +61 -0
  22. package/telegram-plugin/consolidation-legibility.ts +279 -0
  23. package/telegram-plugin/context-exhaustion.ts +124 -0
  24. package/telegram-plugin/dist/bridge/bridge.js +85 -1
  25. package/telegram-plugin/dist/gateway/gateway.js +25802 -8488
  26. package/telegram-plugin/dist/server.js +86 -2
  27. package/telegram-plugin/feed-heartbeat-climb.ts +206 -0
  28. package/telegram-plugin/gateway/activity-card-store.ts +369 -0
  29. package/telegram-plugin/gateway/gateway.ts +1861 -172
  30. package/telegram-plugin/gateway/inbound-delivery-gate.ts +26 -0
  31. package/telegram-plugin/gateway/inbound-spool.ts +22 -0
  32. package/telegram-plugin/gateway/mental-model-propose-card.ts +69 -0
  33. package/telegram-plugin/gateway/mental-model-propose-diff.ts +171 -0
  34. package/telegram-plugin/gateway/mental-model-propose-inbound-builders.ts +147 -0
  35. package/telegram-plugin/gateway/mental-model-propose-resolve.ts +201 -0
  36. package/telegram-plugin/gateway/missed-approvals-card.ts +161 -0
  37. package/telegram-plugin/gateway/missed-approvals-store.ts +167 -0
  38. package/telegram-plugin/gateway/model-command.ts +70 -10
  39. package/telegram-plugin/gateway/permission-rearm.ts +115 -0
  40. package/telegram-plugin/gateway/scoped-grant-store.ts +89 -0
  41. package/telegram-plugin/memory-legibility.ts +217 -0
  42. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  43. package/telegram-plugin/package.json +6 -0
  44. package/telegram-plugin/quota-watch.ts +4 -6
  45. package/telegram-plugin/registry/turns-schema.test.ts +97 -0
  46. package/telegram-plugin/registry/turns-schema.ts +78 -0
  47. package/telegram-plugin/render/ir.ts +209 -0
  48. package/telegram-plugin/render/parse.ts +363 -0
  49. package/telegram-plugin/render/render.ts +440 -0
  50. package/telegram-plugin/render/rich-render.ts +72 -0
  51. package/telegram-plugin/scoped-approval.ts +59 -0
  52. package/telegram-plugin/silent-end.ts +78 -0
  53. package/telegram-plugin/stream-controller.ts +14 -3
  54. package/telegram-plugin/subagent-watcher.ts +60 -6
  55. package/telegram-plugin/tests/activity-card-store.test.ts +530 -0
  56. package/telegram-plugin/tests/activity-card-wiring.test.ts +88 -0
  57. package/telegram-plugin/tests/auth-command-format2.test.ts +1 -1
  58. package/telegram-plugin/tests/auth-snapshot-format.test.ts +30 -16
  59. package/telegram-plugin/tests/claude-code-event-contract.test.ts +48 -0
  60. package/telegram-plugin/tests/consolidation-legibility.test.ts +224 -0
  61. package/telegram-plugin/tests/emission-authority-facade.test.ts +25 -10
  62. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +44 -9
  63. package/telegram-plugin/tests/feed-survival.test.ts +39 -0
  64. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
  65. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +81 -0
  66. package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +82 -0
  67. package/telegram-plugin/tests/inbound-spool.test.ts +105 -0
  68. package/telegram-plugin/tests/liveness-tracker.test.ts +228 -0
  69. package/telegram-plugin/tests/memory-legibility.test.ts +216 -0
  70. package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +67 -0
  71. package/telegram-plugin/tests/mental-model-propose-card.test.ts +56 -0
  72. package/telegram-plugin/tests/mental-model-propose-diff.test.ts +201 -0
  73. package/telegram-plugin/tests/mental-model-propose-inbound-builders.test.ts +68 -0
  74. package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +157 -0
  75. package/telegram-plugin/tests/missed-approvals-card.test.ts +145 -0
  76. package/telegram-plugin/tests/missed-approvals-store.test.ts +147 -0
  77. package/telegram-plugin/tests/missed-approvals-wiring.test.ts +89 -0
  78. package/telegram-plugin/tests/model-command.test.ts +193 -16
  79. package/telegram-plugin/tests/narrative-render.test.ts +125 -0
  80. package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +123 -163
  81. package/telegram-plugin/tests/permission-ledger.test.ts +166 -0
  82. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +1 -1
  83. package/telegram-plugin/tests/permission-rearm-wiring.test.ts +175 -0
  84. package/telegram-plugin/tests/permission-rearm.test.ts +126 -0
  85. package/telegram-plugin/tests/quota-watch.test.ts +1 -4
  86. package/telegram-plugin/tests/rapid-fire-delivery-ordering.test.ts +149 -0
  87. package/telegram-plugin/tests/render/parse-torture.test.ts +136 -0
  88. package/telegram-plugin/tests/render/parse.test.ts +393 -0
  89. package/telegram-plugin/tests/render/render.test.ts +436 -0
  90. package/telegram-plugin/tests/render/rich-render.test.ts +85 -0
  91. package/telegram-plugin/tests/scoped-grant-persist.test.ts +223 -0
  92. package/telegram-plugin/tests/silent-end-transport.test.ts +290 -0
  93. package/telegram-plugin/tests/silent-turn-climb-transport.test.ts +337 -0
  94. package/telegram-plugin/tests/subagent-watcher.test.ts +139 -0
  95. package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +155 -1
  96. package/telegram-plugin/tests/worktree-watch-cwds.test.ts +198 -0
  97. package/telegram-plugin/turn-liveness-floor.ts +35 -1
  98. package/telegram-plugin/uat/assertions.ts +88 -4
  99. package/telegram-plugin/uat/feed-matcher.test.ts +69 -0
  100. package/telegram-plugin/uat/scenarios/fuzz-liveness-climb-dm.test.ts +155 -0
  101. package/telegram-plugin/uat/scenarios/jtbd-directive-capture-nudge-dm.test.ts +185 -0
  102. package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-channel.test.ts +192 -0
  103. package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-dm.test.ts +220 -0
  104. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +137 -0
  105. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +148 -0
  106. package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-channel.test.ts +66 -0
  107. package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-dm.test.ts +61 -0
  108. package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +99 -7
  109. package/telegram-plugin/uat/scenarios/silent-end-recovery-channel.test.ts +136 -0
  110. package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +24 -2
  111. package/telegram-plugin/worktree-watch-cwds.ts +135 -0
  112. package/vendor/hindsight-memory/hooks/hooks.json +9 -0
  113. package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
  114. package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
  115. package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
  116. package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
  117. package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
  118. package/vendor/hindsight-memory/scripts/directive_verify.py +445 -0
  119. package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  120. package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
  121. package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
  122. package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
  123. package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
  124. package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
  125. package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
  126. package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
  127. package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
  128. package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
  129. package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
  130. package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
  131. package/vendor/hindsight-memory/scripts/lib/client.py +11 -1
  132. package/vendor/hindsight-memory/scripts/lib/config.py +46 -2
  133. package/vendor/hindsight-memory/scripts/lib/directives.py +88 -0
  134. package/vendor/hindsight-memory/scripts/lib/switchroom_envelope.py +77 -0
  135. package/vendor/hindsight-memory/scripts/recall.py +217 -10
  136. package/vendor/hindsight-memory/scripts/retain.py +17 -0
  137. package/vendor/hindsight-memory/scripts/setup_hooks.py +9 -0
  138. package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  139. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
  140. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
  141. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
  142. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
  143. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
  144. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
  145. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
  146. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
  147. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
  148. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
  149. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
  150. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
  151. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
  152. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
  153. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
  154. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
  155. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
  156. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
  157. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
  158. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
  159. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
  160. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
  161. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
  162. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
  163. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
  164. package/vendor/hindsight-memory/scripts/tests/test_directive_capture_nudge.py +185 -0
  165. package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +516 -0
  166. package/vendor/hindsight-memory/scripts/tests/test_directives.py +49 -0
  167. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +1 -0
  168. package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +66 -1
  169. package/vendor/hindsight-memory/scripts/tests/test_switchroom_envelope.py +69 -0
  170. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  171. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  172. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
  173. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
  174. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
  175. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
  176. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
  177. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
  178. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
  179. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
  180. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
  181. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  182. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
  183. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
  184. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
  185. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
  186. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
  187. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  188. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
  189. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
  190. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
  191. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  192. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
  193. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
  194. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
  195. package/vendor/hindsight-memory/tests/test_client.py +43 -0
  196. package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +49 -2
  197. package/vendor/hindsight-memory/tests/test_recall_precision.py +114 -0
@@ -23876,6 +23876,7 @@ function createIpcClient(options) {
23876
23876
  onInbound,
23877
23877
  onPermission,
23878
23878
  onStatus,
23879
+ onConnect,
23879
23880
  log = () => {},
23880
23881
  reconnectDelayMs = 2000,
23881
23882
  maxReconnectDelayMs = 30000,
@@ -24011,6 +24012,13 @@ function createIpcClient(options) {
24011
24012
  sendRegister();
24012
24013
  startHeartbeat();
24013
24014
  log(`connected to ${socketPath}`);
24015
+ if (onConnect) {
24016
+ try {
24017
+ onConnect();
24018
+ } catch (err) {
24019
+ log(`onConnect hook threw: ${err}`);
24020
+ }
24021
+ }
24014
24022
  resolve();
24015
24023
  },
24016
24024
  data(sock, data) {
@@ -24232,10 +24240,52 @@ var init_permission_rule = __esm(() => {
24232
24240
  ]);
24233
24241
  });
24234
24242
 
24243
+ // bridge/permission-ledger.ts
24244
+ function createOutstandingPermissionLedger() {
24245
+ const map2 = new Map;
24246
+ return {
24247
+ add(params) {
24248
+ map2.set(params.request_id, params);
24249
+ },
24250
+ delete(requestId) {
24251
+ map2.delete(requestId);
24252
+ },
24253
+ has(requestId) {
24254
+ return map2.has(requestId);
24255
+ },
24256
+ all() {
24257
+ return [...map2.values()];
24258
+ },
24259
+ get size() {
24260
+ return map2.size;
24261
+ }
24262
+ };
24263
+ }
24264
+
24235
24265
  // bridge/bridge.ts
24236
24266
  var exports_bridge = {};
24237
24267
  import { dirname as dirname3, join as join5 } from "path";
24238
24268
  import { homedir as homedir4 } from "os";
24269
+ function flushOutstandingPermissionRequests() {
24270
+ if (!permissionRearmEnabled)
24271
+ return;
24272
+ if (!ipc || !ipc.isConnected())
24273
+ return;
24274
+ const pending = outstandingPermissions.all();
24275
+ if (pending.length === 0)
24276
+ return;
24277
+ process.stderr.write(`telegram bridge: re-sending ${pending.length} outstanding permission request(s) on gateway (re)connect
24278
+ `);
24279
+ for (const p of pending) {
24280
+ ipc.sendPermissionRequest({
24281
+ type: "permission_request",
24282
+ requestId: p.request_id,
24283
+ toolName: p.tool_name,
24284
+ description: p.description,
24285
+ inputPreview: p.input_preview
24286
+ });
24287
+ }
24288
+ }
24239
24289
  function onInbound(msg) {
24240
24290
  mcp.notification({
24241
24291
  method: "notifications/claude/channel",
@@ -24252,6 +24302,7 @@ function onPermission(msg) {
24252
24302
  if (msg.rule) {
24253
24303
  sessionAllowRules.add(msg.rule);
24254
24304
  }
24305
+ outstandingPermissions.delete(msg.requestId);
24255
24306
  mcp.notification({
24256
24307
  method: "notifications/claude/channel/permission",
24257
24308
  params: {
@@ -24305,6 +24356,7 @@ async function main() {
24305
24356
  onInbound,
24306
24357
  onPermission,
24307
24358
  onStatus,
24359
+ onConnect: flushOutstandingPermissionRequests,
24308
24360
  log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}
24309
24361
  `),
24310
24362
  livenessFilePath: process.env.SWITCHROOM_BRIDGE_ALIVE_PATH ?? join5(STATE_DIR, ".bridge-alive")
@@ -24318,7 +24370,7 @@ async function main() {
24318
24370
  }
24319
24371
  await mcp.connect(new StdioServerTransport);
24320
24372
  }
24321
- var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, EFFECTIVE_TOOL_SCHEMAS, sessionAllowRules, ipc = null, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null;
24373
+ var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, EFFECTIVE_TOOL_SCHEMAS, sessionAllowRules, outstandingPermissions, permissionRearmEnabled, ipc = null, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null;
24322
24374
  var init_bridge = __esm(async () => {
24323
24375
  init_server2();
24324
24376
  init_stdio2();
@@ -24681,6 +24733,23 @@ var init_bridge = __esm(async () => {
24681
24733
  required: ["chat_id", "key"]
24682
24734
  }
24683
24735
  },
24736
+ {
24737
+ name: "mental_model_propose",
24738
+ description: "Propose a Hindsight MENTAL MODEL for the operator to approve (agent-proposes \u2192 human-approves, hindsight Phase 5). Use this when \u2014 over real work \u2014 you notice a recurring, domain-specific question worth maintaining a standing, semantically-refreshed answer to from YOUR bank (e.g. a coach's `training-plan-state`, a lawyer's `open-matters`). You may PROPOSE but can NEVER self-approve: this renders a Telegram [Approve]/[Deny] card to the operator. On Approve the model is DECLARED \u2014 appended to your `memory.mental_models[]` in switchroom.yaml via the operator-approved config-edit path \u2014 and ensured in your bank (it then refreshes from your bank content). On Deny nothing is written. This is NOT for identity/'who is the user' (dedicated profile banks own that) and NOT a substitute for `retain` (store a fact) or `create_mental_model` where you already have direct Hindsight tools \u2014 it is the leashed, human-gated way to add a DURABLE declared model to your config. After firing this tool, END YOUR TURN cleanly \u2014 a fresh inbound arrives (`<channel source=\"mental_model_proposal_applied\">` / `mental_model_proposal_denied`) once the operator decides. Do NOT propose a model whose name is already declared (it is rejected), and do NOT spam (one card per proposal; the operator sees every one).",
24739
+ inputSchema: {
24740
+ type: "object",
24741
+ properties: {
24742
+ chat_id: { type: "string", description: "Chat to render the approval card in (use the chat_id of the user message that triggered the workflow)." },
24743
+ name: { type: "string", description: "Stable model name \u2014 the idempotent-ensure identity key (lowercase kebab/snake, e.g. `training-plan-state`). Must be UNIQUE among your already-declared models or the proposal is rejected." },
24744
+ source_query: { type: "string", description: `The reflection query the model answers, semantically refreshed from your bank content (e.g. "What is the athlete's current training plan, recent sessions, and open adjustments?"). Frame it as a DOMAIN question, never an identity question.` },
24745
+ reason: { type: "string", description: 'REQUIRED in practice \u2014 one-line rationale rendered on the card (e.g. "I keep re-deriving the plan state every session; a standing model would save the lookup"). Omitting it renders "why: not provided" and the operator will usually Deny.' },
24746
+ refresh_after_consolidation: { type: "boolean", description: "Refresh this model after each consolidation. Defaults OFF \u2014 refresh adds bounded background model-spend + timeout risk (RFC Phase 5). Only set true when the model genuinely needs to track fast-moving state." },
24747
+ max_tokens: { type: "number", description: "Optional cap on the synthesized model's token size." },
24748
+ message_thread_id: { type: "string", description: "Forum topic thread ID. Auto-applied from the last inbound message if not specified." }
24749
+ },
24750
+ required: ["chat_id", "name", "source_query"]
24751
+ }
24752
+ },
24684
24753
  {
24685
24754
  name: "linear_agent_activity",
24686
24755
  description: 'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) \u2014 pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge \u2192 work \u2192 result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
@@ -24804,6 +24873,8 @@ var init_bridge = __esm(async () => {
24804
24873
  }
24805
24874
  });
24806
24875
  sessionAllowRules = new Set;
24876
+ outstandingPermissions = createOutstandingPermissionLedger();
24877
+ permissionRearmEnabled = process.env.SWITCHROOM_PERMISSION_REARM !== "0";
24807
24878
  mcp.setNotificationHandler(exports_external.object({
24808
24879
  method: exports_external.literal("notifications/claude/channel/permission_request"),
24809
24880
  params: exports_external.object({
@@ -24825,9 +24896,22 @@ var init_bridge = __esm(async () => {
24825
24896
  return;
24826
24897
  }
24827
24898
  }
24899
+ if (permissionRearmEnabled) {
24900
+ outstandingPermissions.add({
24901
+ request_id: params.request_id,
24902
+ tool_name: params.tool_name,
24903
+ description: params.description,
24904
+ input_preview: params.input_preview
24905
+ });
24906
+ }
24828
24907
  if (!ipc || !ipc.isConnected()) {
24829
- process.stderr.write(`telegram bridge: permission_request received but not connected to gateway
24908
+ if (permissionRearmEnabled) {
24909
+ process.stderr.write(`telegram bridge: permission_request buffered (gateway offline), will re-send on reconnect ` + `request_id=${params.request_id}
24910
+ `);
24911
+ } else {
24912
+ process.stderr.write(`telegram bridge: permission_request received but not connected to gateway
24830
24913
  `);
24914
+ }
24831
24915
  return;
24832
24916
  }
24833
24917
  ipc.sendPermissionRequest({
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Deterministic silent-turn card climb — the framework owns "alive" and "for how
3
+ * long"; the model owns "what".
4
+ *
5
+ * Implements Phase 1 (the keystone) of `reference/rfcs/deterministic-turn-liveness.md`.
6
+ * Serves `reference/jobs/know-what-my-agent-is-doing.md` (outcome: at any moment
7
+ * during a turn the user can see the agent is alive and how long it has been
8
+ * working, without asking).
9
+ *
10
+ * ## The bug this closes
11
+ *
12
+ * On the 0-label path of `feedHeartbeatTick` (a turn that has surfaced NO tool
13
+ * label — a long single `Bash`, or only suppressed-by-design tools), the tick
14
+ * delegated to `openLivenessFeedIfDue`, whose WHEN-gate `shouldEarlyOpenLiveness`
15
+ * returns `false` the moment a card is already open (`activityMessageId != null`).
16
+ * Result: once the "Working…" card opened, the 0-label branch NEVER edited it
17
+ * again during a silent tool — the card froze on its opening elapsed while the
18
+ * agent was busy for tens of seconds. The only remaining signal was the ambient
19
+ * "stuck" reaction flip, which reads as *done*, not *working*.
20
+ *
21
+ * ## The fix
22
+ *
23
+ * Give the 0-label branch the SAME climbing-elapsed EDIT the labelled branch
24
+ * already performs: when a card is open and `mirrorLines` is empty, re-render the
25
+ * minimal "Working…" card with a fresh wall-clock elapsed on every heartbeat tick.
26
+ * The render is derived PURELY from `ageMs` (`now - turn.startedAt`), so a tool
27
+ * call blocked mid-flight — emitting nothing — cannot starve it. Worst-case gap
28
+ * between visible edits is one to two `FEED_HEARTBEAT_MIN_STALE_MS` windows
29
+ * (~6-12s), versus the observed ~50s freeze.
30
+ *
31
+ * This is a card EDIT, not a text send: it edits the card the chat already owns,
32
+ * and Telegram card edits do not push-notify — squarely the Ambient layer of
33
+ * `reference/rfcs/conversational-pacing.md`, crossing no invariant (no parallel
34
+ * surface, no mid-turn device buzz). When the model DOES narrate, its words land
35
+ * on the same card chronologically (`mirrorLines` becomes non-empty and the
36
+ * labelled-feed heartbeat takes over) — the climb fills only the gaps the model
37
+ * leaves and never overwrites narration.
38
+ *
39
+ * Kept as a small pure function so the guarantee can be proven at the transport
40
+ * boundary (Phase 4b regression test) rather than by grepping the gateway source.
41
+ */
42
+ import {
43
+ renderActivityFeedWithNested,
44
+ formatStepSuffix,
45
+ type SessionActivityHeader,
46
+ } from './tool-activity-summary.js'
47
+
48
+ /** The minimal placeholder line the silent-turn card carries when no tool label
49
+ * has surfaced. Matches `openLivenessFeedIfDue`'s open render so the climb edit
50
+ * is a clean continuation of the placeholder the OPEN path produced. */
51
+ export const SILENT_TURN_PLACEHOLDER = 'Working…'
52
+
53
+ export interface SilentTurnClimbInput {
54
+ /** `turn.mirrorLines.length`. 0 ⇒ no tool label has surfaced this turn (pure
55
+ * thinking / only suppressed tools) — the case this climb owns. >0 ⇒ a real
56
+ * label drives the labelled-feed heartbeat, which owns the climb instead. */
57
+ mirrorLineCount: number
58
+ /** `turn.activityMessageId`. `null` ⇒ no card is open yet, so the OPEN path
59
+ * (`openLivenessFeedIfDue`) owns this tick; non-null ⇒ a card is open and this
60
+ * branch must keep it climbing (the exact freeze this fix closes). */
61
+ activityMessageId: number | null
62
+ /** `turn.labeledToolCount` — surfaced in the card header's tool count. */
63
+ labeledToolCount: number
64
+ /** `now - turn.startedAt`: wall-clock elapsed since turn start. The single
65
+ * signal the model can never supply while blocked inside a tool. */
66
+ ageMs: number
67
+ }
68
+
69
+ /**
70
+ * The deterministic 0-label climb render. Returns the card body HTML to EDIT the
71
+ * already-open "Working…" card with a climbing wall-clock elapsed, or `null` when
72
+ * this branch must NOT edit:
73
+ * - `mirrorLineCount > 0` — a tool label surfaced; the labelled-feed heartbeat
74
+ * owns the climb (its edit cleanly replaces this placeholder).
75
+ * - `activityMessageId == null` — no card is open yet; the OPEN path
76
+ * (`openLivenessFeedIfDue`) owns this tick.
77
+ *
78
+ * The header's `elapsedMs` carries the climbing clock, so each tick's edit shows
79
+ * a higher "Working · Ns" than the last. Model-independent by construction: it
80
+ * reads only wall-clock `ageMs`.
81
+ */
82
+ export function silentTurnClimbRender(input: SilentTurnClimbInput): string | null {
83
+ if (input.mirrorLineCount > 0) return null
84
+ if (input.activityMessageId == null) return null
85
+ const header: SessionActivityHeader = {
86
+ label: 'Agent',
87
+ elapsedMs: input.ageMs,
88
+ toolCount: input.labeledToolCount,
89
+ state: 'running',
90
+ }
91
+ // Single "step" whose start is the turn start, so `ageMs` IS the step's own
92
+ // elapsed. formatStepSuffix keeps the `→` line timer-free until STEP_TIMER_MIN_MS;
93
+ // the header total still climbs every tick — that is the visible clock.
94
+ return renderActivityFeedWithNested(
95
+ [SILENT_TURN_PLACEHOLDER],
96
+ [],
97
+ false,
98
+ formatStepSuffix(input.ageMs),
99
+ undefined,
100
+ header,
101
+ )
102
+ }
103
+
104
+ /**
105
+ * The per-turn view `runSilentTurnHeartbeatTick` reads — a structural subset of
106
+ * the gateway's `CurrentTurn`, kept transport-free so this module (and its test)
107
+ * never imports the gateway.
108
+ */
109
+ export interface SilentTurnTickView {
110
+ /** `turn.mirrorLines.length` at tick time. */
111
+ mirrorLineCount: number
112
+ /** `turn.activityMessageId` at tick time (null ⇒ no card open yet). */
113
+ activityMessageId: number | null
114
+ /** `turn.labeledToolCount` — surfaced in the card header. */
115
+ labeledToolCount: number
116
+ /** `now - turn.startedAt` at tick time. */
117
+ ageMs: number
118
+ /** The labelled branch's stale floor (`FEED_HEARTBEAT_MIN_STALE_MS`). The
119
+ * climb applies the SAME guard for symmetry: a turn younger than one stale
120
+ * window is fresh (its card just opened ~1.2s in), so no climb edit yet.
121
+ * Without this the climb relied on the tick interval == the stale window
122
+ * (both 6s today) — an implicit coupling this guard makes explicit. */
123
+ minStaleMs: number
124
+ }
125
+
126
+ /**
127
+ * The gateway seams the tick drives, injected as thunks so THIS function — the
128
+ * shipped 0-label branch of `feedHeartbeatTick` — is directly executable in a
129
+ * test with spies at each seam. The gateway wires the real implementations
130
+ * (`openLivenessFeedIfDue`, `turn.activityPendingRender = …`, `cardDrainGate`,
131
+ * `ea.mayDrain`, `ea.openOrEditCard('liveness', …)`,
132
+ * `drainActivitySummary(turn, 'liveness')`); the wiring is pinned structurally
133
+ * in tests/feed-heartbeat-liveness-open.test.ts.
134
+ */
135
+ export interface SilentTurnTickDeps {
136
+ /** The one shared OPEN path (`openLivenessFeedIfDue(turn)`). Owns the open;
137
+ * the climb never opens (reply-is-last gating lives on the open path). */
138
+ openLivenessFeedIfDue: () => void
139
+ /** Stage the climb render (`turn.activityPendingRender = rendered`). */
140
+ setPendingRender: (rendered: string) => void
141
+ /** The centralized chatLock-serialized card-drain gate
142
+ * (`cardDrainGate(turn, ea, run)`). */
143
+ cardDrainGate: (run: () => void) => void
144
+ /** The single-flight pure read (`ea.mayDrain(turn)`). */
145
+ mayDrain: () => boolean
146
+ /** The emission-authority façade routing (`ea.openOrEditCard('liveness', apply)`).
147
+ * With a card open this only ever EDITs — Telegram edits don't push-notify. */
148
+ openOrEditCard: (apply: () => void) => void
149
+ /** The drain kick-off (`turn.activityInFlight = drainActivitySummary(turn,
150
+ * 'liveness')`) — the transport writer (sendMessage / editMessageText via
151
+ * robustApiCall) lives behind this seam. */
152
+ drain: () => void
153
+ }
154
+
155
+ /**
156
+ * The REAL 0-label branch of `feedHeartbeatTick` (deterministic-turn-liveness.md
157
+ * Phase 1) — extracted here so the shipped decision logic is directly under
158
+ * outcome-based test (`tests/silent-turn-climb-transport.test.ts`); the gateway
159
+ * IIFE cannot be imported in-process, so the tick body lives in this module and
160
+ * the gateway calls it with its real deps.
161
+ *
162
+ * Returns true when the tick was handled by the 0-label path (the caller must
163
+ * stop — the labelled-feed heartbeat does not run), false when a tool label has
164
+ * surfaced (`mirrorLineCount > 0`) and the labelled branch owns the tick.
165
+ *
166
+ * Behaviour:
167
+ * - no card open yet → delegate to the one shared OPEN path (never opens here);
168
+ * - card open + turn younger than `minStaleMs` → fresh, no edit yet (the same
169
+ * stale-guard the labelled branch applies to its step elapsed);
170
+ * - card open + stale → stage the climbing "Working · Ns" render and EDIT it
171
+ * through the same cardDrainGate → mayDrain → openOrEditCard → drain gate
172
+ * sequence the labelled branch uses. Deterministic + framework-owned: reads
173
+ * only wall-clock `ageMs`, so a blocked tool call cannot starve it.
174
+ */
175
+ export function runSilentTurnHeartbeatTick(
176
+ view: SilentTurnTickView,
177
+ deps: SilentTurnTickDeps,
178
+ ): boolean {
179
+ if (view.mirrorLineCount !== 0) return false
180
+ if (view.activityMessageId == null) {
181
+ deps.openLivenessFeedIfDue()
182
+ return true
183
+ }
184
+ // Stale-guard (symmetry with the labelled branch's `elapsed <
185
+ // FEED_HEARTBEAT_MIN_STALE_MS` check): the 0-label "step" starts at turn
186
+ // start, so `ageMs` IS its elapsed. A younger turn's card just opened.
187
+ if (view.ageMs < view.minStaleMs) return true
188
+ const rendered = silentTurnClimbRender({
189
+ mirrorLineCount: view.mirrorLineCount,
190
+ activityMessageId: view.activityMessageId,
191
+ labeledToolCount: view.labeledToolCount,
192
+ ageMs: view.ageMs,
193
+ })
194
+ if (rendered == null) return true
195
+ deps.setPendingRender(rendered)
196
+ deps.cardDrainGate(() => {
197
+ if (deps.mayDrain()) {
198
+ // Maintains an already-open card (guarded above on activityMessageId !=
199
+ // null) → only ever EDITs. 'liveness' producer is correct.
200
+ deps.openOrEditCard(() => {
201
+ deps.drain()
202
+ })
203
+ }
204
+ })
205
+ return true
206
+ }