Sessions: account a restored Session to its Workspace

Ensure() only registered a Workspace when the Session was new, so Sessions restored from disk never entered the Workspace record and stayed invisible in the panel. Always create (idempotent) and attach an unaccounted Session; warn on attach failure. Tests gain a shared Workspace fixture.
This commit is contained in:
2026-11-02 04:28:26 +08:00
parent 3bee231da6
commit d527ad8045
4 changed files with 40 additions and 7 deletions
+25 -2
View File
@@ -85,6 +85,28 @@ export function jobRegistry(owner: (sessionId: string) => FakeAgent | undefined
run: (owner: string, label?: string) => { const id = `bash-${jobs.size + 1}`; jobs.set(id, { id, owner, status: 'running', label }); return id } }
}
/**
* DSH's Workspace registry as the Sessions use it: one Workspace per directory, which `create` resolves idempotently by path
* and which accounts the Sessions a launch attaches. `attach` records an attachment per Session so a test can assert that a
* Session restored without its Workspace registration is put back into one.
*/
export function workspaceRegistry() {
const byPath = new Map<string, { id: string; path: string; sessionIds: string[]; attachSession: ReturnType<typeof vi.fn> }>()
const create = vi.fn(async (cwd: string) => {
const existing = byPath.get(cwd)
if (existing) return existing
const workspace = {
id: `workspace-${byPath.size + 1}`,
path: cwd,
sessionIds: [] as string[],
attachSession: vi.fn(async (sessionId: string) => { if (!workspace.sessionIds.includes(sessionId)) workspace.sessionIds.push(sessionId) }),
}
byPath.set(cwd, workspace)
return workspace
})
return { service: { create }, create, byPath }
}
/**
* DSH's subagents as the Executor uses them. `spawn` starts a child of a Session, an agent while it lives, which a drain of
* the Session's descendants refuses from then on; `settle` ends one, whose end wakes its idle parent or steers a running one
@@ -238,8 +260,9 @@ export async function startRuntime(seed?: (store: Store) => void) {
const webServer = { host: '127.0.0.1', port: 3082, register: (route: { handler: HttpHandler }) => { http = route.handler; return () => {} } }
const jobs = jobRegistry(id => agents.get(id))
const subagents = subagentRuntime(agents)
const workspaces = workspaceRegistry()
const services = { agents, llm: { resolveCallConfig: vi.fn(async (value: unknown) => value) }, sessionController: controller,
sessionPersistence: persistence, workspaceRegistry: { create: async (cwd: string) => ({ id: cwd }) }, permissionPresets: { set: () => {} }, subprocess, webServer,
sessionPersistence: persistence, workspaceRegistry: workspaces.service, permissionPresets: { set: () => {} }, subprocess, webServer,
jobs: jobs.service, subagents: subagents.service }
const ctx = new Context()
for (const [name, service] of Object.entries(services)) ctx.reflect.provide(name, service)
@@ -294,6 +317,6 @@ export async function startRuntime(seed?: (store: Store) => void) {
/** A step of the worker claims what waits for it, and is cut off before the Session took it. */
const claim = (sessionId: string) => { claims.get(sessionId)!() }
return { runtime, ctx, directory, agents, tools, deliveries, relays, candidates, runTool, route, acceptedTicket, api, controller, subprocess, hooks, resolveDefault, stop,
jobs, subagents, step, claim }
jobs, subagents, workspaces, step, claim }
}
export type Host = Awaited<ReturnType<typeof startRuntime>>