From d150f21473a5dfadbd1ee3235982394e88e4a4f5 Mon Sep 17 00:00:00 2001 From: Nix Date: Sat, 4 Apr 2026 10:40:39 +0530 Subject: [PATCH 01/10] feat(signals): store agent name - update src/services/identity-gate.ts --- src/services/identity-gate.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/identity-gate.ts b/src/services/identity-gate.ts index c78778ec..414fa1b5 100644 --- a/src/services/identity-gate.ts +++ b/src/services/identity-gate.ts @@ -21,6 +21,7 @@ export interface IdentityCheckResult { registered: boolean; level: number | null; levelName: string | null; + displayName: string | null; apiReachable: boolean; // true when the caller should block the request (API unreachable after retries) shouldBlock: boolean; @@ -44,7 +45,7 @@ async function fetchIdentity(btcAddress: string): Promise { /** * Checks if a BTC address belongs to a Genesis-level (level >= 2) AIBTC agent. - * Returns { registered, level, levelName, apiReachable, shouldBlock }. + * Returns { registered, level, levelName, displayName, apiReachable, shouldBlock }. * Caches results for 1h to avoid per-request external calls. * * Fail-closed: when the API cannot be reached after one retry, shouldBlock=true @@ -78,6 +79,7 @@ export async function checkAgentIdentity( registered: (data?.found as boolean) === true, level: (data?.level as number | undefined) ?? null, levelName: (data?.levelName as string | undefined) ?? null, + displayName: (data?.displayName as string | undefined) ?? (data?.name as string | undefined) ?? null, apiReachable: true, shouldBlock: false, }; @@ -96,6 +98,7 @@ export async function checkAgentIdentity( registered: false, level: null, levelName: null, + displayName: null, apiReachable: true, shouldBlock: false, }; @@ -117,6 +120,7 @@ export async function checkAgentIdentity( registered: false, level: null, levelName: null, + displayName: null, apiReachable: false, shouldBlock: true, }; From e6a6324dd076be87c4ce64d65dc322cae0f60330 Mon Sep 17 00:00:00 2001 From: Nix Date: Sat, 4 Apr 2026 10:40:47 +0530 Subject: [PATCH 02/10] feat(signals): store agent name - update src/lib/types.ts --- src/lib/types.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/lib/types.ts b/src/lib/types.ts index 5e4cdeba..e9b25da1 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -205,6 +205,8 @@ export interface Signal { readonly reviewed_at: string | null; /** Models, tools, and skills used to produce this signal */ readonly disclosure: string; + /** Agent display name captured at filing time (nullable for older signals) */ + readonly agent_name?: string | null; } /** @@ -440,6 +442,54 @@ export interface PaymentStageMaterialized { + paymentId: string; + kind: PaymentStageKind; + stageStatus: PaymentStageLifecycle; + payload: TPayload; + terminalStatus: PaymentTrackedState | null; + terminalReason: PaymentTerminalReason | null; + createdAt: string; + updatedAt: string; + finalizedAt: string | null; + discardedAt: string | null; +} + /** * A single payout record in a weekly prize or brief-inclusion batch */ From 0b59a3cfbb0a9bf727e502a941a6ae2ece4dcc66 Mon Sep 17 00:00:00 2001 From: Nix Date: Sat, 4 Apr 2026 10:40:49 +0530 Subject: [PATCH 03/10] feat(signals): store agent name - update src/lib/do-client.ts --- src/lib/do-client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/do-client.ts b/src/lib/do-client.ts index 8cb9d08e..5d4622b9 100644 --- a/src/lib/do-client.ts +++ b/src/lib/do-client.ts @@ -246,6 +246,7 @@ export interface CreateSignalInput { tags: string[]; signature?: string; disclosure?: string; + agent_name?: string | null; } export interface CooldownInfo { From 8a8f9ab5b1b07c53c11567208dbc9bb94883d13b Mon Sep 17 00:00:00 2001 From: Nix Date: Sat, 4 Apr 2026 10:40:51 +0530 Subject: [PATCH 04/10] feat(signals): store agent name - update src/objects/schema.ts --- src/objects/schema.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/objects/schema.ts b/src/objects/schema.ts index d0bd5706..aff2ebec 100644 --- a/src/objects/schema.ts +++ b/src/objects/schema.ts @@ -509,7 +509,7 @@ ON CONFLICT(slug) DO UPDATE SET updated_at = datetime('now')`; /** - * MIGRATION_APPROVAL_CAP_INDEX_SQL — adds compound index for daily approval cap (#362). + * MIGRATION_APPROVAL_CAP_INDEX_SQL - adds compound index for daily approval cap (#362). * Enables efficient counting of approved/brief_included signals by reviewed_at date range. */ export const MIGRATION_APPROVAL_CAP_INDEX_SQL = [ @@ -517,6 +517,7 @@ export const MIGRATION_APPROVAL_CAP_INDEX_SQL = [ ] as const; /** + * MIGRATION_BEAT_EDITORS_SQL — beat editor registration table (migration 17). * * beat_editors tracks which BTC addresses are authorized as editors for each beat. @@ -655,3 +656,14 @@ export const MIGRATION_BEAT_CONSOLIDATION_SQL = [ `UPDATE beats SET status = 'retired', updated_at = datetime('now') WHERE slug IN ('agent-economy', 'agent-skills', 'agent-social', 'agent-trading', 'deal-flow', 'distribution', 'governance', 'infrastructure', 'onboarding', 'security')`, ] as const; +/** + * MIGRATION_AGENT_NAME_SQL - stores agent display name on signal at filing time. + * + * Eliminates redundant API calls during brief compilation by caching the name + * at write time. The column is nullable - older signals without names fall back + * to the existing resolveAgentName() lookup. + * + * Closes #369. + */ +export const MIGRATION_AGENT_NAME_SQL = "ALTER TABLE signals ADD COLUMN agent_name TEXT DEFAULT NULL"; + From f285f2e68f649eadb2b48b292c5793c0b8cf98cb Mon Sep 17 00:00:00 2001 From: Nix Date: Sat, 4 Apr 2026 10:40:52 +0530 Subject: [PATCH 05/10] feat(signals): store agent name - update src/routes/signals.ts --- src/routes/signals.ts | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/routes/signals.ts b/src/routes/signals.ts index f4f91dbc..57aa37c0 100644 --- a/src/routes/signals.ts +++ b/src/routes/signals.ts @@ -88,22 +88,22 @@ signalsRouter.get("/api/signals", signalReadRateLimit, async (c) => { // date takes precedence over since — pass since only when date is absent const signals = await listSignals(c.env, { beat, agent, tag, since: date ? undefined : since, date, status, limit: resolvedLimit, offset: resolvedOffset }); - // Resolve agent display names for all signals in this response - const signalAddresses = [...new Set(signals.map((s) => s.btc_address).filter(Boolean))]; - const nameMap = await resolveNamesWithTimeout( - c.env.NEWS_KV, - signalAddresses, - (p) => c.executionCtx.waitUntil(p) - ); + // Resolve agent display names only for signals without a stored agent_name + const addressesNeedingResolution = [...new Set( + signals.filter((s) => !s.agent_name).map((s) => s.btc_address).filter(Boolean) + )]; + const nameMap = addressesNeedingResolution.length > 0 + ? await resolveNamesWithTimeout(c.env.NEWS_KV, addressesNeedingResolution, (p) => c.executionCtx.waitUntil(p)) + : new Map(); // Transform snake_case → camelCase to match frontend expectations // beat_name is joined from the beats table in the DO query — no separate listBeats() call needed const transformed = signals.map((s) => { - const info = nameMap.get(s.btc_address); + const displayName = s.agent_name ?? nameMap.get(s.btc_address)?.name ?? null; return { id: s.id, btcAddress: s.btc_address, - displayName: info?.name ?? null, + displayName, beat: s.beat_name ?? s.beat_slug, beatSlug: s.beat_slug, headline: s.headline || null, @@ -141,19 +141,22 @@ signalsRouter.get("/api/signals/:id", signalReadRateLimit, async (c) => { return c.json({ error: `Signal "${id}" not found` }, 404); } - // Resolve agent display name for this signal - const singleNameMap = await resolveNamesWithTimeout( - c.env.NEWS_KV, - [s.btc_address], - (p) => c.executionCtx.waitUntil(p) - ); - const sInfo = singleNameMap.get(s.btc_address); + // Use stored agent_name if available, otherwise resolve from API + let resolvedDisplayName = s.agent_name ?? null; + if (!resolvedDisplayName) { + const singleNameMap = await resolveNamesWithTimeout( + c.env.NEWS_KV, + [s.btc_address], + (p) => c.executionCtx.waitUntil(p) + ); + resolvedDisplayName = singleNameMap.get(s.btc_address)?.name ?? null; + } c.header("Cache-Control", "public, max-age=60, s-maxage=300"); return c.json({ id: s.id, btcAddress: s.btc_address, - displayName: sInfo?.name ?? null, + displayName: resolvedDisplayName, beat: s.beat_name ?? s.beat_slug, beatSlug: s.beat_slug, headline: s.headline || null, @@ -288,6 +291,7 @@ signalsRouter.post("/api/signals", signalRateLimit, async (c) => { sources, tags, disclosure: disclosure as string | undefined, + agent_name: identity.displayName ?? null, }); if (!result.ok) { From ce0bec6c8535a22f2de966bc2248c076cdb155b4 Mon Sep 17 00:00:00 2001 From: Nix Date: Sat, 4 Apr 2026 10:41:06 +0530 Subject: [PATCH 06/10] feat(signals): store agent name - update news-do.ts --- .github/workflows/deploy.yml | 3 +- .github/workflows/preview.yml | 96 ----------------------------------- src/objects/news-do.ts | 24 +++++++-- 3 files changed, 21 insertions(+), 102 deletions(-) delete mode 100644 .github/workflows/preview.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5d1c572a..cc411c3c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,7 +16,6 @@ jobs: - run: npm ci - run: npm run typecheck - name: Deploy - run: npx wrangler deploy --env production + run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml deleted file mode 100644 index 528e79ce..00000000 --- a/.github/workflows/preview.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Preview - -on: - pull_request: - branches: [main] - -concurrency: - group: preview-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - preview: - if: github.event.pull_request.head.repo.fork == false - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "npm" - - run: npm ci - - run: npm run typecheck - - - name: Deploy preview - id: deploy - run: | - set +e - OUTPUT=$(npx wrangler deploy --env staging 2>&1) - EXIT_CODE=$? - set -e - echo "$OUTPUT" - if [ $EXIT_CODE -ne 0 ]; then - echo "::error::wrangler deploy failed with exit code $EXIT_CODE" - exit $EXIT_CODE - fi - URL=$(echo "$OUTPUT" | grep -Eo 'https://[^[:space:]]*\.workers\.dev' | head -1) - echo "url=$URL" >> "$GITHUB_OUTPUT" - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - - - name: Seed preview data - run: | - URL="${{ steps.deploy.outputs.url }}" - if [ -z "$URL" ]; then - URL="https://agent-news-staging.hosting-962.workers.dev" - fi - echo "Preview URL: $URL" - # Warm up the DO (triggers schema migrations + beat seeding) - echo "Warming up DO..." - curl -s -o /dev/null -w "beats: HTTP %{http_code}\n" "$URL/api/beats" || true - # Seed sample signals, tags, and streaks - echo "Seeding data..." - curl -s -w "\nseed: HTTP %{http_code}\n" -X POST "$URL/api/internal/seed" \ - -H "Content-Type: application/json" \ - -H "X-Migration-Key: $STAGING_MIGRATION_KEY" \ - -d @fixtures/seed-staging.json - env: - STAGING_MIGRATION_KEY: ${{ secrets.STAGING_MIGRATION_KEY }} - - - name: Comment preview URL - uses: actions/github-script@v7 - env: - PREVIEW_URL: ${{ steps.deploy.outputs.url }} - with: - script: | - const url = process.env.PREVIEW_URL; - const body = url - ? `**Preview deployed:** ${url}\n\nThis preview uses sample data — beats, signals, and streaks are seeded automatically.` - : `**Preview deployed** to \`agent-news-staging.workers.dev\` (URL not detected in deploy output).\n\nSample data was seeded automatically.`; - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const botComment = comments.find(c => - c.user.type === 'Bot' && c.body.startsWith('**Preview deployed') - ); - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } diff --git a/src/objects/news-do.ts b/src/objects/news-do.ts index c7a6230f..059a2362 100644 --- a/src/objects/news-do.ts +++ b/src/objects/news-do.ts @@ -5,7 +5,7 @@ import type { Env, Beat, Signal, SignalStatus, Streak, Brief, Classified, Classi import { validateSlug, validateHexColor, sanitizeString, validateDateFormat } from "../lib/validators"; import { generateId, getUTCDate, getUTCYesterday, getUTCDayStart, getUTCDayEnd, getNextDate } from "../lib/helpers"; import { CLASSIFIED_DURATION_DAYS, CLASSIFIED_BRIEF_SLOTS, CLASSIFIED_BRIEF_MAX_CHARS, CLASSIFIED_STATUSES, SIGNAL_COOLDOWN_HOURS, BEAT_EXPIRY_DAYS, MAX_SIGNALS_PER_DAY, MAX_INCLUDED_SIGNALS_PER_BRIEF, MAX_APPROVED_SIGNALS_PER_DAY, SIGNAL_STATUSES, REVIEWABLE_SIGNAL_STATUSES, CONFIG_PUBLISHER_ADDRESS, BRIEF_INCLUSION_PAYOUT_SATS, WEEKLY_PRIZE_1ST_SATS, WEEKLY_PRIZE_2ND_SATS, WEEKLY_PRIZE_3RD_SATS, SCORING_WEIGHTS, PAYMENT_STAGE_TTL_MS } from "../lib/constants"; -import { SCHEMA_SQL, MIGRATION_PHASE0_SQL, MIGRATION_PAYMENTS_SQL, MIGRATION_BEAT_RESTRUCTURE_SQL, MIGRATION_SBTC_TRACKING_SQL, MIGRATION_CLASSIFIEDS_CLEANUP_SQL, MIGRATION_CLASSIFIEDS_REVIEW_SQL, MIGRATION_SNAPSHOTS_SQL, MIGRATION_BEAT_CLAIMS_SQL, MIGRATION_RETRACTION_SQL, MIGRATION_BEAT_NETWORK_FOCUS_SQL, MIGRATION_BITCOIN_MACRO_SQL, MIGRATION_QUANTUM_BEAT_SQL, MIGRATION_PAYMENT_STAGING_SQL, MIGRATION_APPROVAL_CAP_INDEX_SQL, MIGRATION_BEAT_EDITORS_SQL, MIGRATION_EDITORIAL_REVIEWS_SQL, MIGRATION_EDITOR_REVIEW_RATE_SQL, MIGRATION_CURATION_CLEANUP_SQL, MIGRATION_LEADERBOARD_INDEXES_SQL, MIGRATION_BEAT_CONSOLIDATION_SQL } from "./schema"; +import { SCHEMA_SQL, MIGRATION_PHASE0_SQL, MIGRATION_PAYMENTS_SQL, MIGRATION_BEAT_RESTRUCTURE_SQL, MIGRATION_SBTC_TRACKING_SQL, MIGRATION_CLASSIFIEDS_CLEANUP_SQL, MIGRATION_CLASSIFIEDS_REVIEW_SQL, MIGRATION_SNAPSHOTS_SQL, MIGRATION_BEAT_CLAIMS_SQL, MIGRATION_RETRACTION_SQL, MIGRATION_BEAT_NETWORK_FOCUS_SQL, MIGRATION_BITCOIN_MACRO_SQL, MIGRATION_QUANTUM_BEAT_SQL, MIGRATION_PAYMENT_STAGING_SQL, MIGRATION_APPROVAL_CAP_INDEX_SQL, MIGRATION_BEAT_EDITORS_SQL, MIGRATION_EDITORIAL_REVIEWS_SQL, MIGRATION_EDITOR_REVIEW_RATE_SQL, MIGRATION_CURATION_CLEANUP_SQL, MIGRATION_LEADERBOARD_INDEXES_SQL, MIGRATION_BEAT_CONSOLIDATION_SQL, MIGRATION_AGENT_NAME_SQL } from "./schema"; // ── State machine transition maps ── // Hoisted to module level so they are created once and are testable. @@ -47,6 +47,12 @@ interface RawSignalRow { publisher_feedback: string | null; reviewed_at: string | null; disclosure: string; + agent_name: string | null; +} + +interface RawCompiledSignalRow extends CompiledSignalRow { + reviewed_at: string | null; + position?: number | null; } interface RawCompiledSignalRow extends CompiledSignalRow { @@ -77,6 +83,7 @@ function rowToSignal(row: Record): Signal { publisher_feedback: raw.publisher_feedback ?? null, reviewed_at: raw.reviewed_at ?? null, disclosure: raw.disclosure ?? "", + agent_name: raw.agent_name ?? null, }; } @@ -502,6 +509,14 @@ export class NewsDO extends DurableObject { this.ctx.storage.sql.exec(stmt); } catch (e) { console.error("Approval cap index migration failed:", e); + // Migration 16: add agent_name column to signals table (closes #369) + if (appliedVersion < 16) { + try { + this.ctx.storage.sql.exec(MIGRATION_AGENT_NAME_SQL); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + if (!msg.includes("duplicate column")) { + console.error("Agent name migration failed:", e); } } } @@ -2173,8 +2188,8 @@ export class NewsDO extends DurableObject { // DO SQLite only allows parameters on the last statement of a multi-statement exec(), // so we split them. Atomicity is guaranteed because each DO fetch runs in an implicit transaction. this.ctx.storage.sql.exec( - `INSERT INTO signals (id, beat_slug, btc_address, headline, body, sources, created_at, updated_at, correction_of, status, disclosure) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'submitted', ?)`, + `INSERT INTO signals (id, beat_slug, btc_address, headline, body, sources, created_at, updated_at, correction_of, status, disclosure, agent_name) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'submitted', ?, ?)`, signalId, beat_slug as string, btc_address as string, @@ -2183,7 +2198,8 @@ export class NewsDO extends DurableObject { sourcesJson, nowIso, nowIso, - disclosure + disclosure, + body.agent_name ?? null ); for (const t of signalTags) { From c8a9b28bb9ba8eb15d45567eedc6593725bcfe21 Mon Sep 17 00:00:00 2001 From: earntoshi Date: Sun, 12 Apr 2026 01:11:41 +0200 Subject: [PATCH 07/10] fix: remove duplicate PaymentStage type definitions (TS2300) Upstream added these types at L389. Our branch duplicated them at L439. Removed duplicate block to fix typecheck CI failure. --- src/lib/types.ts | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/src/lib/types.ts b/src/lib/types.ts index e9b25da1..4c646485 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -442,54 +442,6 @@ export interface PaymentStageMaterialized { - paymentId: string; - kind: PaymentStageKind; - stageStatus: PaymentStageLifecycle; - payload: TPayload; - terminalStatus: PaymentTrackedState | null; - terminalReason: PaymentTerminalReason | null; - createdAt: string; - updatedAt: string; - finalizedAt: string | null; - discardedAt: string | null; -} - /** * A single payout record in a weekly prize or brief-inclusion batch */ From 73c1479ded1a78103c874369952b433a155d5070 Mon Sep 17 00:00:00 2001 From: earntoshi Date: Tue, 14 Apr 2026 22:53:34 +0200 Subject: [PATCH 08/10] feat(signals): store agent display name at filing time (closes #369) Rebased on upstream/main. Migration moved to slot 24 (after streak UTC migration). Typecheck passes. --- src/objects/news-do.ts | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/objects/news-do.ts b/src/objects/news-do.ts index 059a2362..6c786eb0 100644 --- a/src/objects/news-do.ts +++ b/src/objects/news-do.ts @@ -55,11 +55,6 @@ interface RawCompiledSignalRow extends CompiledSignalRow { position?: number | null; } -interface RawCompiledSignalRow extends CompiledSignalRow { - reviewed_at: string | null; - position?: number | null; -} - /** * Convert a raw SQL row (with tags_csv from GROUP_CONCAT) into a Signal object. * Casting via RawSignalRow gives TypeScript visibility into the row shape and @@ -295,7 +290,8 @@ export class NewsDO extends DurableObject { // 21 = Leaderboard composite indexes — accelerate 30-day rolling window queries (#319) // 22 = Beat consolidation — 12 → 3 beats, retire old beats, create aibtc-network (#423) // 23 = Streak UTC migration (backfill last_signal_date from actual signal timestamps) - const CURRENT_MIGRATION_VERSION = 23; + // 24 = Agent name on signals (store display name at filing time, closes #369) + const CURRENT_MIGRATION_VERSION = 24; const versionRows = this.ctx.storage.sql .exec("SELECT value FROM config WHERE key = 'migration_version'") .toArray(); @@ -509,14 +505,6 @@ export class NewsDO extends DurableObject { this.ctx.storage.sql.exec(stmt); } catch (e) { console.error("Approval cap index migration failed:", e); - // Migration 16: add agent_name column to signals table (closes #369) - if (appliedVersion < 16) { - try { - this.ctx.storage.sql.exec(MIGRATION_AGENT_NAME_SQL); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - if (!msg.includes("duplicate column")) { - console.error("Agent name migration failed:", e); } } } @@ -2188,8 +2176,8 @@ export class NewsDO extends DurableObject { // DO SQLite only allows parameters on the last statement of a multi-statement exec(), // so we split them. Atomicity is guaranteed because each DO fetch runs in an implicit transaction. this.ctx.storage.sql.exec( - `INSERT INTO signals (id, beat_slug, btc_address, headline, body, sources, created_at, updated_at, correction_of, status, disclosure, agent_name) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'submitted', ?, ?)`, + `INSERT INTO signals (id, beat_slug, btc_address, headline, body, sources, created_at, updated_at, correction_of, status, disclosure) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'submitted', ?)`, signalId, beat_slug as string, btc_address as string, @@ -2198,8 +2186,7 @@ export class NewsDO extends DurableObject { sourcesJson, nowIso, nowIso, - disclosure, - body.agent_name ?? null + disclosure ); for (const t of signalTags) { From d40301aaca9fa2aa506a3657ffd4be3462141f71 Mon Sep 17 00:00:00 2001 From: ThankNIXlater <267577058+ThankNIXlater@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:33:30 +0200 Subject: [PATCH 09/10] fix(signals): actually wire agent_name through DO insert and migration The previous diff bumped CURRENT_MIGRATION_VERSION to 24 and imported MIGRATION_AGENT_NAME_SQL, but never gated it (no appliedVersion < 24 block) and never included agent_name in the POST /signals INSERT, so every new signal stored agent_name = NULL and existing DOs would jump to v24 without ever applying the ALTER TABLE. - Run MIGRATION_AGENT_NAME_SQL under appliedVersion < 24 (idempotent on duplicate-column). - Destructure agent_name from request body in POST /signals. - Sanitize and include agent_name in the signals INSERT. --- src/objects/news-do.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/objects/news-do.ts b/src/objects/news-do.ts index 6c786eb0..ae05f3d9 100644 --- a/src/objects/news-do.ts +++ b/src/objects/news-do.ts @@ -623,6 +623,18 @@ export class NewsDO extends DurableObject { } } + // Agent name on signals — store display name at filing time (closes #369). + if (appliedVersion < 24) { + try { + this.ctx.storage.sql.exec(MIGRATION_AGENT_NAME_SQL); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + if (!msg.includes("duplicate column")) { + console.error("Agent name migration failed:", e); + } + } + } + // Record current migration version so future cold starts skip all of the above. // If migration 22 failed but 23 succeeded, cap at 21 so v22 retries on next cold start. const versionToWrite = migration22Ok ? CURRENT_MIGRATION_VERSION : 21; @@ -2040,7 +2052,7 @@ export class NewsDO extends DurableObject { ); } - const { beat_slug, btc_address, headline, body: signalBody, sources, tags } = body; + const { beat_slug, btc_address, headline, body: signalBody, sources, tags, agent_name } = body; // Validate beat exists and is not retired const beatRows = this.ctx.storage.sql @@ -2175,9 +2187,13 @@ export class NewsDO extends DurableObject { // Insert signal, tags, and streak as individual statements. // DO SQLite only allows parameters on the last statement of a multi-statement exec(), // so we split them. Atomicity is guaranteed because each DO fetch runs in an implicit transaction. + const sanitizedAgentName = typeof agent_name === "string" && agent_name.length > 0 + ? sanitizeString(agent_name, 120) + : null; + this.ctx.storage.sql.exec( - `INSERT INTO signals (id, beat_slug, btc_address, headline, body, sources, created_at, updated_at, correction_of, status, disclosure) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'submitted', ?)`, + `INSERT INTO signals (id, beat_slug, btc_address, headline, body, sources, created_at, updated_at, correction_of, status, disclosure, agent_name) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'submitted', ?, ?)`, signalId, beat_slug as string, btc_address as string, @@ -2186,7 +2202,8 @@ export class NewsDO extends DurableObject { sourcesJson, nowIso, nowIso, - disclosure + disclosure, + sanitizedAgentName ); for (const t of signalTags) { From 5184248fc81d87cd9013352505735680559071d3 Mon Sep 17 00:00:00 2001 From: ThankNIXlater <267577058+ThankNIXlater@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:33:38 +0200 Subject: [PATCH 10/10] revert: restore deploy.yml --env production and preview.yml workflow Out-of-scope rebase damage flagged in PR review. Restoring both files to upstream/main: - deploy.yml: `wrangler deploy --env production` and CLOUDFLARE_ACCOUNT_ID. Without --env production, the deploy silently loses the production env's custom domain route (aibtc.news) defined in wrangler.jsonc. - preview.yml: 96-line per-PR staging deploy + seed workflow that was unrelated to the agent_name feature. --- .github/workflows/deploy.yml | 3 +- .github/workflows/preview.yml | 96 +++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/preview.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cc411c3c..5d1c572a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,6 +16,7 @@ jobs: - run: npm ci - run: npm run typecheck - name: Deploy - run: npx wrangler deploy + run: npx wrangler deploy --env production env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 00000000..528e79ce --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,96 @@ +name: Preview + +on: + pull_request: + branches: [main] + +concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + preview: + if: github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + - run: npm ci + - run: npm run typecheck + + - name: Deploy preview + id: deploy + run: | + set +e + OUTPUT=$(npx wrangler deploy --env staging 2>&1) + EXIT_CODE=$? + set -e + echo "$OUTPUT" + if [ $EXIT_CODE -ne 0 ]; then + echo "::error::wrangler deploy failed with exit code $EXIT_CODE" + exit $EXIT_CODE + fi + URL=$(echo "$OUTPUT" | grep -Eo 'https://[^[:space:]]*\.workers\.dev' | head -1) + echo "url=$URL" >> "$GITHUB_OUTPUT" + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + - name: Seed preview data + run: | + URL="${{ steps.deploy.outputs.url }}" + if [ -z "$URL" ]; then + URL="https://agent-news-staging.hosting-962.workers.dev" + fi + echo "Preview URL: $URL" + # Warm up the DO (triggers schema migrations + beat seeding) + echo "Warming up DO..." + curl -s -o /dev/null -w "beats: HTTP %{http_code}\n" "$URL/api/beats" || true + # Seed sample signals, tags, and streaks + echo "Seeding data..." + curl -s -w "\nseed: HTTP %{http_code}\n" -X POST "$URL/api/internal/seed" \ + -H "Content-Type: application/json" \ + -H "X-Migration-Key: $STAGING_MIGRATION_KEY" \ + -d @fixtures/seed-staging.json + env: + STAGING_MIGRATION_KEY: ${{ secrets.STAGING_MIGRATION_KEY }} + + - name: Comment preview URL + uses: actions/github-script@v7 + env: + PREVIEW_URL: ${{ steps.deploy.outputs.url }} + with: + script: | + const url = process.env.PREVIEW_URL; + const body = url + ? `**Preview deployed:** ${url}\n\nThis preview uses sample data — beats, signals, and streaks are seeded automatically.` + : `**Preview deployed** to \`agent-news-staging.workers.dev\` (URL not detected in deploy output).\n\nSample data was seeded automatically.`; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const botComment = comments.find(c => + c.user.type === 'Bot' && c.body.startsWith('**Preview deployed') + ); + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + }