agent-switchboard 2.2.0__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.0 → agent_switchboard-2.2.2}/ARCS.md +2 -1
  2. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/PKG-INFO +1 -1
  3. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/adaptive-timing.md +1 -1
  4. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/claude-code.md +97 -0
  5. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/concepts.md +13 -10
  6. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/quickstart.md +3 -3
  7. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/upgrading.md +56 -0
  8. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/viewer.md +10 -0
  9. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/pyproject.toml +1 -1
  10. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/index.html +7 -0
  11. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/render.js +50 -0
  12. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/pyproject.toml +1 -1
  13. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/ROADMAP.md +99 -0
  14. agent_switchboard-2.2.2/roadmap/items/automatic-session-checkpoints.yaml +92 -0
  15. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/__init__.py +1 -1
  16. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/claude_session.py +127 -1
  17. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/cli.py +320 -4
  18. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/client.py +45 -25
  19. agent_switchboard-2.2.2/src/switchboard/desktop.py +292 -0
  20. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/handoff.py +7 -2
  21. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/mcp_server.py +8 -1
  22. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/conftest.py +6 -0
  23. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_claude_session.py +130 -0
  24. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_config.py +59 -18
  25. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_dash_ids.py +77 -13
  26. agent_switchboard-2.2.2/tests/test_desktop.py +211 -0
  27. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_handoff.py +18 -0
  28. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_web_page.py +49 -0
  29. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.dockerignore +0 -0
  30. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/ci.yml +0 -0
  31. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/deploy.yml +0 -0
  32. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/pages.yml +0 -0
  33. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/publish-viewer.yml +0 -0
  34. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/publish.yml +0 -0
  35. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/roadmap.yml +0 -0
  36. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.gitignore +0 -0
  37. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/AGENTS.md +0 -0
  38. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/CONTRIBUTING.md +0 -0
  39. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/Dockerfile +0 -0
  40. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/LICENSE +0 -0
  41. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/README.md +0 -0
  42. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/demo/README.md +0 -0
  43. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/demo/chess.sh +0 -0
  44. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/demo/run.sh +0 -0
  45. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docker-compose.yml +0 -0
  46. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/api.md +0 -0
  47. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/codex-cli.md +0 -0
  48. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/deployment.md +0 -0
  49. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/drills.md +0 -0
  50. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/encryption.md +0 -0
  51. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/environments.md +0 -0
  52. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/images/viewer.png +0 -0
  53. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/layers.md +0 -0
  54. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/managed-hub.md +0 -0
  55. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/model.md +0 -0
  56. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/seam.md +0 -0
  57. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/testing.md +0 -0
  58. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/why-this-exists.md +0 -0
  59. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/examples/coordinated_worker.py +0 -0
  60. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/session-capsule/README.md +0 -0
  61. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/session-capsule/cc.sh +0 -0
  62. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/session-capsule/claude_session_capsule.py +0 -0
  63. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/README.md +0 -0
  64. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/__init__.py +0 -0
  65. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/__main__.py +0 -0
  66. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/viewer.py +0 -0
  67. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/switchboard-open.js +0 -0
  68. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/switchboard-room.js +0 -0
  69. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/README.md +0 -0
  70. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/arcs/distribution.yaml +0 -0
  71. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/arcs/hub-boundary.yaml +0 -0
  72. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/arcs/setup-and-first-run.yaml +0 -0
  73. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/arcs/ttl-ceilings.yaml +0 -0
  74. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/a-lobby-derived-from-the-key.yaml +0 -0
  75. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/abuse-control-after-authorization.yaml +0 -0
  76. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/board-ttl-ceiling.yaml +0 -0
  77. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/ci-workspace-is-public.yaml +0 -0
  78. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/clients-that-cannot-post.yaml +0 -0
  79. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/connect-failure-message.yaml +0 -0
  80. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/cross-key-rendezvous.yaml +0 -0
  81. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/every-silent-failure-looks-like-a-quiet-room.yaml +0 -0
  82. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/hooks-warning-false-positive.yaml +0 -0
  83. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/hub-origin-reachable-bypassing-the-edge.yaml +0 -0
  84. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/identity-rebinds-on-branch-change.yaml +0 -0
  85. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/init-writes-rooms-file.yaml +0 -0
  86. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/intermittent-suite-failure.yaml +0 -0
  87. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/joining-agent-sees-empty-inbox.yaml +0 -0
  88. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/known-rooms-address-book.yaml +0 -0
  89. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/one-resolved-context-across-surfaces.yaml +0 -0
  90. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/presence-ttl-is-not-one-size.yaml +0 -0
  91. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/provisioned-token-is-stale-and-nothing-says-so.yaml +0 -0
  92. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/publish-hub-container-image.yaml +0 -0
  93. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/read-only-rooms.yaml +0 -0
  94. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/robots-policy-for-public-hosts.yaml +0 -0
  95. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/roles-and-authority-between-agents.yaml +0 -0
  96. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/seal-agent-meta.yaml +0 -0
  97. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/selective-wake-for-the-listener.yaml +0 -0
  98. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/stale-resolver-references.yaml +0 -0
  99. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/stale-token-in-session-env.yaml +0 -0
  100. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/standing-checks-that-nothing-runs.yaml +0 -0
  101. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/timing-cold-start-in-ephemeral-environments.yaml +0 -0
  102. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/ttl-clamped-silently.yaml +0 -0
  103. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/unread-dms-not-shown-outside-mcp.yaml +0 -0
  104. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/write-parity-across-surfaces.yaml +0 -0
  105. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/scripts/deploy.sh +0 -0
  106. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/auth.py +0 -0
  107. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/config.py +0 -0
  108. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/crypto.py +0 -0
  109. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/drill.py +0 -0
  110. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/guidance.py +0 -0
  111. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/holds.py +0 -0
  112. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/invite.py +0 -0
  113. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/knownrooms.py +0 -0
  114. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/load.py +0 -0
  115. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/notify.py +0 -0
  116. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/peers.py +0 -0
  117. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/rendezvous.py +0 -0
  118. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/rooms.py +0 -0
  119. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/scripts/wake-on-message.sh +0 -0
  120. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/server.py +0 -0
  121. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/signing.py +0 -0
  122. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/001-SKILL.md +0 -0
  123. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/002-SKILL.md +0 -0
  124. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/003-SKILL.md +0 -0
  125. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/004-SKILL.md +0 -0
  126. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/005-SKILL.md +0 -0
  127. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/006-SKILL.md +0 -0
  128. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/007-SKILL.md +0 -0
  129. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/008-SKILL.md +0 -0
  130. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/009-SKILL.md +0 -0
  131. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/010-SKILL.md +0 -0
  132. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/011-SKILL.md +0 -0
  133. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/012-SKILL.md +0 -0
  134. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/013-SKILL.md +0 -0
  135. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/014-SKILL.md +0 -0
  136. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/015-SKILL.md +0 -0
  137. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/016-SKILL.md +0 -0
  138. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/017-SKILL.md +0 -0
  139. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/018-SKILL.md +0 -0
  140. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/019-SKILL.md +0 -0
  141. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/020-SKILL.md +0 -0
  142. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/021-SKILL.md +0 -0
  143. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/022-SKILL.md +0 -0
  144. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/023-SKILL.md +0 -0
  145. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/024-SKILL.md +0 -0
  146. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/025-SKILL.md +0 -0
  147. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/switchboard-coordinate/SKILL.md +0 -0
  148. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/spec.py +0 -0
  149. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/store.py +0 -0
  150. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/testing.py +0 -0
  151. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/timing.py +0 -0
  152. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/writekey.py +0 -0
  153. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/README.md +0 -0
  154. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/__init__.py +0 -0
  155. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/analysis.py +0 -0
  156. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/economy.py +0 -0
  157. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/flow.py +0 -0
  158. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/llm.py +0 -0
  159. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/manager.py +0 -0
  160. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/report.py +0 -0
  161. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/run.py +0 -0
  162. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier1.json +0 -0
  163. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier1_labour.json +0 -0
  164. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier1_rounds.json +0 -0
  165. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_bound.json +0 -0
  166. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_built.json +0 -0
  167. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_free.json +0 -0
  168. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_paid.json +0 -0
  169. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_spend.json +0 -0
  170. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_told.json +0 -0
  171. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed41_bound.json +0 -0
  172. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/traders.py +0 -0
  173. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter_experiment.py +0 -0
  174. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter_llm_experiment.py +0 -0
  175. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_harness.py +0 -0
  176. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/RESULTS.md +0 -0
  177. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_A_s31337.txt +0 -0
  178. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_A_s4242.txt +0 -0
  179. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_A_s7777.txt +0 -0
  180. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_B_s31337.txt +0 -0
  181. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_B_s4242.txt +0 -0
  182. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_B_s7777.txt +0 -0
  183. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_C_s31337.txt +0 -0
  184. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_C_s4242.txt +0 -0
  185. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_C_s7777.txt +0 -0
  186. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_A_s31337.json +0 -0
  187. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_A_s4242.json +0 -0
  188. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_A_s7777.json +0 -0
  189. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_B_s31337.json +0 -0
  190. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_B_s4242.json +0 -0
  191. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_B_s7777.json +0 -0
  192. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_C_s31337.json +0 -0
  193. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_C_s4242.json +0 -0
  194. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_C_s7777.json +0 -0
  195. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_A_s31337.json +0 -0
  196. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_A_s4242.json +0 -0
  197. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_A_s7777.json +0 -0
  198. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_B_s31337.json +0 -0
  199. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_B_s4242.json +0 -0
  200. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_B_s7777.json +0 -0
  201. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_C_s31337.json +0 -0
  202. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_C_s4242.json +0 -0
  203. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_C_s7777.json +0 -0
  204. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_A_s31337.json +0 -0
  205. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_A_s4242.json +0 -0
  206. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_A_s7777.json +0 -0
  207. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_B_s31337.json +0 -0
  208. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_B_s4242.json +0 -0
  209. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_B_s7777.json +0 -0
  210. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_C_s31337.json +0 -0
  211. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_C_s4242.json +0 -0
  212. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_C_s7777.json +0 -0
  213. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_run.py +0 -0
  214. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/timing_experiment.py +0 -0
  215. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_api.py +0 -0
  216. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_arrive.py +0 -0
  217. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_auth.py +0 -0
  218. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_barter.py +0 -0
  219. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_barter_llm.py +0 -0
  220. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_board.py +0 -0
  221. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_checkin.py +0 -0
  222. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_config.py +0 -0
  223. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_global_flags.py +0 -0
  224. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_init.py +0 -0
  225. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_serve.py +0 -0
  226. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_session.py +0 -0
  227. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_timing.py +0 -0
  228. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_client_invite.py +0 -0
  229. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cors_write_headers.py +0 -0
  230. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_crypto.py +0 -0
  231. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_declare.py +0 -0
  232. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_deployment.py +0 -0
  233. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_drill.py +0 -0
  234. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_evening_replayed.py +0 -0
  235. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_example_viewer.py +0 -0
  236. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_failure_messages.py +0 -0
  237. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_help.py +0 -0
  238. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_invite.py +0 -0
  239. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_isolated_hub.py +0 -0
  240. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_known_rooms.py +0 -0
  241. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_leave_a_ghost.py +0 -0
  242. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_load.py +0 -0
  243. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_lobby.py +0 -0
  244. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_mcp.py +0 -0
  245. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_mcp_session.py +0 -0
  246. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_notify.py +0 -0
  247. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_rendezvous.py +0 -0
  248. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_rooms.py +0 -0
  249. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_spec.py +0 -0
  250. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_store.py +0 -0
  251. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_surface_parity.py +0 -0
  252. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_testing.py +0 -0
  253. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_tier2_harness.py +0 -0
  254. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_timing.py +0 -0
  255. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_timing_experiment.py +0 -0
  256. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_transient_failures.py +0 -0
  257. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_version.py +0 -0
  258. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_wake_listener.py +0 -0
  259. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_web_invite.py +0 -0
  260. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_web_reader.py +0 -0
  261. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_web_snapshot.py +0 -0
  262. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_whisper.py +0 -0
  263. {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_workflow_token.py +0 -0
  264. {agent_switchboard-2.2.0 → 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.0
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
@@ -48,7 +48,7 @@ is never a commitment, and no agent is required to obey another's forecast.
48
48
  machines. It is shared by every repo on that machine, but every read and
49
49
  write is keyed by `(agent_id, workspace)` — and `workspace` defaults to
50
50
  the git remote's org/repo — so history never bleeds from one repo into
51
- another's forecasts. `agent_id` derives from kind/branch/hostname, so the
51
+ another's forecasts. `agent_id` derives from kind/hostname/session, so the
52
52
  same person on two laptops builds two independent histories that converge
53
53
  separately. All of that is deliberate: the premise is that timing is
54
54
  local and personal, and a shared or committed history would be neither.
@@ -466,6 +466,103 @@ 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
+
507
+ ### If you work in the desktop app
508
+
509
+ Installing the transcript is the whole job for a CLI user and half of one for
510
+ everybody else. The desktop app keeps its own list of sessions beside the
511
+ transcripts, so a conversation whose transcript is installed but whose record
512
+ is not exists on disk and is invisible in the app — which is exactly what
513
+ happened the first time a session was handed from a cloud agent to a laptop:
514
+ it resumed perfectly on the command line and could not be found in the app the
515
+ recipient actually used.
516
+
517
+ So `switchboard init` asks, once, on a machine that runs the app: should a
518
+ session handed to you also appear in its sidebar? Say yes and `receive` writes
519
+ the record too. The question is asked at setup rather than per handoff for two
520
+ reasons — it writes into another program's store, which is a thing to agree to
521
+ deliberately, and the account and project the row is filed under are ids that
522
+ app assigns, discoverable only where it runs. A cloud runner has neither, so
523
+ the step is silently skipped there.
524
+
525
+ `switchboard session register <id>` does it for a session already installed,
526
+ and `session unregister <id>` is the undo. Prefer that over deleting the row
527
+ inside the app: in-app delete and archive both write a release marker next to
528
+ the transcript which the CLI acts on, while unregister removes only the record.
529
+
530
+ Two things worth knowing. The app reads its store at launch, so a registered
531
+ session appears after the next restart rather than immediately. And this is an
532
+ undocumented format belonging to another program: `switchboard.desktop` says
533
+ which app version it was verified against, keeps every detail in that one
534
+ module, and answers "not here" rather than guessing when the shape is not what
535
+ it expects.
536
+
537
+ ### What travels, and what a reader can take out
538
+
539
+ The subagent transcripts in the `<id>/` sidecar travel by default, and that
540
+ is the trade worth naming because it is not the obvious one. They are most
541
+ of a capsule's bytes — three quarters of it is normal — and none of its
542
+ resume: `claude --resume` reads the main transcript alone, so the sidecar
543
+ costs the receiver no context at cold start. What it buys is retrievability.
544
+ A subagent's transcript is finished work, often millions of tokens of it,
545
+ that the receiving session can go and read *if it needs to* rather than
546
+ re-derive at model prices. Bytes on a board with a ten-minute TTL are the
547
+ cheap side of that trade, so the default is on.
548
+
549
+ `--no-subagents` (`no_subagents` on `session_handoff`) drops them, for a slow
550
+ link or a receiver that only needs the conversation continued. The capsule
551
+ records that it did, as `omitted_subagent_files`, because a session that
552
+ never spawned a subagent and one whose subagents were left behind both
553
+ arrive as a single file, and a receiver deciding whether to ask again needs
554
+ to tell those apart.
555
+
556
+ A capsule can also be collected with no Switchboard on the machine at all.
557
+ The [viewer](../extras/viewer/) already holds the plaintext — it fetched the
558
+ board entry and opened it with the room key, which is why a sealed value is
559
+ legible there — so a board entry it could open offers a **save** button that
560
+ writes the value out as a file. Feed that file to `switchboard session
561
+ import FILE` wherever the tooling *is* installed. It asks the hub for
562
+ nothing extra, and it is the one path to a handoff for a human with a
563
+ browser and no checkout. Where the page could not open a value it offers
564
+ nothing: a file of envelope bytes would look like the value and is not.
565
+
469
566
  ### What is trusted, and what is not
470
567
 
471
568
  A transcript is instructions. Whoever resumes it runs a conversation
@@ -43,20 +43,23 @@ An agent registers itself (id, name, kind, branch, current task) and then
43
43
  heartbeats. If it stops heartbeating, it disappears from the roster within its
44
44
  TTL — two minutes by default.
45
45
 
46
- The agent id is inferred from `kind`, git branch and hostname, so a session
47
- that restarts on the same branch reclaims its own identity rather than
48
- appearing as a stranger. Override it with `SWITCHBOARD_AGENT_ID` when you want
49
- something specific.
46
+ The agent id is inferred from `kind`, hostname and a hash of the session id,
47
+ so a session that resumes reclaims its own identity its leases, its read
48
+ cursor — rather than appearing as a stranger. Two sessions on one machine get
49
+ different ids, because the session is part of the derivation. Override it with
50
+ `SWITCHBOARD_AGENT_ID` when you want something specific.
50
51
 
51
52
  `kind` is auto-detected as `ci` (in GitHub Actions), `cloud` (in a remote
52
53
  Claude Code session or a Codespace) or `local`.
53
54
 
54
- Because the inferred id has no process or session component, two sessions
55
- that share `kind`, branch and hostname two terminals on the same checkout,
56
- say collide on the same agent id. They are indistinguishable to the hub:
57
- same presence row, same lease ownership, same message cursor. If you run
58
- more than one session against the same branch and host, set
59
- `SWITCHBOARD_AGENT_ID` explicitly for at least one of them.
55
+ **The branch is deliberately not part of the id.** It is registered as its own
56
+ field, which is what `dm` resolves a typed branch name against so it says
57
+ where an agent is *now* and follows it across a checkout, while the id stays
58
+ put. An id has to outlive the session's whole life: it is what the signer
59
+ socket is keyed on, what the roster stores a public key against, what holds a
60
+ lease and what owns a read cursor. Folding the branch in meant that checking
61
+ one out — the most ordinary thing an agent does — minted a second identity and
62
+ orphaned all four.
60
63
 
61
64
  ## Leases
62
65
 
@@ -19,7 +19,7 @@ switchboard serve --port 8787
19
19
  You should see:
20
20
 
21
21
  ```
22
- switchboard 2.2.0 → 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.
@@ -35,7 +35,7 @@ export SWITCHBOARD_WORKSPACE=demo
35
35
  ```
36
36
 
37
37
  In the second terminal, give the agent a distinct identity so you can tell them
38
- apart (normally this is inferred from branch and hostname):
38
+ apart (normally this is inferred from your host and session):
39
39
 
40
40
  ```bash
41
41
  export SWITCHBOARD_AGENT_ID=beta
@@ -47,7 +47,7 @@ export SWITCHBOARD_AGENT_ID=beta
47
47
 
48
48
  ```bash
49
49
  $ switchboard health
50
- { "ok": true, "version": "2.2.0", "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
@@ -3,6 +3,62 @@
3
3
  What changes for you between releases that are not backwards compatible, in
4
4
  the order you will hit them. Everything not listed here kept its behaviour.
5
5
 
6
+ ## 2.2.0 → 2.2.1
7
+
8
+ **Your agent ids change once.** An id was `kind-branch-host-session`; the
9
+ branch is gone from it, so it is now `kind-host-session`. The branch was in
10
+ there from when it was the only thing telling two sessions apart — the
11
+ session suffix took that job and left the branch behind as decoration, and
12
+ decoration with teeth: an id is what the signer socket is keyed on, what the
13
+ roster stores a public key against, what holds a lease and what owns a read
14
+ cursor. Checking out a branch, the most ordinary thing an agent does, minted
15
+ a second identity and orphaned all four.
16
+
17
+ Nothing to do about it. On first announce after upgrading, an agent appears
18
+ under its new id; the row under the old one stops being refreshed and lapses
19
+ on the presence TTL like any other. **Leases and read cursors held by the old
20
+ id are not transferred** — a lease expires on its own TTL (15 minutes by
21
+ default) and a cursor's cost is one repeated read. If you cannot wait for a
22
+ lease to lapse, release it before upgrading, or pin `SWITCHBOARD_AGENT_ID`
23
+ across the upgrade to keep the id you had.
24
+
25
+ Branch-based lookup is unaffected: `dm` and the other recipient resolvers
26
+ match the roster's own `branch` field, registered on every announce, which
27
+ follows an agent across a checkout in a way an id baked at startup never
28
+ did. A name still says the branch if you registered it that way.
29
+
30
+ `client.identity_drift_warning` is now `client.rootless_warning`, and warns
31
+ about a different thing. It existed for this bug's directory half — outside a
32
+ checkout the branch read as `detached` and the id moved — which no longer
33
+ happens. What still drifts outside a checkout is the *workspace*, derived
34
+ from the directory when there is no remote to read, which puts the agent in a
35
+ different room entirely; that is what the renamed function now says.
36
+
37
+ Also fixed: an option value beginning with `-` no longer has to be escaped
38
+ by hand. `switchboard session receive --from <agent-id>` and `board list
39
+ --prefix <key>` died on "expected one argument" for about one id in
40
+ sixty-six, because ids and board keys are base64url and argparse will not
41
+ read a dash-leading token as an option's value. Both now work as typed —
42
+ the same repair `dm -yLAoQ63…` already had for positionals, extended to the
43
+ other half of the problem.
44
+
45
+ Additive in the same release: `--no-subagents` on `session export`,
46
+ `handoff` and `publish` (`no_subagents` on `session_handoff`) leaves the
47
+ subagent transcripts out of a capsule, and the viewer can save an opened
48
+ board value to a file. Neither changes an existing default.
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
+
6
62
  ## 2.1.0 → 2.2.0
7
63
 
8
64
  Additive. A whole Claude Code session can be moved between environments
@@ -104,6 +104,16 @@ the rest: the whole key, who wrote it and when, and a value that keeps its own
104
104
  line breaks instead of being folded to fit. The sidebar itself now widens with
105
105
  the screen rather than staying 340px on every one.
106
106
 
107
+ An opened entry also offers **save**, which writes the value out as a file
108
+ named after its key. The page is already holding the plaintext — it fetched
109
+ the entry and opened it with the room key, which is the whole reason a sealed
110
+ value is legible here — so this asks the hub for nothing and needs no CLI. It
111
+ is how a person with a browser and no checkout collects a
112
+ [session capsule](claude-code.md#7-hand-a-whole-session-to-another-environment)
113
+ and feeds it to `switchboard session import` somewhere else. A value this page
114
+ could not open offers no button: a file of envelope bytes would look like the
115
+ value and is not.
116
+
107
117
  Each of the three side panels says how much is inside it and folds away, and
108
118
  what you fold stays folded. On a window too narrow for two columns they stop
109
119
  being a stack under the conversation — where, the talking being long and never
@@ -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"
@@ -139,6 +139,13 @@
139
139
  .detail .meta { color: var(--dim); font-size: 12px; margin: 4px 0 14px;
140
140
  display: flex; flex-wrap: wrap; gap: 4px 12px; }
141
141
  .detail .body, .detail pre { font-size: 13px; }
142
+ /* Quiet until wanted: a board value is usually read, not taken, and a
143
+ download button competing with the value would say otherwise. */
144
+ .detail .meta .save { border: 1px solid var(--line); border-radius: 6px;
145
+ background: transparent; color: var(--dim); font: inherit;
146
+ font-size: 12px; padding: 0 8px; cursor: pointer; }
147
+ .detail .meta .save:hover, .detail .meta .save:focus-visible {
148
+ color: var(--accent); border-color: var(--accent); }
142
149
  .detail .nothing { color: var(--dim); font-size: 13px; }
143
150
  /* A key is the row's own text, so it reads as text — and as something that
144
151
  does something only where it does something. */
@@ -513,6 +513,8 @@ function drawDetail(s) {
513
513
  <span>${who(entry.updated_by)}</span>
514
514
  <span>${stamp(entry.updated_at)}</span>
515
515
  <span>expires in ${countdown(entry.expires_in, s.generated_at)}</span>
516
+ ${readable(entry) ? `<button class="save" type="button"
517
+ data-save="${esc(entry.key)}">save</button>` : ""}
516
518
  </div>
517
519
  ${bodyHtml(entry.value)}`;
518
520
  if (pane.dataset.html !== html) {
@@ -521,6 +523,49 @@ function drawDetail(s) {
521
523
  }
522
524
  }
523
525
 
526
+ /** Whether this page holds the entry's plaintext at all.
527
+ *
528
+ * A value that could not be opened arrives as `null` — the builder drops the
529
+ * envelope rather than let ciphertext render as content — and that is the
530
+ * condition to check, not `sealed`, which only says whether the *key* stayed
531
+ * a blinded token. Without a room key nothing is unblinded, so `sealed` is
532
+ * false for every entry in a room this page cannot read: exactly the rooms
533
+ * where offering to save one would write out a file that is not the value.
534
+ */
535
+ function readable(entry) {
536
+ return Boolean(entry) && entry.value !== null && entry.value !== undefined;
537
+ }
538
+
539
+ /** Hand the reader the value they are already looking at, as a file.
540
+ *
541
+ * The page has the plaintext: it fetched the entry and opened it with the
542
+ * room key, which is the whole reason a sealed value is legible here at all.
543
+ * Writing it out asks the hub for nothing and needs no checkout, no CLI and
544
+ * no agent — which is the point. A session capsule saved from here goes
545
+ * straight into `switchboard session import`, so a human with a browser can
546
+ * collect a handoff that no tooling on their machine could.
547
+ *
548
+ * Named after the key so the file says what it is, and `.json` when the
549
+ * value is structured, because everything worth saving from a board is.
550
+ */
551
+ function saveEntry(key) {
552
+ const entry = (last?.board || []).find((e) => e.key === key);
553
+ if (!readable(entry)) return;
554
+ const structured = typeof entry.value !== "string";
555
+ const text = structured ? JSON.stringify(entry.value, null, 2) : entry.value;
556
+ const name = `${key.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "")
557
+ || "board-entry"}${structured ? ".json" : ".txt"}`;
558
+ const url = URL.createObjectURL(
559
+ new Blob([text], { type: structured ? "application/json" : "text/plain" }));
560
+ const a = document.createElement("a");
561
+ a.href = url;
562
+ a.download = name;
563
+ a.click();
564
+ // Same turn would race the click on some browsers; the next one is safe and
565
+ // still prompt enough that nothing accumulates.
566
+ setTimeout(() => URL.revokeObjectURL(url), 0);
567
+ }
568
+
524
569
  export function render(s, { onRoom, onClose } = {}) {
525
570
  last = s;
526
571
  if (onRoom || onClose) handlers = { onRoom, onClose };
@@ -784,6 +829,11 @@ export function render(s, { onRoom, onClose } = {}) {
784
829
  scopeChanged();
785
830
  return;
786
831
  }
832
+ const save = e.target.closest("[data-save]");
833
+ if (save) {
834
+ saveEntry(save.dataset.save);
835
+ return;
836
+ }
787
837
  const entry = e.target.closest("[data-board]");
788
838
  if (entry) {
789
839
  opened = entry.dataset.board;
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-switchboard"
3
- version = "2.2.0"
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.