fix(worktree): self-heal Chroma metadata on re-run
Addresses unresolved CodeRabbit finding on WorktreeAdoption.ts:296. Previously, Chroma patch failures stranded rows permanently: adoptedSqliteIds was built only from rows where merged_into_project IS NULL, so once SQL committed, reruns couldn't rediscover them for retry. The Chroma id set is now built from ALL observations whose project matches a merged worktree — including rows already stamped to this parent. Combined with the idempotent updateMergedIntoProject, transient Chroma failures self-heal on the next adoption pass. SQL writes remain idempotent (UPDATE still guards on merged_into_project IS NULL), so adoptedObservations / adoptedSummaries continue to count only newly-adopted rows. chromaUpdates now counts total Chroma writes per pass (may exceed adoptedObservations when retrying).
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -127,15 +127,17 @@ function listMergedBranches(mainRepo: string): Set<string> {
|
|||||||
* Stamp `merged_into_project` on observations and session_summaries for every
|
* Stamp `merged_into_project` on observations and session_summaries for every
|
||||||
* worktree of `opts.repoPath` whose branch has been merged into the parent's HEAD.
|
* worktree of `opts.repoPath` whose branch has been merged into the parent's HEAD.
|
||||||
*
|
*
|
||||||
* Idempotent: a row is only touched when its `merged_into_project IS NULL`.
|
* SQL writes are idempotent: an UPDATE only touches rows where
|
||||||
|
* `merged_into_project IS NULL`. `result.adoptedObservations` / `adoptedSummaries`
|
||||||
|
* reflect the actual SQL changes on each run.
|
||||||
*
|
*
|
||||||
* Chroma is patched AFTER SQL commits. Chroma failure does NOT roll back SQL —
|
* Chroma patches are self-healing: the Chroma id set is built from ALL
|
||||||
* SQL is source of truth. A transient Chroma failure does NOT auto-retry:
|
* observations whose `project` matches a merged worktree (both unadopted rows
|
||||||
* once SQL commits, `merged_into_project IS NULL` no longer matches those rows,
|
* AND rows previously stamped to this parent), and `updateMergedIntoProject`
|
||||||
* so the same adoption pass won't rediscover them. If Chroma patching fails,
|
* is idempotent, so a transient Chroma failure on an earlier run is retried
|
||||||
* `result.chromaFailed` reflects the count — callers should surface this to
|
* automatically on the next adoption pass. `result.chromaUpdates` therefore
|
||||||
* the operator, and re-running adoption after clearing `merged_into_project`
|
* counts the total Chroma writes performed this pass (which may exceed
|
||||||
* (or reseeding Chroma) is the recovery path.
|
* `adoptedObservations` when retries happen).
|
||||||
*/
|
*/
|
||||||
export async function adoptMergedWorktrees(opts: {
|
export async function adoptMergedWorktrees(opts: {
|
||||||
repoPath?: string;
|
repoPath?: string;
|
||||||
@@ -228,8 +230,16 @@ export async function adoptMergedWorktrees(opts: {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectObs = db.prepare(
|
// Select ALL observations for the worktree project (both unadopted rows
|
||||||
'SELECT id FROM observations WHERE project = ? AND merged_into_project IS NULL'
|
// AND rows already stamped to this parent), not just unadopted ones. This
|
||||||
|
// ensures a transient Chroma failure on a prior run gets retried the next
|
||||||
|
// time adoption executes: SQL may already be stamped, but we re-include
|
||||||
|
// those ids in the Chroma patch set (updateMergedIntoProject is idempotent
|
||||||
|
// — it replays the same metadata write).
|
||||||
|
const selectObsForPatch = db.prepare(
|
||||||
|
`SELECT id FROM observations
|
||||||
|
WHERE project = ?
|
||||||
|
AND (merged_into_project IS NULL OR merged_into_project = ?)`
|
||||||
);
|
);
|
||||||
const updateObs = db.prepare(
|
const updateObs = db.prepare(
|
||||||
'UPDATE observations SET merged_into_project = ? WHERE project = ? AND merged_into_project IS NULL'
|
'UPDATE observations SET merged_into_project = ? WHERE project = ? AND merged_into_project IS NULL'
|
||||||
@@ -242,9 +252,14 @@ export async function adoptMergedWorktrees(opts: {
|
|||||||
for (const wt of targets) {
|
for (const wt of targets) {
|
||||||
try {
|
try {
|
||||||
const worktreeProject = getProjectContext(wt.path).primary;
|
const worktreeProject = getProjectContext(wt.path).primary;
|
||||||
const rows = selectObs.all(worktreeProject) as Array<{ id: number }>;
|
const rows = selectObsForPatch.all(
|
||||||
|
worktreeProject,
|
||||||
|
parentProject
|
||||||
|
) as Array<{ id: number }>;
|
||||||
for (const r of rows) adoptedSqliteIds.push(r.id);
|
for (const r of rows) adoptedSqliteIds.push(r.id);
|
||||||
|
|
||||||
|
// updateObs/updateSum only touch WHERE merged_into_project IS NULL,
|
||||||
|
// so .changes reflects only newly-adopted rows (not the re-patched ones).
|
||||||
const obsChanges = updateObs.run(parentProject, worktreeProject).changes;
|
const obsChanges = updateObs.run(parentProject, worktreeProject).changes;
|
||||||
const sumChanges = updateSum.run(parentProject, worktreeProject).changes;
|
const sumChanges = updateSum.run(parentProject, worktreeProject).changes;
|
||||||
result.adoptedObservations += obsChanges;
|
result.adoptedObservations += obsChanges;
|
||||||
|
|||||||
Reference in New Issue
Block a user