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
23 changes: 18 additions & 5 deletions loopx/control_plane/coordination/claim_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {indexCoordinationProjection} from "./coordination_projection.ts";
import {canonicalTaskLease, canonicalTaskLeaseAcquireFacts} from "./task_lease_state.ts";
import {coordinationTodoWriteScopes} from "./todo_write_scopes.ts";
import {leaseEpoch, leaseVersion, leaseIsActive, normalizeTtl, type LeaseRecord} from "../work_items/task_lease_acquire.ts";
import {sameLeaseWorkspace} from "../work_items/task_lease_workspace.ts";
import {sameLeaseWorkspace, type LeaseWorkspace} from "../work_items/task_lease_workspace.ts";
import {leaseWriteRepository} from "../work_items/task_lease_repository.ts";
import {evaluateTaskLeaseAcquireDecision, materializeTaskLeaseAcquire} from "../work_items/task_lease_acquire_decision.ts";
import {decideTaskLeaseLifecycle, materializeTaskLeaseLifecycle} from "../work_items/task_lease_lifecycle_decision.ts";
Expand All @@ -24,6 +24,8 @@ export function claimExecutionReference(id: unknown, key: unknown, version: unkn
type ExecutionInput = {goal_id: string; todo_id: string; actor_agent_id: string | null;
registered_agents: readonly string[]; now: Date};

export type ClaimWorkspaceObserver = (writeScopes: readonly string[]) => Promise<LeaseWorkspace>;

/** Resolve only this accepted execution. No acquisition, adoption of a newer
* generation, or write retry is hidden behind a progress/completion request. */
export async function claimExecutionProof(store: AuthorityStore, head: JsonObject,
Expand Down Expand Up @@ -82,8 +84,9 @@ export function maintainClaimExecution(head: JsonObject, input: ExecutionInput,
/** Prepare execution only after the Todo claim owner accepts the actor (and any
* transfer grant). Expired execution is reclaimable without the old process;
* acquisition still validates scope, eligibility and generation under one CAS. */
export function prepareClaimExecution(head: JsonObject,
input: ExecutionInput & {operation_id: string; claimed_by: string}): {lease: LeaseRecord; changed: boolean} {
export async function prepareClaimExecution(head: JsonObject,
input: ExecutionInput & {operation_id: string; claimed_by: string},
observeWorkspace?: ClaimWorkspaceObserver): Promise<{lease: LeaseRecord; changed: boolean; overlapAdvisories?: JsonObject[]}> {
const index = indexCoordinationProjection(head, input.goal_id);
const facts = canonicalTaskLeaseAcquireFacts(index, input.goal_id, input.todo_id, input.registered_agents, input.now);
const current = facts.current;
Expand All @@ -92,16 +95,26 @@ export function prepareClaimExecution(head: JsonObject,
const renewed = maintainClaimExecution(head, input, current);
return {lease: renewed ?? current, changed: renewed !== null};
}
const todo = index.todos.get(input.todo_id)!;
const writeScopes = coordinationTodoWriteScopes(todo);
// Only declared repository code work can use the code-edit isolation mode.
// Shared-state and historical callers retain their conservative exclusion.
const workspace = observeWorkspace && todo.task_domain === "code" &&
typeof todo.task_repository === "string" && writeScopes.length > 0
? await observeWorkspace(writeScopes) : null;
const command = {owner: input.claimed_by,
idempotency_key: `claim-${canonicalAuthoritySha256({goal_id: input.goal_id, todo_id: input.todo_id,
owner: input.claimed_by, operation_id: input.operation_id})}`,
expected_version: current ? leaseVersion(current) : 0, ttl_seconds: normalizeTtl(null),
write_scopes: coordinationTodoWriteScopes(index.todos.get(input.todo_id)!)};
write_scopes: writeScopes, ...(workspace ? {write_workspace: workspace} : {})};
const decision = evaluateTaskLeaseAcquireDecision({handoff_mode: String(head.handoff_mode),
registered_agents: [...input.registered_agents], ...facts,
// Claim admission has already authorized this assignment. Acquire and the
// Todo owner change commit together; the old assignment is not a new gate.
todo: facts.todo === null ? null : {...facts.todo, claimed_by: input.claimed_by}, command});
if (decision.outcome !== "apply") throw new AuthorityStoreProtocolError(`Task claim execution rejected: ${decision.code}`);
return {lease: materializeTaskLeaseAcquire(input, command, decision, input.now), changed: true};
const overlapAdvisories = (decision.overlap_advisory_indexes ?? []).map(index => ({
todo_id: facts.other_leases[index].todo_id, owner: facts.other_leases[index].owner ?? null,
write_scopes: facts.other_leases[index].write_scopes, reason: "independent_worktree_integration_overlap"}));
return {lease: materializeTaskLeaseAcquire(input, command, decision, input.now), changed: true, overlapAdvisories};
}
7 changes: 7 additions & 0 deletions loopx/control_plane/coordination/local_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from uuid import uuid4

from ...agent_registry import registered_agent_ids_from_registry
from ..agents.workspace_guard import _git_worktree_root
from .authority_source_capture import authority_registry_source
from ..runtime.time import now_local_iso as now_local
from ..effect_runtime import (
Expand Down Expand Up @@ -161,6 +162,11 @@ def claim_canonical_todo_if_promoted(
"""Route a claim to the canonical TypeScript transaction owner."""
with authority_registry_source(registry_path) as registry_source:
registered = registered_agent_ids_from_registry(registry_path, goal_id)
# This is only a candidate location. The lease owner observes physical
# isolation and the current canonical scopes before granting execution.
worktree = _git_worktree_root(Path.cwd()) if manage_execution else None
if worktree is not None and not (worktree / ".git").is_file():
worktree = None
result = effect_runtime_result(
LOCAL_COORDINATION_TODO_CLAIM_METHOD,
{
Expand All @@ -180,6 +186,7 @@ def claim_canonical_todo_if_promoted(
else f"todo-claim:{goal_id}:{todo_id}:{uuid4().hex}"
),
**({"manage_execution": True} if manage_execution else {}),
**({"write_worktree": str(worktree)} if worktree is not None else {}),
"lease_request": (
{
"idempotency_key": task_lease_idempotency_key,
Expand Down
17 changes: 16 additions & 1 deletion loopx/control_plane/coordination/local_authority_runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {reviewedPromotionPlan, promotionPlanDigest, decodeReviewedPromotionOpera
import {registryAuthoritySourceCheck} from "./authority_source.ts";
import {ResearchTerminalEvidenceHost} from "../capabilities/explore_research_terminal.ts";
import {decodeTaskLeaseProof} from "./task_lease_proof.ts";
import {LeaseWorktreeObservationError, observeLeaseWorktree} from "../work_items/task_lease_workspace.ts";
import {evaluateTaskLeaseWriteScopesOverlap} from "../work_items/task_lease_acquire_decision.ts";
import {COORDINATION_TODO_ARCHIVE_RESULT_SCHEMA} from "./todo_archive.ts";
import {readCoordinationOwnership} from "./ownership_observation.ts";
import {executeTodoContinuation} from "./todo_continuation.ts";
Expand Down Expand Up @@ -1035,6 +1037,11 @@ export async function claimLocalCoordinationTodo(
}
if (Object.hasOwn(input, "manage_execution") && input.schema_version !== LOCAL_COORDINATION_TODO_MANAGED_CLAIM_REQUEST_SCHEMA)
throw new TypeError("Managed execution requires claim request v2");
const worktree = input.write_worktree;
if (worktree !== undefined && (input.schema_version !== LOCAL_COORDINATION_TODO_MANAGED_CLAIM_REQUEST_SCHEMA ||
input.manage_execution !== true || typeof worktree !== "string" || !isAbsolute(worktree))) {
throw new TypeError("Worktree observation requires a managed claim and an absolute caller path");
}
const authoritySourcesCurrent = registryAuthoritySourceCheck(input,
input.schema_version !== LOCAL_COORDINATION_TODO_CLAIM_REQUEST_SCHEMA);
if (typeof input.dry_run !== "boolean") {
Expand Down Expand Up @@ -1085,7 +1092,15 @@ export async function claimLocalCoordinationTodo(
...(input.manage_execution === undefined ? {} : {manage_execution: input.manage_execution as boolean}),
dry_run: input.dry_run === true,
now: claimObservedAt(input.observed_at),
}, authoritySourcesCurrent);
}, authoritySourcesCurrent, worktree === undefined ? undefined : async writeScopes => {
try {
return await observeLeaseWorktree(worktree as string, path =>
evaluateTaskLeaseWriteScopesOverlap({left: [...writeScopes], right: [path]}).overlap === true);
} catch (error) {
throw new Error(error instanceof LeaseWorktreeObservationError ? error.message
: "Cannot verify independent Git worktree and origin for task claim");
}
});
return {
...result,
source_authority: sourceAuthority,
Expand Down
11 changes: 8 additions & 3 deletions loopx/control_plane/coordination/todo_claim.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {prepareClaimExecution} from "./claim_execution.ts";
import {prepareClaimExecution, type ClaimWorkspaceObserver} from "./claim_execution.ts";
import {retiredHandoffModeRejection, HANDOFF_MODE_RETIRED_REASON} from "./handoff_mode_policy.ts";
import {AUTHORITY_SOURCE_CHANGED, uncheckedAuthoritySource, type AuthoritySourceCheck} from "./authority_source.ts";
import {currentLeaseAcquisitionProof} from "./lease_acquisition_proof.ts";
Expand Down Expand Up @@ -340,13 +340,14 @@ export async function executeCoordinationTodoClaim(
store: AuthorityStore,
rawInput: CoordinationTodoClaimInput,
authoritySourcesCurrent: AuthoritySourceCheck = uncheckedAuthoritySource,
observeWorkspace?: ClaimWorkspaceObserver,
): Promise<CoordinationTodoClaimResult> {
// Retry only a conclusive provider CAS rejection. Each attempt rereads the
// original receipt and the complete head, then rechecks source authorization,
// claim ownership, acceptance and lease/write-scope exclusion. Pinned revisions
// (including handoff grants) must return to their caller for a fresh observation.
for (let attempt = 0; ; attempt++) {
const result = await executeClaimAttempt(store, rawInput, authoritySourcesCurrent);
const result = await executeClaimAttempt(store, rawInput, authoritySourcesCurrent, observeWorkspace);
if (attempt >= 2 || rawInput.expected_provider_revision !== undefined ||
rawInput.transfer_grant !== undefined || result.status !== "conflict" ||
result.conflict_kind !== "provider_revision_mismatch") return result;
Expand All @@ -357,6 +358,7 @@ async function executeClaimAttempt(
store: AuthorityStore,
rawInput: CoordinationTodoClaimInput,
authoritySourcesCurrent: AuthoritySourceCheck,
observeWorkspace?: ClaimWorkspaceObserver,
): Promise<CoordinationTodoClaimResult> {
let input: CoordinationTodoClaimInput;
try {
Expand Down Expand Up @@ -583,13 +585,15 @@ async function executeClaimAttempt(
let lease: JsonObject | null = null;
let leaseChanged = false;
let leaseIdempotent = false;
let overlapAdvisories: JsonObject[] = [];
try {
const currentLease = projection.leases.get(input.todo_id);
if (handoffMode === "hard_lease" && input.manage_execution === true) {
const prepared = prepareClaimExecution(head.head, input);
const prepared = await prepareClaimExecution(head.head, input, observeWorkspace);
lease = prepared.lease;
leaseChanged = prepared.changed;
leaseIdempotent = !leaseChanged;
overlapAdvisories = prepared.overlapAdvisories ?? [];
} else if (handoffMode === "hard_lease" && leaseRequest !== null) {
// Validate required scopes before planning; a caller cannot omit a required conflict.
const writeScopes = coordinationTodoWriteScopes(todo);
Expand Down Expand Up @@ -692,6 +696,7 @@ async function executeClaimAttempt(
handoff_mode: handoffMode,
...(changed ? {updated_at: updatedAt} : {}),
mutation_authority: mutationAuthority,
...(overlapAdvisories.length ? {integration_overlap_advisories: overlapAdvisories} : {}),
...(input.manage_execution ? {execution_managed: true, claim_operation_id: input.operation_id} : {}),
...(leaseRequest === null && !(input.manage_execution && handoffMode === "hard_lease") ? {} : {
todo_changed: todoChanged,
Expand Down
1 change: 1 addition & 0 deletions skills/loopx-self-repair/references/repair-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ teaches a reusable control-plane lesson.
| `content_layout_sequence_acceptance_gap` | A carousel passes independent per-page density checks, but the cover is not the density peak or interior pages remain visibly sparse. | Typed layout plan, selected template `page_sequence`, renderer measurement packet, per-page densities, and `layout-check` failures. | Acceptance modeled each page in isolation and omitted sequence-level cover prominence and interior compactness defaults. | Keep project-specific narrative rules in the writing adapter, but make built-in templates require a cover first, a first-page density maximum, and an interior density floor; lock low-interior and denser-than-cover cases with focused negative smokes. |
| `shared_adapter_local_path_leak` | A shared adapter projection, such as Lark Kanban sync, would write an absolute local checkout, runtime, active-state, raw-log, or artifact path into a remote/shared row. | adapter dry-run payload, generated record values, docs boundary section, public/private scan. | Adapter reused local executor context as shared control-plane metadata. | Strip or replace the local path at the adapter projection layer, add a focused smoke that rejects the path, and only store explicit worker launch context when the row was authored for that purpose. |
| `peer_workspace_boundary_gap` | A peer edits or prepares repository changes from a non-git directory, unrelated worktree, or checkout that violates the selected task/repository isolation policy; a cross-repository repair may also be misclassified against the goal repo. | `agent_identity`, selected todo `task_repository` and write scopes, goal `repo`, current git origin/worktree/root, quota payload, heartbeat prompt commands, AGENTS worktree policy. | Workspace safety was inferred from identity labels, prompt text, or the goal repo alone instead of the selected task and `quota should-run`. | Add or repair `agent_workspace_guard_v1`; use a credential-free first-class `task_repository` only for genuine cross-repository tasks, and require a matching linked worktree. Block delivery and spend until the peer moves to a compliant worktree and reruns the guard with the same `--agent-id`. Keep the goal repo fallback and do not treat the field as scope or write authority. Read-only and monitor-only work should not be blocked merely by identity. |
| `managed_claim_worktree_observation_gap` | Ordinary `todo claim` reports `write_scope_conflict` for code tasks in independent sibling Git worktrees, while explicit `task-lease acquire --write-worktree` admits the same declared scopes. | Current task domain, repository and required scopes; actual Git roots; original claim response; exact lease owner, key and version; matched File/SQLite CLI reproduction and physical-checkout refusal. | Managed acquisition omitted the existing code-edit workspace observation and treated repository-relative code scopes as shared writes. | Pass only a candidate caller root to the existing lease observer; observe current canonical code scopes, repository and physical paths before the typed acquisition decision. Preserve same-checkout, redirected-path and foreign-repository refusals, shared-state exclusion, integration advisories and original-lease replay. For an older installed caller, use normal explicit acquire with all current scopes, its own worktree and current CAS, then an ordinary managed claim; do not turn that existing key into an inline reacquire without matching observation. Recover ambiguous original receipts first, never take over another lease or infer permission from workspace labels. |
| `workspace_guard_action_selection_projection_gap` | A foreign default Todo hides eligible alternatives, or an explicit choice repeatedly reports `control_repair` while unselected reentry restores a different default. | Initial and explicit selection qualification, Todo priorities and ready deferred successors, workspace guard, action portfolio, and receipt identity. | Workspace repair may hide candidate selection; alternatively, a refused explicit choice leaves no selected Todo and the legacy guard fallback overwrites the typed refusal with a repair for the default. | Preserve alternative selection when otherwise eligible. If selection is already deferred or rejected with no selected Todo, preserve that refusal without guarding the default. Separate hard authority/resume/lease gates from priority advice. A ready higher-priority deferred successor must not veto an otherwise eligible explicit choice. Keep the default recommendation and planning context, repair a priority-only refusal, then revalidate the chosen workspace before binding. An unbound choice is not protected by execution identity. |
| `boundary_repair_action_selection_projection_gap` | Quota selects a Todo whose required write scopes are absent from the current Goal boundary, emits `boundary_projection_repair`, and binds that blocked Todo even though another eligible Todo is runnable; same-turn selection of the alternative then fails with a receipt identity conflict. | Initial and explicitly selected `quota should-run` packets, Goal write scope, selected Todo write scopes, action portfolio, interaction selection command, and append-only heartbeat receipt events. | Boundary repair correctly blocked the default delivery, but the planning gate treated self-repair as if no eligible alternative could be selected, so recommendation order became hidden settlement authority. | Keep boundary repair fail-closed for the mismatched Todo while projecting the typed action portfolio when alternatives exist. Commit the first guard identity-less, revalidate an explicit choice against the live boundary, and append one deterministic identity upgrade without rewriting the original receipt. Retain hard conflicts for genuinely bound settlements and cover the full CLI round trip. |
| `monitor_successor_routing_gap` | A material monitor poll creates a natural-language `advance` successor for another repository, but the child has no repository, action, continuation, capabilities, stable target, or creation receipt; quota then routes plausible work without a safe workspace contract. | Parent monitor metadata, `quota monitor-poll` argv and artifact, child Todo metadata, rollout event successor ids, selected Todo workspace guard. | The agent extended a bounded observation into standing work, while the monitor-poll successor API accepted only text and an optional claimer and discarded typed routing. | Retire the unintended monitor/vision. Require explicit action semantics for every monitor-generated agent successor and explicit successor repository when the source monitor is repository-bound; carry optional execution capabilities, default continuation to `independent_handoff`, and derive a stable transition target when none is supplied. Record compact successor ids and routing receipts in the monitor artifact and rollout event. External-repository work must re-enter through that typed Todo rather than prior observation prose. |
Expand Down
Loading
Loading