# Sessions

> Understand the Synapse Claude Code plugin hooks, session lifecycle, and multi-agent parallel execution. Covers what each hook does, where plugin state lives, and how to run multiple experiments in parallel via Task sub-agents.

- Skill: `vincentwei1021/sessions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vincentwei1021/sessions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vincentwei1021/sessions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: AGPL-3.0
- Author: Vincentwei1021 (https://skillmd.com/u/vincentwei1021)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vincentwei1021/sessions

---


# Sessions Skill

Use this skill when the task is about how the Synapse plugin itself behaves inside Claude Code: which hooks fire, what they inject into context, how sub-agent sessions are managed, and how to dispatch multiple experiments in parallel to Task sub-agents.

## Prompt Boundary

Stay inside this skill when the work is about:
- understanding why Synapse context appeared in the session (which hook injected it)
- debugging a sub-agent that is not behaving as expected (session, heartbeat, or teardown issue)
- deciding how a main agent should dispatch parallel experiment work across sub-agents
- deciding when to call `synapse_create_session` / `synapse_close_session` directly instead of relying on hooks

Hand off to:
- **[experiments](../experiments/SKILL.md)** for the actual work inside one experiment
- **[autonomy](../autonomy/SKILL.md)** for the CC-client autonomous loop

## Core Rule

For sub-agents spawned through the Task tool, sessions are fully automatic. Do not manually call `synapse_create_session` or `synapse_close_session` for a sub-agent unless you are debugging the lifecycle itself. The `SubagentStart` and `SubagentStop` hooks already do it.

## Plugin Hooks At A Glance

The plugin ships ten hooks wired up in `hooks/hooks.json`. Each reads/writes local state under `.synapse/` in the project and, when needed, calls Synapse MCP tools via the bundled `synapse-api.sh` wrapper.

| Hook script | Claude Code event | What it does | State touched | MCP calls |
|---|---|---|---|---|
| `on-session-start.sh` | `SessionStart` (`startup` \| `resume` \| `compact`) | Calls `synapse_checkin`, caches owner / roles / project UUID into `state.json`, scans for pre-assigned sub-agent session files, and builds the rich `additionalContext` block that orients the agent on assignments, projects, and workflow. | `state.json`, reads `sessions/*.json` | `synapse_checkin`, optional `synapse_session_heartbeat` |
| `on-user-prompt.sh` | `UserPromptSubmit` | Fast local-only check on every user turn. Scans `.synapse/sessions/` and injects a brief reminder that sub-agent sessions are auto-managed. No network calls (stays under 100 ms). | reads `sessions/` | none |
| `on-pre-enter-plan.sh` | `PreToolUse` (`EnterPlanMode`) | Injects planning-mode guidance: prefer the current Experiment pipeline (`draft → pending_review → pending_start → in_progress → completed`), plan one independent run per experiment card, do not plan to create sessions manually. | none | none |
| `on-pre-exit-plan.sh` | `PreToolUse` (`ExitPlanMode`) | Reminds the agent to verify that the plan is expressed as Experiment records before executing. | none | none |
| `on-pre-spawn-agent.sh` | `PreToolUse` (`Task`) | Before a sub-agent is spawned, atomically writes `.synapse/pending/{name}` with the agent name and type so the `SubagentStart` hook can claim it. Skips read-only sub-agent types. | writes `pending/{name}` | none |
| `on-subagent-start.sh` | `SubagentStart` | Atomically claims the pending file via `mv`, then either reuses an active session, reopens a closed one, or creates a new one via `synapse_list_sessions` / `synapse_reopen_session` / `synapse_create_session`. Writes `sessions/{name}.json`, stores the mapping in `state.json`, and injects the session UUID plus the execution workflow directly into the sub-agent's context. | `state.json`, `sessions/{name}.json`, `claimed/{agent_id}` | `synapse_list_sessions`, `synapse_create_session`, `synapse_reopen_session`, `synapse_session_heartbeat` |
| `on-teammate-idle.sh` | `TeammateIdle` | When a teammate sub-agent idles between turns, sends a heartbeat so its Synapse session does not auto-time-out after 1 hour. Output suppressed — this fires too often to notify the user. | reads `state.json` | `synapse_session_heartbeat` |
| `on-subagent-stop.sh` | `SubagentStop` | Looks up the session UUID, calls `synapse_close_session`, and cleans up `state.json`, `sessions/{name}.json`, and `claimed/{agent_id}`. | deletes state entries, `sessions/{name}.json`, `claimed/{agent_id}` | `synapse_close_session` |
| `on-task-completed.sh` | `TaskCompleted` | Scans the completed task description/subject for a `synapse:experiment:<uuid>` marker. If found, injects a reminder to finalize the experiment with `synapse_submit_experiment_results` (or report progress if still running). | reads task metadata only | none |
| `on-session-end.sh` | `SessionEnd` | Safety-checked cleanup. Removes `.synapse/` only when all sub-agent sessions are closed and `state.json` has no meaningful content left. | deletes `.synapse/` if safe | none |

## Local State Layout

The plugin keeps per-project state under `.synapse/` in the project working directory:

- `state.json` — flock-guarded key/value store. Holds owner info cached from `synapse_checkin`, agent roles, the primary project UUID, and the `session_{…}` / `agent_for_session_{…}` / `name_for_agent_{…}` mappings used by the start/stop hooks.
- `pending/{name}` — written by `on-pre-spawn-agent.sh`, atomically claimed by `on-subagent-start.sh` via `mv`.
- `claimed/{agent_id}` — marker file showing which pending entry was claimed.
- `sessions/{name}.json` — per-sub-agent session metadata: `sessionUuid`, `agentId`, `agentName`, `agentType`, `sessionAction`, `createdAt`. Other hooks (idle, stop, user-prompt) read from here to locate the session.

`on-session-end.sh` removes `.synapse/` only when it is safe; otherwise state is preserved across sessions so resumed work reconnects to the same Synapse sessions.

## MCP Connection Session vs Synapse Agent Session

These are two different things — do not confuse them.

- **MCP connection session** — the HTTP-streamable session on `/api/mcp`. Identified by the `mcp-session-id` header, auto-renewed on every request, expires after 30 minutes of inactivity. The plugin handles this transparently.
- **Synapse agent session** — a durable record in Synapse of which agent is working on what (the green / yellow / grey indicators on the Settings page). Created/closed by the plugin hooks above, or explicitly by `synapse_create_session` / `synapse_close_session`.

## Session Status Lifecycle

```
active ——(no heartbeat 1h)——> inactive ——(heartbeat)——> active
  \                                 \
   \—— close ——> closed ——(reopen)——> active
```

| Status | Meaning |
|---|---|
| `active` | Agent is working. Green indicator. |
| `inactive` | No heartbeat in over an hour. Yellow indicator. |
| `closed` | Session ended. Grey indicator. Can be reopened. |

## Session Tools

| Tool | Purpose |
|---|---|
| `synapse_list_sessions` | List sessions for the current agent. |
| `synapse_get_session` | Read one session's details. |
| `synapse_create_session` | Create a named session — usually only needed for direct (non-sub-agent) work. |
| `synapse_close_session` | Close a session. |
| `synapse_reopen_session` | Reopen a closed session instead of creating a duplicate with the same name. |
| `synapse_session_heartbeat` | Keep a session active. Hooks already send heartbeats automatically. |
| `synapse_session_checkin_experiment` | Bind a session to a current Experiment. Usually handled by `synapse_start_experiment({ sessionUuid })`. |
| `synapse_session_checkout_experiment` | Unbind a session from a current Experiment. Usually handled by `synapse_submit_experiment_results({ sessionUuid })`. |

## Running Multiple Experiments In Parallel

When the main agent needs to execute several `pending_start` experiments concurrently, it should **orchestrate, not execute**. The plugin does the heavy lifting for session bookkeeping; the main agent only needs to spawn sub-agents and monitor.

### Main agent: dispatch

```text
# 1. Refresh and list what needs to run
synapse_checkin()
synapse_get_assigned_experiments({ researchProjectUuid, statuses: ["pending_start"] })

# 2. Inspect each candidate
synapse_get_experiment({ experimentUuid })

# 3. For each experiment, spawn a Task sub-agent with the experiment UUID in the prompt.
#    The SubagentStart hook auto-creates/reuses a Synapse session and injects the
#    execution workflow. The main agent does not need to call synapse_create_session.
Task({
  subagent_type: "general-purpose",
  name: "training-worker-<short>",
  prompt: "Your Synapse experiment UUID: <experiment-uuid>. Run the experiment end to end."
})
```

### Sub-agent: execute

Each sub-agent follows the full execution checklist in **[experiments](../experiments/SKILL.md)**: `synapse_start_experiment({ sessionUuid })` → `synapse_list_compute_nodes` / `synapse_reserve_gpus` → `synapse_get_node_access_bundle` → run remotely (tmux + unbuffered) → `synapse_report_experiment_progress` at milestones → `synapse_submit_experiment_results({ sessionUuid })` → optional `synapse_save_experiment_report`.

### Main agent: monitor

The main agent does not block on any one sub-agent. After dispatching all sub-agents, it schedules a single Claude Code `CronCreate` heartbeat that polls Synapse on a cadence — see "Monitoring Long Runs With CronCreate" in the [experiments](../experiments/SKILL.md) skill for the full pattern. The heartbeat prompt should look like:

```text
CronCreate({
  cron: "*/10 * * * *",
  prompt: "Check Synapse for project <projectUuid>: synapse_get_assigned_experiments({ researchProjectUuid: '<projectUuid>', statuses: ['in_progress', 'completed'] }). For each still-in-progress experiment, synapse_get_experiment + synapse_report_experiment_progress with the latest tmux/log line. Once all have completed, CronDelete this job.",
  recurring: true,
  durable: true,
})
```

Within each fire of the heartbeat, the iteration looks like:

```text
synapse_get_assigned_experiments({
  researchProjectUuid,
  statuses: ["in_progress", "completed"]
})

