From 12775d56da8114d023843ef229fae48f7fb67b32 Mon Sep 17 00:00:00 2001 From: kami Date: Tue, 21 Jul 2026 18:42:47 +0400 Subject: [PATCH] refactor(acr): ACR Store 1 as a disposable memo, not a durable side-store (#305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shipped Store 1 persisted repo-file descriptors in a new SqliteObservationStore — a second, unsynchronized SQLite writer on the event-log DB (no WAL/busy_timeout, one shared Connection across coroutines) holding a fact the log already carries. That violates "the event log is the only source of truth" (inv #1/#8): an Observation is just FileWrittenEvent.path + postImageHash + describe(bytes).render(), and render is a pure function of content-addressed CAS bytes. Replace the whole durable apparatus with an in-process memo (descriptorMemo, keyed on repoRoot+path+contentHash) on the orchestrator, next to the existing artifactContentCache. Rebuilt from the log on restart, disposable, no external store. Preserves the perf win (skip CAS read + regex on repeat describes) with none of the concurrency/lock risk. Deletes: ObservationStore/InMemoryObservationStore/SqliteObservationStore (+ tests), InfrastructureModule.createObservationStore, the FileReadTool priming path (which never hit anyway — it keyed on a workspace-relative path while every consumer looks up the absolute FileWrittenEvent.path), the FileReadConfig/SessionOrchestrator plumbing, and five orphaned build.gradle deps. Store 2 (soft-confidence hints) unchanged — it was correct. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S --- .../kotlin/com/correx/apps/server/Main.kt | 9 +-- .../context/observation/ObservationStore.kt | 36 --------- .../InMemoryObservationStoreTest.kt | 43 ---------- .../DefaultSessionOrchestrator.kt | 4 +- .../orchestration/SessionOrchestrator.kt | 7 +- .../SessionOrchestratorObservation.kt | Bin 1345 -> 1174 bytes infrastructure/build.gradle | 1 - infrastructure/persistence/build.gradle | 1 - .../observation/SqliteObservationStore.kt | 76 ------------------ .../observation/SqliteObservationStoreTest.kt | 37 --------- .../infrastructure/InfrastructureModule.kt | 8 -- infrastructure/tools/build.gradle | 1 - infrastructure/tools/filesystem/build.gradle | 2 - .../tools/filesystem/FileReadTool.kt | 26 +----- .../tools/filesystem/FileReadToolTest.kt | 19 ----- .../correx/infrastructure/tools/ToolConfig.kt | 6 -- 16 files changed, 9 insertions(+), 267 deletions(-) delete mode 100644 core/context/src/main/kotlin/com/correx/core/context/observation/ObservationStore.kt delete mode 100644 core/context/src/test/kotlin/com/correx/core/context/observation/InMemoryObservationStoreTest.kt delete mode 100644 infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/observation/SqliteObservationStore.kt delete mode 100644 infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/observation/SqliteObservationStoreTest.kt diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt index ca648ca8..6f04420a 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt @@ -344,12 +344,9 @@ fun main() { ), ) - val observationStore = InfrastructureModule.createObservationStore() val wsToolRegistryProvider = WorkspaceToolRegistryProvider { workspace -> val wsRegistry = InfrastructureModule.createToolRegistry( - buildToolConfigForWorkspace( - workspace, shellAllowedExecutables, toolsConfig, researchToolConfig, observationStore, - ), + buildToolConfigForWorkspace(workspace, shellAllowedExecutables, toolsConfig, researchToolConfig), extraTools = extraTools, ) val wsExecutor = DispatchingToolExecutor(wsRegistry) @@ -499,7 +496,6 @@ fun main() { compactionService = journalCompactionService, artifactKindRegistry = artifactKindRegistry, repoKnowledgeRetriever = repoKnowledgeRetriever, - observationStore = observationStore, readyTaskCounter = com.correx.apps.server.tasks.ProjectReadyTaskCounter(taskService), taskClaimCoordinator = com.correx.apps.server.tasks.DefaultTaskClaimCoordinator( taskService, @@ -965,7 +961,6 @@ private fun buildToolConfigForWorkspace( shellAllowedExecutables: Set, toolsConfig: com.correx.core.config.ToolsConfig, research: com.correx.infrastructure.tools.ResearchToolConfig, - observationStore: com.correx.core.context.observation.ObservationStore? = null, ): ToolConfig = ToolConfig( research = research, shell = ShellConfig( @@ -977,8 +972,6 @@ private fun buildToolConfigForWorkspace( enabled = toolsConfig.fileReadEnabled, allowedPaths = workspace.allowedPaths, workingDir = workspace.workingDir, - observationStore = observationStore, - repoRoot = workspace.workingDir?.toString(), ), fileWrite = FileWriteConfig( enabled = toolsConfig.fileWriteEnabled, diff --git a/core/context/src/main/kotlin/com/correx/core/context/observation/ObservationStore.kt b/core/context/src/main/kotlin/com/correx/core/context/observation/ObservationStore.kt deleted file mode 100644 index 97716141..00000000 --- a/core/context/src/main/kotlin/com/correx/core/context/observation/ObservationStore.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.correx.core.context.observation - -import java.util.concurrent.ConcurrentHashMap - -/** - * ACR Store 1 (docs/plans/2026-07-21-acr-knowledge-accretion.md): a fact about a repo file, - * keyed on (repoRoot, path) — task-agnostic, any future run benefits. [contentHash] is the - * staleness check: a caller re-validates by hash before trusting [descriptorRender], per - * invariant #9 (never re-derive from a re-observed environment when the prior observation still - * holds). [descriptorRender] is [com.correx.core.sourcedesc.SourceDescriptor.render] output — the - * only thing every current call site actually consumes, so that's what's stored, not the object. - */ -data class Observation( - val repoRoot: String, - val path: String, - val contentHash: String, - val descriptorRender: String?, - val observedAtMs: Long, -) - -interface ObservationStore { - suspend fun get(repoRoot: String, path: String): Observation? - suspend fun put(observation: Observation) -} - -/** Default/test backend — process-lifetime only. Production wiring swaps in a durable store. */ -class InMemoryObservationStore : ObservationStore { - private val entries = ConcurrentHashMap, Observation>() - - override suspend fun get(repoRoot: String, path: String): Observation? = - entries[repoRoot to path] - - override suspend fun put(observation: Observation) { - entries[observation.repoRoot to observation.path] = observation - } -} diff --git a/core/context/src/test/kotlin/com/correx/core/context/observation/InMemoryObservationStoreTest.kt b/core/context/src/test/kotlin/com/correx/core/context/observation/InMemoryObservationStoreTest.kt deleted file mode 100644 index 960f2089..00000000 --- a/core/context/src/test/kotlin/com/correx/core/context/observation/InMemoryObservationStoreTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.correx.core.context.observation - -import kotlinx.coroutines.runBlocking -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class InMemoryObservationStoreTest { - - @Test - fun `put then get returns the stored observation`(): Unit = runBlocking { - val store = InMemoryObservationStore() - val obs = Observation("repoA", "src/Foo.kt", "hash1", "module=foo", 100L) - - store.put(obs) - - assertEquals(obs, store.get("repoA", "src/Foo.kt")) - } - - @Test - fun `same path in different repos does not collide`(): Unit = runBlocking { - val store = InMemoryObservationStore() - store.put(Observation("repoA", "src/Foo.kt", "hashA", "a", 1L)) - store.put(Observation("repoB", "src/Foo.kt", "hashB", "b", 2L)) - - assertEquals("hashA", store.get("repoA", "src/Foo.kt")?.contentHash) - assertEquals("hashB", store.get("repoB", "src/Foo.kt")?.contentHash) - } - - @Test - fun `put overwrites the prior observation for the same key`(): Unit = runBlocking { - val store = InMemoryObservationStore() - store.put(Observation("repoA", "src/Foo.kt", "hash1", "old", 1L)) - store.put(Observation("repoA", "src/Foo.kt", "hash2", "new", 2L)) - - assertEquals("hash2", store.get("repoA", "src/Foo.kt")?.contentHash) - } - - @Test - fun `unknown key returns null`(): Unit = runBlocking { - assertNull(InMemoryObservationStore().get("repoA", "nope")) - } -} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt index 3d871040..15f82d2d 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt @@ -7,7 +7,6 @@ import com.correx.core.inference.Tokenizer import com.correx.core.journal.DefaultDecisionJournalRepository import com.correx.core.artifacts.kind.ArtifactKindRegistry import com.correx.core.artifactstore.ArtifactStore -import com.correx.core.context.observation.ObservationStore import com.correx.core.events.events.ApprovalDecisionResolvedEvent import com.correx.core.events.events.ArtifactContentStoredEvent import com.correx.core.events.events.ClarificationAnswer @@ -112,8 +111,7 @@ class DefaultSessionOrchestrator( readyTaskCounter: ReadyTaskCounter? = null, taskClaimCoordinator: TaskClaimCoordinator? = null, tuning: OrchestrationTuning = OrchestrationTuning(), - observationStore: ObservationStore? = null, -) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry, repoKnowledgeRetriever = repoKnowledgeRetriever, readyTaskCounter = readyTaskCounter, taskClaimCoordinator = taskClaimCoordinator, tuning = tuning, observationStore = observationStore), ApprovalGateway { +) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry, repoKnowledgeRetriever = repoKnowledgeRetriever, readyTaskCounter = readyTaskCounter, taskClaimCoordinator = taskClaimCoordinator, tuning = tuning), ApprovalGateway { override val tokenizer: Tokenizer? = tokenizer override val cancellations: ConcurrentHashMap = ConcurrentHashMap() diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index 4da4b50d..f6ec2785 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt @@ -8,7 +8,6 @@ import com.correx.core.artifacts.kind.KindInference import com.correx.core.artifactstore.ArtifactStore import com.correx.core.context.builder.ContextPackBuilder import com.correx.core.context.model.ContextPack -import com.correx.core.context.observation.ObservationStore import com.correx.core.events.events.ClarificationAnswer import com.correx.core.events.events.ClarificationAnsweredEvent import com.correx.core.events.events.ClarificationRequestedEvent @@ -203,7 +202,6 @@ abstract class SessionOrchestrator( internal val readyTaskCounter: ReadyTaskCounter? = null, internal val taskClaimCoordinator: TaskClaimCoordinator? = null, internal val tuning: OrchestrationTuning = OrchestrationTuning(), - internal val observationStore: ObservationStore? = null, ) { internal val log = LoggerFactory.getLogger(this::class.java) internal val eventStore: EventStore = repositories.eventStore @@ -250,6 +248,11 @@ abstract class SessionOrchestrator( */ internal val artifactContentCache: ConcurrentHashMap = ConcurrentHashMap() + // ACR Store 1: in-process memo of describe().render(), keyed on (repoRoot, path, contentHash). + // Disposable — the source of truth is FileWrittenEvent + CAS; this only skips recomputing a pure + // function of content-addressed bytes. Empty string = a computed "no descriptor" (negative cache). + internal val descriptorMemo: ConcurrentHashMap = ConcurrentHashMap() + /** Drops a terminated session's cached artifact contents (the heaviest per-session state — full * file/JSON payloads). Safe: rehydrateArtifactContentCache rebuilds it from durable events if the * session is ever resumed. Called on WorkflowCompleted/WorkflowFailed. */ diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorObservation.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorObservation.kt index 800512c3dfe6e3897bd8a9ed0e7f2be68f548e26..8c53da285400758f2846e336c988f7710f50621f 100644 GIT binary patch delta 652 zcmZuv&2AGh5H289B2O_&b4Va8;)ak=g;3zc1zI6g_28^0vDICVHJ*(UN))cWKv59S z!7K18eUgs7R9x_7EZg7DeB(cZUk|TmKfY`aTWraH-XGq3yQ|>DAutAu3_gK@*fGMQ z3$=I(KnoCu19${A1;HRsjJ1p@p@sQIFijy*egb30NbiYc3ekw0f-}iY-~u<9H;I`y z_(J)e7f2UACYY1#T*1D6^&v|z8Ve@aa>lsCHQ-v<6f3>cY(4W?pCYBq2{&JV7J%u* z5qmM>SK5K1pwV3dA2Rt_O>I>ZGp=*N3Rf6*l1kO`#y9v;;o9U(Z{!>vgZe6dRFF{v zNWxt$+PWrVvCUQ;GUg1jL?CfKEc<>{Lf>_gyv5=#>!~GL!q|tEVQ+$*COCTb^yvAd zf>|47qd9SAs7>wExq)FIRZ2DcD8$1)rPMW=A*rW`MxgaNjuP<(6lUW7>i)M+Irx__ z9NmrOV3fylFv??59$o)<-)X963OGNm8WgxxX6ZP9m&z}CW0w%RrhzSN_rD)}KmHF^ W)%t&gwjwU5@uUd-WxKl_YzMc5A>|eT literal 1345 zcmZ`(%Wm5+5WMRvwwF{&Ejr1e1qv938=z|{J1Ld+s?~%QZ^PvJ0j9*=!)45wS5Adt zW26nktBb48<;9os>avu|mrLu`2CD_~oAMaN5v!_zbKFi<@MOO z2j^%2jikB&X*|km3rc#gF(e@1hQ(l|Sy@X^%TU9gU%w$NbrWFbsAd?ZhGH;q*Ks4} z_qf0f$FplHF@x{Y7!G2qfgJOjd*pNn&EOLUvF%l?jl2%6p~Ts88X&zw0e7x+P2w-j zT5dy^WdN3K^t*WMHLwmIrx*eYbH5)U=Z_~ODg6XGz*>8(1g9Qmn%nhO8VGG@*hHAO z7XFv0_xoD%fL3`1{PenhWf#4+i$U6r8lUb&F%LUMzZ}C$3V4p?(-@47JxqM}0F~;( zs6&p$0x9q7?|e;IJ%@aU{V)RC8Utr%>~v(!H!ZpQBbqxJC6f#8f?98TNY`h>bRN=7 z?+J7x&VxGwxw|@vVPbN)OnGd!C{kF|Xeiih!uT=+0i+H;5pCqgBiP6Pi| zDOHG{0IW`d-JQg0LbriI(`_K+4`0h4K1)A&&Tq9bI^@M6Y`5$r+?mshpS@;p7G1l< diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index ca8ad2b1..98e87cb9 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -12,7 +12,6 @@ dependencies { implementation project(":core:approvals") implementation project(":core:sessions") implementation project(":core:config") - implementation project(":core:context") implementation project(":infrastructure:inference") implementation project(":infrastructure:inference:commons") implementation project(":infrastructure:inference:llama_cpp") diff --git a/infrastructure/persistence/build.gradle b/infrastructure/persistence/build.gradle index 509a33ad..49508f08 100644 --- a/infrastructure/persistence/build.gradle +++ b/infrastructure/persistence/build.gradle @@ -10,7 +10,6 @@ dependencies { implementation(project(":core:sessions")) implementation(project(":core:artifacts")) implementation(project(":core:artifacts-store")) - implementation(project(":core:context")) implementation "org.xerial:sqlite-jdbc" implementation "org.slf4j:slf4j-api:2.0.16" testImplementation(testFixtures(project(":testing:contracts"))) diff --git a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/observation/SqliteObservationStore.kt b/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/observation/SqliteObservationStore.kt deleted file mode 100644 index 5aea3883..00000000 --- a/infrastructure/persistence/src/main/kotlin/com/correx/infrastructure/persistence/observation/SqliteObservationStore.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.correx.infrastructure.persistence.observation - -import com.correx.core.context.observation.Observation -import com.correx.core.context.observation.ObservationStore -import com.correx.infrastructure.persistence.util.JDBCHelper.transaction -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.sql.Connection - -/** - * Durable backend for ACR Store 1 (docs/plans/2026-07-21-acr-knowledge-accretion.md), so - * observations survive a server restart the way the event log itself does. One row per - * (repo_root, path); a `put` overwrites the prior observation for that key outright — this store - * only ever holds the latest fact, staleness is resolved by the caller comparing content_hash. - */ -class SqliteObservationStore(private val connection: Connection) : ObservationStore { - - init { - connection.createStatement().use { stmt -> - stmt.execute( - """ - CREATE TABLE IF NOT EXISTS observations ( - repo_root TEXT NOT NULL, - path TEXT NOT NULL, - content_hash TEXT NOT NULL, - descriptor_render TEXT, - observed_at_ms INTEGER NOT NULL, - PRIMARY KEY (repo_root, path) - ) - """.trimIndent(), - ) - } - } - - override suspend fun get(repoRoot: String, path: String): Observation? = withContext(Dispatchers.IO) { - connection.prepareStatement( - "SELECT content_hash, descriptor_render, observed_at_ms FROM observations " + - "WHERE repo_root = ? AND path = ?", - ).use { stmt -> - stmt.setString(1, repoRoot) - stmt.setString(2, path) - stmt.executeQuery().use { rs -> - if (!rs.next()) return@withContext null - Observation( - repoRoot = repoRoot, - path = path, - contentHash = rs.getString("content_hash"), - descriptorRender = rs.getString("descriptor_render"), - observedAtMs = rs.getLong("observed_at_ms"), - ) - } - } - } - - @Suppress("MagicNumber") // JDBC positional parameter indices, not data - override suspend fun put(observation: Observation) = withContext(Dispatchers.IO) { - connection.transaction { - connection.prepareStatement( - "INSERT INTO observations (repo_root, path, content_hash, descriptor_render, observed_at_ms) " + - "VALUES (?, ?, ?, ?, ?) " + - "ON CONFLICT(repo_root, path) DO UPDATE SET " + - "content_hash = excluded.content_hash, " + - "descriptor_render = excluded.descriptor_render, " + - "observed_at_ms = excluded.observed_at_ms", - ).use { stmt -> - stmt.setString(1, observation.repoRoot) - stmt.setString(2, observation.path) - stmt.setString(3, observation.contentHash) - stmt.setString(4, observation.descriptorRender) - stmt.setLong(5, observation.observedAtMs) - stmt.executeUpdate() - } - } - Unit - } -} diff --git a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/observation/SqliteObservationStoreTest.kt b/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/observation/SqliteObservationStoreTest.kt deleted file mode 100644 index 3490344d..00000000 --- a/infrastructure/persistence/src/test/kotlin/com/correx/infrastructure/persistence/observation/SqliteObservationStoreTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.correx.infrastructure.persistence.observation - -import com.correx.core.context.observation.Observation -import kotlinx.coroutines.runBlocking -import java.sql.DriverManager -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class SqliteObservationStoreTest { - - private fun store() = SqliteObservationStore(DriverManager.getConnection("jdbc:sqlite::memory:")) - - @Test - fun `put then get round-trips through sqlite`(): Unit = runBlocking { - val store = store() - store.put(Observation("repoA", "src/Foo.kt", "hash1", "module=foo", 100L)) - - val fetched = store.get("repoA", "src/Foo.kt") - - assertEquals(Observation("repoA", "src/Foo.kt", "hash1", "module=foo", 100L), fetched) - } - - @Test - fun `put overwrites the prior row for the same key`(): Unit = runBlocking { - val store = store() - store.put(Observation("repoA", "src/Foo.kt", "hash1", "old", 1L)) - store.put(Observation("repoA", "src/Foo.kt", "hash2", "new", 2L)) - - assertEquals("hash2", store.get("repoA", "src/Foo.kt")?.contentHash) - } - - @Test - fun `unknown key returns null`(): Unit = runBlocking { - assertNull(store().get("repoA", "nope")) - } -} diff --git a/infrastructure/src/main/kotlin/com/correx/infrastructure/InfrastructureModule.kt b/infrastructure/src/main/kotlin/com/correx/infrastructure/InfrastructureModule.kt index 7271e579..1ee130c0 100644 --- a/infrastructure/src/main/kotlin/com/correx/infrastructure/InfrastructureModule.kt +++ b/infrastructure/src/main/kotlin/com/correx/infrastructure/InfrastructureModule.kt @@ -53,10 +53,8 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import com.correx.infrastructure.router.turbovec.TurboVecL3MemoryStore import com.correx.infrastructure.router.turbovec.TurboVecSidecarConfig -import com.correx.core.context.observation.ObservationStore import com.correx.infrastructure.persistence.SqliteEventStore import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository -import com.correx.infrastructure.persistence.observation.SqliteObservationStore import com.correx.infrastructure.tools.DefaultToolRegistry import com.correx.infrastructure.tools.DispatchingToolExecutor import com.correx.infrastructure.tools.SandboxedToolExecutor @@ -108,12 +106,6 @@ object InfrastructureModule { ) } - fun createObservationStore(dbPath: String = defaultDbPath): ObservationStore { - val dir = java.io.File(dbPath).parentFile - if (!dir.exists()) dir.mkdirs() - return SqliteObservationStore(DriverManager.getConnection("jdbc:sqlite:$dbPath")) - } - fun createArtifactStore(rootDir: Path = Paths.get(defaultArtifactsRoot)): ArtifactStore { Files.createDirectories(rootDir) val index = SqliteArtifactIndex(rootDir.resolve("index.sqlite")) diff --git a/infrastructure/tools/build.gradle b/infrastructure/tools/build.gradle index 55540369..c9dc23c3 100644 --- a/infrastructure/tools/build.gradle +++ b/infrastructure/tools/build.gradle @@ -17,7 +17,6 @@ dependencies { implementation project(":infrastructure:tools:filesystem") implementation project(":core:artifacts") implementation project(":core:artifacts-store") - implementation project(":core:context") // Web research tools (web_search, web_fetch) + deterministic HTML→markdown extraction. implementation 'org.jsoup:jsoup:1.20.1' diff --git a/infrastructure/tools/filesystem/build.gradle b/infrastructure/tools/filesystem/build.gradle index 8a04f7da..5190029e 100644 --- a/infrastructure/tools/filesystem/build.gradle +++ b/infrastructure/tools/filesystem/build.gradle @@ -10,8 +10,6 @@ dependencies { implementation project(':core:approvals') implementation project(':core:artifacts') implementation project(':core:artifacts-store') - implementation project(':core:context') - implementation project(':core:sourcedesc') implementation 'org.slf4j:slf4j-api:2.0.16' } diff --git a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt index 9aa4f2e1..9c5c6277 100644 --- a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt +++ b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt @@ -1,10 +1,7 @@ package com.correx.infrastructure.tools.filesystem import com.correx.core.approvals.Tier -import com.correx.core.context.observation.Observation -import com.correx.core.context.observation.ObservationStore import com.correx.core.events.events.ToolRequest -import com.correx.core.sourcedesc.describe import com.correx.core.tools.compression.CompressionRule.StripBlankLines import com.correx.core.tools.compression.CompressionRule.StripLeadingWhitespace import com.correx.core.tools.compression.DeclarativeCompressor @@ -35,12 +32,6 @@ import kotlin.io.path.name class FileReadTool( private val allowedPaths: Set = emptySet(), private val workingDir: Path? = null, - // ACR Store 1 (docs/plans/2026-07-21-acr-knowledge-accretion.md): primes the cross-session - // observation cache on a whole-file read, not just on write, so a later describeCached lookup - // (SessionOrchestratorArtifacts.kt) for this exact content hash is already warm. Both null by - // default — no-op unless a workspace wiring supplies them. - private val observationStore: ObservationStore? = null, - private val repoRoot: String? = null, ) : Tool, ToolExecutor { // Resolve relative paths against the bound workspace's working dir, NOT the JVM @@ -149,7 +140,7 @@ class FileReadTool( } } - private suspend fun readFile(path: Path, startLine: Int?, endLine: Int?, request: ToolRequest): ToolResult = runCatching { + private fun readFile(path: Path, startLine: Int?, endLine: Int?, request: ToolRequest): ToolResult = runCatching { // Read the file once. Lines and the (whole-read) content hash both derive from these bytes, // so a full read no longer hits the disk twice (readAllLines + readAllBytes for the hash). val bytes = Files.readAllBytes(path) @@ -178,9 +169,7 @@ class FileReadTool( // against what the agent actually saw. A partial or truncated view establishes no baseline — // the agent must read the relevant range before editing. val sawWholeFile = startLine == null && endLine == null && !lineCapped && !charCapped - val hash = if (sawWholeFile) sha256(bytes) else null - val metadata = hash?.let { mapOf("contentHash" to it) } ?: emptyMap() - hash?.let { primeObservation(path, bytes, it) } + val metadata = if (sawWholeFile) mapOf("contentHash" to sha256(bytes)) else emptyMap() ToolResult.Success( invocationId = request.invocationId, output = content + note, @@ -194,17 +183,6 @@ class FileReadTool( ) } - /** No-op unless both [observationStore] and [repoRoot] are wired; skips if already cached for this hash. */ - private suspend fun primeObservation(path: Path, bytes: ByteArray, hash: String) { - val store = observationStore ?: return - val root = repoRoot ?: return - val relPath = workingDir?.let { runCatching { it.relativize(path).toString() }.getOrNull() } ?: path.toString() - val cached = store.get(root, relPath) - if (cached?.contentHash == hash) return - val rendered = describe(relPath, bytes).render().takeIf { it.isNotBlank() } - store.put(Observation(root, relPath, hash, rendered, System.currentTimeMillis())) - } - private fun sha256(bytes: ByteArray): String = java.security.MessageDigest.getInstance("SHA-256").digest(bytes) .joinToString("") { "%02x".format(it) } diff --git a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileReadToolTest.kt b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileReadToolTest.kt index e5ef6dd6..1110784e 100644 --- a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileReadToolTest.kt +++ b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/FileReadToolTest.kt @@ -1,6 +1,5 @@ package com.correx.infrastructure.tools.filesystem -import com.correx.core.context.observation.InMemoryObservationStore import com.correx.core.events.events.ToolRequest import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId @@ -194,24 +193,6 @@ class FileReadToolTest { assertTrue((result as ToolResult.Success).metadata["contentHash"] != null) } - @Test - fun `execute primes the observation store for a whole-file read`(): Unit = runBlocking { - val tempFile = Files.createTempFile("test_prime", ".kt") - Files.writeString(tempFile, "fun foo() {}") - val store = InMemoryObservationStore() - val tool = FileReadTool( - allowedPaths = setOf(tempFile.parent), - workingDir = tempFile.parent, - observationStore = store, - repoRoot = "repoA", - ) - - tool.execute(createRequest(tempFile.toString())) - - val observed = store.get("repoA", tempFile.fileName.toString()) - assertTrue(observed != null) - } - @Test fun `outputCompressor strips blank lines and leading whitespace`() { val tool = FileReadTool() diff --git a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt index 313920ed..d1a53945 100644 --- a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt +++ b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt @@ -1,6 +1,5 @@ package com.correx.infrastructure.tools -import com.correx.core.context.observation.ObservationStore import com.correx.core.tools.contract.Tool import com.correx.infrastructure.tools.filesystem.FileDeleteTool import com.correx.infrastructure.tools.filesystem.FileEditTool @@ -40,9 +39,6 @@ data class FileReadConfig( val enabled: Boolean = false, val allowedPaths: Set = emptySet(), val workingDir: Path? = null, - // ACR Store 1 hot-path priming (docs/plans/2026-07-21-acr-knowledge-accretion.md); both null is a no-op. - val observationStore: ObservationStore? = null, - val repoRoot: String? = null, ) data class FileWriteConfig( val enabled: Boolean = false, @@ -79,8 +75,6 @@ fun ToolConfig.buildTools(): List = buildList { FileReadTool( allowedPaths = fileRead.allowedPaths, workingDir = fileRead.workingDir, - observationStore = fileRead.observationStore, - repoRoot = fileRead.repoRoot, ), ) // list_dir is read-only enumeration; shares the reader's jail + anchor and the same toggle.