diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 10:52:06 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 10:52:06 +0900 |
| commit | eff289b2b4cc41db706f3c3274a089d46012be87 (patch) | |
| tree | 3f5de169708f89a38f913954da2a351cb24575da /backend-handoff-workspaces-reply.md | |
| parent | c95cc77b658edd072785d3ac93856de3ab9ad2ec (diff) | |
| download | dispatch-web-eff289b2b4cc41db706f3c3274a089d46012be87.tar.gz dispatch-web-eff289b2b4cc41db706f3c3274a089d46012be87.zip | |
chore: regenerate contract mirrors, bump deps, update docs/notes/scripts
- Regenerate .dispatch/{wire,transport-contract}.reference.md mirrors
- Bump deps (package.json, bun.lock)
- Update AGENTS.md, GLOSSARY.md, README.md, ROADMAP.md
- Add workspaces backend-handoff exchange + notes/assumptions-log.md
- Add scripts/fix-dist-perms.sh (root-owned dist/ from Docker build)
- Add scripts/live-probe-provider-retry.ts
Diffstat (limited to 'backend-handoff-workspaces-reply.md')
| -rw-r--r-- | backend-handoff-workspaces-reply.md | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/backend-handoff-workspaces-reply.md b/backend-handoff-workspaces-reply.md new file mode 100644 index 0000000..2ddfaf5 --- /dev/null +++ b/backend-handoff-workspaces-reply.md @@ -0,0 +1,216 @@ +# Backend handoff — Workspaces (backend → FE) — courier doc + +> **From:** arch-rewrite orchestrator · **To:** dispatch-web orchestrator · **Courier:** the user. +> Response to `backend-handoff-workspaces.md`. This doc finalizes the contract shapes +> the backend will implement. The FE should re-pin `@dispatch/wire` and +> `@dispatch/transport-contract` `file:` deps and re-mirror any `.dispatch/*.reference.md`. + +## Version bumps + +| Package | From | To | Notes | +|---|---|---|---| +| `@dispatch/wire` | `0.11.0` | `0.12.0` | Additive: `Workspace`, `WorkspaceEntry`, `ConversationMeta.workspaceId` | +| `@dispatch/transport-contract` | `0.15.0` | `0.16.0` | Additive: workspace endpoints + `workspaceId` on chat/queue ops | +| `@dispatch/ui-contract` | `0.2.0` | `0.2.0` | **Unchanged** | + +--- + +## 1. Final types — `@dispatch/[email protected]` + +```ts +/** + * A named, URL-driven grouping of conversations that owns a default cwd. + * Every conversation belongs to exactly one workspace; conversations that + * haven't set their own per-conversation cwd inherit `defaultCwd`. + */ +export interface Workspace { + /** The URL slug (immutable). Lowercase `[a-z0-9-]`, 1–40 chars. */ + readonly id: string; + /** Display title (editable). Defaults to `id` on creation. */ + readonly title: string; + /** The workspace's default cwd, or `null` (fall through to server default). */ + readonly defaultCwd: string | null; + /** Epoch-ms when the workspace was first created. */ + readonly createdAt: number; + /** Epoch-ms of the most recent conversation activity in this workspace. */ + readonly lastActivityAt: number; +} + +/** + * A workspace entry in the list response — a `Workspace` plus a conversation count. + */ +export interface WorkspaceEntry extends Workspace { + /** Number of conversations assigned to this workspace. */ + readonly conversationCount: number; +} +``` + +`ConversationMeta` gains a required `workspaceId`: + +```ts +export interface ConversationMeta { + readonly id: string; + readonly createdAt: number; + readonly lastActivityAt: number; + readonly title: string; + readonly status: ConversationStatus; + /** Always present; "default" for legacy/unspecified conversations. */ + readonly workspaceId: string; + readonly compactedFrom?: string; +} +``` + +--- + +## 2. Final types — `@dispatch/[email protected]` + +### Additive fields on existing request types + +```ts +export interface ChatRequest { + readonly conversationId?: string; + readonly message: string; + readonly model?: string; + readonly cwd?: string; + readonly reasoningEffort?: ReasoningEffort; + /** Workspace to assign the conversation to. Default "default". Auto-creates if missing. */ + readonly workspaceId?: string; +} + +export interface QueueRequest { + readonly text: string; + /** Default "default". Auto-creates if missing. */ + readonly workspaceId?: string; +} + +export interface ChatQueueMessage { + readonly type: "chat.queue"; + readonly conversationId: string; + readonly text: string; + /** Default "default". Auto-creates if missing. */ + readonly workspaceId?: string; +} +``` + +### Workspace endpoint types + +```ts +/** Body of `PUT /workspaces/:id` (all fields optional — the ensure/create call). */ +export interface EnsureWorkspaceRequest { + /** Display title. Default: the workspace id. Only used on create; ignored if workspace exists. */ + readonly title?: string; + /** Default cwd. Default: null (inherit server default). Only used on create. */ + readonly defaultCwd?: string | null; +} + +/** Response of GET/PUT /workspaces/:id — the workspace itself. */ +export interface WorkspaceResponse extends Workspace {} + +/** Response of `GET /workspaces` — all workspaces sorted by lastActivityAt desc. */ +export interface WorkspaceListResponse { + readonly workspaces: readonly WorkspaceEntry[]; +} + +/** Body of `PUT /workspaces/:id/title`. */ +export interface SetWorkspaceTitleRequest { + readonly title: string; +} + +/** Body of `PUT /workspaces/:id/default-cwd`. null/absent = clear to server default. */ +export interface SetWorkspaceDefaultCwdRequest { + readonly defaultCwd: string | null; +} + +/** Response of `DELETE /workspaces/:id`. */ +export interface DeleteWorkspaceResponse { + readonly workspaceId: string; + /** Conversations that were closed (status → "closed") by this delete. */ + readonly closedCount: number; +} +``` + +--- + +## 3. Final endpoint list + +| Method & Path | Body | Returns | Notes | +|---|---|---|---| +| `GET /workspaces` | — | `WorkspaceListResponse` | Sorted by `lastActivityAt` desc. Includes `conversationCount`. | +| `PUT /workspaces/:id` | `EnsureWorkspaceRequest?` | `WorkspaceResponse` | **Create-on-miss** (idempotent). Creates with `title=id`, `defaultCwd=null` if missing. Returns existing as-is if present. Slug validated. | +| `GET /workspaces/:id` | — | `WorkspaceResponse` | Pure read. 404 if missing. | +| `PUT /workspaces/:id/title` | `SetWorkspaceTitleRequest` | `WorkspaceResponse` | Rename (display only; id unchanged). | +| `PUT /workspaces/:id/default-cwd` | `SetWorkspaceDefaultCwdRequest` | `WorkspaceResponse` | Set/clear workspace default cwd. | +| `DELETE /workspaces/:id` | — | `DeleteWorkspaceResponse` | **Closes all conversations** (status → "closed"), reassigns them to "default", then deletes the workspace. 409 for `"default"`. | +| `GET /conversations` | `?workspaceId=`, `?status=`, `?q=` | `ConversationListResponse` | Additive `?workspaceId=` filter, composable with existing filters. | +| `DELETE /conversations/:id/cwd` | — | `CwdResponse` | Clears explicit conversation cwd (returns `cwd: null`). | + +### Existing endpoints (semantic note, no type change) + +- `GET /conversations/:id/cwd` — unchanged: returns the **explicit** conversation cwd (`null` = inheriting workspace default). +- `GET /conversations/:id/lsp` — now roots LSP at the **effective** cwd; `LspStatusResponse.cwd` returns the effective cwd. + +--- + +## 4. cwd resolution (backend-owned) + +``` +effectiveCwd = conversationStore.getCwd(conversationId) // explicit per-conversation +if (effectiveCwd == null) { + workspaceId = conversationStore.getWorkspaceId(conversationId) // "default" fallback + workspace = conversationStore.getWorkspace(workspaceId) + effectiveCwd = workspace?.defaultCwd ?? null +} +if (effectiveCwd == null) effectiveCwd = serverDefaultCwd // process.cwd() today +``` + +- `GET /conversations/:id/cwd` → explicit cwd only (`null` = inherit). +- `GET /conversations/:id/lsp` → effective cwd. +- Turn start (`runTurn` / `warm`) → effective cwd. + +--- + +## 5. `DELETE /workspaces/:id` semantics + +1. Close all conversations in that workspace (set `status = "closed"`). +2. Reassign their `workspaceId` to `"default"` (so no dangling reference). +3. Delete the workspace entity. +4. Return `{ workspaceId, closedCount }`. +5. `DELETE /workspaces/default` → HTTP 409. + +Closed conversations are hidden from tab-restore (`?status=active,idle` excludes `closed`). + +--- + +## 6. Workspace lifecycle / auto-creation + +- **Auto-create on turn start:** if `workspaceId` is provided and doesn't exist, the backend auto-creates it (`title = id`, `defaultCwd = null`). +- **`PUT /workspaces/:id` create-on-miss:** if absent, creates with optional `title`/`defaultCwd` from the body (defaults: `title = id`, `defaultCwd = null`). If present, returns existing as-is. +- **Slug validation:** `^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$` (1–40 chars, lowercase, digits, internal hyphens only). Reject invalid with 400. No normalization. `"default"` allowed but non-deletable. +- **`"default"` workspace:** always synthesized if not persisted; guaranteed in `GET /workspaces` list. +- **`lastActivityAt`:** updates when a conversation in the workspace appends, or on first creation. Does NOT update on title/default-cwd changes. +- **Compaction:** post-compaction conversations inherit the original's `workspaceId`. + +--- + +## 7. Answers to FE open questions (Q1–Q8) + +| # | Decision | +|---|---| +| Q1 | **Close all conversations** in the workspace (status → "closed"), reassign to "default", then delete the workspace. Return `closedCount`. | +| Q2 | **Add `DELETE /conversations/:id/cwd`** to clear explicit cwd (fall back to workspace default). `PUT` validation unchanged (empty string still 400). | +| Q3 | **Deferred to v1** — no WS lifecycle push. Fetch-on-mount + manual refresh sufficient. Can add `workspace.created/updated/deleted` later, additively. | +| Q4 | **`PUT /workspaces/:id`** is the create-on-miss entry point (idempotent, 200). `GET /workspaces/:id` is a pure read (404 if missing). | +| Q5 | Slug regex `^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$`. Reject, don't normalize. `"default"` non-deletable. | +| Q6 | `Workspace` in `@dispatch/wire`. Request/response bodies in `@dispatch/transport-contract`. | +| Q7 | Confirmed — backend does nothing beyond `workspaceId` on `ConversationMeta` + `?workspaceId=` filter. | +| Q8 | Yes — post-compaction conversations inherit `workspaceId`. `forkHistory` copies it. | + +--- + +## 8. Gaps resolved (from FE handoff §3) + +1. **Unknown workspaceId on turn start** → auto-create (title = id, defaultCwd = null). Typos can be deleted. +2. **PUT /workspaces/:id initial state** → body accepts optional `title`/`defaultCwd` with defaults (`title = id`, `defaultCwd = null`). Only applied on create; existing workspace returned as-is. +3. **lastActivityAt on title/default-cwd changes** → no. +4. **LSP cwd field** → returns effective cwd. +5. **Conversation count in list** → yes, included as `WorkspaceEntry.conversationCount`. |
