agent-switchboard 2.2.1__tar.gz → 2.2.2__tar.gz

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 (264) hide show
  1. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/ARCS.md +2 -1
  2. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/PKG-INFO +1 -1
  3. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/claude-code.md +38 -0
  4. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/quickstart.md +2 -2
  5. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/upgrading.md +12 -0
  6. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/viewer/pyproject.toml +1 -1
  7. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/pyproject.toml +1 -1
  8. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/ROADMAP.md +99 -0
  9. agent_switchboard-2.2.2/roadmap/items/automatic-session-checkpoints.yaml +92 -0
  10. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/__init__.py +1 -1
  11. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/claude_session.py +103 -0
  12. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/cli.py +80 -0
  13. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_claude_session.py +100 -0
  14. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_dash_ids.py +34 -11
  15. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/.dockerignore +0 -0
  16. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/.github/workflows/ci.yml +0 -0
  17. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/.github/workflows/deploy.yml +0 -0
  18. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/.github/workflows/pages.yml +0 -0
  19. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/.github/workflows/publish-viewer.yml +0 -0
  20. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/.github/workflows/publish.yml +0 -0
  21. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/.github/workflows/roadmap.yml +0 -0
  22. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/.gitignore +0 -0
  23. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/AGENTS.md +0 -0
  24. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/CONTRIBUTING.md +0 -0
  25. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/Dockerfile +0 -0
  26. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/LICENSE +0 -0
  27. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/README.md +0 -0
  28. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/demo/README.md +0 -0
  29. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/demo/chess.sh +0 -0
  30. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/demo/run.sh +0 -0
  31. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docker-compose.yml +0 -0
  32. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/adaptive-timing.md +0 -0
  33. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/api.md +0 -0
  34. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/codex-cli.md +0 -0
  35. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/concepts.md +0 -0
  36. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/deployment.md +0 -0
  37. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/drills.md +0 -0
  38. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/encryption.md +0 -0
  39. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/environments.md +0 -0
  40. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/images/viewer.png +0 -0
  41. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/layers.md +0 -0
  42. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/managed-hub.md +0 -0
  43. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/model.md +0 -0
  44. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/seam.md +0 -0
  45. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/testing.md +0 -0
  46. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/viewer.md +0 -0
  47. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/docs/why-this-exists.md +0 -0
  48. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/examples/coordinated_worker.py +0 -0
  49. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/session-capsule/README.md +0 -0
  50. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/session-capsule/cc.sh +0 -0
  51. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/session-capsule/claude_session_capsule.py +0 -0
  52. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/viewer/README.md +0 -0
  53. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/__init__.py +0 -0
  54. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/__main__.py +0 -0
  55. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/viewer.py +0 -0
  56. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/index.html +0 -0
  57. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/render.js +0 -0
  58. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/switchboard-open.js +0 -0
  59. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/switchboard-room.js +0 -0
  60. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/README.md +0 -0
  61. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/arcs/distribution.yaml +0 -0
  62. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/arcs/hub-boundary.yaml +0 -0
  63. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/arcs/setup-and-first-run.yaml +0 -0
  64. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/arcs/ttl-ceilings.yaml +0 -0
  65. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/a-lobby-derived-from-the-key.yaml +0 -0
  66. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/abuse-control-after-authorization.yaml +0 -0
  67. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/board-ttl-ceiling.yaml +0 -0
  68. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/ci-workspace-is-public.yaml +0 -0
  69. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/clients-that-cannot-post.yaml +0 -0
  70. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/connect-failure-message.yaml +0 -0
  71. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/cross-key-rendezvous.yaml +0 -0
  72. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/every-silent-failure-looks-like-a-quiet-room.yaml +0 -0
  73. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/hooks-warning-false-positive.yaml +0 -0
  74. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/hub-origin-reachable-bypassing-the-edge.yaml +0 -0
  75. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/identity-rebinds-on-branch-change.yaml +0 -0
  76. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/init-writes-rooms-file.yaml +0 -0
  77. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/intermittent-suite-failure.yaml +0 -0
  78. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/joining-agent-sees-empty-inbox.yaml +0 -0
  79. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/known-rooms-address-book.yaml +0 -0
  80. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/one-resolved-context-across-surfaces.yaml +0 -0
  81. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/presence-ttl-is-not-one-size.yaml +0 -0
  82. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/provisioned-token-is-stale-and-nothing-says-so.yaml +0 -0
  83. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/publish-hub-container-image.yaml +0 -0
  84. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/read-only-rooms.yaml +0 -0
  85. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/robots-policy-for-public-hosts.yaml +0 -0
  86. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/roles-and-authority-between-agents.yaml +0 -0
  87. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/seal-agent-meta.yaml +0 -0
  88. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/selective-wake-for-the-listener.yaml +0 -0
  89. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/stale-resolver-references.yaml +0 -0
  90. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/stale-token-in-session-env.yaml +0 -0
  91. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/standing-checks-that-nothing-runs.yaml +0 -0
  92. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/timing-cold-start-in-ephemeral-environments.yaml +0 -0
  93. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/ttl-clamped-silently.yaml +0 -0
  94. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/unread-dms-not-shown-outside-mcp.yaml +0 -0
  95. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/roadmap/items/write-parity-across-surfaces.yaml +0 -0
  96. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/scripts/deploy.sh +0 -0
  97. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/auth.py +0 -0
  98. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/client.py +0 -0
  99. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/config.py +0 -0
  100. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/crypto.py +0 -0
  101. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/desktop.py +0 -0
  102. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/drill.py +0 -0
  103. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/guidance.py +0 -0
  104. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/handoff.py +0 -0
  105. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/holds.py +0 -0
  106. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/invite.py +0 -0
  107. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/knownrooms.py +0 -0
  108. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/load.py +0 -0
  109. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/mcp_server.py +0 -0
  110. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/notify.py +0 -0
  111. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/peers.py +0 -0
  112. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/rendezvous.py +0 -0
  113. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/rooms.py +0 -0
  114. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/scripts/wake-on-message.sh +0 -0
  115. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/server.py +0 -0
  116. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/signing.py +0 -0
  117. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/001-SKILL.md +0 -0
  118. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/002-SKILL.md +0 -0
  119. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/003-SKILL.md +0 -0
  120. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/004-SKILL.md +0 -0
  121. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/005-SKILL.md +0 -0
  122. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/006-SKILL.md +0 -0
  123. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/007-SKILL.md +0 -0
  124. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/008-SKILL.md +0 -0
  125. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/009-SKILL.md +0 -0
  126. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/010-SKILL.md +0 -0
  127. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/011-SKILL.md +0 -0
  128. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/012-SKILL.md +0 -0
  129. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/013-SKILL.md +0 -0
  130. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/014-SKILL.md +0 -0
  131. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/015-SKILL.md +0 -0
  132. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/016-SKILL.md +0 -0
  133. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/017-SKILL.md +0 -0
  134. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/018-SKILL.md +0 -0
  135. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/019-SKILL.md +0 -0
  136. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/020-SKILL.md +0 -0
  137. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/021-SKILL.md +0 -0
  138. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/022-SKILL.md +0 -0
  139. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/023-SKILL.md +0 -0
  140. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/024-SKILL.md +0 -0
  141. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/history/025-SKILL.md +0 -0
  142. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/skill/switchboard-coordinate/SKILL.md +0 -0
  143. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/spec.py +0 -0
  144. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/store.py +0 -0
  145. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/testing.py +0 -0
  146. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/timing.py +0 -0
  147. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/src/switchboard/writekey.py +0 -0
  148. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/conftest.py +0 -0
  149. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/README.md +0 -0
  150. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/__init__.py +0 -0
  151. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/analysis.py +0 -0
  152. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/economy.py +0 -0
  153. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/flow.py +0 -0
  154. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/llm.py +0 -0
  155. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/manager.py +0 -0
  156. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/report.py +0 -0
  157. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/run.py +0 -0
  158. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier1.json +0 -0
  159. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier1_labour.json +0 -0
  160. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier1_rounds.json +0 -0
  161. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_bound.json +0 -0
  162. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_built.json +0 -0
  163. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_free.json +0 -0
  164. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_paid.json +0 -0
  165. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_spend.json +0 -0
  166. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_told.json +0 -0
  167. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed41_bound.json +0 -0
  168. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter/traders.py +0 -0
  169. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter_experiment.py +0 -0
  170. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/barter_llm_experiment.py +0 -0
  171. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_harness.py +0 -0
  172. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/RESULTS.md +0 -0
  173. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_A_s31337.txt +0 -0
  174. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_A_s4242.txt +0 -0
  175. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_A_s7777.txt +0 -0
  176. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_B_s31337.txt +0 -0
  177. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_B_s4242.txt +0 -0
  178. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_B_s7777.txt +0 -0
  179. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_C_s31337.txt +0 -0
  180. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_C_s4242.txt +0 -0
  181. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_C_s7777.txt +0 -0
  182. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_A_s31337.json +0 -0
  183. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_A_s4242.json +0 -0
  184. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_A_s7777.json +0 -0
  185. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_B_s31337.json +0 -0
  186. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_B_s4242.json +0 -0
  187. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_B_s7777.json +0 -0
  188. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_C_s31337.json +0 -0
  189. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_C_s4242.json +0 -0
  190. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_C_s7777.json +0 -0
  191. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_A_s31337.json +0 -0
  192. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_A_s4242.json +0 -0
  193. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_A_s7777.json +0 -0
  194. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_B_s31337.json +0 -0
  195. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_B_s4242.json +0 -0
  196. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_B_s7777.json +0 -0
  197. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_C_s31337.json +0 -0
  198. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_C_s4242.json +0 -0
  199. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_C_s7777.json +0 -0
  200. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_A_s31337.json +0 -0
  201. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_A_s4242.json +0 -0
  202. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_A_s7777.json +0 -0
  203. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_B_s31337.json +0 -0
  204. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_B_s4242.json +0 -0
  205. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_B_s7777.json +0 -0
  206. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_C_s31337.json +0 -0
  207. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_C_s4242.json +0 -0
  208. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_C_s7777.json +0 -0
  209. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/tier2_run.py +0 -0
  210. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/experiments/timing_experiment.py +0 -0
  211. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_api.py +0 -0
  212. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_arrive.py +0 -0
  213. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_auth.py +0 -0
  214. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_barter.py +0 -0
  215. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_barter_llm.py +0 -0
  216. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_cli_board.py +0 -0
  217. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_cli_checkin.py +0 -0
  218. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_cli_config.py +0 -0
  219. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_cli_global_flags.py +0 -0
  220. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_cli_init.py +0 -0
  221. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_cli_serve.py +0 -0
  222. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_cli_session.py +0 -0
  223. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_cli_timing.py +0 -0
  224. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_client_invite.py +0 -0
  225. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_config.py +0 -0
  226. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_cors_write_headers.py +0 -0
  227. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_crypto.py +0 -0
  228. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_declare.py +0 -0
  229. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_deployment.py +0 -0
  230. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_desktop.py +0 -0
  231. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_drill.py +0 -0
  232. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_evening_replayed.py +0 -0
  233. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_example_viewer.py +0 -0
  234. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_failure_messages.py +0 -0
  235. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_handoff.py +0 -0
  236. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_help.py +0 -0
  237. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_invite.py +0 -0
  238. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_isolated_hub.py +0 -0
  239. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_known_rooms.py +0 -0
  240. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_leave_a_ghost.py +0 -0
  241. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_load.py +0 -0
  242. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_lobby.py +0 -0
  243. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_mcp.py +0 -0
  244. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_mcp_session.py +0 -0
  245. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_notify.py +0 -0
  246. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_rendezvous.py +0 -0
  247. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_rooms.py +0 -0
  248. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_spec.py +0 -0
  249. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_store.py +0 -0
  250. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_surface_parity.py +0 -0
  251. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_testing.py +0 -0
  252. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_tier2_harness.py +0 -0
  253. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_timing.py +0 -0
  254. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_timing_experiment.py +0 -0
  255. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_transient_failures.py +0 -0
  256. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_version.py +0 -0
  257. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_wake_listener.py +0 -0
  258. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_web_invite.py +0 -0
  259. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_web_page.py +0 -0
  260. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_web_reader.py +0 -0
  261. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_web_snapshot.py +0 -0
  262. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_whisper.py +0 -0
  263. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_workflow_token.py +0 -0
  264. {agent_switchboard-2.2.1 → agent_switchboard-2.2.2}/tests/test_write_keys.py +0 -0
@@ -79,7 +79,7 @@ under pressure.
79
79
 
80
80
  ### 🟠 The first ten minutes, for somebody who has not read the docs
81
81
 
82
- `setup-and-first-run` · 18 item(s), 12 startable
82
+ `setup-and-first-run` · 19 item(s), 13 startable
83
83
 
84
84
  `https://github.com/gald33/switchboard/issues/86` · `https://github.com/gald33/switchboard/issues/88` · `https://github.com/gald33/switchboard/issues/89` · `docs/environments.md`
85
85
 
@@ -101,6 +101,7 @@ wrong says so.
101
101
 
102
102
  | item | status | priority |
103
103
  |---|---|---|
104
+ | `automatic-session-checkpoints` | ready | next |
104
105
  | `connect-failure-message` | done | next |
105
106
  | `cross-key-rendezvous` | ready | next |
106
107
  | `init-writes-rooms-file` | ready | next |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-switchboard
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: An ephemeral orchestration hub for AI coding agents: presence, leases, messaging and a shared blackboard, all with TTLs.
5
5
  Project-URL: Homepage, https://github.com/gald33/switchboard
6
6
  Project-URL: Issues, https://github.com/gald33/switchboard/issues
@@ -466,6 +466,44 @@ are the two halves with no hub between them: a capsule file, created `0600`
466
466
  because it is as private as the transcript it carries, that you move however
467
467
  you like.
468
468
 
469
+ ### What a handoff cannot reach, and what to send instead
470
+
471
+ A handoff lands a transcript on a filesystem. That covers every environment
472
+ where a session can run a shell — your laptop, another machine, a cloud
473
+ container — because the receiving side collects the capsule and
474
+ `claude --resume` finds it. The first real handoff went cloud-container to
475
+ laptop that way.
476
+
477
+ It does not reach a session running in Claude's own cloud, and it is worth
478
+ saying why rather than leaving somebody to discover it. Checked against CLI
479
+ 2.1.259 on 2026-09-05: `--cloud <session_id>` **attaches** to a cloud session
480
+ that already exists, `--environment ccpool_…` creates a **new** one on a
481
+ self-hosted pool, and neither takes a transcript. The `/move-to-cloud` and
482
+ `/teleport` commands exist in the binary but are registered only for an
483
+ interactive terminal, and the teleport path seeds a fresh session with a
484
+ written summary rather than the conversation. There is no endpoint anywhere
485
+ that accepts a transcript, so a conversation cannot be uploaded to become a
486
+ cloud session, and nothing in this repo pretends otherwise.
487
+
488
+ Two things do work in that direction, and between them they cover most of
489
+ what people want from it.
490
+
491
+ **Consult it.** A cloud container can run `session receive` like anything
492
+ else. The transcript lands on its disk and the session *reads* it — greps for
493
+ the decision it needs, opens the twenty records that matter — instead of
494
+ resuming it. This is not a workaround for the missing upload: a 4.5 MB
495
+ transcript could not be injected into a context window even if an endpoint
496
+ existed, and consulting on demand is the only shape that scales.
497
+
498
+ **Send the shape of it.** `switchboard session brief <id>` prints what a
499
+ session was for, read off the record rather than retold by a model: what the
500
+ human asked, which files were touched, how it worked. On the first real
501
+ handoff that is 960 bytes out of 4.5 MB, and it fits in a message you can
502
+ paste anywhere — including a cloud session in a browser that no capsule can
503
+ reach. The filtering is the substance: Claude Code writes tool results, hook
504
+ output and notifications as `user` records too, so 285 user turns there
505
+ contain 14 things a human actually typed.
506
+
469
507
  ### If you work in the desktop app
470
508
 
471
509
  Installing the transcript is the whole job for a CLI user and half of one for
@@ -19,7 +19,7 @@ switchboard serve --port 8787
19
19
  You should see:
20
20
 
21
21
  ```
22
- switchboard 2.2.1 → http://127.0.0.1:8787 db=switchboard.db
22
+ switchboard 2.2.2 → http://127.0.0.1:8787 db=switchboard.db
23
23
  ```
24
24
 
25
25
  Leave it running.
@@ -47,7 +47,7 @@ export SWITCHBOARD_AGENT_ID=beta
47
47
 
