Ctrl K
ComunidadGuías…

scripts/cascade-survival-probe.mjs

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
// cascade-survival-probe — track whether the Windsurf/Cascade backend keeps
// serving requests across the July 1 2026 Cascade retirement deadline.
//
// WHY: Cognition retires Cascade (binary-level) on 2026-07-01, replacing the
// local agent with Devin Local + ACP. The open question that decides whether
// WindsurfAPI's gRPC reflection path survives is: does the *backend* API
// (server.self-serve.windsurf.com) keep serving, or does it go down with the
// local Cascade agent? Blogs don't answer this. We measure it directly.
//
// SAFETY MODEL — two modes:
//   * DEFAULT (health-only): ZERO billable model calls. Hits only the running
//     WindsurfAPI /health?verbose=1 for account / tier / LS / drought status.
//     This catches catastrophic backend death (auth path down, accounts flip
//     to expired/dead, LS pool gone) at zero cost.
//   * OPT-IN (--chat / PROBE_CHAT=1): additionally fires ONE cheap entitled
//     gemini-2.5-flash completion (max_tokens 8) to confirm the Cascade *chat*
//     path still yields a token. This IS a real, billable, free-tier call — it
//     spends the account's prompt allowance, so it is deliberately off by
//     default (mirrors the opt-in canary policy from commit 21393b9, which was
//     introduced after a force-probe sweep degraded a working free account).
//
// Each run appends a timestamped JSONL line so we can see the exact moment (if
// any) the backend stops working.
//
// Flags / env:
//   --dry-run   (or DRY_RUN=1)   print resolved config + planned requests, make
//                                NO network call at all, exit 0. Use this to
//                                validate the script anywhere, incl. a box with
//                                no server / no API key.
//   --chat      (or PROBE_CHAT=1) enable the billable gemini chat probe.
//
// Usage (on homecloud, key never leaves the box):
//   KEY=$(sed -n 's/^API_KEY=//p' ~/WindsurfAPI/.env)
//   # zero-billable continuous monitor (recommended cadence: every 30min):
//   API_KEY=$KEY node scripts/cascade-survival-probe.mjs
//   # opt-in chat-path liveness (spends free-tier credit — run sparingly):
//   API_KEY=$KEY node scripts/cascade-survival-probe.mjs --chat
// Schedule via cron every ~30min across 2026-06-30 .. 2026-07-02.

import { appendFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';

const argv = new Set(process.argv.slice(2));
const dryRun = argv.has('--dry-run') || process.env.DRY_RUN === '1';
const chatEnabled = argv.has('--chat') || process.env.PROBE_CHAT === '1';

const baseUrl = (process.env.BASE_URL || 'http://127.0.0.1:3003').replace(/\/+$/, '');
const apiKey = process.env.API_KEY || process.env.WINDSURFAPI_API_KEY || '';
const model = process.env.PROBE_MODEL || 'gemini-2.5-flash';
const outFile = process.env.PROBE_OUT || 'docs-internal/workflow-results/cascade-survival.jsonl';
const timeoutMs = Math.max(5_000, Number(process.env.PROBE_TIMEOUT_MS || 60_000));

if (dryRun) {
  // Make NO network call. Just show exactly what a real run would do.
  const plan = {
    mode: chatEnabled ? 'health+chat (billable opt-in)' : 'health-only (zero billable)',
    dry_run: true,
    baseUrl,
    api_key_present: !!apiKey,
    outFile,
    timeoutMs,
    planned_requests: [
      { step: 1, method: 'GET', path: '/health?verbose=1', billable: false },
      ...(chatEnabled
        ? [{ step: 2, method: 'POST', path: '/v1/chat/completions', model, max_tokens: 8, billable: true }]
        : [{ step: 2, skipped: true, reason: 'chat probe is opt-in (--chat / PROBE_CHAT=1)' }]),
    ],
  };
  console.log(JSON.stringify(plan, null, 2));
  process.exit(0);
}

if (!apiKey) {
  console.error('API_KEY is required (read it locally; do not paste it anywhere).');
  process.exit(2);
}

async function call(path, opts = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  const started = Date.now();
  try {
    const res = await fetch(`${baseUrl}${path}`, {
      ...opts,
      signal: controller.signal,
      headers: { authorization: `Bearer ${apiKey}`, ...(opts.headers || {}) },
    });
    const text = await res.text();
    let body = null;
    try { body = text ? JSON.parse(text) : null; } catch {}
    return { status: res.status, body, ms: Date.now() - started };
  } catch (err) {
    return { status: 0, error: String(err?.message || err), ms: Date.now() - started };
  } finally {
    clearTimeout(timer);
  }
}

const record = { ts: new Date().toISOString() };

// 1. health — accounts + LS state (no upstream call)
const health = await call('/health?verbose=1');
record.health_status = health.status;
record.accounts = health.body?.accounts || null;
record.version = health.body?.version || null;
record.drought = health.body?.drought || null;
record.health_ok = health.status === 200;

// 2. cheap entitled chat probe — OPT-IN ONLY (billable, spends free-tier credit).
//    Confirms the Cascade chat path still yields a token. Off by default so the
//    every-30min monitor never burns the account (see commit 21393b9 canary policy).
if (chatEnabled) {
  const chat = await call('/v1/chat/completions', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      model,
      max_tokens: 8,
      messages: [{ role: 'user', content: 'Reply with the single word: ALIVE' }],
    }),
  });
  record.chat_status = chat.status;
  record.chat_ms = chat.ms;
  const content = chat.body?.choices?.[0]?.message?.content;
  record.chat_text = typeof content === 'string' ? content.slice(0, 40) : null;
  record.chat_error = chat.body?.error?.type || chat.error || null;
  record.cascade_alive = chat.status === 200 && !!content;
} else {
  record.chat_skipped = 'opt-in (--chat / PROBE_CHAT=1)';
}

mkdirSync(dirname(outFile), { recursive: true });
appendFileSync(outFile, JSON.stringify(record) + '\n');
console.log(JSON.stringify(record, null, 2));

// Exit code: in chat mode, success = chat path alive; in health-only mode,
// success = health endpoint reachable + returning 200 (the zero-cost signal).
const ok = chatEnabled ? !!record.cascade_alive : record.health_ok;
process.exit(ok ? 0 : 1);