Skip to content

fix(agent): honor session workdir defaults#1380

Merged
zerob13 merged 2 commits intodevfrom
codex/fix-workdir-propagation-under-acp
Mar 23, 2026
Merged

fix(agent): honor session workdir defaults#1380
zerob13 merged 2 commits intodevfrom
codex/fix-workdir-propagation-under-acp

Conversation

@zerob13
Copy link
Copy Markdown
Collaborator

@zerob13 zerob13 commented Mar 23, 2026

Summary by CodeRabbit

  • Documentation

    • Updated skill runtime spec and prompt-generation guidance to include skill root environment vars and revised execution semantics.
  • Bug Fixes

    • Skill scripts may run from the session working directory when available; fall back to skill root.
    • Improved terminal working-directory resolution and safer initialization/fallbacks.
    • Runtime now exposes SKILL_ROOT and DEEPCHAT_SKILL_ROOT to skill executions.
    • Added session initialization cleanup on failures and ACP workdir synchronization.
  • Tests

    • Added/updated tests for terminal creation, cwd resolution, ACP workdir sync, and skill execution cwd/env behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Conversation-specific working directories are now resolved and propagated to terminal and skill executions. Terminal creation and skill spawn plans prefer an explicit request cwd, then the per-session workdir, then an ACP temp fallback; SKILL_ROOT/DEEPCHAT_SKILL_ROOT env vars are always provided to skill runs.

Changes

