Skip to content

fix(session-handoff): drain relay notes into session-state.md (closes #1738)#2016

Open
john-the-dev wants to merge 2 commits into
sonichi:mainfrom
john-the-dev:fix/relay-consumer-session-handoff-1738
Open

fix(session-handoff): drain relay notes into session-state.md (closes #1738)#2016
john-the-dev wants to merge 2 commits into
sonichi:mainfrom
john-the-dev:fix/relay-consumer-session-handoff-1738

Conversation

@john-the-dev

Copy link
Copy Markdown
Contributor

Problem

PR #1737 removed catchup-after-startup — the only consumer of relay-*.md files written by /relay and proactive-loop step 7. Since then, 43 relay notes have accumulated in workspace/relay/ unread, breaking cross-session continuity (#1738).

Fix

Wire the drain into session-handoff.sh (the PreCompact hook), which already runs on every context compaction and writes session-state.md:

  1. After writing all existing sections, reads all unprocessed workspace/relay/relay-*.md files in sort order.
  2. Appends them as ## Relay Notes (from prior sessions) in session-state.md.
  3. Archives each file to relay/processed/ (mirrors the original drain pattern from catchup-after-startup).

The next session reads session-state.md at startup (per CLAUDE.md "Session Continuity" section) and gets the relay content naturally — no new skill, no new cron, no new config.

Why session-handoff.sh

session-handoff.sh already has WORKSPACE_DIR resolved, runs exactly when session memory rolls off (context compaction), and produces the file the next session reads. It's the minimal, already-running hook that can do this drain without adding a new mechanism.

Test

Ran locally with 43 accumulated relay notes: all 43 moved to relay/processed/, session-state.md includes the ## Relay Notes (from prior sessions) section, script exits 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RUA5TZgDa7VmFcNtPka38t

…onichi#1738)

PR sonichi#1737 removed catchup-after-startup — the only consumer of relay-*.md files
written by /relay and proactive-loop step 7. Since then, 43 relay notes have
accumulated in workspace/relay/ unread, breaking cross-session continuity.

Wire the drain into session-handoff.sh (the PreCompact hook) so it fires on
every context compaction: reads all unprocessed workspace/relay/relay-*.md in
sort order, appends their content as a "## Relay Notes (from prior sessions)"
section in session-state.md, then archives each to relay/processed/. The next
session reads session-state.md at startup (per CLAUDE.md) and picks up the
relay content naturally.

Mirrors the original drain pattern from catchup-after-startup (mv to processed/
after read) without requiring a separate skill or cron entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUA5TZgDa7VmFcNtPka38t
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Coverage Gate

No Python changes — nothing to gate (bar: 95% on changed lines).

@john-the-dev john-the-dev marked this pull request as ready for review July 8, 2026 14:16
Comment thread src/session-handoff.sh Outdated
echo ""
echo "### $(basename "$relay_file")"
cat "$relay_file"
mv "$relay_file" "$RELAY_PROCESSED/" 2>/dev/null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: this drains workspace/relay/*.md into processed/ while the session-state file is still being streamed directly to $STATE_FILE. If the script is interrupted or the write fails after mv "$relay_file" "$RELAY_PROCESSED/", the relay note has been retired but never made it durably into session-state.md, so the next session loses that cross-session context entirely. I think the notes need to stay in place until the new state file has been written successfully (e.g. write to a temp file first, then move/retire the relay notes after the final replace).

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Automated review:

src/session-handoff.sh:136-138 will fail on any workspace that does not already have a relay/ directory. Because the script runs under set -euo pipefail, find "$RELAY_DIR" ... | sort exits non-zero when the directory is missing, and the command substitution aborts the whole compaction handoff before session-state.md is written. Please guard the find with [ -d "$RELAY_DIR" ] (or otherwise force a zero status) before enabling this drain path.

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Automated mergeability review (2026-07-09)

Signal: NOT MERGEABLE

Current blocker(s):

  • Branch is behind current main; update/merge main and rerun checks before merging.

This is a conservative backlog triage signal. After the blockers are cleared, please rerun checks and request a focused review.

Previously each relay note was mv'd to relay/processed/ inside the
loop that cat's it into the redirected block, so an interrupt after
the move but before session-state.md was durably written would retire
a note that was never captured — the next session loses that context.

Reorder to atomic capture-then-retire: the loop now only appends note
content to the redirected block; the mv to processed/ happens after the
`} > "$STATE_FILE"` write completes, gated on the state file being
non-empty AND each note's header actually being present in it. If the
capture failed or was interrupted, the note is left in place to retry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFp2p6XUSAQ1eAKKZeiZPt
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta review of 9c834f2e (since my 2026-07-08 15:06 review). CI: all checks green.

Prior point — addressed:

  • Retire-before-durable-write hazard — fixed. The capture loop no longer moves notes while streaming into $STATE_FILE; retirement happens only after the brace-group write completes, gated on [ -s "$STATE_FILE" ] plus a per-note grep -qF "### $(basename ...)" confirmation that the note's header actually landed (src/session-handoff.sh:161-167). An interrupt between capture and retire now leaves the notes in place for the next run instead of losing them — exactly the at-least-once behavior I asked for. The brace group runs in the current shell, so unprocessed_relay/RELAY_PROCESSED are correctly visible to the retire block; I also ran the capture+retire flow locally (2 notes drained to processed/, content present in the state file; re-run with relay/ absent exits 0 with a clean state file).

Closing the loop on the 2026-07-08 22:46 automated follow-up comment about set -euo pipefail aborting on a missing relay/ dir: that concern does not apply — this script deliberately does not use set -e (see the comment at src/session-handoff.sh:28), find's stderr is suppressed, and the pipeline's exit status is sort's. Verified empirically: missing relay/ yields an empty list and the section is skipped cleanly.

Non-blocking: if the state-file write is partially truncated by e.g. ENOSPC mid-stream, a note whose ### header made it in before truncation could still be retired with a clipped body. The temp-file+rename approach from the original comment would close that residual window too, but given the header check and that session-state.md is regenerated every compaction, this is an acceptable edge.

LGTM (comment only). Branch is behind main (merge state BEHIND) — needs an update-branch before merge; no conflicts reported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants