mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-21 22:01:49 +00:00
Fix Foundry UI bugs: org names, sessions, and repo selection (#250)
* Fix Foundry auth: migrate to Better Auth adapter, fix access token retrieval - Remove @ts-nocheck from better-auth.ts, auth-user/index.ts, app-shell.ts and fix all type errors - Fix getAccessTokenForSession: read GitHub token directly from account record instead of calling Better Auth's internal /get-access-token endpoint which returns 403 on server-side calls - Re-implement workspaceAuth helper functions (workspaceAuthColumn, normalizeAuthValue, workspaceAuthClause, workspaceAuthWhere) that were accidentally deleted - Remove all retry logic (withRetries, isRetryableAppActorError) - Implement CORS origin allowlist from configured environment - Document cachedAppWorkspace singleton pattern - Add inline org sync fallback in buildAppSnapshot for post-OAuth flow - Add no-retry rule to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add Foundry dev panel from fix-git-data branch Port the dev panel component that was left out when PR #243 was replaced by PR #247. Adapted to remove runtime/mock-debug references that don't exist on the current branch. - Toggle with Shift+D, persists visibility to localStorage - Shows context, session, GitHub sync status sections - Dev-only (import.meta.env.DEV) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add full Docker image defaults, fix actor deadlocks, and improve dev experience - Add Dockerfile.full and --all flag to install-agent CLI for pre-built images - Centralize Docker image constant (FULL_IMAGE) pinned to 0.3.1-full - Remove examples/shared/Dockerfile{,.dev} and daytona snapshot example - Expand Docker docs with full runnable Dockerfile - Fix self-deadlock in createWorkbenchSession (fire-and-forget provisioning) - Audit and convert 12 task actions from wait:true to wait:false - Add bun --hot for dev backend hot reload - Remove --force from pnpm install in dev Dockerfile for faster startup - Add env_file support to compose.dev.yaml for automatic credential loading - Add mock frontend compose config and dev panel - Update CLAUDE.md with wait:true policy and dev environment setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * WIP: async action fixes and interest manager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix Foundry UI bugs: org names, hanging sessions, and wrong repo creation - Fix org display name using GitHub description instead of name field - Fix createWorkbenchSession hanging when sandbox is provisioning - Fix auto-session creation retry storm on errors - Fix task creation using wrong repo due to React state race conditions - Remove Bun hot-reload from backend Dockerfile (causes port drift) - Add GitHub sync/install status to dev panel Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
58c54156f1
commit
d8b8b49f37
88 changed files with 9252 additions and 1933 deletions
167
foundry/packages/client/src/interest/remote-manager.ts
Normal file
167
foundry/packages/client/src/interest/remote-manager.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import type { BackendClient } from "../backend-client.js";
|
||||
import type { InterestManager, TopicStatus } from "./manager.js";
|
||||
import { topicDefinitions, type TopicData, type TopicDefinition, type TopicKey, type TopicParams } from "./topics.js";
|
||||
|
||||
const GRACE_PERIOD_MS = 30_000;
|
||||
|
||||
/**
|
||||
* Remote implementation of InterestManager.
|
||||
* Each cache entry owns one actor connection plus one materialized snapshot.
|
||||
*/
|
||||
export class RemoteInterestManager implements InterestManager {
|
||||
private entries = new Map<string, TopicEntry<any, any, any>>();
|
||||
|
||||
constructor(private readonly backend: BackendClient) {}
|
||||
|
||||
subscribe<K extends TopicKey>(topicKey: K, params: TopicParams<K>, listener: () => void): () => void {
|
||||
const definition = topicDefinitions[topicKey] as unknown as TopicDefinition<any, any, any>;
|
||||
const cacheKey = definition.key(params as any);
|
||||
let entry = this.entries.get(cacheKey);
|
||||
|
||||
if (!entry) {
|
||||
entry = new TopicEntry(definition, this.backend, params as any);
|
||||
this.entries.set(cacheKey, entry);
|
||||
}
|
||||
|
||||
entry.cancelTeardown();
|
||||
entry.addListener(listener);
|
||||
entry.ensureStarted();
|
||||
|
||||
return () => {
|
||||
const current = this.entries.get(cacheKey);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
current.removeListener(listener);
|
||||
if (current.listenerCount === 0) {
|
||||
current.scheduleTeardown(GRACE_PERIOD_MS, () => {
|
||||
this.entries.delete(cacheKey);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getSnapshot<K extends TopicKey>(topicKey: K, params: TopicParams<K>): TopicData<K> | undefined {
|
||||
return this.entries.get((topicDefinitions[topicKey] as any).key(params))?.data as TopicData<K> | undefined;
|
||||
}
|
||||
|
||||
getStatus<K extends TopicKey>(topicKey: K, params: TopicParams<K>): TopicStatus {
|
||||
return this.entries.get((topicDefinitions[topicKey] as any).key(params))?.status ?? "loading";
|
||||
}
|
||||
|
||||
getError<K extends TopicKey>(topicKey: K, params: TopicParams<K>): Error | null {
|
||||
return this.entries.get((topicDefinitions[topicKey] as any).key(params))?.error ?? null;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const entry of this.entries.values()) {
|
||||
entry.dispose();
|
||||
}
|
||||
this.entries.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class TopicEntry<TData, TParams, TEvent> {
|
||||
data: TData | undefined;
|
||||
status: TopicStatus = "loading";
|
||||
error: Error | null = null;
|
||||
listenerCount = 0;
|
||||
|
||||
private readonly listeners = new Set<() => void>();
|
||||
private conn: Awaited<ReturnType<TopicDefinition<TData, TParams, TEvent>["connect"]>> | null = null;
|
||||
private unsubscribeEvent: (() => void) | null = null;
|
||||
private unsubscribeError: (() => void) | null = null;
|
||||
private teardownTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private startPromise: Promise<void> | null = null;
|
||||
private started = false;
|
||||
|
||||
constructor(
|
||||
private readonly definition: TopicDefinition<TData, TParams, TEvent>,
|
||||
private readonly backend: BackendClient,
|
||||
private readonly params: TParams,
|
||||
) {}
|
||||
|
||||
addListener(listener: () => void): void {
|
||||
this.listeners.add(listener);
|
||||
this.listenerCount = this.listeners.size;
|
||||
}
|
||||
|
||||
removeListener(listener: () => void): void {
|
||||
this.listeners.delete(listener);
|
||||
this.listenerCount = this.listeners.size;
|
||||
}
|
||||
|
||||
ensureStarted(): void {
|
||||
if (this.started || this.startPromise) {
|
||||
return;
|
||||
}
|
||||
this.startPromise = this.start().finally(() => {
|
||||
this.startPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
scheduleTeardown(ms: number, onTeardown: () => void): void {
|
||||
this.teardownTimer = setTimeout(() => {
|
||||
this.dispose();
|
||||
onTeardown();
|
||||
}, ms);
|
||||
}
|
||||
|
||||
cancelTeardown(): void {
|
||||
if (this.teardownTimer) {
|
||||
clearTimeout(this.teardownTimer);
|
||||
this.teardownTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.cancelTeardown();
|
||||
this.unsubscribeEvent?.();
|
||||
this.unsubscribeError?.();
|
||||
if (this.conn) {
|
||||
void this.conn.dispose();
|
||||
}
|
||||
this.conn = null;
|
||||
this.data = undefined;
|
||||
this.status = "loading";
|
||||
this.error = null;
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
private async start(): Promise<void> {
|
||||
this.status = "loading";
|
||||
this.error = null;
|
||||
this.notify();
|
||||
|
||||
try {
|
||||
this.conn = await this.definition.connect(this.backend, this.params);
|
||||
this.unsubscribeEvent = this.conn.on(this.definition.event, (event: TEvent) => {
|
||||
if (this.data === undefined) {
|
||||
return;
|
||||
}
|
||||
this.data = this.definition.applyEvent(this.data, event);
|
||||
this.notify();
|
||||
});
|
||||
this.unsubscribeError = this.conn.onError((error: unknown) => {
|
||||
this.status = "error";
|
||||
this.error = error instanceof Error ? error : new Error(String(error));
|
||||
this.notify();
|
||||
});
|
||||
this.data = await this.definition.fetchInitial(this.backend, this.params);
|
||||
this.status = "connected";
|
||||
this.started = true;
|
||||
this.notify();
|
||||
} catch (error) {
|
||||
this.status = "error";
|
||||
this.error = error instanceof Error ? error : new Error(String(error));
|
||||
this.started = false;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
for (const listener of [...this.listeners]) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue