summaryrefslogtreecommitdiffhomepage
path: root/packages/conversation-store/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-28 12:59:07 +0900
committerAdam Malczewski <[email protected]>2026-06-28 12:59:07 +0900
commit71c635f7d8ee01a2b23d5ddfdfc4bff043980052 (patch)
tree1285beba4405e8d0955dcee340a1180aa6ce3a46 /packages/conversation-store/src
parentb83aa8ddbb7023ae9dd0332b4989d4baa11522af (diff)
downloaddispatch-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.ts129
-rw-r--r--packages/conversation-store/src/store.ts58
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,
});