mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 12:59:13 +01:00
## Thinking Path
> - Paperclip coordinates agent work through Runner.
> - Scheduled Product E2E campaigns need measured cost and time
forecasts.
> - The saved forecast samples came from October 21.
> - The October 27 qualifications retain final usage for all 19 model
requests.
> - The Claude measurements are hosted; the Codex measurements are
local.
> - This change refreshes the samples and keeps measurement hosts
separate.
> - Operators can inspect the latest evidence without treating it as
current qualification or an invoice.
## Linked Issues or Issue Description
Refs #17127 and #17946.
**What happened?**
The scheduled forecast still uses the older pilot amounts. It reports no
hosted measurement and does not record the measurement host per sample.
**What did you expect to happen?**
The forecast must retain the latest complete model receipts and their
source hashes. It must keep local and hosted timing cohorts separate. A
changed definition must remain unqualified.
**Steps to reproduce**
Prepare the daily or weekly forecast from the default branch. Inspect
its saved measurement date, totals, and hosted fields.
## What Changed
- Refresh four samples from two intact request ledgers and four retained
Product results.
- Record measurement hosts, final request counts, and original campaign
outcomes.
- Add historical hosted totals. Current hosted totals still require
matching evidence for every selected cell.
- Keep hosts in separate per-cell cohorts. Preserve definition drift and
unknown slow-tail estimates.
- Document the model-cost scope and timing limits.
## Verification
- Forecast and scheduled-preflight tests: 33 passed.
- The extraction replays both hash-checked ledgers, requires final
receipts, and checks the retained pricecards. The four samples cover 19
final requests.
- Daily historical model subtotal: $0.428650100; summed attempt time:
107.286 seconds.
- Weekly historical model subtotal: $0.793676500; summed attempt time:
158.093 seconds. The Codex samples are local.
- Full Product support suite at the initial head: 4,510 passed, seven
failed, three skipped. A clean 6b9e5f3a baseline with the same installed
dependencies reproduces the same seven failures: two Copilot image
assertions, four workflow-selection shell assertions, and one database
startup test.
- Product typecheck also fails on that clean baseline because the shared
installed Runner workspace exports are stale. The fresh CI build remains
the typecheck gate.
- No provider request, new live campaign, budget change, or schedule
activation is part of this PR.
## Risks
- These are successful-attempt measurements, not a prediction of failure
frequency or a campaign invoice.
- The feedback source fingerprint changed after the samples.
Current-compatible totals remain unknown; no historical fingerprint is
rewritten.
- Hosted worker cases passed, but their original workflow failed during
report dependency setup. That outcome stays visible.
- Hosted detection deliberately accepts only the measured GitHub Ubuntu
environment. Other host labels remain unqualified until declared with
their own evidence.
- The weekly selection still has no complete hosted timing measurement.
Infrastructure, storage, and observability charges stay separate from
model spend.
## Model Used
OpenAI Codex, GPT-6 based, with tool execution and extended reasoning.
The exact serving model ID and context window are not exposed in this
session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
122 lines
8.3 KiB
TypeScript
122 lines
8.3 KiB
TypeScript
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { runnerMatrix } from "./catalog.js";
|
|
import { validateEvalOperationsManifest, type EvalOperationsManifest } from "./operations.js";
|
|
import { evalRequestReservation } from "./eval-gateway.js";
|
|
import prices from "./scheduled-prices.json" with { type: "json" };
|
|
import { scheduledOperatingOverhead, validateOperatingOverhead, type EvalOperatingOverhead } from "./operating-overhead.js";
|
|
import { usdToUnits } from "../../packages/shared/src/money.js";
|
|
import { scheduledForecast } from "./scheduled-forecast.js";
|
|
|
|
export type EvalCadence = "daily" | "weekly";
|
|
export const EVAL_SCHEDULES = { daily: "47 8 * * *", weekly: "47 9 * * 0" } as const;
|
|
const budget = (cadence: EvalCadence) => cadence === "daily" ? "20" : "100";
|
|
const marker = (profile: string) => `core-compatibility.${profile}.local.message-marker`;
|
|
const feedback = (profile: string) => `feedback-persistence.${profile}.local.complaint-and-suggestion`;
|
|
|
|
export function scheduledSelection(cadence: EvalCadence) {
|
|
if (!["daily", "weekly"].includes(cadence)) throw new Error("Unknown eval cadence");
|
|
return [marker("runner-acpx-claude"), feedback("runner-acpx-claude"),
|
|
...(cadence === "weekly" ? [marker("runner-codex"), feedback("runner-codex")] : [])];
|
|
}
|
|
|
|
export function scheduledCadence(event: string, schedule: string, requested = "manual"): EvalCadence | undefined {
|
|
if (event === "schedule") {
|
|
const found = Object.entries(EVAL_SCHEDULES).find(([, expression]) => expression === schedule);
|
|
if (!found) throw new Error("Unrecognized paid eval schedule");
|
|
return found[0] as EvalCadence;
|
|
}
|
|
if (event !== "workflow_dispatch" || !["manual", "daily", "weekly"].includes(requested)) throw new Error("Unrecognized eval trigger");
|
|
return requested === "manual" ? undefined : requested as EvalCadence;
|
|
}
|
|
|
|
export function validateScheduledBudget(input: { cadence: EvalCadence; now: number; overhead: EvalOperatingOverhead }) {
|
|
if (!Number.isFinite(input.now) || input.now < Date.parse(prices.verifiedAt) || input.now >= Date.parse(prices.expiresAt)) {
|
|
throw new Error("Scheduled eval pricing needs review before paid admission");
|
|
}
|
|
const executionIds = scheduledSelection(input.cadence);
|
|
const executions = executionIds.map(id => {
|
|
const execution = runnerMatrix.find(row => row.id === id);
|
|
if (!execution) throw new Error(`Scheduled execution is absent: ${id}`);
|
|
return execution;
|
|
});
|
|
const models = prices.models.filter(model => executions.some(execution => execution.profile.model === model.price.model)) as EvalOperationsManifest["models"];
|
|
if (executions.some(execution => !models.some(model => model.price.model === execution.profile.model
|
|
&& model.provider === (execution.profile.credential === "ANTHROPIC_API_KEY" ? "anthropic" : "openai")))) {
|
|
throw new Error("Missing exact scheduled model pricing");
|
|
}
|
|
const reserved = validateOperatingOverhead(input.overhead, budget(input.cadence));
|
|
if (input.now < Date.parse(input.overhead.verifiedAt) || input.now + 30 * 60_000 >= Date.parse(input.overhead.expiresAt)) {
|
|
throw new Error("Operating overhead allowance does not cover the campaign window");
|
|
}
|
|
if (models.some(model => reserved + usdToUnits(evalRequestReservation({ ...model, apiKey: "forecast", mode: "braintrust" })) > usdToUnits(budget(input.cadence)))) {
|
|
throw new Error("Operating overhead leaves insufficient allowance for the selected model");
|
|
}
|
|
return { executionIds, executions, models };
|
|
}
|
|
|
|
export function scheduledManifest(input: {
|
|
cadence: EvalCadence; campaignId: string; ledgerPath: string; projectId: string; now: number; runAttempt: string;
|
|
overhead: EvalOperatingOverhead;
|
|
}): EvalOperationsManifest {
|
|
if (input.runAttempt !== "1") throw new Error("Paid campaign reruns require the retained journal; a fresh allowance is refused");
|
|
const { executionIds, executions, models } = validateScheduledBudget(input);
|
|
const manifest: EvalOperationsManifest = {
|
|
version: 1, limits: { campaignId: input.campaignId, maxUsd: budget(input.cadence), overhead: input.overhead,
|
|
deadlineAt: new Date(input.now + 30 * 60_000).toISOString(), maxRequests: input.cadence === "daily" ? 30 : 60,
|
|
maxConcurrent: 1, maxAttemptsPerExecution: 1 },
|
|
ledgerPath: input.ledgerPath, executionIds, maxAttemptsPerExecution: 1, route: "braintrust", projectId: input.projectId,
|
|
models,
|
|
};
|
|
validateEvalOperationsManifest(manifest, executions, 0);
|
|
return manifest;
|
|
}
|
|
|
|
export function scheduledMatrix(cadence: EvalCadence) {
|
|
return { include: [{ executionId: "scheduled-campaign", suiteId: "core-compatibility", profileId: "runner-acpx-claude",
|
|
credentialName: "", environmentId: "local", caseId: "scheduled-campaign", timeoutMinutes: 60, needsDaytona: false }],
|
|
needsDaytona: false, executionIds: scheduledSelection(cadence) };
|
|
}
|
|
|
|
export async function prepareScheduledCampaign(env: NodeJS.ProcessEnv, now = Date.now()) {
|
|
const cadence = scheduledCadence(env.EVAL_EVENT ?? "", env.EVAL_SCHEDULE ?? "", env.EVAL_CADENCE);
|
|
if (!cadence || env.EVAL_OPERATIONS_ENABLED !== "true") throw new Error("Bounded eval operations are not enabled");
|
|
if (!/^[1-9][0-9]*$/.test(env.GITHUB_RUN_ID ?? "") || !env.GITHUB_WORKSPACE) throw new Error("Missing workflow campaign identity");
|
|
const campaignId = `gha-${env.GITHUB_RUN_ID}-1-scheduled-campaign`;
|
|
const directory = path.join(env.GITHUB_WORKSPACE, "tests/runner-e2e/results/scheduled");
|
|
const manifest = scheduledManifest({ cadence, campaignId, ledgerPath: path.join(directory, "ledger.jsonl"),
|
|
projectId: env.PAPERCLIP_E2E_BRAINTRUST_PROJECT_ID ?? "", now, runAttempt: env.GITHUB_RUN_ATTEMPT ?? "",
|
|
overhead: scheduledOperatingOverhead(env.EVAL_OPERATING_OVERHEAD_JSON, env.EVAL_TEST_RUNNER, budget(cadence), now, cadence) });
|
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
const manifestPath = path.join(directory, "manifest.json");
|
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
const forecast = { cadence, executionIds: manifest.executionIds, maxUsd: manifest.limits.maxUsd,
|
|
overhead: manifest.limits.overhead, budgetScope: "model requests plus declared operating overhead reservations",
|
|
measurements: scheduledForecast(runnerMatrix.filter(execution => manifest.executionIds.includes(execution.id)), manifest.models),
|
|
basis: "Retained attempt measurements with per-sample hosts; historical and current-compatible totals remain separate",
|
|
uncertainty: "Sample coverage, definition drift and provider export precision limit the estimate; final invoices and operating reconciliation remain pending",
|
|
maximumRequestReservations: manifest.models.map(model => ({ model: model.price.model,
|
|
usd: evalRequestReservation({ ...model, apiKey: "forecast", mode: "braintrust" }) })),
|
|
maximumMonthlyTotalAllowanceUsd: "1120", pricingExpiresAt: prices.expiresAt };
|
|
await writeFile(path.join(directory, "forecast.json"), `${JSON.stringify(forecast, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
return { manifest, manifestPath, campaignId, cadence };
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
const command = process.argv[2];
|
|
if (command === "matrix") {
|
|
const cadence = scheduledCadence(process.env.EVAL_EVENT ?? "", process.env.EVAL_SCHEDULE ?? "", process.env.EVAL_CADENCE);
|
|
if (!cadence || process.env.EVAL_OPERATIONS_ENABLED !== "true") throw new Error("Bounded eval operations are not enabled");
|
|
const now = Date.now();
|
|
const overhead = scheduledOperatingOverhead(process.env.EVAL_OPERATING_OVERHEAD_JSON, process.env.EVAL_TEST_RUNNER, budget(cadence), now, cadence);
|
|
validateScheduledBudget({ cadence, overhead, now });
|
|
console.log(JSON.stringify({ ...scheduledMatrix(cadence), overhead }));
|
|
} else if (command === "prepare") {
|
|
const prepared = await prepareScheduledCampaign(process.env);
|
|
if (!process.env.GITHUB_ENV) throw new Error("Missing workflow environment file");
|
|
const prior = await readFile(process.env.GITHUB_ENV, "utf8");
|
|
await writeFile(process.env.GITHUB_ENV, `${prior}PAPERCLIP_E2E_OPERATIONS_MANIFEST=${prepared.manifestPath}\nPAPERCLIP_E2E_CAMPAIGN_ID=${prepared.campaignId}\nPAPERCLIP_E2E_ACCOUNTING_CAMPAIGN_ID=${prepared.campaignId}\n`);
|
|
} else throw new Error("Usage: scheduled-operations.ts <matrix|prepare>");
|
|
}
|