Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0afac77
fix: dashboard auto-updates session metadata from GitHub PR state
AgentWrapper Mar 7, 2026
d1a4364
Address PR review feedback on metadata writeback
AgentWrapper Mar 10, 2026
64896e9
Harden sessions dir resolution for metadata writeback
AgentWrapper Mar 10, 2026
840e0b4
Fix follow-up review issues in session metadata resolution
AgentWrapper Mar 10, 2026
a15c7ef
Preserve ci_failed status over review transitions
AgentWrapper Mar 10, 2026
64762a2
Restore ESM .js cache import compatibility
AgentWrapper Mar 10, 2026
48bff97
fix(web): drop cache bridge file and use direct cache import
AgentWrapper Mar 10, 2026
8bea03e
refactor(web): reuse shared PR rate-limit helper
AgentWrapper Mar 10, 2026
6fdb8b8
chore(ci): retrigger PR checks
AgentWrapper Mar 11, 2026
a43bb6d
Merge origin/main into session/ao-10
AgentWrapper Mar 11, 2026
63797c6
fix(web): prevent stale metadata status writeback races
AgentWrapper Mar 11, 2026
784b6eb
refactor(web): narrow project before SCM lookup in sessions route
AgentWrapper Mar 11, 2026
697b4b1
chore(ci): retrigger checks
AgentWrapper Mar 12, 2026
48664fe
fix(tracker-gitlab): remove dependency on scm-gitlab subpath export
AgentWrapper Mar 12, 2026
2c280c1
fix(web): preserve mergeable status and dedupe gitlab utils
AgentWrapper Mar 12, 2026
a873a39
chore(ci): retrigger checks
AgentWrapper Mar 12, 2026
1f7886e
chore(ci): retrigger checks again
AgentWrapper Mar 12, 2026
70cad6a
chore(ci): retrigger checks (rate-limit window)
AgentWrapper Mar 12, 2026
8eed55a
chore(ci): retrigger checks (mar-13)
AgentWrapper Mar 12, 2026
4aa4383
chore(ci): retrigger checks (mar-13-2)
AgentWrapper Mar 13, 2026
0cccd62
Merge origin/main into session/ao-10 and resolve CI conflicts
AgentWrapper Mar 13, 2026
f0278f3
chore(ci): retrigger stuck external checks
AgentWrapper Mar 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Agent Orchestrator (ao) Session

You are running inside an Agent Orchestrator managed workspace.
Session metadata is updated automatically via shell wrappers.

