diff options
| author | Adam Malczewski <[email protected]> | 2026-06-28 12:59:07 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-28 12:59:07 +0900 |
| commit | 71c635f7d8ee01a2b23d5ddfdfc4bff043980052 (patch) | |
| tree | 1285beba4405e8d0955dcee340a1180aa6ce3a46 /packages/conversation-store/src | |
| parent | b83aa8ddbb7023ae9dd0332b4989d4baa11522af (diff) | |
| download | dispatch-71c635f7d8ee01a2b23d5ddfdfc4bff043980052.tar.gz dispatch-71c635f7d8ee01a2b23d5ddfdfc4bff043980052.zip | |
feat(workspace-star): starred workspace priority for concurrency limiting
Diffstat (limited to 'packages/conversation-store/src')
| -rw-r--r-- | packages/conversation-store/src/store-workspace.test.ts | 129 | ||||
| -rw-r--r-- | packages/conversation-store/src/store.ts | 58 |
2 files changed, 186 insertions, 1 deletions
diff --git a/packages/conversation-store/src/store-workspace.test.ts b/packages/conversation-store/src/store-workspace.test.ts index 788a526..59ea990 100644 --- a/packages/conversation-store/src/store-workspace.test.ts +++ b/packages/conversation-store/src/store-workspace.test.ts @@ -47,6 +47,7 @@ describe("WorkspaceStore", () => { title: "my-work", defaultCwd: null, defaultComputerId: null, + starred: false, createdAt: 1000, lastActivityAt: 1000, }); @@ -66,6 +67,7 @@ describe("WorkspaceStore", () => { title: "my-work", defaultCwd: null, defaultComputerId: null, + starred: false, createdAt: 1000, lastActivityAt: 1000, }); @@ -83,6 +85,7 @@ describe("WorkspaceStore", () => { title: "Custom", defaultCwd: "/projects/dispatch", defaultComputerId: null, + starred: false, createdAt: 3000, lastActivityAt: 3000, }); @@ -96,6 +99,7 @@ describe("WorkspaceStore", () => { title: "default", defaultCwd: null, defaultComputerId: null, + starred: false, createdAt: 0, lastActivityAt: 0, }); @@ -117,6 +121,7 @@ describe("WorkspaceStore", () => { title: "Renamed", defaultCwd: null, defaultComputerId: null, + starred: false, createdAt: 1000, lastActivityAt: 1000, }); @@ -214,6 +219,7 @@ describe("WorkspaceStore", () => { title: "default", defaultCwd: null, defaultComputerId: null, + starred: false, createdAt: 0, lastActivityAt: 0, conversationCount: 0, @@ -421,6 +427,125 @@ describe("WorkspaceStore", () => { const meta = await store.getConversationMeta("conv1"); expect(meta?.workspaceId).toBe("my-work"); }); + + // --- starred (priority for concurrency limiting) --- + + it("ensureWorkspace creates with starred: false", async () => { + const store = makeStore(); + const ws = await store.ensureWorkspace("star-work"); + expect(ws.starred).toBe(false); + }); + + it("setWorkspaceStarred true persists and reads back", async () => { + const store = makeStore(); + clock = 1000; + await store.ensureWorkspace("star-work"); + clock = 2000; + const ws = await store.setWorkspaceStarred("star-work", true); + expect(ws.starred).toBe(true); + expect(ws.id).toBe("star-work"); + const reRead = await store.getWorkspace("star-work"); + expect(reRead?.starred).toBe(true); + }); + + it("setWorkspaceStarred false unstars a previously starred workspace", async () => { + const store = makeStore(); + await store.ensureWorkspace("star-work"); + await store.setWorkspaceStarred("star-work", true); + await store.setWorkspaceStarred("star-work", false); + const ws = await store.getWorkspace("star-work"); + expect(ws?.starred).toBe(false); + }); + + it("setWorkspaceStarred creates the workspace if missing", async () => { + const store = makeStore(); + clock = 5000; + const ws = await store.setWorkspaceStarred("brand-new", true); + expect(ws).toEqual({ + id: "brand-new", + title: "brand-new", + defaultCwd: null, + defaultComputerId: null, + starred: true, + createdAt: 5000, + lastActivityAt: 5000, + }); + }); + + it("setWorkspaceStarred preserves title/defaultCwd/defaultComputerId", async () => { + const store = makeStore(); + clock = 1000; + await store.ensureWorkspace("my-work", { + title: "Custom", + defaultCwd: "/projects", + defaultComputerId: "myserver", + }); + clock = 2000; + const ws = await store.setWorkspaceStarred("my-work", true); + expect(ws.title).toBe("Custom"); + expect(ws.defaultCwd).toBe("/projects"); + expect(ws.defaultComputerId).toBe("myserver"); + expect(ws.starred).toBe(true); + expect(ws.createdAt).toBe(1000); + }); + + it("listWorkspaces includes starred field", async () => { + const store = makeStore(); + clock = 1000; + await store.ensureWorkspace("alpha"); + await store.setWorkspaceStarred("alpha", true); + clock = 2000; + await store.ensureWorkspace("beta"); + const list = await store.listWorkspaces(); + const alpha = list.find((w) => w.id === "alpha"); + expect(alpha?.starred).toBe(true); + const beta = list.find((w) => w.id === "beta"); + expect(beta?.starred).toBe(false); + }); + + it("setWorkspaceTitle preserves starred state", async () => { + const store = makeStore(); + await store.ensureWorkspace("my-work"); + await store.setWorkspaceStarred("my-work", true); + const ws = await store.setWorkspaceTitle("my-work", "Renamed"); + expect(ws.starred).toBe(true); + expect(ws.title).toBe("Renamed"); + }); + + it("setWorkspaceDefaultCwd preserves starred state", async () => { + const store = makeStore(); + await store.ensureWorkspace("my-work"); + await store.setWorkspaceStarred("my-work", true); + const ws = await store.setWorkspaceDefaultCwd("my-work", "/new/path"); + expect(ws.starred).toBe(true); + expect(ws.defaultCwd).toBe("/new/path"); + }); + + it("setWorkspaceDefaultComputerId preserves starred state", async () => { + const store = makeStore(); + await store.ensureWorkspace("my-work"); + await store.setWorkspaceStarred("my-work", true); + const ws = await store.setWorkspaceDefaultComputerId("my-work", "new-host"); + expect(ws.starred).toBe(true); + expect(ws.defaultComputerId).toBe("new-host"); + }); + + it("a legacy WorkspaceRow without starred reads back as false", async () => { + const store = makeStore(); + // Simulate a legacy row persisted before `starred` existed. + await storage.set( + "workspace:legacy", + JSON.stringify({ + title: "legacy", + defaultCwd: "/legacy/cwd", + defaultComputerId: null, + createdAt: 100, + lastActivityAt: 200, + }), + ); + const ws = await store.getWorkspace("legacy"); + expect(ws?.starred).toBe(false); + }); }); describe("ComputerStore", () => { @@ -497,6 +622,7 @@ describe("ComputerStore", () => { title: "brand-new", defaultCwd: null, defaultComputerId: "remote-host", + starred: false, createdAt: 5000, lastActivityAt: 5000, }); @@ -520,6 +646,7 @@ describe("ComputerStore", () => { title: "default", defaultCwd: null, defaultComputerId: null, + starred: false, createdAt: 0, lastActivityAt: 0, }); @@ -628,6 +755,7 @@ describe("ComputerStore", () => { title: "Remote", defaultCwd: null, defaultComputerId: "prod-server", + starred: false, createdAt: 1000, lastActivityAt: 1000, }); @@ -652,6 +780,7 @@ describe("ComputerStore", () => { title: "legacy", defaultCwd: "/legacy/cwd", defaultComputerId: null, + starred: false, createdAt: 100, lastActivityAt: 200, }); diff --git a/packages/conversation-store/src/store.ts b/packages/conversation-store/src/store.ts index 69334e6..ed39d8e 100644 --- a/packages/conversation-store/src/store.ts +++ b/packages/conversation-store/src/store.ts @@ -211,6 +211,13 @@ export interface ConversationStore { defaultComputerId: string | null, ) => Promise<Workspace>; /** + * Star or unstar a workspace. Creates the workspace if missing (like + * `setWorkspaceTitle`). Starred workspaces receive PRIORITY in the + * concurrency limiter queue — their agents jump ahead of agents from + * non-starred workspaces (oldest-agent-first within each group). + */ + readonly setWorkspaceStarred: (id: string, starred: boolean) => Promise<Workspace>; + /** * Delete a workspace: (1) find all conversations with `workspaceId === id`, * (2) set each to `status = "closed"` and reassign `workspaceId = "default"`, * (3) delete the workspace entity. Returns `closedCount`. Throws if `id @@ -358,6 +365,12 @@ interface WorkspaceRow { * workspace inherit it when they set no `computerId` of their own. */ readonly defaultComputerId: string | null; + /** + * Whether the workspace is starred by the user. Starred workspaces receive + * PRIORITY in the concurrency limiter queue. Defaults to `false` on legacy + * rows (normalized by `parseWorkspaceRow`). + */ + readonly starred: boolean; readonly createdAt: number; readonly lastActivityAt: number; } @@ -470,10 +483,13 @@ function parseWorkspaceRow(raw: string): WorkspaceRow | null { // (mirrors `defaultCwd`). Absent on legacy rows → null (local). const defaultComputerId = typeof row.defaultComputerId === "string" ? row.defaultComputerId : null; + // `starred` may be absent on legacy rows; treat anything non-boolean as false. + const starred = row.starred === true; return { title: row.title, defaultCwd, defaultComputerId, + starred, createdAt: row.createdAt, lastActivityAt: row.lastActivityAt, }; @@ -485,6 +501,7 @@ function toWorkspace(id: string, row: WorkspaceRow): Workspace { title: row.title, defaultCwd: row.defaultCwd, defaultComputerId: row.defaultComputerId, + starred: row.starred === true, createdAt: row.createdAt, lastActivityAt: row.lastActivityAt, }; @@ -545,6 +562,7 @@ export function createConversationStore( title: workspaceId, defaultCwd: null, defaultComputerId: null, + starred: false, createdAt: ts, lastActivityAt: ts, } @@ -552,6 +570,7 @@ export function createConversationStore( title: existing.title, defaultCwd: existing.defaultCwd, defaultComputerId: existing.defaultComputerId, + starred: existing.starred, createdAt: existing.createdAt, lastActivityAt: ts, }; @@ -1102,13 +1121,14 @@ export function createConversationStore( if (row !== null) return toWorkspace(id, row); // Synthesize the always-present "default" workspace when it was // never persisted (title "default", defaultCwd null, defaultComputerId - // null [local], timestamps 0). + // null [local], starred false, timestamps 0). if (id === DEFAULT_WORKSPACE_ID) { return { id: DEFAULT_WORKSPACE_ID, title: DEFAULT_WORKSPACE_ID, defaultCwd: null, defaultComputerId: null, + starred: false, createdAt: 0, lastActivityAt: 0, }; @@ -1126,6 +1146,7 @@ export function createConversationStore( title: opts?.title ?? id, defaultCwd: opts?.defaultCwd ?? null, defaultComputerId: opts?.defaultComputerId ?? null, + starred: false, createdAt: ts, lastActivityAt: ts, }; @@ -1142,6 +1163,7 @@ export function createConversationStore( title: id, defaultCwd: null as string | null, defaultComputerId: null as string | null, + starred: false as boolean, createdAt: ts, lastActivityAt: ts, } @@ -1150,6 +1172,7 @@ export function createConversationStore( title, defaultCwd: base.defaultCwd, defaultComputerId: base.defaultComputerId, + starred: base.starred, createdAt: base.createdAt, lastActivityAt: base.lastActivityAt, }; @@ -1166,6 +1189,7 @@ export function createConversationStore( title: id, defaultCwd: null as string | null, defaultComputerId: null as string | null, + starred: false as boolean, createdAt: ts, lastActivityAt: ts, } @@ -1174,6 +1198,7 @@ export function createConversationStore( title: base.title, defaultCwd, defaultComputerId: base.defaultComputerId, + starred: base.starred, createdAt: base.createdAt, lastActivityAt: base.lastActivityAt, }; @@ -1190,6 +1215,7 @@ export function createConversationStore( title: id, defaultCwd: null as string | null, defaultComputerId: null as string | null, + starred: false as boolean, createdAt: ts, lastActivityAt: ts, } @@ -1198,6 +1224,7 @@ export function createConversationStore( title: base.title, defaultCwd: base.defaultCwd, defaultComputerId, + starred: base.starred, createdAt: base.createdAt, lastActivityAt: base.lastActivityAt, }; @@ -1205,6 +1232,34 @@ export function createConversationStore( return toWorkspace(id, row); }, + async setWorkspaceStarred(id, starred) { + const existing = await readWorkspaceRow(id); + const ts = now(); + const base = + existing === null + ? { + title: id, + defaultCwd: null as string | null, + defaultComputerId: null as string | null, + createdAt: ts, + lastActivityAt: ts, + } + : existing; + const row: WorkspaceRow = { + title: base.title, + defaultCwd: base.defaultCwd, + defaultComputerId: base.defaultComputerId, + starred, + createdAt: base.createdAt, + lastActivityAt: base.lastActivityAt, + }; + await storage.set(workspaceKey(id), JSON.stringify(row)); + if (logger !== undefined) { + logger.debug("workspace starred set", { workspaceId: id, starred }); + } + return toWorkspace(id, row); + }, + async deleteWorkspace(id) { if (id === DEFAULT_WORKSPACE_ID) { throw new Error('The "default" workspace cannot be deleted.'); @@ -1269,6 +1324,7 @@ export function createConversationStore( title: DEFAULT_WORKSPACE_ID, defaultCwd: null, defaultComputerId: null, + starred: false, createdAt: 0, lastActivityAt: 0, }); |
