Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
26472a4
feat(workspace): reuse opens within ChatGPT sessions
Waishnav Jul 31, 2026
e027d46
fix(workspace): keep reused cards visually stable
Waishnav Jul 31, 2026
5c39987
fix(review): restore checkpoints after restart
Waishnav Jul 31, 2026
5b2e56e
fix(ui): expose workspace card metadata
Waishnav Jul 31, 2026
44c94ca
test(workspace): make reuse assertions deterministic
Waishnav Jul 31, 2026
a87e40c
test(ui): remove duplicate workspace assertions
Waishnav Jul 31, 2026
9892c4a
feat(workspace): track project bootstrap delivery
Waishnav Jul 31, 2026
969d5bc
fix(workspace): always create requested worktrees
Waishnav Jul 31, 2026
826c0b1
fix(server): separate workspace reuse from bootstrap
Waishnav Jul 31, 2026
e1e2d48
refactor(workspace): store conversation scope ids directly
Waishnav Aug 4, 2026
b1ee6a8
fix(review): preserve partial checkpoints safely
Waishnav Aug 4, 2026
733a4cf
fix(workspace): preserve valid bindings on context errors
Waishnav Aug 4, 2026
73566a8
fix(ui): stringify empty diagnostics safely
Waishnav Aug 4, 2026
7a13ba0
docs(workspace): remove unsupported reopen guidance
Waishnav Aug 4, 2026
3a9b60e
fix(db): make bootstrap backfill deterministic
Waishnav Aug 4, 2026
08fabf7
fix(workspace): canonicalize missing checkout targets
Waishnav Aug 4, 2026
cf68ee6
test(workspace): use canonical binding keys
Waishnav Aug 4, 2026
5540eef
fix(workspace): claim bootstrap after context loads
Waishnav Aug 4, 2026
12e57cf
fix(review): preserve checkpoint meanings after ref loss
Waishnav Aug 4, 2026
1d132fe
docs(workspace): include mode switches in reopen guidance
Waishnav Aug 4, 2026
7cea982
fix(review): activate recreated baselines
Waishnav Aug 4, 2026
603feb6
fix(review): retry eligibility after repository changes
Waishnav Aug 4, 2026
2557bc3
fix(review): fall back when last-shown checkpoint is missing
Waishnav Aug 4, 2026
b926238
fix(review): recover missing checkpoint history safely
Waishnav Aug 4, 2026
ac7d32e
fix(request): validate conversation scope metadata
Waishnav Aug 4, 2026
35e1d8c
fix(workspace): isolate stale binding recovery
Waishnav Aug 4, 2026
c8e1f67
fix(server): expose workspace lifecycle state
Waishnav Aug 4, 2026
32ddfa8
test(db): cover deterministic bootstrap backfill
Waishnav Aug 4, 2026
5ef1e47
fix(review): remove unsupported last-review selector
Waishnav Aug 4, 2026
8690d9d
docs(workflow): clarify bootstrap delivery
Waishnav Aug 4, 2026
3c8a39c
test(workspace): isolate conversation lifecycle coverage
Waishnav Aug 4, 2026
6c776d3
test(review): name isolated behavior scenarios
Waishnav Aug 4, 2026
5d73476
test(db): isolate workspace migration coverage
Waishnav Aug 4, 2026
6be2e3a
fix(review): reject checkpoint root mismatches
Waishnav Aug 4, 2026
245cb2a
test(workspace): strengthen conversation lifecycle coverage
Waishnav Aug 4, 2026
548f0ac
test(review): clarify isolated behavior scenarios
Waishnav Aug 4, 2026
e723518
test(db): split workspace migration coverage
Waishnav Aug 4, 2026
2d83fa4
revert: restore 8690d9d tree
Waishnav Aug 4, 2026
0249608
test(workspace): isolate conversation lifecycle coverage
Waishnav Aug 4, 2026
5ea08d3
test(review): name isolated behavior scenarios
Waishnav Aug 4, 2026
ce302ba
test(db): isolate workspace migration coverage
Waishnav Aug 4, 2026
bf752da
test(review): prove last-shown persistence after restart
Waishnav Aug 4, 2026
aa9ee28
refactor(review): keep checkpoint scope focused
Waishnav Aug 4, 2026
38dc6c0
fix(workspace): propagate unexpected binding errors
Waishnav Aug 4, 2026
20e8348
docs(workspace): clarify optional conversation reuse
Waishnav Aug 4, 2026
c3a642b
test(workspace): canonicalize macOS binding fixture
Waishnav Aug 4, 2026
e1e2019
test(workspace): remove duplicate root coverage
Waishnav Aug 4, 2026
45e60e4
refactor(server): make workspace guidance actionable
Waishnav Aug 4, 2026
61b29b2
docs(workspace): describe resume behavior for users
Waishnav Aug 4, 2026
6541ce2
test(workspace): avoid dependency error text assertions
Waishnav Aug 4, 2026
8e6e7cc
fix(workspace): scope bootstrap suppression to reused checkouts
Waishnav Aug 4, 2026
44fb31e
refactor(server): hide workspace lifecycle flags from models
Waishnav Aug 4, 2026
b964909
docs(workspace): explain context-aware workspace recovery
Waishnav Aug 4, 2026
d806994
test(server): cover concurrent workspace responses
Waishnav Aug 4, 2026
6a32634
docs(workspace): name retained conversation records
Waishnav Aug 4, 2026
e32504c
docs(workspace): keep reuse bookkeeping out of workflow guidance
Waishnav Aug 4, 2026
72aa34e
test(server): verify reuse after restart
Waishnav Aug 4, 2026
4a8e15f
test(server): close restart fixtures before cleanup
Waishnav Aug 4, 2026
a13a24e
refactor(workspace): remove unused bootstrap ledger
Waishnav Aug 4, 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
45 changes: 37 additions & 8 deletions docs/chatgpt-coding-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,36 @@ ChatGPT should call `open_workspace` once for a project folder:
The result includes a `workspaceId`. All later file, search, edit, show-changes,
and shell calls should reuse that same `workspaceId`.

