Skip to content

fix: skip repeated workspace file injection after first turn; include agentDir bootstrap files#52916

Closed
ajitpratap0 wants to merge 1 commit into
openclaw:mainfrom
ajitpratap0:fix/agent-bootstrap-token-waste-and-agentdir
Closed

fix: skip repeated workspace file injection after first turn; include agentDir bootstrap files#52916
ajitpratap0 wants to merge 1 commit into
openclaw:mainfrom
ajitpratap0:fix/agent-bootstrap-token-waste-and-agentdir

Conversation

@ajitpratap0

Copy link
Copy Markdown

Summary

Fixes #9157 and #29387.

#9157 — Token waste from repeated workspace file injection

  • resolveBootstrapContextForRun() was called on every message turn, re-injecting workspace files (AGENTS.md, CLAUDE.md, etc.) each time
  • Added session-level tracking to skip injection after the first turn
  • Reduces token waste by ~93.5% in multi-turn sessions

#29387 — agentDir bootstrap files silently ignored

  • Bootstrap file resolution only checked the workspace directory, ignoring files in agentDir
  • Extended resolution to also discover and inject AGENTS.md/CLAUDE.md from agentDir
  • Fixes security gap where AGENTS.md safety rules in the agent directory never took effect

Test plan

  • Unit tests for bootstrap skip-after-first logic
  • Unit tests for agentDir file discovery
  • pnpm test -- src/agents/bootstrap-files.test.ts passes

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S r: too-many-prs Auto-close: author has more than twenty active PRs. labels Mar 23, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because the author has more than 10 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.

@greptile-apps

greptile-apps Bot commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR addresses two orthogonal issues: it eliminates repeated workspace bootstrap-file injection on every message turn (isSubsequentTurn guard in resolveBootstrapContextForRun), and extends bootstrap resolution to also pick up AGENTS.md/CLAUDE.md from the agent-specific directory (agentDir) with priority over workspace-level files.

  • Token savings: isSubsequentTurn is correctly derived from a pre-existing session file stat and the guard is placed before buildBootstrapContextFiles, so bootstrapFiles are still populated for budget analysis while contextFiles is empty on subsequent turns — exactly the right semantics.
  • agentDir merging: mergeBootstrapFilesFromAgentDir correctly deduplicates by name (agentDir wins). Minor robustness concern: the resolvedAgentDir !== resolvedWorkspaceDir comparison uses resolveUserPath which only expands ~ — paths with trailing slashes or .. segments could slip past the guard.
  • isSubsequentTurn stat: Duplicates the later hadSessionFile stat (~line 2029). Not a bug since repairSessionFileIfNeeded rewrites rather than deletes the file, but worth noting for future maintainers.
  • Tests: New tests cover the isSubsequentTurn branching and agentDir priority/deduplication scenarios well.

Confidence Score: 4/5

  • Safe to merge — both fixes are correct with good test coverage; remaining items are non-blocking style suggestions.
  • Core logic for both isSubsequentTurn and agentDir merging is correct and well-tested. The two P2 items (path normalisation for the agentDir comparison, and the duplicate stat call) are style suggestions that don't affect correctness under normal usage. No data-loss or security risk identified.
  • No files require special attention, though the resolvedAgentDir !== resolvedWorkspaceDir comparison in bootstrap-files.ts could be hardened with path.resolve.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 1688-1691

Comment:
**Duplicate `stat` on session file**

`isSubsequentTurn` is derived from `stat(params.sessionFile)` here, but an identical check is repeated ~340 lines later as `hadSessionFile` (line ~2029). Since `repairSessionFileIfNeeded` rewrites rather than deletes the file, both checks will always agree in practice.

Consider deriving `isSubsequentTurn` from `hadSessionFile` to avoid the redundant syscall and keep the source-of-truth in one place. This would require moving bootstrap resolution to after the session lock block, or threading `hadSessionFile` back up — but even a short comment linking the two would help future readers.

```suggestion
    // isSubsequentTurn is determined from the session-file stat that also drives hadSessionFile
    // (see ~line 2029). Pre-check here so bootstrap resolution (below) can skip token injection
    // on subsequent turns. repair does not delete the file, so both checks agree.
    const isSubsequentTurn = await fs
      .stat(params.sessionFile)
      .then(() => true)
      .catch(() => false);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/bootstrap-files.ts
Line: 116-121

Comment:
**Path comparison may miss semantically-identical paths**

`resolveUserPath` only expands `~` prefixes and trims whitespace — it does not call `path.resolve` or `path.normalize`. Two paths that refer to the same directory but are expressed differently (e.g. trailing slash, embedded `..`, or symlinks) would compare as unequal, causing `mergeBootstrapFilesFromAgentDir` to run unnecessarily and potentially produce duplicate entries.

Consider normalising both paths with `path.resolve` before comparing:

```suggestion
  const resolvedAgentDir = params.agentDir ? path.resolve(resolveUserPath(params.agentDir)) : undefined;
  const resolvedWorkspaceDir = path.resolve(resolveUserPath(params.workspaceDir));
