Prepare datamodel for multi-environment#1765
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Redundant no-op identity map in effect pipeline
- Removed the no-op
Effect.map((option) => option)line that was a leftover from refactoring.
- Removed the no-op
- ✅ Fixed: Replay events enriched twice in subscription handler
- Moved
enrichProjectEventto only wrap the livestreamDomainEventsstream so already-enriched replay events skip the redundant enrichment pass.
- Moved
- ✅ Fixed: Selector creates unstable array references every call
- Added a per-projectScopedId reference-equality cache that returns the previous result array when the underlying scopedIds and sidebarMap references haven't changed.
Or push these changes by commenting:
@cursor push 481ff4254f
Preview (481ff4254f)
diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
--- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
+++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
@@ -748,7 +748,6 @@
"ProjectionSnapshotQuery.getActiveProjectByWorkspaceRoot:decodeRow",
),
),
- Effect.map((option) => option),
Effect.flatMap((option) =>
Option.isNone(option)
? Effect.succeed(Option.none<OrchestrationProject>())
diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts
--- a/apps/server/src/ws.ts
+++ b/apps/server/src/ws.ts
@@ -503,7 +503,10 @@
Effect.catch(() => Effect.succeed([] as Array<OrchestrationEvent>)),
);
const replayStream = Stream.fromIterable(replayEvents);
- const source = Stream.merge(replayStream, orchestrationEngine.streamDomainEvents);
+ const liveStream = orchestrationEngine.streamDomainEvents.pipe(
+ Stream.mapEffect(enrichProjectEvent),
+ );
+ const source = Stream.merge(replayStream, liveStream);
type SequenceState = {
readonly nextSequence: number;
readonly pendingBySequence: Map<number, OrchestrationEvent>;
@@ -515,43 +518,33 @@
return source.pipe(
Stream.mapEffect((event) =>
- enrichProjectEvent(event).pipe(
- Effect.flatMap((enrichedEvent) =>
- Ref.modify(
- state,
- ({
- nextSequence,
- pendingBySequence,
- }): [Array<OrchestrationEvent>, SequenceState] => {
- if (
- enrichedEvent.sequence < nextSequence ||
- pendingBySequence.has(enrichedEvent.sequence)
- ) {
- return [[], { nextSequence, pendingBySequence }];
- }
+ Ref.modify(
+ state,
+ ({
+ nextSequence,
+ pendingBySequence,
+ }): [Array<OrchestrationEvent>, SequenceState] => {
+ if (event.sequence < nextSequence || pendingBySequence.has(event.sequence)) {
+ return [[], { nextSequence, pendingBySequence }];
+ }
- const updatedPending = new Map(pendingBySequence);
- updatedPending.set(enrichedEvent.sequence, enrichedEvent);
+ const updatedPending = new Map(pendingBySequence);
+ updatedPending.set(event.sequence, event);
- const emit: Array<OrchestrationEvent> = [];
- let expected = nextSequence;
- for (;;) {
- const expectedEvent = updatedPending.get(expected);
- if (!expectedEvent) {
- break;
- }
- emit.push(expectedEvent);
- updatedPending.delete(expected);
- expected += 1;
- }
+ const emit: Array<OrchestrationEvent> = [];
+ let expected = nextSequence;
+ for (;;) {
+ const expectedEvent = updatedPending.get(expected);
+ if (!expectedEvent) {
+ break;
+ }
+ emit.push(expectedEvent);
+ updatedPending.delete(expected);
+ expected += 1;
+ }
- return [
- emit,
- { nextSequence: expected, pendingBySequence: updatedPending },
- ];
- },
- ),
- ),
+ return [emit, { nextSequence: expected, pendingBySequence: updatedPending }];
+ },
),
),
Stream.flatMap((events) => Stream.fromIterable(events)),
diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts
--- a/apps/web/src/store.ts
+++ b/apps/web/src/store.ts
@@ -1305,22 +1305,36 @@
]
: undefined;
+const _threadIdsByProjectCache = new Map<
+ string,
+ { scopedIds: string[]; sidebarMap: Record<string, SidebarThreadSummary>; result: ThreadId[] }
+>();
+
export const selectThreadIdsByProjectId =
(projectId: ProjectId | null | undefined) =>
- (state: AppState): ThreadId[] =>
- projectId
- ? (
- state.threadScopedIdsByProjectScopedId[
- getProjectScopedId({
- environmentId: state.activeEnvironmentId,
- id: projectId,
- })
- ] ?? EMPTY_SCOPED_IDS
- )
- .map((scopedId) => state.sidebarThreadsByScopedId[scopedId]?.id ?? null)
- .filter((threadId): threadId is ThreadId => threadId !== null)
- : EMPTY_THREAD_IDS;
+ (state: AppState): ThreadId[] => {
+ if (!projectId) return EMPTY_THREAD_IDS;
+ const projectScopedId = getProjectScopedId({
+ environmentId: state.activeEnvironmentId,
+ id: projectId,
+ });
+ const scopedIds = state.threadScopedIdsByProjectScopedId[projectScopedId] ?? EMPTY_SCOPED_IDS;
+ const sidebarMap = state.sidebarThreadsByScopedId;
+
+ const cached = _threadIdsByProjectCache.get(projectScopedId);
+ if (cached && cached.scopedIds === scopedIds && cached.sidebarMap === sidebarMap) {
+ return cached.result;
+ }
+
+ const result = scopedIds
+ .map((scopedId) => sidebarMap[scopedId]?.id ?? null)
+ .filter((threadId): threadId is ThreadId => threadId !== null);
+
+ _threadIdsByProjectCache.set(projectScopedId, { scopedIds, sidebarMap, result });
+ return result;
+ };
+
export function setError(state: AppState, threadId: ThreadId, error: string | null): AppState {
return updateThreadState(state, state.activeEnvironmentId, threadId, (t) => {
if (t.error === error) return t;You can send follow-ups to the cloud agent here.
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Outdated
Show resolved
Hide resolved
ApprovabilityVerdict: Needs human review 2 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Unreachable fallback values in
resolveServerUrl- Removed the redundant third and fourth arguments from firstNonEmptyString since resolvePrimaryEnvironmentBootstrapUrl() already contains the complete fallback chain and always returns a non-empty string.
- ✅ Fixed: Cache returns stale null for later-configured remotes
- Skip caching null identity results so directories without a git remote are re-resolved on subsequent calls, allowing later-configured remotes to be detected.
Or push these changes by commenting:
@cursor push ffbf2a1e3c
Preview (ffbf2a1e3c)
diff --git a/apps/server/src/project/Layers/RepositoryIdentityResolver.ts b/apps/server/src/project/Layers/RepositoryIdentityResolver.ts
--- a/apps/server/src/project/Layers/RepositoryIdentityResolver.ts
+++ b/apps/server/src/project/Layers/RepositoryIdentityResolver.ts
@@ -115,12 +115,14 @@
}
const resolved = yield* Effect.promise(() => resolveRepositoryIdentity(cwd));
- yield* Ref.update(cacheRef, (current) => {
- const next = new Map(current);
- next.set(cwd, resolved.identity);
- next.set(resolved.cacheKey, resolved.identity);
- return next;
- });
+ if (resolved.identity !== null) {
+ yield* Ref.update(cacheRef, (current) => {
+ const next = new Map(current);
+ next.set(cwd, resolved.identity);
+ next.set(resolved.cacheKey, resolved.identity);
+ return next;
+ });
+ }
return resolved.identity;
});
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
--- a/apps/web/src/lib/utils.ts
+++ b/apps/web/src/lib/utils.ts
@@ -53,12 +53,7 @@
pathname?: string | undefined;
searchParams?: Record<string, string> | undefined;
}): string => {
- const rawUrl = firstNonEmptyString(
- options?.url,
- resolvePrimaryEnvironmentBootstrapUrl(),
- import.meta.env.VITE_WS_URL,
- window.location.origin,
- );
+ const rawUrl = firstNonEmptyString(options?.url, resolvePrimaryEnvironmentBootstrapUrl());
const parsedUrl = new URL(rawUrl);
if (options?.protocol) {You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Nullish coalescing collapses null into fallback incorrectly
- Replaced
environmentId ?? resolveCurrentEnvironmentId()withenvironmentId !== undefined ? environmentId : resolveCurrentEnvironmentId()so explicitnullis preserved.
- Replaced
- ✅ Fixed: Multiple exported functions are unused outside definitions
- Removed
scopeThreadSessionRef,resolveEnvironmentClient,tagEnvironmentValue,attachEnvironmentDescriptor, and their supporting types (EnvironmentClientRegistry,EnvironmentScopedRef) from client-runtime.
- Removed
Or push these changes by commenting:
@cursor push 4730c6f1c2
Preview (4730c6f1c2)
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -531,7 +531,10 @@
try {
const snapshot = await api.orchestration.getSnapshot();
if (!disposed) {
- syncServerReadModel(snapshot, environmentId ?? resolveCurrentEnvironmentId());
+ syncServerReadModel(
+ snapshot,
+ environmentId !== undefined ? environmentId : resolveCurrentEnvironmentId(),
+ );
reconcileSnapshotDerivedState();
if (recovery.completeSnapshotRecovery(snapshot.snapshotSequence)) {
void runReplayRecovery("sequence-gap");
diff --git a/packages/client-runtime/src/knownEnvironment.ts b/packages/client-runtime/src/knownEnvironment.ts
--- a/packages/client-runtime/src/knownEnvironment.ts
+++ b/packages/client-runtime/src/knownEnvironment.ts
@@ -1,4 +1,4 @@
-import type { EnvironmentId, ExecutionEnvironmentDescriptor } from "@t3tools/contracts";
+import type { EnvironmentId } from "@t3tools/contracts";
export interface KnownEnvironmentConnectionTarget {
readonly type: "ws";
@@ -37,14 +37,3 @@
): string | null {
return environment?.target.wsUrl ?? null;
}
-
-export function attachEnvironmentDescriptor(
- environment: KnownEnvironment,
- descriptor: ExecutionEnvironmentDescriptor,
-): KnownEnvironment {
- return {
- ...environment,
- environmentId: descriptor.environmentId,
- label: descriptor.label,
- };
-}
diff --git a/packages/client-runtime/src/scoped.ts b/packages/client-runtime/src/scoped.ts
--- a/packages/client-runtime/src/scoped.ts
+++ b/packages/client-runtime/src/scoped.ts
@@ -2,20 +2,10 @@
EnvironmentId,
ProjectId,
ScopedProjectRef,
- ScopedThreadSessionRef,
ScopedThreadRef,
ThreadId,
} from "@t3tools/contracts";
-interface EnvironmentScopedRef<TId extends string> {
- readonly environmentId: EnvironmentId;
- readonly id: TId;
-}
-
-export interface EnvironmentClientRegistry<TClient> {
- readonly getClient: (environmentId: EnvironmentId) => TClient | null | undefined;
-}
-
export function scopeProjectRef(
environmentId: EnvironmentId,
projectId: ProjectId,
@@ -27,34 +17,7 @@
return { environmentId, threadId };
}
-export function scopeThreadSessionRef(
- environmentId: EnvironmentId,
- threadId: ThreadId,
-): ScopedThreadSessionRef {
- return { environmentId, threadId };
-}
-
-export function scopedRefKey(
- ref: EnvironmentScopedRef<string> | ScopedProjectRef | ScopedThreadRef | ScopedThreadSessionRef,
-): string {
- const localId = "id" in ref ? ref.id : "projectId" in ref ? ref.projectId : ref.threadId;
+export function scopedRefKey(ref: ScopedProjectRef | ScopedThreadRef): string {
+ const localId = "projectId" in ref ? ref.projectId : ref.threadId;
return `${ref.environmentId}:${localId}`;
}
-
-export function resolveEnvironmentClient<TClient>(
- registry: EnvironmentClientRegistry<TClient>,
- ref: EnvironmentScopedRef<string>,
-): TClient {
- const client = registry.getClient(ref.environmentId);
- if (!client) {
- throw new Error(`No client registered for environment ${ref.environmentId}.`);
- }
- return client;
-}
-
-export function tagEnvironmentValue<T>(
- environmentId: EnvironmentId,
- value: T,
-): { readonly environmentId: EnvironmentId; readonly value: T } {
- return { environmentId, value };
-}You can send follow-ups to the cloud agent here.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- Skip registering the beforeunload flush unless `window.addEventListener` exists - Avoid crashes in environments with a partial `window` implementation
…t query layer OrchestrationProjectionSnapshotQueryLive was self-providing RepositoryIdentityResolverLive via Layer.provideMerge, creating a separate resolver instance with its own cache. This duplicated the top-level instance provided in server.ts, causing redundant git subprocess calls for the same workspace paths. Remove the self-provision so the snapshot query layer declares RepositoryIdentityResolver as an unsatisfied requirement, which is fulfilled by the single top-level instance. Update all test files to explicitly provide RepositoryIdentityResolverLive.
…t query layer OrchestrationProjectionSnapshotQueryLive was self-providing RepositoryIdentityResolverLive via Layer.provideMerge, creating a separate resolver instance with its own cache. This duplicated the top-level instance provided in server.ts, causing redundant git subprocess calls for the same workspace paths. Remove the self-provision so the snapshot query layer declares RepositoryIdentityResolver as an unsatisfied requirement, which is fulfilled by the single top-level instance. Update all test files to explicitly provide RepositoryIdentityResolverLive. Applied via @cursor push command
- Cache thread jump labels in sidebar state - Read terminal state inline in chat view selectors - Avoid rebuilding shortcut maps on every render
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Snapshot enrichment spawns processes inside DB query path
- Moved repository identity resolution (which spawns git child processes) outside the sql.withTransaction scope so the transaction only covers SQL queries and is released before any subprocess I/O occurs.
Or push these changes by commenting:
@cursor push 40b804cb76
Preview (40b804cb76)
diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
--- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
+++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
@@ -439,281 +439,283 @@
const getSnapshot: ProjectionSnapshotQueryShape["getSnapshot"] = () =>
sql
.withTransaction(
- Effect.gen(function* () {
- const [
- projectRows,
- threadRows,
- messageRows,
- proposedPlanRows,
- activityRows,
- sessionRows,
- checkpointRows,
- latestTurnRows,
- stateRows,
- ] = yield* Effect.all([
- listProjectRows(undefined).pipe(
- Effect.mapError(
- toPersistenceSqlOrDecodeError(
- "ProjectionSnapshotQuery.getSnapshot:listProjects:query",
- "ProjectionSnapshotQuery.getSnapshot:listProjects:decodeRows",
- ),
+ Effect.all([
+ listProjectRows(undefined).pipe(
+ Effect.mapError(
+ toPersistenceSqlOrDecodeError(
+ "ProjectionSnapshotQuery.getSnapshot:listProjects:query",
+ "ProjectionSnapshotQuery.getSnapshot:listProjects:decodeRows",
),
),
- listThreadRows(undefined).pipe(
- Effect.mapError(
- toPersistenceSqlOrDecodeError(
- "ProjectionSnapshotQuery.getSnapshot:listThreads:query",
- "ProjectionSnapshotQuery.getSnapshot:listThreads:decodeRows",
- ),
+ ),
+ listThreadRows(undefined).pipe(
+ Effect.mapError(
+ toPersistenceSqlOrDecodeError(
+ "ProjectionSnapshotQuery.getSnapshot:listThreads:query",
+ "ProjectionSnapshotQuery.getSnapshot:listThreads:decodeRows",
),
),
- listThreadMessageRows(undefined).pipe(
- Effect.mapError(
- toPersistenceSqlOrDecodeError(
- "ProjectionSnapshotQuery.getSnapshot:listThreadMessages:query",
- "ProjectionSnapshotQuery.getSnapshot:listThreadMessages:decodeRows",
- ),
+ ),
+ listThreadMessageRows(undefined).pipe(
+ Effect.mapError(
+ toPersistenceSqlOrDecodeError(
+ "ProjectionSnapshotQuery.getSnapshot:listThreadMessages:query",
+ "ProjectionSnapshotQuery.getSnapshot:listThreadMessages:decodeRows",
),
),
- listThreadProposedPlanRows(undefined).pipe(
- Effect.mapError(
- toPersistenceSqlOrDecodeError(
- "ProjectionSnapshotQuery.getSnapshot:listThreadProposedPlans:query",
- "ProjectionSnapshotQuery.getSnapshot:listThreadProposedPlans:decodeRows",
- ),
+ ),
+ listThreadProposedPlanRows(undefined).pipe(
+ Effect.mapError(
+ toPersistenceSqlOrDecodeError(
+ "ProjectionSnapshotQuery.getSnapshot:listThreadProposedPlans:query",
+ "ProjectionSnapshotQuery.getSnapshot:listThreadProposedPlans:decodeRows",
),
),
- listThreadActivityRows(undefined).pipe(
- Effect.mapError(
- toPersistenceSqlOrDecodeError(
- "ProjectionSnapshotQuery.getSnapshot:listThreadActivities:query",
- "ProjectionSnapshotQuery.getSnapshot:listThreadActivities:decodeRows",
- ),
+ ),
+ listThreadActivityRows(undefined).pipe(
+ Effect.mapError(
+ toPersistenceSqlOrDecodeError(
+ "ProjectionSnapshotQuery.getSnapshot:listThreadActivities:query",
+ "ProjectionSnapshotQuery.getSnapshot:listThreadActivities:decodeRows",
),
),
- listThreadSessionRows(undefined).pipe(
- Effect.mapError(
- toPersistenceSqlOrDecodeError(
- "ProjectionSnapshotQuery.getSnapshot:listThreadSessions:query",
- "ProjectionSnapshotQuery.getSnapshot:listThreadSessions:decodeRows",
- ),
+ ),
+ listThreadSessionRows(undefined).pipe(
+ Effect.mapError(
+ toPersistenceSqlOrDecodeError(
+ "ProjectionSnapshotQuery.getSnapshot:listThreadSessions:query",
+ "ProjectionSnapshotQuery.getSnapshot:listThreadSessions:decodeRows",
),
),
- listCheckpointRows(undefined).pipe(
- Effect.mapError(
- toPersistenceSqlOrDecodeError(
- "ProjectionSnapshotQuery.getSnapshot:listCheckpoints:query",
- "ProjectionSnapshotQuery.getSnapshot:listCheckpoints:decodeRows",
- ),
+ ),
+ listCheckpointRows(undefined).pipe(
+ Effect.mapError(
+ toPersistenceSqlOrDecodeError(
+ "ProjectionSnapshotQuery.getSnapshot:listCheckpoints:query",
+ "ProjectionSnapshotQuery.getSnapshot:listCheckpoints:decodeRows",
),
),
- listLatestTurnRows(undefined).pipe(
- Effect.mapError(
- toPersistenceSqlOrDecodeError(
- "ProjectionSnapshotQuery.getSnapshot:listLatestTurns:query",
- "ProjectionSnapshotQuery.getSnapshot:listLatestTurns:decodeRows",
- ),
+ ),
+ listLatestTurnRows(undefined).pipe(
+ Effect.mapError(
+ toPersistenceSqlOrDecodeError(
+ "ProjectionSnapshotQuery.getSnapshot:listLatestTurns:query",
+ "ProjectionSnapshotQuery.getSnapshot:listLatestTurns:decodeRows",
),
),
- listProjectionStateRows(undefined).pipe(
- Effect.mapError(
- toPersistenceSqlOrDecodeError(
- "ProjectionSnapshotQuery.getSnapshot:listProjectionState:query",
- "ProjectionSnapshotQuery.getSnapshot:listProjectionState:decodeRows",
- ),
+ ),
+ listProjectionStateRows(undefined).pipe(
+ Effect.mapError(
+ toPersistenceSqlOrDecodeError(
+ "ProjectionSnapshotQuery.getSnapshot:listProjectionState:query",
+ "ProjectionSnapshotQuery.getSnapshot:listProjectionState:decodeRows",
),
),
- ]);
+ ),
+ ]),
+ )
+ .pipe(
+ Effect.flatMap(
+ ([
+ projectRows,
+ threadRows,
+ messageRows,
+ proposedPlanRows,
+ activityRows,
+ sessionRows,
+ checkpointRows,
+ latestTurnRows,
+ stateRows,
+ ]) =>
+ Effect.gen(function* () {
+ const messagesByThread = new Map<string, Array<OrchestrationMessage>>();
+ const proposedPlansByThread = new Map<string, Array<OrchestrationProposedPlan>>();
+ const activitiesByThread = new Map<string, Array<OrchestrationThreadActivity>>();
+ const checkpointsByThread = new Map<string, Array<OrchestrationCheckpointSummary>>();
+ const sessionsByThread = new Map<string, OrchestrationSession>();
+ const latestTurnByThread = new Map<string, OrchestrationLatestTurn>();
- const messagesByThread = new Map<string, Array<OrchestrationMessage>>();
- const proposedPlansByThread = new Map<string, Array<OrchestrationProposedPlan>>();
- const activitiesByThread = new Map<string, Array<OrchestrationThreadActivity>>();
- const checkpointsByThread = new Map<string, Array<OrchestrationCheckpointSummary>>();
- const sessionsByThread = new Map<string, OrchestrationSession>();
- const latestTurnByThread = new Map<string, OrchestrationLatestTurn>();
+ let updatedAt: string | null = null;
- let updatedAt: string | null = null;
+ for (const row of projectRows) {
+ updatedAt = maxIso(updatedAt, row.updatedAt);
+ }
+ for (const row of threadRows) {
+ updatedAt = maxIso(updatedAt, row.updatedAt);
+ }
+ for (const row of stateRows) {
+ updatedAt = maxIso(updatedAt, row.updatedAt);
+ }
- for (const row of projectRows) {
- updatedAt = maxIso(updatedAt, row.updatedAt);
- }
- for (const row of threadRows) {
- updatedAt = maxIso(updatedAt, row.updatedAt);
- }
- for (const row of stateRows) {
- updatedAt = maxIso(updatedAt, row.updatedAt);
- }
+ for (const row of messageRows) {
+ updatedAt = maxIso(updatedAt, row.updatedAt);
+ const threadMessages = messagesByThread.get(row.threadId) ?? [];
+ threadMessages.push({
+ id: row.messageId,
+ role: row.role,
+ text: row.text,
+ ...(row.attachments !== null ? { attachments: row.attachments } : {}),
+ turnId: row.turnId,
+ streaming: row.isStreaming === 1,
+ createdAt: row.createdAt,
+ updatedAt: row.updatedAt,
+ });
+ messagesByThread.set(row.threadId, threadMessages);
+ }
- for (const row of messageRows) {
- updatedAt = maxIso(updatedAt, row.updatedAt);
- const threadMessages = messagesByThread.get(row.threadId) ?? [];
- threadMessages.push({
- id: row.messageId,
- role: row.role,
- text: row.text,
- ...(row.attachments !== null ? { attachments: row.attachments } : {}),
- turnId: row.turnId,
- streaming: row.isStreaming === 1,
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- });
- messagesByThread.set(row.threadId, threadMessages);
- }
+ for (const row of proposedPlanRows) {
+ updatedAt = maxIso(updatedAt, row.updatedAt);
+ const threadProposedPlans = proposedPlansByThread.get(row.threadId) ?? [];
+ threadProposedPlans.push({
+ id: row.planId,
+ turnId: row.turnId,
+ planMarkdown: row.planMarkdown,
+ implementedAt: row.implementedAt,
+ implementationThreadId: row.implementationThreadId,
+ createdAt: row.createdAt,
+ updatedAt: row.updatedAt,
+ });
+ proposedPlansByThread.set(row.threadId, threadProposedPlans);
+ }
- for (const row of proposedPlanRows) {
- updatedAt = maxIso(updatedAt, row.updatedAt);
- const threadProposedPlans = proposedPlansByThread.get(row.threadId) ?? [];
- threadProposedPlans.push({
- id: row.planId,
- turnId: row.turnId,
- planMarkdown: row.planMarkdown,
- implementedAt: row.implementedAt,
- implementationThreadId: row.implementationThreadId,
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- });
- proposedPlansByThread.set(row.threadId, threadProposedPlans);
- }
+ for (const row of activityRows) {
+ updatedAt = maxIso(updatedAt, row.createdAt);
+ const threadActivities = activitiesByThread.get(row.threadId) ?? [];
+ threadActivities.push({
+ id: row.activityId,
+ tone: row.tone,
+ kind: row.kind,
+ summary: row.summary,
+ payload: row.payload,
+ turnId: row.turnId,
+ ...(row.sequence !== null ? { sequence: row.sequence } : {}),
+ createdAt: row.createdAt,
+ });
+ activitiesByThread.set(row.threadId, threadActivities);
+ }
- for (const row of activityRows) {
- updatedAt = maxIso(updatedAt, row.createdAt);
- const threadActivities = activitiesByThread.get(row.threadId) ?? [];
- threadActivities.push({
- id: row.activityId,
- tone: row.tone,
- kind: row.kind,
- summary: row.summary,
- payload: row.payload,
- turnId: row.turnId,
- ...(row.sequence !== null ? { sequence: row.sequence } : {}),
- createdAt: row.createdAt,
- });
- activitiesByThread.set(row.threadId, threadActivities);
- }
+ for (const row of checkpointRows) {
+ updatedAt = maxIso(updatedAt, row.completedAt);
+ const threadCheckpoints = checkpointsByThread.get(row.threadId) ?? [];
+ threadCheckpoints.push({
+ turnId: row.turnId,
+ checkpointTurnCount: row.checkpointTurnCount,
+ checkpointRef: row.checkpointRef,
+ status: row.status,
+ files: row.files,
+ assistantMessageId: row.assistantMessageId,
+ completedAt: row.completedAt,
+ });
+ checkpointsByThread.set(row.threadId, threadCheckpoints);
+ }
- for (const row of checkpointRows) {
- updatedAt = maxIso(updatedAt, row.completedAt);
- const threadCheckpoints = checkpointsByThread.get(row.threadId) ?? [];
- threadCheckpoints.push({
- turnId: row.turnId,
- checkpointTurnCount: row.checkpointTurnCount,
- checkpointRef: row.checkpointRef,
- status: row.status,
- files: row.files,
- assistantMessageId: row.assistantMessageId,
- completedAt: row.completedAt,
- });
- checkpointsByThread.set(row.threadId, threadCheckpoints);
- }
+ for (const row of latestTurnRows) {
+ updatedAt = maxIso(updatedAt, row.requestedAt);
+ if (row.startedAt !== null) {
+ updatedAt = maxIso(updatedAt, row.startedAt);
+ }
+ if (row.completedAt !== null) {
+ updatedAt = maxIso(updatedAt, row.completedAt);
+ }
+ if (latestTurnByThread.has(row.threadId)) {
+ continue;
+ }
+ latestTurnByThread.set(row.threadId, {
+ turnId: row.turnId,
+ state:
+ row.state === "error"
+ ? "error"
+ : row.state === "interrupted"
+ ? "interrupted"
+ : row.state === "completed"
+ ? "completed"
+ : "running",
+ requestedAt: row.requestedAt,
+ startedAt: row.startedAt,
+ completedAt: row.completedAt,
+ assistantMessageId: row.assistantMessageId,
+ ...(row.sourceProposedPlanThreadId !== null && row.sourceProposedPlanId !== null
+ ? {
+ sourceProposedPlan: {
+ threadId: row.sourceProposedPlanThreadId,
+ planId: row.sourceProposedPlanId,
+ },
+ }
+ : {}),
+ });
+ }
- for (const row of latestTurnRows) {
- updatedAt = maxIso(updatedAt, row.requestedAt);
- if (row.startedAt !== null) {
- updatedAt = maxIso(updatedAt, row.startedAt);
- }
- if (row.completedAt !== null) {
- updatedAt = maxIso(updatedAt, row.completedAt);
- }
- if (latestTurnByThread.has(row.threadId)) {
- continue;
- }
- latestTurnByThread.set(row.threadId, {
- turnId: row.turnId,
- state:
- row.state === "error"
- ? "error"
- : row.state === "interrupted"
- ? "interrupted"
- : row.state === "completed"
- ? "completed"
- : "running",
- requestedAt: row.requestedAt,
- startedAt: row.startedAt,
- completedAt: row.completedAt,
- assistantMessageId: row.assistantMessageId,
- ...(row.sourceProposedPlanThreadId !== null && row.sourceProposedPlanId !== null
- ? {
- sourceProposedPlan: {
- threadId: row.sourceProposedPlanThreadId,
- planId: row.sourceProposedPlanId,
- },
- }
- : {}),
- });
- }
+ for (const row of sessionRows) {
+ updatedAt = maxIso(updatedAt, row.updatedAt);
+ sessionsByThread.set(row.threadId, {
+ threadId: row.threadId,
+ status: row.status,
+ providerName: row.providerName,
+ runtimeMode: row.runtimeMode,
+ activeTurnId: row.activeTurnId,
+ lastError: row.lastError,
+ updatedAt: row.updatedAt,
+ });
+ }
- for (const row of sessionRows) {
- updatedAt = maxIso(updatedAt, row.updatedAt);
- sessionsByThread.set(row.threadId, {
- threadId: row.threadId,
- status: row.status,
- providerName: row.providerName,
- runtimeMode: row.runtimeMode,
- activeTurnId: row.activeTurnId,
- lastError: row.lastError,
- updatedAt: row.updatedAt,
- });
- }
+ const repositoryIdentities = new Map(
+ yield* Effect.forEach(
+ projectRows,
+ (row) =>
+ repositoryIdentityResolver
+ .resolve(row.workspaceRoot)
+ .pipe(Effect.map((identity) => [row.projectId, identity] as const)),
+ { concurrency: repositoryIdentityResolutionConcurrency },
+ ),
+ );
- const repositoryIdentities = new Map(
- yield* Effect.forEach(
- projectRows,
- (row) =>
- repositoryIdentityResolver
- .resolve(row.workspaceRoot)
- .pipe(Effect.map((identity) => [row.projectId, identity] as const)),
- { concurrency: repositoryIdentityResolutionConcurrency },
- ),
- );
+ const projects: ReadonlyArray<OrchestrationProject> = projectRows.map((row) => ({
+ id: row.projectId,
+ title: row.title,
+ workspaceRoot: row.workspaceRoot,
+ repositoryIdentity: repositoryIdentities.get(row.projectId) ?? null,
+ defaultModelSelection: row.defaultModelSelection,
+ scripts: row.scripts,
+ createdAt: row.createdAt,
+ updatedAt: row.updatedAt,
+ deletedAt: row.deletedAt,
+ }));
- const projects: ReadonlyArray<OrchestrationProject> = projectRows.map((row) => ({
- id: row.projectId,
- title: row.title,
- workspaceRoot: row.workspaceRoot,
- repositoryIdentity: repositoryIdentities.get(row.projectId) ?? null,
- defaultModelSelection: row.defaultModelSelection,
- scripts: row.scripts,
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- deletedAt: row.deletedAt,
- }));
+ const threads: ReadonlyArray<OrchestrationThread> = threadRows.map((row) => ({
+ id: row.threadId,
+ projectId: row.projectId,
+ title: row.title,
+ modelSelection: row.modelSelection,
+ runtimeMode: row.runtimeMode,
+ interactionMode: row.interactionMode,
+ branch: row.branch,
+ worktreePath: row.worktreePath,
+ latestTurn: latestTurnByThread.get(row.threadId) ?? null,
+ createdAt: row.createdAt,
+ updatedAt: row.updatedAt,
+ archivedAt: row.archivedAt,
+ deletedAt: row.deletedAt,
+ messages: messagesByThread.get(row.threadId) ?? [],
+ proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
+ activities: activitiesByThread.get(row.threadId) ?? [],
+ checkpoints: checkpointsByThread.get(row.threadId) ?? [],
+ session: sessionsByThread.get(row.threadId) ?? null,
+ }));
- const threads: ReadonlyArray<OrchestrationThread> = threadRows.map((row) => ({
- id: row.threadId,
- projectId: row.projectId,
- title: row.title,
- modelSelection: row.modelSelection,
- runtimeMode: row.runtimeMode,
- interactionMode: row.interactionMode,
- branch: row.branch,
- worktreePath: row.worktreePath,
- latestTurn: latestTurnByThread.get(row.threadId) ?? null,
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- archivedAt: row.archivedAt,
- deletedAt: row.deletedAt,
- messages: messagesByThread.get(row.threadId) ?? [],
- proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
- activities: activitiesByThread.get(row.threadId) ?? [],
- checkpoints: checkpointsByThread.get(row.threadId) ?? [],
- session: sessionsByThread.get(row.threadId) ?? null,
- }));
+ const snapshot = {
+ snapshotSequence: computeSnapshotSequence(stateRows),
+ projects,
+ threads,
+ updatedAt: updatedAt ?? new Date(0).toISOString(),
+ };
- const snapshot = {
- snapshotSequence: computeSnapshotSequence(stateRows),
- projects,
- threads,
- updatedAt: updatedAt ?? new Date(0).toISOString(),
- };
-
- return yield* decodeReadModel(snapshot).pipe(
- Effect.mapError(
- toPersistenceDecodeError("ProjectionSnapshotQuery.getSnapshot:decodeReadModel"),
- ),
- );
- }),
- )
- .pipe(
+ return yield* decodeReadModel(snapshot).pipe(
+ Effect.mapError(
+ toPersistenceDecodeError("ProjectionSnapshotQuery.getSnapshot:decodeReadModel"),
+ ),
+ );
+ }),
+ ),
Effect.mapError((error) => {
if (isPersistenceError(error)) {
return error;You can send follow-ups to the cloud agent here.
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Outdated
Show resolved
Hide resolved
…tSnapshot The repositoryIdentityResolver.resolve calls spawn git child processes, which should not run inside a database transaction scope. This change restructures getSnapshot so the transaction only covers the SQL queries, and the repository identity resolution + snapshot assembly happens after the transaction completes via Effect.flatMap.
- Use deferred mutation promises to control toast lifecycle - Simplify test mocks and reset deferred state between cases
…tSnapshot The repositoryIdentityResolver.resolve calls spawn git child processes, which should not run inside a database transaction scope. This change restructures getSnapshot so the transaction only covers the SQL queries, and the repository identity resolution + snapshot assembly happens after the transaction completes via Effect.flatMap. Applied via @cursor push command
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Draft branch changes silently ignored without draftId prop
- Replaced the silent early return with a fallback to
threadRef(always available) whendraftIdis absent, restoring the original behavior wheresetDraftThreadContextwas called withthreadRef.
- Replaced the silent early return with a fallback to
Or push these changes by commenting:
@cursor push 1de72b65d5
Preview (1de72b65d5)
diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx
--- a/apps/web/src/components/BranchToolbar.tsx
+++ b/apps/web/src/components/BranchToolbar.tsx
@@ -105,10 +105,7 @@
currentWorktreePath: activeWorktreePath,
effectiveEnvMode,
});
- if (!draftId) {
- return;
- }
- setDraftThreadContext(draftId, {
+ setDraftThreadContext(draftId ?? threadRef, {
branch,
worktreePath,
envMode: nextDraftEnvMode,
@@ -124,6 +121,7 @@
setThreadBranchAction,
setDraftThreadContext,
draftId,
+ threadRef,
environmentId,
effectiveEnvMode,
],You can send follow-ups to the cloud agent here.
When BranchToolbar is rendered for a draft thread without a draftId prop, selecting a branch silently discarded the user's selection due to an early return guard. Restore the original behavior by falling back to threadRef (which is always available) when draftId is not provided. Applied via @cursor push command
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Snapshot resolution spawns git subprocesses for deleted projects
- Filtered projectRows to only active (non-deleted) projects before the Effect.forEach that resolves repository identities via git subprocesses; deleted projects already fall back to repositoryIdentity: null via the existing ?? null default.
Or push these changes by commenting:
@cursor push 3f3542a048
Preview (3f3542a048)
diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
--- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
+++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
@@ -658,9 +658,10 @@
});
}
+ const activeProjectRows = projectRows.filter((row) => row.deletedAt === null);
const repositoryIdentities = new Map(
yield* Effect.forEach(
- projectRows,
+ activeProjectRows,
(row) =>
repositoryIdentityResolver
.resolve(row.workspaceRoot)You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 18b9087. Configure here.
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
6215701 to
3481ed8
Compare
| const REPLAY_RECOVERY_RETRY_DELAY_MS = 100; | ||
| const MAX_NO_PROGRESS_REPLAY_RETRIES = 3; | ||
|
|
||
| function useRegisteredWsRpcClientEntries(): ReadonlyArray<WsRpcClientEntry> { |
There was a problem hiding this comment.
🟢 Low routes/__root.tsx:223
useRegisteredWsRpcClientEntries returns a new array reference on every render because both listWsRpcClientEntries() and the fallback [getPrimaryWsRpcClientEntry()] allocate fresh arrays. This causes EventRouter's main useEffect (line 726) to re-run on every EventRouter re-render, tearing down and re-establishing all subscriptions unnecessarily, which can drop events during transition and degrade performance. Consider memoizing the result with useMemo keyed on the revision state.
-function useRegisteredWsRpcClientEntries(): ReadonlyArray<WsRpcClientEntry> {
- const [, setRevision] = useState(0);
-
- useEffect(() => subscribeWsRpcClientRegistry(() => setRevision((value) => value + 1)), []);
-
- const entries = listWsRpcClientEntries();
- return entries.length > 0 ? entries : [getPrimaryWsRpcClientEntry()];
-}🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/routes/__root.tsx around line 223:
`useRegisteredWsRpcClientEntries` returns a new array reference on every render because both `listWsRpcClientEntries()` and the fallback `[getPrimaryWsRpcClientEntry()]` allocate fresh arrays. This causes `EventRouter`'s main `useEffect` (line 726) to re-run on every `EventRouter` re-render, tearing down and re-establishing all subscriptions unnecessarily, which can drop events during transition and degrade performance. Consider memoizing the result with `useMemo` keyed on the revision state.
Evidence trail:
apps/web/src/routes/__root.tsx lines 224-230 (`useRegisteredWsRpcClientEntries` hook definition showing direct call to `listWsRpcClientEntries()` without memoization)
apps/web/src/wsRpcClient.ts line 167 (`listWsRpcClientEntries` returns `[...wsRpcClientEntriesByKey.values()].map(toReadonlyEntry)` - new array each call)
apps/web/src/routes/__root.tsx line 262 (`clientEntries = useRegisteredWsRpcClientEntries()`)
apps/web/src/routes/__root.tsx lines 724-738 (useEffect dependency array containing `clientEntries`)
There was a problem hiding this comment.
🟡 Medium
t3code/apps/web/src/lib/gitReactQuery.ts
Line 89 in 3481ed8
The enabled condition at line 89 only checks input.cwd !== null but the queryFn throws when input.environmentId is null. When cwd is non-null but environmentId is null, the query is enabled and immediately errors. Include input.environmentId !== null in the enabled condition to prevent the query from running with invalid input.
Also found in 1 other location(s)
apps/web/src/localApi.ts:74
In
readLocalApi(), whenwindow.nativeApiis undefined (non-Electron browser environment), callingcreateLocalApi()without arguments triggers the default parametergetPrimaryWsRpcClientEntry().client. If no primary websocket environment is configured yet,getPrimaryWsRpcClientEntry()throws anError("Unable to resolve the primary websocket environment."). This causesreadLocalApi()to throw instead of returningundefined, which contradicts its return typeLocalApi | undefinedand breaks callers likeThreadTerminalDrawer.tsxthat checkif (!localApi) return;expecting gracefulundefinedrather than an exception.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/lib/gitReactQuery.ts around line 89:
The `enabled` condition at line 89 only checks `input.cwd !== null` but the `queryFn` throws when `input.environmentId` is null. When `cwd` is non-null but `environmentId` is null, the query is enabled and immediately errors. Include `input.environmentId !== null` in the `enabled` condition to prevent the query from running with invalid input.
Evidence trail:
apps/web/src/lib/gitReactQuery.ts lines 66-93 at REVIEWED_COMMIT: function `gitBranchSearchInfiniteQueryOptions` has input type `environmentId: EnvironmentId | null` (line 68), `queryFn` throws at line 79 when `!input.environmentId`, but `enabled` at line 89 only checks `input.cwd !== null && (input.enabled ?? true)` without checking `environmentId`.
Also found in 1 other location(s):
- apps/web/src/localApi.ts:74 -- In `readLocalApi()`, when `window.nativeApi` is undefined (non-Electron browser environment), calling `createLocalApi()` without arguments triggers the default parameter `getPrimaryWsRpcClientEntry().client`. If no primary websocket environment is configured yet, `getPrimaryWsRpcClientEntry()` throws an `Error("Unable to resolve the primary websocket environment.")`. This causes `readLocalApi()` to throw instead of returning `undefined`, which contradicts its return type `LocalApi | undefined` and breaks callers like `ThreadTerminalDrawer.tsx` that check `if (!localApi) return;` expecting graceful `undefined` rather than an exception.
- Keep the latest `onAdvance` in a ref for delayed selection handling - Avoid resubscribing the option toggle handler when only the callback changes
- Replace the callback with `useEffectEvent` so option selection sees fresh state without extra deps - Keep keyboard auto-advance behavior unchanged
|
<script crossorigin="anonymous" src="//unpkg.com/react-scan/dist/auto.global.js"></script>But Regular CI doesn’t catch it because it only runs bun run build:desktop, not the release artifact build. Was react-scan meant to be dev-only? That seems like the intended fix. |
|
Was meant to merge as stack with #1768 but ran into some issues. Fixing. Be patient |
…threadId (#2) * Raise slow RPC ack warning threshold to 15s (pingdotgg#1760) * Use active worktree path for workspace saves (pingdotgg#1762) * Stream git status updates over WebSocket (pingdotgg#1763) Co-authored-by: codex <codex@users.noreply.github.com> * fix(web): unwrap windows shell command wrappers (pingdotgg#1719) * Rename "Chat" to "Build" in interaction mode toggle (pingdotgg#1769) Co-authored-by: Julius Marminge <julius0216@outlook.com> * Assign default capabilities to Codex custom models (pingdotgg#1793) * Add project rename support in the sidebar (pingdotgg#1798) * Support multi-select pending user inputs (pingdotgg#1797) * Add Zed support to Open actions via editor command aliases (pingdotgg#1303) Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Julius Marminge <julius0216@outlook.com> * Closes pingdotgg#1795 - Support building and developing in a devcontainer (pingdotgg#1791) * Add explicit timeouts to CI and release workflows (pingdotgg#1825) * fix(web): distinguish singular/plural in pending action submit label (pingdotgg#1826) * Refactor web stores into atomic slices ready to split ChatView (pingdotgg#1708) * Add VSCode Insiders and VSCodium icons (pingdotgg#1847) * Prepare datamodel for multi-environment (pingdotgg#1765) Co-authored-by: justsomelegs <145564979+justsomelegs@users.noreply.github.com> Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> * Implement server auth bootstrap and pairing flow (pingdotgg#1768) Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Julius Marminge <julius@macmini.local> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> * Use dev proxy for loopback auth and environment requests (pingdotgg#1853) * Refresh local git status on turn completion (pingdotgg#1821) Co-authored-by: codex <codex@users.noreply.github.com> * fix(desktop): add Copy Link action for chat links (pingdotgg#1835) * fix: map runtime modes to correct permission levels (pingdotgg#1587) Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com> * Fix persisted composer image hydration typo (pingdotgg#1831) * Clarify environment and workspace picker labels (pingdotgg#1854) * Scope git toast state by thread ref (pingdotgg#1855) * fix build (pingdotgg#1859) * Stabilize keybindings toast stream setup (pingdotgg#1860) Co-authored-by: Julius Marminge <julius@macmini.local> * feat(web): add embeddable thread route for canvas tile hosts Adds /embed/thread/:environmentId/:threadId — a standalone route that renders the existing ChatView without the app sidebar chrome. This is the iframe target for t3-canvas agent shapes (see rororowyourboat/t3-canvas#3). - New file-based route embed.thread.\$environmentId.\$threadId.tsx - __root.tsx bypasses AppSidebarLayout for any /embed/* pathname so the environment connection + websocket surface + toasts still initialize but the sidebar/diff/plan chrome does not render - minimal=1 search param is parsed and wired to a data attribute on the container for future targeted CSS; chrome hiding (BranchToolbar, PlanSidebar, ThreadTerminalDrawer) stays as a follow-up pass - routeTree.gen.ts regenerated by the @tanstack/router-plugin --------- Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: legs <145564979+justsomelegs@users.noreply.github.com> Co-authored-by: sonder <168988030+heysonder@users.noreply.github.com> Co-authored-by: Adem Ben Abdallah <96244394+AdemBenAbdallah@users.noreply.github.com> Co-authored-by: Kyle Gottfried <6462596+Spitfire1900@users.noreply.github.com> Co-authored-by: Jacob <589761+jvzijp@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> Co-authored-by: Julius Marminge <julius@macmini.local> Co-authored-by: Klemencina <56873773+Klemencina@users.noreply.github.com> Co-authored-by: Oskar Sekutowicz <me.oski646@gmail.com> Co-authored-by: Noxire <59626436+noxire-dev@users.noreply.github.com>


prep work for multi-environments
Summary
Testing
bun fmt- Not run.bun lint- Not run.bun typecheck- Not run.bun run test- Not run.Note
High Risk
High risk because it changes server->client contract payloads (server config/lifecycle + orchestration events), introduces new persisted identifiers, and touches core orchestration snapshot/event plumbing plus client routing/state keys.
Overview
Prepares the app for multi-environment support by introducing a persistent server
ExecutionEnvironmentDescriptor(stableenvironmentId, platform, version, capability flags) and threading it intoServerRuntimeStartuplifecycle events andsubscribeServerConfigpayloads.Adds a server-side
RepositoryIdentityResolver(git-remote based, cached) and uses it to enrich project snapshot data and orchestration WS replay/live events withrepositoryIdentity, including concurrent resolution inProjectionSnapshotQuery.Updates desktop IPC/preload to expose a
getLocalEnvironmentBootstrappayload, expands tests to wire the new layers, and adjusts timeouts where needed; the web app starts adopting environment-scoped APIs/keys (e.g.,environmentApi, scoped refs/keys) and updates related browser/logic tests accordingly.Reviewed by Cursor Bugbot for commit aa92532. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Scope all project, thread, and API state to discrete environments to support multi-environment
@t3tools/client-runtimepackage withScopedProjectRef,ScopedThreadRef,KnownEnvironment, and scoped key utilities for identifying resources across environments.AppStatein store.ts so projects, threads, and related state live underenvironmentStateByIdinstead of globally; selectors and reducers updated throughout.NativeApiintoLocalApi(desktop shell/dialogs) andEnvironmentApi(per-environment backend operations); all components and hooks updated to use the appropriate API.RepositoryIdentityResolverserver service that resolves a project's canonical git remote identity from its workspace root, enrichingproject.createdandproject.meta-updatedevents and snapshot data.ServerEnvironmentservice that persists a stableEnvironmentIdper server installation and exposes anExecutionEnvironmentDescriptor(platform, version, capabilities) included in WS handshake and lifecycle events./draft/$draftId) and environment-scoped thread routes (/$environmentId/$threadId), replacing the previous/$threadIdroute; draft promotion to server threads is now a two-step lifecycle.ThreadId/ProjectIdto environment-scoped string keys; persisted state versions are bumped with migration paths.Macroscope summarized aa92532.