Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/node/services/aiService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ import { ExperimentsService } from "./experimentsService";
import type { DevToolsService } from "./devToolsService";
import { TelemetryService } from "@/node/services/telemetryService";
import type { WorkspaceGoalService } from "./workspaceGoalService";
import type { WorkspaceMcpOverridesService } from "./workspaceMcpOverridesService";
import * as additionalSystemContext from "./additionalSystemContext";
import * as agentResolution from "./agentResolution";
import * as streamContextBuilder from "./streamContextBuilder";
import * as messagePipeline from "./messagePipeline";
Expand Down Expand Up @@ -1442,6 +1444,69 @@ describe("AIService.streamMessage compaction boundary slicing", () => {
mock.restore();
});

it("starts independent workspace context loads without serial waiting", async () => {
using xumHome = new DisposableTempDir("ai-service-concurrent-workspace-context");
const projectPath = path.join(xumHome.path, "project");
await fs.mkdir(projectPath, { recursive: true });

const workspaceId = "workspace-concurrent-context";
const metadata = createLocalWorkspaceMetadata(workspaceId, projectPath);
let mcpOverridesLoadStarted = false;
let scratchpadLoadStarted = false;
const goalLoadStarted = Promise.withResolvers<void>();
const goalLoad = Promise.withResolvers<null>();
const workspaceMcpOverridesService = {
getOverridesForWorkspace: mock(() => {
mcpOverridesLoadStarted = true;
return Promise.resolve({ overrides: undefined });
}),
} as unknown as WorkspaceMcpOverridesService;
const { config, historyService, initStateManager, providerService } = createBasicAIService(
xumHome.path
);
const concurrentService = new AIService(
config,
historyService,
initStateManager,
providerService,
undefined,
undefined,
workspaceMcpOverridesService
);
stubCommonStreamMessageDependencies({
service: concurrentService,
config,
historyService,
initStateManager,
metadata,
});
spyOn(additionalSystemContext, "readAdditionalSystemContext").mockImplementation(() => {
scratchpadLoadStarted = true;
return Promise.resolve(null);
});
const goalService = {
getGoal: mock(() => {
goalLoadStarted.resolve();
return goalLoad.promise;
}),
} as unknown as WorkspaceGoalService;

const streamPromise = concurrentService.streamMessage({
messages: [createMuxMessage("latest-user", "user", "hello")],
workspaceId,
modelString: "openai:gpt-5.2",
thinkingLevel: "off",
workspaceGoalService: goalService,
});

await goalLoadStarted.promise;
expect(mcpOverridesLoadStarted).toBe(true);
expect(scratchpadLoadStarted).toBe(true);

goalLoad.resolve(null);
expect((await streamPromise).success).toBe(true);
});