48
48
  ```bash
49
49
  $ switchboard health
50
- { "ok": true, "version": "2.2.1", "auth": true }
50
+ { "ok": true, "version": "2.2.2", "auth": true }
51
51
 
52
52
  $ switchboard register -c build
53
53
  registered local-main-yourhost (local) in demo
@@ -47,6 +47,18 @@ Additive in the same release: `--no-subagents` on `session export`,
47
47
  subagent transcripts out of a capsule, and the viewer can save an opened
48
48
  board value to a file. Neither changes an existing default.
49
49
 
50
+ ## 2.2.1 → 2.2.2
51
+
52
+ Additive. `switchboard session brief <id>` prints what a session was for —
53
+ the human's instructions, the files touched, the tools used — read off the
54
+ transcript rather than retold by a model. It exists for the place a capsule
55
+ cannot reach: a session running in Claude's own cloud, which accepts no
56
+ transcript through any endpoint. On a 4.5 MB transcript the brief is under a
57
+ kilobyte, so it fits in a message you can paste anywhere.
58
+
59
+ `docs/claude-code.md` now states plainly what a handoff can and cannot reach,
60
+ checked against Claude Code 2.1.259. Nothing that worked in 2.2.1 changed.
61
+
50
62
  ## 2.1.0 → 2.2.0
51
63
 
52
64
  Additive. A whole Claude Code session can be moved between environments
@@ -19,7 +19,7 @@ build-backend = "hatchling.build"
19
19
 
20
20
  [project]
21
21
  name = "switchboard-viewer"
22
- version = "0.10.0"
22
+ version = "0.11.0"
23
23
  description = "A read-only page showing your Switchboard rooms to a human: who is awake, what is claimed, and the conversation as it happens."
24
24
  readme = "README.md"
25
25
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-switchboard"
3
- version = "2.2.1"
3
+ version = "2.2.2"
4
4
  description = "An ephemeral orchestration hub for AI coding agents: presence, leases, messaging and a shared blackboard, all with TTLs."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -17,8 +17,12 @@ Claim before starting: `roadmap claim <key>`
17
17
  - ↔ related: **`init-writes-rooms-file`** — Both decide where a room identifier is allowed to live. This one is about an identifier that should not have been committed; that one proposes that `init` start committing a rooms record carrying a workspace token by default. Settle the rule here first, or `init` ships the same mistake as the default for every adopter.
18
18
  - ↔ related: **`read-only-rooms`** — Half of that item — anyone who can read the repo can post as CI — is closed by minting the CI room write-protected: the identifier can stay committed, since knowing it no longer lets anyone write into it. The other half, sealing what CI announces, is unchanged.
19
19
  - `now` **`stale-resolver-references`** — Delete the comments describing auth machinery that no longer exists
20
+ - `next` **`automatic-session-checkpoints`** — A session that ends anywhere is collectable everywhere, without anyone running a command
21
+ - ↔ related: **`cross-key-rendezvous`** — Shares the addressing problem from the other end. That one asks how a stranger finds you; this one asks which of several checkpoints is the one you meant. Both resolve to "the tool decides, the agent does not choose", so settle the rule in one place rather than twice.
22
+ - ↔ related: **`selective-wake-for-the-listener`** — Both are about a session being reachable across a gap the agent is not awake for. That one wakes a parked session when a message lands; this one makes sure the session's own context survives the gap at all. Read that one first for the hook and background-process conventions this must follow.
20
23
  - `next` **`cross-key-rendezvous`** — Two agents that share no key have no room to meet in, and a human had to carry the coordinates
21
24
  - ↔ related: **`a-lobby-derived-from-the-key`** — That one made the lobby unguessable by deriving it from the key, deliberately rejecting a well-known room. This is the cost of that decision, observed: holders of different keys have no lobby in common. Read it first; the fix here keeps its rejection and works around it, rather than reopening it.
25
+ - ↔ related: **`automatic-session-checkpoints`** — Shares the addressing problem from the other end. That one asks how a stranger finds you; this one asks which of several checkpoints is the one you meant. Both resolve to "the tool decides, the agent does not choose", so settle the rule in one place rather than twice.
22
26
  - ↔ related: **`known-rooms-address-book`** — That one gives a project a room strangers can find. This is what an agent does with rooms once it knows them: a published meeting room, an invite it was handed, a side room it minted, its repo rooms — one list, swept when searching, chosen from when parking. Do that one first; this consumes what it publishes.
23
27
  - ↔ related: **`selective-wake-for-the-listener`** — The other half of the same evening: a turn-based session that cannot park a listener returns on shared slots instead, which worked. Meeting failed one layer up — there was no shared room for the slots to be in.
24
28
  - `next` **`init-writes-rooms-file`** — Make init produce the rooms record the model says is authoritative
@@ -60,6 +64,7 @@ Claim before starting: `roadmap claim <key>`
60
64
  - ↔ related: **`every-silent-failure-looks-like-a-quiet-room`** — The symptom when this fires is a 401 on the published credential, which two agents spent a day mis-attributing — first to a stale client, then to a perimeter that had moved. Read that item for why a wrong credential is hard to tell from a quiet room.
61
65
  - **`selective-wake-for-the-listener`** — Wake the listener on what matters, and on the time it promised, not on every message
62
66
  - ↔ related: **`a-lobby-derived-from-the-key`** — What makes a lobby worth having rather than merely tidy: an agent parked with `listen` is visible on a roster, so a lobby is a place peers can actually be found waiting rather than a room that is empty whenever nobody is mid-turn.
67
+ - ↔ related: **`automatic-session-checkpoints`** — Both are about a session being reachable across a gap the agent is not awake for. That one wakes a parked session when a message lands; this one makes sure the session's own context survives the gap at all. Read that one first for the hook and background-process conventions this must follow.
63
68
  - ↔ related: **`cross-key-rendezvous`** — The other half of the same evening: a turn-based session that cannot park a listener returns on shared slots instead, which worked. Meeting failed one layer up — there was no shared room for the slots to be in.
64
69
  - ↔ related: **`every-silent-failure-looks-like-a-quiet-room`** — Cause 8 below is a defect in what that item builds on — a parked listener watches the inbox of the id it started under, and a `git checkout` re-derives that id without telling either side. The fix belongs in `listen`, not here.
65
70
  - ↔ related: **`known-rooms-address-book`** — Same primitive, other axis. That one is about which *messages* wake a parked listener; this is about which *rooms* it is parked in, and why it need not be all of them.
@@ -110,6 +115,7 @@ _Nothing blocked._
110
115
  graph TD
111
116
  a_lobby_derived_from_the_key["Give every key a lobby, so agents that share one can find each other without naming a room"]
112
117
  abuse_control_after_authorization["Replace the abuse control that per-token authorization used to provide"]
118
+ automatic_session_checkpoints["A session that ends anywhere is collectable everywhere, without anyone running a command"]
113
119
  board_ttl_ceiling["Decide whether a board value has earned seven times a lease's lifetime"]
114
120
  ci_workspace_is_public["Stop publishing the one room identifier that was never meant to be guessable"]
115
121
  clients_that_cannot_post["Decide what a client that cannot hold a secret or issue arbitrary HTTP gets"]
@@ -144,6 +150,8 @@ graph TD
144
150
  a_lobby_derived_from_the_key -.- one_resolved_context_across_surfaces
145
151
  a_lobby_derived_from_the_key -.- selective_wake_for_the_listener
146
152
  abuse_control_after_authorization -.- ci_workspace_is_public
153
+ automatic_session_checkpoints -.- cross_key_rendezvous
154
+ automatic_session_checkpoints -.- selective_wake_for_the_listener
147
155
  board_ttl_ceiling -.- ttl_clamped_silently
148
156
  ci_workspace_is_public -.- init_writes_rooms_file
149
157
  ci_workspace_is_public -.- read_only_rooms
@@ -282,6 +290,95 @@ graph TD
282
290
 
283
291
  </details>
284
292
 
293
+ ### `automatic-session-checkpoints`
294
+
295
+ - **title:** A session that ends anywhere is collectable everywhere, without anyone running a command
296
+ - **status:** ready
297
+ - **arc:** setup-and-first-run
298
+ - **priority:** next
299
+ - **related to** (not a dependency — both are startable):
300
+ - `cross-key-rendezvous` — Shares the addressing problem from the other end. That one asks how a stranger finds you; this one asks which of several checkpoints is the one you meant. Both resolve to "the tool decides, the agent does not choose", so settle the rule in one place rather than twice.
301
+ - `selective-wake-for-the-listener` — Both are about a session being reachable across a gap the agent is not awake for. That one wakes a parked session when a message lands; this one makes sure the session's own context survives the gap at all. Read that one first for the hook and background-process conventions this must follow.
302
+ - **refs:**
303
+ - `src/switchboard/handoff.py`
304
+ - `src/switchboard/claude_session.py`
305
+ - `docs/claude-code.md`
306
+ - `https://github.com/gald33/switchboard/pull/229`
307
+
308
+ <details><summary>evidence</summary>
309
+
310
+ > **Inferred from a design conversation on 2026-09-05, after the first real
311
+ > handoff the night before.** Nothing is broken; the mechanism works. What this
312
+ > item records is what the working mechanism still asks of a human, and one
313
+ > fragility that only shows up when something goes wrong.
314
+ >
315
+ > **The question that started it: how does Cursor move sessions between cloud
316
+ > and local so smoothly, and can we?** Reading Cursor's own store on a machine
317
+ > that runs it answers the first half. Its `conversations` table tags each row
318
+ > with a `source` — 496 `local` against 47 `cloud-cache` on the machine
319
+ > inspected — the cloud ones carry a server-side `scope` hash the local ones
320
+ > lack, the table has `root_fingerprint` and `cache_fingerprint` columns, and a
321
+ > `conversation_search_reconciliation` table holds a sync `cursor`. That is a
322
+ > client cache over a server-authoritative store. Cursor does not move a
323
+ > session between cloud and local; both ends are clients of one record its
324
+ > servers hold, so there is nothing to move.
325
+ >
326
+ > **That is exactly the trade Switchboard must not make.** Copying it means the
327
+ > hub storing transcripts it currently cannot read, which is the single
328
+ > property the whole design exists to protect (`docs/model.md`). The local-first
329
+ > choice is what let a session cross from a cloud container to a laptop on
330
+ > 2026-09-04 between two agents sharing no account, sealed with a key the hub
331
+ > never saw. A cache-and-sync design cannot do that, because the server has to
332
+ > hold the conversation in order to synchronise it.
333
+ >
334
+ > **So the achievable version of "seamless" is not shared storage — it is that
335
+ > nobody types a command.** A Cursor user does not experience syncing; they
336
+ > experience "it was there". That experience needs the handoff to happen at the
337
+ > boundaries automatically, and every part is already built:
338
+ >
339
+ > - `session publish` with no recipient is already a **checkpoint** — the
340
+ > capsule goes up sealed, addressed to nobody, collectable by anyone holding
341
+ > the room's key, which in practice is your own other machines
342
+ > (`handoff.py`).
343
+ > - `init` already installs `SessionStart` and `Stop` hooks
344
+ > (`cli.py`, `_session_start_cmd` / `_stop_cmd`).
345
+ > - `session receive` already takes `--wait`.
346
+ >
347
+ > Wiring them is the work: checkpoint on `Stop` and on going idle, collect on
348
+ > `SessionStart`.
349
+ >
350
+ > **And it closes a fragility, which is the stronger half of the case.** A
351
+ > handoff is explicit today, so a session that dies unexpectedly leaves nothing
352
+ > behind — the 2026-09-04 transfer survived only because the cloud agent chose
353
+ > to publish before it finished. Cursor survives that case because the server
354
+ > already holds the conversation. Periodic self-checkpointing is how a
355
+ > local-first design gets the same durability without giving anything away: the
356
+ > last checkpoint is always collectable and expires on its own, so nothing
357
+ > accumulates on the hub.
358
+ >
359
+ > **The open question, and the reason this is an item rather than a patch.**
360
+ > Addressing. When several sessions have checkpointed, a starting session must
361
+ > not collect all of them, and "the newest" is not obviously right either.
362
+ > `--from` and sender verification are the raw material; the rule is not
363
+ > decided. Most likely: the most recent checkpoint for **this repo** from **this
364
+ > account**, with everything else needing to be asked for by id. Settle it
365
+ > together with `cross-key-rendezvous`, which has the same shape of question.
366
+ >
367
+ > **What this must not promise, and the docs should say so rather than let
368
+ > somebody find out.** Two live sessions cannot edit one conversation: without
369
+ > an authority to reconcile against, a handoff is a baton and not a mirror.
370
+ > Continuity is only as good as the checkpoint interval. And a transcript is
371
+ > large — the first real one was 19.8 MB across 175 files — so checkpointing
372
+ > every turn is not free, which argues for idle and end-of-session rather than
373
+ > continuous.
374
+ >
375
+ > **Done when** a session that ends on one machine is collectable on another
376
+ > with nobody running `handoff` or `receive`, when a session killed without
377
+ > warning still leaves a collectable checkpoint, and when the addressing rule is
378
+ > written down and tested rather than left to whoever calls `receive` first.
379
+
380
+ </details>
381
+
285
382
  ### `board-ttl-ceiling`
