Avoid git refresh for agent status updates
This commit is contained in:
@@ -134,6 +134,7 @@ import type { AgentStorage } from "./agent/agent-storage.js";
|
|||||||
import {
|
import {
|
||||||
checkoutLiteFromGitSnapshot,
|
checkoutLiteFromGitSnapshot,
|
||||||
normalizeWorkspaceId as normalizePersistedWorkspaceId,
|
normalizeWorkspaceId as normalizePersistedWorkspaceId,
|
||||||
|
deriveProjectGroupingName,
|
||||||
deriveWorkspaceId,
|
deriveWorkspaceId,
|
||||||
deriveProjectRootPath,
|
deriveProjectRootPath,
|
||||||
deriveProjectKind,
|
deriveProjectKind,
|
||||||
@@ -1363,14 +1364,26 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findWorkspaceByDirectory(cwd: string): Promise<PersistedWorkspaceRecord | null> {
|
private async findWorkspaceByDirectory(
|
||||||
const normalizedCwd = await this.resolveWorkspaceDirectory(cwd);
|
cwd: string,
|
||||||
|
options?: { refreshGit?: boolean },
|
||||||
|
): Promise<PersistedWorkspaceRecord | null> {
|
||||||
|
const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
|
||||||
const workspaces = await this.workspaceRegistry.list();
|
const workspaces = await this.workspaceRegistry.list();
|
||||||
return workspaces.find((workspace) => workspace.cwd === normalizedCwd) ?? null;
|
const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(normalizedCwd, workspaces);
|
||||||
|
return workspaces.find((workspace) => workspace.workspaceId === workspaceId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveWorkspaceDirectory(cwd: string): Promise<string> {
|
private async resolveWorkspaceDirectory(
|
||||||
|
cwd: string,
|
||||||
|
options?: { refreshGit?: boolean },
|
||||||
|
): Promise<string> {
|
||||||
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
||||||
|
if (options?.refreshGit === false) {
|
||||||
|
const snapshot = this.workspaceGitService.peekSnapshot(normalizedCwd);
|
||||||
|
return normalizePersistedWorkspaceId(snapshot?.git.repoRoot ?? normalizedCwd);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const snapshot = await this.workspaceGitService.getSnapshot(normalizedCwd);
|
const snapshot = await this.workspaceGitService.getSnapshot(normalizedCwd);
|
||||||
return normalizePersistedWorkspaceId(snapshot.git.repoRoot ?? normalizedCwd);
|
return normalizePersistedWorkspaceId(snapshot.git.repoRoot ?? normalizedCwd);
|
||||||
@@ -1424,10 +1437,32 @@ export class Session {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildProjectPlacementForCwd(cwd: string): Promise<ProjectPlacementPayload | null> {
|
private async buildProjectPlacementForCwd(
|
||||||
const workspace = await this.findWorkspaceByDirectory(cwd);
|
cwd: string,
|
||||||
|
options?: { refreshGit?: boolean; fallback?: boolean },
|
||||||
|
): Promise<ProjectPlacementPayload | null> {
|
||||||
|
const workspace = await this.findWorkspaceByDirectory(cwd, {
|
||||||
|
refreshGit: options?.refreshGit,
|
||||||
|
});
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
return null;
|
if (!options?.fallback) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
||||||
|
return {
|
||||||
|
projectKey: normalizedCwd,
|
||||||
|
projectName: deriveProjectGroupingName(normalizedCwd),
|
||||||
|
checkout: {
|
||||||
|
cwd: normalizedCwd,
|
||||||
|
isGit: false,
|
||||||
|
currentBranch: null,
|
||||||
|
remoteUrl: null,
|
||||||
|
worktreeRoot: null,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return this.buildProjectPlacementForWorkspace(workspace);
|
return this.buildProjectPlacementForWorkspace(workspace);
|
||||||
}
|
}
|
||||||
@@ -1437,7 +1472,10 @@ export class Session {
|
|||||||
const subscription = this.agentUpdatesSubscription;
|
const subscription = this.agentUpdatesSubscription;
|
||||||
const payload = await this.buildAgentPayload(agent);
|
const payload = await this.buildAgentPayload(agent);
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
const project = await this.buildProjectPlacementForCwd(payload.cwd);
|
const project = await this.buildProjectPlacementForCwd(payload.cwd, {
|
||||||
|
refreshGit: false,
|
||||||
|
fallback: true,
|
||||||
|
});
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(`Workspace not found for agent ${payload.id}`);
|
throw new Error(`Workspace not found for agent ${payload.id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,48 @@ function makeAgent(input: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeManagedAgent(input: {
|
||||||
|
id: string;
|
||||||
|
cwd: string;
|
||||||
|
lifecycle: AgentSnapshotPayload["status"];
|
||||||
|
updatedAt: string;
|
||||||
|
}) {
|
||||||
|
const now = new Date(input.updatedAt);
|
||||||
|
const snapshot = makeAgent({
|
||||||
|
id: input.id,
|
||||||
|
cwd: input.cwd,
|
||||||
|
status: input.lifecycle,
|
||||||
|
updatedAt: input.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...snapshot,
|
||||||
|
lifecycle: snapshot.status,
|
||||||
|
config: {
|
||||||
|
provider: snapshot.provider,
|
||||||
|
cwd: snapshot.cwd,
|
||||||
|
},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
pendingPermissions: new Map(),
|
||||||
|
bufferedPermissionResolutions: new Map(),
|
||||||
|
inFlightPermissionResponses: new Set(),
|
||||||
|
pendingReplacement: false,
|
||||||
|
persistence: null,
|
||||||
|
historyPrimed: true,
|
||||||
|
lastUserMessageAt: null,
|
||||||
|
attention: {
|
||||||
|
requiresAttention: false,
|
||||||
|
attentionReason: null,
|
||||||
|
attentionTimestamp: now,
|
||||||
|
},
|
||||||
|
foregroundTurnWaiters: new Set(),
|
||||||
|
unsubscribeSession: null,
|
||||||
|
session: null,
|
||||||
|
activeForegroundTurnId: input.lifecycle === "running" ? "turn-1" : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function agentIdsFromEntries(entries: Array<{ agent: Pick<AgentSnapshotPayload, "id"> }>) {
|
function agentIdsFromEntries(entries: Array<{ agent: Pick<AgentSnapshotPayload, "id"> }>) {
|
||||||
return entries.map((entry) => entry.agent.id);
|
return entries.map((entry) => entry.agent.id);
|
||||||
}
|
}
|
||||||
@@ -272,6 +314,114 @@ function createSessionForWorkspaceTests(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("workspace aggregation", () => {
|
describe("workspace aggregation", () => {
|
||||||
|
test("agent_update placement does not refresh git snapshots", async () => {
|
||||||
|
const emitted: SessionOutboundMessage[] = [];
|
||||||
|
const getSnapshot = vi.fn(async () => {
|
||||||
|
throw new Error("getSnapshot should not be called for agent_update placement");
|
||||||
|
});
|
||||||
|
const workspaceGitService = {
|
||||||
|
...createNoopWorkspaceGitService(),
|
||||||
|
getSnapshot,
|
||||||
|
peekSnapshot: vi.fn(() => null),
|
||||||
|
};
|
||||||
|
const session = createSessionForWorkspaceTests({
|
||||||
|
onMessage: (message) => emitted.push(message),
|
||||||
|
workspaceGitService,
|
||||||
|
}) as any;
|
||||||
|
const project = createPersistedProjectRecord({
|
||||||
|
projectId: "proj-1",
|
||||||
|
rootPath: "/tmp/repo",
|
||||||
|
kind: "git",
|
||||||
|
displayName: "repo",
|
||||||
|
createdAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
});
|
||||||
|
const workspace = createPersistedWorkspaceRecord({
|
||||||
|
workspaceId: "ws-1",
|
||||||
|
projectId: project.projectId,
|
||||||
|
cwd: "/tmp/repo",
|
||||||
|
kind: "local_checkout",
|
||||||
|
displayName: "repo",
|
||||||
|
createdAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
session.projectRegistry.get = async (id: string) => (id === project.projectId ? project : null);
|
||||||
|
session.workspaceRegistry.list = async () => [workspace];
|
||||||
|
session.agentUpdatesSubscription = {
|
||||||
|
subscriptionId: "sub-agents",
|
||||||
|
filter: {},
|
||||||
|
isBootstrapping: false,
|
||||||
|
pendingUpdatesByAgentId: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await session.forwardAgentUpdate(
|
||||||
|
makeManagedAgent({
|
||||||
|
id: "agent-1",
|
||||||
|
cwd: "/tmp/repo",
|
||||||
|
lifecycle: "running",
|
||||||
|
updatedAt: "2026-03-30T15:00:00.000Z",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getSnapshot).not.toHaveBeenCalled();
|
||||||
|
expect(emitted.find((message) => message.type === "agent_update")?.payload).toMatchObject({
|
||||||
|
kind: "upsert",
|
||||||
|
agent: {
|
||||||
|
id: "agent-1",
|
||||||
|
status: "running",
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
projectKey: "proj-1",
|
||||||
|
projectName: "repo",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("agent_update emits fallback placement when no workspace is registered", async () => {
|
||||||
|
const emitted: SessionOutboundMessage[] = [];
|
||||||
|
const getSnapshot = vi.fn(async () => {
|
||||||
|
throw new Error("getSnapshot should not be called for fallback agent_update placement");
|
||||||
|
});
|
||||||
|
const session = createSessionForWorkspaceTests({
|
||||||
|
onMessage: (message) => emitted.push(message),
|
||||||
|
workspaceGitService: {
|
||||||
|
...createNoopWorkspaceGitService(),
|
||||||
|
getSnapshot,
|
||||||
|
peekSnapshot: vi.fn(() => null),
|
||||||
|
},
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
session.agentUpdatesSubscription = {
|
||||||
|
subscriptionId: "sub-agents",
|
||||||
|
filter: {},
|
||||||
|
isBootstrapping: false,
|
||||||
|
pendingUpdatesByAgentId: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await session.forwardAgentUpdate(
|
||||||
|
makeManagedAgent({
|
||||||
|
id: "agent-1",
|
||||||
|
cwd: "/tmp/unregistered",
|
||||||
|
lifecycle: "running",
|
||||||
|
updatedAt: "2026-03-30T15:00:00.000Z",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getSnapshot).not.toHaveBeenCalled();
|
||||||
|
expect(emitted.find((message) => message.type === "agent_update")?.payload).toMatchObject({
|
||||||
|
kind: "upsert",
|
||||||
|
project: {
|
||||||
|
projectKey: "/tmp/unregistered",
|
||||||
|
projectName: "unregistered",
|
||||||
|
checkout: {
|
||||||
|
cwd: "/tmp/unregistered",
|
||||||
|
isGit: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("archive emits an authoritative agent_update upsert for subscribed clients", async () => {
|
test("archive emits an authoritative agent_update upsert for subscribed clients", async () => {
|
||||||
const emitted: Array<{ type: string; payload: any }> = [];
|
const emitted: Array<{ type: string; payload: any }> = [];
|
||||||
const archivedRecord = {
|
const archivedRecord = {
|
||||||
|
|||||||
@@ -2090,7 +2090,7 @@ export const AgentUpdateMessageSchema = z.object({
|
|||||||
z.object({
|
z.object({
|
||||||
kind: z.literal("upsert"),
|
kind: z.literal("upsert"),
|
||||||
agent: AgentSnapshotPayloadSchema,
|
agent: AgentSnapshotPayloadSchema,
|
||||||
project: ProjectPlacementPayloadSchema,
|
project: ProjectPlacementPayloadSchema.nullable().optional(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
kind: z.literal("remove"),
|
kind: z.literal("remove"),
|
||||||
|
|||||||
@@ -42,6 +42,46 @@ describe("workspace message schemas", () => {
|
|||||||
expect(activeScoped.scope).toBe("active");
|
expect(activeScoped.scope).toBe("active");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parses agent_update without project placement", () => {
|
||||||
|
const result = SessionOutboundMessageSchema.safeParse({
|
||||||
|
type: "agent_update",
|
||||||
|
payload: {
|
||||||
|
kind: "upsert",
|
||||||
|
agent: {
|
||||||
|
id: "agent-1",
|
||||||
|
provider: "codex",
|
||||||
|
cwd: "/tmp/repo",
|
||||||
|
model: null,
|
||||||
|
features: [],
|
||||||
|
thinkingOptionId: null,
|
||||||
|
effectiveThinkingOptionId: null,
|
||||||
|
createdAt: "2026-04-04T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
||||||
|
lastUserMessageAt: null,
|
||||||
|
status: "running",
|
||||||
|
capabilities: {
|
||||||
|
supportsStreaming: true,
|
||||||
|
supportsSessionPersistence: true,
|
||||||
|
supportsDynamicModes: true,
|
||||||
|
supportsMcpServers: true,
|
||||||
|
supportsReasoningStream: true,
|
||||||
|
supportsToolInvocations: true,
|
||||||
|
},
|
||||||
|
currentModeId: null,
|
||||||
|
availableModes: [],
|
||||||
|
pendingPermissions: [],
|
||||||
|
persistence: null,
|
||||||
|
title: null,
|
||||||
|
labels: {},
|
||||||
|
requiresAttention: false,
|
||||||
|
attentionReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("parses paginated fetch_agent_history_request and response", () => {
|
test("parses paginated fetch_agent_history_request and response", () => {
|
||||||
const request = SessionInboundMessageSchema.parse({
|
const request = SessionInboundMessageSchema.parse({
|
||||||
type: "fetch_agent_history_request",
|
type: "fetch_agent_history_request",
|
||||||
|
|||||||
Reference in New Issue
Block a user