Do not reopen the same folder unless:
ChatGPT may support automatic checkout recovery through optional host
conversation metadata. This is an OpenAI-host adapter detail, not a standard MCP
conversation field. When that optional context is available, opening the same
checkout project again in the same conversation can continue in the existing
workspace, and the context already provided for that reused checkout is not
repeated. The portable workflow remains the same: keep using the `workspaceId`
returned by `open_workspace` for later operations. Hosts without supported
conversation context receive a normal new workspace and continue with that
explicit `workspaceId` workflow.
The model receives actionable workspace instructions; automatic-reuse
bookkeeping is not a model-facing choice.

Worktree mode is deliberately different: every call creates a new managed
worktree and a new workspace session with complete context, even for the same
path and base ref.

The first successful open of a checkout provides complete instructions and
coding context. A repeated open that reuses the same checkout workspace does
not repeat the model-visible context, but the workspace UI continues to show the
complete details. Every new worktree establishes and returns its own complete
context, even when the same project was already opened in checkout or another
worktree. Opening checkout after a worktree therefore provides the checkout's
own context.

Do not call `open_workspace` again for the same checkout folder unless:

- the `workspaceId` is rejected as unknown
- the user switches to another folder
- the user switches between checkout and worktree mode
- the user explicitly asks to reopen
- work moves to a different project folder
- work switches between checkout and worktree mode
- the user asks for a new isolated worktree

## Checkout Mode

Expand Down Expand Up @@ -56,6 +80,11 @@ Managed worktrees are created under:
Worktree mode requires a Git repository with at least one commit. It starts from
`HEAD` unless `baseRef` is provided.

Each worktree-mode call creates a new managed worktree and returns a new
`workspaceId`. Reuse that ID for work inside that worktree; call
`open_workspace` in worktree mode again only when another isolated worktree is
actually required.

Uncommitted source checkout changes are not copied into the managed worktree.
DevSpace reports when the source checkout was dirty so the model can decide how
to proceed with the user.
Expand Down Expand Up @@ -158,10 +187,10 @@ and shell tools. The aggregate `show_changes` tool is not exposed by default.
Use `DEVSPACE_WIDGETS=off` to disable widget UI, or `DEVSPACE_WIDGETS=changes`
to expose the aggregate show-changes flow.

When `show_changes` is exposed, models should call it exactly once after the
final file modification in any turn that changes files. The tool only requires
the `workspaceId`; DevSpace automatically compares against the last shown
checkpoint and advances that checkpoint after rendering the aggregate diff.
When `show_changes` is exposed, call it exactly once after the final file
modification in any turn that changes files. It shows the combined changes for
that turn and advances the review point automatically. Reusing a workspace does
not change this workflow.

## Shell Use

Expand Down
19 changes: 17 additions & 2 deletions docs/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,23 @@ npx @waishnav/devspace init --force
client receives an unknown workspace error, call `open_workspace` again for that
project.

Workspace session metadata is persisted, but clients should still treat
`open_workspace` as the way to begin a fresh working session.
Workspace session metadata is persisted. ChatGPT may provide optional
conversation metadata that lets DevSpace resume the same checkout workspace for
the same project in that conversation; repeated opens reuse the `workspaceId`
and do not repeat context already provided for that reused checkout. Worktree
mode always creates a new isolated workspace with its own complete context.
Hosts without supported conversation metadata receive a normal new workspace.
In all cases, continue passing the `workspaceId` returned by `open_workspace` to
later tools. Other MCP hosts use this explicit workspace workflow as well.

To review work, call `show_changes` once after the final related file change. It
shows the combined changes and advances the review point automatically.

## Data Retention

DevSpace does not currently prune workspace sessions, conversation bindings,
or review refs. A future product retention policy will define safe cleanup for
these records; no automatic deletion is performed today.