286
383
 
287
384
  - **title:** Decide whether a board value has earned seven times a lease's lifetime
@@ -484,6 +581,7 @@ graph TD
484
581
  - **priority:** next
485
582
  - **related to** (not a dependency — both are startable):
486
583
  - `a-lobby-derived-from-the-key` — That one made the lobby unguessable by deriving it from the key, deliberately rejecting a well-known room. This is the cost of that decision, observed: holders of different keys have no lobby in common. Read it first; the fix here keeps its rejection and works around it, rather than reopening it.
584
+ - `automatic-session-checkpoints` — Shares the addressing problem from the other end. That one asks how a stranger finds you; this one asks which of several checkpoints is the one you meant. Both resolve to "the tool decides, the agent does not choose", so settle the rule in one place rather than twice.
487
585
  - `known-rooms-address-book` — That one gives a project a room strangers can find. This is what an agent does with rooms once it knows them: a published meeting room, an invite it was handed, a side room it minted, its repo rooms — one list, swept when searching, chosen from when parking. Do that one first; this consumes what it publishes.
488
586
  - `selective-wake-for-the-listener` — The other half of the same evening: a turn-based session that cannot park a listener returns on shared slots instead, which worked. Meeting failed one layer up — there was no shared room for the slots to be in.
489
587
  - **refs:**
