feat(kernel): deterministic per-task claim stage + per-task write scope
A stage flagged claimTask gets the kernel (not the LLM) to claim the next ready task — or re-surface the one already claimed on a review bounce — and inject its TaskContextAssembler bundle as L0. While a task is claimed, plane-2's write manifest narrows to the task's affectedPaths. Project identity is resolved from the origin-session link the task tools already record (TaskService.projectForSession), so the loop predicate and claim stage agree regardless of the free-form project key the agent passed. Claiming flows through a kernel-declared TaskClaimCoordinator implemented in apps/server (no cross-core import).
This commit is contained in:
@@ -368,7 +368,17 @@ fun main() {
|
||||
compactionService = journalCompactionService,
|
||||
artifactKindRegistry = artifactKindRegistry,
|
||||
repoKnowledgeRetriever = repoKnowledgeRetriever,
|
||||
readyTaskCounter = com.correx.apps.server.tasks.ProjectReadyTaskCounter(eventStore, taskService),
|
||||
readyTaskCounter = com.correx.apps.server.tasks.ProjectReadyTaskCounter(taskService),
|
||||
taskClaimCoordinator = com.correx.apps.server.tasks.DefaultTaskClaimCoordinator(
|
||||
taskService,
|
||||
com.correx.core.tasks.TaskContextAssembler(
|
||||
taskService,
|
||||
taskKnowledgeRetriever,
|
||||
taskDocumentResolver,
|
||||
taskArtifactResolver,
|
||||
taskSessionResolver,
|
||||
),
|
||||
),
|
||||
)
|
||||
val workflowRegistry = FileSystemWorkflowRegistry(
|
||||
InfrastructureModule.createWorkflowLoader(configArtifactKindsEarly),
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.correx.apps.server.tasks
|
||||
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.kernel.orchestration.TaskClaimCoordinator
|
||||
import com.correx.core.tasks.TaskContextAssembler
|
||||
import com.correx.core.tasks.TaskService
|
||||
|
||||
/**
|
||||
* [TaskClaimCoordinator] over the task projection: claims the session's next ready task (claimant =
|
||||
* session id) and renders its [TaskContextAssembler] bundle as the implementer's L0 context. The
|
||||
* claim emits a `TaskClaimedEvent`, so the task leaves `ready()` and the `tasks_ready` predicate can
|
||||
* eventually terminate the loop — driven by recorded fact, not agent compliance.
|
||||
*/
|
||||
class DefaultTaskClaimCoordinator(
|
||||
private val taskService: TaskService,
|
||||
private val assembler: TaskContextAssembler,
|
||||
) : TaskClaimCoordinator {
|
||||
|
||||
override suspend fun claimNext(sessionId: SessionId): String? {
|
||||
val active = taskService.activeClaim(sessionId.value)
|
||||
val task = active ?: taskService.nextReadyForSession(sessionId.value)
|
||||
?.also { taskService.claim(it.taskId, sessionId.value) }
|
||||
?: return null
|
||||
return assembler.assemble(task.taskId)?.render()
|
||||
}
|
||||
|
||||
override fun activeScope(sessionId: SessionId): List<String> =
|
||||
taskService.activeClaim(sessionId.value)?.state?.affectedPaths ?: emptyList()
|
||||
}
|
||||
+6
-19
@@ -1,31 +1,18 @@
|
||||
package com.correx.apps.server.tasks
|
||||
|
||||
import com.correx.core.approvals.ProjectIdentity
|
||||
import com.correx.core.events.events.SessionWorkspaceBoundEvent
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.kernel.orchestration.ReadyTaskCounter
|
||||
import com.correx.core.tasks.TaskService
|
||||
|
||||
/**
|
||||
* [ReadyTaskCounter] over the task projection. Resolves the session to its project the same way the
|
||||
* approval gate does (bound workspace root → [ProjectIdentity.of]) and counts the ready tasks there.
|
||||
* Pure read over event-derived state, so it stays replay-safe.
|
||||
*
|
||||
* Caveat for the execution loop: tasks are keyed by whatever `project` string the decomposer passes
|
||||
* to task tools; this counts the project derived from the workspace root. Those must agree for the
|
||||
* loop predicate to fire — Slice 2/4 pins that down.
|
||||
* [ReadyTaskCounter] over the task projection. Resolves the session to the project its tasks were
|
||||
* actually filed under ([TaskService.projectForSession] — via the CONTEXT/SESSION link the task
|
||||
* tools record) and counts the ready tasks there, so the loop predicate agrees with the claim stage
|
||||
* regardless of the free-form `project` key the agent chose. Pure read over event-derived state.
|
||||
*/
|
||||
class ProjectReadyTaskCounter(
|
||||
private val eventStore: EventStore,
|
||||
private val taskService: TaskService,
|
||||
) : ReadyTaskCounter {
|
||||
override fun count(sessionId: SessionId): Int {
|
||||
val workspaceRoot = eventStore.read(sessionId)
|
||||
.mapNotNull { it.payload as? SessionWorkspaceBoundEvent }
|
||||
.lastOrNull()
|
||||
?.workspaceRoot
|
||||
?: return 0
|
||||
return taskService.ready(ProjectIdentity.of(workspaceRoot)).size
|
||||
}
|
||||
override fun count(sessionId: SessionId): Int =
|
||||
taskService.projectForSession(sessionId.value)?.let { taskService.ready(it).size } ?: 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user