Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Echo-commune — the I/O contract

When a session ends without a graceful signoff, its context would be lost. The echo-commune recovers it: spt-core runs the adapter’s bounded summarizer over the session, captures the brief the summarizer prints, and files it as the session’s context delta — the same delta a hand-written commune would have carried.

This page is the adapter-facing I/O contract for that mechanism: the role you declare, the keys spt-core fills, what it does (and does not) feed the summarizer, how the summarizer locates the harness, and the drop-file protocol spt-core uses to file the result. It is the companion to the [session.echo_commune] role in the manifest reference.

The echo-commune is spt-core’s. The adapter supplies one command template and one watched directory — spt-core owns the spawn, the keys, the file-drop, and the ingest. Everything below is the seam between those halves.

The role

[session.echo_commune] is one outbound role template. Its fields are the standard role shape:

[session.echo_commune]
command = "my-harness run --agent summarize --session {session_id}"
recursion_guard_env = "SPT_ECHO_COMMUNE"     # set on the child so its own hooks bail
env_remove = ["MY_HARNESS_SESSION_ID"]        # stripped from the child's env
keys = ["id", "session_id"]                   # the keys this template expects filled
FieldRequiredMeaning
commandyesOpaque command line with {key} placeholders. Model, tools, flags — all inside the string; spt-core never parses it.
cwdnoWorking directory for the child (substitutable). A role cwd wins over the endpoint default.
recursion_guard_envnoEnv var name set on the summarizer child so its harness hooks bail — no echo-of-an-echo.
detachno (default false)Spawn detached.
env_removenoEnv vars stripped from the child’s inherited environment.
keysnoThe substitution keys spt-core fills for this role (your declared expectation list).

The child runs bounded — a timeout caps it, and a non-zero exit files nothing (the failure is loud, never a half-written delta).

Keys spt-core fills

For an echo-commune spawn spt-core fills its base catalog. Template only the keys you are given; a {placeholder} spt-core does not supply for this role fails the spawn with a one-line error naming the missing key.

Keyspt-core fills it with
{id}The endpoint id being summarized. Always filled.
{session_id}The harness session id — filled when one is known.
{node}This node’s advertised label — filled only when non-empty (a {node} reference with no value fails loud rather than resolving an empty token).
{subnet}The endpoint’s home-subnet label (local when unhomed) — filled only when non-empty.
{adapter_dir}The adapter’s install dir — adapter-static, always available (lets the command point at the adapter’s own packed summarizer binary).
{adapter_name}The adapter’s declared name.
{VAR}Any manifest-declared [env] direction = "read" var captured at bind — see Self-locating the harness.

This is the same base catalog the Psyche and notification roles build on; it deliberately does not include {session_name} (a [session.self] key) or the Psyche-only {psyche_context_file} / {parent_session_id}.

History is not fed on stdin

spt-core has a stdin channel for the summarizer — but you must not depend on it carrying the transcript. The rule, field-proven against the reference Claude Code adapter:

  • If the manifest declares a [history] strategy that yields records, spt-core normalizes them and pipes them to the summarizer on stdin.
  • With no [history] section, or a native history store that is still empty, there are no records — so stdin is empty. This is the field case: the reference adapter’s history is native and typically empty at echo time, so the summarizer receives an empty stdin.

The load-bearing consequence for an adapter author: the echo-commune command must self-source the session it summarizes (locate and read the transcript itself), exactly as a [digest] fetcher extractor does. Do not write a summarizer that reads its transcript from stdin. What spt-core reliably supplies is the command template with the key catalog filled (and the child’s env — see below); the transcript is the summarizer’s to find.

Self-locating the harness

A summarizer that self-sources its transcript needs to find the harness’s config/log root. spt-core carries that in through the manifest’s read-env allowlist, so the value survives the daemon boundary (the echo child runs in the daemon context, where the original launch environment is long gone).

[env.CLAUDE_CONFIG_DIR]
direction = "read"
value = "~/.claude"          # fallback when the launch env didn't set it
  • Declare each locator var with direction = "read". spt-core captures it from the launch environment at bind — an explicit allowlist, never the whole environment, and only when the ambient value is actually present.
  • The captured value is written onto the perch record, so it is available when the echo (or digest, or Psyche) child spawns later.
  • At spawn spt-core injects it as a {VAR} substitution key. Resolution order: the captured ambient value wins (a relocating profile — e.g. a wrapper that sets CLAUDE_CONFIG_DIR to a private dir) → else the directive’s own value fallback (the base harness default, e.g. ~/.claude) → else the var is omitted, so a {VAR} reference fails loud rather than resolving a wrong path. A leading ~ expands to the home dir.

Reference the captured key in the echo command the same way [digest] does:

[session.echo_commune]
command = "my-harness-summarize --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}"
keys = ["session_id", "CLAUDE_CONFIG_DIR"]

Drop-file protocol

spt-core files the summarizer’s output as a drop file — the exact same channel a hand-written commune uses — then ingests and deletes it. Three invariants define the contract:

1. spt-core is the single writer. The filename is fixed by spt-core — <endpoint_id>-commune.md — and the adapter declares only the directory ([session] commune_dir). The mind never writes this file; spt-core writes it atomically (with a bounded access-denied-only retry for transient antivirus/indexer locks) and is the sole deleter. Never have the summarizer write the drop file itself.

2. The directory resolves per-endpoint (hardened in v0.29.0). A commune_dir may be absolute or relative:

  • Absolute → used as-is.
  • Relative → resolved against the endpoint’s own recorded working directory, read fresh at drop time — never against the daemon’s process cwd.
  • Relative with no recorded cwd → spt-core skips the drop and warns once (per endpoint, per daemon run). It never guesses and never falls back to the daemon’s cwd. This is the fix for a real outage: under a service-launched daemon whose process cwd was a system directory, a relative drop dir once resolved there and failed with a permission error on every write. The loud skip makes a mis-declared relative dir a diagnosable signal instead of a silent failure. Declare an absolute commune_dir, or ensure the endpoint’s cwd is recorded, to avoid the skip.

A missing [session.echo_commune] role is likewise a loud once-skip, not a retried fault.

3. Ingest deletes the drop. On its next pulse tick spt-core reads the drop, routes it into the durable context tiers, and deletes the file — whether the content was written or suppressed as a stale snapshot (both mean “consumed”). A read/write error leaves the file in place to retry on the next pass. The file disappearing is the success signal.

What spt-core expects on stdout

The summarizer’s stdout is the brief — the cheap-model synthesis of the session, as plain text. spt-core does not require a structured format at write time; it stamps a provenance header (Source: echo-commune) and writes the result as the commune drop file.

On the later ingest tick that body is parsed with the two-slice envelope grammar, the same one a hand-written commune uses:

  • <live-context>…</live-context> → the live tier (who the agent is and what it is doing; follows the endpoint everywhere).
  • <project-context>…</project-context> → the project tier (scoped to the current project).
  • An untagged body routes whole to the live tier.

Every write is precedence-guarded — a stale snapshot arriving inside another writer’s protection window is suppressed (but still consumed and deleted). The checkpoint sentinel !!checkpoint!!, if the brief carries one, is stripped before both presentation and the durable write, so it never persists in the stored context.

In one line

Declare [session.echo_commune] with a command that self-sources its transcript (found via a direction = "read" locator key) and prints the brief to stdout; declare an absolute commune_dir; let spt-core do the spawn, the file-drop, the ingest, and the delete. That is the whole contract.