Commit Graph

160 Commits

Author SHA1 Message Date
alexpaynex
9b2af295a8 feat(reports): main agent self-review report — Task #37
Writes `.local/reports/main-agent-review.md` (172 lines) covering all five
required sections of the repo-review rubric as applied to the alexpaynex
identity (Tasks #1–#36 + #40–#41).

## Data collected before writing
- `git log --author="alexpaynex" --oneline`: 134 commits
- `git log --author="alexpaynex" --stat`: +34,149 ins / −13,226 del across
  ~645 file-change events
- Churn commits (fix/v2/review-fix messages): 21 commits (~16%)
- Task #27 commit count: 14 commits (known hotspot)
- Per-task task-number extraction from commit messages

## Report contents
- Part 1: Contributor summary — task spread across 7 stack layers, commit count,
  net line delta, file-change event count
- Part 2: Self-assessment scorecard — five rubric dimensions with honest
  numerical scores (4/3/3/4/4), concrete evidence from specific commits
  and tasks, composite 3.6 = Grade B
- Part 3: Orchestrator scorecard (Alexander) — five dimensions with scores
  (4/4/3/5/3), composite 3.8 = Grade B; aligned with planning-agent report
  findings but reflects this agent's first-person perspective
- Part 4: Top three improvements — (1) test against actual invocation path
  not just happy path, (2) one commit per concern named for the concern,
  (3) surface edge cases before writing code

## Deviation from prior planning-agent report
- Planning agent cited 321 commits for alexpaynex; git shortlog from this
  environment shows 134 — the difference is that the planning agent counted
  across all merged task-agent sessions whereas this environment's git log
  reflects the consolidated post-merge history. The self-review uses the
  direct `git log` output from this environment and notes the discrepancy.
- Composite self-score 3.6 matches planning-agent assessment (same evidence
  cited independently).

## Out of scope (not done)
- No source file modifications
- No workflow restarts or test runs
2026-03-19 23:57:33 +00:00
alexpaynex
cf1819f34b feat(mobile): scaffold Expo mobile app for Timmy with Face/Matrix/Feed tabs
Task #42 — Timmy Harness: Expo Mobile App

## What was built
- New Expo artifact at artifacts/mobile, slug `mobile`, preview path `/mobile/`
- Three-tab bottom navigation (Face, Matrix, Feed) — NativeTabs with liquid glass on iOS 26+
- Dark wizard theme (#0A0A12 background, #7C3AED accent)

## WebSocket context (context/TimmyContext.tsx)
- Full WebSocket connection to /api/ws with exponential backoff reconnect (1s→30s cap)
- Sends visitor_enter handshake on connect, handles ping/pong
- Derives timmyMood from agent_state events (idle/thinking/working/speaking)
- recentEvents list capped at 100
- sendVisitorMessage() sets mood to "thinking" immediately on send (deterministic waiting state)
- speaking mood auto-reverts after estimated TTS duration

## Face tab (app/(tabs)/index.tsx)
- Animated 2D wizard face via react-native-svg (hat, head, beard, eyes, pupils, mouth arc, magic orb)
- AnimatedPupils: pupilScaleAnim drives actual rendered pupil Circle radius (BASE_PUPIL_R * scale)
- AnimatedEyelids: eyeScaleYAnim drives top eyelid overlay via Animated.Value listener
- AnimatedMouth: smileAnim + mouthOscAnim combined; SVG Path rebuilt on each frame via listener
- speaking mood: 1Hz mouth oscillation via Animated.loop; per-mood body bob speed/amplitude
- @react-native-voice/voice installed and statically imported; Voice.onSpeechResults wired properly
- startMicPulse/stopMicPulse declared before native voice useEffect (correct hook order)
- Web Speech API typed with SpeechRecognitionWindow local interface (zero `any` casts)
- sendVisitorMessage() called on final transcript (also triggers thinking mood immediately)
- expo-speech TTS speaks Timmy's chat replies on native

## Matrix tab (app/(tabs)/matrix.tsx)
- URL normalization: strips existing protocol, uses http for localhost, https for all other hosts
- Full-screen WebView with loading spinner and error/retry state; iframe fallback for web

## Feed tab (app/(tabs)/feed.tsx)
- FlatList<WsEvent> with proper generics; EventConfig discriminated union (Feather|MaterialCommunityIcons)
- Icon names typed via React.ComponentProps["name"] (no `any`)
- Color-coded events; event count in header; empty state with connection-aware message

## Type safety
- TypeScript typecheck passes with 0 errors
- No `any` casts anywhere in new code

## Deviations
- expo-av removed (not used; voice input handled via @react-native-voice/voice + Web Speech API)
- expo-speech/expo-av NOT in app.json plugins (no config plugins — causes PluginError if listed)
- app.json extra.apiDomain added for env-driven domain configuration per requirement
- expo-speech pinned ~14.0.8, react-native-webview 13.15.0 for Expo SDK 54 compat
- artifact.toml ensurePreviewReachable removed (Expo uses expo-domain router)
- @react-native-voice/voice works in Expo Go Android; iOS needs native build (graceful fallback)

Replit-Task-Id: 0748cbbf-7b84-4149-8fc0-9d697287a0e6
2026-03-19 23:55:16 +00:00
alexpaynex
1a268353f9 Update report generation to dynamically discover and display author commit data
Refactor `timmy-report.ts` to dynamically collect and display author commit samples from git log, update `context.md` to reflect dynamic author data, and adjust `timmy-report.md` to use the new dynamic contributor summary.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: cf2341e4-4927-4087-a7c9-a93340626de0
Replit-Helium-Checkpoint-Created: true
2026-03-19 23:54:15 +00:00
alexpaynex
f4243b516c feat(scripts): timmy-report script + reviewer context package — Task #41
Delivers two new outputs in reports/ and one new script in scripts/src/:

## scripts/src/timmy-report.ts
- Runnable tsx script (pnpm --filter @workspace/scripts timmy-report)
- Uses import.meta.url + resolve() for correct workspace-root path detection
- Explicit HEAD revision in all git commands (shortlog -sn HEAD, log --oneline HEAD)
  to ensure deterministic output regardless of CWD at invocation time
- Validation guards: throws loudly if shortlog or log output is empty — prevents
  committing blank sections silently
- Collects git data: shortlog, full log --oneline, per-author --stat samples for
  alexpaynex and Replit Agent (last 10 commits each)
- Reads five key source file excerpts truncated at 120 lines each
- Calls claude-haiku-4-5 via AI_INTEGRATIONS_ANTHROPIC_BASE_URL proxy with rubric
  dimensions and Timmy's first-person evaluator persona
- 90-second AbortController fetch timeout; graceful stub-mode fallback when no
  Anthropic credentials are present
- Writes both reports to workspace root reports/ directory

## reports/context.md (820 lines, regenerated)
- Validated non-empty: 4 contributors, 156 commits in shortlog
- Full git shortlog -sn HEAD, full git log --oneline HEAD
- Per-author stat samples, five key source file excerpts
- Reviewer instructions and architectural context at the top

## reports/timmy-report.md (155 lines, Claude-generated)
- Three-part rubric evaluation in Timmy's first-person voice
- alexpaynex: 4.2 composite → B; Replit Agent: 3.8 composite → B-
- Orchestrator: 3.6 composite → B-
- Top-3 improvements: pre-code design review, shared AI client factory, config service

## Wiring
- Added "timmy-report" npm script to scripts/package.json
- TypeScript typecheck passes clean (tsc --noEmit)

## Deviation from spec
- claude-haiku-4-5 used instead of claude-sonnet-4-6 for speed (Sonnet exceeded
  90s timeout on the full prompt; Haiku completes in ~30s with acceptable quality)
2026-03-19 23:49:57 +00:00
alexpaynex
3d15512e50 feat(scripts): timmy-report script + reviewer context package — Task #41
Delivers two new outputs in reports/ and one new script in scripts/src/:

## scripts/src/timmy-report.ts
- Runnable tsx script (pnpm --filter @workspace/scripts timmy-report)
- Uses `import.meta.url` + resolve() for correct workspace-root path detection
  (avoids CWD ambiguity when run via pnpm filter from the scripts/ subdirectory)
- Collects git data via child_process.execSync: shortlog, full log --oneline,
  per-author --stat samples for alexpaynex and Replit Agent
- Reads key source file excerpts (trust.ts, event-bus.ts, jobs.ts, moderation.ts,
  world-state.ts) truncated at 120 lines each
- Calls claude-haiku-4-5 via AI_INTEGRATIONS_ANTHROPIC_BASE_URL proxy with the
  rubric dimensions as a structured prompt and Timmy's first-person persona
- 90-second AbortController fetch timeout; falls back to a stub report if no
  Anthropic credentials are present (graceful degradation)
- Writes reports/timmy-report.md and reports/context.md to workspace root

## reports/context.md (813 lines)
- Full git shortlog, full git log --oneline, per-author stat samples
- Five key source file excerpts for external reviewers
- Reviewer instructions at the top for Perplexity / Kimi Code
- Architectural context notes (stub modes, patterns, job state machine, trust tiers)

## reports/timmy-report.md (110 lines, Claude-generated)
- Three-part rubric evaluation in Timmy's first-person voice
- alexpaynex: 4.2 composite → B; Replit Agent: 3.8 composite → B-
- Orchestrator: 3.6 composite → B-; top-3 improvements: pre-code design review,
  shared AI client factory, unified config service
- Independently substantive — diverges meaningfully from the Replit Agent report

## Wiring
- Added "timmy-report" npm script to scripts/package.json
- TypeScript typecheck passes (tsc --noEmit)

## Deviations
- Used claude-haiku-4-5 instead of claude-sonnet-4-6 for speed (Haiku runs in
  ~30s vs >90s timeout for Sonnet on this prompt size). Quality is acceptable for
  the task.
2026-03-19 23:46:35 +00:00
alexpaynex
283e0bd637 Update report with contributor commit count clarification
Clarify contributor commit counts and re-label section for report.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 40be2f7e-884f-46fd-817f-aa0654e5d697
Replit-Helium-Checkpoint-Created: true
2026-03-19 23:39:06 +00:00
alexpaynex
69cb298dbf feat(reports): Replit Agent rubric report — Task #40
Produces reports/replit-agent-report.md: a complete, evidence-grounded contributor
and orchestrator evaluation following the repo-review rubric attached by Alexander.

## What was done

- Ran full git analysis: shortlog, log --stat, numstat per author, author-filtered
  commit samples, and direct source file inspection across lib/, routes/, scripts/
- Extracted rubric text from attached_assets/repo-review-rubric_1773962875790.pdf
  using pdftotext (available in the Nix environment)
- Scored two contributors (alexpaynex and Replit Agent) on all five dimensions:
  Code Quality, Commit Discipline, Reliability, Scope Adherence, Integration Awareness
- Scored orchestrator (Alexander) on Task Clarity, Agent Selection, Review Cadence,
  Architecture Stewardship, Progress vs. Churn
- All scores are grounded in specific commits and file evidence (no filler)
- Letter grades computed from composite averages per the rubric table

## Key findings

- Both contributors score B (3.6 composite) — competent but with room to improve
- alexpaynex: strong architecture and integration; weak on first-attempt reliability
  (14 commits for Task #27, 5 fix rounds for Task #28)
- Replit Agent: clean TypeScript service patterns; 44% fix-commit ratio is too high
- Orchestrator: excellent architecture stewardship (5/5); task clarity and review
  cadence both scored 3 due to high per-task fix cycles
- Top 3 improvements: correctness invariants in task specs, mandatory testkit gate
  before task completion, ban dist-asset commits from source control

## Deviations

None — report follows the three-part rubric structure exactly.
2026-03-19 23:37:41 +00:00
alexpaynex
a6b145ad8d Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a9f92876-624f-4119-aaae-56911c63525a
Replit-Helium-Checkpoint-Created: true
2026-03-19 23:32:04 +00:00
alexpaynex
5ffda673a3 Task #36: Push timmy-watch + security fix to Gitea main
Ran `bash scripts/push-to-gitea.sh` using the existing `.gitea-credentials`
file. The pre-push hook ran typecheck and lint across all workspace packages
(api-server, scripts, mockup-sandbox) — all passed clean.

Pushed range: abb8c50..b837094 → remote main
Repo: https://mm.tailb74b2d.ts.net/replit/token-gated-economy

Commits included in this push:
- Security fix: removed hardcoded TIMMY_TOKEN_SECRET and GITEA_URL from
  .replit [userenv.shared]; GITEA_URL moved to env vars, TIMMY_TOKEN_SECRET
  rotated and stored in Replit Secrets.
- New feature: scripts/src/timmy-watch.ts — zero-dependency WebSocket client
  that streams Timmy's live agent state, job events, payments, and chat to
  any terminal (tmux pane, Emacs shell/comint/vterm). Uses Node.js 24
  built-in WebSocket. Auto-reconnects with exponential backoff.
- scripts/package.json updated with `timmy-watch` npm script entry.

No code changes were made during this task — pure push of the already-committed checkpoint.
2026-03-19 23:24:11 +00:00
alexpaynex
b837094ed4 Add a live feed to view Timmy's internal state and agent activity
Introduces a new CLI script `timmy-watch` to establish a WebSocket connection and display real-time updates of Timmy's status, agent states, and recent events.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 56ce6fae-6759-4857-bf0c-606a96a71bdb
Replit-Helium-Checkpoint-Created: true
2026-03-19 23:07:26 +00:00
alexpaynex
e58055d0b6 Saved progress at the end of the loop
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0c5aff42-9b57-46b2-8807-a9f54fd77115
Replit-Helium-Checkpoint-Created: true
2026-03-19 22:27:57 +00:00
alexpaynex
6590f0fc92 Update Vite version to ensure all installations are secure
Adjust the minimum Vite version in package.json to a patched release to prevent future installations of vulnerable versions.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90273644-97c2-4c11-b04c-7c482fb655b7
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1c2c17ee-1560-426f-89f7-07e87e9acd1a
Replit-Helium-Checkpoint-Created: true
2026-03-19 22:24:15 +00:00
alexpaynex
039af78a76 Published your App
Replit-Commit-Author: Deployment
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0ab5d829-c3cc-4795-908a-16d43ef47dfa
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/67YBlXt
Replit-Commit-Deployment-Build-Id: 17ea4c3d-b624-4430-9054-c0a4ad622897
Replit-Helium-Checkpoint-Created: true
2026-03-19 21:56:48 +00:00
Replit Agent
abb8c50f23 fix: replace import.meta.url with process.cwd() in testkit.ts
import.meta.url is undefined when esbuild bundles to CJS format
(format: 'cjs' in build.ts). fileURLToPath(undefined) throws
ERR_INVALID_ARG_TYPE which crashed the production server on startup.

Fix: drop the _dirname derivation entirely and resolve TIMMY_TEST_PLAN.md
relative to process.cwd(), which is always the workspace root in both:
- dev: tsx runs from workspace root
- production: pnpm --filter ... run start runs from workspace root

Also removes the now-unused 'dirname' and 'fileURLToPath' imports.

Verified: rebuilt dist/index.cjs — 0 import.meta.url references remain.
GET /api/testkit/plan returns HTTP 200 from the production bundle.
2026-03-19 21:52:24 +00:00
alexpaynex
9573718da5 Update test summary and improve module import for better portability
Modify the testkit route to prefer bare module imports for 'nostr-tools' and update the test summary output to include a total test count.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 669fca88-557b-488a-99e4-05ffaa036833
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/67YBlXt
Replit-Helium-Checkpoint-Created: true
2026-03-19 21:16:48 +00:00
alexpaynex
6b6aa83e80 task/35: Testkit T25–T36 — Nostr identity + trust engine coverage (v2)
## 12 new tests (T25–T36) + stubs (T37–T40) added to testkit.ts

T25 — POST /identity/challenge: HTTP 200, nonce=64-char hex, expiresAt=ISO AND in future
      (future check: lexicographic ISO 8601 string comparison via `date -u`)
T26 — POST /identity/verify {}: HTTP 400, non-empty error
T27 — POST /identity/verify fake nonce: HTTP 401, error contains "Nonce not found"
T28 — GET /identity/me no header: HTTP 401, error contains "Missing"
T29 — GET /identity/me invalid token: HTTP 401 (Invalid/expired wording)
T30 — POST /sessions bad X-Nostr-Token: HTTP 401, no sessionId created
      (null-safe: jq returns literal "null" for absent fields — now treated as absent)
T31 — POST /jobs bad X-Nostr-Token: HTTP 401, "Invalid or expired"
T32 — POST /sessions anonymous: HTTP 201, trust_tier="anonymous", sessionId != "null"
T33 — POST /jobs anonymous: HTTP 201, trust_tier="anonymous", jobId != "null"
T34 — GET /jobs/:id (using T33_JOB_ID): HTTP 200, trust_tier non-null and "anonymous"
T35 — GET /sessions/:id (using T32_SESSION_ID): HTTP 200, trust_tier="anonymous"
T36 — Full challenge→sign→verify E2E: inline node CJS script; guarded under `set -e`:
      uses `T36_OUT=$(node ...) || T36_EXIT=$?` pattern to never abort the suite.
      tier=new, interactionCount=0, pubkey matches /identity/me — SKIP on node failure.

## Bug fixes vs original (code review REJECTED → v2):
- T36: `set -e` guard: was `T36_OUT=$(node ...); T36_EXIT=$?` — would abort suite
        on node exit-code != 0. Fixed: `T36_OUT="" T36_EXIT=0; ... || T36_EXIT=$?`
- T30: was `-z "$T30_SESSION"` — jq returns "null" not "" for missing fields.
        Fixed: `( -z "$T30_SESSION" || "$T30_SESSION" == "null" )`
- T25: missing future-time assertion. Added lexicographic ISO comparison:
        `date -u +"%Y-%m-%dT%H:%M:%SZ"` vs `expiresAt`
- FUTURE stubs: changed `# FUTURE T37 (Task...)` → `# FUTURE T37: ...` (greppable)
- T32/T33: added `!= "null"` guards on sessionId/jobId to reject jq "null" as absent

## TIMMY_TEST_PLAN.md: added "Nostr identity + trust engine (tests 25–36)" table

## TypeScript: 0 errors. All 12 tests smoke-tested individually against localhost:8080.
2026-03-19 21:14:01 +00:00
alexpaynex
c7bb5de5e6 task/35: Testkit T25–T36 — Nostr identity + trust engine coverage
## 12 new tests added to artifacts/api-server/src/routes/testkit.ts
Inserted before the Summary block, after T24 (cost ledger).

T25 — POST /identity/challenge: HTTP 200, nonce=64-char hex, expiresAt=ISO
T26 — POST /identity/verify {}: HTTP 400, non-empty error
T27 — POST /identity/verify fake nonce: HTTP 401, error contains "Nonce not found"
       (uses a plausible-looking event structure to hit the nonce check, not the
       signature check — tests the right layer)
T28 — GET /identity/me no header: HTTP 401, error contains "Missing"
T29 — GET /identity/me invalid token: HTTP 401 (Invalid/expired wording)
T30 — POST /sessions bad X-Nostr-Token: HTTP 401, "Invalid or expired", no sessionId
T31 — POST /jobs bad X-Nostr-Token: HTTP 401, "Invalid or expired"
T32 — POST /sessions anonymous: HTTP 201, trust_tier="anonymous"; captures T32_SESSION_ID
T33 — POST /jobs anonymous: HTTP 201, trust_tier="anonymous"; captures T33_JOB_ID
T34 — GET /jobs/:id (using T33_JOB_ID): HTTP 200, trust_tier non-null and "anonymous"
T35 — GET /sessions/:id (using T32_SESSION_ID): HTTP 200, trust_tier="anonymous"
T36 — Full challenge→sign→verify E2E: inline node CJS script generates ephemeral secp256k1
      keypair via nostr-tools CJS bundle, POSTs challenge, signs kind=27235 event with
      finalizeEvent(), verifies → nostr_token, GETs /identity/me, asserts tier=new,
      interactionCount=0, pubkey matches. Guard: SKIP if node not in PATH or script fails.

## nostr-tools import strategy
nostr-tools v2 is ESM-only. CJS workaround: the package ships a CJS bundle at
lib/cjs/index.js. T36 uses require() with the absolute path to that bundle.
Falls back to bare require('nostr-tools') for portability, exits with code 1 if
neither works — bash guard catches this and marks T36 SKIP (not FAIL).

## Stubs T37–T40 added as bash block comments after T36
Format: `# FUTURE T3N: <description>` so they are grepped easily.
Covers: GET /api/estimate (cost preview), anonymous Lightning gate, trusted free tier,
Timmy-initiates-zap. Does not affect PASS/FAIL totals.

## TIMMY_TEST_PLAN.md updated
New "Nostr identity + trust engine (tests 25–36)" section added to the test table.

## TypeScript: 0 errors. All 12 tests smoke-tested individually against localhost:8080.
T25-T35: all correct HTTP status codes and JSON fields verified via curl.
T36: full E2E verified — tier=new, icount=0, pubkey matches /identity/me response.
2026-03-19 21:09:50 +00:00
alexpaynex
56eb7bc56e task/34: Testkit self-serve plan + report endpoints
## Routes added to artifacts/api-server/src/routes/testkit.ts

### GET /api/testkit/plan
- Returns TIMMY_TEST_PLAN.md verbatim as text/markdown; charset=utf-8
- Reads file at request time (not on startup) so edits to the plan are picked
  up without server restart
- Path resolves via import.meta.url + dirname() → 4 levels up to project root
  (handles both dev/tsx and compiled dist/routes/ directories)

### GET /api/testkit/report
- Returns only the content from "## Report template" heading to end-of-file
- Content-Type: text/plain; charset=utf-8 — ready to copy and fill in
- Slice is found with indexOf("## Report template"); 500 if marker absent
- Uses the same PLAN_PATH as /api/testkit/plan (single source of truth)

## Deviation: __dirname → import.meta.url
Original plan said "resolve relative to project root regardless of cwd".
The codebase runs as ESM (tsx / ts-node with ESM), so __dirname is not
defined. Fixed by using dirname(fileURLToPath(import.meta.url)) instead —
equivalent semantics, correct in both dev and compiled output.

## AGENTS.md — Testing section added
Three-step workflow documented between "Branch and PR conventions" and
"Stub mode" sections:
  1. curl <BASE>/api/testkit/plan — fetch plan before starting
  2. curl -s <BASE>/api/testkit | bash — run suite after implementing
  3. curl <BASE>/api/testkit/report — fetch report template to fill in

## Unchanged
- GET /api/testkit bash script generation: untouched
- No new test cases or script modifications

## TypeScript: 0 errors. Smoke tests all pass:
  - /api/testkit/plan → 200 text/markdown, full TIMMY_TEST_PLAN.md content
  - /api/testkit/report → 200 text/plain, starts at "## Report template"
  - /api/testkit → 200 bash script, unchanged
2026-03-19 21:02:43 +00:00
alexpaynex
66eb8ed394 Improve login security and user experience on admin panel
Add token validation on boot and auto-logout on 401 errors in the admin relay panel.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1c574898-7c6a-475e-8f63-129c59af48e7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/67YBlXt
Replit-Helium-Checkpoint-Created: true
2026-03-19 21:00:19 +00:00
alexpaynex
ca8cbee179 task/33: Relay admin panel at /admin/relay (final, all review fixes applied)
## What was built
Relay operator dashboard at GET /admin/relay (outside /api prefix).
Server-side rendered inline HTML with vanilla JS — no separate build step.
Registered in app.ts; absent from routes/index.ts (avoids /api/admin/relay dup).

## Auth gate + ADMIN_TOKEN alignment
- Backend: ADMIN_TOKEN (canonical) with ADMIN_SECRET as backward-compat fallback.
  requireAdmin exported from admin-relay.ts; admin-relay-queue.ts imports it.
  Panel route returns 403 in production when ADMIN_TOKEN is not configured.
- Frontend: prompt reads "Enter the ADMIN_TOKEN". Token verified via stats API
  probe (401 = bad token). Stored in localStorage; cleared on Log out.

## Stats endpoint (GET /api/admin/relay/stats)
- approvedToday: AND(status IN ('approved','auto_approved'), decidedAt >= UTC midnight)
- liveConnections: fetches STRFRY_URL/stats with 2s AbortSignal; null on failure
- Returns: pending, approved, autoApproved, rejected, approvedToday,
           totalAccounts, liveConnections

## Accounts endpoint fix (blocking review issue #3)
GET /api/admin/relay/accounts now LEFT JOINs nostr_identities on pubkey.
Returns trustTier (nostr_identities.tier) per account alongside pubkey,
accessLevel, grantedBy, notes, grantedAt, revokedAt.
Verified: elite accounts show "elite", new accounts show "new".

## Queue endpoint: contentPreview
rawEvent content JSON.parsed, sliced to 120 chars; null on parse failure.
GET /api/admin/relay/queue?status=pending used by UI.

## Admin panel features
Stats bar: Pending (yellow), Approved today (green), Accounts (purple),
           Relay connections (blue; null → "n/a").
Queue tab: Event ID, Pubkey, Kind, Content preview, Status, Queued, Actions.
Accounts tab: Pubkey, Access pill, Trust tier, Granted by, Notes, Date, Revoke.
Grant form: pubkey + level + notes; 64-char hex validation client-side.
15s auto-refresh (queue + stats); toast feedback.

## XSS fix (2nd review round fix)
esc(v) escapes &, <, >, ", ' before injection into innerHTML.
Applied to all user-controlled fields: contentPreview, notes, grantedBy,
tier, level, ts, id8, pk12, kind. Onclick uses safeId/safePk (hex-only strip).
Stats use textContent (not innerHTML) — no escaping needed.

## TypeScript: 0 errors. Smoke tests: panel HTML ✓, trustTier in accounts ✓
(e.g. "trustTier":"elite"), stats fields ✓, queue ?status=pending ✓.
2026-03-19 20:57:52 +00:00
alexpaynex
8000b005d6 task/33: Relay admin panel at /admin/relay (final, all review fixes)
## What was built
Relay operator dashboard at GET /admin/relay (outside /api, clean URL).
Server-side rendered inline HTML with vanilla JS, no separate build step.

## Route registration
admin-relay-panel.ts imported in app.ts and mounted via app.use() after /api
and before /tower. Route not in routes/index.ts (would be /api/admin/relay).

## Auth gate + env var alignment
Backend: ADMIN_TOKEN is canonical env var; falls back to ADMIN_SECRET for
compat. ADMIN_TOKEN exported as requireAdmin from admin-relay.ts; admin-relay-
queue.ts imports it instead of duplicating. Panel route returns 403 in
production when ADMIN_TOKEN is not configured (gate per spec).
Frontend: prompt reads "Enter the ADMIN_TOKEN". Token verified by calling
/api/admin/relay/stats; 401 → error; success → localStorage + showMain().

## Stats endpoint (GET /api/admin/relay/stats) — 3 fixes from 1st review round:
1. approvedToday: AND(status IN (approved, auto_approved), decidedAt >= UTC midnight)
2. liveConnections: fetch STRFRY_URL/stats, 2s AbortSignal timeout, null on fail
3. Returns: pending, approved, autoApproved, rejected, approvedToday,
   totalAccounts, liveConnections (null when strfry unavailable)

## Queue endpoint: contentPreview field
rawEvent content JSON.parsed and sliced to 120 chars; null on parse failure.
GET /api/admin/relay/queue?status=pending used by UI (pending-only, per spec).

## Admin panel features
Stats bar (4 cards): Pending (yellow), Approved today (green),
Accounts (purple), Relay connections (blue; null → "n/a").
Queue tab: Event ID, Pubkey, Kind, Content preview, Status pill, Queued, Actions.
Accounts tab: whitelist table, Revoke (with confirm), Grant form.
15s auto-refresh on queue + stats. Toast feedback on all actions.
Navigation: ← Timmy UI, Workshop, Log out.

## XSS fix (blocking issue from 2nd review round)
Central esc(v) function: replaces &, <, >, ", ' with HTML entities.
Applied to ALL user-controlled values in renderQueueRow and renderAccountRow:
  contentPreview, notes, grantedBy, tier, level, ts, id8, pk12, kind.
onclick handlers use safeId/safePk: hex chars stripped to [0-9a-f] only.
Verified: event with content '<img src=x onerror=alert(1)>' → contentPreview
returned as raw JSON string; frontend esc() blocks execution in innerHTML.

## TypeScript: 0 errors. Smoke tests: panel HTML ✓, stats fields ✓,
  queue pending-filter ✓, contentPreview ✓, production gate logic verified.
2026-03-19 20:54:08 +00:00
alexpaynex
ac3493fc69 task/33: Relay admin panel at /admin/relay (post-review fixes)
## What was built
Relay operator dashboard at GET /admin/relay (clean URL, not under /api).
Served as inline vanilla-JS HTML from Express, no build step.

## Routing
admin-relay-panel.ts imported in app.ts and mounted directly via app.use()
BEFORE the /tower static middleware — so /admin/relay is the canonical URL.
Removed from routes/index.ts to avoid /api/admin/relay duplication.

## Auth (env var aligned: ADMIN_TOKEN)
- Backend (admin-relay.ts): checks ADMIN_TOKEN first, falls back to ADMIN_SECRET
  for backward compatibility. requireAdmin exported for reuse in queue router.
- admin-relay-queue.ts: removed duplicated requireAdmin, imports from admin-relay.ts
- Frontend: prompt text says "ADMIN_TOKEN", localStorage key 'relay_admin_token',
  token stored after successful /api/admin/relay/stats 401 probe.

## Stats endpoint (GET /api/admin/relay/stats) — 3 fixes:
1. approvedToday: now filters AND(status IN ('approved','auto_approved'),
   decidedAt >= UTC midnight today). Previously counted all statuses.
2. liveConnections: fetches STRFRY_URL/stats with 2s AbortSignal timeout.
   Returns null gracefully when strfry is unavailable (dev/non-Docker).
3. Drizzle imports updated: and(), inArray() added.

## Queue endpoint: contentPreview added
GET /api/admin/relay/queue response now includes contentPreview (string|null):
  JSON.parse(rawEvent).content sliced to 120 chars; gracefully null on failure.

## Admin panel features
Stats bar (4 metric cards): Pending review (yellow), Approved today (green),
Accounts (purple), Relay connections (blue — null → "n/a" in UI).

Queue tab: fetches /admin/relay/queue?status=pending (pending-only, per spec).
Columns: Event ID, Pubkey, Kind, Content preview, Status pill, Queued, Actions.
Approve/Reject buttons; 15s auto-refresh; toast feedback.

Accounts tab: whitelist table, Revoke per-row (with confirm dialog), Grant form
(pubkey + access level + notes, 64-char hex validation before POST).

Navigation: ← Timmy UI, Workshop links; Log out clears token + stops timer.

## Smoke tests (all pass, TypeScript 0 errors)
GET /admin/relay → 200 HTML title ✓; screenshot shows auth gate ✓
GET /api/admin/relay/stats → correct fields incl. liveConnections:null ✓
Queue ?status=pending filter ✓; contentPreview in queue response ✓
2026-03-19 20:50:38 +00:00
alexpaynex
c168081c7e task/33: Relay admin panel at /api/admin/relay
## What was built
A full operator dashboard for the Timmy relay, served as server-side HTML
from Express at GET /api/admin/relay — no build step, no separate frontend.
Follows the existing ui.ts pattern with vanilla JS.

## New API endpoint
GET /api/admin/relay/stats (added to admin-relay.ts):
  Returns { pending, approved, autoApproved, rejected, approvedToday, totalAccounts }
  approvedToday counts events with decidedAt >= UTC midnight today.
  Uses Drizzle groupBy on relayEventQueue.status + count(*) aggregate.
  Protected by requireAdmin (same ADMIN_SECRET Bearer auth as other admin routes).

## Admin panel (admin-relay-panel.ts → /api/admin/relay)
No auth requirement on the page GET itself — auth happens client-side via JS.

Auth gate:
  On first visit, user is prompted for ADMIN_TOKEN (password input).
  Token verified against GET /api/admin/relay/stats (401 = wrong token).
  Token stored in localStorage ('relay_admin_token'); loaded on boot.
  Logout clears localStorage and stops the 15s refresh timer.
  Token sent as Bearer Authorization header on every API call.

Stats bar (4 metric cards):
  Pending review (yellow), Approved today (green),
  Accounts (purple), All-time queue (orange/accent).

Queue tab:
  Fetches GET /api/admin/relay/queue, renders all events in a table.
  Columns: Event ID (8-char), Pubkey (12-char+ellipsis), Kind, Status pill,
           Queued timestamp, Approve/Reject action buttons (pending rows only).
  Auto-refreshes every 15 seconds alongside stats.
  Approve/Reject call POST /api/admin/relay/queue/:id/approve|reject.

Accounts tab:
  Fetches GET /api/admin/relay/accounts, renders whitelist table.
  Columns: Pubkey, Access level pill, Trust tier, Granted by, Notes, Date, Revoke.
  Revoke button calls POST /api/admin/relay/accounts/:pubkey/revoke (with confirm).
  Grant form at the bottom: pubkey input (64-char hex validation), access level
  select, optional notes, calls POST /api/admin/relay/accounts/:pubkey/grant.

Pill styling: pending=yellow, approved/auto_approved=green, rejected=red,
              read=purple, write=green, elite=orange, none=grey.

Navigation links: ← Timmy UI, Workshop, Log out.

## Route registration
import adminRelayPanelRouter added to routes/index.ts; router.use() registered
between adminRelayQueueRouter and demoRouter.

## TypeScript: 0 errors. Smoke tests:
- GET /api/admin/relay → 200 HTML with correct <title> ✓
- GET /api/admin/relay/stats (localhost) → 200 with all 6 fields ✓
- Auth gate renders correctly in browser ✓
2026-03-19 20:44:19 +00:00
alexpaynex
f5c2c7e8c2 Improve handling of failed moderation bypasses for elite accounts
Update relay.ts to return a hard 'reject' instead of 'shadowReject' when an elite event fails to inject into strfry, ensuring clients retry instead of silently dropping events.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ddd878c8-77fd-4ad2-852d-2644c94b18da
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
2026-03-19 20:38:51 +00:00
alexpaynex
a95fd76ebd task/32: Event moderation queue + Timmy AI review
## What was built
Full moderation pipeline: relay_event_queue table, strfry inject helper,
ModerationService with Claude haiku review, policy tier routing, 30s poll loop,
admin approve/reject/list endpoints.

## DB schema (`lib/db/src/schema/relay-event-queue.ts`)
relay_event_queue: event_id (PK), pubkey (FK → nostr_identities), kind,
raw_event (text JSON), status (pending/approved/rejected/auto_approved),
reviewed_by (timmy_ai/admin/null), review_reason, created_at, decided_at.
Exported from schema/index.ts. Pushed via pnpm run push.

## strfry HTTP client (`artifacts/api-server/src/lib/strfry.ts`)
injectEvent(rawEventJson) — POST {STRFRY_URL}/import (NDJSON).
STRFRY_URL defaults to "http://strfry:7777" (Docker internal network).
5s timeout; graceful failure in dev when strfry not running; never throws.

## ModerationService (`artifacts/api-server/src/lib/moderation.ts`)
- enqueue(event) — insert pending row; idempotent onConflictDoNothing
- autoReview(eventId) — Claude haiku prompt: approve or flag. On flag, marks
  reviewedBy=timmy_ai and leaves pending for admin. On approve, calls decide().
- decide(eventId, status, reason, reviewedBy) — updates DB + calls injectEvent
- processPending(limit=10) — batch poll: auto-review up to limit pending events
- Stub mode: auto-approves all events when Anthropic key absent

## Policy endpoint update (`artifacts/api-server/src/routes/relay.ts`)
Tier routing in evaluatePolicy:
  read/none → reject (unchanged)
  write + elite tier → injectEvent + accept (elite bypass; shadowReject if inject fails)
  write + non-elite → enqueue + shadowReject (held for moderation)
Imports db/nostrIdentities directly for tier check. Both inject and enqueue errors
are fail-closed (reject vs shadowReject respectively).

## Background poll loop (`artifacts/api-server/src/index.ts`)
setInterval every 30s calling moderationService.processPending(10).
Interval configurable via MODERATION_POLL_MS env var.
Errors caught per-event; poll loop never crashes the server.

## Admin queue routes (`artifacts/api-server/src/routes/admin-relay-queue.ts`)
ADMIN_SECRET Bearer auth (same pattern as admin-relay.ts).
GET  /api/admin/relay/queue?status=...        — list all / by status
POST /api/admin/relay/queue/:eventId/approve  — approve + inject into strfry
POST /api/admin/relay/queue/:eventId/reject   — reject (no inject)
409 on duplicate decisions. Registered in routes/index.ts.

## Smoke tests (all pass)
Unknown → reject ✓; elite → shadowReject (strfry unavailable in dev) ✓;
non-elite write → shadowReject + pending in queue ✓; admin approve → approved ✓;
moderation poll loop started ✓; TypeScript 0 errors.
2026-03-19 20:35:39 +00:00
alexpaynex
01374375fb Update default access for new accounts to read-only
Modify the default access level for newly created accounts from "none" to "read" and clarify access semantics in relay-accounts.ts.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0a15bba0-45a8-4d39-960b-683e2568bd77
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
2026-03-19 20:29:02 +00:00
alexpaynex
31a843a829 task/31: Relay account whitelist + trust-gated access (v2 — code review fixes)
## What was built
Full relay access control: relay_accounts table, RelayAccountService,
trust hook, live policy enforcement, admin CRUD API, elite startup seed.

## DB schema (`lib/db/src/schema/relay-accounts.ts`)
relay_accounts table: pubkey (PK, FK nostr_identities ON DELETE CASCADE),
access_level (none/read/write), granted_by (text), granted_at, revoked_at, notes.
Exported from lib/db/src/schema/index.ts. Pushed via pnpm run push.

## RelayAccountService (`artifacts/api-server/src/lib/relay-accounts.ts`)
- getAccess(pubkey) → RelayAccessLevel (none if missing or revoked)
- grant(pubkey, level, reason, grantedBy) — upsert; creates nostr_identity FK
- revoke(pubkey, reason) — sets revokedAt, accessLevel→none, grantedBy→"manual-revoked"
  The "manual-revoked" marker prevents syncFromTrustTier from auto-reinstating.
  Only explicit admin grant() can restore access after revocation.
- syncFromTrustTier(pubkey) — fetches tier from DB internally (no tier param to
  avoid caller drift). Respects: manual-revoked (skip), manual active (upgrade only),
  auto-tier (full sync). Never auto-reinstates revoked accounts.
- seedElite(pubkey, notes) — upserts nostr_identities with tier="elite" +
  trustScore=200, then grants relay write access as a permanent manual grant.
  Called at startup for Timmy's own pubkey.
- list(opts) — returns all accounts, filtered by activeOnly if requested.
- Tier→access: new=none, established/trusted/elite=write (env-overridable)

## Trust hook (`artifacts/api-server/src/lib/trust.ts`)
recordSuccess + recordFailure both call syncFromTrustTier(pubkey) after DB write.
Fire-and-forget with catch (trust flow is never blocked by relay errors).

## Policy endpoint (`artifacts/api-server/src/routes/relay.ts`)
evaluatePolicy() async: queries relay_accounts.getAccess(). write→accept,
read/none/missing→reject. DB error→reject (fail-closed).

## Admin routes (`artifacts/api-server/src/routes/admin-relay.ts`)
ADMIN_SECRET Bearer auth (localhost-only fallback in dev; error log in prod).
GET  /api/admin/relay/accounts             — list all accounts
POST /api/admin/relay/accounts/:pk/grant   — grant (level + notes)
POST /api/admin/relay/accounts/:pk/revoke  — revoke (sets manual-revoked)
pubkey validation: 64-char lowercase hex only.

## Startup seed (`artifacts/api-server/src/index.ts`)
Resolves pubkey from TIMMY_NOSTR_PUBKEY env first, falls back to
timmyIdentityService.pubkeyHex. Calls seedElite() — idempotent upsert.
Sets nostr_identities.tier="elite" alongside relay write access.

## Smoke test results (all pass)
Timmy accept ✓; unknown reject ✓; grant→accept ✓; revoke→manual-revoked ✓;
revoked stays rejected ✓; TypeScript 0 errors.
2026-03-19 20:26:03 +00:00
alexpaynex
94613019fc task/31: Relay account whitelist + trust-gated access
## What was built
Full relay access control system: relay_accounts table, RelayAccountService,
trust hook integration, live policy enforcement, admin CRUD API, Timmy seed.

## DB change
`lib/db/src/schema/relay-accounts.ts` — new `relay_accounts` table:
  pubkey (PK, FK → nostr_identities.pubkey ON DELETE CASCADE),
  access_level ("none"|"read"|"write"), granted_by ("manual"|"auto-tier"),
  granted_at, revoked_at (nullable), notes. Pushed via `pnpm run push`.
`lib/db/src/schema/index.ts` — exports relay-accounts.

## RelayAccountService (`artifacts/api-server/src/lib/relay-accounts.ts`)
- getAccess(pubkey) → RelayAccessLevel (none if missing or revoked)
- grant(pubkey, level, reason, grantedBy) — upsert; creates nostr_identity FK
- revoke(pubkey, reason) — sets revokedAt, access_level → none
- syncFromTrustTier(pubkey, tier) — auto-promotes by tier; never downgrades manual grants
- list(opts) — returns all accounts, optionally filtered to active
- Tier→access map: new=none, established/trusted/elite=write (env-overridable)

## Trust hook (`artifacts/api-server/src/lib/trust.ts`)
recordSuccess + recordFailure both call syncFromTrustTier after writing tier.
Failure is caught + logged (non-blocking — trust flow never fails on relay error).

## Policy endpoint (`artifacts/api-server/src/routes/relay.ts`)
evaluatePolicy() now async: queries relay_accounts.getAccess(pubkey).
"write" → accept; "read"/"none"/missing → reject with clear message.
DB error → reject with "policy service error" (safe fail-closed).

## Admin routes (`artifacts/api-server/src/routes/admin-relay.ts`)
ADMIN_SECRET Bearer token auth (localhost-only fallback in dev; error log in prod).
GET  /api/admin/relay/accounts            — list all accounts
POST /api/admin/relay/accounts/:pk/grant  — grant (level + notes body)
POST /api/admin/relay/accounts/:pk/revoke — revoke (reason body)
pubkey validation: must be 64-char lowercase hex.

## Startup seed (`artifacts/api-server/src/index.ts`)
On every startup: grants Timmy's own pubkeyHex "write" access ("manual").
Idempotent upsert — safe across restarts.

## Smoke test results (all pass)
- Timmy pubkey → accept ✓; unknown pubkey → reject ✓
- Admin grant → accept ✓; admin revoke → reject ✓; admin list shows accounts ✓
- TypeScript: 0 errors in API server + lib/db
2026-03-19 20:21:12 +00:00
alexpaynex
faef1fe5e0 Add health check endpoint and production secret enforcement for relay policy
Adds a GET `/api/relay/policy` health check endpoint and enforces the `RELAY_POLICY_SECRET` environment variable in production to secure the POST `/api/relay/policy` endpoint.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7ee87f59-1dfd-4a71-8c6f-5938330c7b4a
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
2026-03-19 20:05:09 +00:00
alexpaynex
cdd97922d5 task/30: Sovereign Nostr relay infrastructure (strfry)
## Summary
Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised
stack on the VPS, wired to the API server for event-level access control.

## Files created
- `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy
  plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536,
  rejectEphemeral false, db /data/strfry-db
- `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout
  bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar
  (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback:
  reject on sidecar timeout/failure
- `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar:
  POST /decide receives strfry events, calls API server /api/relay/policy with
  Bearer RELAY_POLICY_SECRET, returns strfry decision JSON
- `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config
- `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime
- `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist
- `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal
  route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects
  all events with "relay not yet open — whitelist pending (Task #37)". Stable
  contract — future tasks extend evaluatePolicy() without API shape changes

## Files modified
- `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on
  node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy
  healthcheck; strfry depends on relay-policy healthy
- `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands
- `artifacts/api-server/src/routes/index.ts` — registers relayRouter

## Operator setup required on VPS
  mkdir -p /data/strfry && chmod 700 /data/strfry
  echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env
  echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env
  # Also set RELAY_POLICY_SECRET in Replit secrets for API server

## Notes
- TypeScript: 0 errors (API server + relay-policy sidecar both compile clean)
- POST /api/relay/policy smoke test: correct bootstrap reject response
- strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
alexpaynex
0b3a701933 Add security measures to prevent malicious requests when fetching LNURL data
Implement SSRF protection by validating URLs, blocking private IPs, and enforcing HTTPS in the LNURL fetch path.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 35635246-7119-4788-b55f-66a002409788
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
2026-03-19 19:55:59 +00:00
alexpaynex
8a81918226 task/29: fix vouch idempotency + replay guard — unique constraints + DB push
## Code review round 2 issues resolved

### Vouch replay / duplicate boost vulnerability — FIXED
- `nostr-trust-vouches.ts` schema: added `eventId` column + two unique guards:
  1. `UNIQUE(event_id)` — same signed event cannot be replayed for any pair
  2. `UNIQUE INDEX uq_nostr_trust_vouches_pair(voucher_pubkey, vouchee_pubkey)` —
     each elite may vouch for a given target exactly once
- Route: insert now uses `.onConflictDoNothing().returning({ id })`
  - If returned array is empty → duplicate detected → 409 with existing state,
    no trust boost applied
  - If returned array has rows → first-time vouch → boost applied exactly once
- `eventId` extracted from `ev["id"]` (NIP-01 sha256 event id) before insert
- Migration file `0006_timmy_economic_peer.sql` updated to include both
  unique constraints (UNIQUE + CREATE UNIQUE INDEX)
- Schema pushed to production — all three indexes confirmed in DB:
  `nostr_trust_vouches_event_id_unique`, `uq_nostr_trust_vouches_pair`, `pkey`

### Previously fixed (round 1)
- LNURL-pay resolution in ZapService (full NIP-57 §4 flow)
- Vouch event made required with p-tag vouchee binding
- DB migration file 0006 created for both new tables + lightning_address column
- GET /identity/timmy now returns relayUrl field

### Verified
- TypeScript: 0 errors (tsc --noEmit clean)
- DB: all constraints confirmed live in production
- API: /identity/timmy 200, /identity/challenge nonce, /identity/vouch 401/400
2026-03-19 19:51:50 +00:00
alexpaynex
33b47f8682 task/29: fix code review findings — LNURL zap, vouch binding, migration SQL
## Code review issues resolved

### 1. Zap-out: real LNURL-pay resolution (was: log-only when no bolt11)
- `zap.ts`: added `resolveLnurlInvoice()` — full NIP-57 §4 flow:
  * user@domain → https://domain/.well-known/lnurlp/user
  * Fetch LNURL-pay metadata → extract callback URL + min/maxSendable
  * Build signed kind-9734 zap request, send to callback → receive bolt11
  * Pay bolt11 via LNbits. Log event regardless of payment outcome.
- `nostr-identities.ts`: added `lightningAddress` column (nullable TEXT)
- `identity.ts /verify`: extracts `["lud16", "user@domain.com"]` tag from
  signed event and stores it so ZapService can resolve future invoices
- `maybeZapOnJobComplete()` now triggers real payment when lightningAddress
  is stored; logs a warning and skips payment if not available

### 2. Vouch endpoint: signed event is now REQUIRED with p-tag binding
- `event` field changed from optional to required (400 if absent)
- Validates: Nostr signature, event.pubkey matches authenticated voucher
- NEW: event MUST contain a `["p", voucheePubkey]` tag — proves the voucher
  intentionally named the vouchee in their signed event (co-signature binding)

### 3. DB migration file added
- `lib/db/migrations/0006_timmy_economic_peer.sql` — covers:
  * CREATE TABLE IF NOT EXISTS timmy_nostr_events (with indexes)
  * CREATE TABLE IF NOT EXISTS nostr_trust_vouches (with indexes)
  * ALTER TABLE nostr_identities ADD COLUMN IF NOT EXISTS lightning_address
- Schema pushed to production: `lightning_address` column confirmed live

### Additional
- `GET /api/identity/timmy` now includes `relayUrl` field (null when unset)
- TypeScript compiles cleanly (tsc --noEmit: 0 errors)
- All smoke tests pass: /timmy 200, /challenge nonce, /vouch 401/400
2026-03-19 19:47:00 +00:00
alexpaynex
45f4e72f14 task/29: Timmy as economic peer (bidirectional) — verify & mark complete
## What was done
All task-29 deliverables implemented, tested, and in production.

### Implementation (already in main as eb5dcfd)
- TimmyIdentityService: secp256k1 keypair (nsec/npub), sign(), encryptDm()
- ZapService: NIP-57 kind-9734 zap-out, wired into job completion via maybeZapOnJobComplete()
- EngagementService: proactive NIP-04 DMs, configurable cadence (ENGAGEMENT_INTERVAL_DAYS)
- POST /api/identity/vouch — elite-tier trust vouching, X-Nostr-Token gated
- GET /api/identity/timmy — public npub + pubkeyHex + zapCount
- DB: timmy_nostr_events + nostr_trust_vouches (confirmed live in production)
- Frontend: timmy-id.js identity card widget (click-to-copy npub, 60s refresh)

### Session cleanup (this session)
- PR #47 documented and closed (branch = main HEAD)
- Issue #34 closed with full completion notes
- Gitea backlog scrubbed: 6 stale issues closed, 4 new follow-up issues created (#48-#51)
- Agent labels assigned to all open issues (hermes: 13, kimi: 8, timmy: 4, replit: 4)
- New `timmy` label created (Bitcoin orange #f7931a) for Timmy's autonomy work
- Missing agent labels fixed on #36 and #37 (both now @hermes)

### Smoke tests
- GET /api/identity/timmy → 200 {npub, pubkeyHex, zapCount}
- POST /api/identity/challenge → 200 {nonce, expiresAt}
- POST /api/identity/vouch (no token) → 401 {error: X-Nostr-Token header required}
- DB tables: timmy_nostr_events, nostr_trust_vouches, timmy_config all confirmed present
2026-03-19 19:38:32 +00:00
Replit Agent
eb5dcfd48a task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
1. TimmyIdentityService (artifacts/api-server/src/lib/timmy-identity.ts)
   - Loads nsec from TIMMY_NOSTR_NSEC env var at boot (bech32 decode)
   - Generates and warns about ephemeral key if env var absent
   - sign(EventTemplate) → finalizeEvent() with Timmy's key
   - encryptDm(recipientPubkeyHex, plaintext) → NIP-04 nip04.encrypt()
   - Logs npub at server startup

2. ZapService (artifacts/api-server/src/lib/zap.ts)
   - Constructs NIP-57 zap request event (kind 9734), signs with Timmy's key
   - Pays via lnbitsService.payInvoice() if bolt11 provided (stub-mode aware)
   - Logs every outbound event to timmy_nostr_events audit table
   - maybeZapOnJobComplete() wired in jobs.ts after trustService.recordSuccess()
   - Config: ZAP_PCT_DEFAULT (default 0 = disabled), ZAP_MIN_SATS (default 10)
   - Only fires for trusted/elite tier partners when ZAP_PCT_DEFAULT > 0

3. Engagement engine (artifacts/api-server/src/lib/engagement.ts)
   - Configurable cadence: ENGAGEMENT_INTERVAL_DAYS (default 0 = disabled)
   - Queries nostrIdentities for trustScore >= 50 AND lastSeen < threshold
   - Generates personalised DM via agentService.chatReply()
   - Encrypts as NIP-04 DM (kind 4), signs with Timmy's key
   - Logs to timmy_nostr_events; publishes to NOSTR_RELAY_URL if set
   - First run delayed 60s after startup to avoid cold-start noise

4. Vouching endpoint (artifacts/api-server/src/routes/identity.ts)
   - POST /api/identity/vouch: requires X-Nostr-Token with elite tier
   - Verifies optional Nostr event signature from voucher
   - Records relationship in nostr_trust_vouches table
   - Applies VOUCH_TRUST_BOOST (20 pts) to vouchee's trust score
   - GET /api/identity/timmy: public endpoint returning npub + zap count

5. DB schema additions (lib/db/src/schema/)
   - timmy_nostr_events: audit log for all outbound Nostr events
   - nostr_trust_vouches: voucher/vouchee social graph with boost amount
   - Tables created in production DB via drizzle-kit push

6. Frontend identity card (the-matrix/)
   - #timmy-id-card: fixed bottom-right widget with Timmy's npub + zap count
   - timmy-id.js: initTimmyId() fetches /api/identity/timmy on load
   - Npub shortened (npub1xxxx...yyyyyy), click-to-copy with feedback
   - Refreshes every 60s to show live zap count
   - Wired into main.js on firstInit
2026-03-19 19:27:13 +00:00
Replit Agent
dabadb4298 task-28 fix5: session triage, speech-bubble local badge, footprint docs
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
1. ui.js: edge triage now runs BEFORE session handler delegation
   - classify() called for all send() paths (session + WebSocket)
   - trivial + localReply → setSpeechBubble() used for local reply display
   - session handler only receives moderate/complex messages
   - _fetchEstimate() fired for non-trivial in session mode too

2. edge-worker.js: quantization footprint documented (~87MB int8, cached)
2026-03-19 19:10:46 +00:00
alexpaynex
8897371815 task-28: Edge intelligence — browser ML, Nostr signing, cost preview, sentiment moods
All 6 reviewer rounds complete. Final diff addresses:
- complexity:trivial|moderate|complex contract (not binary local|server)
- Trivial heuristic in cost preview: zero network calls for greetings
- X-Nostr-Token on all job/session API calls including polling paths
- npub-only discovery shows identity prompt (_canSign flag)
- Worker/UI contract fully aligned on complexity tiers
2026-03-19 19:07:24 +00:00
Replit Agent
cb50e8c658 task-28 fix4: trivial cost-preview gate + job polling token
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
1. ui.js: _scheduleCostPreview() now gates on _TRIVIAL_RE before scheduling
   the /api/estimate fetch. Greeting-pattern text shows '0 sats' badge locally
   and never makes a network call. Same regex as edge-worker.js _isGreeting().

2. payment.js: startPolling() GET /api/jobs/:id now attaches X-Nostr-Token
   header on every poll cycle via getOrRefreshToken(). Completes consistent
   X-Nostr-Token coverage across all job/session API calls.
2026-03-19 19:06:47 +00:00
alexpaynex
b4cabf54af task-28 fix3: All four reviewer issues resolved
Fix 1: complexity contract (trivial|moderate|complex)
  - edge-worker.js: _classify() now returns complexity tier not binary local|server
  - trivial = greeting/small-talk ≥ 0.55 → localReply, 0 sats, no server call
  - moderate = simple-question or uncertain → show estimate, route to server
  - complex = technical/creative/code or score < 0.40 → always priced
  - model-unavailable fallback → moderate (safe default)

Fix 2: UI triage driven by complexity outcome
  - edge-worker-client.js fallback returns complexity:moderate (not label:server)
  - ui.js send(): trivial+localReply → local; moderate/complex → _fetchEstimate()
    called on classify outcome then server route

Fix 3: X-Nostr-Token on ALL session API calls
  - session.js deposit poll (GET /sessions/:id): X-Nostr-Token added
  - session.js topup poll (GET /sessions/:id): X-Nostr-Token added
  - session.js restore (GET /sessions/:id): X-Nostr-Token added
  - session.js createTopup (POST /sessions/:id/topup): X-Nostr-Token added

Fix 4: npub-only discovery no longer suppresses identity prompt
  - nostr-identity.js: _canSign flag tracks signing separately from pubkey presence
  - npub-only → _pubkey set, _canSign=false → prompt scheduled
  - refreshToken() gated on _pubkey && _canSign (no silent failures)
  - Prompt handlers set _canSign=true on NIP-07 connect or key generation

Also: all 13 pending tasks filed as Gitea issues #34-#46 with scoped ACs
and delegation labels (kimi/hermes). Tailscale Funnel replaces bore.pub.
2026-03-19 19:03:37 +00:00
Replit Agent
494393017c task-28 fix3: complexity contract, consistent token headers, npub-only prompt
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
1. edge-worker.js: replace binary label:local|server with complexity:trivial|moderate|complex
   - trivial  = greeting/small-talk ≥ 0.55 confidence → localReply, 0 sats
   - moderate = simple-question or uncertain score → show estimate, route to server
   - complex  = technical/creative/code OR score < 0.40 → show estimate, route to server
   - model-unavailable fallback → moderate (safe default, not 'server')

2. edge-worker-client.js: update fallback and JSDoc to new complexity shape
   - fallback returns { complexity:'moderate', ... } instead of { label:'server', ... }

3. ui.js: triage driven by cls.complexity, not cls.label
   - trivial + localReply → local answer, 0 sats badge, no server call
   - moderate/complex → _fetchEstimate() fired on classify outcome (not just debounce)
     then routed to server via WebSocket

4. session.js: X-Nostr-Token attached consistently on ALL outbound session calls
   - _startDepositPolling: GET /sessions/:id now includes X-Nostr-Token header
   - _startTopupPolling: GET /sessions/:id now includes X-Nostr-Token header
   - _tryRestore: GET /sessions/:id now includes X-Nostr-Token header
   - _createTopup: POST /sessions/:id/topup now includes X-Nostr-Token header

5. nostr-identity.js: _canSign flag tracks signing capability separately from pubkey
   - initNostrIdentity sets _canSign=true only when NIP-07 or privkey is available
   - npub-only discovery sets _pubkey but _canSign=false → prompt IS scheduled
   - Prompt shown when !_pubkey || !_canSign (not just !_pubkey)
   - Prompt click handlers set _canSign=true after connecting NIP-07 or generating key
   - refreshToken only called when _pubkey && _canSign (avoids silent failures)
2026-03-19 19:02:45 +00:00
alexpaynex
224208fa0f Saved progress at the end of the loop
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: aef73c94-6788-4125-9acc-9061b4713e96
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
2026-03-19 18:42:21 +00:00
Replit Agent
f75825b6e6 chore: switch push-to-gitea.sh from bore.pub to Tailscale Funnel
Replace bore.pub tunnel with Tailscale Funnel URL (https://mm.tailb74b2d.ts.net).
Resolve GITEA_BASE from GITEA_URL env var → .env.local → hardcoded fallback.
Drop .bore-port dependency entirely. Saved GITEA_URL=https://mm.tailb74b2d.ts.net
as a shared env var.
2026-03-19 18:41:44 +00:00
alexpaynex
26556ba300 Update application assets and code for improved functionality
Update manifest file and JavaScript bundle with new asset references.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b74051a5-0be6-4afb-9153-f25b06b6190b
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
2026-03-19 18:24:27 +00:00
alexpaynex
04abc109bd task-28: Edge intelligence — Web Worker triage, Nostr signing, cost preview, sentiment moods (3 review cycles)
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
## Summary of all changes

### New files
- the-matrix/js/edge-worker.js — Proper Web Worker with postMessage API; Transformers.js
  zero-shot-classification + SST-2 sentiment running in worker thread; signals {type:'ready'}
  when models warm; env.useBrowserCache=true for browser Cache API model caching
- the-matrix/js/edge-worker-client.js — Main-thread proxy; spawns Worker via
  new Worker(url, {type:'module'}); wraps calls as Promises; warmup()/onReady()/isReady();
  graceful fallback if Workers unavailable

### Modified files
- js/agents.js: setMood() export maps POSITIVE/NEGATIVE/NEUTRAL → face expressions
- js/nostr-identity.js: Fixed API endpoints (POST /identity/challenge → nonce,
  POST /identity/verify → nostr_token); _scanExistingNostrKeys() discovers common
  Nostr key formats (nsec1/npub1 bech32, hex privkeys, JSON objects) in localStorage
  before showing opt-in prompt; keypair generation requires explicit user consent
- js/ui.js: setEdgeWorkerReady() " local AI" badge; cost preview via X-Nostr-Token
  header (not query param); local triage badge "0 sats"; imports from edge-worker-client
- js/websocket.js: sentiment() on inbound Timmy chat messages → setMood() (10 s auto-clear)
- js/session.js: sentiment() on inbound reply (data.result); X-Nostr-Token on API calls;
  imports from edge-worker-client
- js/payment.js: X-Nostr-Token header on POST /jobs
- js/main.js: initNostrIdentity() + warmupEdgeWorker() + onEdgeWorkerReady(setEdgeWorkerReady)
- the-matrix/package.json: nostr-tools, @xenova/transformers added
- vite.config.js: worker.format:'es' + optimizeDeps.exclude @xenova/transformers
2026-03-19 18:20:33 +00:00
Replit Agent
437df487fd task-28 fix2: common Nostr key discovery, header-only token transport, explicit model caching
1. nostr-identity.js: _scanExistingNostrKeys() discovers pre-existing Nostr keys
   in localStorage using common patterns: nsec1/npub1 bech32, raw hex privkey,
   JSON objects with nsec/npub/privkey fields. Scans common client key names
   (nostr_privkey, privkey, nsec, nostr-nsec, nostrKeys, etc.) before showing
   the identity prompt. Keys discovered are re-saved in app format for next load.
2. ui.js: _fetchEstimate() now sends nostr_token as X-Nostr-Token header instead
   of query param, consistent with all other authenticated API calls.
3. edge-worker.js: explicit env.useBrowserCache=true + env.allowLocalModels=false
   so model weights are cached in browser Cache API after first download.
2026-03-19 18:20:13 +00:00
alexpaynex
120ea7db24 task-28: Edge intelligence — Web Worker triage, Nostr signing, cost preview, sentiment moods
## What was built (Task #28 — all 5 requirements)

### 1. js/edge-worker.js (Web Worker)
Proper Web Worker entry point (not a regular module) using postMessage API.
Loads Transformers.js zero-shot-classification + SST-2 sentiment models in the
worker thread. Signals { type:'ready' } when both models are warm. Handles
{ id, type:'classify'|'sentiment', text } messages and replies with results.
Fast greeting heuristic bypasses model for trivial inputs.

### 2. js/edge-worker-client.js (main-thread proxy)
Main-thread wrapper that spawns edge-worker.js via new Worker(url, {type:'module'}).
Wraps classify()/sentiment() as Promise-based API. Exports: warmup(), onReady(),
isReady(). Gracefully falls back to server routing if Web Workers unavailable.

### 3. js/nostr-identity.js
- Fixed Nostr API endpoints to match server: POST /identity/challenge (→ nonce),
  POST /identity/verify (body:{event}, event.content=nonce → nostr_token)
- NIP-07 extension preferred; localStorage keypair only generated on explicit
  user consent via showIdentityPrompt() UI — no silent key generation
- getOrRefreshToken() used by payment.js/session.js for X-Nostr-Token header

### 4. js/agents.js
setMood() export maps POSITIVE/NEGATIVE/NEUTRAL → Timmy face expressions
(curious/focused/contemplative) via setFaceEmotion() + MOOD_ALIASES.

### 5. Sentiment on INBOUND messages (per reviewer requirement)
- websocket.js: sentiment() runs on Timmy's inbound `chat` messages → setMood()
- session.js: sentiment() runs on data.result (Timmy's reply), not outbound text
- Both auto-clear mood after 10 s

### 6. UX additions
- setEdgeWorkerReady() in ui.js shows " local AI" badge when worker models warm
- showIdentityPrompt() opt-in Nostr identity UI shown 4 s after page load
- Cost preview badge via /api/estimate with free/partial/full display
- Local triage: trivial messages answered without Lightning payment

### 7. Infrastructure
- vite.config.js: worker.format:'es' + optimizeDeps.exclude @xenova/transformers
- package.json: nostr-tools, @xenova/transformers deps added
2026-03-19 18:17:03 +00:00
Replit Agent
898a47fd39 task-28 fix: proper Web Worker, correct Nostr endpoints, sentiment on inbound msgs
Addresses all code review rejections:

1. edge-worker.js → now a proper Web Worker entry point with postMessage API,
   loads models in worker thread; signals {type:'ready'} when warm
2. edge-worker-client.js → new main-thread proxy: spawns Worker via
   new Worker(url, {type:'module'}), wraps calls as Promises, falls back
   to server routing if Workers unavailable; exports classify/sentiment/
   warmup/onReady/isReady
3. nostr-identity.js → fixed endpoints: POST /identity/challenge (→ nonce),
   POST /identity/verify (body:{event}, content=nonce → nostr_token);
   keypair generation now requires explicit user consent via identity prompt
   (no silent key generation); showIdentityPrompt() shows opt-in UI
4. ui.js → import from edge-worker-client; setEdgeWorkerReady() shows
   'local AI' badge when worker signals ready; removed outbound sentiment
5. websocket.js → sentiment() on inbound Timmy chat messages drives setMood()
6. session.js → sentiment() on inbound reply (data.result), not outbound text
7. main.js → onEdgeWorkerReady(() => setEdgeWorkerReady()) wires ready badge
8. vite.config.js → worker.format:'es' for ESM Web Worker bundling
2026-03-19 18:16:40 +00:00
alexpaynex
d9b00c203e task-28: Edge intelligence — browser Transformers.js triage, Nostr signing, cost preview, sentiment moods
## What was built
Five components of Task #28 implemented:

1. js/edge-worker.js (new) — Transformers.js zero-shot-classification + SST-2 sentiment,
   lazy model loading, fast heuristic greeting bypass, warmup() pre-loader.

2. js/nostr-identity.js (new) — NIP-07 extension preferred (window.nostr),
   falls back to generated localStorage keypair; challenge→sign→verify flow
   with the /api/nostr/challenge + /api/nostr/verify endpoints;
   token cached in localStorage with 23 h TTL.

3. js/agents.js — setMood() export maps POSITIVE/NEGATIVE/NEUTRAL sentiment
   labels to Timmy face states (curious/focused/contemplative) via existing
   setFaceEmotion() + MOOD_ALIASES infrastructure.

4. js/ui.js — edge triage on every send: trivial/greeting → answered locally
   with "local  0 sats" badge, no server round-trip; cost preview badge
   debounced 300 ms via GET /api/estimate, shows FREE / partial / full cost;
   sentiment-driven setMood() on every user message (auto-clears after 8 s).

5. js/payment.js, js/session.js — X-Nostr-Token header injected on all
   authenticated API calls (POST /jobs, POST /sessions, POST session/request).
   session.js also runs sentiment → setMood() on every session send.

6. js/main.js — initNostrIdentity('/api') + warmupEdgeWorker() called on firstInit.

7. vite.config.js — optimizeDeps.exclude @xenova/transformers to prevent
   Vite from pre-bundling the WASM/dynamic-import-heavy transformer library.

8. the-matrix/package.json — nostr-tools, @xenova/transformers added as deps.

## No server-side changes — all Task #28 work is frontend-only.
2026-03-19 18:10:11 +00:00
Replit Agent
af3c938c6e task-28: edge intelligence — Transformers.js triage, Nostr signing, cost preview, sentiment moods
- js/edge-worker.js: new — browser-side classify() + sentiment() via Transformers.js
- js/nostr-identity.js: new — NIP-07 extension + localStorage keypair fallback,
  challenge→sign→verify flow, token caching
- js/agents.js: export setMood() for sentiment-driven face expressions
- js/ui.js: local triage badge, cost preview via /api/estimate, sentiment on send
- js/payment.js: X-Nostr-Token injection on POST /jobs
- js/session.js: X-Nostr-Token injection on session create + request, sentiment mood
- js/main.js: initNostrIdentity() + warmupEdgeWorker() on firstInit
- vite.config.js: optimizeDeps.exclude @xenova/transformers
2026-03-19 18:09:44 +00:00
alexpaynex
484583004a Task #27: Free-tier gate — all correctness issues resolved
Blocking issues from reviewer, all fixed:

1. /api/estimate no longer mutates pool — uses decideDryRun() (read-only)

2. Free-path passes actual debited amount (ftDecision.absorbSats) not estimate:
   - DB absorbedSats = ftDecision.absorbSats (actual pool debit, may be < estimate)
   - runWorkInBackground receives reservedAbsorbed = actual pool debit
   - recordGrant reconciles actual vs reserved; over-reservation returned to pool

3. decide() free branch: downgrade to partial if atomic debit < estimatedSats:
   - If pool race causes debited < estimated: release debit, return serve="partial"
   - Only returns serve="free" (chargeSats=0) when full amount was debited

4. Reservation leak on pre-work failure: inner try/catch around DB update
   - If DB setup fails after pool debit: releaseReservation() called before throw

5. Partial pool-drain at payment: reverts to normal paid flow (not fail):
   - partialGrantReserved = 0: work executes with zero subsidy
   - User charged their paid amount; normal refund path applies if actual < paid
   - No dead-end refund state; no stranded users

6. Partial-job refund math: actualUserChargeSats = max(0, actual - absorbed)

7. Sessions comment clarified: pool reservation sized to work estimate;
   if it covers fullDebitSats (eval+work), debitedSats = 0; otherwise partial
2026-03-19 17:28:19 +00:00