## Workspace Path Rejected

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"test": "tsx src/config.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down
24 changes: 24 additions & 0 deletions src/db/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const migrations: Migration[] = [
name: "local-agent-sessions",
up: migrateLocalAgentSessions,
},
{
version: 4,
name: "workspace-conversation-bindings",
up: migrateWorkspaceConversationBindings,
},
];

export function migrateDatabase(sqlite: Database.Database): void {
Expand Down Expand Up @@ -174,6 +179,25 @@ function migrateLocalAgentSessions(sqlite: Database.Database): void {
addColumnIfMissing(sqlite, "local_agent_sessions", "thinking", "text");
}

function migrateWorkspaceConversationBindings(sqlite: Database.Database): void {
sqlite.exec(`
create table if not exists workspace_conversation_bindings (
conversation_scope_id text not null,
target_key text not null,
workspace_session_id text not null,
created_at text not null,
last_used_at text not null,
primary key (conversation_scope_id, target_key),
foreign key (workspace_session_id)
references workspace_sessions(id)
on delete cascade
);

create index if not exists workspace_conversation_bindings_workspace_idx
on workspace_conversation_bindings(workspace_session_id);
`);
}

function addColumnIfMissing(
sqlite: Database.Database,
table: "workspace_sessions" | "local_agent_sessions",
Expand Down
19 changes: 19 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ export const loadedAgentFiles = sqliteTable(
],
);

export const workspaceConversationBindings = sqliteTable(
"workspace_conversation_bindings",
{
conversationScopeId: text("conversation_scope_id").notNull(),
targetKey: text("target_key").notNull(),
workspaceSessionId: text("workspace_session_id")
.notNull()
.references(() => workspaceSessions.id, { onDelete: "cascade" }),
createdAt: text("created_at").notNull(),
lastUsedAt: text("last_used_at").notNull(),
},
(table) => [
primaryKey({ columns: [table.conversationScopeId, table.targetKey] }),
index("workspace_conversation_bindings_workspace_idx").on(table.workspaceSessionId),
],
);

export const oauthClients = sqliteTable(
"oauth_clients",
{
Expand Down Expand Up @@ -101,5 +118,7 @@ export type WorkspaceSessionRow = typeof workspaceSessions.$inferSelect;
export type NewWorkspaceSessionRow = typeof workspaceSessions.$inferInsert;
export type LoadedAgentFileRow = typeof loadedAgentFiles.$inferSelect;
export type NewLoadedAgentFileRow = typeof loadedAgentFiles.$inferInsert;
export type WorkspaceConversationBindingRow = typeof workspaceConversationBindings.$inferSelect;
export type NewWorkspaceConversationBindingRow = typeof workspaceConversationBindings.$inferInsert;
export type LocalAgentSessionRow = typeof localAgentSessions.$inferSelect;
export type NewLocalAgentSessionRow = typeof localAgentSessions.$inferInsert;
1 change: 1 addition & 0 deletions src/oauth-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ async function testDatabaseConfiguration(stateDir: string): Promise<void> {
{ version: 1, name: "workspace-state" },
{ version: 2, name: "oauth-state" },
{ version: 3, name: "local-agent-sessions" },
{ version: 4, name: "workspace-conversation-bindings" },
]);
} finally {
database.close();
Expand Down
38 changes: 38 additions & 0 deletions src/request-meta.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import assert from "node:assert/strict";
import test from "node:test";
import { openAiConversationScopeId } from "./request-meta.js";

test("undefined request metadata has no conversation scope", () => {
assert.equal(openAiConversationScopeId(undefined), undefined);
});

test("missing session metadata has no conversation scope", () => {
assert.equal(openAiConversationScopeId({}), undefined);
});

test("an empty session string has no conversation scope", () => {
assert.equal(openAiConversationScopeId({ "openai/session": "" }), undefined);
});

test("a non-string session value has no conversation scope", () => {
assert.equal(openAiConversationScopeId({ "openai/session": 42 }), undefined);
assert.equal(openAiConversationScopeId({ "openai/session": {} }), undefined);
});

test("valid OpenAI session metadata returns the raw opaque session value", () => {
assert.equal(
openAiConversationScopeId({ "openai/session": "chat-session-opaque-value" }),
"chat-session-opaque-value",
);
});

test("unrelated metadata fields do not alter the selected conversation scope", () => {
assert.equal(
openAiConversationScopeId({
"openai/session": "chat-session-opaque-value",
"openai/subject": "user-1",
"openai/organization": "org-1",
}),
"chat-session-opaque-value",
);
});
14 changes: 14 additions & 0 deletions src/request-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
function metadataString(
meta: unknown,
key: string,
): string | undefined {
if (typeof meta !== "object" || meta === null) return undefined;
const value = (meta as Record<string, unknown>)[key];
return typeof value === "string" && value.length > 0 ? value : undefined;
}

export function openAiConversationScopeId(
meta: unknown,
): string | undefined {
return metadataString(meta, "openai/session");
}
Loading
Loading