it("keeps set_goal disabled for one-shot streams that do not opt into agent-created goals", async () => {
using xumHome = new DisposableTempDir("ai-service-set-goal-disabled");
const projectPath = path.join(xumHome.path, "project");
Expand Down
105 changes: 59 additions & 46 deletions src/node/services/aiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1988,12 +1988,60 @@ export class AIService extends EventEmitter {
const advisorToolEligible =
advisorExperimentEnabled && agentAdvisorEnabled && advisorModelString.length > 0;

// These independent workspace reads can all be slow (especially on remote filesystems),
// so start them together once agent resolution has established the request context.
const currentGoalForToolsPromise: Promise<GoalRecordV1 | null> = workspaceGoalService
? workspaceGoalService.getGoal(workspaceId)
: Promise.resolve(null);

// Fetch workspace MCP overrides (for filtering servers and tools).
// NOTE: Stored in <workspace>/.xum/mcp.local.jsonc (not ~/.xum/config.json).
const loadWorkspaceMcpOverridesStartedAt = Date.now();
const mcpOverridesPromise: Promise<WorkspaceMCPOverrides | undefined> =
this.workspaceMcpOverridesService
.getOverridesForWorkspace(workspaceId)
.then(({ overrides }) => overrides)
.catch((error: unknown) => {
log.warn("[MCP] Failed to load workspace MCP overrides; continuing without overrides", {
workspaceId,
error,
});
return undefined;
})
.finally(() => {
recordStartupPhaseTiming(
"loadWorkspaceMcpOverridesMs",
loadWorkspaceMcpOverridesStartedAt
);
});

const loadAdditionalSystemContextStartedAt = Date.now();
const workspaceAdditionalSystemContextPromise: Promise<string> = (
additionalSystemContext != null
? Promise.resolve(additionalSystemContext)
: readAdditionalSystemContext(this.config, workspaceId)
.then(effectiveAdditionalSystemContext)
.catch((error: unknown) => {
// The scratchpad is user-editable state, so a transient read failure should not block a send.
log.warn(
"Failed to load workspace additional system context; continuing without it",
{
workspaceId,
error,
}
);
return "";
})
).finally(() => {
recordStartupPhaseTiming(
"loadAdditionalSystemContextMs",
loadAdditionalSystemContextStartedAt
);
});

// Goals graduated to GA: tools are gated solely on the workspace's
// current goal status + agent capability, not on an experiment flag.
let currentGoalForTools: GoalRecordV1 | null = null;
if (workspaceGoalService) {
currentGoalForTools = await workspaceGoalService.getGoal(workspaceId);
}
const currentGoalForTools = await currentGoalForToolsPromise;
const effectiveGoalDefaults = mergeGoalDefaults(
normalizeGoalDefaults(cfg.goalDefaults ?? DEFAULT_GOAL_DEFAULTS),
metadata.goalDefaults ?? null
Expand All @@ -2005,23 +2053,6 @@ export class AIService extends EventEmitter {
agentInheritanceChain,
});

// Fetch workspace MCP overrides (for filtering servers and tools)
// NOTE: Stored in <workspace>/.xum/mcp.local.jsonc (not ~/.xum/config.json).
let mcpOverrides: WorkspaceMCPOverrides | undefined;
const loadWorkspaceMcpOverridesStartedAt = Date.now();
try {
mcpOverrides = (
await this.workspaceMcpOverridesService.getOverridesForWorkspace(workspaceId)
).overrides;
} catch (error) {
log.warn("[MCP] Failed to load workspace MCP overrides; continuing without overrides", {
workspaceId,
error,
});
mcpOverrides = undefined;
}
recordStartupPhaseTiming("loadWorkspaceMcpOverridesMs", loadWorkspaceMcpOverridesStartedAt);

// Agent Plugins: discovery follows the active checkout and is disabled
// for workspaces that exec off-host (SSH/Docker/devcontainer).
const agentPluginsMcpContext = hostCheckoutRoot
Expand All @@ -2047,7 +2078,9 @@ export class AIService extends EventEmitter {
log.warn("Agent plugin hooks: ensure failed; continuing without plugin hooks", { error });
}

// Fetch MCP server config for system prompt (before building message).
// Fetch MCP server config for system prompt (before building message). Awaiting the
// override read here still overlaps it with the goal and scratchpad reads above.
const mcpOverrides = await mcpOverridesPromise;
const listMcpServersStartedAt = Date.now();
const mcpServers = this.mcpServerManager
? await this.mcpServerManager.listServers(
Expand All @@ -2059,33 +2092,13 @@ export class AIService extends EventEmitter {
: undefined;
recordStartupPhaseTiming("listMcpServersMs", listMcpServersStartedAt);

const loadAdditionalSystemContextStartedAt = Date.now();
let workspaceAdditionalSystemContext = additionalSystemContext;
if (workspaceAdditionalSystemContext == null) {
try {
// Fall back to disk only when the renderer did not send a live snapshot.
// `effectiveAdditionalSystemContext` honors the `enabled` toggle: when
// the user has disabled the scratchpad, the persisted content is
// intentionally not injected.
const record = await readAdditionalSystemContext(this.config, workspaceId);
workspaceAdditionalSystemContext = effectiveAdditionalSystemContext(record);
} catch (error) {
// The scratchpad is user-editable state, so a transient read failure should not block a send.
log.warn("Failed to load workspace additional system context; continuing without it", {
workspaceId,
error,
});
workspaceAdditionalSystemContext = "";
}
}
// Fall back to disk only when the renderer did not send a live snapshot.
// `effectiveAdditionalSystemContext` honors the `enabled` toggle: when the user has
// disabled the scratchpad, the persisted content is intentionally not injected.
const scratchpadAdditionalSystemInstructions = mergeAdditionalSystemInstructions(
workspaceAdditionalSystemContext,
await workspaceAdditionalSystemContextPromise,
additionalSystemInstructions
);
recordStartupPhaseTiming(
"loadAdditionalSystemContextMs",
loadAdditionalSystemContextStartedAt
);

// Build plan-aware instructions and determine plan→exec transition content.
// IMPORTANT: Derive this from the same boundary-sliced message payload that is sent to
Expand Down