Skip to content

Commit cc9efd2

Browse files
committed
mason: wire git commit backlog drain into dream-timer sweep
After the indexed git sweep releases its lease, invoke drainCommitBacklogForProject (ignoreCooldown + releaseGitSweepLease) so pre-existing unembedded commits embed when git log finds no new commits.
1 parent b66449d commit cc9efd2

4 files changed

Lines changed: 139 additions & 6 deletions

File tree

packages/plugin/src/features/magic-context/project-embedding-registry.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ import {
1010
replaceCompartmentChunkEmbeddings,
1111
} from "./compartment-chunk-embedding";
1212
import { appendCompartments, getCompartments } from "./compartment-storage";
13+
import type { GitCommit } from "./git-commits/git-log-reader";
14+
import { countEmbeddedCommits } from "./git-commits/storage-git-commit-embeddings";
15+
import { upsertCommits } from "./git-commits/storage-git-commits";
16+
import { acquireGitSweepLease, releaseGitSweepLease } from "./git-commits/sweep-coordinator";
1317
import type { EmbeddingProvider, EmbeddingPurpose } from "./memory/embedding-provider";
1418
import { insertMemory } from "./memory/storage-memory";
1519
import { getStoredModelId, saveEmbedding } from "./memory/storage-memory-embeddings";
1620
import {
1721
_resetProjectEmbeddingRegistryForTests,
1822
_setTestProviderFactoryForProject,
23+
drainCommitBacklogForProject,
1924
embedSessionCompartmentChunks,
2025
embedTextForProject,
2126
embedUnembeddedCompartmentChunksForProject,
@@ -72,6 +77,17 @@ function localConfig(model: string, maxInputTokens?: number): EmbeddingConfig {
7277
};
7378
}
7479

80+
function makeGitCommit(shaSeed: string, committedAtMs: number): GitCommit {
81+
const sha = shaSeed.padEnd(40, shaSeed);
82+
return {
83+
sha,
84+
shortSha: sha.slice(0, 7),
85+
message: `commit ${shaSeed}`,
86+
author: "dev@example.com",
87+
committedAtMs,
88+
};
89+
}
90+
7591
function seedCompartmentWithFts(
7692
db: NonNullable<ReturnType<typeof openDatabase>>,
7793
sessionId: string,
@@ -151,6 +167,92 @@ describe("project embedding registry", () => {
151167
tempDirs.length = 0;
152168
});
153169

170+
it("drainCommitBacklogForProject embeds pre-indexed commits with no new git log work", async () => {
171+
_setTestProviderFactoryForProject(
172+
(config) =>
173+
new FakeEmbeddingProvider(config.provider === "local" ? config.model : "off"),
174+
);
175+
const db = useTempDb();
176+
const projectIdentity = "git:commit-backlog";
177+
upsertCommits(db, projectIdentity, [
178+
makeGitCommit("backlog-a", 1000),
179+
makeGitCommit("backlog-b", 2000),
180+
]);
181+
registerProjectEmbeddingAndMaybeWipe(
182+
db,
183+
projectIdentity,
184+
localConfig("model-commits"),
185+
{ memoryEnabled: true, gitCommitEnabled: true },
186+
"/tmp/commits",
187+
);
188+
189+
expect(countEmbeddedCommits(db, projectIdentity)).toBe(0);
190+
const drained = await drainCommitBacklogForProject(
191+
db,
192+
projectIdentity,
193+
Date.now() + 60_000,
194+
);
195+
expect(drained).toBe(2);
196+
expect(countEmbeddedCommits(db, projectIdentity)).toBe(2);
197+
});
198+
199+
it("drainCommitBacklogForProject skips when git commit indexing is disabled", async () => {
200+
_setTestProviderFactoryForProject(
201+
(config) =>
202+
new FakeEmbeddingProvider(config.provider === "local" ? config.model : "off"),
203+
);
204+
const db = useTempDb();
205+
const projectIdentity = "git:commits-off";
206+
upsertCommits(db, projectIdentity, [makeGitCommit("off-a", 1000)]);
207+
registerProjectEmbeddingAndMaybeWipe(
208+
db,
209+
projectIdentity,
210+
localConfig("model-commits"),
211+
{ memoryEnabled: true, gitCommitEnabled: false },
212+
"/tmp/commits-off",
213+
);
214+
215+
const drained = await drainCommitBacklogForProject(
216+
db,
217+
projectIdentity,
218+
Date.now() + 60_000,
219+
);
220+
expect(drained).toBe(0);
221+
expect(countEmbeddedCommits(db, projectIdentity)).toBe(0);
222+
});
223+
224+
it("drainCommitBacklogForProject short-circuits when the git sweep lease is held", async () => {
225+
_setTestProviderFactoryForProject(
226+
(config) =>
227+
new FakeEmbeddingProvider(config.provider === "local" ? config.model : "off"),
228+
);
229+
const db = useTempDb();
230+
const projectIdentity = "git:lease-block";
231+
upsertCommits(db, projectIdentity, [makeGitCommit("lease-a", 1000)]);
232+
registerProjectEmbeddingAndMaybeWipe(
233+
db,
234+
projectIdentity,
235+
localConfig("model-commits"),
236+
{ memoryEnabled: true, gitCommitEnabled: true },
237+
"/tmp/lease",
238+
);
239+
240+
const holder = acquireGitSweepLease(db, projectIdentity, "other-holder");
241+
expect(holder.acquired).toBe(true);
242+
243+
const drained = await drainCommitBacklogForProject(
244+
db,
245+
projectIdentity,
246+
Date.now() + 60_000,
247+
);
248+
expect(drained).toBe(0);
249+
expect(countEmbeddedCommits(db, projectIdentity)).toBe(0);
250+
251+
if (holder.acquired) {
252+
releaseGitSweepLease(db, projectIdentity, holder.holderId);
253+
}
254+
});
255+
154256
it("keeps independent snapshots and providers for two projects in one process", async () => {
155257
_setTestProviderFactoryForProject(
156258
(config) =>

packages/plugin/src/features/magic-context/project-embedding-registry.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -635,17 +635,18 @@ async function embedCommitBatch(
635635
/**
636636
* Drain a project's unembedded-commit backlog, coordinated across processes.
637637
*
638-
* This is the ONLY path that drains pure backlogs (the dream-timer git-sweep
639-
* only embeds when `git log` finds NEW commits, so a repo indexed before
640-
* embeddings were enabled never drains there). Every plugin process runs this
638+
* Drains pure backlogs (indexed commits with no embedding row). The dream-timer
639+
* git-sweep embeds new commits from `git log` but skips backlog drain when
640+
* `embedded=0`; this path runs after each sweep (ignoreCooldown lease) so
641+
* pre-existing backlogs clear. Every plugin process runs this
641642
* on its dream-timer tick, so without coordination N processes hammer the
642643
* embedding provider with the same commits. We take the shared git-sweep lease
643644
* (mutual exclusion) per identity — but with `ignoreCooldown`, because a
644645
* backlog must keep draining every tick until empty and must not be blocked by
645646
* the cooldown the dream-timer sweep advances. We release without marking
646647
* success so the two paths' cooldown tracking stays independent.
647648
*/
648-
async function drainCommitBacklogForProject(
649+
export async function drainCommitBacklogForProject(
649650
db: Database,
650651
projectIdentity: string,
651652
deadline: number,

packages/plugin/src/plugin/dream-timer.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,13 @@ describe("dream-timer null-DB guards (static)", () => {
7171
expect(source).not.toContain("db: Database = openDatabase()");
7272
});
7373
});
74+
75+
describe("dream-timer git commit backlog drain (static)", () => {
76+
const source = readFileSync(join(import.meta.dir, "dream-timer.ts"), "utf8");
77+
78+
test("sweepGitCommits invokes coordinated backlog drain after the index sweep", () => {
79+
expect(source).toContain("drainCommitBacklogForProject");
80+
expect(source).toContain("memorySnapshot?.gitCommitEnabled");
81+
expect(source).toContain("backlogDrained");
82+
});
83+
});

packages/plugin/src/plugin/dream-timer.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
embedUnembeddedMemoriesForProject,
1414
getProjectEmbeddingSnapshot,
1515
} from "../features/magic-context/memory/embedding";
16+
import { drainCommitBacklogForProject } from "../features/magic-context/project-embedding-registry";
1617
import { openDatabase, runSqliteOptimize } from "../features/magic-context/storage";
1718
import { log } from "../shared/logger";
1819
import { resolveFallbackChain } from "../shared/resolve-fallbacks";
@@ -21,6 +22,8 @@ import type { PluginContext } from "./types";
2122

2223
/** Check interval for dream schedule (15 minutes). */
2324
const DREAM_TIMER_INTERVAL_MS = 15 * 60 * 1000;
25+
/** Wall-clock budget for post-sweep commit backlog drain (matches indexer embed sweep). */
26+
const GIT_COMMIT_BACKLOG_DRAIN_MAX_MS = 5 * 60 * 1000;
2427

2528
/**
2629
* Per-project work registered with the timer. The timer is a process-wide
@@ -313,7 +316,7 @@ async function sweepGitCommits(args: {
313316
sinceDays: gitCommitIndexing.since_days,
314317
maxCommits: gitCommitIndexing.max_commits,
315318
});
316-
// Drain any remaining embedding backlog (indexer caps per run).
319+
// Drain any remaining embedding backlog from this sweep (indexer caps per run).
317320
let drainedEmbeddings = 0;
318321
if (result.embedded > 0) {
319322
drainedEmbeddings = await embedUnembeddedCommits(db, projectIdentity);
@@ -325,9 +328,26 @@ async function sweepGitCommits(args: {
325328
`[git-commits] sweep finished for ${projectIdentity}, but lease was no longer active; cooldown not advanced`,
326329
);
327330
}
331+
332+
const memorySnapshot = getProjectEmbeddingSnapshot(projectIdentity);
333+
let backlogDrained = 0;
334+
if (memorySnapshot?.gitCommitEnabled) {
335+
try {
336+
backlogDrained = await drainCommitBacklogForProject(
337+
db,
338+
projectIdentity,
339+
Date.now() + GIT_COMMIT_BACKLOG_DRAIN_MAX_MS,
340+
);
341+
} catch (error) {
342+
log(
343+
`[git-commits] commit backlog drain failed for ${projectIdentity}: ${error instanceof Error ? error.message : String(error)}`,
344+
);
345+
}
346+
}
347+
328348
const elapsedMs = Date.now() - startedAt;
329349
log(
330-
`[git-commits] sweep finished for ${projectIdentity} in ${elapsedMs}ms: scanned=${result.scanned} inserted=${result.inserted} updated=${result.updated} evicted=${result.evicted} embedded=${result.embedded} drained=${drainedEmbeddings}`,
350+
`[git-commits] sweep finished for ${projectIdentity} in ${elapsedMs}ms: scanned=${result.scanned} inserted=${result.inserted} updated=${result.updated} evicted=${result.evicted} embedded=${result.embedded} drained=${drainedEmbeddings} backlogDrained=${backlogDrained}`,
331351
);
332352
} catch (error) {
333353
releaseGitSweepLease(db, projectIdentity, holderId);

0 commit comments

Comments
 (0)