Files
pulse-bridge/README.md
T
Nirav PatelandClaude Fable 5 eb0033b952 migrate write+read path from mib007 comms to the unified message envelope
Pulse's Activity UI (verified live on :5520, Vite dev serving the checkout
at 7293d857) now reads AND writes /api/workspaces/:ws/messages, so the
bridge follows:

- writes: POST /messages with server-side idempotencyKey (ledger:<id> for
  roots, (id,op,at) for feed comments, pulse-* keys for intake replies);
  replay returns the ORIGINAL message, so the v0 find-existing recovery
  scan and drop-on-500 comment policy are gone (5xx now retries safely,
  only 400/404/410 drop)
- reads: intake ingests envelope roots (kind=post), normalized to the old
  comms row shape at the boundary; comms GET would miss envelope-native
  user posts entirely
- state carries over unchanged: backfill 0103 + mirror trigger 0104
  preserve comms ids, so the ledger→post map and channel id stay valid and
  old threads accept replies (verified via /messages/threads/<old-id>)
- self-recognition: envelope posts carry no userName override, so intake
  now skips the service actor id (00000000-…-0001) structurally
- codex P2 fixed: adopt unmapped v0-era posts (no idempotency key) by
  ledger:<id> body scan before creating, so an old insert-then-error
  survivor cannot be duplicated

Known cosmetic tradeoff (documented in README): no byline override on the
envelope route — new posts render under the service actor, not "Ledger".

E2E on the live feed: post 715f52ec, stage comment 74d19219, needs-you
@rapidnir comment 2f898b75 + comment.mention notification row for
rapidnir-admin, close comment 9f0a5097; timed-out create retried into the
same row (idempotency proven, count=1).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YECpkAwQUwgu7NVy91R8fW
2026-08-22 22:21:34 -04:00

6.8 KiB

pulse-bridge v1

Mirrors the estate's shared work ledger (~/.local/bin/shre-items, JSONL store at ~/.shre/open-items/items.jsonl) into Pulse — the social-feed surface of the local mib007 instance (http://127.0.0.1:5520, launchd ai.shre.mib007). Ledger items become posts, stage changes become threaded comments, needs-you escalations mention the user.

Where Pulse actually lives (re-verified 2026-08-23)

The Pulse UI is mib007's Activity app (/activity, PULSE_ROUTE in ui/src/platform/lib/app-routes.ts). Since the unified message envelope landed (migrations 0101-0111 applied; feed_items view live) the Activity feed reads AND writes the envelope (useActivity.ts stage 3), so the bridge does too:

  • create post: POST /api/workspaces/:wid/messages {channelId | channelSlug, body, idempotencyKey} → returns the message (id = post id; a replayed idempotencyKey returns the ORIGINAL message)
  • comment/reply: same endpoint with threadId: <post message id>
  • read roots: GET /api/workspaces/:wid/messages?channelId=&kind=post (root posts only; replies are nested in blocks.replies) — the same query the Activity UI issues
  • channel: the envelope channel with slug activity. Comms-era channel and message ids are preserved in the envelope (backfill 0103 + mirror trigger 0104), so v0 state (channel id, ledger→post map) carried over unchanged and old threads still accept replies.

Auth: mib007 service token (~/.shre/service-tokens.json, key mib007), board-level, valid from loopback. mib007 runs in authenticated mode, so requests without it are rejected. Attribution caveat: the envelope attributes writes to the authenticated actor and has no userName override, so bridge posts no longer carry the "Ledger" byline — they render under the service actor (UI fallback "User"). Legacy posts keep their mirrored actorDisplayName: "Ledger".

Mapping

