Summary
In the recursive monorepo docs feature, createRepositorySourceSnapshot
(src/agent/utils.ts) builds a source fingerprint that is not scoped to
the subproject subtree. This leaves a residual cross-subproject cascade on
subproject resume: a sibling commit can force a subproject to discard its
interrupted plan and replan, even though that subproject did not change.
Found during adversarial review of the upstream re-integration merge e9cb709
(PR langchain-ai#439). This is conservative — it can only cause extra planning, never
a false skip and never stale output — so it is a follow-up, not a blocker.
Invariant at stake
The recursive feature guarantees no cross-subproject cascade: a subproject
must not regenerate or replan merely because a sibling subproject changed.
This is enforced by scoping git evidence to a subtree via a GitScope pathspec
(git status/diff are repo-wide regardless of cwd). getUpdateNoopStatus,
getChangedPathsSinceLastUpdate, and getRepositoryChangedPaths all already
honor a scope?: GitScope; plannerEvidenceScope(role) in
src/generation/repository-run.ts maps the run's RecursionRole to the right
scope.
The gap
createRepositorySourceSnapshot fingerprints two repo-wide inputs:
git status --porcelain dirty-tree entries — filtered only by
assertFingerprintGitPath / isFingerprintSourcePath, which reject ../
escapes but include sibling repo-root-relative paths
(e.g. packages/other/x.ts).
- The repo-wide HEAD commit hash.
Because both are repo-wide, a subproject's fingerprint changes whenever any
sibling changes or any commit advances HEAD.
Impact (established by review)
- Fresh subproject runs are safe — the no-op skip decision uses the scoped
getUpdateNoopStatus, not the fingerprint. The fingerprint only appears in a
self-consistent stability check (current-vs-current mid-run drift detection)
and in resumeRepositoryRun.
- Subproject resume is where it bites: a sibling commit between begin and
resume flips the fingerprint → sourceChanged=true → the interrupted plan is
discarded and the subproject replans. Conservative (extra planning only).
- A rarer mid-run race (sibling change between the two stability snapshots) can
abort a no-op finalize and fall through to planning — also conservative.
Why the fix is non-trivial
Scoping only the git status --porcelain query is insufficient: the
repo-wide HEAD hash alone still flips the fingerprint when a sibling commit
advances HEAD. A full subtree-scoped fingerprint needs a subtree-scoped notion
of "HEAD" too (e.g. git log -1 --format=%H -- <subtree>, or the subtree tree
hash git rev-parse HEAD:<subtree>) rather than the global HEAD — and that value
interacts with the page-manifest baseline gitHead, which is compared against
global commit ranges in getRepositoryChangedPaths(root, ignore, baseline, scope). So the manifest-baseline vs fingerprint-HEAD semantics must be reasoned
through carefully to avoid breaking page fast-forwarding.
Acceptance criteria
A detailed implementation plan (options + recommendation) will be added as a
comment.
Summary
In the recursive monorepo docs feature,
createRepositorySourceSnapshot(
src/agent/utils.ts) builds a source fingerprint that is not scoped tothe subproject subtree. This leaves a residual cross-subproject cascade on
subproject resume: a sibling commit can force a subproject to discard its
interrupted plan and replan, even though that subproject did not change.
Found during adversarial review of the upstream re-integration merge
e9cb709(PR langchain-ai#439). This is conservative — it can only cause extra planning, never
a false skip and never stale output — so it is a follow-up, not a blocker.
Invariant at stake
The recursive feature guarantees no cross-subproject cascade: a subproject
must not regenerate or replan merely because a sibling subproject changed.
This is enforced by scoping git evidence to a subtree via a
GitScopepathspec(git status/diff are repo-wide regardless of cwd).
getUpdateNoopStatus,getChangedPathsSinceLastUpdate, andgetRepositoryChangedPathsall alreadyhonor a
scope?: GitScope;plannerEvidenceScope(role)insrc/generation/repository-run.tsmaps the run'sRecursionRoleto the rightscope.
The gap
createRepositorySourceSnapshotfingerprints two repo-wide inputs:git status --porcelaindirty-tree entries — filtered only byassertFingerprintGitPath/isFingerprintSourcePath, which reject../escapes but include sibling repo-root-relative paths
(e.g.
packages/other/x.ts).Because both are repo-wide, a subproject's fingerprint changes whenever any
sibling changes or any commit advances HEAD.
Impact (established by review)
getUpdateNoopStatus, not the fingerprint. The fingerprint only appears in aself-consistent stability check (current-vs-current mid-run drift detection)
and in
resumeRepositoryRun.resume flips the fingerprint →
sourceChanged=true→ the interrupted plan isdiscarded and the subproject replans. Conservative (extra planning only).
abort a no-op finalize and fall through to planning — also conservative.
Why the fix is non-trivial
Scoping only the
git status --porcelainquery is insufficient: therepo-wide HEAD hash alone still flips the fingerprint when a sibling commit
advances HEAD. A full subtree-scoped fingerprint needs a subtree-scoped notion
of "HEAD" too (e.g.
git log -1 --format=%H -- <subtree>, or the subtree treehash
git rev-parse HEAD:<subtree>) rather than the global HEAD — and that valueinteracts with the page-manifest baseline
gitHead, which is compared againstglobal commit ranges in
getRepositoryChangedPaths(root, ignore, baseline, scope). So the manifest-baseline vs fingerprint-HEAD semantics must be reasonedthrough carefully to avoid breaking page fast-forwarding.
Acceptance criteria
today (gate on
recursionRole, mirroring hownoopScopeleaves rootrepo-wide).
.run.json/ page manifests carryrepo-wide hashes; first resume/update after the change behaves sanely
(worst case: one conservative replan, never stale output).
A detailed implementation plan (options + recommendation) will be added as a
comment.