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
136 lines
6.8 KiB
Markdown
136 lines
6.8 KiB
Markdown
# 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`:
|
|
|
|
```sh
|
|
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 item** — `shre-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 task** — `hermes 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.
|