feat(cli): Add session owner index for workspace runtimes

Route live session ownership through a registry-backed owner index so multi-workspace sessions can resolve active sessions without scanning every bridge first. Expand trusted workspace load/resume and live read routing while keeping non-session surfaces primary-only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
doudouOUC 2026-07-08 20:32:44 +08:00
parent 955ad27fc7
commit a7f29a7cde
15 changed files with 1445 additions and 337 deletions

View file

@ -114,6 +114,22 @@ Attaches to existing sessions are NOT counted toward the cap, so an idle daemon'
Fired when a `session/load` is issued for an id that already has a `session/resume` in flight (or vice versa). Wait at least `Retry-After` seconds and retry — the underlying restore completes within `initTimeoutMs` (default 10s). Same-action races (`load` vs `load`, `resume` vs `resume`) coalesce instead of erroring.
`SessionWorkspaceConflictError` — emitted by `POST /session/:id/load` and `POST /session/:id/resume` when the requested `cwd` targets one registered workspace but the same session id is already live or being restored by another runtime — returns `409` with:
```json
{
"error": "Session \"<sid>\" is already live or restoring in another workspace runtime.",
"code": "session_workspace_conflict",
"sessionId": "<sid>",
"workspaceCwd": "/requested/workspace",
"workspaceId": "requested-workspace-id",
"liveWorkspaceCwd": "/live/owner/workspace",
"liveWorkspaceId": "live-owner-workspace-id"
}
```
Clients should retry with the owning workspace or wait for the in-flight restore to finish before restoring the id into a different workspace. Same-workspace restore races continue to use the bridge's `restore_in_progress` / coalescing behavior.
`SessionArchivedError` is emitted when a caller tries to load or resume a session whose JSONL is under `chats/archive/`:
```json
@ -415,7 +431,13 @@ the daemon log path; `full` may include it for authenticated operators.
"supported": ["v1"]
},
"mode": "http-bridge",
"features": ["health", "daemon_status", "capabilities", "multi_workspace_sessions", "..."],
"features": [
"health",
"daemon_status",
"capabilities",
"multi_workspace_sessions",
"..."
],
"limits": {
"maxPendingPromptsPerSession": 5,
"maxSessionsPerWorkspace": 20,
@ -1258,9 +1280,9 @@ Request:
}
```
| Field | Required | Notes |
| ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cwd` | no | Same canonicalization + `workspace_mismatch` rules as `POST /session`. Omit to inherit `/capabilities.workspaceCwd`. `mcpServers` is intentionally NOT accepted here — daemon-wide MCP is settings-driven (matches `POST /session`). |
| Field | Required | Notes |
| ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cwd` | no | Same canonicalization + `workspace_mismatch` rules as `POST /session`. Omit to inherit `/capabilities.workspaceCwd`. When `features` contains `multi_workspace_sessions`, callers may pass any trusted registered `workspaces[].cwd`; untrusted non-primary workspaces return `403 untrusted_workspace`. `mcpServers` is intentionally NOT accepted here — daemon-wide MCP is settings-driven (matches `POST /session`). |
Response:
@ -1287,8 +1309,10 @@ Response:
- `404` — persisted session id doesn't exist (`SessionNotFoundError`).
- `400``workspace_mismatch` (same shape as `POST /session`).
- `403``untrusted_workspace` when `cwd` targets an untrusted non-primary workspace.
- `503``session_limit_exceeded` (counts against `--max-sessions`; in-flight restores are accounted for too).
- `409``restore_in_progress` (a `session/resume` for the same id is already in flight). `Retry-After: 5`. Same-action races (two concurrent `session/load` for the same id) coalesce — exactly one returns `attached: false`, the rest return `attached: true` with the same `state`.
- `409``session_workspace_conflict` when the same session id is already live or being restored by another workspace runtime.
- `409``session_archived` when the id exists only under `chats/archive/`; call `POST /sessions/unarchive` before `load` or `resume`.
- `409``session_archiving` when archive or unarchive is in flight for the same id. `Retry-After: 5`.
- `409``session_conflict` when the id exists in both `chats/` and `chats/archive/`; delete the session with `POST /sessions/delete` before loading.
@ -1303,13 +1327,14 @@ Use `/load` when the client has no history rendered (cold reconnect, picker →
> ⚠️ **Why is `unstable_session_resume` still advertised?** The daemon's HTTP route and `session_resume` capability are stable for v1, but the bridge still calls ACP's `connection.unstable_resumeSession`. The old tag remains only so SDKs that shipped before `session_resume` can keep working.
### `GET /workspace/:id/sessions`
### `GET /workspace/:id/sessions` and `GET /workspaces/:workspace/sessions`
List persisted sessions whose canonical workspace matches `:id` (URL-encoded absolute cwd). The default list is active sessions from `chats/`; pass `archiveState=archived` to list archived sessions from `chats/archive/`. `archiveState=all` is not supported in v1. The default response and numeric `cursor` semantics are unchanged by `session_organization`.
List sessions whose canonical workspace matches `:id` or `:workspace`. The path parameter first resolves as an exact workspace id and then as a URL-encoded absolute cwd. `GET /workspaces/:workspace/sessions` is a plural alias with the same response shape. Primary workspaces include the existing persisted/live merge: the default list is active sessions from `chats/`; pass `archiveState=archived` to list archived sessions from `chats/archive/`. Non-primary workspaces are live-only in the current multi-workspace sessions surface and reject archived, organized, or grouped queries. `archiveState=all` is not supported in v1. The default response and numeric `cursor` semantics are unchanged by `session_organization`.
```bash
curl http://127.0.0.1:4170/workspace/$(jq -rn --arg c "$PWD" '$c|@uri')/sessions
curl http://127.0.0.1:4170/workspace/$(jq -rn --arg c "$PWD" '$c|@uri')/sessions?archiveState=archived
curl http://127.0.0.1:4170/workspaces/<workspace-id>/sessions
```
Query parameters: