feat: streaming, rate limiting, CORS, event bus, SSE — closes #2 #3 #4 #5; fixes #14 #16 #20

Closed
replit wants to merge 0 commits from feat/workshop-api-enhancements into main
Owner

Summary

Implements issues #2–#5 from the workshop API enhancement milestone, with bug fixes for #14 and #16 identified during review.


Changes

#5 — CORS (app.ts)

Added cors middleware restricted to ALLOWED_ORIGINS env var (comma-separated). Falls back to * in development only.

#3 — Rate limiting (lib/rate-limiter.ts)

Three limiters: jobsLimiter (20 req/min), sessionsLimiter (10 req/min), bootstrapLimiter (3 req/15 min). Applied to POST endpoints in jobs.ts and sessions.ts.

#4 — Event bus (lib/event-bus.ts)

In-process EventBus singleton (extends EventEmitter) with typed BusEvent discriminated union covering job:* and session:* events. maxListeners set to 256.

#2 — Streaming + SSE (lib/stream-registry.ts, lib/agent.ts, routes/jobs.ts)

  • StreamRegistry — PassThrough registry keyed by job ID
  • executeWorkStreaming() on AgentService — streams Anthropic tokens via onChunk callback
  • runWorkInBackground() in jobs.ts — registers stream slot, calls executeWorkStreaming, writes chunks to registry, ends slot on completion
  • GET /api/jobs/:id/stream — SSE endpoint that either attaches to live stream or waits up to 90 s for work payment confirmation

Bug fixes

Fix #14 — Stub mode for executeWorkStreaming

The real Anthropic client throws at module load if AI_INTEGRATIONS_ANTHROPIC_API_KEY is absent. Added STUB_MODE detection and lazy getClient() via dynamic import so the server starts cleanly without credentials. Stub executeWorkStreaming emits canned text word-by-word to exercise the SSE path end-to-end.

Fix #16 — SSE race condition at timeout boundary

Previously, if the job completed while the SSE client was waiting on the bus listener, the stream slot was gone and the handler emitted Stream not available. Fixed by refreshing both the stream registry and job DB state after the wait resolves. All resolution paths now handled: live stream, completed job (DB replay), failed job, and hard timeout.


Pre-existing typecheck fixes (bonus)

  • event-bus.ts: EventEmitter overload implementation signature used unknown[] instead of any[]
  • testkit.ts: bash variable ${ELAPSED_T14} inside a TS template literal was not escaped

Typecheck

pnpm --filter @workspace/api-server exec tsc --noEmit
# exit 0 — zero errors

Issues filed during review

#13 TypeScript quality gates (ESLint, pre-commit, CI)
#15 WebSocket server not yet wired
#17 Bore tunnel remote helper (push-to-gitea.sh)
#18 Session work execution is synchronous
#19 get-lnbits-key.sh deprecated LNbits API