@@ -1744,6 +1842,7 @@ graph TD
1744
1842
  - **arc:** setup-and-first-run
1745
1843
  - **related to** (not a dependency — both are startable):
1746
1844
  - `a-lobby-derived-from-the-key` — What makes a lobby worth having rather than merely tidy: an agent parked with `listen` is visible on a roster, so a lobby is a place peers can actually be found waiting rather than a room that is empty whenever nobody is mid-turn.
1845
+ - `automatic-session-checkpoints` — Both are about a session being reachable across a gap the agent is not awake for. That one wakes a parked session when a message lands; this one makes sure the session's own context survives the gap at all. Read that one first for the hook and background-process conventions this must follow.
1747
1846
  - `cross-key-rendezvous` — The other half of the same evening: a turn-based session that cannot park a listener returns on shared slots instead, which worked. Meeting failed one layer up — there was no shared room for the slots to be in.
1748
1847
  - `every-silent-failure-looks-like-a-quiet-room` — Cause 8 below is a defect in what that item builds on — a parked listener watches the inbox of the id it started under, and a `git checkout` re-derives that id without telling either side. The fix belongs in `listen`, not here.
1749
1848
  - `known-rooms-address-book` — Same primitive, other axis. That one is about which *messages* wake a parked listener; this is about which *rooms* it is parked in, and why it need not be all of them.
