All notable changes to Oddish are documented in this file.
The format is based on Keep a Changelog.
- The verdict now says
acceptorrejectinstead ofis_good: true/false. Stored payloads keepis_goodtoo, so old rows, the dashboard queries, and the Slack alert still work. The badge shows "Accepted" or "Rejected". - The verdict judge used to bury its hard rules inside exceptions, and it accepted a task whose own audit had found a
must_fixleak — on tests the untouched base model already passed (0.96 against a 0.25 threshold). The prompt (verdict_prompt.txt) is rewritten as two steps: first look for evidence that rejects the task by itself (a leak, weak tests, a failed baseline), and only then weigh the trials' opinions, which need agreement. - The task overview panel used to list only the current experiment's trials, but the verdict is computed over every trial of the task — so the panel could show a verdict whose deciding trial it refused to list. It now shows every trial of the version. Trials from other experiments carry a dashed "elsewhere" chip and open in a new tab. Long subtypes also stopped pushing the "View trial" button out of its row.
- The verdict badge used to hide its rerun button once a verdict existed, and the button that did exist re-classified every trial from scratch. Tasks with a verdict now show "Rerun verdict" (
qa/backfillwithforce: false), which keeps the stored trial analyses and redoes only the verdict. The full re-classify stays onqa/retry. - Submitting new trials used to delete the task's verdict immediately, and the task had no verdict until QA finished the new trials. The old verdict now stays until the new QA run replaces it.
- The cc_chat dashboard chat feature is gone end to end: the
/chat-sessionsbackend router and orchestrator, the chat drawer/button UI and its/api/chat-sessionsproxies in the frontend, theChatSession/ChatSessionEvent/ChatTurnmodels, and the chat tables themselves (dropped by backend migrationdropchat001;api_keys.is_internalstays — internal key minting also serves probe credentials and the sandbox analyzer). The chat-only settingsODDISH_CC_CHAT_DAYTONA_SNAPSHOTandODDISH_PUBLIC_API_BASE_URLare removed with it.⚠️ Deployments that set onlyODDISH_CC_CHAT_DAYTONA_SNAPSHOTmust now setODDISH_AGENT_DAYTONA_SNAPSHOT(same snapshot name) or analyzer sandboxes fall back to installing claude-code + harbor at provision time. - The shared sandbox infrastructure the chat feature grew — Daytona client,
provisioner, Claude Code runtime, stream renderer — survives because the
hosted analyzer runs on it; it moved from
backend/api/services/cc_chat/tobackend/api/services/sandbox/, and the analyzer cohort modules (analyzer_block_runner,analyzer_parse,analyzer_prompt) moved tobackend/api/services/blocks/analyzer/.
- Quota cancellation, retry, and append reconciliation no longer hide a preserved accepted verdict by leaving its payload paired with a missing status. Verdict lifecycle changes now use one state-transition module: replacement QA retains the published payload while queued/running, cancellation or a no-op restores it to
SUCCESS, and only terminal QA failure discards it. A database constraint repairs and prevents invalid payload/status pairs. - Worker heartbeats used to stop as soon as the agent finished, but the worker still had to upload and save the results. When that took over 15 minutes, the cleanup sweep marked the trial "Worker heartbeat stalled for over 15 minutes", threw away the finished result, and re-ran the whole trial. The heartbeat now runs until the results are saved and settled.
opencodetrials can now run on closed-internet tasks. Stock opencode self-installs (nvm/Node/opencode-ai) during agent SETUP, which runs under the ENVIRONMENT baseline network policy — the agent-phase allowlist (extra_allowed_hosts, runtime-host merges) only applies aroundagent.run(), so no agent-phase declaration can save a self-installing agent: the trial died at DNS during setup (curl: (6) Could not resolve host: raw.githubusercontent.com) before the model was ever reached. The fix mirrors the existing claude-code installer arm inrun_harbor_trial_async:-a opencodenow mergesOPENCODE_INSTALL_HOSTSplus the model transport host (viaoutbound_hosts_for_model, which resolvesopenrouter/tencent/hy3→openrouter.ai) intoenv_config.extra_allowed_hosts, which harbor folds into the environment baseline so the allowlist spans install and run. On legacy closed tasks ([environment] allow_internet=false→ no-network baseline for every phase, e.g. the GDM SWE-Marathon samples) this is the only channel that works at all; on modern swe-marathon-shaped tasks (public setup → restricted agent) harbor ignores baseline extras on the public baseline and the agent phase keeps its model-host-only allowlist, so no install hosts leak into agent run there._build_agent_configstill routes-a opencodethrough theOddishOpenCodewrapper. Note:required_outbound_domains— the hook two earlier revisions of this change relied on, and which several wrapper docstrings describe as "Harbor builds the Modal egress allowlist from this hook" — has no consumer in oddish or harbor; it is kept declarative-only for interface parity (both failed approaches were validated end-to-end on the PR preview backend before landing on this one).
- Enforced quotas now cancel every quota-counted nonterminal trial as soon as live or settled spend reaches the payer's rolling 24-hour cap or the organization's monthly cap. User caps stop that payer's trials; org caps stop all trials in the org, including queued and retrying work, and remote workers are terminated after the cancellation transaction commits.
- The task-level QA worker job now leases concurrency from the analysis model's queue key (
get_qa_queue_key()returnsnormalize_queue_key(analysis_model), currentlyanthropic/claude-sonnet-5) instead of the verdict model's. The bulk of a QA job's LLM work is the per-trial classification pass on the analysis model; keying the lease off the verdict model capped QA throughput at the verdict bucket's default (48) while the analysis bucket sat idle. ANALYZER jobs share the QA queue key and move with it (#802). - Raise the baked
anthropic/claude-sonnet-5queue-key concurrency override in the Modal deploy from 128 to 256, giving the relocated QA jobs and the analysis model's trials more headroom; operators can still override the whole JSON via the env var /oddish-prodsecret (#802).
- The shared analysis model (
ODDISH_ANALYSIS_MODEL— trajectory graph, trajectory summary, trial classifier, probe analysis) now defaults to Claude Sonnet 5 as the plain Anthropic-style idclaude-sonnet-5, replacing the Bedrock inference-profile idglobal.anthropic.claude-sonnet-4-6. Plain Claude ids route analysis calls to the direct Anthropic API, and the analysis queue key changes accordingly toanthropic/claude-sonnet-5(#794). - Bake a per-model
ODDISH_MODEL_CONCURRENCY_OVERRIDESdefault into the Modal deploy that raises theanthropic/claude-sonnet-5queue-key concurrency lease to 128 (up from the 48 default), giving the relocated analysis model the same headroom its predecessor queue key had; operators can still override the whole JSON via the env var /oddish-prodsecret (#795). - Bake a per-model
ODDISH_MODEL_CONCURRENCY_OVERRIDESdefault into the Modal deploy that raises theglobal.anthropic.claude-sonnet-4-6queue-key concurrency lease to 128 (up from the 48 default) — the queue key every Sonnet 4.6 trial id spelling normalizes to; operators can still override the whole JSON via the env var /oddish-prodsecret (#796).
- Dashboard queue stats no longer fold the trajectory-analysis and verdict pipeline counts into the analysis/verdict model's queue bucket. They now live under reserved
analysis/verdictqueue keys, so trials awaiting or undergoing classification can no longer masquerade as that model's queued/running trial workers (an incident showed 4k+ phantom "running" rows under one model's queue while the model's real trials were misrouted into the "analyses" pipeline). The reserved buckets report the QA job bucket's concurrency instead of a meaningless per-model default. - A QA job that dies or is cancelled mid-classification no longer strands trials in a non-terminal
analysis_status. The stale-heartbeat reap now resets the dead job's task trials inline (RETRYING →QUEUED, exhausted →FAILED), the append-supersede cancel requeues in-flight rows, and a new_reset_orphaned_trial_analysiscleanup phase heals any remaining orphans: never-classifiable rows (superseded / skipped / gate-skipped / bulk-imported trials, soft-deleted tasks, or terminal tasks with no active QA job) are finalizedFAILED, while rows a future QA attempt will re-classify are moved back toQUEUED. Previously these accumulated forever as phantom in-flight analyses. Orphan-finalized rows carry anAnalysis orphaned:sentinel prefix onanalysis_error, and resurrecting a task by appending trials reopens them so the fresh QA pass classifies them instead of inheriting a permanent verdict gap. Every reset selects its trial rowsFOR UPDATE SKIP LOCKEDso the sweep can never deadlock against the trials-then-task lock order the cancel path takes.
- A grok trial killed by an xAI rate limit (
You've hit your team's API rate limit) is no longer thrown away mid-run: the resume loop that already rescues idle-timeout deaths now also resumes rate-limited ones, sleeping first with a doubling backoff (60s, 120s, 240s) before each replay. The case was previously excluded on purpose, since an immediategrok -cre-hits the same wall — the throttle is on the account, not on one replica — but xAI's limits are refilling token buckets, so a resume that waits often lands, and a limit that never clears just fails as it did before. Idle timeouts still resume with no delay. Observed on a trial that spent 19 minutes and 437k tokens, announced its next step, and died to the limit; the truncated trajectory was then graded as a model failure rather than an infra one (#758).
- Analyzers: a new cross-experiment trajectory-analysis feature that gathers finished trials from one or more experiments and synthesizes four evidence-backed narrative sections (bad failures, good failures, universal capabilities, headroom) via a Haiku agent-team map/reduce pipeline, with inline
[trajectory](...)deep links, newanalyzersREST endpoints, anoddish analyzer createCLI, and a dashboard Analyzers tab with list/create/detail pages (#706). Analyzer pipeline logs are now prefixed with the driving job's kind (e.g.[ANALYZER]) for easier attribution in mixed worker logs (#710), and the reduce-stage prompt that produced an analyzer's sections is now persisted on the row for debugging/reproducibility, though not exposed via the API (#711). - Task pages can now promote any stored task version to be the default: a new
PUT /tasks/{task_id}/versions/{version}/defaultendpoint updates the task's current-version pointer (and legacy storage mirrors), and the task page gained a "Make default" action with an optimistic update and inline error handling (#713). - Owner-directed expense alerts (expensive experiment/trial) and a new failed-experiment alert (fires when a finished experiment has no active trials and at least half — configurable — of its current trials are FAILED) can now be DMed to the owner on Slack via
SLACK_ALERT_BOT_TOKEN, matched to their Slack account by account email, alongside the existing webhook and email channels (#703). - Oddish task, experiment, and public-share links posted in a configured Slack workspace now unfurl with outcome glyphs, run details, and a compact task-by-agent result matrix for smaller experiments, via a new signed
POST /webhooks/slack/eventsendpoint bound to one workspace/org (#700). - Trial drawer gained an adaptive Verifier Results card: test-based tasks report Common Test Report Format (CTRF) passed/failed/skipped/pending counts, benchmark-style tasks show scalar metrics, and other tasks fall back to the reward score; historical trials without a persisted summary lazily discover and parse their
verifier/ctrf.jsonartifact (#699).
- Markdown-rendered hyperlinks (analyzer reports, probe summaries, and other markdown content) now render in a theme-aware blue instead of a hard-to-recognize brown/off-white, so they read as clickable links (#712).
- The trial Live tab now shares its step, tool-call, and observation rendering with the Trajectory tab, grouping streamed events into collapsible per-turn steps where the newest step auto-expands as the previous one collapses; Claude live-tail events now carry a
turn_id/block_index/text_modeso streamed text deltas merge into one step instead of duplicating (#675).
- Analyzer reports no longer wait behind the QA backlog they share a queue with:
ANALYZERandQAjobs both land on the QA queue key, and the claim orders bypriority DESC, running_count ASC, created_at ASC— but every enqueue site leftpriorityat 0, so the first two keys tied and claims fell through to pure FIFO, stranding an analyzer behind whatever QA burst a sweep had just produced (one report waited ~59 minutes to start). Analyzer jobs now enqueue atpriority=1, so a draining worker picks them up ahead of that backlog (#744). - Non-Meta
mini-swe-agenttrials (e.g. Claude, GPT models) no longer crash deterministically on their first model call withModuleNotFoundError: orjson— thelitellm[proxy]reinstall that previously only applied to Meta-model trials now applies to all mini-swe-agent trials via a sharedOddishMiniSweAgentbase class (#714). - Experiment cost rollups now attribute spend only to a trial's home experiment (
trials.experiment_id), so collection/rollup views that render trials gathered from other experiments no longer double-count that spend on both the collection and the trial's owning experiment (#702). - Pricing lookups now walk a specificity-ordered, case-insensitive chain of model-id candidates (exact id, path suffixes, provider vocabulary aliases, spelling variants, then generic provider prefixes) instead of one-off guesses, resolving previously-unpriced production model ids while preserving provider-specific rates; token-bearing trials that still settle to an unpriced
NULLcost now emit a structuredtrial_cost_unpricedwarning to logs and Logfire (#660).
oddish preflight <path>checks local tasks for integrity problems before they cost a trial: solution/tests baked into the agent image, repo fetches or.gitdirectories that expose branch history, unjustified open internet, patch-file solutions, and brittle source-scanning anti-cheat.--jsonemits findings for CI.oddish runnow runs preflight before upload.--forcesubmits anyway and still prints the findings.oddish uploadis gated the same way (same--forceoverride) — closing a two-step bypass where uploading a leaky task directly, then running it by ID, skipped preflight entirely.
oddish runaborts when any resolved task fails preflight. Previously a broken task in a multi-task run was reported and silently skipped while the rest proceeded. Use--forcefor the old behaviour.- API key creation is now self-service for every organization, gated on the caller's role in their current org instead of membership in the hardcoded Abundant org.
can_create_api_keysno longer checks an org-slug/Clerk-org allowlist — anyadminormember(Clerk-JWT auth only) may create keys for their own org, admins mintingfull/tasks/readand members mintingtasks/read. API-key auth still cannot mint keys, and listing/revoking all org keys stays admin-only. RemovedAPI_KEY_CREATOR_ORG_SLUGS/API_KEY_CREATOR_CLERK_ORG_IDSand refreshed the stale@abundant.ai/Abundant-org wording in the settings UI, endpoint errors, and docs (#617).
- Admin cost dashboard "Cost by user" rows now link to the per-user drilldown whenever the row resolves to a real oddish user (billed user or submitting credential), even when some or all of its trials are unbilled — previously a row was only clickable when every trial was billed, so real users with pre-billing spend (e.g. created before quota billing stamping shipped) or offboarded/unlinked spend were shown as non-clickable "unbilled". A new
CostUserBreakdown.has_unbilled_spendfield drives an "unbilled" chip: on a linkable (registered-user) row the tooltip explains the drilldown counts billed spend only, so its total may be less than the row total; GitHub-handle-only / Unattributed rows that are not registered users stay non-clickable with the existing "not a registered user" wording.
- Org-wide aggregate calendar-month (UTC) spend cap, layered on top of the per-user rolling-24h cap. Admission now sums every payer's settled org spend (including unattributed NULL-billed spend) plus the org's in-flight reservation and blocks when it reaches the effective org limit (override row
org_quotas??ODDISH_DEFAULT_ORG_MONTHLY_QUOTA_USD?? none); over-cap submissions get HTTP 402 underenforceand logreason=org_over_budgetundershadow. Advisory-lock order is org → payer → row locks (ENFORCE-only, org lock only when a cap is configured). Admins set/clear the cap viaPUT /quotas/organd see month-to-date org usage onGET /quotas; any member reads the org budget snapshot plus an adaptive daily-goal via the newGET /quotas/org. Ships inert (no default, no override rows). oddish core readsorg_quotasvia rawtext()SQL to preserve the oddish→backend package boundary; per-user rolling-24h behavior is unchanged.
- Per-user quotas now use a rolling 24-hour window instead of a UTC-midnight reset.
quota_window_start()replacesstart_of_today_utc()in admission and quota usage reads, whileODDISH_DEFAULT_DAILY_QUOTA_USDkeeps its existing name and value.
- Trial-collection experiments:
oddish experiment create --name "..." <trial_id...>andPOST /experiments/collections(TASKS scope) gather existing trials into a new read-onlyis_collectionexperiment viacreate_trial_collection_core, without moving trials out of their home experiment. Membership is additive through a newexperiment_trialsjoin table (plustask_experimentsfor parent tasks); read paths — dashboard aggregates, task listing/effective-version resolution (including older task versions), export, and public/share views — treat membership as homeexperiment_idOR a gathered row via sharedexperiment_membershiphelpers, with probes still excluded from every public surface. New runs and sweep-appends targeting a collection are rejected. (First landed in #536, reverted, relanded in #552, reverted again after a prod migration deadlock, and relanded unchanged in #556 with a deadlock-safeexp_trials_join_001migration — each DDL step now runs in its ownautocommit_block, FKs are addedNOT VALIDthenVALIDATE CONSTRAINT, and alock_timeoutguards against lock-order conflicts with hot-table DML.) (#536, #552, #556) - Task browser overhaul:
browse_tasks_core/GET /tasks/browseaccept a large set of new filters — task metadata (status, priority, verdict, has-link, run-analysis/probe, created-date presets/custom range, experiments), trial-levelEXISTSfilters (agent, model, agent·model pair, provider, environment, trial status, origin, analysis classification, has-error, has-trajectory, attempts, tokens/steps/reward ranges), on-the-fly aggregate filters/sort (avg score, total tokens, trial counts, pass/partial/fail/harness buckets, run time), agent/model "Compare A vs B" and "top performer" comparisons, and anor_groupsDNF combinator for "match any of" condition groups. A newGET /tasks/browse/facetsendpoint supplies sidebar option values. The frontend tasks page moves from client-side SWR to URL-driven, server-rendered filtering with a sticky filter sidebar (draft-then-Apply for heavy fields), Suspense/skeleton loading, link-based pagination, and whole-URL (v2) saved filters. (#540) - Migration Head Guard CI workflow: a new
.github/workflows/migration-head-guard.ymlrunsalembic headson the PR's merge ref for both theoddishandbackendAlembic trees and fails if either would have more than one head after merging into main, catching staledown_revisionforks (the kind that caused a priorexperiments.is_collectionprod incident) before they reach main. (#549)
- Harbor artifact extraction is now centralized: a new
oddish.core.harbor_artifactsmodule holds shared trajectory-metrics, timing, token, reward, and error extraction from HarborTrialResults; both the live worker outcome path (workers/harbor/outcome.py) and CLI/server trial import (cli/api.py) reuse it instead of duplicating logic. Sauron trial-directory discovery now uses Harbor'sJobScanner(with legacy fallback preserved), zip-import task-name inference prefers Harbor config/result model parsing over nested legacy JSON discovery, and several edge cases were fixed along the way (worker reward propagation, multi-trial token precedence, invalid trajectory cost handling, non-numeric verifier reward handling). (#557) - Task version content hashing (
compute_task_content_hash) now hashes only execution-relevant task contents: it uses Harbor'sPackager.collect_filespublishable-file selection (respecting default ignores like__pycache__) and semantically parsestask.tomlviaTaskConfig, so descriptive[metadata]/[task]edits no longer create a new Oddish task version — only changes to runtime fields (verifier, agent, environment, steps, etc.) or other files do. (#546) - Dispatch planning is now shared across hosts: a new
build_dispatch_plan/DispatchPlaninoddish.dispatch.cyclecentralizes queue discovery, counts, held-slot accounting, concurrency limits, and spawn-plan calculation. Modal'spoll_queue(backend/worker/functions.py) and the standalone self-hostedrun_polling_worker()(now routed throughrun_dispatch_loopwith a newInProcessDispatcher) both consume the shared planner, while assigned single-queue Docker/Kubernetes workers are unchanged. The shared dispatch loop also now catches and retries on transient failures instead of propagating them. (#545, #547) - Backend router decomposition: hosted task-submission identity resolution, GitHub attribution, experiment-owner stamping, and auto-publish logic are extracted out of the large
backend/api/routers/tasks.pyinto a newtask_submission.pymodule; Claude Code's Anthropic-compatible environment setup inworkers/harbor/agent_config.pyis now shared across the OpenRouter, Fireworks, z.ai, MiniMax, and Moonshot routes instead of being duplicated per provider. (#554) - Trial
environmentis now surfaced directly in trial responses:TrialResponse.environment(including the compact experiment-page response) exposestrials.environment, added to the compact eager-loadload_onlyset inlist_tasks_core; the trial detail drawer's sandbox badge and rerun command now prefer this field over worker-job metadata, so the badge renders instantly instead of waiting onjobsdata (worker-job metadata remains the fallback for legacy rows). (#531) - Locked
harborgit dependency (rishidesai/harbor@main) advances fromaeadaf4bto2ae61e86, withHARBOR_DEFAULT_SHAand bothuv.lockfiles updated in lockstep. (#543) - Probe detail panel: the trial ID on the probe run detail view now renders as a labeled "Trial: " line (matching the existing "Preset:" styling) instead of small unlabeled muted text; the panel also no longer shows the red trial-error block when
error_messagecontains "exit 137" (SIGKILL/OOM-style termination). (#532, #534)
- Off-Modal dispatch no longer over-spawns workers:
run_dispatch_cyclenow sizes its spawn plan againstmax(running, held)perqueue_key, using a newcount_held_queue_slotshelper, instead ofworker_jobsRUNNING counts alone. An event-triggered cycle re-firing before newly-spawned workers show as RUNNING previously over-planned, causing wasted Docker/Kubernetes container churn as the extras lost thequeue_slotsacquire race and exited; Modal's productionpoll_queueis unaffected since it doesn't pass held counts. (#539) - An empty Alembic merge migration (
merge_mca01_wjtoken_heads) unified two Alembic heads left by concurrently-merged migrations, which had blockedalembic upgrade headwith "Multiple head revisions" and stalled the production Supabase DB migration deploy. (#538) - Grok Build trials no longer fail deterministically on large instructions:
OddishGrokBuild.run()now uploads the rendered task instruction into the sandbox as a file (environment.upload_file,chmod 0644) and reads it back viagrok -p "$(cat ...)"instead of inlining it into the exec argv (embedded up to three times across CLI fallbacks); large instructions previously produced exec commands exceeding Modal's 65536-byte ARG_MAX limit and failed at agent start. (#535)
- The hosted per-user probe auto-opt-in default is removed: the
users.run_probe_defaultcolumn and its submission-time hook are dropped, sorun_probeis now only ever true when explicitly requested on a submission — no per-user server-side default can silently enable probe trials. (#553)
- The unauthenticated public-experiments list endpoint (
list_public_experiments) no longer queries or returns any experiment rows — it always returns an empty list — so share tokens can no longer be discovered by enumeration; direct/public/experiments/{public_token}lookups for a token a caller already has continue to work unchanged. (#558)
- Grok Build trials are now converted into full ATIF trajectories: streamed
grok-build.jsonevents are parsed into reasoning/tool-call/tool-result/message steps with final metrics (tokens, cost) via a new agent wrapper, and the trajectory endpoint falls back to synthesizing ATIF fromagent/grok-build.jsonfor older trials that predate the conversion (#530)
- Completed Grok Build trials are now marked as having a fetchable trajectory even when the DB's
has_trajectoryflag is stale, so the dashboard no longer skips trajectory loading for them (#530)
oddish-queryCLI gains five probe-only commands:solution cat,solution fetch,verifier source,harbor src, andverify run; probe-only assets are now staged to a root-owned hidden directory (/opt/oddish-probe, viaODDISH_PROBE_STAGE_DIR) off the agent's browsable tree so the agent can't passively stumble on verifier/solution content; every command output is wrapped in aPROBE-ONLYboundary banner (carried in anotefield forverify run's JSON) so the boundary travels with the data through subagents; both local and cloud runners updated for identical container layout (#504)- CI guard (
oddish/scripts/load_only_guard.py) that statically diffs columns read on the compact/tasksresponse-builder path against columns declared inload_only(...)sets inlist_tasks_core, failing the build on any gap; prevents theMissingGreenletclass of 500 from shipping silently; triggered on PRs touchingoddish/src/oddish/**(#495)
- Probe details now open in a sliding
ResizableDrawerpanel from both the experiment trials matrix and the task probe-history table instead of navigating to a full page; a new sharedProbeDetailPanelcomponent handles self-fetching, SWR polling, on-demand artifact loading, prev/next navigation across a task's probes, and agent-process keyword filtering; the standalone/tasks/[id]/probe/[trial_id]URL continues to render a full page viaProbeDetailPanelincontentOnlymode so deep links are preserved (#505, #508) - Task Analysis card on the task page now shows the latest run of each distinct probe type as a separate labeled section (probe-type header with
agent · modelsub-line) instead of collapsing all probe runs for a version to a single newest trial; polling continues while any per-type latest run is still in-flight (#512) - Reverted experiment grid to use
GET /tasks?include_trials=trueinstead of the short-livedslim-tasksendpoint; removes the dedicatedGET /experiments/{id}/slim-tasksbackend route,GET /trials/{trial_id}single-trial detail route, and associated Next.js proxies; also reverts the Usage page Cost tab,CostingPanelSSR component, cost CSV export, and theseries_top_nadmin costs parameter (#507) - Bake a per-model
ODDISH_MODEL_CONCURRENCY_OVERRIDESdefault into the Modal deploy that raises thexai/redacted-modelqueue-key concurrency lease to 128 (up from the 48 default), giving Harbor's grok-build arm more throughput; operators can still override the whole JSON via the env var /oddish-prodsecret
- Probe trials are no longer returned by any public unauthenticated endpoint;
get_public_taskstripsis_probetrials before returning the task,list_public_experiment_tasksexcludes probes when scoping each task's trials (and now filters unconditionally regardless of experiment-id resolution), andlist_public_task_trialsalways passesprobe=Falsewith the publicprobequery parameter removed — probe data never reaches the browser regardless of UI guards (#513)
- Probe instructions now include a REFERENCE SOLUTION section when the golden/oracle solution is staged, telling the probe it may copy or adapt it into
/appas a baseline before pursuing the operator directive; omitted when nosolution/directory was staged (#500) - Probe instructions always include a SUBAGENTS section encouraging the probe to fan out parallel Task-tool subagents for independent investigation threads, noting the one-level nesting limit so all parallel work is dispatched directly (#500)
- Probe run page now shows a Preset line (between the run header and Summary section) displaying
harbor_config.probe_name, falling back to the agent name for older or preset-less runs (#502)
- Probe launch form "Instructions" field now populated from the selected skill's SKILL.md body (frontmatter stripped) instead of the legacy
operator_prompt; skills without a SKILL.md file fall back tooperator_prompt(#501) - Probe launch form fields renamed: "Extra instructions" → "Instructions", "Result focus (optional)" → "Output JSON / Result Focus" (#501)
extractSkillMdBodyextracted fromskills-client.tsxinto a new sharedfrontend/src/lib/skill-md.tsmodule, reused by both the skills editor and the probe form (#501)CLAUDE_CODE_SUBAGENT_MODELis now pinned to the main agent's normalized model id for probe claude-code trials on both cloud (agent_config.py) and local (local_runner.py) paths, ensuring Task-tool subagents have an explicit model on the direct Anthropic API path where Harbor'srun()does not set it; a pre-set value is never overridden (#500)
- Probe summaries using a
result_focusJSON Schema withoneOf(e.g. from Pydantic v2 discriminated unions) no longer fail withBadRequestError: Schema type 'oneOf' is not supported;normalize_findings_schemanow rewritesoneOf→anyOfat analysis time since the two are equivalent for constrained generation, transparently unblocking already-saved skills without operator action (#499)
- Batch probe-analysis backfill Modal script (
backend/scripts/backfill_analysis.py) that accepts comma-separated task names, finds eligible probe trials with S3 artifacts whose analysis isn'tSUCCESS, resets their analysis state, and re-runs the analyzer; dry-run by default,--executeto write; reports per-name match counts and trials skipped for having no S3 artifacts (#493) - LLM-powered
result_focusrepair (core/result_focus_repair.py): malformed-but-JSON-intendedresult_focusvalues (trailing commas, single quotes, code fences) are coerced into valid JSON via a cheap Haiku pass before driving probe analysis or being stored on a skill; falls back gracefully on any failure, leaving the original value unchanged (#492)
- Experiment owner and PR link are now stamped set-once on the experiment itself (new
experiments.owner/experiments.linkcolumns, backfilled from each experiment's earliest linked task) from the creating run's submitter; re-runs of shared tasks no longer overwrite the original experiment's provenance; dashboard and experiment detail view prefer experiment-level fields with task-derived fallback for un-backfilled rows;taskPrUrlnow preferslinkovergithub_meta.pr_url(#358) - Probe directive fields (operator prompt, evaluation metric, result focus) removed from the skill upload/edit form as they duplicate configuration handled elsewhere; the form now only shows upload folder, name, description, SKILL.md body, and additional files (#497)
- Probe summaries using a structured-output
result_focusschema no longer crash withTypeError: unexpected keyword argument 'output_config'; the field is now forwarded via the Anthropic SDK'sextra_bodyescape hatch, compatible with the pinnedanthropic==0.76.0(#493)
- Probe result cells in the experiment trials table now navigate to the probe's dedicated run page (
/tasks/{id}/probe/{trial_id}) instead of opening the trial drawer; non-probe cells are unchanged (#490)
- Admin cost breakdown dashboard tab (
/admin→ Costs) with per-window totals (24h/7d/30d/all-time), cost-over-time chart stacked by model/user/agent dimensions, and ranked tables by user, model, and experiment;GET /api/admin/costsbackend endpoint aggregates globally using nativecost_usdwhen present and per-model token estimates otherwise; cost split between native and estimated spend is surfaced per entry (#452) - Run Probe tab in the QA tab bar (
/qa/run) with a full-page task search that replaces the old "+ New probe run" dialog on the Probe Runs page; typing filters tasks, clicking one navigates to/tasks/{id}/probeto configure and launch; default landing on/qastill shows Probe Runs (#478)
- Probe presets and skills unified into a single Skills feature:
SkillModelgains optionaloperator_prompt,result_focus, andevaluation_metricdirective columns; probe form now selects a Skill (not a preset); skills mount only when explicitly selected at launch viaskill_idsrather than auto-mounting into every probe;probe_presetstable, router, and schemas fully removed;/qa/presetsredirects to/qa/skills; existing presets migrated into skills (ids preserved); 13 built-in directive and bundle seed skills seeded on fresh databases; auto-probe default repointed to thecheat-detectorseed skill (#477) - Probe trials now render as a sorted-last "Probe" agent group inside the normal trials grid on both the task detail page and the experiment matrix, scoped to the task's effective version; the separate Probe tab is removed from the experiment view; backend batch-loads effective-version probe trials and merges them into each task's trials while keeping aggregate counts (total/completed/reward) probe-free (#471)
- Probe launch buttons (task detail header, experiment trials table icon, and experiment "New probe") now navigate directly to
/tasks/{id}/probeinstead of opening an inline modal; the "Submit a probe run" CTA interstitial on the probe page is removed so the form renders immediately (#474) - Preview database schema bootstrap reverted to model-based creation (
Base.metadata.create_all+alembic stamp head) instead of running the full Alembic migration chain on rebuild; migration fingerprint removed from schema trust marker (#469)
- Production incident: every
GET /tasks(experiments page) 500-ing and all worker jobs failing withInvalidRequestError: One or more mappers failed to initialize—OrganizationModel.api_keysandUserModel.api_keyslost their join condition after #466 dropped DB-level FKs; fixed by adding explicitprimaryjoinwithforeign()annotation andviewonly=Trueon both relationships (#468) - Trials with verification disabled (
verifier.disable: true) that complete withreward=Noneno longer consume all retry attempts before failing; the worker now terminates them asSUCCESSon the first attempt; the UI shows a newscorelessstatus (slate "SCORELESS" badge, minus-circle icon) instead of treating them as perpetually pending (#462)
oddish backfill-analysisCLI command to (re)run trial analysis (LLM trajectory classification + task verdict) for an experiment, a task, or a single trial;POST /tasks/{task_id}/qa/backfillbackend endpoint on both cloud and local server withforce,enable_analysis, andtrial_idsoptions;rerun_task_qa_corerefactored to delegate to the newbackfill_task_analysis_coreprimitive (#456)- "Open task page" link button in the experiment trials table for direct navigation from the experiment view to a task's dedicated page; hidden on read-only share view since
/tasks/[id]requires authentication (#442)
- Chat button across all scopes (global
/tasksheader, per-task detail, per-experiment header) switched from outline to solid blue fill (bg-blue-600) for improved visibility (#465) - Preview database schema rebuilt by running
alembic upgrade headfor both stacks in order instead ofBase.metadata.create_all+alembic stamp head; eliminates missing DB objects (e.g.queue_runtime_status,tag_projection_sweep_state, partial unique indexes) that only exist as raw DDL in migrations and are invisible to the ORM graph; production DROP SCHEMA guard re-introduced (#460) - Preview schema trust marker now folds in a migration fingerprint (SHA256 of both stacks' Alembic head revisions) in addition to the model-graph fingerprint, so a cached schema is also invalidated when a migration is added without any ORM model change (#460, #440)
- Core package layout reorganized into focused subpackages:
oddish.core.tags(service, projection, enqueue, filter_ast, naming, permissions, policies, profanity, ownership_transfer, saved_filters),oddish.core.sharing(public, helpers, documents),oddish.core.ingest(trial_imports, zip_imports, extraction),oddish.core.probe(auto_probe, presets); workers reorganized intooddish.workers.agents(claude_code, codex) andoddish.workers.harbor(runner, ephemeral, agent_config, outcome, storage, patches, modal_debug) (#438)
api_keyscross-stack foreign keys dropped fromorg_idandcreated_by_user_idcolumns; new migrationapk01dropfkdrops constraintsIF EXISTSso production converges; fixesNoReferencedTableErrorthat prevented the oddish Alembic chain from bootstrapping independently (e.g. in the schema-parity CI job) (#466)- Probe and offline trials now get
network_mode = "public"(andallowed_hostscleared) instead of the legacyallow_internet = trueflag inenable_local_internet; fixes silent no-op on Harbor tasks that setnetwork_mode/allowed_hostsexplicitly, which caused claude-code installs to SYN-timeout (~127s, curl exit 28) and the oddish-query CLI to lose egress; applies to both local Docker and cloud Modal probe paths (#464) - Chat session provisioning (
POST /chat-sessions) no longer fails with "could not start chat" when the harbor pip install errors; harbor install is now best-effort (logs a warning and continues) since chat reads trial data through the oddish-query CLI, not the harbor package; claude-code install remains fatal (#458) uq_worker_jobs_tag_project_activepartial unique index declared onWorkerJobModel.__table_args__; prevents silent omission from model-built schemas whereON CONFLICT … DO NOTHINGindex inference for TAG_PROJECT job coalescing had nothing to infer against; no new migration (index already exists in the DB viaaa00ta01core) (#454)- Preview bootstrap
_rebuild_schemanow calls_assert_preview_branchbeforeDROP SCHEMA, refusing to proceed whenODDISH_DATABASE_URLresolves to production (matched viaSUPABASE_PROJECT_REForPREVIEW_SAMPLE_SOURCE_DB_URL) (#450) - Cloud auth migrations
a1b2c3d4e5f6andr4s5t6u7v8w9made replay-safe against schemas built from the current model graph:supabase_user_idindex creation guarded on column existence;userroleenum rebuild guarded onownerstill being an enum value; fixesSupabase DB Migrationsworkflow failing withcolumn "supabase_user_id" does not exist(#443) - Daytona dependency floor raised from
>=0.165.0to>=0.185.0; fixes Daytona trials failing with a misleadingMissingExtraErrorcaused by Harbor importingGpuTypefrom the daytona SDK, which was only added in version 0.185.0 (#439)
- Configurable per-run Harbor source via
--harbor <spec>flag (orODDISH_HARBORenv /oddish.toml[harbor]manifest): resolves the spec to a concrete commit SHA at submit time, stampstrials.harbor_shaandworker_jobs.harbor_variant_id, and executes the run on that exact Harbor version; blessed variants use digest-pinned worker images while arbitrary refs run in an ephemeral out-of-process engine (uv run --no-project --with harbor@<sha>); dispatcher routes on(queue_key, harbor_variant_id)so variants are isolated but share per-queue-key provider caps; Harbor commit shown in the trial detail drawer (#413) - Trajectory viewer keyword search bar: filters steps by message text, reasoning, tool call names/arguments, and observations; step count shows "N of M steps" while a filter is active; clicking a hidden step in the timing bar clears the filter to reveal it (#425)
trials.total_stepscolumn (nullable integer) persists the total agent trajectory step count; extracted fromtrajectory.jsonat trial completion on cloud workers, the local runner, and CLI imports; surfaced in trial API responses and model usage aggregation (#396)
- PR preview pipeline: backend stop step folded into prepare-database; Vercel and backend deploys decoupled via a deterministic Modal URL formula, enabling parallel execution;
preview_api_urlsingle-sourced as a workflow job output;stop_previous_preview_backend.shremoved (#430) - Preview database bootstrap now rebuilds the
publicschema from the combined oddish+backend model graph (Base.metadata.create_all) when the schema is untrusted (nooddish-preview:schema-built-from-basenamespace comment); trusted branches continue to runalembic upgrade headonly; re-seeds when a rebuild drops data (#429, #430) - Preview database schema is now upgraded to head on any backend deploy, not only when migration files change, so code-only PRs on reused branches cannot run against a stale schema; seeding remains gated to new branches and explicit migration runs (#424, #430)
- Preview seed sample sizes drastically reduced (random experiments 4000→8, trials per experiment 100→50, skills/documents/presets 200→10) to cut seed time; code-only pushes no longer trigger re-seeding of an already-populated branch; prepare-preview-database workflow job capped at 12 minutes (#419)
- Experiment page 500-ing with
MissingGreenleton the compact trials path:harbor_shawas not included in theload_onlyset inlist_tasks_core, causing a deferred-column lazy-load outside the async greenlet; added alongside its siblingharbor_config; CLAUDE.md updated to document the trap for future columns (#433) - Task cancellation database errors (e.g. deadlocks) now return HTTP 503 with a clear user message instead of propagating as an opaque 500 (
Internal Server Error); full diagnostic detail (sqlstate, failing SQL, traceback) logged server-side; the Next.js cancel proxy now guardsJSON.parseso a plain-text error body is forwarded correctly instead of surfacing a misleading parse exception (#421) - New preview database branches no longer replay the full Alembic migration history against an already-at-head schema: the bootstrap script reads each stack's revision from the parent database and stamps the branch to that revision before running
upgrade head, so only this PR's new migrations run (#417)
- Per-run container-registry credentials: any way a run is triggered (the
oddish runCLI's--registry-loginflag /ODDISH_DOCKERHUB_*env, the experiments-repo and harbor-forge CI workflows, a directPOST /tasks/sweepregistry_authfield, or a trial retry request) can now supply its own Docker login so the trial sandbox's inner Docker-in-Docker daemon authenticates compose image pulls — fixing multi-service tasks failing at setup with Docker Hubtoomanyrequests. The credential is per-user (never a shared Modal/oddish secret), Fernet-encrypted as it crosses the queue onworker_jobs.payload(never written totrials.harbor_config), passed todocker loginbeforecompose build/upvia the Harbor DinD shim, then logged out on teardown. The encrypted ciphertext stays on the transientworker_jobsrow while automatic retries remain possible and is scrubbed from the payload once the row reaches a terminal state. GET /experiments/{id}/trialsread endpoint returns all non-superseded trials for an experiment, gated by READ scope with org-scoped access control; trial rows now include anis_probeflag (#414)- Probe agent receives short-lived read-only API credentials and the
oddish-queryCLI on launch, enabling it to pull trial trajectories and logs on demand; a credentials-mint failure now fails the trial with a stored error rather than silently proceeding without access (#414)
oddish-queryCLI ported from Python to a dependency-free Node.js script so it works in all claude-code environments where Node is guaranteed butpython3is not; old Python CLI removed (#414)APIKeyModel,APIKeyScope, and key helpers relocated frombackend/models.pyintooddish.db.modelsandoddish.core.api_keysso workers can mint credentials;backend/models.pyre-exports both for backwards compatibility (#414)- cc_chat scope CLAUDE.md templates rewritten around the
oddish-queryCLI (experiments trials,tasks trials,trials logs); per-scope artifact file mounting (experiment_files.py,task_files.py,file_loader.py) removed in favour of query-on-demand access (#414) - Probe trials are now forced to
allow_internet=trueto give theoddish-queryCLI egress to the Oddish API (#414) - Experiment probe-launch button in the trials table is now always visible instead of appearing only on row hover;
group/task-rowhover marker removed (#400) backend/uv.locksynced to record the missingjsonschemadependency edges under theoddisheditable package, fixing dirty lockfile state on everyuvinvocation anduv sync --frozencompatibility in CI (#415)
- Codex trials on
openai/*models (e.g.openai/gpt-5.5) no longer time out with zero tokens:AzureCompatibleCodexnow implementsrequired_outbound_domainsto declare the configured Azure OpenAI endpoint host and per-trialOPENAI_BASE_URLalongside the OpenAI defaults, so Harbor's Modal egress firewall allowlists the Azure host and requests reach the model (#416)
- Automated daily changelog updated with entries for 2026-06-20 changes (#409)
POST /tasks/sweep/batchendpoint on cloud and standalone servers for submitting N task-sweeps in one request with per-item partial-success; each item runs inside its own savepoint so a failure in one item neither aborts the batch nor rolls back siblings; returns HTTP 207 Multi-Status when at least one item fails; CLI prefers the batch path and falls back to per-task only on 404/405 (#406)- Adaptive AIMD in-flight limiter for
oddish runtask submission: replaces fixed one-at-a-time concurrency with an additive-increase/multiplicative-decrease controller clamped to [4, 16]; backs off on 429/5xx, request timeouts, slow pool checkouts, and EWMA latency overshoot; S3 presigned-PUT step uses a separate smaller bound ([1, 6]) to avoid polluting the API backpressure signal; configurable via--submit-concurrencyflag orODDISH_TASK_UPLOAD_CONCURRENCYenv (#404) - Submission idempotency for
POST /tasks/sweep: CLI stamps a stableIdempotency-Key(canonical digest of experiment + task_id + sweep spec) on every sweep call; server deduplicates retried submissions and replays the stored response from a newsubmission_idempotencytable (24h TTL, unique-insert-wins savepoint); same key + different body → 409 Conflict; prevents duplicate trials on transient 5xx retries (#399) - Opt-in task-submission timing harness in
oddish/tests/perf/measuring throughput (tasks/min), per-call latency (p50/p95/max per phase), 5xx count, and client-vs-server split; skipped in CI unlessODDISH_PERFandODDISH_API_URLare both set (#391)
- Batch sweep submission now chunks payloads client-side into groups of at most
ODDISH_SWEEP_BATCH_MAX_TASKS(default 10, tunable via env) to stay under Modal's per-request ceiling; first-chunk 404/405 still falls back to per-task; later-chunk failures surface as an error to prevent double-submission after earlier chunks already committed (#407) - Task tarballing in
upload_taskis now deferred until the server returns a presigned upload URL; dedup hits (content-hash match on/tasks/upload/init) skip archiving entirely, saving CPU on re-runs where the task content hasn't changed (#390) - Sweep re-runs reconcile to exactly k trials per task in the target experiment: unchanged tasks (same version) count existing live trials and only add the shortfall; changed tasks (new version) still get a full k; the
--addopt-out flag is removed — reconcile-to-N is now unconditional (#386)
/tasks/sweepserver-side DB round-trips cut: deduplicated browse-projection recompute increate_task(was running twice — once on incomplete pre-version state, once after trials); per-rowsession.addloops replaced with a singleINSERT … SELECT unnest(…) WITH ORDINALITYstatement for both trials and worker_jobs, keeping the statement shape constant under Supavisor transaction pooling (#397)- Upload
initandcompletecalls now retry on 429/500/502/503/504 with capped exponential backoff, full jitter,Retry-Afterheader support, and a token-bucket retry budget (≤10% of requests); transient blips no longer abort a submission entirely (#397) probe_presets_001Alembic migration now addsratio_unitandratio_verbcolumns idempotently before the bulk seed insert, fixingalembic upgrade headfailures on fresh databases where000_initial_schemacreatesprobe_presetsfrom current models that no longer carry those columns (#401)
- Experiment-level probes: submit a probe whose agent can read artifacts from all trials in an experiment (per-task-balanced, up to 30 trials); "New probe" button added to the experiment Probe tab in both empty and populated states (#372)
- Probe launch button in the experiment trials table (hover icon next to version badge) and task detail header (labeled "Launch probe"), both opening the probe submit form in a dialog (#385)
- DinD Docker daemon failure diagnostics: when
dockerdfails to start in a Harbor DinD worker, its logs (tail 200), process state, memory, and disk are captured from the still-alive VM and folded intoexception.txtto aid root-cause analysis (#361)
- All claude-code trials now route to the direct Anthropic API instead of Bedrock (Bedrock credentials were unavailable); trial classifier also switched off Bedrock to the direct Anthropic API (#374, #377)
- claude-code model id now matches the transport: when Bedrock env signals are absent,
_build_agent_configemits a plain Anthropic API model id (e.g.claude-sonnet-4-6) instead of a Bedrock inference-profile id (global.anthropic.claude-sonnet-4-6), preventing HTTP 400 "Operation not allowed" errors (#359) - Probe result-focus redesigned:
result_focusis now a JSON-schema structure with enforcedResultFocusFindingsfields; action items lead instead of free-form text; ratio metric removed; full JSON summary accessible via toggle (#370, #378) - Probe result display overhauled: cheating badges, "investigation steps," and "task is gameable" tally removed from the summary panel; summary row and latest-probe card now show action-items count (
no action items/N action items · M must-fix) instead of cheat verdicts;reward 0.0fallback replaced with "awaiting analyzer" (#365, #379) - Probe transcripts no longer clipped before summarization; probe analyzer upgraded to a larger model (#364)
- Auto-probe now defaults to the Task Construction Auditor preset (#376)
- Subscription auth route (
sub/<model>prefix, OAuth token / Codex auth.json path) and theclaude-opus-4-8sub-bucket concurrency bump reverted; related config symbols, tests, and BYOC credential infrastructure removed (#371) - Modal worker container cap raised 768 → 2688; connection-budget comment updated to document the 2882/3000 worst-case client-connection estimate (#387)
- nop/oracle queue concurrency default raised 32 → 256 in both core config and the Modal deployment (#380)
- Harbor runner split into focused submodules (
harbor_agent_config.py,harbor_modal_debug.py,harbor_outcome.py,harbor_storage.py);harbor_runner.pyretained as an orchestration facade for backward-compat imports (#383) - Project repositioned as "batch execution and continuous QA for Harbor-compatible RL environments" in the landing page hero copy, README tagline, site metadata, and
pyproject.tomlkeywords (#388) - API key creation permission check extended to recognize the Abundant production Clerk org ID (
org_39ufkEqie8rLlVhoK4YMm4IMx0L) in addition to org slugs (#382)
- Cancel deadlocks with concurrent worker progress:
cancel_tasks_runsnow locks trial rows first then parent task rows (matching the worker domain-write order), and uses aWITH … FOR UPDATECTE to lock matchingworker_jobsrows before cancelling them (#393, #395) - Unresolved Git LFS pointer files are now detected before archiving and uploading a task directory;
oddish runfails fast with the affected relative paths and agit lfs pullremediation hint (#394) - Probe summary crash on token-cap-truncated analyzer JSON (#375)
- Probe build error: second result-focus panel now rendered via
ResultFocusFindings(#373) - Experiment agent and model icons: agent-harness logos shown in experiment table column headers; model logos used for model rows and pass@k legend;
/separator replaces@in model-scoped experiment labels (#381)
- Official provider logo assets: Gemini, Kimi, and MiniMax now use official SVG assets (
google-gemini.svg,kimi-k-only.svg,minimax-vertical.svg) instead of third-party icon library glyphs; a sharedProviderLogoImagecomponent consolidates the rendering (#355) - Harbor pin updated to
beabbb7apicking up the agent-tools image update that bakesripgrepinto closed-internet Codex trials so runtime installs are skipped whencodexandrgare already prebaked (#357) sub/claude-opus-4-8deploy-time concurrency raised 4 → 8 inmodal-deploy.yml, roughly halving wall-clock for the claude-code eval arm running against the OAuth-multiplexed Claude Max subscription (#352)- Task and trial ID columns widened:
tasks.idVARCHAR(64→128),task_versions.idVARCHAR(128→160),trials.idVARCHAR(128→160), and all FK references widened correspondingly; Alembic migrationlong_task_ids_001handles the resize underACCESS EXCLUSIVElock with FK rebuild (#354) - Org role model simplified to
admin/member; the legacyownerrole is removed (existing owners promoted toadminvia migrationr4s5t6u7v8w9) and API key creation is now gated to admins with an@abundant.aiemail in the Abundant org; newGET /api-keys/permissionsendpoint reports whether the current user may create keys (#170) - CI preview pipeline now computes component diffs via the GitHub compare API instead of
dorny/paths-filter, which ignored itsbaseinput onpull_requestevents and always diffed the whole PR, forcing unnecessary ~15-min Supabase DB seed runs on frontend-only pushes (#346)
- Experiment-scope chat sessions now mount the jobs artifact tree inside the Daytona sandbox; previously the
experimentbranch of_resolve_scope_inputsleftfilesempty so the sandbox only receivedCLAUDE.mdand the agent reported "(no trial data available yet)"; newcollect_experiment_filesmirrorscollect_task_version_filesand uploads artifacts atjobs/{experiment_id}/{trial_id}/…as the CLAUDE.md template promises (#347)
- Fireworks routing: GLM / MiniMax / Kimi (and other open models) can run on the stock
claude-codeagent via Fireworks' single Anthropic-compatible endpoint. Opt in with an explicitfireworks/(orfw/) prefix (e.g.fireworks/glm-5.2,fireworks/minimax-m3,fireworks/kimi-k2.7-code), which gets its ownfireworks/<id>provider/queue bucket and authenticates with${FIREWORKS_API_KEY}; bareglm/minimax/kimiids keep their existing direct-provider routes. Default claude-code settings (no forced thinking/effort) - Global chat scope (
global) with anoddish-queryread-only CLI injected into the sandbox; the agent can search, inspect, and drill into trial logs across all org tasks; a short-lived internal API key is minted per-session (45-min TTL) for credential isolation; global-scope Chat button added to the tasks page (#332)
- Chat sandbox provisioning now supports an optional pre-baked Daytona snapshot (
ODDISH_CC_CHAT_DAYTONA_SNAPSHOT):ClaudeCodeRuntime.install()skips tools already present and installs any missing tools concurrently instead of sequentially, reducing provisioning from ~1 min to a few seconds when a snapshot is configured (#340) - API container concurrency reduced from 8 → 3 and max containers raised 24 → 64 (peak throughput unchanged at 192); experiment-scoped
GET /tasksnow loads only the requested experiment's non-probe trials in SQL instead of fetching every trial for each task and filtering in Python;GET /taskslimit parameter capped at 2000 (#337)
- Chat session creation (
POST /chat-sessions) returned 500 in prod because the Daytona region only permits ephemeral sandboxes;create_sandboxnow passesephemeral=True(#333, #339) - Chat messages returned
session_not_foundwhen the API routed the request to a different autoscaled container than the one that provisioned the session; sandbox handles are now reconnected from the DB-persistedsandbox_idso any container can serve any session (#334, #339) - First chat message showed nothing for ~10 seconds during sandbox provisioning and the composer stayed live, allowing a second send to race in; user bubble is now echoed and composer locked before
ensureSession()runs (#335) - Per-task Chat button showed "no trial data available yet" because it used the stub
task_probesscope; button now uses thetaskscope with the full trial tree staged per version; probe trials are marked(probe)in the agent'sCLAUDE.mdwith a "Regular runs vs probe runs" explanation (#336) GET /chat-sessions(chat history list) took 19–43 s under prod DB load; a composite index on(org_id, scope_kind, scope_id, last_activity desc)now serves the filter and sort directly, and turn counts are batched into one grouped query instead of a correlated subquery per row (#338)- Claude Code ran headless (
--print) without a permission flag, so every tool call (Bash, Read, etc.) blocked on an approval gate nothing could answer;--permission-mode bypassPermissionsis now passed on both the chat (stream_chat) and probe (run_once) launch paths (#341) - After ~30 min idle, Daytona auto-stops and deletes the ephemeral sandbox; the next message raised
session_not_found;send()now transparently self-heals by callingresume()to re-provision the sandbox and restore from the per-turn archive (#342) - Container startup sweep (
sweep_orphan_chat_sessions) ran on every API container start and marked all active chats broken — since the API autoscales across many containers with no session affinity, any new container (autoscale-up or deploy rollout) was killing every live chat globally; startup sweep removed from lifespan; recovery is now lazy (reconnect bysandbox_id, self-heal viaresume(), Daytona idle auto-stop as backstop) (#343)
- Claude Code chat sessions (Phase 1): durable
chat_session_eventsappend-only log andchat_turnstable (one-running-turn-per-session enforced by a partial unique index) with a full orchestration engine — Daytona sandbox provisioner, Claude Code runtime, idle reaper, and restart sweep that marks orphaned running turnsfailedwhile preserving the event log; API routesPOST /chat-sessions,GET /chat-sessions/{id}, SSEPOST /chat-sessions/{id}/messages, events-replayGET /chat-sessions/{id}/events?since=<seq>, andDELETE /chat-sessions/{id}; sessions survive page refresh and backend container restarts (#306) - Chat
taskscope (Phase 2a): chat sessions scoped to a task download trial log files from S3 and upload them into the Daytona sandbox asjobs/v{version}/{trial_id}/…(byte-capped at 50 MB); a version-awareCLAUDE.mdhighlights the current version as the default focus and de-emphasizes past versions (#307) - Task detail page now shows a "Probe runs" card for the selected version: the latest probe run's agent/preset name, run status, cheat/blocked/neutral result, and prioritized action items from the analyzer; auto-polls while the probe is in-flight and links to the full probe result page (#310)
- Admin
GET /queue-healthendpoint and dashboard overview card exposing throughput, per-queue-key capacity fill, and persisted dispatcher/reconciler heartbeats so operators can self-diagnose "queued but not running" without querying psql or Modal logs; backed by a newqueue_runtime_statustable written at the end of each dispatcher/reconciler cycle (#312)
- Probe submit page now shows a prominent "Submit a probe run" button that expands to reveal the agent picker and form on click (previously the form rendered inline on agent selection); probe history list sorted newest-first; task ID on the probe run detail page rendered as a clickable link to the experiment page (#313)
- Modal function CPU/memory resource floors now configurable via env vars (
ODDISH_MODAL_API_CPU/MEMORY_MB,ODDISH_MODAL_WORKER_CPU/MEMORY_MB,ODDISH_MODAL_DISPATCHER_CPU/MEMORY_MB,ODDISH_MODAL_RECONCILER_CPU/MEMORY_MB); API defaults to 2 CPU / 4 GiB (was unconstrained fractional-core), reducing latency spikes under concurrent load;WORKER_MAX_CONTAINERSraised 320 → 448 (#312) - Probe submit form converted to shadcn
Button,Input, andSelectcomponents; unused frontend exports flagged by knip removed; pre-commit hooks (ruff, black, mypy, prettier) pass cleanly across the full repo; dead code removed:TrialClassifier.classify_trialsbatch method andAUTO_PROBE_INSTRUCTIONSconstant (#311)
- Worker dispatcher no longer starved by a slow or deadlocking reconciliation sweep:
reconcile_queue_statenow runs as its own dedicated Modal scheduled function (240s interval, 600s timeout) instead of inline insidepoll_queue; a SIGKILL mid-sweep previously left orphanedidle in transactionlocks that deadlocked the next sweep cycle and spawned zero workers;poll_queuenow only discovers queue keys and spawns workers, withMAX_WORKERS_PER_POLLraised 64 → 128 (#309)
- Copy button in the task file viewer header copies the raw file content to clipboard with a 2-second check-icon confirmation; resets on file switch and cleans up its timeout on unmount (#299)
- Trajectory analysis is now a single task-level
QAworker job instead of one classification job per trial plus a separate verdict job: when every trial of arun_analysistask finishes, oneQAjob classifies all live trials (unchanged taxonomy/evidence/reasoning, still written totrials.analysis) and then synthesizes the task verdict (tasks.verdict), so a sweep ofTtasks ×Ntrials enqueuesTjobs instead ofT × (N + 1). The whole surface uses one "QA" concept: worker-job kindQA(migrationqa01/qa02adds it and repoints oldVERDICTrows;ANALYSIS/VERDICTremain only as legacy enum values for historical/in-flight rows), onerun_task_qa_jobhandler, one set of endpoints (POST /tasks/{id}/qa/retry,POST /tasks/{id}/qa/cancel), one CLI surface (oddish run --retry --qa,oddish cancel --qa), and one dashboard control (Run QA / Cancel QA). The per-trial analysis and separate verdict retry/cancel endpoints, CLI flags (--analysis/--verdict), and UI buttons were removed (#315) (#315) - Probe agent container now receives the full staged task directory via a Harbor
AGENT_STARThook, with all probe-only material (tests/,solution/,harbor_src/,related_trials/,AGENT_BRIEF.md) staged under/probe-harness/instead of/app, keeping the real agent's workspace pristine; probe instruction reframes the task spec as a "REAL AGENT BRIEF" with an auto-generated visibility map, eliminating false-positive vulnerability reports for files the real agent cannot access (#300, #301) - Probe analyzer prompt gains a SCOPE section instructing it not to emit recommendations premised on probe-only paths (under
/probe-harness/) being agent-reachable, and to preserve the probe agent's own hedges rather than upgrading them tomust_fix(#301) - Harbor bumped to
07a576944picking up MiniMax M3 and Kimi K2.7 long-run hardening: streaming/timeout env vars (API_TIMEOUT_MS=3.6M, idle stream timeout, eager flush, max output tokens), Claude Code pinned to2.1.167(fixes MiniMax exit-137 mid-stream stalls), and plan-mode tools (EnterPlanMode,ExitPlanMode,AskUserQuestion) disabled for Kimi variants (fixes K2.7 plan-mode no-op bail) (#303) - QA Probe Runs listing (
/qa/runs) now aggregates in SQL using window functions — one row per task — rather than fetching all probe trial rows and folding them in Python; backed by a new partial index on(org_id, task_id, created_at DESC) WHERE is_probe, making load time scale with tasks-per-org rather than total probe trial count (#286)
- Advanced free-text grammar for the task browser search box: space-separated terms AND together in any order,
"quoted text"matches as a contiguous phrase, a leading-(or uppercaseNOT) excludes a term, and uppercaseORmakes either side of a group match; a?icon inside the input opens a syntax cheatsheet tooltip;parse_search_querylives inoddish/core/helpers.pyso the dashboard, standalone server, and cloud API all share one grammar; LIKE metacharacters remain escaped as literals (preserving #285 semantics); needles capped at 16 (#295) - "Delete tag for everyone…" action in the tag chip editor: an inline confirm panel shows the tag's current name (refreshed after 409 races) and its direct-assignment count before the destructive click; sends
cascade=trueto flip ACTIVE assignments;onDeleteddrops the chip locally without a redundant unassign call;DELETEpassthrough added to the/api/tags/[tag_id]Next.js proxy route (#293)
- Bake per-model
ODDISH_MODEL_CONCURRENCY_OVERRIDESdefaults into the Modal deploy that raise theglobal.anthropic.claude-haiku-4-5-20251001-v1:0(also the analysis-model queue key) andopenai/gpt-5.4-miniqueue-key concurrency leases to 128 (up from the 48 default); trajectory analysis gets more headroom; operators can still override the whole JSON via the env var /oddish-prodsecret (#297)
DELETE /tags/{id}backend route now accepts a?cascade=query parameter so callers can consent to flipping ACTIVE assignments to REMOVED; previously the flag was unreachable from HTTP and tag deletion always failed for any still-assigned tag (#293)
- Task page now lists all affiliated experiments as linked chips (dot-separated) instead of just the primary one; public share view only exposes public experiment names to prevent private experiment name leakage, and
GET /public/tasks/{id}?include_trials=falseno longer 500s for tasks in multiple public experiments (#288) - Tag chips on dashboard experiment rows and
tag:/-tag:/OR/NOTfilter syntax in the experiments search box, matching the existing/tasksgrammar; tag chips hydrated in a single batch query per page with graceful degradation on failure (#291) - Admin "Tag Policy" tab now fully functional: numeric limits, who-can-create and profanity-mode toggles, comma-list editors for reserved prefixes and allow/deny lists, and a 403 → "Admins only" error state (#291)
- Probe summary now includes a prioritized "Action items" block with
must_fix/should_fix/optionalrecommendations, color-coded and sorted by severity; an empty probe returns a "No fixes needed — task held up to probing" confirmation; legacy probe rows without the field render nothing (#284) - Probe trials now upload the staged task directory (including
related_trials/,harbor_src/,tests/,solution/) into the agent container at start time via a HarborAGENT_STARThook, so the agent can actually access the reward-hack surface the probe instruction references (#282) - Skill create/edit form gains an "Upload folder" button that reads a
SKILL.mdplus supporting files and auto-fills the form; strips dotfiles and binary files; shows a "skipped N files" notice for anything dropped (#281) - Saved-filter bookmark menu beside the tasks search bar: lists org-shared and private saved filters, applies one as
tag:search text, and saves the current query under a name with Private/Org visibility; filters persist stable tag IDs so they survive renames and merges; deletes are optimistic with SWR rollback on failure (#280)
- Dashboard "Avg score" column and experiment page KPI tile now use a task-weighted average (mean over tasks of per-task mean reward) with nop/oracle baselines excluded everywhere; backend computes and returns this as a new
avg_scorefield; a loading spinner is shown on the KPI tile while trial pages are still streaming in; both surfaces explain the calculation on hover (#292) - Tag mutations (assign, unassign, delete, archive, merge, set-visibility) now invalidate the dashboard cache, so tag chip and filter changes appear immediately without waiting for the 30-second TTL (#291)
- Probe runs no longer pollute the task browser: trial counts, reward stats, experiment chips, and
last_run_at(which drives page ordering) now all excludeis_probetrials, consistent with probes having their own tab (#285) - LIKE wildcards in task browser and document search are now escaped as literals: searching
_no longer matches every task, and%and\behave as plain characters (#285) - Browse page query performance improved: the org-wide trial aggregate now computes only the
max(activity)needed for ordering; per-task counters are fetched as a separate targeted query over the visible page only, reducing latency ~40% at prod volume (#285) GET /tasksno longer intermittently 500s withMissingGreenlet;TrialModel.is_probeadded to the compact-trialsload_onlyallowlist so it is loaded eagerly on the async path (#283)- Probe summarizer no longer reports "received no output" for skills: user-turn text blocks (the mechanism claude-code uses to deliver skill bodies) are now captured as
injected_contextso the summarizer sees the full skill content (#289)
- QA tab consolidating probe runs, presets, skills, and documents under a new
/qaroute group; includes a Probe Runs list (one row per task with probe activity, ordered most-recent-first), a full Presets CRUD management page, and a new org-wideGET /probesbackend endpoint; old/skillsand/documentsroutes redirect transparently to their new/qa/*homes (#266) - Probe run summary accessible from the experiment task-file drawer sidebar: a "Latest probe run" entry below the file tree opens a compact card with status, agent/model, headline, metric chips, and cheating verdict; links to the full probe detail page; hidden on public share view; auto-polls while the probe is still running (#265)
- Auto-probe is now opt-in and off by default;
maybe_enqueue_auto_probepreviously fired unconditionally on every sweep; now gated on a newrun_probe: boolfield onTaskSubmission,TaskSweepSubmission, andTaskStatusResponse, arun_probeDB column with migrationrun_probe_001, and a--run-probeCLI flag onoddish run; append-mode submissions flip the flag on first opt-in, mirroring the existingrun_analysispattern (#271) - Org switcher moved from Settings > Workspace into the top nav bar so the active workspace is always visible and switchable; settings page retains a read-only current-workspace card with updated copy; SWR cache is flushed on org change to prevent stale data from the previous workspace leaking through (#274)
- Nav "QA" label renamed to "Agents" (route, active-state checks, and
QA Verdict/QA Reviewstrings elsewhere are unchanged) (#270) - Probe run detail page and experiment task-drawer probe card now share a single
ProbeRunSummarycomponent, bringing the drawer card to full parity with the detail page; the card previously omittedkey_actionsandtool_insightssections (#269) - Preview environments now seeded with a pseudo-random, deterministic subset of real production data (rows drawn by
md5(id || PR_NUMBER)) instead of curated fixtures; reviewers authenticate with their real org credentials in preview; in-flight tasks/trials are normalized toFAILEDon import to prevent the preview's safety nets from enqueuing real analysis/verdict jobs; convergence tracked via a private_preview_seed_statetable; per-row savepoints prevent a single constraint collision from aborting the run (#264)
- Probe trials no longer appear in the experiment trial matrix; filtered server-side in the
experiment_idbranch oflist_tasksbefore version resolution, preventing probe runs from cluttering the main task grid and preventing a probe-only version from skewing the effective version display; public/public/tasks/{task_id}/trialsendpoint gains an optional?probe=filter param (true/false/omit) (#276) ?task=deep links on the experiment page now fall back to matching by task name when no exact task ID is found, fixing hand-written links such as?task=ghsa-rpfr-x88x-xwcwthat previously opened nothing (#275)- Task version badges (
v{n}) in the public experiment table are now hidden for unauthenticated viewers;oddish runreproduction command in the trial drawer hidden on public share pages; timing row in the trial drawer now shows the viewer's local timezone abbreviation (e.g.PDT) (#277) - GLM/z.ai provider icon corrected from the ChatGLM mammoth glyph to the ZAI "Z" logo (#277)
- Footer "by Abundant AI" link updated from
abundantdata.comtoabundant.ai(#277)
- Auto-enqueue one probe trial per task version on sweep submit using round-robin model rotation; new
GET /experiments/{experiment_id}/probesendpoint lists the latest probe trial per task; experiment page fetches and displays the default probe preset (#248) - Dashboard experiment owner filter (Org / Mine / admin member picker) via
experiments_authorparameter on/dashboard; defaults the Recent Experiments table to the signed-in user's experiments; legacy rows matched via GitHub username and email for accurate attribution (#214) - Self-healing experiments owner backfill runs on every queue-poll tick, converging
owner_user_idso the dashboard Mine filter stays on its indexed fast path; adaptive filter uses a pure indexedowner_user_idseek once all experiments in the org are attributed, with an__unattributed__sentinel for unowned rows (#255) ShareNavcomponent for public experiment share pages with Oddish logo, "by Abundant AI" branding, and theme toggle; replaces the full appNavon share pages (#260)- Cursor provider icon support:
cursorandcursor-cliagents and thecomposer-*model family now resolve to the Cursor icon in the queue key icon display (#260) - Claude Fable 5 mapped to Bedrock global cross-region inference profile (
global.anthropic.claude-fable-5); note this is a Covered Model requiring AWS account data retention mode set toprovider_data_share(#254)
- Pass@k graph and leaderboard default to visible on public experiment share pages; authenticated views are unchanged (#256)
- Tasks/Probe tab toggle hidden on public share view — probing requires an authenticated session (#260)
- OpenRouter models (Gemini 2.5 Flash, DeepSeek Chat) removed from probe model rotation; only
claude-haiku-4-5remains (#253)
- Pass@k calculation now uses per-agent per-task attempt count instead of the global maximum, fixing incorrect curves when agents in the same experiment have different trial counts (e.g. oracle with 1 trial shown as
33%/67%/100%instead of its observed flat rate) (#258) - Dashboard Mine filter no longer degenerates into a near-full-table scan; an adaptive indexed owner seek is used once
owner_user_idis backfilled; attribution profiles are served from cache instantly with background refresh; frontend keeps existing rows visible during polling revalidation instead of blanking to "Loading…" (#255) - GitHub PR branch link (experiment PR chip) hidden on public share view to avoid exposing internal repository context to anonymous viewers (#261)
skills_001anddocuments_001Alembic migrations made idempotent for data-less preview branches, fixing "relation already exists" errors that caused PR preview deploys to fail atalembic upgrade head(#251)
- Skills library and agent doc store: org-scoped skills with CRUD, a frontend Skills page, probe sandbox injection (
.claude/skills/via overlay), and HarborAgentConfig.skillsdelivery; a document library with text/markdown/PDF/CSV ingestion, LLM digest generation (summary + tags), and keyword+tag search;oddish-docstoreMCP server exposessearch,get, andinspecttools; Skills and Documents added to the app nav (#217) is_probeboolean column ontrialswith migration and backfill fromharbor_config->>'mode';?probe=true/falsefilter onGET /tasks/{id}/trials; probe history table now filters server-side (?probe=true) instead of client-side; inline probe summary generated in the cloud trial handler after each probe trial completes (#231)- Probe skill and MCP tool usage captured as structured signal in probe artifacts:
_classify_tool_usetags every transcripttool_useentry asskill,mcp, orbuiltin;_summarize_tool_usageproduces a deterministictool_usageroll-up (skill slugs and MCP server/tool pairs, ordered by first appearance) surfaced underextract_probe_artifacts(#244) - Probe summary "Tools & skills used" section: when an agent invoked skills or MCP servers,
run_probe_analyzerappends per-tooltool_insightsentries (name, kind, one-sentence note grounded in the transcript); rendered as a labeled list withSkill/MCPchips on the probe result page (#245)
- PR preview databases switched from prod-clone (
--with-data) to data-less Supabase branches populated by a deterministic seed (backend/preview_seed.py): reflection-driven, idempotent and convergent (upsert + full-PK reconcile), spanning both Alembic stacks without ORM imports;seed-gateCI job validates schema/seed drift on every PR; preview branch readiness deadline cut from 20 min to 5 min (#243) - Harbor dependency bumped to 0.13.1 (from 0.8.0), adding the
glm-claude-codeagent (z.ai base URL,ZAI_API_KEYauth, recommended streaming env, Claude Code version pin 2.1.167), closed-internet IPv4 fixes forapi.z.ai, and harbor-framework v0.13.1 (#247) Settingsnow auto-loads.env.locallayered over.envfor local backend development; later file wins on duplicate keys, exported env vars still outrank both (#230)
- Probe result page now shows agent transcript and verifier output for cloud trials; cloud runs do not inline
_artifactsintotrial.result, so a newGET /trials/{id}/probe-artifactsendpoint and BFF proxy download artifacts on demand from object storage and cache them for finished trials (#227) - Cloud probe summary no longer fails with auth errors (
PermissionDeniedError/TypeError); the probe analyzer now always runs on the direct Anthropic API (ANTHROPIC_API_KEY) instead of Bedrock; Bedrock inference-profile model IDs are normalized to their plain API form via newto_anthropic_api_model_id()helper (#236) - Cloud probe summary
NameErrorfixed:extract_probe_artifactsandrun_probe_analyzerimports were missing fromtrial_handler.py, causing every cloud probe run's inline summary to fail withNameError: name 'extract_probe_artifacts' is not defined(#232) - Experiment page no longer returns 500
MissingGreenleterrors for tasks with trials;TrialModel.harbor_configandTrialModel.is_probeadded to the compact-trialsload_onlyallowlist so deferred JSONB columns are not lazy-loaded outside the async greenlet (#228, #235) - Preview branches stuck in
RESTORE_FAILED,INIT_FAILED, orPAUSE_FAILEDstates are now detected immediately and torn down for recreation instead of polling until the 20-minute deadline; deadline timeouts also trigger delete-and-recreate; retry budget raised to 3; seed reclaimsclerk_org_idfrom pre-existing rows on reused branches to avoid duplicate-key errors (#224)
- GLM / z.ai routing for the
claude-codeharness: GLM models (zai/glm-x-preview[1m], bareglm-..., orz-ai//z.ai/prefixes) canonicalize to azai/<id>id so they get their ownzaiprovider andzai/<id>queue bucket instead of inheriting claude-code's fixed Bedrock provider/queue (keeping GLM trials from contending with Bedrock traffic for concurrency slots);harbor_runnerinjects the z.ai Anthropic-skin env (ANTHROPIC_BASE_URL,ANTHROPIC_AUTH_TOKEN=${ZAI_API_KEY}, model + size aliases, z.ai's recommended long-context settings) and blanks the ambient Bedrock/Anthropic credentials so the z.ai route wins oddish probeCLI command with full cloud probe agent support: probe presets stored in Postgres with CRUD backend endpoints and UI on task pages; probe trials bypass stricttask.tomltimeout validation and use a capped 30-minute agent timeout; local and cloud runners share the same probe implementation (#218)
- Experiment trials table on the experiment page now defaults to A→Z task name sort instead of insertion order; the sort header toggle still cycles through all options (#220)
- Probe UI no longer shows "Failed to fetch" CORS errors; browser fetches for probe presets and sweep submissions moved from direct cross-origin backend calls to the same-origin Next.js BFF proxy (
/api/probe-presets,/api/tasks/sweep), removing CORS as a failure point; reverts the hardcoded prod-origin CORS allowlist from #221 since it is no longer needed (#222) - Probe trials on tasks whose
task.tomlomits agent timeout settings no longer hard-fail validation; a sharedPROBE_AGENT_TIMEOUT_SECconstant (default 30 min, overridable viaODDISH_PROBE_AGENT_TIMEOUT_SEC) is applied by both local and cloud runners (#225) - PR lineage badges now render consistently across dashboard, experiment page, task detail header, and task browser cards; fixed blank badge on the experiment page caused by
TaskModel.linkmissing from thecompact_trialsload_onlyset; fixed missing badge on task cards when the PR URL lived only ingithub_meta.pr_url; added sharedtaskPrUrl(link, github_meta)resolver inlib/utils.ts; dashboard PR column now falls back to thelinkcolumn whengithub_metais absent (#197, #223) - Fresh-database
alembic upgrade headno longer fails with duplicate column or foreign-key errors; three incremental migrations (add_column last_activity_at,fk_tasks_current_version_id,fk_trials_experiment_id) are now idempotent; a merge migration joins the two divergent heads into a single chain (#215) - Resolved Alembic double-head in the oddish migration chain caused by
probe_presets_001anddispatch_logbranching from the same migration tip;probe_presets_001re-parented ontoc1d2e3f4a5b6to restore linear history (#219)
- Repository dispatch emitter (
github/dispatch.py) andexperiment_dispatch_logtable dropped; therepository_dispatchevent path requiresContents:writepermission that the production token lacks (#211)
- Fire a
repository_dispatchwebhook to consumer repos when all tasks in an experiment reach a terminal state; anexperiment_dispatch_logtable provides idempotent single-fire semantics; dispatch target and event type are read fromgithub_meta.dispatchon task tags; gated byGITHUB_DISPATCH_ALLOWED_REPOSallowlist and authenticated viaGITHUB_DISPATCH_TOKEN(falls back toGITHUB_TOKEN) (#206)
- Bake a per-model
ODDISH_MODEL_CONCURRENCY_OVERRIDESdefault into the Modal deploy that raises thegoogle/gemini-3.5-flashqueue-key concurrency lease to 128 (up from the 48 default); operators can still override the whole JSON via the env var /oddish-prodsecret (#213) - Raise
ODDISH_DEFAULT_MODEL_CONCURRENCYfallback from 32 to 48, increasing per-model queue-key concurrency in the Modal runtime without changing the per-poll spawn cap (#208) - Experiment trials table tooltip for truncated task names now shows the full task name instead of the generic "View task files" label, with responsive max-width and word-break styling for long names (#207)
- Automated daily changelog updated with entries for 2026-06-06 changes (#202)
- Trial detail panel now shows a "Sandbox" button linking to the Daytona dashboard when a trial has an associated Daytona worker job;
providerandexternal_idfields exposed in the worker job API response to enable this (#190)
- Codex workers running against Azure OpenAI endpoints no longer fail with 302 errors from the websocket Responses route; a new
AzureCompatibleCodexrunner disables theunified_execwebsocket transport and injects an HTTP-only OpenAI-compatible provider config; trajectory is recovered from stdout JSONL as a fallback when the Codex session file is sparse (#193) enqueue_analysis_worker_jobnow skips enqueueing analysis for trials with no stored result (neither S3 key nor local path), immediately marking analysisFAILEDinstead of burning all 6 retries on a doomed job; a staleness-gated cleanup backstop finalizesANALYZINGtasks with no live trials and cancels their dangling queuedANALYSISworker jobs (#196)- Stuck-
ANALYZINGcleanup pass rescoped to correctly target tasks whose live trials haveanalysis_status = NULL(analysis was never enqueued) rather than tasks with no live trials at all; NULL analysis statuses are now marked terminal somaybe_start_verdict_stagecan advance the task toVERDICT_PENDINGinstead of leaving it indefinitely blocked; tasks with no live trials are still finalizedFAILED(#200) - Worker containers now drain short-job queues by claiming and running multiple jobs back-to-back on their held slot until the queue empties or a wall-clock budget (
ODDISH_MODAL_WORKER_BATCH_BUDGET_SECONDS, default 300s) expires; lifts utilization for analysis (~54s), verdict (~9s), and nop-oracle (~46s) queues toward 100% without changing global spawn rates or concurrency limits; long agent trials exceed the budget on the first job and continue to run one-per-container (#201)
oddish cancelgains--analysisand--verdictflags to cancel active analysis or verdict jobs independently without stopping unrelated trials; new API endpointsPOST /tasks/{task_id}/analysis/cancel,POST /tasks/{task_id}/verdict/cancel, andPOST /trials/{trial_id}/analysis/cancel; dashboard adds per-trial, bulk-selection, and task-detail cancellation controls for both stages (#189)oddish run --link <url>attaches a source URL (PR, issue, or CI run) to a task at submission time; auto-derived from--github-metapr_urlwhen--linkis omitted; displayed in the task detail page header; re-runs update the link when a new value is provided and leave it unchanged when none is given (#178)
- Daytona sandbox creation no longer fails with "Only ephemeral sandboxes are permitted in this region"; a new
daytona_ephemeralsetting (defaultTrue) causes harbor trials to request ephemeral sandboxes, matching the Daytona region's configuration; harbor pin bumped to include matching ephemeral sandbox support (#188) - GitHub PR comment now auto-updates as trials, analyses, and verdicts complete; the previous implementation nested two DB sessions inside
notify_trial_update,notify_analysis_update, andnotify_verdict_update, deadlocking the worker's size-1 connection pool before the GitHub write was reached (#187) alembic upgrade headno longer fails with "Multiple head revisions are present"; a merge migration (74a0eab3e564) joins the divergentprovider/external_idandtask_linkheads into a single unified head (#186)
oddish runno longer crashes when a task'stask.tomlomits thegpusfield;_task_config_requests_gpunow treats an absentgpusas 0 GPUs instead of raisingTypeError: '>' not supported between 'NoneType' and 'int'(#181)- Claude Code workers using an OpenRouter model now receive the correct Anthropic-skin environment (
ANTHROPIC_BASE_URLpointing to the OpenRouter endpoint,ANTHROPIC_AUTH_TOKENset to${OPENROUTER_API_KEY}); conflicting Bedrock and direct-Anthropic ambient credentials are blanked so the OpenRouter route takes effect (#175)
- OpenAI-family workers now route through Azure OpenAI by default;
ODDISH_OPENAI_PROVIDER(defaultazure) selects the transport, withAZURE_OPENAI_API_KEY,AZURE_OPENAI_ENDPOINT,AZURE_OPENAI_API_VERSION, andODDISH_AZURE_OPENAI_DEPLOYMENTSfor per-model deployment mapping; setODDISH_OPENAI_PROVIDER=openaito use the public OpenAI API instead - Daytona sandboxes now auto-stop (30 min) and auto-delete (60 min) as a backstop for sandboxes that escape explicit teardown;
worker_jobsgainsprovider/external_idcolumns and cancel/orphan-reap paths now terminate the underlying sandbox by ID, preventing idle sandbox accumulation
to_bedrock_model_id()now passes through any model ID with an explicit non-Anthropic provider prefix (e.g.openrouter/anthropic/claude-opus-4.8) unchanged so it runs through that provider rather than being rewritten to a Bedrock inference-profile ID;claude-codeagent in harbor_runner updated to reflect the same pass-through semantics- Oddish GitHub PR comment now includes a "Performance: X/Y trials passed (Z%)" summary line and Status + Reward columns in the experiment trajectory table, surfacing actual agent scores alongside the existing classification status
- API key creation now requires the requesting user to be an admin of the Abundant organization; the API keys settings section is hidden in the UI for non-admins
claude-opus-4-8added to the Bedrock model ID mapping table, resolving toglobal.anthropic.claude-opus-4-8;claude-opus-4-7moved to the legacy section of the table; regression tests added for all resolution forms (bare,anthropic/-prefixed, dotted foundation-model id) (#169)
- Automated daily changelog updated with entries for 2026-05-31 changes (#166)
- Automated daily changelog updated with entries for 2026-05-30 changes (#165)
oddish run --retryre-runs existing work for a trial, task, or experiment; re-queues failed trials by default, or re-runs analysis/verdict stages with--analysis/--verdict;-y/--yesskips confirmation (#163)oddish publish/oddish unpublishcommands toggle public read-only sharing for an experiment from the CLI and return the shareable URL; previously only possible at submit time viarun --publishor in the web UI (#163)--jsonmachine-readable output added tooddish status,cancel,delete, andpullvia a sharedprint_jsonhelper;--jsonimplies non-interactive mode and takes a single snapshot (no live watch) (#163)oddish combineCLI command merges two or more experiments into a new result experiment, copying finished trials and their artifacts; supports--name,--copy-artifacts/--no-copy-artifacts, and--jsonflags (#162)
oddish run --retryre-runs existing work for a trial, task, or experiment id (positional,--task, or--experiment): re-queues failed trials by default, or re-runs analysis/verdict with--analysis/--verdict;-yskips confirmationoddish publish/oddish unpublishcommands toggle public read-only sharing for an experiment from the CLI and return the shareable URL--jsonmachine-readable output added tooddish status,cancel,delete, andpull(previously only onrun/upload/ls)oddish combineCLI command to merge two or more experiments into a new result experiment; copies finished trials with artifacts from source experiments and supports--name,--copy-artifacts/--no-copy-artifacts, and--jsonflags (#162)POST /experiments/combineAPI endpoint that creates a new result experiment by merging task memberships and finished trials (with S3 artifacts) from two or more source experiments; in-flight trials are skipped and counted in the response; append-only so requires onlytasksscope (#157)
- Analysis and verdict UI (trial analysis dots, legend section, analysis card, verdict badge, and run analysis/verdict actions) is now hidden in the public share view (
/share/[token]) via a newshowAnalysisprop onExperimentDetailView; authenticated views are unchanged (#159)
- Trial retry no longer returns a 500 error; the new trial row is now flushed before the old trial's
superseded_by_trial_idself-referential FK is set, preventing a Postgres FK violation (#155)
- Harbor dependency updated to a fork commit that corrects Google API CIDR ranges for proper network access in restricted environments
- Automated daily changelog updated with entries for 2026-05-27 changes (#153)
- Automated daily changelog updated with entries for 2026-05-26 changes (#152)
- Preview banner now sticks to the top of the viewport and no longer overlaps the nav bar, task drawer, or settings sidebar; a CSS custom property
--preview-banner-h(0px normally, 1.75rem in preview mode viadata-previewon<html>) propagates the banner height to all affected components socalc()offsets stay in sync without hardcoded values (#146)
- Automated daily changelog updated with entries for 2026-05-25 changes (#151)
- Automated daily changelog updated with entries for 2026-05-24 changes (#149)
- Automated daily changelog updated with entries for 2026-05-23 changes (#148)
- Daytona is now the default execution environment for CPU-only hosted tasks; Modal is automatically selected when a task's
task.tomldeclares GPU requirements or when--override-gpusis set to a positive value;--envhelp text updated to reflect the new defaults - Harbor dependency updated to a version that runs build tools under a restricted network
- Automated daily changelog updated with entries for 2026-05-21 changes (#142)
- Sticky PR comment automatically posted (and updated on re-pushes) with preview environment links — Vercel frontend URL, stable
pr-NNNVercel alias, and Modal API URL — via newpost_preview_links.pyscript (#141) - In-app preview banner rendered when
NEXT_PUBLIC_IS_PREVIEW=true, surfacing PR context to reviewers using the preview environment (#141)
- PR preview workflow refactored from a monolithic
modal-preview.ymlintopr-preview.ymlbacked by focused per-phase shell scripts (prepare_preview_database.sh,deploy_preview_backend.sh,update_vercel_preview.sh, etc.), making migration-only and backend-only preview runs possible without triggering a full component redeploy (#141) - Deployment planning now tracks
deploy_frontendas a separate output flag alongsidedeploy_backendandrun_migrations; frontend-only PRs skip Supabase/Modal provisioning entirely, and non-synchronizeevents fall back to PR-wide path filters to decide which components need deploying (#141) - Newly created Supabase preview branches now cancel in-flight cloned production work (queued/running jobs, tasks, and trials) via
cancel_cloned_preview_work.shto prevent spurious activity from the data clone (#141)
ODDISH_MODAL_MAX_WORKERS_PER_POLLdefault raised from 48 to 64, allowing the dispatcher to ramp queued work faster when per-queue slot capacity is available; env override behavior unchanged (#138)
max_trial_attemptstop-level field for sweep YAML/JSON configs andTaskSubmission/TaskSweepSubmissionAPI schemas, plus--max-trial-attemptsCLI flag onoddish run, to control the total Oddish worker attempt budget per trial including the initial run; oldmax_attemptsconfig key is now rejected with a clear error (#134)ODDISH_MODAL_POLL_INTERVAL_SECONDSenv var to configure the Modal queue dispatcher poll cadence; preserves the existing 180-second default when unset (#133)- Baseline-specific context injected into the trial analysis classifier prompt for
oracleandnop/noopagents: oracle trials are no longer penalized for reading reference solutions, and NoOp runs are evaluated as baseline checks; normal agents receive no extra context (#132) --environment-kwarg/--harbor-environment-kwargCLI flag and top-levelharbor.environment.kwargsblock in sweep YAML for passing arbitrary Harbor environment kwargs (primary use case:agent_tools_imagefor Modal closed-internet runs); CLI values override config-file values on collision (#131)- "Any error" row filter in the experiment trials table to show only tasks where at least one agent hit a harness or infrastructure error, complementing the existing "Any failed" / "All failed" filters (#130)
DELETE /experiments/{experiment_id}admin API endpoint for soft-deleting an experiment and its scoped trials; artifacts are preserved in S3 (#128)
nopandoraclesweep config entries no longer require amodel_namefield (#131)- Experiment table toolbar UI polished: filter buttons styled with updated tokens and tighter layout, toolbar reorganized into a responsive flex row (#130)
- Experiment detail page creation timestamp now uses the canonical
ExperimentModel.created_atvalue (surfaced via newexperiment_created_atfield on task-status responses) instead of inferring creation time from the earliest task in the experiment (#129) - Experiment-to-task membership rows in
task_experimentsare now tombstoned (deleted_atset) instead of hard-deleted when an experiment or scoped task is removed; DB migrationk2l3m4n5o6p7adds the column with partial indexes on live rows; dashboard cache is invalidated after experiment and trial deletions (#128)
- Dashboard status filter now includes a "Retrying trials" option; retrying trial counts shown as
(nR)in amber in the Trials column (#125) - Dedicated
nop_oraclequeue fornopandoracletrial agents with a separateODDISH_NOP_ORACLE_CONCURRENCYsetting (default 32; 48 in Modal), preventing these lightweight trials from competing with model-provider queues; DB migration moves existing non-terminal nop/oracle jobs to the new queue key (#121) - Bounded exponential backoff for trial retries: 30 s base delay, up to 30 min cap, with ±25% jitter; rate-limit errors (429, quota exceeded, throttled, etc.) start at a 5 min base; retry delay persisted to
worker_jobs.available_afterand mirrored totrials.next_retry_at(#122)
- Modal image build failures (
Image build for im-... failed) now permanently fail the trial instead of requeueing, preventing repeated retry burns on deterministic Dockerfile errors; user-cancelled trial state is preserved when a build failure and a user cancel race (#124) - Retry API proxy routes (trial retry, trial analysis retry, task analysis retry, task verdict retry) now surface the real upstream error when the backend returns non-JSON plain text, instead of a misleading JSON parse exception; shared
backend-response.tshelper introduced for safe response reading (#126)
oddish statusandoddish status --watchnow show aDetailcolumn with per-trial status context —cancelled by user, the active Harbor stage while running, or the terminal error message on failure — replacing the oldStagecolumn that only populated duringrunningstate (#112)- CLI task discovery no longer calls the removed
TaskPaths.is_validAPI;is_task_dirandget_task_paths_from_localnow validate candidate directories by constructingTask(path), matching the path already used byvalidate_tasksand preventing submit failures on newer Harbor builds that dropped the compatibility helper (#119)
- Copy-to-clipboard button beside task names in the experiment trials table: a copy icon appears on hover/focus and shows a brief check-mark confirmation after copying, without opening the task files panel (#114)
- Drawer panels (
TaskFilesPanel,TrialDetailPanel,ArtifactsViewer,TrajectoryViewer) in experiment and task detail views are now lazy-loaded via Next.jsdynamic()imports, shrinking the initial page bundle (#113) - Browser Logfire/OpenTelemetry tracing is now deferred behind a conditional dynamic import in
instrumentation-client.ts, keeping it off the critical bundle when disabled or unconfigured (#113) - Browser observability spans now export directly to Logfire's OTLP endpoint using a
NEXT_PUBLIC_LOGFIRE_TOKENwrite-only token, replacing the backend proxy route (/logfire-proxy/*) that consumed Modal container concurrency slots;LogfireProxyCORSMiddlewareandmount_browser_proxy()removed from the backend (#111) - Preview branch provisioning switched back to Supabase's native
--with-dataclone; the customrestore_prod_data.shpg_dump | pg_restorescript is removed (#111)
- Task detail page (
/tasks/[task_id]) with KPI bar showing total cost, trial count, average score, and last run time; version switcher for per-version breakdown; per-agent stacked cards with trial-status chips that open existing task/trial drawers; newGET /tasks/{task_id}/detailendpoint bundles task, trials, per-version summaries, and cost rollups in one round-trip (#103) - Trajectory JSON export button on the trajectory viewer side-pane; downloads the loaded trajectory payload as
trajectory-<trialId>.jsonclient-side without additional API calls (#92)
- Claude Code now routes through AWS Bedrock by default in the Modal deployment:
CLAUDE_CODE_USE_BEDROCK=1baked into the Modal image; newto_bedrock_model_idnormalizer inoddish/config.pyconverts Anthropic-style and bare Claude model ids to invokable Bedrock cross-region inference profile ids (global.prefix for most models,us.for Opus 4.1 / Opus 4 which have no global profile); trial analysis classifier strips Bedrock env vars when running against a non-Bedrock analysis model id (#108)
- Bedrock model id mapping table now emits
global./us.cross-region inference profile ids instead of bareanthropic.claude-...foundation-model ids; bare foundation-model ids are also re-resolved through the table rather than passed through, closing a gap that caused 400 "Invocation of model ID with on-demand throughput isn't supported" errors in production (#109) - Alembic migrations now pin
search_path=publicvia asyncpgserver_settingsfor both oddish and backend migration chains, fixingInvalidSchemaNameErroron freshly-created Supabase preview branches where the Supavisor session pooler hands out backends with an emptysearch_path(#103) - Vercel preview environment now updated and redeployed whenever the Modal backend redeploys (not only on first Supabase branch creation), so previews that failed mid-flight on a prior push self-recover on the next push rather than silently serving the production API (#103)
- Pydantic Logfire full-stack observability: backend auto-instruments FastAPI, SQLAlchemy, asyncpg, and httpx; browser spans tunnel through a server-mounted
/logfire-proxy/v1/tracesroute so the write token never reaches the client;Server-Timing: traceparentheader injected by middleware to fix document-load orphan spans; worker container init and per-job cycles wrapped in explicitworker.container_init/worker.poll_queue_cycle/worker.process_single_jobspans; PR/SHA/branch/env resource attributes attached for per-deployment filtering in Logfire (#89) - Side-by-side task files + trial detail layout in the experiment drawer:
ResizablePanelGroupwith an adjustable 40/60 split, a toggle button, localStorage persistence (oddish:trial-drawer-side-by-side), auto-expand of drawer width on enable, and direct presigned-S3 artifact loading with backend-proxy fallback (#91) ghcr.io/abundant-ai/oddish-ci-baseprebuilt Docker image baking Python 3.13, uv, Node 20, Vercel CLI, Supabase CLI, PostgreSQL 17 client, Modal CLI, and pre-built project venvs at/opt/venvs/{backend,oddish}; published to GHCR weekly and on lockfile/Dockerfile changes via.github/workflows/ci-base-image.yml(#101)daytona>=0.165.0added to theoddish[worker]extra so hosted workers can construct Harbor Daytona environments for Docker-in-Docker compose tasks (#86)- Daily changelog CI workflow (
.github/workflows/daily-changelog.yml) that runs nightly at 00:00 UTC, uses Claude to summarize merged PRs from commits and diffs, and opens achangelog/YYYY-MM-DDPR with auto-merge enabled;CHANGELOG.mdbackfilled for all PRs to date (#84) - Vercel Speed Insights integration:
@vercel/speed-insightsdependency added and<SpeedInsights />component mounted in the root layout to track Core Web Vitals across all pages (#82)
- CI workflows (
modal-preview.yml,modal-deploy.yml,supabase-db-migrations.yml) now run insideghcr.io/abundant-ai/oddish-ci-base;UV_PROJECT_ENVIRONMENTpoints at the image's pre-built venvs, makinguv sync --frozena near-instant no-op instead of a full dependency install on every push (#100) - Preview branch data population switched from Supabase's
--with-datalogical-replication clone (~20 min) to a directpg_dump | pg_restorestream (~5 min); empty branches are provisioned first and populated from prod only on first branch creation via.github/scripts/preview/restore_prod_data.sh(#96) modal-preview.ymlnow has adetect-changesjob that queries the GHA API for the last successful deploy of each component and usesdorny/paths-filterto skip unchanged backend/migration deploys, reducing unnecessary CI runs (#88)- Signed-in users are now redirected from
/to/dashboardviaclerkMiddlewareat the edge; the dead client-side<Show when="signed-in"><RedirectToDashboard /></Show>wrapper inpage.tsxremoved (#97) - Nav account dropdown and sign-in button now driven by
isLoaded && isSignedInfromuseUser()directly, replacing the server-only<Show>component wrapper that caused incorrect client-side visibility (#99) - Observability environment label standardized to
"production"(was"prod") acrossbackend/observability.py,frontend/src/instrumentation.ts, andfrontend/src/lib/observability.ts(#93) oddish run --env daytonanow passes through to the Modal-hosted Oddish Cloud API instead of being forced to--env modal; warning message updated to reflect that bothmodalanddaytonaare supported cloud environments (#86)- Daily changelog workflow is now safe to re-run the same day: the date branch is force-pushed and an existing open PR is reused instead of failing with a non-fast-forward error (#106)
- GitHub Actions versions bumped across all workflows:
actions/checkoutv4→v5,actions/setup-pythonv5→v6,astral-sh/setup-uvv4→v8.1.0,supabase/setup-cliv1→v2.0.0 (#90)
- Preview database restore now drops all public-schema FK constraints via
ALTER TABLE ... DROP CONSTRAINTbefore runningpg_restore, preventing prod's stray dangling refs from rolling back entire COPY operations fortasks,task_versions,trials, and related tables;--disable-triggerswas not viable because Supabase'spostgresrole lacks superuser privileges (#99)
- Next-trial-index allocators now include soft-deleted trials when scanning for the next available index, preventing PK collision 500s on
INSERTafter a trial at{task_id}-{N}is soft-deleted;execution_options(include_deleted=True)added toinitialize_trial_import,reserve_next_trial_index, andappend_trials_to_task(#81)
oddish/environment_policy.pymodule (its exportsnormalize_environment,enforce_trial_environment,EnvironmentNamehad no callers; hosted policy lives inbackend/cloud_policy.py) (#80)- Unused
trialHasActiveAnalysisandgetActiveAnalysisCountexports fromfrontend/src/lib/job-status.ts(#80)
- Frontend cleanup pass: downgraded several
job-status.tshelpers (ACTIVE_TRIAL_STATUSES,ACTIVE_PIPELINE_STATUSES,ACTIVE_VISIBLE_JOB_STATUSES,isActiveTrialStatus,isActiveVisibleJob,getActiveTrialCount) from public exports to module-private; type-only exports (TaskStatus,TrialStatus,VisibleJobKind,VisibleJobStatus) made file-local (#80) - Settings sidebar nav and import-dialog "Clear" control rewritten to use the shadcn
Buttonprimitive instead of raw<button>elements (#80) - Removed unused
loggingimport and unusedloggerfrombackend/api/routers/github_webhooks.py(#80)
- Supabase database migrations workflow now syncs oddish with
--extra serverso server-specific deps (alembic, SQLAlchemy, asyncpg) are present during migration runs (#79)
ODDISH_SAURON_AWS_SECRET_NAMEsetting on the backend Modal app, defaulting toaws-credentials, to control which Modal secret is layered onto worker containers for the sauron S3 mirror; setting it to empty skips loading (#68, #74)
- Worker runtime now loads the
aws-credentialsModal secret alongsideoddish-prod, soAWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYare populated andSauronS3Uploader.is_enabled()actually returns true; this completes the wiring for the sauron mirror introduced in #39, whose credential plumbing was dropped during the original squash-merge (#68, #74) - Backend Dockerfile installs
gitsouv sync --frozencan fetch the harbor dependency (sourced via git URL in[tool.uv.sources]) when building outside Modal (e.g. Railway, generic container hosts) (#75)
- Rollback merge that resets
mainback to a known-good state after the task-first data model (#55) caused breakage; reverts the bulk of that change-set onmain(#72)
- Sauron S3 mirror: when
ODDISH_SAURON_S3_BUCKETis set, oddish workers mirror trial artifacts to a sauron-compatible S3 layout and write arun-meta.jsonmanifest (schema_version 1) at the run root so sauron's existing/{org}/{repo}/{pr}/{run}route renders both PR-triggered ({owner}/{repo}/pr-{N}/run-{exp}/...) and CLI-triggered ({ODDISH_SAURON_S3_ORG}/runs/{exp}/run-{exp}/...) runs without sauron changes; disabled by default, best-effort with try/except on failures (#39) - Drag-and-drop zip import UI: dashboard now has an import dialog with native drag-and-drop slots for task and trial zips, mirroring
oddish upload(#42) POST /imports/zipandPOST /imports/zip/inspectbackend endpoints for streaming task/run zip uploads with 1 GiB per-file cap, presigned-URL task uploads, concurrent trial fan-out, and a read-only preview path; new framework-agnosticoddish/core/zip_imports.pyreuses CLI utilities for parity (#42)- Task-first data model (Phase 1 + Phase 3): new
JobModelandExperimentCellModeltables,JobKindenum (validation,experiment_backfill,ad_hoc), agent equivalence keying via SHA256 of(harness | model | provider)for trial fungibility, and trials joining experiments through(task_version_id, agent_equivalence_key)at read time rather than ownership; 7 alembic migrations seed cells/jobs and enforcetask_versionsimmutability (#55) POST /experiments,GET /experiments/{id}/cells, cell CRUD,/experiments/{id}/resolve,/experiments/{id}/backfill,/agents/known, and/api/jobs/*endpoints, plusexperiment-cell-matrix.tsx,experiment-leaderboard.tsx,experiment-pass-at-k.tsx,trial-inspect-drawer.tsx,jobs-client.tsx, andnew-experiment-client.tsxfrontend components (#55)ExperimentCreateResponseschema that extendsResolvedExperimentResponsewith an optionalbackfillreceipt field (#69)
POST /experimentsnow enqueues a backfill automatically and returns the resolved experiment with the new trial receipts in a single round-trip; pass?dry_run=trueto keep the previous create-only semantics (#69)- Frontend experiment creation flows updated to use the combined create+backfill call and drop the separate backfill request (#69)
- Gemini model routes canonicalized:
google/gemini...and bare Gemini model inputs now normalize onto LiteLLM'sgemini/...route in the queue/model resolution helpers (#71)
- Experiment-visibility regression: migration
p7e8f9a0b1c2backfillsexperiment_tasksfromtask_experimentsjoined to each task's current version, andexperiment_agentsfrom distinct(experiment_id, agent_equivalence_key)pairs observed in trials (using the most recent trial's identity strings); both inserts useON CONFLICT DO NOTHINGso the migration is re-runnable (#66) - Pass@k calculation now only counts completed attempts (
have_n_successful + have_n_failed), excluding running and queued trials that produced no evidence; each task result carries its ownnso per-task attempt counts are honored, with fallback to the agent-levelnwhen absent (#66) - Supabase migration workflow now installs the
serverextra so thealembicconsole script is present; the previousuv sync --frozenwithout--extra serversilently failed every run (#70)
- Drop Python 3.14 support (range tightened to
>=3.12,<3.14) to fix dep resolution:harbor==0.6.2requireslitellm>=1.83.14, which declaresRequires-Python <3.14.tool.mypy.python_version, Trove classifier,backend/Dockerfile, and GitHub Actionssetup-pythonall moved from 3.14 → 3.13;uv.lockrelocked in bothoddish/andbackend/(#54)
--force-new-versionflag onoddish run(and correspondingforce_new_versionfield onTaskUploadInitRequest) that allocates a new task version even when the local content hash matches the latest existing version, enabling callers to flip per-version-immutable flags likerun_analysiswithout a content change (#59)
create_task_sweep_corenow flipstask.run_analysisfromFalsetoTruewhen an append submission requests analysis, instead of returning a 400 "Cannot enable run_analysis when appending..." — this matches the documented intent of--force-new-versionand unblocks full validation on tasks first registered without analysis (#60)
- Task author now resolved backend-side from the authenticated identity (precedence:
--user→--github-user→ Clerk-backedUserModel.email→api_key.name→"unknown") instead ofgetpass.getuser(); CLI no longer fillstask.userfrom the OS username, so experiments stop showingubuntu/rootas Author (#52) TaskSubmission.user/TaskSweepSubmission.userare now optional on the wire;submission.github_usernameis auto-filled from the actor'sUserModel.github_usernamewhen missing (#52)
- Removed the 400 guard in
create_task_sweep_corethat refused append-mode submissions for tasks inANALYZING/VERDICT_PENDING; the existingappend_trials_to_taskpath already handles the state cleanup (flips status back toRUNNING, clears verdict fields, cancels in-flightVERDICTworker jobs), so re-appending now lands cleanly and re-enters the analysis/verdict pipeline once the new trials complete (#53)
- Bump
harborto0.6.2in the core package; regenerateoddishandbackendlockfiles; realign direct pins onlitellm,openai, and backendpython-dotenvto match the new harbor dep graph; update task-status test doubles to the currentbuild_trial_responseshape (#48) - Preview environment strategy: Supabase preview branches are now created with
--with-dataso they clone production data instead of starting empty, and the bootstrap script usesON CONFLICT DO NOTHINGfor idempotent org seeding; branches are reused across pushes within a PR (#46)
DELETE /tasks/{task_id},DELETE /experiments/{experiment_id},DELETE /trials/{trial_id}HTTP endpoints from bothoddish/serverandbackend/api/routers(the underlyingdelete_*_corehelpers remain available for admin/CLI use through an auth-scoped surface) (#46)
oddish lsCLI command that lists tasks via the existing/tasks/browseAPI and renders a Rich table with latest version, trial counts, reward summary, last run time, and linked experiments; supports--limit(capped at 100),--offset, and--jsonfor scripting (#40)- README section documenting
pip installfrom a GitHub ref via#subdirectory=oddish, alongside the existing PyPI quick-start (#41)
- Supabase preview branch provisioning in the
modal-previewPR workflow: Python polling step waits up to 10 minutes for Supabase to create the preview branch, runs bothoddishandbackendalembic chains against it, and layers aPREVIEW_DATABASE_URLModal secret on top of the production secret so PR previews use isolated preview databases (#35) supabase/config.tomlwith project ID to enable Supabase's GitHub integration, plusSUPABASE_ACCESS_TOKEN/SUPABASE_PROJECT_REFenv vars in the workflow (#35)
- "Rendered" vs "Raw" view-mode toggle on the task-files panel for text-based files, backed by a new
RawRenderercomponent that displays content in a monospace<pre>block; URL-based renderers (image, video, audio, PDF, xlsx, docx, binary) ignore the toggle (#37)
- File-content fetching no longer sniffs binary-vs-text — all text-based files are fetched via
response.text(); legacy detection helpers (isTextContent,shouldSniffTextContent,looksLikeTextBytes,readResponseTextContent,getBinaryFileMessage) removed (#37) - CLI docs (
DOCS.md) gained a new "Reading data from Oddish" section with a decision table foroddish statusvsoddish pull, expanded examples for--watch, the auto-detection fallback chain, per-trial file layout, idempotent re-pulling, and public-endpoint fallback for shared experiments (#38)
- Experiment legend Trial-outcome chips resized to 22×18 /
rounded-[4px]with a 10×10 SVG (was 14×14 /rounded-[3px]with 8×8 SVG) so legend swatches read as the same primitive as the matrix cells and the anatomy demo in the same toolbar (#36)
/settingspage redesigned with a sidebar layout (Account / Workspace / API keys),Panel/PanelHeader/SectionHeadingprimitives, Clerk-nativeOrganizationSwitcherinstead of a hand-rolled workspace list, status-dot active-workspace indicator, and a real empty state for API keys; legacy?tab=URLs still accepted alongside the new?section=(#33)
- Frontend
JobStatus.PENDINGis now folded into thequeuedmatrix bucket:getMatrixStatusreturnsqueuedfortrial.status === "pending",STATUS_FILTER_ORDERand the URL filter type-guard no longer listpending, while backend-wire-aligned types and analysis/verdict in-flight checks still acceptpendingsince the backend enum is unchanged (TODO comment added onJobStatusdocumenting the eventual full deprecation) (#27) - Task detail drawer navigation simplified: removed the always-disabled left chevron and the standalone
FileTextindicator; the icon-only right chevron is now a labeled "View trials →" button; vertical progress sliver replaced with a legible "N / M" text readout between up/down chevrons (#29) - Experiment trials table: first column now has a dedicated 240px default width so the
v1/v2version badge no longer sits flush against the cell border; header cells gainedpy-3so the header row is visibly taller than data rows (#31) - Experiment results visual refresh: 22×18 rounded matrix tiles with hover lift; thick-stroke geometric SVGs for pass/fail/partial/error/queued/running/pending replacing lucide glyphs; warm oklch color ramp (red → orange → olive → green) for partial scores; legend renamed (
Trial outcome/QA verdict) with anatomy key,Partialchip dropped,Harness errorrenamed toError; pass@k chart and leaderboard cross-highlight on agent hover, leaderboard bars switched to the sharedAGENT_COLORSpalette (#21)
/settingsdark-mode contrast: bumped--muted-foregroundfrom30 6% 62%→30 8% 74%, pointed Clerk'scolorTextSecondaryathsl(var(--foreground) / 0.78), added the missingappearance.elementskeys for active-device / profile-section / org-preview surfaces, plus a small.dark .cl-*block inglobals.cssfor cases where Clerk's internal styles win the cascade (#34)/settingssection-switch flicker: all three sections now render with CSS visibility rather than conditional mount/unmount, so Clerk'sUserProfile/OrganizationProfileno longer remount on every tab click (#34)
- Experiment-level cost tracking in the summary bar:
oddish/model_pricing.pyprovides per-token pricing for Anthropic (Claude 3.5/3.7/4/4.1/4.5), OpenAI (GPT-4o, GPT-4.1, GPT-5.x including codex variants, o3/o4-mini, codex-mini), and Google (Gemini 2.5/3) families with substring matching for Anthropic-API, Bedrock, and LiteLLM-style provider-prefixed names; ordered most-specific-first sogpt-5-mininever resolves togpt-5rates (#23) cost_usdandcost_is_estimatedfields on the trial response builders (full + compact);ExperimentDetailViewsummary bar aggregates cost across visible trials with~for pure estimates and trailing*for mixed native+estimated totals (#23)
- Frontend major-dep upgrades landed:
@clerk/nextjs6.36.8 → 7.2.5 (replacedSignedIn/SignedOut, swappedafterSignInUrl/afterSignUpUrlforsignInFallbackRedirectUrl/signUpFallbackRedirectUrl);lucide-react0.468.0 → 1.9.0 with an inlineGithubIconSVG replacing the removed brand icon;tailwindcss3.4.19 → 4.2.4 via the official@tailwindcss/upgradecodemod (rewroteglobals.cssto@import "tailwindcss"+@theme {}, swapped to@tailwindcss/postcss, droppedautoprefixer, mechanical class renamesshadow-sm→shadow-xs,outline-none→outline-hidden,flex-shrink-0→shrink-0, etc.,tailwindcss-animatewired via@plugin);eslint9.39.2 bump deferred pendingeslint-plugin-reactpeer-range update (#24)
frontend/run-prod-clerk-local.shnow preservesPATHwhen re-execing itself viasudo, so the documentedcd frontend && sudo rm -rf .next && ./run-prod-clerk-local.shflow works on systems wherepnpmlives on a user-managed path (e.g. nvm) (#22)
- Per-file expanded S3 layout for task files alongside the canonical tarball: new
TASK_EXPANDWorkerJobKind, alembic migrationc4b5a6d7e8f9adding nullableexpanded_at/expanded_manifest_keyontask_versions,task_expand_handler.pyworker with semaphore-bounded per-member uploads + 30s heartbeats,tasks_expand_archive/tasks_expand_max_bytes/tasks_expand_max_member_bytes/tasks_archive_cache_mbsettings, andStorageClient.upload_bytes; UI reads from the expanded layout by default and falls back to the archive for in-flight expansions or legacy versions (#13) StorageClientbytes+parsed-members cache per archive ETag (default 256 MB) so a listing + content click on the same version reuses one download and one tarball parse; backend returnsETag+Cache-Control: private, max-age=86400, immutableand 304s onIf-None-Matchwhenversionis pinned (#13)- Local-storage preflight on Harbor worker startup that validates free bytes, inode headroom, and a create/write/delete probe against both
harbor_jobs_dirand the active temp root (#14) - Temp-dir cleanup when S3 hydration fails before Oddish falls back or raises, and pruning of empty Harbor parent directories after trial artifact upload cleanup (#14)
- Clickable Task column header on the experiment trials table cycling
default → name A→Z → name Z→AwithArrowUpDown/ArrowUp/ArrowDownindicators; sort layers on top of the existing search filter so virtualization and row selection pick it up unchanged (#19) - Per-PR Modal preview webhook subdomains:
@modal.asgi_app(label=...)label now derives fromMODAL_APP_NAME("api"for production,"{app}-api"for previews likeoddish-pr-19-api) so concurrent PR previews no longer collide onabundant-ai-preview--api.modal.run(#20)
- Harbor temp-root preflight now only probes
tempfile.gettempdir()whenharbor_config.docker_imageorharbor_config.mcp_serversrequires task patching; previously a constrained/tmprejected valid trials that never needed temp patching (#16) oddishsdist packaging: thepyproject.tomlincludeoverride that restricted the sdist tosrc/oddish/analyze/*.txtis removed, sopip install oddishfrom sdist now ships the full package instead of an empty shell; regression test assertssrc/oddish/__init__.pyandsrc/oddish/cli/__init__.pyare present in built sdists (#18)
- Pass@K graph tooltip replaced with a custom recharts
contentrenderer: entries sorted by pass rate descending to match the visual line order, agent labels shown with color-indicator squares, values formatted as percentages with one decimal, card styling with max-height and scrolling for many agents (#8)
- Heavy-run preset bumped from Claude Opus 4.6 to Opus 4.7 (#12)
- Strict
/tasks/upload/init+/tasks/upload/completehandshake sooddish runreserves task/version metadata, uploads task archives directly to S3 via presignedPUT, and finalizes the version without proxying bytes through the API;oddish pulllikewise prefers presigned trial-file URLs and presigned-archive downloads (#11)
- Legacy proxied
/tasks/uploadflow; the CLI now fails fast when direct upload is unavailable instead of silently falling back (#11) ODDISH_S3_ENABLEDsetting and persistent local task-storage branches — S3-compatible storage is now required for task/artifact storage; self-hosting docs updated accordingly (#11)
- Org-scoped
/tasks/browsebackend endpoint with latest-version task aggregates, experiment lists, compact latest-version trial rows, search, and pagination (#10) - Clerk-authenticated frontend API proxy and shared task-browser response types (#10)
/taskspage rendered as a card grid with latest-version trial status graphics, debounced search, SWR polling, skeleton/loading states, and a Tasks nav link (#10)
- Experiments view replaces the manual
LOAD MOREbutton (10 tasks/page) with a two-phase progressive loader: phase 1 fetches all tasks at once viainclude_trials=falseso the list appears instantly; phase 2 streams trial data in 50-task batches viainclude_trials=true&compact_trials=true, progressively filling trial status icons with a subtle "Loading trials 50/200…" header indicator (#7)
- Backend module restructure: split the monolithic
backend/worker.pyinto aworker/package (functions.pyfor the Modal dispatcher / spawn orchestration,runtime.pyfor Modal runtime patching and storage setup,github.pyfor GitHub notification hooks around shared queue execution); extract hosted-only environment policy intobackend/cloud_policy.py(ALLOWED_CLOUD_ENVIRONMENTS,get_default_cloud_environment,enforce_trial_environment); move public-API helpers intooddish.api.public_helpers; drop the now-unownedqueue_slotstable frombackend/models.pyand stub its migration (#5) - No-op tweak to
.github/workflows/modal-preview.ymlto exercise the shared Modalpreviewenvironment plus per-PR app-naming end-to-end on a real PR (#3)
- Monorepo restructure with
oddish/(core Python package, published to PyPI),backend/(Modal-hosted API + worker orchestration with multi-tenant Clerk/API-key auth, org-scoped data, and queue-key concurrency), andfrontend/(Next.js dashboard); two-stack alembic migrations (oddish/alembic/for core,backend/alembic/for cloud auth tables); cloud auth schema includingorganizations,users(with Clerk + Supabase user-id columns),api_keys(scopedfull/tasks/read), with FKs addingorg_id,created_by_user_idontotasks; pre-commit pipeline covering ruff, black, mypy, prettier, and eslint acrossbackend|oddishandfrontendpaths (#1)