Key agent placement by workspace id, not directory
Agent project-placement and the fetch/list grouping still resolved a workspace from the agent's directory, even though every agent now carries a workspaceId. Walk workspaceId -> workspace -> project instead: an agent whose cwd is shared with or nested under another workspace now groups under its own workspace, not a directory-prefix match. The fetch path's placement map is keyed by workspace id, closing the same-directory collision. resolveWorkspaceIdForPath survives only at the archive-by-path boundary (auto-archive-on-merge, the MCP archive tool), where a client passes a bare worktree path with no id; renamed to match that role.
This commit is contained in:
@@ -113,7 +113,7 @@ import { LoopService } from "./loop-service.js";
|
||||
import { ScheduleService } from "./schedule/service.js";
|
||||
import { DaemonConfigStore } from "./daemon-config-store.js";
|
||||
import { WorkspaceGitServiceImpl } from "./workspace-git-service.js";
|
||||
import { resolveWorkspaceIdForPath } from "./workspace-ownership.js";
|
||||
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
|
||||
import { archivePersistedWorkspaceRecord } from "./workspace-archive-service.js";
|
||||
import { setupAutoArchiveOnMerge } from "./auto-archive-on-merge/index.js";
|
||||
import type { ActiveWorkspaceRef } from "./paseo-worktree-archive-service.js";
|
||||
|
||||
+3
-4
@@ -3,7 +3,7 @@ import { basename } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createPersistedWorkspaceRecord } from "./workspace-registry.js";
|
||||
import { resolveWorkspaceIdForPath } from "./workspace-ownership.js";
|
||||
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
|
||||
|
||||
function createWorkspaceRecord(
|
||||
cwd: string,
|
||||
@@ -23,9 +23,8 @@ function createWorkspaceRecord(
|
||||
}
|
||||
|
||||
// resolveWorkspaceIdForPath is the external path→workspace adapter for
|
||||
// archive-by-path and agent project-placement display. It is NOT ownership: a
|
||||
// record's owner is its workspaceId. The per-id status law is exercised in
|
||||
// workspace-directory.test.ts.
|
||||
// archive-by-path. It is NOT ownership: a record's owner is its workspaceId.
|
||||
// The per-id status law is exercised in workspace-directory.test.ts.
|
||||
|
||||
describe("resolveWorkspaceIdForPath", () => {
|
||||
test("returns a single id when multiple workspaces share the exact cwd", () => {
|
||||
+6
-6
@@ -5,12 +5,12 @@ import type { PersistedWorkspaceRecord } from "./workspace-registry.js";
|
||||
|
||||
// external path→workspace adapter, not ownership.
|
||||
//
|
||||
// Resolves a raw filesystem path to a single workspace id for the two flows that
|
||||
// receive a path rather than an id: archive-by-path (an old client / CLI passes a
|
||||
// worktree path) and agent project-placement display (an agent's cwd, which may
|
||||
// be a subdirectory of its workspace). This is NEVER used to attribute a record's
|
||||
// status to a workspace — status is per `workspaceId`, and git facts derive from a
|
||||
// workspace's OWN cwd (id → cwd).
|
||||
// Resolves a raw filesystem path to a single workspace id ONLY at the boundary
|
||||
// where a client hands the daemon a bare worktree path with no id:
|
||||
// archive-by-path (old client / CLI), auto-archive-after-merge, and the MCP
|
||||
// `archive_worktree` tool. It is NEVER used to attribute agent status or place
|
||||
// agents under a workspace — those are keyed by `workspaceId`, and git facts
|
||||
// derive from a workspace's OWN cwd (id → cwd).
|
||||
//
|
||||
// Resolution: an exact directory match wins; otherwise the deepest enclosing
|
||||
// workspace directory, never the home directory; null when nothing encloses it.
|
||||
@@ -159,11 +159,10 @@ import {
|
||||
import {
|
||||
checkoutLiteFromGitSnapshot,
|
||||
classifyDirectoryForProjectMembership,
|
||||
deriveProjectGroupingName,
|
||||
deriveWorkspaceDisplayName,
|
||||
generateWorkspaceId,
|
||||
} from "./workspace-registry-model.js";
|
||||
import { resolveWorkspaceIdForPath } from "./workspace-ownership.js";
|
||||
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
|
||||
import {
|
||||
createPersistedProjectRecord,
|
||||
createPersistedWorkspaceRecord,
|
||||
@@ -1648,19 +1647,6 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private async findWorkspaceByDirectory(
|
||||
cwd: string,
|
||||
options?: { refreshGit?: boolean },
|
||||
): Promise<PersistedWorkspaceRecord | null> {
|
||||
const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
|
||||
const workspaces = await this.workspaceRegistry.list();
|
||||
const workspaceId = resolveWorkspaceIdForPath(normalizedCwd, workspaces);
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
}
|
||||
return workspaces.find((workspace) => workspace.workspaceId === workspaceId) ?? null;
|
||||
}
|
||||
|
||||
private async findExactWorkspaceByDirectory(
|
||||
cwd: string,
|
||||
options?: { refreshGit?: boolean },
|
||||
@@ -1702,37 +1688,11 @@ export class Session {
|
||||
};
|
||||
}
|
||||
|
||||
private async buildProjectPlacementForCwd(
|
||||
cwd: string,
|
||||
options?: { refreshGit?: boolean; fallback?: boolean },
|
||||
private async buildProjectPlacementForWorkspaceId(
|
||||
workspaceId: string,
|
||||
): Promise<ProjectPlacementPayload | null> {
|
||||
const workspace = await this.findWorkspaceByDirectory(cwd, {
|
||||
refreshGit: options?.refreshGit,
|
||||
});
|
||||
if (!workspace) {
|
||||
if (!options?.fallback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// An agent can run in a directory that has no registered workspace yet
|
||||
// (e.g. a fresh non-git folder). Synthesize a directory-scoped placement so
|
||||
// the agent still emits updates. projectKey/cwd are paths here — a project
|
||||
// grouping key, not a workspace id — so this stays opaque-id-safe.
|
||||
const resolvedCwd = resolve(cwd);
|
||||
return {
|
||||
projectKey: resolvedCwd,
|
||||
projectName: deriveProjectGroupingName(resolvedCwd),
|
||||
checkout: {
|
||||
cwd: resolvedCwd,
|
||||
isGit: false,
|
||||
currentBranch: null,
|
||||
remoteUrl: null,
|
||||
worktreeRoot: null,
|
||||
isPaseoOwnedWorktree: false,
|
||||
mainRepoRoot: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
const workspace = await this.workspaceRegistry.get(workspaceId);
|
||||
if (!workspace) return null;
|
||||
return this.buildProjectPlacementForWorkspace(workspace);
|
||||
}
|
||||
|
||||
@@ -1741,30 +1701,33 @@ export class Session {
|
||||
const subscription = this.agentUpdatesSubscription;
|
||||
const payload = await this.buildAgentPayload(agent);
|
||||
if (subscription) {
|
||||
const project = await this.buildProjectPlacementForCwd(payload.cwd, {
|
||||
refreshGit: false,
|
||||
fallback: true,
|
||||
});
|
||||
const project = payload.workspaceId
|
||||
? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
|
||||
: null;
|
||||
if (!project) {
|
||||
throw new Error(`Workspace not found for agent ${payload.id}`);
|
||||
}
|
||||
const matches = this.matchesAgentFilter({
|
||||
agent: payload,
|
||||
project,
|
||||
filter: subscription.filter,
|
||||
});
|
||||
|
||||
if (matches) {
|
||||
this.bufferOrEmitAgentUpdate(subscription, {
|
||||
kind: "upsert",
|
||||
agent: payload,
|
||||
project,
|
||||
});
|
||||
} else {
|
||||
this.bufferOrEmitAgentUpdate(subscription, {
|
||||
kind: "remove",
|
||||
agentId: payload.id,
|
||||
});
|
||||
} else {
|
||||
const matches = this.matchesAgentFilter({
|
||||
agent: payload,
|
||||
project,
|
||||
filter: subscription.filter,
|
||||
});
|
||||
|
||||
if (matches) {
|
||||
this.bufferOrEmitAgentUpdate(subscription, {
|
||||
kind: "upsert",
|
||||
agent: payload,
|
||||
project,
|
||||
});
|
||||
} else {
|
||||
this.bufferOrEmitAgentUpdate(subscription, {
|
||||
kind: "remove",
|
||||
agentId: payload.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2474,7 +2437,9 @@ export class Session {
|
||||
|
||||
if (this.agentUpdatesSubscription) {
|
||||
const payload = this.buildStoredAgentPayload(archivedRecord);
|
||||
const project = await this.buildProjectPlacementForCwd(payload.cwd);
|
||||
const project = payload.workspaceId
|
||||
? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
|
||||
: null;
|
||||
if (project) {
|
||||
const matches = this.matchesAgentFilter({
|
||||
agent: payload,
|
||||
@@ -6450,7 +6415,7 @@ export class Session {
|
||||
return this.isProviderVisibleToClient(payload.provider) ? payload : null;
|
||||
}
|
||||
|
||||
private async buildActiveProjectPlacementsByWorkspaceCwd(): Promise<
|
||||
private async buildActiveProjectPlacementsByWorkspaceId(): Promise<
|
||||
Map<string, ProjectPlacementPayload>
|
||||
> {
|
||||
const [persistedWorkspaces, persistedProjects] = await Promise.all([
|
||||
@@ -6462,7 +6427,7 @@ export class Session {
|
||||
.filter((project) => !project.archivedAt)
|
||||
.map((project) => [project.projectId, project] as const),
|
||||
);
|
||||
const placementsByCwd = new Map<string, ProjectPlacementPayload>();
|
||||
const placementsByWorkspaceId = new Map<string, ProjectPlacementPayload>();
|
||||
|
||||
const pairs = persistedWorkspaces.flatMap((workspace) => {
|
||||
if (workspace.archivedAt) return [];
|
||||
@@ -6476,16 +6441,16 @@ export class Session {
|
||||
),
|
||||
);
|
||||
for (let i = 0; i < pairs.length; i += 1) {
|
||||
placementsByCwd.set(resolve(pairs[i].workspace.cwd), placements[i]);
|
||||
placementsByWorkspaceId.set(pairs[i].workspace.workspaceId, placements[i]);
|
||||
}
|
||||
|
||||
return placementsByCwd;
|
||||
return placementsByWorkspaceId;
|
||||
}
|
||||
|
||||
private async collectFetchAgentsEntries(params: {
|
||||
candidates: AgentSnapshotPayload[];
|
||||
limit: number;
|
||||
getPlacement: (cwd: string) => Promise<ProjectPlacementPayload | null>;
|
||||
getPlacement: (workspaceId: string | undefined) => Promise<ProjectPlacementPayload | null>;
|
||||
filter: AgentUpdatesFilter | undefined;
|
||||
}): Promise<FetchAgentsResponseEntry[]> {
|
||||
const { candidates, limit, getPlacement, filter } = params;
|
||||
@@ -6499,7 +6464,7 @@ export class Session {
|
||||
const batch = candidates.slice(start, start + batchSize);
|
||||
const batchEntries = await Promise.all(
|
||||
batch.map(async (agent) => {
|
||||
const project = await getPlacement(agent.cwd);
|
||||
const project = await getPlacement(agent.workspaceId);
|
||||
return project ? { agent, project } : null;
|
||||
}),
|
||||
);
|
||||
@@ -6542,25 +6507,33 @@ export class Session {
|
||||
includeArchived: filter?.includeArchived,
|
||||
includeUnavailablePersisted: request.type === "fetch_agent_history_request",
|
||||
});
|
||||
const activePlacementsByCwd =
|
||||
scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceCwd() : null;
|
||||
if (activePlacementsByCwd) {
|
||||
const activePlacementsByWorkspaceId =
|
||||
scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceId() : null;
|
||||
if (activePlacementsByWorkspaceId) {
|
||||
agents = agents.filter(
|
||||
(agent) => !agent.archivedAt && activePlacementsByCwd.has(resolve(agent.cwd)),
|
||||
(agent) =>
|
||||
!agent.archivedAt &&
|
||||
agent.workspaceId != null &&
|
||||
activePlacementsByWorkspaceId.has(agent.workspaceId),
|
||||
);
|
||||
}
|
||||
|
||||
const placementByCwd = new Map<string, Promise<ProjectPlacementPayload | null>>();
|
||||
const getPlacement = (cwd: string): Promise<ProjectPlacementPayload | null> => {
|
||||
if (activePlacementsByCwd) {
|
||||
return Promise.resolve(activePlacementsByCwd.get(resolve(cwd)) ?? null);
|
||||
const placementByWorkspaceId = new Map<string, Promise<ProjectPlacementPayload | null>>();
|
||||
const getPlacement = (
|
||||
workspaceId: string | undefined,
|
||||
): Promise<ProjectPlacementPayload | null> => {
|
||||
if (!workspaceId) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const existing = placementByCwd.get(cwd);
|
||||
if (activePlacementsByWorkspaceId) {
|
||||
return Promise.resolve(activePlacementsByWorkspaceId.get(workspaceId) ?? null);
|
||||
}
|
||||
const existing = placementByWorkspaceId.get(workspaceId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const placementPromise = this.buildProjectPlacementForCwd(cwd);
|
||||
placementByCwd.set(cwd, placementPromise);
|
||||
const placementPromise = this.buildProjectPlacementForWorkspaceId(workspaceId);
|
||||
placementByWorkspaceId.set(workspaceId, placementPromise);
|
||||
return placementPromise;
|
||||
};
|
||||
|
||||
@@ -8197,7 +8170,7 @@ export class Session {
|
||||
};
|
||||
await this.agentStorage.upsert(nextRecord);
|
||||
const agent = this.buildStoredAgentPayload(nextRecord);
|
||||
const project = await this.buildProjectPlacementForCwd(agent.cwd);
|
||||
const project = await this.buildProjectPlacementForWorkspace(workspace);
|
||||
this.emit({
|
||||
type: "agent_update",
|
||||
payload: {
|
||||
@@ -8276,7 +8249,9 @@ export class Session {
|
||||
return;
|
||||
}
|
||||
|
||||
const project = await this.buildProjectPlacementForCwd(agent.cwd);
|
||||
const project = agent.workspaceId
|
||||
? await this.buildProjectPlacementForWorkspaceId(agent.workspaceId)
|
||||
: null;
|
||||
this.emit({
|
||||
type: "fetch_agent_response",
|
||||
payload: { requestId, agent, project, error: null },
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type PersistedProjectRecord,
|
||||
type PersistedWorkspaceRecord,
|
||||
} from "./workspace-registry.js";
|
||||
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
|
||||
|
||||
interface Harness {
|
||||
session: Session;
|
||||
@@ -456,17 +457,17 @@ test("S11: re-opening an archived project by exact path unarchives project + wor
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// S12. Prefix-fallback resolver must not surface an archived ancestor: looking
|
||||
// up a child cwd whose only matching record is an archived parent should
|
||||
// return null (not the archived parent).
|
||||
// return null (not the archived parent). The behavior lives in
|
||||
// `resolveWorkspaceIdForPath` now; this invariant keeps archive-by-path
|
||||
// from accidentally resolving into an archived workspace subtree.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
test("S12: findWorkspaceByDirectory does not return archived ancestor via prefix fallback", async () => {
|
||||
test("S12: resolveWorkspaceIdForPath does not return archived ancestor via prefix fallback", async () => {
|
||||
const archivedAt = "2026-04-22T13:08:05.400Z";
|
||||
const h = createHarness({
|
||||
workspaces: [dirWorkspace(PARENT, archivedAt)],
|
||||
projects: [dirProject(PARENT, archivedAt)],
|
||||
});
|
||||
const found = await asInternals<{
|
||||
findWorkspaceByDirectory(cwd: string): Promise<unknown>;
|
||||
}>(h.session).findWorkspaceByDirectory(PARENT_CHILD);
|
||||
const found = resolveWorkspaceIdForPath(PARENT_CHILD, Array.from(h.workspaces.values()));
|
||||
expect(found).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ interface SessionTestAccess {
|
||||
listFetchAgentsEntries(params: unknown): Promise<ListFetchResult>;
|
||||
resolveAgentIdentifier(identifier: string): Promise<unknown>;
|
||||
getAgentPayloadById(agentId: string): Promise<unknown>;
|
||||
buildProjectPlacementForCwd(cwd: string): Promise<unknown>;
|
||||
buildProjectPlacementForWorkspaceId(workspaceId: string): Promise<unknown>;
|
||||
buildProjectPlacement(cwd: string): Promise<unknown>;
|
||||
buildWorkspaceDescriptorMap(...args: unknown[]): Promise<Map<string, unknown>>;
|
||||
describeWorkspaceRecord(...args: unknown[]): Promise<unknown>;
|
||||
@@ -250,6 +250,7 @@ function makeStoredAgent(input: {
|
||||
function makeManagedAgent(input: {
|
||||
id: string;
|
||||
cwd: string;
|
||||
workspaceId?: string;
|
||||
lifecycle: AgentSnapshotPayload["status"];
|
||||
updatedAt: string;
|
||||
}) {
|
||||
@@ -257,6 +258,7 @@ function makeManagedAgent(input: {
|
||||
const snapshot = makeAgent({
|
||||
id: input.id,
|
||||
cwd: input.cwd,
|
||||
workspaceId: input.workspaceId,
|
||||
status: input.lifecycle,
|
||||
updatedAt: input.updatedAt,
|
||||
});
|
||||
@@ -765,9 +767,11 @@ test("create_agent_request keeps requested child cwd when grouped under an exist
|
||||
|
||||
const [createdAgent] = agentManager.listAgents();
|
||||
expect(createdAgent?.cwd).toBe(child);
|
||||
await expect(session.buildProjectPlacementForCwd(createdAgent.cwd)).resolves.toMatchObject({
|
||||
projectKey: "proj-parent",
|
||||
checkout: { cwd: parent },
|
||||
await expect(
|
||||
session.buildProjectPlacementForWorkspaceId(createdAgent!.workspaceId!),
|
||||
).resolves.toMatchObject({
|
||||
projectKey: parent,
|
||||
checkout: { cwd: child },
|
||||
});
|
||||
expect(findByType(emitted, "status")?.payload).toMatchObject({
|
||||
status: "agent_created",
|
||||
@@ -930,6 +934,8 @@ test("agent_update placement does not refresh git snapshots", async () => {
|
||||
|
||||
session.projectRegistry.get = async (id: string) => (id === project.projectId ? project : null);
|
||||
session.workspaceRegistry.list = async () => [workspace];
|
||||
session.workspaceRegistry.get = async (id: string) =>
|
||||
id === workspace.workspaceId ? workspace : null;
|
||||
session.agentUpdatesSubscription = {
|
||||
subscriptionId: "sub-agents",
|
||||
filter: {},
|
||||
@@ -941,6 +947,7 @@ test("agent_update placement does not refresh git snapshots", async () => {
|
||||
makeManagedAgent({
|
||||
id: "agent-1",
|
||||
cwd: REPO_CWD,
|
||||
workspaceId: workspace.workspaceId,
|
||||
lifecycle: "running",
|
||||
updatedAt: "2026-03-30T15:00:00.000Z",
|
||||
}),
|
||||
@@ -960,7 +967,7 @@ test("agent_update placement does not refresh git snapshots", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("agent_update emits a directory-scoped placement when no workspace is registered", async () => {
|
||||
test("agent_update emits remove when the agent has no workspaceId", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const getSnapshot = vi.fn(async () => {
|
||||
throw new Error("getSnapshot should not be called for unregistered agent_update placement");
|
||||
@@ -993,15 +1000,11 @@ test("agent_update emits a directory-scoped placement when no workspace is regis
|
||||
);
|
||||
|
||||
expect(getSnapshot).not.toHaveBeenCalled();
|
||||
// An agent in a directory with no registered workspace must still emit an
|
||||
// upsert (live model/thinking switches depend on this). It gets a directory-
|
||||
// scoped placement keyed by the path — a project key, not a workspace id.
|
||||
const update = emitted.find((message) => message.type === "agent_update");
|
||||
if (update?.type !== "agent_update" || update.payload.kind !== "upsert") {
|
||||
throw new Error("expected an agent_update upsert");
|
||||
}
|
||||
expect(update.payload.agent.id).toBe("agent-1");
|
||||
expect(update.payload.project?.checkout.isGit).toBe(false);
|
||||
expect(update?.payload).toMatchObject({
|
||||
kind: "remove",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
});
|
||||
|
||||
test("archive emits an authoritative agent_update upsert for subscribed clients", async () => {
|
||||
@@ -1010,6 +1013,7 @@ test("archive emits an authoritative agent_update upsert for subscribed clients"
|
||||
id: "agent-1",
|
||||
provider: "codex",
|
||||
cwd: REPO_CWD,
|
||||
workspaceId: "ws-1",
|
||||
createdAt: "2026-03-30T15:00:00.000Z",
|
||||
updatedAt: "2026-03-30T15:00:00.000Z",
|
||||
lastActivityAt: "2026-03-30T15:00:00.000Z",
|
||||
@@ -1379,6 +1383,7 @@ test("close_items_request archives agents and kills terminals in one batch", asy
|
||||
id: "agent-1",
|
||||
provider: "codex",
|
||||
cwd: REPO_CWD,
|
||||
workspaceId: "ws-close",
|
||||
model: null,
|
||||
thinkingOptionId: null,
|
||||
effectiveThinkingOptionId: null,
|
||||
@@ -1738,6 +1743,7 @@ test("close_items_request continues after an archive failure", async () => {
|
||||
...makeAgent({
|
||||
id: "agent-good",
|
||||
cwd: REPO_CWD,
|
||||
workspaceId: "ws-err",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
}),
|
||||
@@ -1912,7 +1918,7 @@ test("non-git workspace uses deterministic directory name and no unknown branch
|
||||
expect(result.entries[0]?.name).not.toBe("Unknown branch");
|
||||
});
|
||||
|
||||
test("active-scoped fetch_agents includes only unarchived agents in active exact workspaces", async () => {
|
||||
test("active-scoped fetch_agents includes only unarchived agents in active workspaces", async () => {
|
||||
const session = createSessionForWorkspaceTests();
|
||||
const archivedAt = "2026-03-02T12:00:00.000Z";
|
||||
const activeProject = createPersistedProjectRecord({
|
||||
@@ -1973,24 +1979,28 @@ test("active-scoped fetch_agents includes only unarchived agents in active exact
|
||||
makeAgent({
|
||||
id: "agent-active",
|
||||
cwd: "/tmp/active",
|
||||
workspaceId: "ws-active",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:04:00.000Z",
|
||||
}),
|
||||
makeAgent({
|
||||
id: "agent-subdir",
|
||||
cwd: "/tmp/active/packages/app",
|
||||
workspaceId: "ws-active",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:03:00.000Z",
|
||||
}),
|
||||
makeAgent({
|
||||
id: "agent-archived-workspace",
|
||||
cwd: "/tmp/archived-workspace",
|
||||
workspaceId: "ws-archived",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:02:00.000Z",
|
||||
}),
|
||||
makeAgent({
|
||||
id: "agent-archived-project",
|
||||
cwd: "/tmp/archived-project",
|
||||
workspaceId: "ws-archived-project",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:01:00.000Z",
|
||||
}),
|
||||
@@ -1998,6 +2008,7 @@ test("active-scoped fetch_agents includes only unarchived agents in active exact
|
||||
...makeAgent({
|
||||
id: "agent-archived",
|
||||
cwd: "/tmp/active",
|
||||
workspaceId: "ws-active",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
}),
|
||||
@@ -2012,7 +2023,7 @@ test("active-scoped fetch_agents includes only unarchived agents in active exact
|
||||
filter: { includeArchived: true },
|
||||
});
|
||||
|
||||
expect(agentIdsFromEntries(result.entries)).toEqual(["agent-active"]);
|
||||
expect(agentIdsFromEntries(result.entries)).toEqual(["agent-active", "agent-subdir"]);
|
||||
expect(result.pageInfo.hasMore).toBe(false);
|
||||
});
|
||||
|
||||
@@ -2062,18 +2073,21 @@ test("active-scoped fetch_agents pages within active scope instead of global his
|
||||
makeAgent({
|
||||
id: "active-one",
|
||||
cwd: "/tmp/pages/one",
|
||||
workspaceId: "ws-active-one",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:03:00.000Z",
|
||||
}),
|
||||
makeAgent({
|
||||
id: "stale-between",
|
||||
cwd: "/tmp/pages/stale",
|
||||
workspaceId: "ws-stale",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:02:00.000Z",
|
||||
}),
|
||||
makeAgent({
|
||||
id: "active-two",
|
||||
cwd: "/tmp/pages/two",
|
||||
workspaceId: "ws-active-two",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:01:00.000Z",
|
||||
}),
|
||||
@@ -2133,16 +2147,22 @@ test("legacy unscoped fetch_agents keeps global workspace behavior", async () =>
|
||||
|
||||
session.projectRegistry.get = async () => project;
|
||||
session.workspaceRegistry.list = async () => [activeWorkspace, archivedWorkspace];
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
[activeWorkspace, archivedWorkspace].find(
|
||||
(workspace) => workspace.workspaceId === workspaceId,
|
||||
) ?? null;
|
||||
session.listAgentPayloads = async () => [
|
||||
makeAgent({
|
||||
id: "legacy-active",
|
||||
cwd: activeCwd,
|
||||
workspaceId: "ws-legacy-active",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:01:00.000Z",
|
||||
}),
|
||||
makeAgent({
|
||||
id: "legacy-archived-workspace",
|
||||
cwd: archivedCwd,
|
||||
workspaceId: "ws-legacy-archived",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
}),
|
||||
@@ -2187,11 +2207,13 @@ test("fetch_agent_history_request pages archived historical rows separately", as
|
||||
};
|
||||
session.projectRegistry.get = async () => project;
|
||||
session.workspaceRegistry.list = async () => [workspace];
|
||||
session.workspaceRegistry.get = async () => workspace;
|
||||
session.listAgentPayloads = async () => [
|
||||
{
|
||||
...makeAgent({
|
||||
id: "history-archived",
|
||||
cwd: historyCwd,
|
||||
workspaceId: "ws-history",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
}),
|
||||
@@ -2467,6 +2489,7 @@ test("fetch_agent_request still resolves archived historical agents", async () =
|
||||
...makeAgent({
|
||||
id: "archived-history-agent",
|
||||
cwd: path.resolve("/tmp/history-detail"),
|
||||
workspaceId: "ws-history-detail",
|
||||
status: "idle",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
}),
|
||||
@@ -2481,11 +2504,11 @@ test("fetch_agent_request still resolves archived historical agents", async () =
|
||||
? { ok: true, agentId: agent.id }
|
||||
: { ok: false, error: `Agent not found: ${identifier}` };
|
||||
session.getAgentPayloadById = async (agentId: string) => (agentId === agent.id ? agent : null);
|
||||
session.buildProjectPlacementForCwd = async (cwd: string) => ({
|
||||
session.buildProjectPlacementForWorkspaceId = async () => ({
|
||||
projectKey: "proj-history-detail",
|
||||
projectName: "history detail",
|
||||
checkout: {
|
||||
cwd,
|
||||
cwd: agent.cwd,
|
||||
isGit: false,
|
||||
currentBranch: null,
|
||||
remoteUrl: null,
|
||||
|
||||
Reference in New Issue
Block a user