```

(`path` is already imported in this file's peer code; add `import path from "node:path"` at the top if not already present.)

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix: skip repeated workspace file inject..." | Re-trigger Greptile

Comment on lines +1688 to +1691
const isSubsequentTurn = await fs
.stat(params.sessionFile)
.then(() => true)
.catch(() => false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Duplicate stat on session file

isSubsequentTurn is derived from stat(params.sessionFile) here, but an identical check is repeated ~340 lines later as hadSessionFile (line ~2029). Since repairSessionFileIfNeeded rewrites rather than deletes the file, both checks will always agree in practice.

Consider deriving isSubsequentTurn from hadSessionFile to avoid the redundant syscall and keep the source-of-truth in one place. This would require moving bootstrap resolution to after the session lock block, or threading hadSessionFile back up — but even a short comment linking the two would help future readers.

Suggested change
const isSubsequentTurn = await fs
.stat(params.sessionFile)
.then(() => true)
.catch(() => false);
// isSubsequentTurn is determined from the session-file stat that also drives hadSessionFile
// (see ~line 2029). Pre-check here so bootstrap resolution (below) can skip token injection
// on subsequent turns. repair does not delete the file, so both checks agree.
const isSubsequentTurn = await fs
.stat(params.sessionFile)
.then(() => true)
.catch(() => false);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 1688-1691

Comment:
**Duplicate `stat` on session file**

`isSubsequentTurn` is derived from `stat(params.sessionFile)` here, but an identical check is repeated ~340 lines later as `hadSessionFile` (line ~2029). Since `repairSessionFileIfNeeded` rewrites rather than deletes the file, both checks will always agree in practice.

Consider deriving `isSubsequentTurn` from `hadSessionFile` to avoid the redundant syscall and keep the source-of-truth in one place. This would require moving bootstrap resolution to after the session lock block, or threading `hadSessionFile` back up — but even a short comment linking the two would help future readers.

```suggestion
    // isSubsequentTurn is determined from the session-file stat that also drives hadSessionFile
    // (see ~line 2029). Pre-check here so bootstrap resolution (below) can skip token injection
    // on subsequent turns. repair does not delete the file, so both checks agree.
    const isSubsequentTurn = await fs
      .stat(params.sessionFile)
      .then(() => true)
      .catch(() => false);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +116 to +121
const resolvedAgentDir = params.agentDir ? resolveUserPath(params.agentDir) : undefined;
const resolvedWorkspaceDir = resolveUserPath(params.workspaceDir);
const mergedRawFiles =
resolvedAgentDir && resolvedAgentDir !== resolvedWorkspaceDir
? await mergeBootstrapFilesFromAgentDir(rawFiles, resolvedAgentDir)
: rawFiles;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Path comparison may miss semantically-identical paths

resolveUserPath only expands ~ prefixes and trims whitespace — it does not call path.resolve or path.normalize. Two paths that refer to the same directory but are expressed differently (e.g. trailing slash, embedded .., or symlinks) would compare as unequal, causing mergeBootstrapFilesFromAgentDir to run unnecessarily and potentially produce duplicate entries.

Consider normalising both paths with path.resolve before comparing:

Suggested change
const resolvedAgentDir = params.agentDir ? resolveUserPath(params.agentDir) : undefined;
const resolvedWorkspaceDir = resolveUserPath(params.workspaceDir);
const mergedRawFiles =
resolvedAgentDir && resolvedAgentDir !== resolvedWorkspaceDir
? await mergeBootstrapFilesFromAgentDir(rawFiles, resolvedAgentDir)
: rawFiles;
const resolvedAgentDir = params.agentDir ? path.resolve(resolveUserPath(params.agentDir)) : undefined;
const resolvedWorkspaceDir = path.resolve(resolveUserPath(params.workspaceDir));

(path is already imported in this file's peer code; add import path from "node:path" at the top if not already present.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/bootstrap-files.ts
Line: 116-121

Comment:
**Path comparison may miss semantically-identical paths**

`resolveUserPath` only expands `~` prefixes and trims whitespace — it does not call `path.resolve` or `path.normalize`. Two paths that refer to the same directory but are expressed differently (e.g. trailing slash, embedded `..`, or symlinks) would compare as unequal, causing `mergeBootstrapFilesFromAgentDir` to run unnecessarily and potentially produce duplicate entries.

Consider normalising both paths with `path.resolve` before comparing:

```suggestion
  const resolvedAgentDir = params.agentDir ? path.resolve(resolveUserPath(params.agentDir)) : undefined;
  const resolvedWorkspaceDir = path.resolve(resolveUserPath(params.workspaceDir));
```

(`path` is already imported in this file's peer code; add `import path from "node:path"` at the top if not already present.)

How can I resolve this? If you propose a fix, please make it concise.

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

Copy link
Copy Markdown

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: 890afe5d13

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1688 to +1690
const isSubsequentTurn = await fs
.stat(params.sessionFile)
.then(() => true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Base turn detection on transcript state, not file existence

isSubsequentTurn is derived from fs.stat(sessionFile), which marks any existing file as a later turn even when the transcript is empty/header-only (for example pre-created files or a failed first attempt). In those cases this path skips contextFiles injection, so bootstrap instructions (AGENTS/BOOTSTRAP, etc.) are omitted on what is effectively the first usable turn. This is reachable because the runner already handles hadSessionFile && !hasAssistant as an initial-state repair path; turn detection should similarly check transcript content (at least one persisted turn) rather than file existence alone.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling r: too-many-prs Auto-close: author has more than twenty active PRs. size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance: Workspace file injection wastes 93.5% of token budget

1 participant