Cohort / File(s) Summary
Spec
docs/specs/skill-runtime-hardening/spec.md
Clarified execution semantics: Python uses uv run --project; non-Python may run from session workdir when available; added skill root env vars to prompt requirements.
ACP Terminal & Process
src/main/presenter/llmProviderPresenter/acp/acpProcessManager.ts, src/main/presenter/llmProviderPresenter/acp/acpTerminalManager.ts
Added resolveTerminalCwd logic to prefer params.cwd → session workdir → ACP temp dir; terminal creation now receives resolved cwd and uses Electron temp-path handling and mkdir fallback.
New Agent / Session init
src/main/presenter/newAgentPresenter/index.ts
Session initialization centralized via initializeSessionRuntime; added ACP workdir sync (syncAcpSessionWorkdir) on create/fork/send/queue; added failure cleanup for session init.
Skill Presenter / Execution
src/main/presenter/skillPresenter/index.ts, src/main/presenter/skillPresenter/skillExecutionService.ts
SkillExecutionService accepts optional resolver to prefer conversation workdir for execution cwd; spawn plans include SKILL_ROOT and DEEPCHAT_SKILL_ROOT env entries.
Tooling wiring
src/main/presenter/toolPresenter/agentTools/agentToolManager.ts
Wired resolveConversationWorkdir into SkillExecutionService construction via manager delegate.
ACP tests
test/main/presenter/llmProviderPresenter/acp/acpProcessManager.test.ts, test/main/presenter/llmProviderPresenter/acp/acpTerminalManager.test.ts
Added tests verifying cwd selection precedence and terminal spawn options, mocking node-pty, Electron app.getPath, fs mkdir/stat where needed.
Agent presenter tests
test/main/presenter/newAgentPresenter/*.test.ts, test/main/presenter/newAgentPresenter/integration.test.ts
Extended LLM provider mock with setAcpWorkdir; added tests asserting workdir sync on session creation and error cleanup when setAcpWorkdir fails.
SkillExecutionService tests & setup
test/main/presenter/skillPresenter/skillExecutionService.test.ts, test/setup.ts
Updated tests to provide resolveConversationWorkdir and to assert spawn plan cwd/env behavior; added fs.promises.stat mock to test setup.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NewAgentPresenter
    participant LLMProviderPresenter
    participant SkillExecutionService
    participant TerminalPTY

    Client->>NewAgentPresenter: createSession(sessionId, projectDir)
    NewAgentPresenter->>NewAgentPresenter: initializeSessionRuntime()
    NewAgentPresenter->>NewAgentPresenter: agent.initSession()
    NewAgentPresenter->>LLMProviderPresenter: setAcpWorkdir(sessionId, agentId, projectDir)
    LLMProviderPresenter-->>NewAgentPresenter: ack / register session workdir

    Client->>NewAgentPresenter: sendMessage(sessionId, message)
    NewAgentPresenter->>NewAgentPresenter: syncAcpSessionWorkdir()
    NewAgentPresenter->>LLMProviderPresenter: setAcpWorkdir(sessionId, agentId, projectDir)

    NewAgentPresenter->>SkillExecutionService: buildSpawnPlan(skillRoot, conversationId)
    SkillExecutionService->>LLMProviderPresenter: getWorkdirForConversation(conversationId)
    LLMProviderPresenter-->>SkillExecutionService: sessionWorkdir or null
    SkillExecutionService-->>NewAgentPresenter: spawnPlan { cwd: sessionWorkdir|skillRoot, env: ...SKILL_ROOT... }

    NewAgentPresenter->>TerminalPTY: spawn(command, { cwd: spawnPlan.cwd, env: spawnPlan.env })
    TerminalPTY-->>NewAgentPresenter: process started
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hoppity-hop, workdirs we tuck,

Sessions remember their little luck.
From request to session to temp fallback true,
Scripts and terminals know just what to do.
— The Code Rabbit 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(agent): honor session workdir defaults' accurately reflects the main change: implementing proper handling of session-specific working directories for ACP agent execution, which is the primary objective across multiple files in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-workdir-propagation-under-acp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0d1319634

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +181 to +184
const resolvedWorkdir = await this.resolveConversationWorkdir(conversationId)
const normalizedWorkdir = resolvedWorkdir?.trim()
if (normalizedWorkdir) {
return path.resolve(normalizedWorkdir)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard skill_run against stale session workdirs

When a conversation's saved projectDir has gone stale (for example the repo was renamed or deleted), resolveExecutionCwd() now returns that path unconditionally and both the foreground path here and the background executor use it as cwd. In Node, spawning with a missing cwd raises ENOENT, so skill_run fails before the script even starts even though the skill itself is still present under skillRoot. Before this change the runner always used skillRoot, so missing session workspaces did not brick skill execution.

Useful? React with 👍 / 👎.

Comment on lines +1373 to +1377
} catch (error) {
console.warn('[NewAgentPresenter] Failed to sync ACP workdir for session:', {
conversationId,
agentId,
projectDir: normalizedProjectDir,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail the send when ACP workdir sync does not persist

This helper is awaited from createSession(), sendMessage(), and queuePendingInput() specifically to make the ACP workdir authoritative before the first turn runs, but the new catch converts every persistence/update failure into a warning. If setAcpWorkdir() fails (for example on a transient SQLite write error), acpProvider.coreStream() later reads the old record and AcpSessionPersistence.getWorkdir() falls back to the default home directory, so the agent's first command executes in the wrong workspace with no visible error. That makes the regression very hard to diagnose for affected users.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/presenter/newAgentPresenter/index.ts (1)

275-288: ⚠️ Potential issue | 🟠 Major

Resolve ACP aliases before inferring providerId.

Line 278 and Line 333 still compare the raw session agent id against getAcpAgents(). For legacy ACP ids such as kimi-cli or claude-code-acp, getSessionState() can be empty after a restart, so these branches never mark the session as ACP and the new setAcpWorkdir(...) path is skipped entirely.

🛠️ Suggested fix
-    let providerId = state?.providerId ?? ''
+    const resolvedAgentId = resolveAcpAgentAlias(session.agentId)
+    let providerId = state?.providerId ?? ''
     if (!providerId) {
       const acpAgents = await this.configPresenter.getAcpAgents()
-      if (acpAgents.some((item) => item.id === session.agentId)) {
+      if (acpAgents.some((item) => item.id === resolvedAgentId)) {
         providerId = 'acp'
       }
     }
@@
-    let providerId = (await agent.getSessionState(sessionId))?.providerId ?? ''
+    const resolvedAgentId = resolveAcpAgentAlias(currentSession.agentId)
+    let providerId = (await agent.getSessionState(sessionId))?.providerId ?? ''
     if (!providerId) {
       const acpAgents = await this.configPresenter.getAcpAgents()
-      if (acpAgents.some((item) => item.id === currentSession.agentId)) {
+      if (acpAgents.some((item) => item.id === resolvedAgentId)) {
         providerId = 'acp'
       }
     }

Also applies to: 330-343

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/newAgentPresenter/index.ts` around lines 275 - 288, When
inferring providerId (the providerId variable and the branch that calls
this.configPresenter.getAcpAgents()), normalize/resolve legacy ACP agent aliases
before comparing against acpAgents (i.e., don't compare raw session.agentId);
call the existing configPresenter method that returns the canonical ACP id for a
given agent (e.g., a resolve/getCanonicalAcpId helper on configPresenter) on
session.agentId and use that normalized id in the acpAgents.some(...) check,
then propagate that same normalized logic to the other branch that determines
ACP (the code path that later calls assertAcpSessionHasWorkdir and
syncAcpSessionWorkdir / setAcpWorkdir) so legacy ids like "kimi-cli" or
"claude-code-acp" are recognized as ACP.
🧹 Nitpick comments (4)
test/main/presenter/skillPresenter/skillExecutionService.test.ts (1)

106-116: Assert resolver invocation to lock the propagation contract

Consider asserting that resolveConversationWorkdir is called with the conversation id, so future regressions can’t bypass the resolver silently.

✅ Suggested test addition
     const plan = await (service as never).buildSpawnPlan(
       {
         skill: 'ocr',
         script: 'scripts/run.py',
         args: ['--lang', 'en']
       },
       'conv-1'
     )

+    expect(resolveConversationWorkdir).toHaveBeenCalledWith('conv-1')
     expect(plan.cwd).toBe('/workspace/session')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/skillPresenter/skillExecutionService.test.ts` around
lines 106 - 116, Add an assertion that the resolver for the conversation workdir
was invoked with the conversation id when building the spawn plan: after calling
(service as never).buildSpawnPlan(...) assert that resolveConversationWorkdir
was called with 'conv-1' (or the variable used for the conversation id) to
ensure the propagation contract is enforced; locate the mock or spy for
resolveConversationWorkdir in the test setup (or create one if missing) and use
the test framework's call/spy assertion (e.g., toHaveBeenCalledWith or
equivalent) to validate the invocation alongside the existing expectations on
plan.cwd and plan.env.PATH.
test/main/presenter/llmProviderPresenter/acp/acpProcessManager.test.ts (1)

150-151: Prefer public session registration in tests over private map mutation.

Using registerSessionWorkdir keeps tests aligned with real lifecycle behavior (including fs handler setup) and reduces brittle coupling to internals.

♻️ Proposed test change
-    ;(manager as any).sessionWorkdirs.set('session-1', '/tmp/workspace')
+    manager.registerSessionWorkdir('session-1', '/tmp/workspace')

...
-    ;(manager as any).sessionWorkdirs.set('session-1', '/tmp/workspace')
+    manager.registerSessionWorkdir('session-1', '/tmp/workspace')

Also applies to: 179-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/acp/acpProcessManager.test.ts`
around lines 150 - 151, Replace direct private map mutation of (manager as
any).sessionWorkdirs with the public API: call
manager.registerSessionWorkdir('session-1', '/tmp/workspace') so the test
exercises real lifecycle behavior (including fs handler setup); update the other
occurrence at the second location (lines 179-180) similarly. Ensure you
reference the manager instance and use the public registerSessionWorkdir method
rather than touching sessionWorkdirs directly.
test/main/presenter/llmProviderPresenter/acp/acpTerminalManager.test.ts (1)

23-27: Consider restoring spies after each test.

This avoids accidental mock leakage when additional cases are added later.

♻️ Minimal isolation tweak
-import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
...
   beforeEach(() => {
     vi.clearAllMocks()
     vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined)
     vi.mocked(spawn).mockReturnValue(createPty() as never)
   })
+
+  afterEach(() => {
+    vi.restoreAllMocks()
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/acp/acpTerminalManager.test.ts`
around lines 23 - 27, Add an afterEach block to restore spies/mocks so they
don't leak between tests: afterEach should call vi.restoreAllMocks() (to restore
vi.spyOn(fs, 'mkdirSync') and other spies) and vi.clearAllMocks() (to clear
calls), ensuring the mocked spawn (vi.mocked(spawn)) and the createPty-related
mocks are reset between tests and preventing accidental cross-test interference.
src/main/presenter/llmProviderPresenter/acp/acpTerminalManager.ts (1)

40-59: Guard non-existent cwd before spawning the terminal.

If a provided/session cwd is stale or deleted, terminal creation can fail hard. Falling back to the controlled temp directory keeps behavior resilient.

♻️ Proposed hardening
 private resolveTerminalCwd(cwd?: string | null): string {
   const normalized = cwd?.trim()
   if (normalized) {
-    return path.resolve(normalized)
+    const resolved = path.resolve(normalized)
+    try {
+      if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
+        return resolved
+      }
+      console.warn(`[ACP Terminal] Invalid cwd "${resolved}", using fallback directory`)
+    } catch (error) {
+      console.warn(`[ACP Terminal] Failed to validate cwd "${resolved}", using fallback directory`, error)
+    }
   }

   const fallbackDir = path.join(app.getPath('temp'), 'deepchat-acp', 'terminals')
   try {

Also applies to: 69-69, 103-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/llmProviderPresenter/acp/acpTerminalManager.ts` around
lines 40 - 59, The resolveTerminalCwd function should verify that the provided
cwd actually exists and is a directory before returning it; change the early
return path in resolveTerminalCwd to check fs.existsSync(normalized) and
fs.statSync(normalized).isDirectory() (wrapped in try/catch), and if the check
fails, log a warning and fall back to creating/using the fallbackDir (same logic
already present) so stale/deleted session cwd values don't cause terminal spawn
failures; apply the same existence/is-directory guard wherever terminal spawn
uses resolveTerminalCwd or similar cwd resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/newAgentPresenter/index.ts`:
- Around line 1352-1380: The syncAcpSessionWorkdir helper currently swallows
setAcpWorkdir errors, causing callers (init/send/queue/fork) to proceed with an
incorrect workdir; change it to surface failures by either removing the
try/catch so the exception bubbles from syncAcpSessionWorkdir (propagating the
error from llmProviderPresenter.setAcpWorkdir) or by returning an explicit
failure result (e.g., boolean or Result) and updating all callers to check that
result and abort/cleanup when it indicates failure; keep the existing log but do
not suppress the error—ensure syncAcpSessionWorkdir, resolveAcpAgentAlias and
llmProviderPresenter.setAcpWorkdir are updated consistently so callers can react
to a failed sync.

In `@src/main/presenter/skillPresenter/skillExecutionService.ts`:
- Around line 174-200: In resolveExecutionCwd, validate the path returned from
resolveConversationWorkdir before using it as the cwd: after obtaining
resolvedWorkdir (in the resolveExecutionCwd method), call fs.promises.stat() (or
fs.stat) to ensure the path exists and is a directory; if stat fails or the path
is not a directory, log a warning (including conversationId and the invalid
path) and fall back to normalizedSkillRoot; keep resolveConversationWorkdir
error handling as-is but add this extra validation step before returning
path.resolve(normalizedWorkdir).

In `@test/main/presenter/llmProviderPresenter/acp/acpProcessManager.test.ts`:
- Around line 217-222: The assertion hardcodes '/tmp/deepchat-acp/sessions'
which breaks on Windows; update the test to build a platform-safe expectation by
importing Node's path and asserting cwd contains the platform-joined subpath
instead of the full hardcoded string—for example, in acpProcessManager.test.ts
change the createTerminal expectation (the expect.objectContaining block for
createTerminal) to use cwd:
expect.stringContaining(path.join('deepchat-acp','sessions')) (keep sessionId:
'missing-session' and command: 'pwd' the same) so the test uses path.join and is
path-separator agnostic.

In `@test/main/presenter/llmProviderPresenter/acp/acpTerminalManager.test.ts`:
- Around line 38-44: Tests assume a Unix shell and POSIX paths; make the spawn
expectations platform-aware by branching on process.platform (or using
path.normalize) so the assertion matches the correct shell executable and cwd
format. Update the assertions around spawn in acpTerminalManager.test.ts (the
mocked spawn checks) to: check cwd using path.normalize or
expect.stringContaining(path.normalize('/tmp/workspace')) and assert the shell
argument with a platform-specific matcher (e.g., allow '/bin/bash' or 'bash' on
POSIX and 'cmd.exe' or 'powershell' on win32), or use
expect.stringMatching(/bash|cmd|powershell/i) for the first arg—apply the same
change to the other spawn assertion block (the one at lines ~55-63).

---

Outside diff comments:
In `@src/main/presenter/newAgentPresenter/index.ts`:
- Around line 275-288: When inferring providerId (the providerId variable and
the branch that calls this.configPresenter.getAcpAgents()), normalize/resolve
legacy ACP agent aliases before comparing against acpAgents (i.e., don't compare
raw session.agentId); call the existing configPresenter method that returns the
canonical ACP id for a given agent (e.g., a resolve/getCanonicalAcpId helper on
configPresenter) on session.agentId and use that normalized id in the
acpAgents.some(...) check, then propagate that same normalized logic to the
other branch that determines ACP (the code path that later calls
assertAcpSessionHasWorkdir and syncAcpSessionWorkdir / setAcpWorkdir) so legacy
ids like "kimi-cli" or "claude-code-acp" are recognized as ACP.

---

Nitpick comments:
In `@src/main/presenter/llmProviderPresenter/acp/acpTerminalManager.ts`:
- Around line 40-59: The resolveTerminalCwd function should verify that the
provided cwd actually exists and is a directory before returning it; change the
early return path in resolveTerminalCwd to check fs.existsSync(normalized) and
fs.statSync(normalized).isDirectory() (wrapped in try/catch), and if the check
fails, log a warning and fall back to creating/using the fallbackDir (same logic
already present) so stale/deleted session cwd values don't cause terminal spawn
failures; apply the same existence/is-directory guard wherever terminal spawn
uses resolveTerminalCwd or similar cwd resolution.

In `@test/main/presenter/llmProviderPresenter/acp/acpProcessManager.test.ts`:
- Around line 150-151: Replace direct private map mutation of (manager as
any).sessionWorkdirs with the public API: call
manager.registerSessionWorkdir('session-1', '/tmp/workspace') so the test
exercises real lifecycle behavior (including fs handler setup); update the other
occurrence at the second location (lines 179-180) similarly. Ensure you
reference the manager instance and use the public registerSessionWorkdir method
rather than touching sessionWorkdirs directly.

In `@test/main/presenter/llmProviderPresenter/acp/acpTerminalManager.test.ts`:
- Around line 23-27: Add an afterEach block to restore spies/mocks so they don't
leak between tests: afterEach should call vi.restoreAllMocks() (to restore
vi.spyOn(fs, 'mkdirSync') and other spies) and vi.clearAllMocks() (to clear
calls), ensuring the mocked spawn (vi.mocked(spawn)) and the createPty-related
mocks are reset between tests and preventing accidental cross-test interference.

In `@test/main/presenter/skillPresenter/skillExecutionService.test.ts`:
- Around line 106-116: Add an assertion that the resolver for the conversation
workdir was invoked with the conversation id when building the spawn plan: after
calling (service as never).buildSpawnPlan(...) assert that
resolveConversationWorkdir was called with 'conv-1' (or the variable used for
the conversation id) to ensure the propagation contract is enforced; locate the
mock or spy for resolveConversationWorkdir in the test setup (or create one if
missing) and use the test framework's call/spy assertion (e.g.,
toHaveBeenCalledWith or equivalent) to validate the invocation alongside the
existing expectations on plan.cwd and plan.env.PATH.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e99f9f3-c558-484f-bf68-006d981ed046

📥 Commits

Reviewing files that changed from the base of the PR and between 98450f5 and f0d1319.

📒 Files selected for processing (13)
  • docs/specs/skill-runtime-hardening/spec.md
  • src/main/presenter/llmProviderPresenter/acp/acpProcessManager.ts
  • src/main/presenter/llmProviderPresenter/acp/acpTerminalManager.ts
  • src/main/presenter/newAgentPresenter/index.ts
  • src/main/presenter/skillPresenter/index.ts
  • src/main/presenter/skillPresenter/skillExecutionService.ts
  • src/main/presenter/toolPresenter/agentTools/agentToolManager.ts
  • test/main/presenter/llmProviderPresenter/acp/acpProcessManager.test.ts
  • test/main/presenter/llmProviderPresenter/acp/acpTerminalManager.test.ts
  • test/main/presenter/newAgentPresenter/integration.test.ts
  • test/main/presenter/newAgentPresenter/newAgentPresenter.test.ts
  • test/main/presenter/newAgentPresenter/usageDashboard.test.ts
  • test/main/presenter/skillPresenter/skillExecutionService.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/main/presenter/skillPresenter/skillExecutionService.ts (1)

190-221: Avoid duplicate warning logs for one fallback event.

When workdir resolution fails/non-directory, the method logs a specific warning and then also logs “Missing conversation workdir.” This can create noisy, misleading duplicate warnings for a single failure.

🧹 Suggested adjustment
   private async resolveExecutionCwd(conversationId: string, skillRoot: string): Promise<string> {
     const normalizedSkillRoot = path.resolve(skillRoot)
     if (!this.resolveConversationWorkdir) {
       return normalizedSkillRoot
     }

     try {
       const resolvedWorkdir = await this.resolveConversationWorkdir(conversationId)
       const normalizedWorkdir = resolvedWorkdir?.trim()
       if (normalizedWorkdir) {
         const resolvedPath = path.resolve(normalizedWorkdir)
         try {
           const stat = await fs.promises.stat(resolvedPath)
           if (stat.isDirectory()) {
             return resolvedPath
           }
           logger.warn(
             '[SkillExecutionService] Conversation workdir is not a directory, falling back to skill root',
             {
               conversationId,
               invalidWorkdir: resolvedPath
             }
           )
+          return normalizedSkillRoot
         } catch (error) {
           logger.warn(
             '[SkillExecutionService] Conversation workdir is invalid, falling back to skill root',
             {
               conversationId,
               invalidWorkdir: resolvedPath,
               error
             }
           )
+          return normalizedSkillRoot
         }
       }
     } catch (error) {
       logger.warn('[SkillExecutionService] Failed to resolve conversation workdir', {
         conversationId,
         error
       })
+      return normalizedSkillRoot
     }

     logger.warn(
       '[SkillExecutionService] Missing conversation workdir, falling back to skill root',
       {
         conversationId,
         skillRoot: normalizedSkillRoot
       }
     )
     return normalizedSkillRoot
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/skillPresenter/skillExecutionService.ts` around lines 190
- 221, The method in SkillExecutionService emits multiple logger.warn messages
for the same fallback (e.g., when resolvedPath is not a directory or resolution
throws), causing duplicate "Missing conversation workdir" warnings; change the
control flow so only one warning is emitted per fallback: either return/exit the
function after logging the specific warning for resolvedPath (the logger.warn
calls that include conversationId and invalidWorkdir) or introduce a boolean
flag (e.g., workdirResolved or didFallback) and set it when you log the specific
warning, then skip the final logger.warn that logs "Missing conversation
workdir" (which uses normalizedSkillRoot and conversationId) if that flag is
set; update logic around the try/catch blocks where resolvedPath, error,
normalizedSkillRoot and conversationId are used to ensure a single consolidated
warning is logged.
src/main/presenter/newAgentPresenter/index.ts (1)

293-298: Consider guarding redundant sync calls with a dirty check.

syncAcpSessionWorkdir is called on every sendMessage and queuePendingInput, even when the workdir hasn't changed since the last sync. Per the acpProvider.updateAcpWorkdir implementation (context snippet), this is idempotent—the session is only cleared if the resolved path differs—but it still incurs unnecessary async I/O.

A simple optimization would be to track the last-synced workdir per session and skip the call when unchanged.

Also applies to: 348-353

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/newAgentPresenter/index.ts` around lines 293 - 298, Add a
per-session "lastSyncedWorkdir" check to avoid redundant async syncs: in the
callers sendMessage and queuePendingInput, before calling
syncAcpSessionWorkdir(providerId, sessionId, session.agentId, session.projectDir
?? null) compare session.projectDir (or resolved workdir) against a stored
lastSyncedWorkdir on the session (or in a small in-memory map keyed by
sessionId); only call syncAcpSessionWorkdir when the value differs, and update
lastSyncedWorkdir after a successful sync. Keep reference to
syncAcpSessionWorkdir, sendMessage, queuePendingInput, and
acpProvider.updateAcpWorkdir to locate where to add the check and update.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/presenter/newAgentPresenter/index.ts`:
- Around line 293-298: Add a per-session "lastSyncedWorkdir" check to avoid
redundant async syncs: in the callers sendMessage and queuePendingInput, before
calling syncAcpSessionWorkdir(providerId, sessionId, session.agentId,
session.projectDir ?? null) compare session.projectDir (or resolved workdir)
against a stored lastSyncedWorkdir on the session (or in a small in-memory map
keyed by sessionId); only call syncAcpSessionWorkdir when the value differs, and
update lastSyncedWorkdir after a successful sync. Keep reference to
syncAcpSessionWorkdir, sendMessage, queuePendingInput, and
acpProvider.updateAcpWorkdir to locate where to add the check and update.

In `@src/main/presenter/skillPresenter/skillExecutionService.ts`:
- Around line 190-221: The method in SkillExecutionService emits multiple
logger.warn messages for the same fallback (e.g., when resolvedPath is not a
directory or resolution throws), causing duplicate "Missing conversation
workdir" warnings; change the control flow so only one warning is emitted per
fallback: either return/exit the function after logging the specific warning for
resolvedPath (the logger.warn calls that include conversationId and
invalidWorkdir) or introduce a boolean flag (e.g., workdirResolved or
didFallback) and set it when you log the specific warning, then skip the final
logger.warn that logs "Missing conversation workdir" (which uses
normalizedSkillRoot and conversationId) if that flag is set; update logic around
the try/catch blocks where resolvedPath, error, normalizedSkillRoot and
conversationId are used to ensure a single consolidated warning is logged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c784ae9d-be6b-4828-aa10-9be868fdc80b

📥 Commits

Reviewing files that changed from the base of the PR and between f0d1319 and b2153f0.

📒 Files selected for processing (7)
  • src/main/presenter/newAgentPresenter/index.ts
  • src/main/presenter/skillPresenter/skillExecutionService.ts
  • test/main/presenter/llmProviderPresenter/acp/acpProcessManager.test.ts
  • test/main/presenter/llmProviderPresenter/acp/acpTerminalManager.test.ts
  • test/main/presenter/newAgentPresenter/newAgentPresenter.test.ts
  • test/main/presenter/skillPresenter/skillExecutionService.test.ts
  • test/setup.ts
✅ Files skipped from review due to trivial changes (2)
  • test/setup.ts
  • test/main/presenter/newAgentPresenter/newAgentPresenter.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/main/presenter/skillPresenter/skillExecutionService.test.ts

@zerob13 zerob13 merged commit a97e477 into dev Mar 23, 2026
3 checks passed
@zerob13 zerob13 deleted the codex/fix-workdir-propagation-under-acp branch March 29, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant