feat(toolintent): record the session's active task as a session-local fact

Claiming a task now emits SessionWorkingTaskEvent{taskId, affectedPaths} into the
session's own stream (via a SessionFactRecorder port + event-store adapter), and
SessionContextProjection folds it into SessionContext.activeTask. So a gate learns
"which task is this session working, and its scope" by reading the session stream
— no cross-stream scan, no kernel->tasks dependency. Re-emitted on an affected_paths
edit by the claimant, so the snapshot scope stays current; latest wins on replay.

This is the data source the write-scope gate needs (next).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 20:35:40 +00:00
parent 47a109e49f
commit 30321e08a5
10 changed files with 131 additions and 2 deletions
@@ -1,5 +1,6 @@
package com.correx.core.toolintent
import com.correx.core.events.events.SessionWorkingTaskEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
@@ -40,6 +41,12 @@ class SessionContextProjection(private val sessionId: SessionId) : Projection<Se
when (val payload = event.payload) {
is ToolInvocationRequestedEvent -> recordPendingRead(state, payload)
is ToolExecutionCompletedEvent -> recordCompletion(state, payload)
is SessionWorkingTaskEvent ->
if (payload.sessionId == sessionId) {
state.copy(activeTask = ActiveTask(payload.taskId.value, payload.affectedPaths))
} else {
state
}
else -> state
}
@@ -6,9 +6,11 @@ import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.SessionWorkingTaskEvent
import com.correx.core.events.events.ToolReceipt
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.EventId
import com.correx.core.events.types.TaskId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
@@ -76,4 +78,16 @@ class SessionContextProjectionTest {
assertEquals(setOf("core/A.kt", "core/B.kt"), ctx.writes)
assertTrue(ctx.reads.isEmpty())
}
@Test
fun `a SessionWorkingTask event sets the active task and the latest wins`() {
val ctx = fold(
listOf(
stored(SessionWorkingTaskEvent(session, TaskId("auth-1"), listOf("core/a/**"))),
stored(SessionWorkingTaskEvent(session, TaskId("auth-2"), listOf("core/auth/**"))),
),
)
assertEquals("auth-2", ctx.activeTask?.taskId)
assertEquals(listOf("core/auth/**"), ctx.activeTask?.scope)
}
}