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.
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/ARCS.md +2 -1
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/PKG-INFO +1 -1
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/adaptive-timing.md +1 -1
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/claude-code.md +97 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/concepts.md +13 -10
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/quickstart.md +3 -3
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/upgrading.md +56 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/viewer.md +10 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/pyproject.toml +1 -1
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/index.html +7 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/render.js +50 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/pyproject.toml +1 -1
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/ROADMAP.md +99 -0
- agent_switchboard-2.2.2/roadmap/items/automatic-session-checkpoints.yaml +92 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/__init__.py +1 -1
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/claude_session.py +127 -1
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/cli.py +320 -4
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/client.py +45 -25
- agent_switchboard-2.2.2/src/switchboard/desktop.py +292 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/handoff.py +7 -2
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/mcp_server.py +8 -1
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/conftest.py +6 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_claude_session.py +130 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_config.py +59 -18
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_dash_ids.py +77 -13
- agent_switchboard-2.2.2/tests/test_desktop.py +211 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_handoff.py +18 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_web_page.py +49 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.dockerignore +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/ci.yml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/deploy.yml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/pages.yml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/publish-viewer.yml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/publish.yml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.github/workflows/roadmap.yml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/.gitignore +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/AGENTS.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/CONTRIBUTING.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/Dockerfile +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/LICENSE +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/README.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/demo/README.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/demo/chess.sh +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/demo/run.sh +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docker-compose.yml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/api.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/codex-cli.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/deployment.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/drills.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/encryption.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/environments.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/images/viewer.png +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/layers.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/managed-hub.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/model.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/seam.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/testing.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/docs/why-this-exists.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/examples/coordinated_worker.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/session-capsule/README.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/session-capsule/cc.sh +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/session-capsule/claude_session_capsule.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/README.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/__init__.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/__main__.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/viewer.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/switchboard-open.js +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/switchboard-room.js +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/README.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/arcs/distribution.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/arcs/hub-boundary.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/arcs/setup-and-first-run.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/arcs/ttl-ceilings.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/a-lobby-derived-from-the-key.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/abuse-control-after-authorization.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/board-ttl-ceiling.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/ci-workspace-is-public.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/clients-that-cannot-post.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/connect-failure-message.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/cross-key-rendezvous.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/every-silent-failure-looks-like-a-quiet-room.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/hooks-warning-false-positive.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/hub-origin-reachable-bypassing-the-edge.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/identity-rebinds-on-branch-change.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/init-writes-rooms-file.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/intermittent-suite-failure.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/joining-agent-sees-empty-inbox.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/known-rooms-address-book.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/one-resolved-context-across-surfaces.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/presence-ttl-is-not-one-size.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/provisioned-token-is-stale-and-nothing-says-so.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/publish-hub-container-image.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/read-only-rooms.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/robots-policy-for-public-hosts.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/roles-and-authority-between-agents.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/seal-agent-meta.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/selective-wake-for-the-listener.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/stale-resolver-references.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/stale-token-in-session-env.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/standing-checks-that-nothing-runs.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/timing-cold-start-in-ephemeral-environments.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/ttl-clamped-silently.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/unread-dms-not-shown-outside-mcp.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/roadmap/items/write-parity-across-surfaces.yaml +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/scripts/deploy.sh +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/auth.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/config.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/crypto.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/drill.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/guidance.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/holds.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/invite.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/knownrooms.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/load.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/notify.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/peers.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/rendezvous.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/rooms.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/scripts/wake-on-message.sh +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/server.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/signing.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/001-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/002-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/003-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/004-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/005-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/006-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/007-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/008-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/009-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/010-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/011-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/012-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/013-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/014-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/015-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/016-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/017-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/018-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/019-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/020-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/021-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/022-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/023-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/024-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/history/025-SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/skill/switchboard-coordinate/SKILL.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/spec.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/store.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/testing.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/timing.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/src/switchboard/writekey.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/README.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/__init__.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/analysis.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/economy.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/flow.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/llm.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/manager.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/report.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/run.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier1.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier1_labour.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier1_rounds.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_bound.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_built.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_free.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_paid.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_spend.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed1_told.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/tier2_seed41_bound.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter/traders.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter_experiment.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/barter_llm_experiment.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_harness.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/RESULTS.md +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_A_s31337.txt +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_A_s4242.txt +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_A_s7777.txt +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_B_s31337.txt +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_B_s4242.txt +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_B_s7777.txt +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_C_s31337.txt +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_C_s4242.txt +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/prompt_C_s7777.txt +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_A_s31337.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_A_s4242.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_A_s7777.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_B_s31337.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_B_s4242.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_B_s7777.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_C_s31337.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_C_s4242.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_haiku_C_s7777.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_A_s31337.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_A_s4242.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_A_s7777.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_B_s31337.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_B_s4242.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_B_s7777.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_C_s31337.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_C_s4242.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_opus_C_s7777.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_A_s31337.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_A_s4242.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_A_s7777.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_B_s31337.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_B_s4242.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_B_s7777.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_C_s31337.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_C_s4242.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_pilot/response_sonnet_C_s7777.json +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/tier2_run.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/experiments/timing_experiment.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_api.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_arrive.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_auth.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_barter.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_barter_llm.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_board.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_checkin.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_config.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_global_flags.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_init.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_serve.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_session.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cli_timing.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_client_invite.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_cors_write_headers.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_crypto.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_declare.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_deployment.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_drill.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_evening_replayed.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_example_viewer.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_failure_messages.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_help.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_invite.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_isolated_hub.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_known_rooms.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_leave_a_ghost.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_load.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_lobby.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_mcp.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_mcp_session.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_notify.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_rendezvous.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_rooms.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_spec.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_store.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_surface_parity.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_testing.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_tier2_harness.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_timing.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_timing_experiment.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_transient_failures.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_version.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_wake_listener.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_web_invite.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_web_reader.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_web_snapshot.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_whisper.py +0 -0
- {agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/tests/test_workflow_token.py +0 -0
- {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` ·
|
|
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.
|
|
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/
|
|
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`,
|
|
47
|
-
|
|
48
|
-
appearing as a stranger.
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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"
|
{agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/index.html
RENAMED
|
@@ -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. */
|
{agent_switchboard-2.2.0 → agent_switchboard-2.2.2}/extras/viewer/switchboard_viewer/web/render.js
RENAMED
|
@@ -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.
|
|
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.
|