Post 24 — v0.9.4: Context, Dispatch, and Relay

Branch: feat/v0.9.4-context-dispatch-relay
Version: v0.9.4


Where we were

At the end of PR #59 (dispatch context scoping), synlynk had a working async daemon with background job dispatch. The P0 race condition was fixed: generate_context() now returns a string, dispatch_agent() uses it in-memory instead of reading a shared file, and context_mode lets tasks opt out of context injection. The foundation for v0.9.4 was clean.

What moved the goalpost

BS-1 (2026-06-24) answered Q1–Q10 on the context/dispatch/relay layer. Nine decisions locked:

  1. Context delivery: in-memory (already in P0) — no global file read in dispatch path
  2. SQLite canon: stories table is primary; todo.md is a generated view
  3. synlynk jobs: one-shot table (TUI HUD deferred to v1.0)
  4. Relay topology: standalone HTTP broker (SSE fan-out, POST publish, stdlib only)
  5. Relay event schema: 7 types — story_updated, job_dispatched, job_completed, alert_raised, context_checkpoint, table_changed, broadcast
  6. broadcast event: kind field (motd|wellness|message|joke|custom) — presence layer for team ambient awareness
  7. Pre-flight gate: agent CLI must be on PATH before dispatch
  8. Context budget: soft warn at 80KB (_warn_context_size), hard block via agent profile context_max_bytes
  9. Agent profiles: .agents/<agent>.json overrides context_mode and context_max_bytes per agent

Three additional fixes surfaced during dispatch dogfooding:

  • subprocess.Popen in dispatch_agent() had no cwd= — Codex inferred the worktree path from process CWD, but Agy resets its CWD on startup
  • Agy prompt needed an explicit ## Working Directory header — added to _format_prompt_for_agent()
  • Claude headless dispatch was blocked by the permission system — added --dangerously-skip-permissions to Claude's non_interactive_flags

What this PR ships

Relay architecture
Context-dispatch architecture

T1 — SQLite canon (stories.status + generated todo.md)
_migrate_db() now adds a status TEXT DEFAULT 'open' column to stories idempotently. _generate_todo_md() reads the stories table and writes project-docs/todo.md as a generated view (- [ ] open, - [x] done, - [-] deferred). _import_todo_to_stories() parses hand-written - [ ] lines and inserts new story rows, skipping any with a known <!-- id:story-XXXX --> tag. cmd_story_create() calls _generate_todo_md() after every create. SQLite is now the single source of truth — no more manual todo.md editing for task state.

T2 — Per-agent context profiles (.agents/<agent>.json)
_load_agent_profile(agent) -> dict reads .agents/<agent>.json silently (returns {} if absent, unlike _load_agent_config() which raises). dispatch_agent() loads the profile and applies: context_mode override and context_max_bytes truncation before prompt assembly. _format_prompt_for_agent() agy branch drops the hardcoded [:2000] truncation — truncation is now profile-controlled. synlynk agent configure <name> writes the profile interactively.

T3 — synlynk jobs SQLite read + --watch + pre-flight gate
cmd_jobs() replaced: now reads daemon_jobs SQLite, prints a one-shot table (ID, AGENT, STORY, STATUS, AGE, EXIT), supports --all to include done/failed, --watch for 2s auto-refresh. _preflight_dispatch(agent) checks the agent CLI is on PATH before spawning — raises ValueError with install hint on miss. dispatch_agent() now writes a row to daemon_jobs (mirror of jobs.json for structured queries). synlynk dispatch gained --context-mode {none,task,full}.

T4 — Relay wire protocol (HTTP SSE broker)
RELAY_EVENT_TYPES frozenset defines the 7 valid event types. _build_relay_event(type, payload) -> dict adds ts and origin_node, rejects unknown types with ValueError. _make_relay_handler(subscribers, lock) returns an http.server.BaseHTTPRequestHandler subclass: GET /events (SSE stream), POST /publish (fan-out to all subscribers, returns {"ok": true, "fans": N}), GET /health (liveness check). SynlynkRelay wraps the server with start() (foreground, blocks on Ctrl-C) and is_alive(). synlynk relay start [--port N] and synlynk relay broadcast <body> [--kind motd|wellness|message|joke|custom] [--relay-url URL] wired to CLI.

T5 — Sentinel VERIFY_SKIP pattern
_extract_compliance_tags(output_text) -> dict scans agent output for test and verify phrases (ran_tests, verify_before_commit boolean flags). Pattern 4 in check_sentinel_patterns(): when exit_code == 0 but neither flag is True, fires an informational VERIFY_SKIP alert to sentinel.md. Builds the compliance dataset for the upcoming Agent Behaviour (AB) epic.

Dispatch fixes (committed as pre-T2 patch)

  • subprocess.Popen in dispatch_agent() now sets cwd=os.getcwd()
  • Agy prompt header now leads with ## Working Directory\n{os.getcwd()}\nAll file edits MUST be in this directory.
  • Claude non_interactive_flags now includes --dangerously-skip-permissions for headless dispatch

Dispatch dogfooding findings

All 5 tasks were dispatched via synlynk dispatch using the system's own dispatch mechanism. Key learnings:

  • Codex (T1, T2, T3, T5): reliable for TDD loops when given scannable criteria. Completed all 4 tasks cleanly.
  • Agy (T4 attempt): authenticated, CWD fix worked (probe confirmed), but stalls on multi-step tasks when it hits a blocking shell command (waited for pytest to finish instead of proceeding). Good for read-only exploration, not TDD loops.
  • Claude (T2 attempt in prior session): blocked by permission system in non-interactive mode. Fixed by --dangerously-skip-permissions.
  • CWD inheritance: subprocess.Popen without cwd= relies on the calling process CWD — works for Codex (reads workdir from process), not for Agy (resets CWD on startup).

Visuals

BS-1 brainstorm visuals that informed this PR: docs/brainstorm/bs1-context-dispatch-relay/relay-events-final.html (event schema, broadcast kinds, deployment staircase) and agent-failure-surface.html (pre-flight gate, job lifecycle, context budget).

What this achieves

synlynk now has a coherent context/dispatch layer: task state is SQLite-primary with generated views, dispatch is profile-driven per agent, jobs are queryable from SQLite, relay events have a typed schema and a working HTTP broker, and the sentinel now detects when agents skip verification. 465 tests, +26 since T1 baseline (439).

New goalpost

v0.9.4 merged → next: BS-2 (Onboarding + Mode Taxonomy), BS-3 (Agent Behaviour — builds on VERIFY_SKIP), BS-4 (Command Audit). Agent Ecosystem Epic (v0.8.1–v0.8.4) remains parked for contiguous effort after BS-2/3/4 complete.