Public: anyone can read this; the crew can change it.

A like is a signal in the decision ledger, reachable the same way for a person and an agent, and a test reads it back #10410

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A like is now one row in the `feed_signal` ledger (who, what, when), written the same way from the page, the API and the MCP. The gate is clean after adding the commit-message lines it asked for. `tsc` printed no errors. `test/feed-like.test.ts`, `test/mcp-size.test.ts`, `test/mcp.test.ts` and `test/conversation-confirm.test.ts` pass (93 tests). I did not run the full suite here, because the forge…

shipped production is serving it deploy aa46b8b

opened this

A like is now one row in the feed_signal ledger (who, what, when), written the same way from the page, the API and the MCP. The gate is clean after adding the commit-message lines it asked for. tsc printed no errors. test/feed-like.test.ts, test/mcp-size.test.ts, test/mcp.test.ts and test/conversation-confirm.test.ts pass (93 tests). I did not run the full suite here, because the forge runs it on push.

  • Writing a like: setLike in src/feed/likes.ts is the single write path. POST /feed/like (the button), POST /api/v1/feed/interactions with kind: "liked" or "unliked", and the MCP tool feed_like all go through it.
  • Idempotent: a second like writes nothing and answers the same.
  • Unlike: it appends a new unlike row. Nothing is edited or deleted.
  • Count: feed_like holds the current state and is updated in the same transaction. A post's likes therefore equals the ledger's count for it, and the test asserts that after a like, an unlike and a repeat.
  • Auth: a write needs a session with a same-origin POST, or a token with rw scope. An anonymous caller gets 401 naming /signin. A token meets the ordinary per-token rate limit, so an agent can't flood likes.
  • Reading it back: GET /api/v1/feed/likes?item=…, or feed_like with act: "history". This is described in OpenAPI.
  • Docs: docs/FEED.md says where a like is stored and how to read it.
  • Migration: 20261109200000-feed-like-ledger adds the ledger table and its pin.
  • MCP: this adds a new tool file, src/http/mcp/tools/feed_like.ts, instead of an action on an existing tool. mcp-size passes without raising any ceiling.
  • Terminal: the Feed screen shows ♥/♡ counts and l toggles a like through the same code.

Not done: a like does not move feed ranking. I did not add a separate rate limit for likes.

To try it on the preview, like a post with POST /feed/like, then call GET /api/v1/feed/likes?item=event:<id>.

Closes #10389

commented

A like is now one row in the feed_signal ledger (who, what, when), written the same way from the page, the API and the MCP. The gate is clean after adding the commit-message lines it asked for. tsc printed no errors. test/feed-like.test.ts, test/mcp-size.test.ts, test/mcp.test.ts and test/conversation-confirm.test.ts pass (93 tests). I did not run the full suite here, because the forge runs it on push.

  • Writing a like: setLike in src/feed/likes.ts is the single write path. POST /feed/like (the button), POST /api/v1/feed/interactions with kind: "liked" or "unliked", and the MCP tool feed_like all go through it.
  • Idempotent: a second like writes nothing and answers the same.
  • Unlike: it appends a new unlike row. Nothing is edited or deleted.
  • Count: feed_like holds the current state and is updated in the same transaction. A post's likes therefore equals the ledger's count for it, and the test asserts that after a like, an unlike and a repeat.
  • Auth: a write needs a session with a same-origin POST, or a token with rw scope. An anonymous caller gets 401 naming /signin. A token meets the ordinary per-token rate limit, so an agent can't flood likes.
  • Reading it back: GET /api/v1/feed/likes?item=…, or feed_like with act: "history". This is described in OpenAPI.
  • Docs: docs/FEED.md says where a like is stored and how to read it.
  • Migration: 20261109200000-feed-like-ledger adds the ledger table and its pin.
  • MCP: this adds a new tool file, src/http/mcp/tools/feed_like.ts, instead of an action on an existing tool. mcp-size passes without raising any ceiling.
  • Terminal: the Feed screen shows ♥/♡ counts and l toggles a like through the same code.

Not done: a like does not move feed ranking. I did not add a separate rate limit for likes.

To try it on the preview, like a post with POST /feed/like, then call GET /api/v1/feed/likes?item=event:<id>.

Changes requested — before the latest push

The write path is sound: one setLike, idempotent, append-only, 401 naming /signin, token scope and rate limit tested. Changes needed, all in one round:

  1. Both doors: the terminal can toggle a like but cannot read the ledger. Add the history to the Feed screen in src/tui/screens/feed.ts (a key on the post under the cursor listing who/act/at from likeLedger), gated by the same readInteraction check, with a test.
  2. The how-to belongs in docs/features/<name>.md (e.g. docs/features/feed-like-ledger.md), not only in docs/FEED.md. Also docs/FEED.md still says the terminal runs toggleLike "as the page"; the page now runs setLike.
  3. likeLedger returns the OLDEST 200 rows with no way to page, so a busy item's recent likes silently never appear, and a user can flip like/unlike to grow the ledger without bound. Add a cursor (after=seq) or return newest-first with a more flag, and test it.
  4. The ledger endpoint shows every liker's username to anyone who can read the item. Say this is deliberate in the docs, or return only the caller's own rows plus counts for non-admins.
  5. Clean: const { changed: _, ...now } = setLike(...) is repeated three times. Have setLike return Likes plus a separate way to know it changed, or make one helper, so the throwaway binding goes away.
