Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions platform/daemon/src/db-owner-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { activeVectorProjectionTable, findSqliteVecExtension, vectorSearchWithMe
import {
applyObsidianSourceStructureInTx,
applyObsidianSourceStructurePurgeInTx,
isObsidianSourceFileIndexedInTx,
purgeObsidianSourceFileStructureInTx,
} from "./obsidian-source-graph";
import { indexSourceArtifactStructureInTx, purgeSourceArtifactStructureInTx } from "./source-artifact-graph";
Expand Down Expand Up @@ -877,9 +878,19 @@ export function runDbOwnerWorker(): void {
"SELECT source_sha256 FROM memory_artifacts WHERE agent_id = ? AND source_path = ? AND COALESCE(is_deleted, 0) = 0 LIMIT 1",
)
.get(input.agentId, sourcePath) as { source_sha256: string } | null | undefined;
const artifactChanged = existing?.source_sha256 !== input.sourceHash;
const graphCurrent =
input.graph !== undefined &&
!artifactChanged &&
isObsidianSourceFileIndexedInTx(db as unknown as import("./db-accessor").WriteDb, {
agentId: input.agentId,
sourceId: input.graph.sourceId,
filePath: sourcePath,
});
const graphIndexed = input.graph !== undefined && !graphCurrent;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
db.exec("BEGIN IMMEDIATE");
try {
upsertMemoryArtifactInTx(db as unknown as BunDatabase, nativeMemoryArtifactFields(input));
if (artifactChanged) upsertMemoryArtifactInTx(db as unknown as BunDatabase, nativeMemoryArtifactFields(input));
if (checkpoint !== undefined) {
db.prepare(
`UPDATE source_sync_failures
Expand All @@ -888,7 +899,7 @@ export function runDbOwnerWorker(): void {
AND item_path = ? AND resolved_at IS NULL`,
).run(input.agentId, checkpoint.sourceKey, sourcePath);
}
if (input.graph !== undefined) {
if (graphIndexed && input.graph !== undefined) {
applyObsidianSourceStructureInTx(db as unknown as import("./db-accessor").WriteDb, {
agentId: input.agentId,
sourceId: input.graph.sourceId,
Expand Down Expand Up @@ -920,8 +931,8 @@ export function runDbOwnerWorker(): void {
}
commit(context);
return {
artifactChanged: existing?.source_sha256 !== input.sourceHash,
graphIndexed: input.graph !== undefined,
artifactChanged,
graphIndexed,
embeddingProviderUnavailable: embeddingResult.providerUnavailable,
};
} catch (error) {
Expand Down
63 changes: 63 additions & 0 deletions platform/daemon/src/native-memory-sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,69 @@ describe("native memory sources", () => {
expect(target).toEqual({ entity_type: "source_document", source_path: targetFile });
});

it("does not rewrite artifacts, graph edges, or dependency history for unchanged Obsidian notes", async () => {
const root = join(dir, "vault");
mkdirSync(root, { recursive: true });
writeFileSync(join(root, "Source.md"), "# Source\n\nLinks to [[Target]].\n");
writeFileSync(join(root, "Target.md"), "# Target\n\nThe target note stays unchanged.\n");
const source = obsidianNativeMemorySource(root, "Unchanged Vault", "obsidian:unchanged-vault");
const snapshot = () =>
getDbAccessor().withReadDb((db) => ({
artifacts: db
.prepare("SELECT source_path, updated_at FROM memory_artifacts WHERE agent_id = ? ORDER BY source_path")
.all("agent-native"),
dependencies: db
.prepare("SELECT id, updated_at FROM entity_dependencies WHERE agent_id = ? ORDER BY id")
.all("agent-native"),
history: db
.prepare("SELECT COUNT(*) AS n FROM entity_dependency_history WHERE agent_id = ?")
.get("agent-native"),
}));
const sync = async (): Promise<number> => {
const handle = startNativeMemoryBridge([source], {
agentId: "agent-native",
pollIntervalMs: 0,
sourceGraphEnabled: true,
});
try {
return await handle.syncExisting();
} finally {
await handle.close();
}
};

expect(await sync()).toBe(2);
const first = snapshot();
expect(first.dependencies.length).toBeGreaterThan(0);
await new Promise((resolve) => setTimeout(resolve, 1_100));

expect(await sync()).toBe(0);
expect(snapshot()).toEqual(first);

writeFileSync(join(root, "Source.md"), "# Source\n\nStill links to [[Target]], with new prose.\n");
expect(await sync()).toBe(1);
expect(snapshot().artifacts).not.toEqual(first.artifacts);

const targetDocument = () =>
getDbAccessor().withReadDb(
(db) =>
db
.prepare(
"SELECT COUNT(*) AS n FROM entities WHERE agent_id = ? AND entity_type = 'source_document' AND source_path = ?",
)
.get("agent-native", join(root, "Target.md")) as { n: number },
);
getDbAccessor().withWriteTx((db) => {
db.prepare("DELETE FROM entities WHERE agent_id = ? AND entity_type = 'source_document' AND source_path = ?").run(
"agent-native",
join(root, "Target.md"),
);
});
expect(targetDocument()).toEqual({ n: 0 });
expect(await sync()).toBe(1);
expect(targetDocument()).toEqual({ n: 1 });
});

it("can defer stale source cleanup during bridge sync", async () => {
const root = join(dir, "vault");
const file = join(root, "permanent", "Old.md");
Expand Down
38 changes: 38 additions & 0 deletions platform/daemon/src/obsidian-source-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { join } from "node:path";
import { closeDbAccessor, getDbAccessor, initDbAccessor } from "./db-accessor";
import {
buildObsidianMarkdownPathIndex,
isObsidianSourceFileIndexedInTx,
indexObsidianSourceStructure,
purgeObsidianSourceFileStructure,
purgeObsidianSourceStructure,
Expand Down Expand Up @@ -251,6 +252,43 @@ describe("Obsidian source graph structure", () => {
expect(rows.deps.some((row) => row.reason.includes("Old Target"))).toBe(false);
});

it("treats a note as indexed only after its own structure is built, not when another note links to it", () => {
const doc = join(vault, "literature", "Arch-Linux", "indexed.md");
const target = join(vault, "literature", "Arch-Linux", "target.md");
writeFileSync(doc, "# Indexed\n\nSee [[target]].\n");
writeFileSync(target, "# Target\n\nA note another note links to.\n");
const index = (filePath: string) =>
indexObsidianSourceStructure({
agentId: "obsidian-graph-agent",
sourceId: "obsidian:test-vault",
sourceName: "Test Vault",
root: vault,
filePath,
content: readFileSync(filePath, "utf-8"),
markdownPathIndex: buildObsidianMarkdownPathIndex(vault, [doc, target]),
});
const indexed = (sourceId: string, filePath: string): boolean =>
getDbAccessor().withWriteTx((db: import("./db-accessor").WriteDb) =>
isObsidianSourceFileIndexedInTx(db, { agentId: "obsidian-graph-agent", sourceId, filePath }),
);

index(doc);
const linkedTarget = getDbAccessor().withReadDb(
(db) =>
db
.prepare("SELECT entity_type FROM entities WHERE agent_id = ? AND source_path = ?")
.get("obsidian-graph-agent", target) as { entity_type: string } | undefined,
);
expect(linkedTarget?.entity_type).toBe("source_document");
expect(indexed("obsidian:test-vault", target)).toBe(false);
expect(indexed("obsidian:test-vault", doc)).toBe(true);
expect(indexed("obsidian:test-vault", `${vault}/literature/./Arch-Linux/../Arch-Linux//indexed.md`)).toBe(true);
expect(indexed("obsidian:other-vault", doc)).toBe(false);

index(target);
expect(indexed("obsidian:test-vault", target)).toBe(true);
});

it("removes a deleted file's structure, keeps sibling notes, and flags Dreaming claims citing it", () => {
const doc = join(vault, "literature", "Arch-Linux", "removed.md");
const sibling = join(vault, "literature", "Arch-Linux", "sibling.md");
Expand Down
23 changes: 23 additions & 0 deletions platform/daemon/src/obsidian-source-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,29 @@ export function purgeObsidianSourceFileStructureInTx(
return removed;
}

export function isObsidianSourceFileIndexedInTx(
db: WriteDb,
input: { readonly agentId: string; readonly sourceId: string; readonly filePath: string },
): boolean {
const filePath = normalizedPath(input.filePath);
return (
db
.prepare(
`SELECT 1 FROM entity_dependencies d
JOIN entities e ON e.id = d.target_entity_id
WHERE d.agent_id = ?
AND d.source_id = ?
AND d.source_kind = ?
AND d.source_path = ?
AND d.dependency_type = 'contains'
AND e.entity_type = 'source_document'
AND e.source_path = ?
LIMIT 1`,
)
.get(input.agentId, input.sourceId, OBSIDIAN_SOURCE_KIND, filePath, filePath) != null
);
}

function removeObsidianSourceFileStructureInTx(
db: WriteDb,
input: PurgeObsidianSourceFileStructureInput,
Expand Down
34 changes: 17 additions & 17 deletions scripts/event-loop-contract-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -854,100 +854,100 @@
},
{
"path": "db-owner-worker.ts",
"line": 130,
"line": 131,
"api": "readFileSync",
"source": "return readFileSync(cancellationRegistryPath, \"utf8\").includes(`${jobId}\\n`);",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 158,
"line": 159,
"api": "writeFileSync",
"source": "writeFileSync(startupStarted, \"started\\n\");",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 160,
"line": 161,
"api": "existsSync",
"source": "while (!existsSync(startupRelease)) Atomics.wait(signal, 0, 0, 5);",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 165,
"line": 166,
"api": "existsSync",
"source": "if (sqlitePath !== undefined && existsSync(sqlitePath) && typeof Database.setCustomSQLite === \"function\") {",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 203,
"line": 204,
"api": "writeFileSync",
"source": "if (commitMarker !== undefined) writeFileSync(commitMarker, commitPauseMs > 0 ? \"waiting\\n\" : \"started\\n\");",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 211,
"line": 212,
"api": "writeFileSync",
"source": "writeFileSync(commitMarker, \"completed\\n\");",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 218,
"line": 219,
"api": "existsSync",
"source": "existsSync(commitResultGate)",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 220,
"line": 221,
"api": "rmSync",
"source": "rmSync(commitResultGate, { force: true });",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 222,
"line": 223,
"api": "writeFileSync",
"source": "if (commitMarker !== undefined) writeFileSync(commitMarker, \"result-wait\\n\");",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 1009,
"line": 1020,
"api": "withReadDbAsync",
"source": "const embedding = await getDbAccessor().withReadDbAsync(",
"category": "hot-path",
"siteToken": "db:recall.embedding.config.read"
},
{
"path": "db-owner-worker.ts",
"line": 1039,
"line": 1050,
"api": "withReadDbAsync",
"source": "return await getDbAccessor().withReadDbAsync(",
"category": "hot-path",
"siteToken": "db:recall.vector.search.read"
},
{
"path": "db-owner-worker.ts",
"line": 1049,
"line": 1060,
"api": "writeFileSync",
"source": "writeFileSync(startedMarker, \"started\\n\");",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 1050,
"line": 1061,
"api": "existsSync",
"source": "while (!existsSync(releaseMarker)) await new Promise((resolve) => setTimeout(resolve, 5));",
"category": "hot-path"
},
{
"path": "db-owner-worker.ts",
"line": 1188,
"line": 1199,
"api": "writeFileSync",
"source": "if (activeFile) writeFileSync(activeFile, `${Date.now()}\\n`);",
"category": "hot-path"
Expand Down Expand Up @@ -2285,23 +2285,23 @@
},
{
"path": "obsidian-source-graph.ts",
"line": 633,
"line": 656,
"api": "withWriteTx",
"source": "return getDbAccessor().withWriteTx(",
"category": "hot-path",
"siteToken": "db:source-graph.obsidian.index.write"
},
{
"path": "obsidian-source-graph.ts",
"line": 643,
"line": 666,
"api": "withWriteTx",
"source": "return getDbAccessor().withWriteTx(",
"category": "hot-path",
"siteToken": "db:source-graph.obsidian.file-purge.write"
},
{
"path": "obsidian-source-graph.ts",
"line": 721,
"line": 744,
"api": "withWriteTx",
"source": "return getDbAccessor().withWriteTx(",
"category": "hot-path",
Expand Down
Loading