# For any experiment still in_progress, read its latest state
synapse_get_experiment({ experimentUuid })

# Once all have completed, synthesize. If this is the assigned autonomous-loop
# agent, propose follow-ups; otherwise use synapse_create_experiment for
# user-directed terminal work.
synapse_propose_experiment({ researchProjectUuid, title, description })
```

`durable: true` keeps the heartbeat alive across CC restarts so cards do not go stale just because the user closed their laptop. Always `CronDelete` once every experiment is `completed` and synthesis has been written.

### Sequential multi-experiment sub-agent

A single sub-agent can also handle multiple experiments in order when dependencies matter:

```text
Task({
  subagent_type: "general-purpose",
  name: "sequential-worker",
  prompt: """
    Synapse experiments, in order (each depends on the previous):
      1. <experiment-uuid-1> — baseline evaluation
      2. <experiment-uuid-2> — ablation built on #1's results

    For each: start_experiment → run → report_progress → submit_results.
  """
})
```

## Project-Level MCP For Sub-Agents

Sub-agents only inherit Synapse MCP access if the MCP server is configured at the project level. Put the config in `.mcp.json` at the project root (the plugin bundle ships the template at `public/synapse-plugin/.mcp.json`). User-level-only MCP configs will not reach sub-agents.

## Troubleshooting

| Symptom | Likely cause / fix |
|---|---|
| Sub-agent cannot see Synapse tools | MCP is user-level only. Move `.mcp.json` to the project root. |
| Sub-agent session shown as `inactive` | Heartbeat has not fired for 1 h. Usually the sub-agent crashed; respawn with the same name — `SubagentStart` will reopen the existing session instead of making a duplicate. |
| Duplicate sessions appear with similar names | A previous sub-agent stopped without the `SubagentStop` hook firing (hard crash). Close stale sessions with `synapse_close_session`, then respawn. |
| Main agent did not receive checkin context | `SessionStart` hook failed (check `SYNAPSE_URL`, `SYNAPSE_API_KEY`). Call `synapse_checkin()` manually to recover. |
| Experiment stuck in `in_progress` | The sub-agent died before `synapse_submit_experiment_results`. Either resume by respawning the sub-agent with the same experiment UUID, or the main agent calls `synapse_report_experiment_progress` and then `synapse_submit_experiment_results({ outcome: "failure", experimentResults: { error: "..." } })` to close it out. |

## Reference

- **[Session and sub-agent reference](../synapse/references/05-session-sub-agent.md)**
- **[Experiments skill](../experiments/SKILL.md)**
- **[Autonomy skill](../autonomy/SKILL.md)**
- **[Common tools](../synapse/references/00-common-tools.md)**