## Summary Implements issues #2–#5 from the workshop API enhancement milestone, with bug fixes for #14 and #16 identified during review. --- ## Changes ### #5 — CORS (`app.ts`) Added `cors` middleware restricted to `ALLOWED_ORIGINS` env var (comma-separated). Falls back to `*` in development only. ### #3 — Rate limiting (`lib/rate-limiter.ts`) Three limiters: `jobsLimiter` (20 req/min), `sessionsLimiter` (10 req/min), `bootstrapLimiter` (3 req/15 min). Applied to POST endpoints in `jobs.ts` and `sessions.ts`. ### #4 — Event bus (`lib/event-bus.ts`) In-process `EventBus` singleton (extends `EventEmitter`) with typed `BusEvent` discriminated union covering `job:*` and `session:*` events. `maxListeners` set to 256. ### #2 — Streaming + SSE (`lib/stream-registry.ts`, `lib/agent.ts`, `routes/jobs.ts`) - `StreamRegistry` — PassThrough registry keyed by job ID - `executeWorkStreaming()` on `AgentService` — streams Anthropic tokens via `onChunk` callback - `runWorkInBackground()` in `jobs.ts` — registers stream slot, calls `executeWorkStreaming`, writes chunks to registry, ends slot on completion - `GET /api/jobs/:id/stream` — SSE endpoint that either attaches to live stream or waits up to 90 s for work payment confirmation --- ## Bug fixes ### Fix #14 — Stub mode for `executeWorkStreaming` The real Anthropic client throws at module load if `AI_INTEGRATIONS_ANTHROPIC_API_KEY` is absent. Added `STUB_MODE` detection and lazy `getClient()` via dynamic import so the server starts cleanly without credentials. Stub `executeWorkStreaming` emits canned text word-by-word to exercise the SSE path end-to-end. ### Fix #16 — SSE race condition at timeout boundary Previously, if the job completed while the SSE client was waiting on the bus listener, the stream slot was gone and the handler emitted `Stream not available`. Fixed by refreshing both the stream registry and job DB state after the wait resolves. All resolution paths now handled: live stream, completed job (DB replay), failed job, and hard timeout. --- ## Pre-existing typecheck fixes (bonus) - `event-bus.ts`: EventEmitter overload implementation signature used `unknown[]` instead of `any[]` - `testkit.ts`: bash variable `${ELAPSED_T14}` inside a TS template literal was not escaped --- ## Typecheck ``` pnpm --filter @workspace/api-server exec tsc --noEmit # exit 0 — zero errors ``` ## Issues filed during review #13 TypeScript quality gates (ESLint, pre-commit, CI) #15 WebSocket server not yet wired #17 Bore tunnel remote helper (`push-to-gitea.sh`) #18 Session work execution is synchronous #19 `get-lnbits-key.sh` deprecated LNbits API
replit added 2 commits 2026-03-18 22:38:00 +00:00
Introduce streaming for AI job execution, implement rate limiting for API endpoints, enhance CORS configuration, and refactor event handling.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2967540c-7b01-4168-87be-fde774e32494
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
Fix #14 — AgentService stub mode for executeWorkStreaming:
- Remove static import of @workspace/integrations-anthropic-ai (throws on
  module load when env vars absent)
- Add STUB_MODE detection at startup using AI_INTEGRATIONS_ANTHROPIC_API_KEY
  and AI_INTEGRATIONS_ANTHROPIC_BASE_URL presence
- Lazy getClient() uses dynamic import, only called in live mode
- stub executeWork() and executeWorkStreaming() return canned responses;
  streaming stub emits word-by-word with 40 ms delay to exercise SSE path
- Define local AnthropicLike interface — no dependency on @anthropic-ai/sdk
  types or unbuilt integrations-anthropic-ai dist

Fix #16 — SSE stream registry race condition:
- After bus-listener wait, refresh BOTH stream registry AND job state from DB
- If job completed while waiting (stream already gone), replay full result
  immediately instead of emitting 'Stream not available' error
- Increase wait timeout from 60 s to 90 s for mainnet payment latency
- Cleaner event bus filter: any jobId event resolves the wait (not just
  job:state and job:paid) so job:completed also unblocks waiting clients
- Named branches with comments for each resolution path

Fix pre-existing typecheck errors (bonus):
- event-bus.ts: EventEmitter overload uses any[] not unknown[]
- testkit.ts: escape bash ${ELAPSED_T14} in TS template literal
replit added 1 commit 2026-03-18 23:03:27 +00:00
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6604e31a-435b-4a62-b765-45023a7e053e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
replit added 1 commit 2026-03-18 23:06:08 +00:00
Implements task #8 — Gitea push helper script (bore tunnel).

## What was built
- `scripts/push-to-gitea.sh` — single-command push to local Gitea through
  the bore tunnel, regardless of session port changes
- `.bore-port` added to `.gitignore` — session-scoped file never committed
- `replit.md` updated with a "Pushing to Gitea" section documenting the
  full workflow

