Skip to content

Commit 6beb8a4

Browse files
committed
fixing CC trajectory issue; frontend badges
1 parent 9b93dc1 commit 6beb8a4

7 files changed

Lines changed: 153 additions & 25 deletions

File tree

frontend/next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./.next/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

frontend/src/components/experiment-detail-view.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,49 @@ export function ExperimentDetailView({
503503
});
504504
}, [tasksForExperiment, searchParams, buildTrialGroups]);
505505

506+
// Re-sync the open drawer with freshly-loaded trial data. On direct URL
507+
// loads the drawer opens as soon as the lightweight task shells arrive,
508+
// before trial pages stream in; without this, ``trialGroups`` stays empty
509+
// and the task↔trial nav row (``onNavigateToFirstTrial``) never appears.
510+
// Also handles the case where trials finish loading while a drawer opened
511+
// from a row click is already mounted.
512+
useEffect(() => {
513+
if (!drawerState) return;
514+
const liveTask = tasksForExperiment.find(
515+
(t) => t.id === drawerState.task.id,
516+
);
517+
if (!liveTask) return;
518+
const liveTrialCount = liveTask.trials?.length ?? 0;
519+
const snapshotTrialCount = drawerState.task.trials?.length ?? 0;
520+
if (
521+
liveTask === drawerState.task &&
522+
liveTrialCount === snapshotTrialCount
523+
) {
524+
return;
525+
}
526+
const { trialGroups, orderedTrials } = buildTrialGroups(liveTask);
527+
const foundTrialIndex = drawerState.trial
528+
? orderedTrials.findIndex((t) => t.id === drawerState.trial!.id)
529+
: -1;
530+
const resolvedTrialIndex = foundTrialIndex >= 0 ? foundTrialIndex : null;
531+
const resolvedTrial =
532+
resolvedTrialIndex != null
533+
? orderedTrials[resolvedTrialIndex]
534+
: drawerState.trial;
535+
const resolvedTaskIndex = tasksForExperiment.indexOf(liveTask);
536+
setDrawerState({
537+
...drawerState,
538+
task: liveTask,
539+
taskIndex:
540+
resolvedTaskIndex >= 0 ? resolvedTaskIndex : drawerState.taskIndex,
541+
orderedTasks: tasksForExperiment,
542+
trial: resolvedTrial,
543+
trialIndex: resolvedTrialIndex,
544+
orderedTrials,
545+
trialGroups,
546+
});
547+
}, [tasksForExperiment, drawerState, buildTrialGroups]);
548+
506549
const summary = useMemo(
507550
() => buildExperimentSummary(tasksForExperiment),
508551
[tasksForExperiment],

frontend/src/components/trajectory-viewer.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ function ContentRenderer({
175175
{content.map((part, idx) => {
176176
if (part.type === "text") {
177177
return (
178-
<div key={idx} className="whitespace-pre-wrap wrap-break-word text-sm">
178+
<div
179+
key={idx}
180+
className="whitespace-pre-wrap wrap-break-word text-sm"
181+
>
179182
{part.text}
180183
</div>
181184
);
@@ -694,19 +697,32 @@ function StepContent({
694697

695698
interface TrajectoryViewerProps {
696699
trialId: string;
700+
/**
701+
* Whether the backend recorded an ATIF trajectory for this trial
702+
* (mirrors ``TrialResponse.has_trajectory``). When ``false`` we skip
703+
* the fetch entirely — the endpoint would just return ``null`` after
704+
* a multi-second S3 probe, and some trials (older rows with a stale
705+
* ``harbor_result_path`` pointing at the decommissioned Modal volume)
706+
* additionally surface a spurious 403 on the local-fallback branch.
707+
* ``undefined`` preserves legacy behaviour (always fetch) for
708+
* consumers that haven't been updated.
709+
*/
710+
hasTrajectory?: boolean;
697711
apiBaseUrl?: string;
698712
}
699713

700714
export function TrajectoryViewer({
701715
trialId,
716+
hasTrajectory,
702717
apiBaseUrl = "/api",
703718
}: TrajectoryViewerProps) {
719+
const shouldFetch = hasTrajectory !== false;
704720
const {
705721
data: trajectory,
706722
isLoading,
707723
error,
708724
} = useSWR<Trajectory | null>(
709-
`${apiBaseUrl}/trials/${trialId}/trajectory`,
725+
shouldFetch ? `${apiBaseUrl}/trials/${trialId}/trajectory` : null,
710726
fetcher,
711727
{
712728
revalidateOnFocus: false,

frontend/src/components/trial-detail-panel.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {
3030
ChevronUp,
3131
ChevronLeft,
3232
ChevronRight,
33-
Ban,
3433
RotateCcw,
3534
Loader2,
3635
Microscope,
@@ -60,6 +59,7 @@ import {
6059
import { HarborStageTimeline } from "@/components/harbor-stage-timeline";
6160
import { HarborStageBadge } from "@/components/harbor-stage-badge";
6261
import { QueueKeyIcon } from "@/components/queue-key-icon";
62+
import { StatusIcon } from "@/components/status-icon";
6363

6464
interface TrialDetailPanelProps {
6565
isOpen: boolean;
@@ -522,10 +522,10 @@ export function TrialDetailPanel({
522522
groupTrial.error_message,
523523
);
524524
const groupConfig = STATUS_CONFIG[groupStatus];
525-
const badgeLabel =
526-
groupStatus === "partial"
527-
? formatPartialRewardBadgeValue(groupTrial.reward)
528-
: groupConfig.symbol;
525+
const isPartial = groupStatus === "partial";
526+
const partialLabel = isPartial
527+
? formatPartialRewardBadgeValue(groupTrial.reward)
528+
: null;
529529
const isActive = index === currentGroupTrialIndex;
530530
return (
531531
<Button
@@ -535,26 +535,26 @@ export function TrialDetailPanel({
535535
size="icon"
536536
onClick={() => navigateToGroupTrial(index)}
537537
className={cn(
538-
"flex h-5 w-5 items-center justify-center rounded-sm border p-0 font-mono font-semibold leading-none transition",
539-
groupStatus === "partial"
540-
? "text-[8px] tracking-[-0.03em]"
541-
: "text-sm",
538+
"flex h-5 w-5 shrink-0 items-center justify-center rounded-sm border p-0 leading-none transition hover:opacity-90",
542539
groupConfig.matrixClass,
540+
isPartial
541+
? "font-mono text-[8px] font-semibold tracking-[-0.03em]"
542+
: "",
543543
isActive
544544
? "ring-2 ring-primary/60 ring-offset-1 ring-offset-background"
545545
: "",
546546
)}
547-
aria-label={`Trial ${index + 1}`}
547+
style={getRewardStyle(groupTrial.reward)}
548+
aria-label={`Trial ${index + 1} ${groupConfig.shortLabel}`}
548549
title={`${groupConfig.shortLabel} • Trial ${index + 1}`}
549550
>
550-
{groupStatus === "pending" ||
551-
groupStatus === "queued" ||
552-
groupStatus === "running" ? (
553-
<Loader2 className="h-3.5 w-3.5" />
554-
) : groupStatus === "harness-error" ? (
555-
<Ban className="h-3.5 w-3.5" />
551+
{isPartial ? (
552+
partialLabel
556553
) : (
557-
badgeLabel
554+
<StatusIcon
555+
status={groupStatus}
556+
className="h-3.5 w-3.5"
557+
/>
558558
)}
559559
</Button>
560560
);
@@ -950,7 +950,11 @@ export function TrialDetailPanel({
950950
value="trajectory"
951951
className="m-0 h-full overflow-auto p-0"
952952
>
953-
<TrajectoryViewer trialId={trial.id} apiBaseUrl={apiBaseUrl} />
953+
<TrajectoryViewer
954+
trialId={trial.id}
955+
hasTrajectory={trial.has_trajectory}
956+
apiBaseUrl={apiBaseUrl}
957+
/>
954958
</TabsContent>
955959
</div>
956960
</Tabs>

frontend/src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface Trial {
7676
output_tokens?: number | null;
7777
cost_usd?: number | null;
7878
cost_is_estimated?: boolean | null;
79+
has_trajectory?: boolean;
7980
created_at: string;
8081
started_at?: string | null;
8182
finished_at?: string | null;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""null stale /data/harbor harbor_result_path values
2+
3+
Commit d2154f5 ("removed modal volume stuff") stopped mounting the
4+
shared ``/data`` Modal Volume and switched ``harbor_jobs_dir`` from
5+
``/data/harbor`` to each container's ephemeral ``/tmp/harbor-jobs``.
6+
Any ``trials.harbor_result_path`` written before that commit still
7+
references ``/data/harbor/...`` even though that path can never resolve
8+
on the new containers. The column is a legacy breadcrumb anyway — the
9+
trial's artifacts are in S3 (see
10+
``StorageClient.upload_trial_results``) and the local dir is deleted
11+
immediately after upload by ``_cleanup_uploaded_job_dir`` — so these
12+
rows are pure noise.
13+
14+
Beyond the noise, leaving them set tripped the path-containment guard
15+
in ``oddish.core.trial_io._resolve_local_job_dir``: trajectory / result
16+
reads for trials without an S3 trajectory would fall through to the
17+
local-fallback branch, hit a path outside the current
18+
``harbor_jobs_dir``, and surface as a confusing ``403`` to the
19+
frontend. The guard has been softened to return ``None`` for that
20+
case, but nulling the stale rows removes the dead walk entirely and
21+
makes ``harbor_result_path`` stop lying about where artifacts live.
22+
23+
This migration is idempotent: running it repeatedly is a no-op once
24+
the stale rows are nulled.
25+
26+
Revision ID: d5e6f7a8b9c0
27+
Revises: c4b5a6d7e8f9
28+
Create Date: 2026-04-23 16:30:00.000000
29+
"""
30+
31+
from typing import Sequence, Union
32+
33+
from alembic import op
34+
35+
36+
revision: str = "d5e6f7a8b9c0"
37+
down_revision: Union[str, Sequence[str], None] = "c4b5a6d7e8f9"
38+
branch_labels: Union[str, Sequence[str], None] = None
39+
depends_on: Union[str, Sequence[str], None] = None
40+
41+
42+
def upgrade() -> None:
43+
op.execute(
44+
"""
45+
UPDATE trials
46+
SET harbor_result_path = NULL
47+
WHERE harbor_result_path LIKE '/data/harbor/%'
48+
"""
49+
)
50+
51+
52+
def downgrade() -> None:
53+
# Irreversible: the original per-row values are not recoverable, and
54+
# they pointed at a volume that no longer exists, so there's nothing
55+
# useful to restore.
56+
pass

oddish/src/oddish/core/trial_io.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,17 @@ def _should_cache_trial(trial: TrialModel) -> bool:
6161

6262

6363
def _resolve_local_job_dir(trial: TrialModel) -> Path | None:
64-
"""Resolve and validate the local Harbor job directory for a trial."""
64+
"""Resolve and validate the local Harbor job directory for a trial.
65+
66+
Returns ``None`` (not a 403) when ``harbor_result_path`` points outside the
67+
current container's ``harbor_jobs_dir``. This happens for trials run
68+
before the Modal Volume was removed (they stored ``/data/harbor/...``
69+
paths that no longer resolve on the ephemeral-``/tmp`` containers);
70+
those trials just have no local fallback and should fall through to
71+
the "no trajectory / no result" branch rather than surfacing a
72+
spurious auth-looking error. The path comes from our own DB, not
73+
user input, so there's no traversal risk to guard against here.
74+
"""
6575
if not trial.harbor_result_path:
6676
return None
6777

@@ -76,9 +86,7 @@ def _resolve_local_job_dir(trial: TrialModel) -> Path | None:
7686
base_dir not in result_path_resolved.parents
7787
and result_path_resolved != base_dir
7888
):
79-
raise HTTPException(
80-
status_code=403, detail="Refusing to read trial outside harbor_jobs_dir"
81-
)
89+
return None
8290

8391
job_dir = result_path_resolved.parent
8492
if not job_dir.exists() or not job_dir.is_dir():

0 commit comments

Comments
 (0)