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
3 changes: 3 additions & 0 deletions packages/acp-bridge/src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,9 @@ export const IDLE_HOOK_EVENTS: Record<HookEventName, ServeHookEventMeta> = {
description: 'When a new session is started',
matcherKind: 'sessionTrigger',
},
CwdChanged: {
description: 'After the session changes its working directory',
},
MessageDisplay: {
description: 'Repeatedly, as the assistant reply streams',
},
Expand Down
24 changes: 23 additions & 1 deletion packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2075,6 +2075,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
installPendingManagedConversationBinding: ReturnType<typeof vi.fn>;
commitManagedConversationBinding: ReturnType<typeof vi.fn>;
releaseManagedConversationBinding: ReturnType<typeof vi.fn>;
startCronScheduler: ReturnType<typeof vi.fn>;
appendLiveConversationTranscript: ReturnType<typeof vi.fn>;
collectActiveWorkHolds: ReturnType<typeof vi.fn>;
hasStandaloneRelocationBlockers: ReturnType<typeof vi.fn>;
Expand Down Expand Up @@ -4360,6 +4361,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
getCreatedAt: vi.fn().mockReturnValue(1_700_000_000_000),
getTurnCount: vi.fn().mockReturnValue(3),
prompt: vi.fn().mockResolvedValue({ stopReason: 'end_turn' }),
refreshSkillsFromSettings: vi.fn().mockResolvedValue(undefined),
};
lastSessionMock = sessionMock;
return sessionMock as unknown as InstanceType<typeof Session>;
Expand Down Expand Up @@ -5078,6 +5080,19 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect(
lastSessionMock?.hardSuspendTodoStopGuard.mock.invocationCallOrder[0],
).toBeLessThan(relocateWorkingDirectory.mock.invocationCallOrder[0]!);
expect(lastSessionMock?.startCronScheduler).toHaveBeenCalledTimes(2);
// The daemon owns the settings watcher for non-managed sessions, so
// this is the only push of `available_commands_update` after a move.
expect(
(
lastSessionMock as unknown as {
refreshSkillsFromSettings: ReturnType<typeof vi.fn>;
}
).refreshSkillsFromSettings,
).toHaveBeenCalledWith({
reloadSettings: false,
notifyConfigChanged: false,
});
} finally {
await fs.rm(targetDir, { recursive: true, force: true });
}
Expand Down Expand Up @@ -5361,7 +5376,11 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect(innerConfig.relocateWorkingDirectory).toHaveBeenCalledWith(
expectation.child.canonicalPath,
expectation.child.canonicalPath,
{ skipProcessChdir: true, skipArtifactMigration: true },
{
skipProcessChdir: true,
skipArtifactMigration: true,
trustedFolder: true,
},
);
expect(
lastSessionMock?.installPendingManagedConversationBinding,
Expand Down Expand Up @@ -10936,6 +10955,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect.objectContaining({
toolInvocationGuard: expect.any(Function),
}),
expect.anything(),
);

mockConnectionState.resolve();
Expand Down Expand Up @@ -19699,8 +19719,10 @@ describe('QwenAgent loadSession / unstable_resumeSession', () => {
}

const sessionSettings = vi.mocked(loadCliConfig).mock.calls[0]?.[0];
const hostPolicy = vi.mocked(loadCliConfig).mock.calls[0]?.[9];
expect(sessionSettings?.experimental?.cron).toBe(false);
expect(requestSettings.merged.experimental?.cron).toBe(true);
expect(hostPolicy?.projectRuntimeCronEnabled).toBe(false);

mockConnectionState.resolve();
await agentPromise;
Expand Down
79 changes: 74 additions & 5 deletions packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import type { LoadedSettings } from '../config/settings.js';
import { formatCronRelocationNotice } from '../config/cron-relocation-notice.js';
import {
loadSettings,
reloadEnvironment,
Expand Down Expand Up @@ -2304,8 +2305,9 @@ function readScopeSettings(
async function resolvePreferredMemoryFile(
dir: string,
fallbackFilename: string,
contextFileNames: readonly string[],
): Promise<string> {
for (const filename of getAllMemoryFilenames()) {
for (const filename of contextFileNames) {
const filePath = path.join(dir, filename);
try {
await fs.access(filePath);
Expand All @@ -2321,15 +2323,25 @@ async function resolvePreferredMemoryFile(
async function resolveQwenMemoryPaths(params: {
cwd: string;
projectRoot: string;
/**
* The session's context-file names. `/cd` makes these session-scoped
* and never updates the process-global list, so a host asking for the
* memory paths after a move must be answered from the session, or it
* is handed `QWEN.md` for a project whose file is `CONTEXT.md`.
*/
contextFileNames?: readonly string[];
}): Promise<QwenMemoryPaths> {
const fallbackFilename = getAllMemoryFilenames()[0] ?? 'QWEN.md';
const contextFileNames = params.contextFileNames ?? getAllMemoryFilenames();
const fallbackFilename = contextFileNames[0] ?? 'QWEN.md';
const userMemoryFile = await resolvePreferredMemoryFile(
Storage.getGlobalQwenDir(),
fallbackFilename,
contextFileNames,
);
const projectMemoryFile = await resolvePreferredMemoryFile(
params.cwd,
fallbackFilename,
contextFileNames,
);
const autoMemoryDir = getAutoMemoryRoot(params.projectRoot);

Expand Down Expand Up @@ -8148,7 +8160,11 @@ class QwenAgent implements Agent {
? params['projectRoot']
: cwd;
return {
paths: await resolveQwenMemoryPaths({ cwd, projectRoot }),
paths: await resolveQwenMemoryPaths({
cwd,
projectRoot,
contextFileNames: this.contextFileNamesForCwd(cwd),
}),
};
}
case SERVE_STATUS_EXT_METHODS.workspaceMcp:
Expand Down Expand Up @@ -10194,7 +10210,11 @@ class QwenAgent implements Agent {
const relocation = await config.relocateWorkingDirectory(
canonicalPath,
canonicalPath,
{ skipProcessChdir: true, skipArtifactMigration: true },
{
skipProcessChdir: true,
skipArtifactMigration: true,
trustedFolder: true,
},
);
if (conversationDirectoryExpectation !== undefined) {
await assertManagedConversationDirectoryIdentity(
Expand Down Expand Up @@ -10226,6 +10246,32 @@ class QwenAgent implements Agent {
}`,
);
}
for (const error of relocation.projectRuntimeRefreshErrors ?? []) {
warnings.push(
`Project runtime refresh failed: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
if (relocation.cronExitSummary) {
warnings.push(
formatCronRelocationNotice(relocation.cronExitSummary),
);
}

try {
await session.refreshSkillsFromSettings({
reloadSettings: false,
notifyConfigChanged: false,
});
} catch (error) {
warnings.push(
`Available commands refresh failed: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
session.startCronScheduler();

try {
await config
Expand Down Expand Up @@ -12493,14 +12539,20 @@ class QwenAgent implements Agent {
// not process.exit(1) the shared ACP child and every session on its
// channel. newSessionConfig maps the throw to a RequestError.
true,
this.managedToolInvocationGuard || restoreOptions || provisionalWorkspace
this.managedToolInvocationGuard ||
restoreOptions ||
provisionalWorkspace ||
sessionSource?.sourceType === 'channel'
? {
...(provisionalWorkspace
? { provisionalWorkspace: true as const }
: {}),
...(this.managedToolInvocationGuard
? { toolInvocationGuard: this.managedToolInvocationGuard }
: {}),
...(sessionSource?.sourceType === 'channel'
? { projectRuntimeCronEnabled: false }
: {}),
...(restoreOptions && sessionId
? {
sessionRestore: {
Expand All @@ -12516,6 +12568,7 @@ class QwenAgent implements Agent {
: {}),
}
: undefined,
settings,
);
if (sessionSource) {
config.setSessionSource(sessionSource.sourceType, sessionSource.sourceId);
Expand Down Expand Up @@ -12696,6 +12749,22 @@ class QwenAgent implements Agent {
config.setFileSystemService(acpFileSystemService);
}

/**
* Context-file names for a host request about `cwd`. `/cd` scopes the
* names to the session and leaves the process-global list untouched, so
* an agent-level request has to be answered from the session that owns
* the directory; the global list is only right when no session does.
*/
private contextFileNamesForCwd(cwd: string): readonly string[] {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R2-3: the session-matching branch of contextFileNamesForCwd — the point of the change — is untested: the only qwen/settings/getMemoryPaths test in the tree (acpAgent.test.ts ~12029) queries a cwd that matches no live session, so only the return getAllMemoryFilenames() fallback executes. Deleting the session loop and always returning the global list keeps every existing test green while reintroducing the exact bug the doc comment names (a host asking for memory paths about a moved session's cwd is handed QWEN.md for a project whose context file is CONTEXT.md).

Fix witness: add a test where a session owns the queried cwd with getContextFileNames() ['CONTEXT.md'] and assert the resolved paths use CONTEXT.md — it must go red if the session loop is removed.

中文说明

contextFileNamesForCwd 的会话匹配分支——本次改动的核心——没有测试:树中唯一的 qwen/settings/getMemoryPaths 测试(acpAgent.test.ts ~12029)查询的 cwd 不属于任何活跃会话,因此只有 return getAllMemoryFilenames() 回退分支被执行。删除会话循环、总是返回全局列表,所有现有测试仍为绿,却重新引入了文档注释所指明的 bug(宿主查询已迁移会话的 cwd 时,项目上下文文件明明是 CONTEXT.md 却拿到 QWEN.md)。

修复见证:新增测试,让会话持有被查询的 cwd 且 getContextFileNames()['CONTEXT.md'],断言解析路径使用 CONTEXT.md;删除会话循环后必须变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R2-3: still stands — the session-matching branch of contextFileNamesForCwd — the point of the change — is untested: the only qwen/settings/getMemoryPaths test in the tree queries a cwd that matches no live session and asserts the global-fallback QWEN.md answer. Reverting the contextFileNames wiring leaves that test green, so the exact regression the method exists to prevent could ship. Add an ext-method test with a stored session whose config.getWorkingDir() equals the requested cwd and getContextFileNames() returns ['CONTEXT.md'], asserting the resolved paths use CONTEXT.md; deleting contextFileNamesForCwd(cwd) from the call must turn it red.

中文说明

仍然成立——contextFileNamesForCwd 的会话匹配分支(本改动的意义所在)没有测试:树中唯一的 qwen/settings/getMemoryPaths 测试查询的 cwd 不属于任何活动会话,断言的是全局回退 QWEN.md。回退 contextFileNames 接线后该测试仍为绿,因此该方法本要防止的回归恰好可以溜走。建议补一个 ext-method 测试:存储会话的 config.getWorkingDir() 等于请求的 cwd、getContextFileNames() 返回 ['CONTEXT.md'],断言解析出的路径使用 CONTEXT.md;删除调用中的 contextFileNamesForCwd(cwd) 后应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

for (const session of this.sessions.values()) {
const config = session.getConfig();
if (config.getWorkingDir() === cwd) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R2-2: this lookup compares the raw host-supplied cwd (params['cwd']; extMethodInternal normalizes only sessionId) against the session's working directory by strict string equality, but the stored directory is realpath-canonicalized (fs.realpath during session creation/relocation). A host asking qwen/settings/getMemoryPaths with a differently spelled but equivalent path — a trailing slash, a ./.. segment, or the pre-realpath symlink alias — misses the equality check and falls back to getAllMemoryFilenames(): the stale process-global list from the original project — the exact "QWEN.md for a project whose file is CONTEXT.md" answer this method's doc comment says it exists to prevent.

Suggested change
if (config.getWorkingDir() === cwd) {
if (path.resolve(config.getWorkingDir()) === path.resolve(cwd)) {

(or fs.realpath on the request side to also cover symlink aliases)

Fix witness: an acpAgent test creating a session whose getWorkingDir() returns /project/foo and getContextFileNames() returns ['CONTEXT.md'], calling getMemoryPaths with { cwd: '/project/foo/' } and expecting CONTEXT.md — red against the current strict equality.

中文说明

该查找以严格字符串相等比较宿主提供的原始 cwdparams['cwd']extMethodInternal 只规范化 sessionId)与会话工作目录,但存储的目录是经过 realpath 规范化的(会话创建/迁移时 fs.realpath)。宿主以不同拼写但等价的路径(结尾斜杠、./.. 段、realpath 之前的软链别名)请求 qwen/settings/getMemoryPaths 时,相等判断落空,回退到 getAllMemoryFilenames():原项目遗留的进程级全局列表——正是本方法文档注释声称要防止的“项目上下文文件是 CONTEXT.md 却返回 QWEN.md”。

修复见证:新增 acpAgent 测试:会话 getWorkingDir()/project/foogetContextFileNames()['CONTEXT.md'],以 { cwd: '/project/foo/' } 调用 getMemoryPaths,期望得到 CONTEXT.md;当前严格相等实现下该测试为红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R2-2: still stands — contextFileNamesForCwd compares the raw host-supplied cwd against the session's stored working dir by strict string equality, but the stored directory is normalized (path.resolve in the Config constructor, fs.realpath on the /cd path) while the ext-method param is not. A host calling qwen/settings/getMemoryPaths with '/proj/', '/x/../proj', or a symlinked spelling misses the === comparison, gets the process-global getAllMemoryFilenames() fallback, and is handed QWEN.md paths for a project whose context file is CONTEXT.md — host memory edits silently target the wrong file. Normalize before comparing: path.resolve(config.getWorkingDir()) === path.resolve(cwd) (or fs.realpath both sides to also cover symlinked spellings, matching how relocation stores the path); fix witness: a stored dir differing from the query only by a trailing slash must still resolve CONTEXT.md — removing the normalization turns it red.

中文说明

仍然成立——contextFileNamesForCwd 用严格字符串相等比较宿主提供的原始 cwd 与会话存储的工作目录,但存储目录是规范化过的(Config 构造里的 path.resolve/cd 路径上的 fs.realpath),ext-method 参数却不是。宿主以 '/proj/''/x/../proj' 或符号链接拼写调用 qwen/settings/getMemoryPaths 时,=== 比较落空,得到进程级全局 getAllMemoryFilenames() 回退,于是拿到 QWEN.md 路径——而该项目的上下文文件是 CONTEXT.md,宿主的记忆编辑静默写错文件。建议比较前先规范化:path.resolve(config.getWorkingDir()) === path.resolve(cwd)(或两侧都 fs.realpath 以覆盖符号链接拼写,与迁移存储路径的方式一致);修复见证:存储目录与查询仅差一个结尾斜杠时仍应解析出 CONTEXT.md;去掉规范化后应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

return config.getContextFileNames();
}
}
return getAllMemoryFilenames();
}

private async createAndStoreSession(
config: Config,
settings: LoadedSettings,
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,9 @@ describe('Session', () => {
getTool: ReturnType<typeof vi.fn>;
ensureTool: ReturnType<typeof vi.fn>;
registerTool: ReturnType<typeof vi.fn>;
registerSessionTool: ReturnType<typeof vi.fn>;
registerPermissionDeferredFactory: ReturnType<typeof vi.fn>;
registerSessionPermissionDeferredFactory: ReturnType<typeof vi.fn>;
revealDeferredTool: ReturnType<typeof vi.fn>;
pinDeferredToolReveal: ReturnType<typeof vi.fn>;
warmAll: ReturnType<typeof vi.fn>;
Expand Down Expand Up @@ -782,14 +784,23 @@ describe('Session', () => {
getTool: vi.fn(),
ensureTool: vi.fn().mockResolvedValue(true),
registerTool: vi.fn(),
registerSessionTool: vi.fn(),
registerPermissionDeferredFactory: vi.fn(),
registerSessionPermissionDeferredFactory: vi.fn(),
revealDeferredTool: vi.fn(),
pinDeferredToolReveal: vi.fn(),
warmAll: vi.fn().mockResolvedValue(undefined),
getFunctionDeclarationsFiltered: vi.fn((names: string[]) =>
names.map((name) => ({ name })),
),
};
mockToolRegistry.registerSessionTool = vi.fn((tool) =>
mockToolRegistry.registerTool(tool),
);
mockToolRegistry.registerSessionPermissionDeferredFactory = vi.fn(
(name, factory) =>
mockToolRegistry.registerPermissionDeferredFactory(name, factory),
);
const fileService = {
shouldGitIgnoreFile: vi.fn().mockReturnValue(false),
shouldIgnoreFile: vi.fn().mockReturnValue(false),
Expand Down Expand Up @@ -823,6 +834,7 @@ describe('Session', () => {
assertCanStartTurn: vi.fn().mockResolvedValue(undefined),
getWorkingDir: vi.fn().mockReturnValue(process.cwd()),
getProjectRoot: vi.fn().mockReturnValue('/repo'),
getContextFileNames: vi.fn().mockReturnValue(['QWEN.md', 'AGENTS.md']),
// Folder trust gates the project `.qwen/loop.md`; default trusted (the
// production default). Untrusted-folder tests override to false.
isTrustedFolder: vi.fn().mockReturnValue(true),
Expand Down Expand Up @@ -2016,6 +2028,11 @@ describe('Session', () => {
await session.enableLiveScreenContext();
const screenTool = registered.get(CAPTURE_SCREEN_CONTEXT_TOOL_NAME);
expect(screenTool?.name).toBe('capture_screen_context');
// Session-owned, not merely registered: a plain `registerTool` here
// would let the next `/cd` dispose the live channel tool.
expect(mockToolRegistry.registerSessionTool).toHaveBeenCalledWith(
expect.objectContaining({ name: CAPTURE_SCREEN_CONTEXT_TOOL_NAME }),
);
const invocation = screenTool?.build({});
expect(invocation).toBeDefined();
await expect(invocation?.getDefaultPermission()).resolves.toBe('allow');
Expand Down
17 changes: 11 additions & 6 deletions packages/cli/src/acp-integration/session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1691,14 +1691,14 @@ export async function registerCreateSubSessionTool(
}
const toolRegistry = config.getToolRegistry();
if (registrationStatus === 'deferred') {
toolRegistry.registerPermissionDeferredFactory(
toolRegistry.registerSessionPermissionDeferredFactory(
ToolNames.CREATE_SUB_SESSION,
Comment on lines +1694 to 1695

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-12: round 1 flagged that Session's switch from registerTool/registerPermissionDeferredFactory to the session-owned variants had no witness; the fix pinned only capture_screen_context (Session.test.ts ~2033). This site (and its eager twin ~1701, plus the live-voice/speak_to_user switches ~3231/~3258) still has none: Session.test.ts wires registerSessionTool = vi.fn((tool) => mockToolRegistry.registerTool(tool)) (same delegation for the deferred factory), so assertions on the plain methods pass either way. Verified by mutation: reverting these CREATE_SUB_SESSION sites to the plain methods keeps all 5 create_sub_session tests green, while reverting the pinned capture_screen_context site makes its witness fail — so the next /cd's replaceCoreToolsFrom would silently drop create_sub_session (not session-owned, not discovered, not in the fresh core registry), removing sub-session spawning from daemon sessions for the rest of the session, and no test would notice.

Fix witness: assert registerSessionTool / registerSessionPermissionDeferredFactory directly in these registration tests (mirroring the capture_screen_context assertion); those assertions must go red if the call sites revert to the plain methods.

中文说明

第 1 轮曾指出 Session 从 registerTool/registerPermissionDeferredFactory 切换到会话级变体缺少见证;修复只钉住了 capture_screen_context(Session.test.ts ~2033)。本处(及其非延迟孪生 ~1701,以及 live-voice/speak_to_user 切换 ~3231/~3258)仍无见证:Session.test.tsregisterSessionTool 委托为 vi.fn((tool) => mockToolRegistry.registerTool(tool))(延迟工厂同样委托),因此对普通方法的断言两种实现都能通过。变异验证:把 CREATE_SUB_SESSION 这两处还原为普通方法,全部 5 个 create_sub_session 测试仍为绿;而还原已钉住的 capture_screen_context 处,其见证测试即失败——也就是说下一次 /cdreplaceCoreToolsFrom 会静默丢弃 create_sub_session(非会话级、非发现工具、也不在新核心注册表中),守护进程会话在本会话余下时间内失去创建子会话的能力,而没有任何测试能发现。

修复见证:在这些注册测试中直接断言 registerSessionTool / registerSessionPermissionDeferredFactory(仿照 capture_screen_context 的断言);调用点还原为普通方法后这些断言必须变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

async () => new CreateSubSessionTool(config),
);
await config.getLlmClient().setTools();
return;
}
toolRegistry.registerTool(new CreateSubSessionTool(config));
toolRegistry.registerSessionTool(new CreateSubSessionTool(config));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-12: still stands — Session's switch from registerTool/registerPermissionDeferredFactory to the session-owned variants is pinned only for capture_screen_context; create_sub_session (here and the deferred branch), speak_to_user, and the live-voice tool batch assert only the plain delegate mocks (Session.test.ts maps registerSessionTool/registerSessionPermissionDeferredFactory to their delegates). Reverting create_sub_session or speak_to_user to plain registerTool keeps Session.test.ts green, yet the next /cd's replaceCoreToolsFrom would then dispose the tool mid-session and drop the model's speak_to_user/create_sub_session surface. Mirror the capture-screen-context assertion for these registrations, asserting the session variants directly (not their delegates); fix witness: reverting Session.ts to the plain registrations at those sites must turn the new assertions red.

中文说明

仍然成立——Session 从 registerTool/registerPermissionDeferredFactory 切换到会话自有变体,目前只为 capture_screen_context 固定;create_sub_session(此处及延迟分支)、speak_to_user 与实时语音工具批只断言了普通的委托 mock(Session.test.ts 把 registerSessionTool/registerSessionPermissionDeferredFactory 映射到其委托)。把 create_sub_sessionspeak_to_user 回退为普通 registerTool,Session.test.ts 仍为绿,而下一次 /cdreplaceCoreToolsFrom 会在会话中途处置该工具,模型将失去 speak_to_user/create_sub_session 入口。建议仿照 capture-screen-context 的断言为这些注册直接断言会话变体(而非委托);修复见证:把 Session.ts 这些位置回退为普通注册后,新断言应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

// The registration lands after `config.initialize()` → `startChat()` already
// snapshotted the chat's tool declarations, and the tool is deferred — so it
// stays filtered out of the declarations until revealed. Reveal it and
Expand Down Expand Up @@ -3207,7 +3207,7 @@ export class Session implements SessionContext {
screenshotPath,
};
});
registry.registerTool(tool);
registry.registerSessionTool(tool);
if (registry.getTool(CAPTURE_SCREEN_CONTEXT_TOOL_NAME) !== tool) {
Comment thread
qqqys marked this conversation as resolved.
throw new Error(
'capture_screen_context is required for Live Voice but is disabled.',
Expand All @@ -3231,7 +3231,7 @@ export class Session implements SessionContext {
);
}
}
for (const tool of tools) registry.registerTool(tool);
for (const tool of tools) registry.registerSessionTool(tool);
for (const tool of tools) {
if (registry.getTool(tool.name) !== tool) {
throw new Error(
Expand All @@ -3258,7 +3258,7 @@ export class Session implements SessionContext {
message,
});
});
registry.registerTool(tool);
registry.registerSessionTool(tool);
if (registry.getTool(SPEAK_TO_USER_TOOL_NAME) !== tool) {
throw new Error(
'speak_to_user is required for Live Voice but is disabled.',
Expand Down Expand Up @@ -10094,6 +10094,7 @@ export class Session implements SessionContext {
const matchedContextFileWrite = didWriteProjectContextFile(
memoryWriteCandidates,
this.config.getProjectRoot(),
this.config.getContextFileNames(),
);
debugLogger.debug(
`ACP session ${this.sessionId} checked marked context-file memory tool batch; matched=${matchedContextFileWrite}`,
Expand Down Expand Up @@ -10971,7 +10972,11 @@ export class Session implements SessionContext {
// prompt right after an allow-rule call just worked.
const forceAutoReviewForAllow =
approvalMode === ApprovalMode.AUTO &&
(shouldForceAutoModeReviewForAllow(pmCtx, this.config.getCwd()) ||
(shouldForceAutoModeReviewForAllow(
pmCtx,
this.config.getCwd(),
this.config.getContextFileNames(),
) ||
shouldClassifyAllShellForAutoMode(policyToolName, this.config));
const confirmationPermission = getEffectivePermissionForConfirmation(
finalPermission,
Expand Down
Loading
Loading