ledger op Pulse action
add new post: emoji by kind + title, detail, tag line (#kind #stage #tags surface: project: ledger:<id>)
update comment on the mapped post: stage → review, ⛔ failed at verify, attempt 2 — <note>, kind → …
update to kind: needs-you comment @nir NEEDS YOU: …
close comment ✅ done — <why> or 🗑 dropped — <why>

First run seeds posts for currently-open items only (shre-items list --json); history before the bridge is not replayed.

State & config

  • State: ~/.shre/pulse-bridge/state.json — checkpoint (max at seen), boundary-second (id,op,at) dedupe set (the feed window is inclusive), ledger-id → post-id map, resolved channel id. Written atomically (tmp + os.replace).
  • Config: bridge.env next to the script (or PULSE_BRIDGE_ENV); environment variables override. See the file for keys (poll interval, mib base URL, workspace, channel, mention handle, token file).
  • Poll cadence: every 30 s the daemon runs shre-items feed --since <checkpoint> and processes new records in at order. A failed record stops the batch before the checkpoint advances past it, so it is retried next cycle.

launchd

ai.shre.pulse-bridge — KeepAlive daemon, logs to ~/.shre/pulse-bridge/bridge.log:

cp ai.shre.pulse-bridge.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.shre.pulse-bridge.plist

v1 intake (reverse direction)

New top-level posts in the activity channel written by a human (not the bridge, not agent accounts, not 🤖-prefixed, not mirror posts carrying a ledger: tag line) become executed, tracked background work.

Prefix a post with 💬 (or chat:) to just talk — chat-only posts are never turned into a ledger item or task and the bridge never replies to them. The prefix list is configurable via PULSE_BRIDGE_INTAKE_OPTOUT (comma-separated, case-insensitive).

  1. Ledger itemshre-items add --kind pipeline --stage queued --tag pulse-intake, title = first 80 chars of the post, detail = full post + post id. The ledger→post map is seeded with the USER'S post id before the item's op:add reaches the feed, so the outbound mirror adopts the user's post instead of creating a duplicate.
  2. Kanban taskhermes kanban create … --created-by pulse-bridge --idempotency-key pulse-<post id> --json on the default board; the Hermes embedded-gateway dispatcher picks it up (~60 s). Cap: max 3 concurrent intake-spawned tasks; overflow posts get ⏳ queued behind N tasks and start when a slot frees.
  3. Acknowledgment — comment 🤖 picked up — ledger <id>, kanban <task> on the user's post.
  4. Tracking — each poll reads kanban list --json once; on a status change it comments (▶️ running / ⛔ blocked / ✅ completed — <result>) and advances the ledger item (running → stage build; done → closed with --why "kanban <id> completed").

Intake state (intake_checkpoint epoch-ms + post↔ledger↔task map) lives in the same atomically-written state.json. First intake run only sets the checkpoint to the newest existing post — history is never ingested.

Intake caveats (accepted, reviewer-flagged)

  • Status/ack comments are best-effort: a crash between a state save and its comment can drop or (once) repeat a comment. Ledger and kanban stay consistent — shre-items done on a closed item is a logged no-op and --idempotency-key pulse-<post id> adopts the existing task on retry.
  • Bridge-output filtering is structural first (envelope-native bridge posts carry the service actor id 00000000-…-0001, agents carry actorType: "agent"), then content-based for legacy posts (display name, 🤖 prefix, ledger: tag line, threaded).

Limitations

  • Mentions dispatch server-side — the envelope route runs the same dispatchMention side-effect comms did: the first @handle matching a workspace agent fires an AI reply; a matching workspace member gets a comment.mention notification. The bridge zwsp-neutralises every @ in ledger-derived text and deliberately keeps the needs-you @rapidnir escalation live so the human is actually notified. The historical insert-then-500 mention crash is fixed AND harmless now: writes carry idempotency keys, so a retry returns the original message instead of duplicating it (DROP now only happens on 400/404/410 — thread target gone).
  • No byline override. See the attribution caveat above — posts render under the service actor, not "Ledger". Needs a mib007-side actorDisplayName passthrough for service tokens to restore.
  • Updates for items that pre-date the bridge and were never seeded (closed before first run) are skipped with a log line — there is no post to comment on.
  • One workspace, one channel. Multi-workspace fan-out is future work.
  • Reactions on posts are not mirrored back to the ledger.