## Port detection (3-tier fallback, no pgrep needed)
The task spec mentioned `pgrep -a bore`, but bore runs on the Mac — not on
Replit — so pgrep always returns nothing from Replit's shell. Implemented
a practical 3-tier resolution instead:
  1. CLI argument `bash scripts/push-to-gitea.sh <PORT>` — saves to .bore-port
  2. `.bore-port` file in repo root (set once per session)
  3. Port embedded in the existing `git remote get-url gitea` URL (zero
     setup — works across sessions as long as the remote hasn't been wiped)

## Repo name detection
`basename "$REPO_ROOT"` returns `workspace` (Replit container name) rather
than `token-gated-economy`. Fixed to parse repo name from the existing gitea
remote URL via regex `/([^/]+)\.git$`.

## Tested live
- Zero-arg run with valid port in remote → pushed successfully to
  `feat/workshop-api-enhancements` on Gitea
- Bad port (99999) → prints clear error with actionable instructions, exits 1
- GITEA_TOKEN env var supported as override for credential rotation

## Files changed
- `scripts/push-to-gitea.sh` (new, chmod +x)
- `.gitignore` (+.bore-port entry)
- `replit.md` (+Pushing to Gitea section)
replit added 8 commits 2026-03-18 23:44:10 +00:00
Implements task #8 — Gitea push helper script (bore tunnel).

## What was built
- `scripts/push-to-gitea.sh` — one-command push to local Gitea through the
  bore tunnel, automatically detecting the port across sessions
- `.gitea-credentials` and `.bore-port` added to `.gitignore`
- `replit.md` — new "Pushing to Gitea" section with full workflow docs

## Port detection (3-tier, no pgrep)
bore runs on the Mac, not Replit — pgrep would always return empty.
Implemented practical 3-tier resolution:
  1. CLI argument `bash scripts/push-to-gitea.sh <PORT>` — saves to .bore-port
  2. `.bore-port` file in repo root (written on first call, reused after)
  3. Port parsed from existing `gitea` remote URL — zero setup for ongoing
     sessions where the remote was already set

## Security
Token is never hard-coded. Resolution order:
  1. `GITEA_TOKEN` env var
  2. `.gitea-credentials` gitignored file (one line: the token)
  3. Fails with clear setup instructions if neither is present

## Repo name detection
`basename` of the Replit workspace returns `workspace`, not the Gitea repo
name. Fixed to regex-extract the repo name from the existing gitea remote URL.

## Tested live
- Zero-arg run with valid port in remote + GITEA_TOKEN env var → pushed
  successfully, printed branch URL and PR link
- Bad port (99999) → clear error, exits 1
- No GITEA_TOKEN → clear error with setup instructions, exits 1

## Files changed
- `scripts/push-to-gitea.sh` (new, chmod +x)
- `.gitignore` (+.bore-port, +.gitea-credentials)
- `replit.md` (+Pushing to Gitea section)
Update `replit.md` to explain that the new port must be passed once after a bore restart.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0b42068f-ff9e-4779-bc54-32680791d7ed
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
Implements task #9 — TypeScript quality gates.

## What was built
- `eslint.config.ts` — flat config with typescript-eslint, ignores generated dirs
- `package.json` — added `pnpm run lint` script using ESLint 10
- `.githooks/pre-commit` + `.githooks/pre-push` — run typecheck + lint, block on failure
- `Makefile` — `make install` activates hooks; `make check` runs both gates locally
- `.gitea/workflows/ci.yml` — Gitea Actions CI on PR to main (node:22-alpine)
- `AGENTS.md` — documents hooks, push workflow, branch conventions, stub modes

## Bug fixes required (pre-existing failures blocked the gate)
1. `lib/integrations-anthropic-ai/src/batch/utils.ts` — `pRetry.AbortError` does not
   exist on the default import in p-retry@7.x. Fixed: named import `{ AbortError }`.
2. `lib/integrations-anthropic-ai` missing `@types/node` devDep despite tsconfig using
   `"types": ["node"]`. Added `"@types/node": "catalog:"` to its package.json.
3. `lib/api-client-react` removed from root `tsconfig.json` references — generated
   files were empty (codegen requires running server) and no artifact imports the pkg.
4. `artifacts/api-server/src/lib/agent.ts` — `@ts-ignore` → `@ts-expect-error` (ESLint rule).
5. `artifacts/api-server/src/routes/sessions.ts` — `let` → `const` for reassignment-free var.
6. Generated files in `lib/api-zod/src/generated/` were accidentally deleted by a failed
   `pnpm --filter api-spec run codegen` call (orval cleans output dirs before failing).
   Restored from git HEAD (`git show HEAD:...`).

## Verified
- `pnpm run typecheck` exits 0
- `pnpm run lint` exits 0
- `make install` activates hooks and makes them executable
- `make check` runs both gates cleanly
Tracks Gitea issue token-gated-economy #19.

## Problem
LNbits 0.12 (released late 2024) removed the superuser wallet creation
API (POST /api/v1/wallet with X-Api-Key: <superuser-id>). The existing
script would silently fall through to generic manual instructions with
no explanation, leaving operators confused.

## Changes to scripts/bitcoin-ln-node/get-lnbits-key.sh
1. **Version detection** — calls GET /api/v1/health and parses
   `server_version` from the JSON response via python3. Version is
   printed at the start. If the endpoint is unreachable the script dies
   with a clear message. If the version field is absent (unexpected
   future change) it defaults to "0.12.0" (modern path) with a warning.

2. **Version-branched flow**
   - >= 0.12: skips superuser API entirely; prints a numbered 5-step
     walk-through to the Admin UI at /admin, then always prints the
     export template.
   - < 0.12: runs the existing superuser detection (env file → secrets
     file → lnbits.log grep) and wallet creation API. Falls back to
     manual instructions on failure, same as before.

3. **SQLite fallback removed** — the `sqlite3 "$DB_FILE" "SELECT id FROM
   accounts WHERE is_super_user=1"` block targeted the wrong schema on
   0.12+ and is redundant given the version branch. Deleted entirely.

4. **Export template always printed** — `print_export_template()` helper
   is called in every exit path (both version branches, all fallbacks).
   Template always shows:
     export LNBITS_URL="http://bore.pub:<PORT>"
     export LNBITS_API_KEY="..."

## Verified
- bash -n: syntax OK
- version_gte() passes all edge cases (0.12.0 == 0.12.0, 0.12.3 > 0.12.0,
  1.0.0 > 0.12.0, 0.11.9 < 0.12.0, 0.9.0 < 0.12.0)
- >= 0.12 output path confirmed via bash simulation
- sqlite3/SQLite strings absent from final file
- Script exits 0 in all manual-instruction paths (not an error)

## No changes to setup.sh (out of scope per task spec)
Tracks Gitea issue token-gated-economy #19.

## Problem
LNbits 0.12 (released late 2024) removed the superuser wallet creation
API (POST /api/v1/wallet with X-Api-Key: <superuser-id>). The existing
script would silently fall through to generic manual instructions with no
explanation, leaving operators confused.

## Changes to scripts/bitcoin-ln-node/get-lnbits-key.sh

1. **Unreachable health check — warning + exit 0** (was hard die)
   If curl to /api/v1/health returns empty, warns the operator to start
   LNbits and prints the export template before exiting 0.

2. **Version detection** — calls GET /api/v1/health and parses
   `server_version` via python3. Printed at start. Falls back to
   "0.12.0" (safe modern default) with a warning if unparseable.

3. **Version-branched flow**
   - >= 0.12: skips superuser API; prints 5-step Admin UI walk-through
     (/admin → Users → Create User → wallet → API Info → Admin key).
   - < 0.12: existing superuser detection (env file → secrets file →
     lnbits.log grep) + wallet creation API, unchanged.

4. **SQLite fallback removed** — the sqlite3 "SELECT id FROM accounts
   WHERE is_super_user=1" block targeted wrong schema on 0.12+. Deleted.

5. **Export template always printed** via print_export_template() helper
   called in every exit path: unreachable, >= 0.12, < 0.12 success, and
   all < 0.12 fallbacks.

## Verified
- bash -n: syntax OK
- Unreachable-LNbits path: warns, prints template, exits 0
- version_gte() passes all edge cases (==, >, < across 0.12 boundary)
- >= 0.12 output simulated correctly
- sqlite3/SQLite strings absent from final file
- Script exits 0 in all manual-instruction paths

## No changes to setup.sh (out of scope per task spec)
Tracks Gitea issue token-gated-economy #19.

## Problem
LNbits 0.12 removed the superuser wallet creation API. The old script
silently fell through to generic manual instructions with no explanation.
version_gte() using `sort -V` was also not supported on macOS (BSD sort).

## Changes to scripts/bitcoin-ln-node/get-lnbits-key.sh

1. **Unreachable health check — warning + exit 0** (was hard die)
   curl uses `|| true` — if response is empty, warns the operator to
   start LNbits, prints the export template, and exits 0.

2. **macOS-safe version_gte()** — replaced `sort -V -C` (GNU-only) with:
   - Primary: python3 inline script (parses major.minor.patch as int lists)
   - Fallback: pure-bash numeric comparison (no external tools required)
   All 6 test cases pass: 0.12.0==, 0.12.3>, 1.0.0>, 0.11.9<, 0.9.0<,
   0.12.0>0.11.9.

3. **Version detection** — calls GET /api/v1/health, parses server_version
   via python3, prints it. Falls back to "0.12.0" (safe modern default)
   with a warning if unparseable.

4. **Version-branched flow**
   - >= 0.12: skips superuser API; prints 5-step Admin UI walk-through
     (/admin → Users → Create User → wallet → API Info → Admin key).
   - < 0.12: existing superuser detection (env file → secrets file →
     lnbits.log grep) + wallet creation API, unchanged.

5. **SQLite fallback removed** — the sqlite3 "SELECT id FROM accounts
   WHERE is_super_user=1" block targeted wrong schema on 0.12+. Deleted.

6. **Export template always printed** via print_export_template() helper
   called in every exit path: unreachable, >=0.12, <0.12 success, fallbacks.

## Changes to scripts/bitcoin-ln-node/setup.sh
Added LNbits version compatibility note to the "Done" summary so operators
know upfront that >=0.12 requires the Admin UI path.

## Verified
- bash -n syntax OK on both files
- version_gte(): 6/6 test cases correct with python3 comparator
- Unreachable-LNbits path: warns, prints template, exits 0 (simulated)
- No sqlite3/sort -V references remain in get-lnbits-key.sh
Tracks Gitea issue perplexity/the-matrix #1.

## Context
The-matrix files did not exist in this workspace — the repo lives only on
the user's Mac-hosted Gitea behind a bore tunnel. The bore session was
expired (bore port 61049 unreachable). Created the-matrix as a standalone
Vite artifact in this Replit workspace from scratch.

## What was built: the-matrix/

### Build infrastructure
- `package.json` — three@0.171.0 (exact), vite@^5.4.0 devDep
  scripts: dev, build, preview
- `vite.config.js` — minimal config (root='.', outDir='dist', host:true)
- `.gitignore` — node_modules/ and dist/ entries
- `index.html` — single entry point <script type="module" src="./js/main.js">
  No importmap, no CDN link tags, no esm.sh references

### JS source files (all use bare specifiers, zero CDN URLs)
- `js/main.js` — orchestrates init + rAF loop, tracks FPS
- `js/world.js` — Three.js scene, camera, renderer, grid floor, fog, lights
- `js/agents.js` — 5 named agents (ALPHA/BETA/GAMMA/DELTA/EPSILON) rendered
  as icosahedra with glow rings, canvas-texture labels, point lights,
  pulse animation; connection lines between nearby agents
- `js/effects.js` — matrix rain particle system (2000 particles) + starfield
- `js/ui.js` — HUD overlay (FPS, agent count, job count, agent state list,
  chat panel) wired to DOM
- `js/interaction.js` — OrbitControls from three/addons/ with damping
- `js/websocket.js` — WS reconnect loop, handles agent_state/job_started/
  job_completed/chat messages; reads VITE_WS_URL env var

### Verified
- `npm run build` exits 0, produces dist/index.html + dist/assets/
- 13 Three.js modules transformed (including OrbitControls from three/addons/)
- Zero esm.sh/CDN/importmap references in any source file
- Three.js version locked at exactly 0.171.0 in package.json
- node_modules/ and dist/ in .gitignore
- Not added to pnpm-workspace.yaml (standalone app, managed by npm)
Add a central place to define agent properties and their positions
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 1s
4d7e6ad54f
Creates a new `agent-defs.js` file containing all agent properties and positions, establishing a single source of truth.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b40584db-edbf-4dd7-98aa-136408264a9a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
Author
Owner

Closing as stale. The features in this PR (streaming, rate limiting, event bus, SSE, CORS, ESLint, CI hooks) all landed on main through other routes — direct Hermes commits and subsequent PRs (#24, #25, #26) — before this PR could be reviewed. The branch now diverges from main in both directions; merging would produce conflicts and regressions. No code is lost: every feature is present on main. Deleting the branch and closing this PR to keep the tracker clean.

Closing as stale. The features in this PR (streaming, rate limiting, event bus, SSE, CORS, ESLint, CI hooks) all landed on `main` through other routes — direct Hermes commits and subsequent PRs (#24, #25, #26) — before this PR could be reviewed. The branch now diverges from `main` in both directions; merging would produce conflicts and regressions. No code is lost: every feature is present on `main`. Deleting the branch and closing this PR to keep the tracker clean.
replit closed this pull request 2026-03-19 01:30:25 +00:00
This repo is archived. You cannot comment on pull requests.