feat(wake): W2 — three-cursor durable store + RECEIVED/CONSUMED ack-wrapper + watch-list schema
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

Implements W2 of the wake/heartbeat canon (EPIC #892), honoring the
CONVERGED-DESIGN contracts exactly.

A2 store.sh — three-cursor durable queue (§1.2/§2.3/§2.4):
  - observed_seq authoritative monotonic cursor (detector assigns, lib records);
    SHAs are descriptors, not the cursor.
  - durable pending-inbox {observed_seq, locators, class, emit_ts, hmac}; hmac is
    an UNSIGNED placeholder (signing is W3/A5).
  - consumed_seq advances ONLY on a CONSUMED ack over a CONTIGUOUS gapless prefix
    <=N; never on enqueue/exit-0/paste.
  - coalesce: digest REPLACES the pending cumulative-state entry; actionable/human
    APPEND; ALL classes durable (durability never bypassed); absent class => actionable.
  - drain returns the deliverable set; every mutation is atomic write-tmp+rename.

A4 ack.sh — RECEIVED/CONSUMED wrapper (§2.2):
  - RECEIVED dedups DELIVERY via wake_id (dup = re-RECEIVE, never re-action).
  - CONSUMED N local-write-only to the XDG ledger, cumulative, gapless-prefix
    enforced; background sync ships the ack (never blocks on network).
  - `embed` prints the one copy-run line a digest embeds.

Watch-list SCHEMA contract (defaults/wake-watch-list.schema.json) with
schema_version; wake manifest.txt declares component semver + supported schema
range (schema_min/schema_max, Gate B — version metadata only, NOT path ownership).

RED-first shell harness wired into test:framework-shell covers each invariant
(three-cursor, coalesce-vs-append, contiguous-prefix CONSUMED, wake_id dedup,
atomic tmp+rename crash-safety, durability-across-restart, ack-never-blocks-on-net).
shellcheck clean; sanitization gate + operator firewall pass.

Part of #892

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
This commit is contained in:
mosaic-coder
2026-07-25 18:28:45 -05:00
parent ab6e8e80dc
commit 0f1a37ed2d
7 changed files with 1163 additions and 1 deletions

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# _wake-common.sh — shared state resolution + atomic-write primitive for the
# wake/heartbeat durable queue (W2 of the wake canon, EPIC #892).
#
# CONTRACT ANCHORS (docs/scratchpads/heartbeat-planning/CONVERGED-DESIGN.md):
# §1.2 three-cursor durable queue; ALL state XDG, atomic write-tmp+rename.
# §2.3 durability is NEVER bypassed.
#
# This file is sourced by store.sh and ack.sh. It defines NO top-level actions;
# sourcing it is side-effect-free except for setting readonly path vars.
#
# Operator-agnostic (framework firewall): state location is derived purely from
# XDG / env. No operator paths, names, or secrets appear here.
# ---------------------------------------------------------------------------
# State layout (XDG). §1.2: "all state XDG".
# base = ${WAKE_STATE_HOME:-${XDG_STATE_HOME:-$HOME/.local/state}/mosaic/wake}
# agent = ${WAKE_AGENT:-default} (per-agent queue namespace)
# STATE_DIR = <base>/<agent>
# ---------------------------------------------------------------------------
wake_state_dir() {
local base agent
base="${WAKE_STATE_HOME:-${XDG_STATE_HOME:-$HOME/.local/state}/mosaic/wake}"
agent="${WAKE_AGENT:-default}"
printf '%s/%s' "$base" "$agent"
}
# File names within STATE_DIR.
# observed_seq — highest observed_seq ever recorded (max monotonic int).
# consumed_seq — consumer cursor: top of the contiguous consumed prefix.
# observed.set — observed seqs in the live window (> consumed_seq), one int
# per line; the gap-detector's source of truth. Immune to
# coalescing (coalescing removes a pending ENTRY, never the
# fact that its seq was observed).
# pending.jsonl — the durable pending-inbox: one entry JSON object per line,
# {observed_seq, locators, class, emit_ts, hmac}.
# ack-ledger.jsonl — local-write-only ack ledger (RECEIVED / CONSUMED).
# ack-sync.state — background-sync bookkeeping (last shipped / outage flag).
# _wake_tmp_glob DIR — the glob used for atomic-write temp files, so readers can
# ignore in-flight/crashed writes. A crash leaves one of these; it is NEVER the
# live file (only rename promotes content), so it can never corrupt a read.
_wake_tmp_prefix='.wake.tmp.'
# _atomic_write TARGET (content on stdin)
# §1.2 / task: EVERY state mutation is atomic write-tmp+rename. Write a temp
# file in the SAME directory (so mv is a same-filesystem atomic rename), fsync
# is best-effort, then rename over the target. A crash before the rename leaves
# the old target fully intact and a stale .wake.tmp.* that readers ignore.
_atomic_write() {
local target="$1" dir tmp
dir="$(dirname "$target")"
[ -d "$dir" ] || mkdir -p "$dir"
tmp="$(mktemp "$dir/${_wake_tmp_prefix}XXXXXX")" || return 1
if ! cat >"$tmp"; then
rm -f "$tmp"
return 1
fi
# Best-effort durability of the temp file before the rename. Not fatal if the
# platform lacks it — the rename atomicity is the load-bearing guarantee.
sync "$tmp" 2>/dev/null || true
if ! mv -f "$tmp" "$target"; then
rm -f "$tmp"
return 1
fi
}
# _wake_clean_stale_tmp DIR — remove leftover atomic-write temp files (e.g. from
# a crash mid-write). Safe: these are never the live store.
_wake_clean_stale_tmp() {
local dir="$1"
[ -d "$dir" ] || return 0
find "$dir" -maxdepth 1 -name "${_wake_tmp_prefix}*" -type f -delete 2>/dev/null || true
}
# _wake_read_int FILE DEFAULT — read a single integer from FILE, or DEFAULT.
_wake_read_int() {
local file="$1" def="$2" val
if [ -f "$file" ]; then
val="$(tr -d '[:space:]' <"$file")"
case "$val" in
'' | *[!0-9]*) printf '%s' "$def" ;;
*) printf '%s' "$val" ;;
esac
else
printf '%s' "$def"
fi
}
# _wake_init_dir STATE_DIR — ensure the state layout exists; idempotent.
_wake_init_dir() {
local dir="$1"
mkdir -p "$dir" || return 1
_wake_clean_stale_tmp "$dir"
[ -f "$dir/observed_seq" ] || printf '0' | _atomic_write "$dir/observed_seq"
[ -f "$dir/consumed_seq" ] || printf '0' | _atomic_write "$dir/consumed_seq"
[ -f "$dir/observed.set" ] || printf '' | _atomic_write "$dir/observed.set"
[ -f "$dir/pending.jsonl" ] || printf '' | _atomic_write "$dir/pending.jsonl"
[ -f "$dir/ack-ledger.jsonl" ] || printf '' | _atomic_write "$dir/ack-ledger.jsonl"
}