-
-
Notifications
You must be signed in to change notification settings - Fork 371
feat(workspace): reuse checkout opens and trim repeated bootstrap #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
866d49e
ff9c337
8ea171f
e2121b4
44c0969
f7fc11b
fef5250
540d51c
f5ab680
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import assert from "node:assert/strict"; | ||
| import { openAiConversationScopeHash } from "./request-meta.js"; | ||
|
|
||
| assert.equal(openAiConversationScopeHash(undefined), undefined); | ||
| assert.equal(openAiConversationScopeHash({}), undefined); | ||
| assert.equal(openAiConversationScopeHash({ "openai/session": "" }), undefined); | ||
|
|
||
| const sessionOnly = openAiConversationScopeHash({ "openai/session": "chat-1" }); | ||
| assert.match(sessionOnly ?? "", /^[a-f0-9]{64}$/); | ||
| assert.equal( | ||
| sessionOnly, | ||
| openAiConversationScopeHash({ "openai/session": "chat-1" }), | ||
| ); | ||
| assert.notEqual( | ||
| sessionOnly, | ||
| openAiConversationScopeHash({ "openai/session": "chat-2" }), | ||
| ); | ||
|
|
||
| assert.equal( | ||
| sessionOnly, | ||
| openAiConversationScopeHash({ | ||
| "openai/session": "chat-1", | ||
| "openai/subject": "user-1", | ||
| "openai/organization": "org-1", | ||
| }), | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { createHash } from "node:crypto"; | ||
|
|
||
| function metadataString( | ||
| meta: Record<string, unknown> | undefined, | ||
| key: string, | ||
| ): string | undefined { | ||
| const value = meta?.[key]; | ||
| return typeof value === "string" && value.length > 0 ? value : undefined; | ||
| } | ||
|
|
||
| export function openAiConversationScopeHash( | ||
| meta: Record<string, unknown> | undefined, | ||
| ): string | undefined { | ||
| const session = metadataString(meta, "openai/session"); | ||
| if (!session) return undefined; | ||
|
|
||
| return createHash("sha256") | ||
| .update(JSON.stringify(["openai", session])) | ||
| .digest("hex"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,26 +48,32 @@ const REVIEW_REF_PREFIX = "refs/devspace/review"; | |
|
|
||
| export function createReviewCheckpointManager(): ReviewCheckpointManager { | ||
| const states = new Map<string, WorkspaceReviewState>(); | ||
| const initializations = new Map<string, Promise<void>>(); | ||
|
|
||
| return { | ||
| async initializeWorkspace({ workspaceId, root }) { | ||
| const refs = reviewRefs(workspaceId); | ||
| const state: WorkspaceReviewState = { root, ...refs }; | ||
| states.set(workspaceId, state); | ||
| const existingState = states.get(workspaceId); | ||
| if ( | ||
| existingState?.root === root && | ||
| (existingState.gitRoot !== undefined || existingState.diagnostic !== undefined) | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| const pending = initializations.get(workspaceId); | ||
| if (pending) { | ||
| await pending; | ||
| return; | ||
| } | ||
|
|
||
| const initialize = initializeWorkspaceState(states, workspaceId, root); | ||
| initializations.set(workspaceId, initialize); | ||
| try { | ||
| const eligibility = await getGitEligibility(root); | ||
| if (!eligibility.ok || !eligibility.gitRoot) { | ||
| state.diagnostic = eligibility.message ?? "show_changes requires a Git workspace in this version."; | ||
| return; | ||
| await initialize; | ||
| } finally { | ||
| if (initializations.get(workspaceId) === initialize) { | ||
| initializations.delete(workspaceId); | ||
| } | ||
|
|
||
| state.gitRoot = eligibility.gitRoot; | ||
| const commit = await createWorkingTreeSnapshot(eligibility.gitRoot); | ||
| await git(eligibility.gitRoot, ["update-ref", state.openRef, commit]); | ||
| await git(eligibility.gitRoot, ["update-ref", state.baselineRef, commit]); | ||
| } catch (error) { | ||
| state.diagnostic = error instanceof Error ? error.message : String(error); | ||
| } | ||
| }, | ||
|
|
||
|
|
@@ -111,6 +117,50 @@ export function createReviewCheckpointManager(): ReviewCheckpointManager { | |
| }; | ||
| } | ||
|
|
||
| async function initializeWorkspaceState( | ||
| states: Map<string, WorkspaceReviewState>, | ||
| workspaceId: string, | ||
| root: string, | ||
| ): Promise<void> { | ||
| const refs = reviewRefs(workspaceId); | ||
| const state: WorkspaceReviewState = { root, ...refs }; | ||
| states.set(workspaceId, state); | ||
|
|
||
| try { | ||
| const eligibility = await getGitEligibility(root); | ||
| if (!eligibility.ok || !eligibility.gitRoot) { | ||
| state.diagnostic = eligibility.message ?? "show_changes requires a Git workspace in this version."; | ||
| return; | ||
| } | ||
|
|
||
| state.gitRoot = eligibility.gitRoot; | ||
| const [hasOpenRef, hasBaselineRef] = await Promise.all([ | ||
| hasCommitRef(eligibility.gitRoot, state.openRef), | ||
| hasCommitRef(eligibility.gitRoot, state.baselineRef), | ||
| ]); | ||
| if (hasOpenRef && hasBaselineRef) return; | ||
|
|
||
| const commit = await createWorkingTreeSnapshot(eligibility.gitRoot); | ||
| if (!hasOpenRef) { | ||
| await git(eligibility.gitRoot, ["update-ref", state.openRef, commit]); | ||
| } | ||
| if (!hasBaselineRef) { | ||
| await git(eligibility.gitRoot, ["update-ref", state.baselineRef, commit]); | ||
|
Comment on lines
+141
to
+148
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When DevSpace restarts with the workspace open ref present but the baseline ref missing, initialization preserves the open ref but snapshots the current edited tree into the missing baseline. The next |
||
| } | ||
| } catch (error) { | ||
| state.diagnostic = error instanceof Error ? error.message : String(error); | ||
| } | ||
| } | ||
|
|
||
| async function hasCommitRef(gitRoot: string, ref: string): Promise<boolean> { | ||
| try { | ||
| await git(gitRoot, ["rev-parse", "--verify", `${ref}^{commit}`]); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| function reviewRefs(workspaceId: string): Pick<WorkspaceReviewState, "openRef" | "baselineRef"> { | ||
| const segment = safeWorkspaceRefSegment(workspaceId); | ||
| return { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Fix the readiness race between
initializeWorkspaceStateand its consumers.initializeWorkspaceStatepublishesstateintostatessynchronously (line 127) before eligibility resolves, then setsstate.gitRoot = eligibility.gitRoot(line 136) before the open/baseline refs are verified or created (lines 137-149). This creates two related problems:initializeWorkspace's own early-return check at Lines 56-59 treatsgitRoot !== undefinedas "fully initialized." A second concurrentinitializeWorkspacecall for the sameworkspaceIdcan seegitRootset and return immediately, bypassing theinitializationspending-map wait, even though ref creation is still in flight.reviewChanges(unchanged in this diff) only checksif (!state)before deciding whether to callinitializeWorkspace. Sincestateis set synchronously and early, a concurrentreviewChangescall sees a non-null but incomplete state, skipsinitializeWorkspaceentirely, and then either throws a generic "requires a Git workspace" error (ifgitRootisn't set yet) or runsgit rev-parse --verify <ref>^{commit}against a ref that doesn't exist yet (ifgitRootis set but refs aren't created).This is the same issue raised in a previous review round (marked "Addressed"), but the current code still exhibits it. Set
state.gitRootonly after refs are confirmed or created, and makereviewChangesre-enterinitializeWorkspacewhenevergitRoot/diagnosticare both absent, mirroring the readiness condition already used at Lines 56-59.🔧 Proposed fix
state.gitRoot = eligibility.gitRoot; const [hasOpenRef, hasBaselineRef] = await Promise.all([ hasCommitRef(eligibility.gitRoot, state.openRef), hasCommitRef(eligibility.gitRoot, state.baselineRef), ]); - if (hasOpenRef && hasBaselineRef) return; - - const commit = await createWorkingTreeSnapshot(eligibility.gitRoot); - if (!hasOpenRef) { - await git(eligibility.gitRoot, ["update-ref", state.openRef, commit]); - } - if (!hasBaselineRef) { - await git(eligibility.gitRoot, ["update-ref", state.baselineRef, commit]); - } + if (!hasOpenRef || !hasBaselineRef) { + const commit = await createWorkingTreeSnapshot(eligibility.gitRoot); + if (!hasOpenRef) { + await git(eligibility.gitRoot, ["update-ref", state.openRef, commit]); + } + if (!hasBaselineRef) { + await git(eligibility.gitRoot, ["update-ref", state.baselineRef, commit]); + } + } + state.gitRoot = eligibility.gitRoot;Also update the unchanged
reviewChangesreadiness check outside this range:Also applies to: 120-163
🤖 Prompt for AI Agents