commented

All five findings are addressed and pushed. tsc, scripts/gate.ts, scripts/design-lint.ts and six test files covering the feed, likes, contract, MCP and the terminal Feed screen pass. I did not run the full suite: scripts/fast.ts named 1636 files, which is too broad to run here, so the forge's check will run it.

  1. Terminal history. On the Feed screen, h on the post under the cursor lists who, act and when from likeLedger, newest first. m reads older rows and esc closes it. It goes through the same readInteraction check as l. A new test covers an empty history, a like showing up in it, and a reader who can't see the item being refused.
  2. Docs. The how-to is now docs/features/feed-like-ledger.md. In docs/FEED.md the terminal paragraph now says it writes through setLike, with toggleLike only choosing the act. The same paragraph describes h and links to the new file.
  3. Paging. likeLedger returns { rows, more }, newest first, 50 a page, with a before=<seq> cursor. That cursor works on the API and on the MCP history act. A bad before is a 422 (invalid_before). Tests build 60 rows and read two pages over the API and the MCP tool.

- The OpenAPI route and the MCP tool docs say the same. - I did not cap or prune the ledger. A like/unlike flip still appends a row, bounded only by the per-token rate limit. Paging means the newest rows are always reachable, but the table can still grow.

  1. Liker names. I chose to document it rather than narrow it. A like is a public act on a post, as a comment is, so anyone who can read the item sees who liked it. This is stated in the docs and in a comment at the route. I did not add a non-admin view that hides other likers.
  2. Throwaway binding. setLike now returns plain Likes, and the changed field and LikeResult type are gone. Nothing used changed, since the tests count ledger rows.

Existing ledger tests were updated for the new newest-first order and the { rows, more } return shape.

To try it, open the Feed screen over ssh (Ctrl-K, "Feed"), press l, then h. Over the API, call GET /api/v1/feed/likes?item=event:…&before=<seq>.

commented

I merged main into the branch, resolved the two conflicts, and pushed.

tsc and scripts/gate.ts are clean. Four test files pass: tui-feed-reply, feed-like, contract and feed-interact. I did not run the full suite; the forge's check does that on the push.

  • src/tui/screens/feed.ts and test/tui-feed-reply.test.ts: I kept both sides. main's t key (timeline, newest first) and my h key (like history) both work, and the header comment and both tests are kept.
  • Name clash in src/http/feed.ts: main had added its own parseBefore, which clashed with mine. I renamed mine to parseLedgerBefore.
commented

I fixed the three palette failures, but I haven't fixed the two timing failures. I can't tell whether the next check will clear them.

  • Palette roster (3 failures, shard 64): my branch changed the Feed palette hint to "…, l likes, h who liked". test/palette-roster.test.ts still expected the old text. I updated the expected text in all three places; the test was stale, not the code. It passes now.
  • Sweep timing (2 failures, shards 12 and 47): "the three passes' request loops…" in test/security-sweep.test.ts and "the two passes'…" in test/security-floor-routes.test.ts both enforce a 5 s ceiling. That limit comes from #10416, already on origin/main, and I didn't touch those tests.

- Not my diff: my branch adds only one cheap route, GET /api/v1/feed/likes. Instrumenting the sweep here, the slowest requests were /og/*.png and the repository pages that spawn git. Those are unrelated to the like ledger. - Load-sensitive: locally the loop ran about 4–5.7 s with no check load, so it was already near 5 s. The same sweep on origin/main would likely fail the same way when the check runs 72 shards at once. I did not raise the ceiling, because that is another pull's guard. - If it fails again: it needs a decision on the ceiling or the slow routes, outside this pull.

I merged the current origin/main into the branch and pushed. Locally the gate, tsc --noEmit and the palette, feed-like, tui-feed-reply and mcp tests all pass. I did not run the two sweep tests as a pass/fail check, because locally they sit on the 5 s limit.

commented

I fixed one of the two failures and pushed. I couldn't reproduce the other.

  • "every module under src/ is imported by another, or allowed": this one was real, and it comes from main rather than from the like-ledger change. Main stopped importing src/kernel/why_now.ts from scripts/gate.ts, so only its test still reached it. I merged current main into the branch and added the file to scripts/unreached-allow.json. I chose the allowlist over deleting the module because test/why-now.test.ts still exercises it. If you'd rather delete the module and its test, say so. The test now passes here.
  • "a repository name with markup in it is escaped in the select, never raw": I couldn't reproduce this. It passed before the merge, after it, and in three more repeated runs. I made no change for it, so it may come back as a one-off in the shard run. If it fails again, send me the log of that failure.

bun scripts/gate.ts and bun x tsc --noEmit are clean. I ran only the two failing test files, not the full suite.

Approved

All earlier asks are met: terminal history (h/m, same readInteraction gate, tested), the how-to in docs/features/feed-like-ledger.md with README untouched, paged ledger with a validated before cursor on API and MCP, liker names documented as intentional, and the throwaway return type removed. Nothing new since my last read raises a security or design issue: writes go through one setLike in a transaction, every door runs readInteraction, anonymous callers get 401 naming /signin, and the flood is bounded by the per-token rate limit (tested). Non-blocking: feed_like is marked writes:true, so a read-only token cannot read history through MCP although the API route allows ro; consider splitting. Non-blocking: test/tui-feed-reply.test.ts has a whitespace-only line after the expect for alice like.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs