Trade & store logistics: host-agnostic trade watchers (meets WatchTradeFeed/Display) #52

Closed
opened 2026-07-05 01:22:35 +00:00 by coilysiren · 2 comments
Owner

Context

Trade watchers under the DiscordLink-replacement epic (#37), on the trades-ledger foundation (#6). DiscordLink ships WatchTradeFeed (event stream to a DM) and WatchTradeDisplay (self-updating snapshot), plus UnwatchTradeFeed/Display and ListTradeWatchers. Kai's epic thesis is that this read-only info belongs on the website, not Discord DMs, so this meets the watcher feature and exceeds it by making it host-agnostic.

No-reset spine

Watchers evaluate against the CurrencyTrade history the server already exports - each poll diffs the newest trades against stored watch queries. No new C# mod, no server reset. Persist watchers in a small SQLite store, mirroring the fair-price cache pattern (~/.cache/eco-mcp-app/).

Scope

The agent builds a host-agnostic trade-watcher subsystem:

  • Subscribe - watch an item, a store, a trader, or a price threshold (for example "iron ingot under 2.5"). MCP tool to create, list, and remove watchers, matching WatchTradeFeed / ListTradeWatchers / UnwatchTradeFeed.
  • Match - on each evaluation, surface trades matching the query since the watcher's last-seen mark (the feed semantic), plus the current matching state (the display semantic).
  • Surfaces - the watcher list and hits show in the SPA /trade route (sibling) and are queryable via MCP, so a URL-connected host like claude.ai can ask "what did my watchers catch." No Discord dependency.
  • Persistence - SQLite, keyed by watcher id, with the query, last-seen timestamp, and creation time.

Meets WatchTradeFeed, WatchTradeDisplay, UnwatchTradeFeed/Display, and ListTradeWatchers. Exceeds by delivering to the website and MCP instead of Discord DMs, and by allowing a price-threshold predicate, not just a name match.

Acceptance

  • MCP tools registered (create/list/remove/evaluate), ward exec smoke passes.
  • SQLite schema created idempotently, survives process restart.
  • Feed vs display semantics both covered (new-since-last-seen and current-state).
  • respx-mocked unit tests for match logic and the last-seen advance.
  • ward exec test / ward exec lint green. docs/FEATURES.md updated.

Dependencies

Foundation: #6. Parent: #37. Surfaced in the /trade SPA route (sibling).

## Context Trade watchers under the DiscordLink-replacement epic ([#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37)), on the trades-ledger foundation ([#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6)). DiscordLink ships `WatchTradeFeed` (event stream to a DM) and `WatchTradeDisplay` (self-updating snapshot), plus `UnwatchTradeFeed/Display` and `ListTradeWatchers`. Kai's epic thesis is that this read-only info belongs on **the website**, not Discord DMs, so this meets the watcher feature and **exceeds** it by making it host-agnostic. ## No-reset spine Watchers evaluate against the `CurrencyTrade` history the server **already exports** - each poll diffs the newest trades against stored watch queries. No new C# mod, no server reset. Persist watchers in a small SQLite store, mirroring the fair-price cache pattern (`~/.cache/eco-mcp-app/`). ## Scope The agent builds a **host-agnostic trade-watcher** subsystem: * **Subscribe** - watch an item, a store, a trader, or a price threshold (for example "iron ingot under 2.5"). MCP tool to create, list, and remove watchers, matching `WatchTradeFeed` / `ListTradeWatchers` / `UnwatchTradeFeed`. * **Match** - on each evaluation, surface trades matching the query since the watcher's last-seen mark (the feed semantic), plus the current matching state (the display semantic). * **Surfaces** - the watcher list and hits show in the SPA `/trade` route (sibling) and are queryable via MCP, so a URL-connected host like claude.ai can ask "what did my watchers catch." No Discord dependency. * **Persistence** - SQLite, keyed by watcher id, with the query, last-seen timestamp, and creation time. ## DiscordLink parity **Meets** `WatchTradeFeed`, `WatchTradeDisplay`, `UnwatchTradeFeed/Display`, and `ListTradeWatchers`. **Exceeds** by delivering to the website and MCP instead of Discord DMs, and by allowing a price-threshold predicate, not just a name match. ## Acceptance * MCP tools registered (create/list/remove/evaluate), `ward exec smoke` passes. * SQLite schema created idempotently, survives process restart. * Feed vs display semantics both covered (new-since-last-seen and current-state). * `respx`-mocked unit tests for match logic and the last-seen advance. * `ward exec test` / `ward exec lint` green. `docs/FEATURES.md` updated. ## Dependencies Foundation: [#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6). Parent: [#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37). Surfaced in the `/trade` SPA route (sibling).
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-52 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-05T03:27:44Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-eco-app-52` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-05T03:27:44Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh** — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - host-agnostic trade watchers (eco_trade_watchers MCP tool + SQLite store + /trades SPA sidebar), landed on main, closes #52.

This one went smoothly, mostly because the trades-ledger foundation (#6) had already done the hard part. Watchers are just a stored query diffed against ledger.trades each poll, so the whole subsystem is a pure match function plus a small SQLite store mirroring the fair-price cache. The feed-vs-display split fell out naturally: feed = matches newer than a last-seen mark, display = the current matching set, and the mark advance is the only stateful bit worth testing hard.

The one real design call was where the last-seen mark gets consumed. I made MCP evaluate advance it (the feed semantic) but the SPA /preview/watchers.json peek in advance=false mode, so browsing the website never eats a feed hit. That felt right but it's the part I'd want a second pair of eyes on.

What fought back was purely mechanical: main moved three times under me while I worked (stores #50, then market #49), so I resolved conflicts twice - once hand-resolving interleaved preview_*_json handlers and dispatch blocks in server.py/http_app.py, once a clean auto-merge. All green after each: 347 pytest, 38 vitest, smoke, ruff, mypy, precommit.

Confidence is high on the logic and persistence. Rough edges / follow-ups worth filing: (1) watchers evaluate against one server's ledger per call - the stored server field is metadata, not yet used to fan out per-server; (2) no SPA affordance to create/remove a watcher (MCP-only by design, but a small form could be nice); (3) the ledger is capped at the newest 4000 rows, so a long-quiet watcher's feed is bounded by that window, not truly unbounded.

WARD-OUTCOME: done - host-agnostic trade watchers (eco_trade_watchers MCP tool + SQLite store + /trades SPA sidebar), landed on main, closes #52. This one went smoothly, mostly because the trades-ledger foundation (#6) had already done the hard part. Watchers are just a stored query diffed against `ledger.trades` each poll, so the whole subsystem is a pure match function plus a small SQLite store mirroring the fair-price cache. The feed-vs-display split fell out naturally: feed = matches newer than a last-seen mark, display = the current matching set, and the mark advance is the only stateful bit worth testing hard. The one real design call was where the last-seen mark gets consumed. I made MCP `evaluate` advance it (the feed semantic) but the SPA `/preview/watchers.json` peek in `advance=false` mode, so browsing the website never eats a feed hit. That felt right but it's the part I'd want a second pair of eyes on. What fought back was purely mechanical: `main` moved three times under me while I worked (stores #50, then market #49), so I resolved conflicts twice - once hand-resolving interleaved `preview_*_json` handlers and dispatch blocks in server.py/http_app.py, once a clean auto-merge. All green after each: 347 pytest, 38 vitest, smoke, ruff, mypy, precommit. Confidence is high on the logic and persistence. Rough edges / follow-ups worth filing: (1) watchers evaluate against one server's ledger per call - the stored `server` field is metadata, not yet used to fan out per-server; (2) no SPA affordance to create/remove a watcher (MCP-only by design, but a small form could be nice); (3) the ledger is capped at the newest 4000 rows, so a long-quiet watcher's feed is bounded by that window, not truly unbounded.
Sign in to join this conversation.
No description provided.