If automatic updates fail, you can manually update metadata:
```bash
~/.ao/bin/ao-metadata-helper.sh # sourced automatically
# Then call: update_ao_metadata <key> <value>
```
39 changes: 32 additions & 7 deletions packages/web/src/app/api/sessions/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getSessionsDir } from "@composio/ao-core";
import { NextResponse, type NextRequest } from "next/server";
import { getServices, getSCM } from "@/lib/services";
import {
Expand All @@ -7,6 +8,20 @@ import {
enrichSessionsMetadata,
} from "@/lib/serialize";

function resolveSessionsDir(
configPath: string,
projectPath: string,
metadata: Record<string, string>,
): string | null {
const metadataDir = metadata["AO_DATA_DIR"] ?? metadata["aoDataDir"] ?? metadata["sessionsDir"];
if (metadataDir) return metadataDir;
try {
return getSessionsDir(configPath, projectPath);
} catch {
return null;
}
}

export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params;
Expand All @@ -22,16 +37,26 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
// Enrich metadata (issue labels, agent summaries, issue titles)
await enrichSessionsMetadata([coreSession], [dashboardSession], config, registry);

// Enrich PR — serve cache immediately, refresh in background if stale
// Enrich PR — always fetch fresh data for the detail endpoint (no cache)
if (coreSession.pr) {
const project = resolveProject(coreSession, config.projects);
const scm = getSCM(registry, project);
if (scm) {
const cached = await enrichSessionPR(dashboardSession, scm, coreSession.pr, { cacheOnly: true });
if (!cached) {
// Nothing cached yet — block once to populate, then future calls use cache
await enrichSessionPR(dashboardSession, scm, coreSession.pr);
}
if (scm && project) {
const sessionsDir = resolveSessionsDir(
config.configPath,
project.path,
coreSession.metadata,
);
await enrichSessionPR(dashboardSession, scm, coreSession.pr, {
bypassCache: true,
metadata: sessionsDir
? {
sessionsDir,
sessionId: coreSession.id,
currentStatus: coreSession.status,
}
: undefined,
});
}
}

Expand Down
28 changes: 26 additions & 2 deletions packages/web/src/app/api/sessions/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ACTIVITY_STATE } from "@composio/ao-core";
import { ACTIVITY_STATE, getSessionsDir } from "@composio/ao-core";
import { NextResponse } from "next/server";
import { getServices, getSCM } from "@/lib/services";
import {
Expand All @@ -9,6 +9,20 @@ import {
computeStats,
} from "@/lib/serialize";

function resolveSessionsDir(
configPath: string,
projectPath: string,
metadata: Record<string, string>,
): string | null {
const metadataDir = metadata["AO_DATA_DIR"] ?? metadata["aoDataDir"] ?? metadata["sessionsDir"];
if (metadataDir) return metadataDir;
try {
return getSessionsDir(configPath, projectPath);
} catch {
return null;
}
}

/** GET /api/sessions — List all sessions with full state
* Query params:
* - active=true: Only return non-exited sessions
Expand Down Expand Up @@ -50,7 +64,17 @@ export async function GET(request: Request) {
const project = resolveProject(core, config.projects);
const scm = getSCM(registry, project);
if (!scm) return Promise.resolve();
return enrichSessionPR(dashboardSessions[i], scm, core.pr);
if (!project) return Promise.resolve();
const sessionsDir = resolveSessionsDir(config.configPath, project.path, core.metadata);
return enrichSessionPR(dashboardSessions[i], scm, core.pr, {
metadata: sessionsDir
? {
sessionsDir,
sessionId: core.id,
currentStatus: core.status,
}
: undefined,
});
});
const enrichTimeout = new Promise<void>((resolve) => setTimeout(resolve, 4_000));
await Promise.race([Promise.allSettled(enrichPromises), enrichTimeout]);
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/components/PRStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export function PRStatus({ pr }: PRStatusProps) {
+{pr.additions} -{pr.deletions} {sizeLabel}
</span>
)}
{rateLimited && (
<span className="inline-flex items-center rounded-full bg-[rgba(210,153,34,0.1)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-accent-yellow)]">
stale
</span>
)}

{/* Merged badge */}
{pr.state === "merged" && (
Expand Down
39 changes: 37 additions & 2 deletions packages/web/src/components/SessionDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import { useState, useEffect, useRef } from "react";
import { useSearchParams } from "next/navigation";
import { type DashboardSession, type DashboardPR, isPRMergeReady } from "@/lib/types";
import {
type DashboardSession,
type DashboardPR,
isPRMergeReady,
isPRRateLimited,
} from "@/lib/types";
import { CI_STATUS } from "@composio/ao-core/types";
import { cn } from "@/lib/cn";
import { CICheckList } from "./CIBadge";
Expand Down Expand Up @@ -468,6 +473,7 @@ function PRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: string }) {
};

const allGreen = isPRMergeReady(pr);
const rateLimited = isPRRateLimited(pr);

const failedChecks = pr.ciChecks.filter((c) => c.status === "failed");

Expand All @@ -493,6 +499,11 @@ function PRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: string }) {
PR #{pr.number}: {pr.title}
</a>
<div className="mt-1.5 flex flex-wrap items-center gap-2 text-[11px]">
{rateLimited && (
<span className="rounded-full bg-[rgba(210,153,34,0.12)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-status-attention)]">
data may be stale
</span>
)}
<span>
<span className="text-[var(--color-status-ready)]">+{pr.additions}</span>{" "}
<span className="text-[var(--color-status-error)]">-{pr.deletions}</span>
Expand Down Expand Up @@ -535,7 +546,12 @@ function PRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: string }) {

{/* CI Checks */}
{pr.ciChecks.length > 0 && (
<div className="mt-4 border-t border-[var(--color-border-subtle)] pt-4">
<div
className={cn(
"mt-4 border-t border-[var(--color-border-subtle)] pt-4",
rateLimited && "opacity-60",
)}
>
<CICheckList checks={pr.ciChecks} layout={failedChecks.length > 0 ? "expanded" : "inline"} />
</div>
)}
Expand Down Expand Up @@ -621,6 +637,25 @@ function PRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: string }) {
// ── Issues list (pre-merge blockers) ─────────────────────────────────

function IssuesList({ pr }: { pr: DashboardPR }) {
const rateLimited = isPRRateLimited(pr);
if (rateLimited) {
return (
<div className="space-y-1.5">
<h4 className="mb-2 text-[10px] font-bold uppercase tracking-[0.08em] text-[var(--color-text-tertiary)]">
Blockers
</h4>
<div className="flex items-center gap-2.5 text-[12px]">
<span className="w-3 shrink-0 text-center text-[11px] text-[var(--color-status-attention)]">
</span>
<span className="text-[var(--color-text-secondary)]">
GitHub API rate limited, PR checks may be stale
</span>
</div>
</div>
);
}

const issues: Array<{ icon: string; color: string; text: string }> = [];

if (pr.ciStatus === CI_STATUS.FAILING) {
Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/lib/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ interface CacheEntry<T> {
expiresAt: number;
}

const DEFAULT_TTL_MS = 5 * 60_000; // 5 minutes
export const PR_CACHE_TTL_SUCCESS_MS = 5 * 60_000; // 5 minutes
export const PR_CACHE_TTL_RATE_LIMIT_MS = 30_000; // 30 seconds
const DEFAULT_TTL_MS = PR_CACHE_TTL_SUCCESS_MS;

/**
* Simple TTL cache backed by a Map.
Expand Down
108 changes: 94 additions & 14 deletions packages/web/src/lib/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@
* (string dates, flattened DashboardPR) suitable for JSON serialization.
*/

import type {
Session,
Agent,
SCM,
PRInfo,
Tracker,
ProjectConfig,
OrchestratorConfig,
PluginRegistry,
import {
SESSION_STATUS,
updateMetadata,
type Session,
type Agent,
type SCM,
type PRInfo,
type SessionStatus,
type Tracker,
type ProjectConfig,
type OrchestratorConfig,
type PluginRegistry,
} from "@composio/ao-core";
import type { DashboardSession, DashboardPR, DashboardStats } from "./types.js";
import { TTLCache, prCache, prCacheKey, type PREnrichmentData } from "./cache";
import {
TTLCache,
prCache,
prCacheKey,
PR_CACHE_TTL_SUCCESS_MS,
PR_CACHE_TTL_RATE_LIMIT_MS,
type PREnrichmentData,
} from "./cache";

/** Cache for issue titles (5 min TTL — issue titles rarely change) */
const issueTitleCache = new TTLCache<string>(300_000);
Expand Down Expand Up @@ -106,14 +116,22 @@ export async function enrichSessionPR(
dashboard: DashboardSession,
scm: SCM,
pr: PRInfo,
opts?: { cacheOnly?: boolean },
opts?: {
cacheOnly?: boolean;
bypassCache?: boolean;
metadata?: {
sessionsDir: string;
sessionId: string;
currentStatus: SessionStatus;
};
},
): Promise<boolean> {
if (!dashboard.pr) return false;

const cacheKey = prCacheKey(pr.owner, pr.repo, pr.number);

// Check cache first
const cached = prCache.get(cacheKey);
const cached = opts?.bypassCache ? null : prCache.get(cacheKey);
if (cached && dashboard.pr) {
dashboard.pr.state = cached.state;
dashboard.pr.title = cached.title;
Expand All @@ -125,6 +143,7 @@ export async function enrichSessionPR(
dashboard.pr.mergeability = cached.mergeability;
dashboard.pr.unresolvedThreads = cached.unresolvedThreads;
dashboard.pr.unresolvedComments = cached.unresolvedComments;
maybeWriteSessionStatusTransition(dashboard, opts?.metadata, isPRRateLimited(dashboard.pr));
return true;
}

Expand Down Expand Up @@ -232,7 +251,10 @@ export async function enrichSessionPR(
unresolvedThreads: dashboard.pr.unresolvedThreads,
unresolvedComments: dashboard.pr.unresolvedComments,
};
prCache.set(cacheKey, rateLimitedData, 60 * 60_000); // 60 min — GitHub rate limit resets hourly
if (!opts?.bypassCache) {
prCache.set(cacheKey, rateLimitedData, PR_CACHE_TTL_RATE_LIMIT_MS);
}
maybeWriteSessionStatusTransition(dashboard, opts?.metadata, true);
return true;
}

Expand All @@ -248,10 +270,68 @@ export async function enrichSessionPR(
unresolvedThreads: dashboard.pr.unresolvedThreads,
unresolvedComments: dashboard.pr.unresolvedComments,
};
prCache.set(cacheKey, cacheData);
if (!opts?.bypassCache) {
prCache.set(cacheKey, cacheData, PR_CACHE_TTL_SUCCESS_MS);
}
maybeWriteSessionStatusTransition(dashboard, opts?.metadata, false);
return true;
}

function isPRRateLimited(pr: DashboardPR): boolean {
return pr.mergeability.blockers.includes("API rate limited or unavailable");
}

function deriveSessionStatusTransition(
currentStatus: SessionStatus,
pr: DashboardPR,
rateLimited: boolean,
): SessionStatus | null {
if (pr.state === "merged") return SESSION_STATUS.MERGED;
if (pr.state === "closed") return SESSION_STATUS.DONE;

// During rate limiting, CI/review data can be stale defaults.
if (rateLimited) return null;

if (currentStatus === SESSION_STATUS.CI_FAILED && pr.ciStatus === "passing") {
return SESSION_STATUS.PR_OPEN;
}
if (pr.reviewDecision === "approved") {
return SESSION_STATUS.APPROVED;
}
if (pr.reviewDecision === "changes_requested") {
return SESSION_STATUS.CHANGES_REQUESTED;
}

return null;
}

function maybeWriteSessionStatusTransition(
dashboard: DashboardSession,
metadata:
| {
sessionsDir: string;
sessionId: string;
currentStatus: SessionStatus;
}
| undefined,
rateLimited: boolean,
): void {
if (!dashboard.pr || !metadata) return;

const nextStatus = deriveSessionStatusTransition(metadata.currentStatus, dashboard.pr, rateLimited);
if (!nextStatus || nextStatus === metadata.currentStatus) return;

try {
updateMetadata(metadata.sessionsDir, metadata.sessionId, { status: nextStatus });
dashboard.status = nextStatus;
} catch (error) {
console.warn(
`[enrichSessionPR] failed to update metadata for session ${metadata.sessionId}:`,
error,
);
}
}

/** Enrich a DashboardSession's issue label using the tracker plugin. */
export function enrichSessionIssue(
dashboard: DashboardSession,
Expand Down
Loading