@@ -0,0 +1,92 @@
1
+ id: automatic-session-checkpoints
2
+ title: A session that ends anywhere is collectable everywhere, without anyone running a command
3
+ status: ready
4
+ arc: setup-and-first-run
5
+ priority: next
6
+ blocked_on: []
7
+ related_to:
8
+ - key: selective-wake-for-the-listener
9
+ note: 'Both are about a session being reachable across a gap the agent is not awake
10
+ for. That one wakes a parked session when a message lands; this one makes sure the
11
+ session''s own context survives the gap at all. Read that one first for the hook
12
+ and background-process conventions this must follow.'
13
+ - key: cross-key-rendezvous
14
+ note: 'Shares the addressing problem from the other end. That one asks how a stranger
15
+ finds you; this one asks which of several checkpoints is the one you meant. Both
16
+ resolve to "the tool decides, the agent does not choose", so settle the rule in
17
+ one place rather than twice.'
18
+ refs:
19
+ - 'src/switchboard/handoff.py'
20
+ - 'src/switchboard/claude_session.py'
21
+ - 'docs/claude-code.md'
22
+ - 'https://github.com/gald33/switchboard/pull/229'
23
+ evidence: |
24
+ **Inferred from a design conversation on 2026-09-05, after the first real
25
+ handoff the night before.** Nothing is broken; the mechanism works. What this
26
+ item records is what the working mechanism still asks of a human, and one
27
+ fragility that only shows up when something goes wrong.
28
+
29
+ **The question that started it: how does Cursor move sessions between cloud
30
+ and local so smoothly, and can we?** Reading Cursor's own store on a machine
31
+ that runs it answers the first half. Its `conversations` table tags each row
32
+ with a `source` — 496 `local` against 47 `cloud-cache` on the machine
33
+ inspected — the cloud ones carry a server-side `scope` hash the local ones
34
+ lack, the table has `root_fingerprint` and `cache_fingerprint` columns, and a
35
+ `conversation_search_reconciliation` table holds a sync `cursor`. That is a
36
+ client cache over a server-authoritative store. Cursor does not move a
37
+ session between cloud and local; both ends are clients of one record its
38
+ servers hold, so there is nothing to move.
39
+
40
+ **That is exactly the trade Switchboard must not make.** Copying it means the
41
+ hub storing transcripts it currently cannot read, which is the single
42
+ property the whole design exists to protect (`docs/model.md`). The local-first
43
+ choice is what let a session cross from a cloud container to a laptop on
44
+ 2026-09-04 between two agents sharing no account, sealed with a key the hub
45
+ never saw. A cache-and-sync design cannot do that, because the server has to
46
+ hold the conversation in order to synchronise it.
47
+
48
+ **So the achievable version of "seamless" is not shared storage — it is that
49
+ nobody types a command.** A Cursor user does not experience syncing; they
50
+ experience "it was there". That experience needs the handoff to happen at the
51
+ boundaries automatically, and every part is already built:
52
+
53
+ - `session publish` with no recipient is already a **checkpoint** — the
54
+ capsule goes up sealed, addressed to nobody, collectable by anyone holding
55
+ the room's key, which in practice is your own other machines
56
+ (`handoff.py`).
57
+ - `init` already installs `SessionStart` and `Stop` hooks
58
+ (`cli.py`, `_session_start_cmd` / `_stop_cmd`).
59
+ - `session receive` already takes `--wait`.
60
+
61
+ Wiring them is the work: checkpoint on `Stop` and on going idle, collect on
62
+ `SessionStart`.
63
+
64
+ **And it closes a fragility, which is the stronger half of the case.** A
65
+ handoff is explicit today, so a session that dies unexpectedly leaves nothing
66
+ behind — the 2026-09-04 transfer survived only because the cloud agent chose
67
+ to publish before it finished. Cursor survives that case because the server
68
+ already holds the conversation. Periodic self-checkpointing is how a
69
+ local-first design gets the same durability without giving anything away: the
70
+ last checkpoint is always collectable and expires on its own, so nothing
71
+ accumulates on the hub.
72
+
73
+ **The open question, and the reason this is an item rather than a patch.**
74
+ Addressing. When several sessions have checkpointed, a starting session must
75
+ not collect all of them, and "the newest" is not obviously right either.
76
+ `--from` and sender verification are the raw material; the rule is not
77
+ decided. Most likely: the most recent checkpoint for **this repo** from **this
78
+ account**, with everything else needing to be asked for by id. Settle it
79
+ together with `cross-key-rendezvous`, which has the same shape of question.
80
+
81
+ **What this must not promise, and the docs should say so rather than let
82
+ somebody find out.** Two live sessions cannot edit one conversation: without
83
+ an authority to reconcile against, a handoff is a baton and not a mirror.
84
+ Continuity is only as good as the checkpoint interval. And a transcript is
85
+ large — the first real one was 19.8 MB across 175 files — so checkpointing
86
+ every turn is not free, which argues for idle and end-of-session rather than
87
+ continuous.
88
+
89
+ **Done when** a session that ends on one machine is collectable on another
90
+ with nobody running `handoff` or `receive`, when a session killed without
91
+ warning still leaves a collectable checkpoint, and when the addressing rule is
92
+ written down and tested rather than left to whoever calls `receive` first.
@@ -47,7 +47,7 @@ from .store import (
47
47
  from .timing import unwrap_forecast, wrap_forecast
48
48
  from .writekey import RoomWriteKey, generate_write_key
49
49
 
50
- __version__ = "2.2.1"
50
+ __version__ = "2.2.2"
51
51
 
52
52
  __all__ = [
53
53
  "Invite",
@@ -224,6 +224,109 @@ def transcript_metadata(path: Path) -> dict[str, Any]:
224
224
  return meta
225
225
 
226
226
 
227
+ #: Wrappers the harness puts in `user` records that no human typed: tool
228
+ #: results, hook output, notifications, slash-command echoes. A brief that
229
+ #: counted these as instructions would report hundreds of turns for a session
230
+ #: whose human said twenty things.
231
+ _NOT_TYPED = (
232
+ "<task-notification", "<system-reminder", "<local-command", "<command-name",
233
+ "<command-message", "<command-args", "<user-prompt-submit-hook",
234
+ # Not a wrapper but not an instruction either: the harness writes this
235
+ # where a human pressed escape, and it says nothing about the work.
236
+ "[Request interrupted",
237
+ )
238
+ #: How much of one instruction a brief keeps. Long enough to recognise what was
239
+ #: asked, short enough that the whole brief stays pasteable.
240
+ BRIEF_CHARS = 240
241
+
242
+
243
+ def _record_text(record: dict[str, Any]) -> str:
244
+ """The prose of a record, ignoring tool calls and results."""
245
+ content = (record.get("message") or {}).get("content")
246
+ if isinstance(content, str):
247
+ return content
248
+ if not isinstance(content, list):
249
+ return ""
250
+ return "\n".join(
251
+ part.get("text", "") for part in content
252
+ if isinstance(part, dict) and part.get("type") == "text"
253
+ )
254
+
255
+
256
+ def brief(path: Path, *, limit: int = 0) -> dict[str, Any]:
257
+ """What a session was *about*, small enough to paste into another one.
258
+
259
+ A transcript is the wrong thing to hand a fresh session even where the
260
+ machinery allows it: this one is 4.5 MB, which no context window holds, and
261
+ replaying it would cost more than the work it describes. What actually
262
+ carries is much smaller — what the human asked for, which files were
263
+ touched, how the session worked. Read off the record rather than
264
+ summarised by a model, so it states what happened instead of what a
265
+ retelling remembers.
266
+
267
+ The filtering is the substance. Claude Code writes tool results, hook
268
+ output and notifications as ``user`` records too, so a naive count reports
269
+ hundreds of "instructions" for a session whose human typed twenty. Only
270
+ prose that no wrapper claims is kept.
271
+
272
+ ``limit`` caps the instructions returned, newest first after the opening
273
+ one — a long session's shape is its first ask and its last few, not its
274
+ middle.
275
+ """
276
+ records = 0
277
+ instructions: list[str] = []
278
+ files: dict[str, int] = {}
279
+ tools: dict[str, int] = {}
280
+ with path.open("rb") as fh:
281
+ for raw in fh:
282
+ records += 1
283
+ try:
284
+ record = json.loads(raw)
285
+ except ValueError:
286
+ continue
287
+ if not isinstance(record, dict):
288
+ continue
289
+ kind = record.get("type")
290
+ if kind == "user":
291
+ text = " ".join(_record_text(record).split())
292
+ if text and not text.startswith(_NOT_TYPED):
293
+ text = text[:BRIEF_CHARS]
294
+ # A repeated instruction is usually the same ask re-sent
295
+ # after an interruption, not a second thing wanted.
296
+ if not instructions or instructions[-1] != text:
297
+ instructions.append(text)
298
+ elif kind == "assistant":
299
+ content = (record.get("message") or {}).get("content")
300
+ if not isinstance(content, list):
301
+ continue
302
+ for part in content:
303
+ if not isinstance(part, dict) or part.get("type") != "tool_use":
304
+ continue
305
+ name = str(part.get("name") or "?")
306
+ tools[name] = tools.get(name, 0) + 1
307
+ target = (part.get("input") or {}).get("file_path")
308
+ if isinstance(target, str) and target:
309
+ files[target] = files.get(target, 0) + 1
310
+ kept = instructions
311
+ if limit and len(instructions) > limit:
312
+ # The opening ask plus the most recent ones: a session's shape is what
313
+ # it was for and where it got to, not the middle.
314
+ kept = instructions[:1] + instructions[-(limit - 1):]
315
+ meta = transcript_metadata(path)
316
+ return {
317
+ "records": records,
318
+ "cwd": meta["cwd"],
319
+ "git_branch": meta["git_branch"],
320
+ "version": meta["version"],
321
+ "user_messages": meta["user_messages"],
322
+ "assistant_messages": meta["assistant_messages"],
323
+ "instructions": kept,
324
+ "instructions_total": len(instructions),
325
+ "files": sorted(files, key=lambda f: -files[f]),
326
+ "tools": dict(sorted(tools.items(), key=lambda kv: -kv[1])),
327
+ }
328
+
329
+
227
330
  # -------------------------------------------------------------- package ---
228
331
 
229
332
  def _sha256(data: bytes) -> str:
@@ -3150,6 +3150,8 @@ def cmd_session(args: argparse.Namespace) -> int:
3150
3150
  return _session_resume(args)
3151
3151
  if action in ("register", "unregister"):
3152
3152
  return _session_register(args)
3153
+ if action == "brief":
3154
+ return _session_brief(args)
3153
3155
  if action in ("handoff", "publish"):
3154
3156
  return _session_publish(args, fmt)
3155
3157
  return _session_receive(args, fmt)
@@ -3260,6 +3262,64 @@ def _session_register(args: argparse.Namespace) -> int:
3260
3262
  return EXIT_OK if report.get("registered") else EXIT_ERROR
3261
3263
 
3262
3264
 
3265
+ def _session_brief(args: argparse.Namespace) -> int:
3266
+ """What a session was about, small enough to paste into another one.
3267
+
3268
+ The answer to a question `receive` cannot address: a cloud session in
3269
+ somebody's browser cannot be handed a transcript — no endpoint accepts one
3270
+ — and could not hold this one if it could, since 4.5 MB does not fit in a
3271
+ context window. What travels is the shape: what was asked, what was
3272
+ touched, how it worked. Read off the record rather than retold by a model.
3273
+ """
3274
+ session_id = claude_session.valid_session_id(args.session_id)
3275
+ cfg = claude_session.config_dir(getattr(args, "config_dir", None))
3276
+ found = claude_session.find_transcripts(cfg, session_id)
3277
+ if not found:
3278
+ print(f"error: no transcript for {session_id} under {cfg / 'projects'}",
3279
+ file=sys.stderr)
3280
+ return EXIT_ERROR
3281
+ if len(found) > 1 and not args.cwd:
3282
+ where = ", ".join(str(p.parent.name) for p in found)
3283
+ print(f"error: {session_id} is under more than one project key ({where}); "
3284
+ f"name the directory with --cwd", file=sys.stderr)
3285
+ return EXIT_ERROR
3286
+ path = found[0]
3287
+ if args.cwd:
3288
+ key = claude_session.project_key(args.cwd)
3289
+ path = next((p for p in found if p.parent.name == key), found[0])
3290
+ report = claude_session.brief(path, limit=args.limit)
3291
+ if args.json:
3292
+ _print_json({"session_id": session_id, **report})
3293
+ return EXIT_OK
3294
+ fmt = Fmt(_use_color(sys.stdout))
3295
+ print(fmt.bold(f"session {session_id}"))
3296
+ print(f" {report['records']} records · {report['user_messages']} user / "
3297
+ f"{report['assistant_messages']} assistant turns · "
3298
+ f"claude {report['version'] or '?'}")
3299
+ if report["cwd"]:
3300
+ branch = f" on {report['git_branch']}" if report["git_branch"] else ""
3301
+ print(f" worked in {report['cwd']}{branch}")
3302
+ if report["tools"]:
3303
+ top = ", ".join(f"{name} {count}" for name, count in
3304
+ list(report["tools"].items())[:5])
3305
+ print(f" tools: {top}")
3306
+ if report["files"]:
3307
+ shown = ", ".join(Path(f).name for f in report["files"][:8])
3308
+ more = f" (+{len(report['files']) - 8})" if len(report["files"]) > 8 else ""
3309
+ print(f" files: {shown}{more}")
3310
+ total = report["instructions_total"]
3311
+ kept = len(report["instructions"])
3312
+ heading = "what was asked" + (f" ({kept} of {total})" if kept < total else
3313
+ f" ({total})")
3314
+ print(f"\n{fmt.bold(heading)}")
3315
+ for line in report["instructions"]:
3316
+ print(f" - {line}")
3317
+ if not report["instructions"]:
3318
+ print(fmt.dim(" (nothing a human typed — every user record is tool "
3319
+ "output or a notification)"))
3320
+ return EXIT_OK
3321
+
3322
+
3263
3323
  def _session_import(args: argparse.Namespace) -> int:
3264
3324
  capsule = claude_session.read_capsule(args.capsule_file)
3265
3325
  result = claude_session.install(capsule, cwd=args.cwd, force=args.force)
@@ -6624,6 +6684,26 @@ def build_parser() -> argparse.ArgumentParser:
6624
6684
  s.add_argument("--any-sender", action="store_true",
6625
6685
  help="with --resume: start sessions from any roster-verified sender "
6626
6686
  "(never one nobody vouched for)")
6687
+ s = ssub.add_parser(
6688
+ "brief",
6689
+ help="what a session was about, small enough to paste into another one",
6690
+ description="A transcript is the wrong thing to hand a fresh session even "
6691
+ "where the machinery allows it: a large one does not fit in a "
6692
+ "context window, and replaying it costs more than the work it "
6693
+ "describes. This prints what actually carries — what the human "
6694
+ "asked for, which files were touched, how the session worked — "
6695
+ "read off the record rather than retold by a model. Use it to "
6696
+ "start a session somewhere a capsule cannot reach, a cloud "
6697
+ "session in a browser being the case that prompted it.",
6698
+ )
6699
+ s.add_argument("session_id", metavar="session-id")
6700
+ s.add_argument("--limit", type=int, default=0,
6701
+ help="keep at most this many instructions: the first, then the "
6702
+ "most recent (default: all)")
6703
+ s.add_argument("--cwd", help="which project key, when the id is under more than one")
6704
+ s.add_argument("--config-dir", help=argparse.SUPPRESS)
6705
+ s.set_defaults(func=cmd_session, session_action="brief")
6706
+
6627
6707
  s = ssub.add_parser(
6628
6708
  "register",
6629
6709
  help="give the desktop app a row for a session installed here",