812 lines
29 KiB
Python
812 lines
29 KiB
Python
"""Canonical Task projection intent and same-identity recovery primitives.
|
|
|
|
This module is intentionally Task-Plane-owned. It does not open a network
|
|
listener and it does not authenticate callers; existing control-plane services
|
|
must enforce their normal service-side ``agent-task:control`` boundary before
|
|
calling it.
|
|
|
|
The recovery primitive has one purpose: converge a durable, complete
|
|
ProjectionIntent to exactly one canonical Task row. It never fabricates task
|
|
semantics from a Bridge binding and it never generates a replacement task id.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass
|
|
from datetime import datetime, timezone
|
|
from enum import Enum
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import sqlite3
|
|
from typing import Any, Mapping, Sequence
|
|
|
|
|
|
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
|
|
JOB_ID_RE = TASK_ID_RE
|
|
SHA40_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
SHA64_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9._/-]{1,220}$")
|
|
SCHEMA = "aios.canonical-task-projection-intent.v1"
|
|
RECEIPT_SCHEMA = "aios.canonical-task-projection-receipt.v1"
|
|
|
|
|
|
class ProjectionRecoveryError(RuntimeError):
|
|
def __init__(self, code: str, status: int = 409) -> None:
|
|
super().__init__(code)
|
|
self.code = code
|
|
self.status = status
|
|
|
|
|
|
class ProjectionState(str, Enum):
|
|
PENDING = "PENDING"
|
|
CONVERGED = "CONVERGED"
|
|
DRIFT = "DRIFT"
|
|
FAILED = "FAILED"
|
|
BLOCKED = "BLOCKED"
|
|
UNKNOWN = "UNKNOWN"
|
|
BACKFILL_REQUIRED = "BACKFILL_REQUIRED"
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
def _canonical(value: object) -> bytes:
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
).encode("utf-8")
|
|
|
|
|
|
def sha256_json(value: object) -> str:
|
|
return hashlib.sha256(_canonical(value)).hexdigest()
|
|
|
|
|
|
def _string(value: object, code: str, pattern: re.Pattern[str] | None = None) -> str:
|
|
if not isinstance(value, str) or not value.strip():
|
|
raise ProjectionRecoveryError(code, 400)
|
|
result = value.strip()
|
|
if pattern is not None and pattern.fullmatch(result) is None:
|
|
raise ProjectionRecoveryError(code, 400)
|
|
return result
|
|
|
|
|
|
def _integer(value: object, code: str, *, minimum: int = 0) -> int:
|
|
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
|
raise ProjectionRecoveryError(code, 400)
|
|
return value
|
|
|
|
|
|
def _plain_mapping(value: object, code: str) -> dict[str, Any]:
|
|
if not isinstance(value, Mapping):
|
|
raise ProjectionRecoveryError(code, 400)
|
|
return {str(k): v for k, v in value.items()}
|
|
|
|
|
|
def _plain_list(value: object, code: str) -> list[Any]:
|
|
if not isinstance(value, list):
|
|
raise ProjectionRecoveryError(code, 400)
|
|
return list(value)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CanonicalTaskPayload:
|
|
task_id: str
|
|
goal: str
|
|
instructions: str
|
|
success_criteria: tuple[Mapping[str, Any], ...]
|
|
constraints: Mapping[str, Any]
|
|
required_capabilities: tuple[str, ...]
|
|
metadata: Mapping[str, Any]
|
|
priority: int = 100
|
|
max_attempts: int = 20
|
|
deadline: str | None = None
|
|
|
|
@classmethod
|
|
def from_mapping(cls, value: Mapping[str, Any]) -> "CanonicalTaskPayload":
|
|
allowed = {
|
|
"task_id",
|
|
"goal",
|
|
"instructions",
|
|
"success_criteria",
|
|
"constraints",
|
|
"required_capabilities",
|
|
"metadata",
|
|
"priority",
|
|
"max_attempts",
|
|
"deadline",
|
|
}
|
|
if set(value) - allowed:
|
|
raise ProjectionRecoveryError("TASK_PAYLOAD_UNKNOWN_FIELDS", 400)
|
|
|
|
task_id = _string(value.get("task_id"), "TASK_ID_INVALID", TASK_ID_RE)
|
|
goal = _string(value.get("goal"), "TASK_GOAL_INVALID")
|
|
instructions = _string(value.get("instructions"), "TASK_INSTRUCTIONS_INVALID")
|
|
if len(goal) > 8192 or len(instructions) > 65536:
|
|
raise ProjectionRecoveryError("TASK_TEXT_TOO_LARGE", 413)
|
|
|
|
criteria_raw = _plain_list(value.get("success_criteria"), "TASK_SUCCESS_CRITERIA_INVALID")
|
|
criteria: list[Mapping[str, Any]] = []
|
|
for item in criteria_raw:
|
|
if not isinstance(item, Mapping):
|
|
raise ProjectionRecoveryError("TASK_SUCCESS_CRITERIA_INVALID", 400)
|
|
criteria.append(dict(item))
|
|
if not criteria:
|
|
raise ProjectionRecoveryError("TASK_SUCCESS_CRITERIA_REQUIRED", 400)
|
|
|
|
constraints = _plain_mapping(value.get("constraints"), "TASK_CONSTRAINTS_INVALID")
|
|
capabilities_raw = _plain_list(
|
|
value.get("required_capabilities", []),
|
|
"TASK_CAPABILITIES_INVALID",
|
|
)
|
|
capabilities: list[str] = []
|
|
for item in capabilities_raw:
|
|
capabilities.append(_string(item, "TASK_CAPABILITY_INVALID"))
|
|
|
|
# Recovery recreates the pristine canonical row. Initial execution
|
|
# projection remains the sole owner of runtime capabilities.
|
|
if capabilities:
|
|
raise ProjectionRecoveryError(
|
|
"TASK_RECOVERY_REQUIRES_PRISTINE_CAPABILITIES",
|
|
409,
|
|
)
|
|
|
|
metadata = _plain_mapping(value.get("metadata", {}), "TASK_METADATA_INVALID")
|
|
if "execution" in metadata or "execution_projection" in metadata:
|
|
raise ProjectionRecoveryError(
|
|
"TASK_RECOVERY_REQUIRES_PRISTINE_METADATA",
|
|
409,
|
|
)
|
|
|
|
priority = _integer(value.get("priority", 100), "TASK_PRIORITY_INVALID", minimum=0)
|
|
max_attempts = _integer(
|
|
value.get("max_attempts", 20),
|
|
"TASK_MAX_ATTEMPTS_INVALID",
|
|
minimum=1,
|
|
)
|
|
deadline = value.get("deadline")
|
|
if deadline is not None:
|
|
deadline = _string(deadline, "TASK_DEADLINE_INVALID")
|
|
|
|
return cls(
|
|
task_id=task_id,
|
|
goal=goal,
|
|
instructions=instructions,
|
|
success_criteria=tuple(criteria),
|
|
constraints=constraints,
|
|
required_capabilities=tuple(capabilities),
|
|
metadata=metadata,
|
|
priority=priority,
|
|
max_attempts=max_attempts,
|
|
deadline=deadline,
|
|
)
|
|
|
|
def public(self) -> dict[str, Any]:
|
|
return {
|
|
"task_id": self.task_id,
|
|
"goal": self.goal,
|
|
"instructions": self.instructions,
|
|
"success_criteria": [dict(x) for x in self.success_criteria],
|
|
"constraints": dict(self.constraints),
|
|
"required_capabilities": list(self.required_capabilities),
|
|
"metadata": dict(self.metadata),
|
|
"priority": self.priority,
|
|
"max_attempts": self.max_attempts,
|
|
"deadline": self.deadline,
|
|
}
|
|
|
|
@property
|
|
def payload_sha256(self) -> str:
|
|
return sha256_json(self.public())
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GovernedProjectionBinding:
|
|
governed_task_id: str
|
|
bridge_job_id: str
|
|
repository: str
|
|
branch: str
|
|
generation: int
|
|
base_oid: str
|
|
expected_branch_oid: str
|
|
|
|
@classmethod
|
|
def from_mapping(cls, value: Mapping[str, Any]) -> "GovernedProjectionBinding":
|
|
allowed = {
|
|
"governed_task_id",
|
|
"bridge_job_id",
|
|
"repository",
|
|
"branch",
|
|
"generation",
|
|
"base_oid",
|
|
"expected_branch_oid",
|
|
}
|
|
if set(value) != allowed:
|
|
raise ProjectionRecoveryError("GOVERNED_BINDING_FIELDS_INVALID", 400)
|
|
repository = _string(value["repository"], "REPOSITORY_INVALID")
|
|
if repository != "aios/aios":
|
|
raise ProjectionRecoveryError("REPOSITORY_MISMATCH", 409)
|
|
return cls(
|
|
governed_task_id=_string(
|
|
value["governed_task_id"],
|
|
"GOVERNED_TASK_ID_INVALID",
|
|
TASK_ID_RE,
|
|
),
|
|
bridge_job_id=_string(
|
|
value["bridge_job_id"],
|
|
"BRIDGE_JOB_ID_INVALID",
|
|
JOB_ID_RE,
|
|
),
|
|
repository=repository,
|
|
branch=_string(value["branch"], "BRANCH_INVALID", BRANCH_RE),
|
|
generation=_integer(value["generation"], "GENERATION_INVALID", minimum=1),
|
|
base_oid=_string(value["base_oid"], "BASE_OID_INVALID", SHA40_RE),
|
|
expected_branch_oid=_string(
|
|
value["expected_branch_oid"],
|
|
"EXPECTED_OID_INVALID",
|
|
SHA40_RE,
|
|
),
|
|
)
|
|
|
|
def public(self) -> dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ProjectionSourceArtifact:
|
|
ref: str
|
|
sha256: str
|
|
|
|
@classmethod
|
|
def from_mapping(cls, value: Mapping[str, Any]) -> "ProjectionSourceArtifact":
|
|
if set(value) != {"ref", "sha256"}:
|
|
raise ProjectionRecoveryError("SOURCE_ARTIFACT_FIELDS_INVALID", 400)
|
|
return cls(
|
|
ref=_string(value["ref"], "SOURCE_ARTIFACT_REF_INVALID"),
|
|
sha256=_string(value["sha256"], "SOURCE_ARTIFACT_SHA_INVALID", SHA64_RE),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CanonicalTaskProjectionIntent:
|
|
projection_id: str
|
|
projection_generation: int
|
|
idempotency_key: str
|
|
task: CanonicalTaskPayload
|
|
binding: GovernedProjectionBinding
|
|
source_artifact: ProjectionSourceArtifact
|
|
schema: str = SCHEMA
|
|
|
|
@classmethod
|
|
def from_mapping(cls, value: Mapping[str, Any]) -> "CanonicalTaskProjectionIntent":
|
|
allowed = {
|
|
"schema",
|
|
"projection_id",
|
|
"projection_generation",
|
|
"idempotency_key",
|
|
"task",
|
|
"governed_binding",
|
|
"source_artifact",
|
|
"task_payload_sha256",
|
|
}
|
|
if set(value) != allowed:
|
|
raise ProjectionRecoveryError("PROJECTION_INTENT_FIELDS_INVALID", 400)
|
|
if value["schema"] != SCHEMA:
|
|
raise ProjectionRecoveryError("PROJECTION_INTENT_SCHEMA_INVALID", 400)
|
|
|
|
task = CanonicalTaskPayload.from_mapping(
|
|
_plain_mapping(value["task"], "TASK_PAYLOAD_INVALID")
|
|
)
|
|
binding = GovernedProjectionBinding.from_mapping(
|
|
_plain_mapping(value["governed_binding"], "GOVERNED_BINDING_INVALID")
|
|
)
|
|
source = ProjectionSourceArtifact.from_mapping(
|
|
_plain_mapping(value["source_artifact"], "SOURCE_ARTIFACT_INVALID")
|
|
)
|
|
if binding.governed_task_id != task.task_id:
|
|
raise ProjectionRecoveryError("PROJECTION_TASK_IDENTITY_MISMATCH", 409)
|
|
|
|
expected_hash = _string(
|
|
value["task_payload_sha256"],
|
|
"TASK_PAYLOAD_SHA_INVALID",
|
|
SHA64_RE,
|
|
)
|
|
if task.payload_sha256 != expected_hash:
|
|
raise ProjectionRecoveryError("TASK_PAYLOAD_SHA_MISMATCH", 409)
|
|
|
|
projection_id = _string(
|
|
value["projection_id"],
|
|
"PROJECTION_ID_INVALID",
|
|
TASK_ID_RE,
|
|
)
|
|
idempotency_key = _string(
|
|
value["idempotency_key"],
|
|
"IDEMPOTENCY_KEY_INVALID",
|
|
TASK_ID_RE,
|
|
)
|
|
generation = _integer(
|
|
value["projection_generation"],
|
|
"PROJECTION_GENERATION_INVALID",
|
|
minimum=1,
|
|
)
|
|
return cls(
|
|
projection_id=projection_id,
|
|
projection_generation=generation,
|
|
idempotency_key=idempotency_key,
|
|
task=task,
|
|
binding=binding,
|
|
source_artifact=source,
|
|
)
|
|
|
|
def public(self) -> dict[str, Any]:
|
|
return {
|
|
"schema": self.schema,
|
|
"projection_id": self.projection_id,
|
|
"projection_generation": self.projection_generation,
|
|
"idempotency_key": self.idempotency_key,
|
|
"task": self.task.public(),
|
|
"governed_binding": self.binding.public(),
|
|
"source_artifact": asdict(self.source_artifact),
|
|
"task_payload_sha256": self.task.payload_sha256,
|
|
}
|
|
|
|
@property
|
|
def request_sha256(self) -> str:
|
|
return sha256_json(self.public())
|
|
|
|
|
|
class CanonicalTaskProjectionStore:
|
|
"""Task-Plane-owned intent/recovery store.
|
|
|
|
The caller must already be inside the Task Plane authorization boundary.
|
|
"""
|
|
|
|
def __init__(self, db_path: Path | str) -> None:
|
|
self.db_path = Path(db_path)
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
connection = sqlite3.connect(self.db_path, timeout=30.0)
|
|
connection.row_factory = sqlite3.Row
|
|
connection.execute("PRAGMA foreign_keys=ON")
|
|
connection.execute("PRAGMA busy_timeout=30000")
|
|
return connection
|
|
|
|
@staticmethod
|
|
def _columns(connection: sqlite3.Connection, table: str) -> dict[str, sqlite3.Row]:
|
|
return {
|
|
str(row["name"]): row
|
|
for row in connection.execute(f'PRAGMA table_info("{table}")')
|
|
}
|
|
|
|
@staticmethod
|
|
def _table_names(connection: sqlite3.Connection) -> set[str]:
|
|
return {
|
|
str(row[0])
|
|
for row in connection.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
)
|
|
}
|
|
|
|
@classmethod
|
|
def _event_table(
|
|
cls,
|
|
connection: sqlite3.Connection,
|
|
) -> tuple[str, dict[str, sqlite3.Row]]:
|
|
tables = cls._table_names(connection)
|
|
for name in ("task_events", "events"):
|
|
if name in tables:
|
|
columns = cls._columns(connection, name)
|
|
if "task_id" in columns and "event_type" in columns:
|
|
return name, columns
|
|
raise ProjectionRecoveryError("TASK_EVENT_TABLE_UNSUPPORTED", 503)
|
|
|
|
@staticmethod
|
|
def _ensure_control_tables(connection: sqlite3.Connection) -> None:
|
|
connection.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS task_projection_intents (
|
|
projection_id TEXT PRIMARY KEY,
|
|
task_id TEXT NOT NULL,
|
|
bridge_job_id TEXT NOT NULL,
|
|
projection_generation INTEGER NOT NULL,
|
|
idempotency_key TEXT NOT NULL,
|
|
request_sha256 TEXT NOT NULL,
|
|
task_payload_sha256 TEXT NOT NULL,
|
|
intent_json TEXT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
last_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
UNIQUE(task_id, projection_generation),
|
|
UNIQUE(task_id, idempotency_key)
|
|
)
|
|
"""
|
|
)
|
|
connection.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS task_projection_receipts (
|
|
projection_id TEXT PRIMARY KEY,
|
|
task_id TEXT NOT NULL,
|
|
bridge_job_id TEXT NOT NULL,
|
|
projection_generation INTEGER NOT NULL,
|
|
task_payload_sha256 TEXT NOT NULL,
|
|
canonical_revision INTEGER NOT NULL,
|
|
state TEXT NOT NULL,
|
|
created_new_row INTEGER NOT NULL,
|
|
observed_at TEXT NOT NULL,
|
|
receipt_json TEXT NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
|
|
def persist_intent(
|
|
self,
|
|
intent: CanonicalTaskProjectionIntent,
|
|
) -> dict[str, Any]:
|
|
now = _now()
|
|
request_sha = intent.request_sha256
|
|
intent_text = _canonical(intent.public()).decode("utf-8")
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
self._ensure_control_tables(connection)
|
|
existing = connection.execute(
|
|
"""
|
|
SELECT * FROM task_projection_intents
|
|
WHERE task_id=? AND projection_generation=?
|
|
""",
|
|
(intent.task.task_id, intent.projection_generation),
|
|
).fetchone()
|
|
if existing is not None:
|
|
if (
|
|
str(existing["projection_id"]) != intent.projection_id
|
|
or str(existing["idempotency_key"]) != intent.idempotency_key
|
|
or str(existing["request_sha256"]) != request_sha
|
|
):
|
|
raise ProjectionRecoveryError(
|
|
"PROJECTION_INTENT_IDEMPOTENCY_CONFLICT",
|
|
409,
|
|
)
|
|
connection.commit()
|
|
return {
|
|
"projection_id": intent.projection_id,
|
|
"task_id": intent.task.task_id,
|
|
"state": str(existing["state"]),
|
|
"idempotent": True,
|
|
}
|
|
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO task_projection_intents(
|
|
projection_id,task_id,bridge_job_id,projection_generation,
|
|
idempotency_key,request_sha256,task_payload_sha256,intent_json,
|
|
state,last_error,created_at,updated_at
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
|
""",
|
|
(
|
|
intent.projection_id,
|
|
intent.task.task_id,
|
|
intent.binding.bridge_job_id,
|
|
intent.projection_generation,
|
|
intent.idempotency_key,
|
|
request_sha,
|
|
intent.task.payload_sha256,
|
|
intent_text,
|
|
ProjectionState.PENDING.value,
|
|
None,
|
|
now,
|
|
now,
|
|
),
|
|
)
|
|
connection.commit()
|
|
return {
|
|
"projection_id": intent.projection_id,
|
|
"task_id": intent.task.task_id,
|
|
"state": ProjectionState.PENDING.value,
|
|
"idempotent": False,
|
|
}
|
|
|
|
def _task_row_payload(
|
|
self,
|
|
row: sqlite3.Row,
|
|
) -> dict[str, Any]:
|
|
try:
|
|
success = json.loads(row["success_criteria_json"])
|
|
constraints = json.loads(row["constraints_json"])
|
|
capabilities = json.loads(row["required_capabilities_json"])
|
|
metadata = json.loads(row["metadata_json"])
|
|
except (TypeError, json.JSONDecodeError) as exc:
|
|
raise ProjectionRecoveryError("CANONICAL_TASK_JSON_INVALID", 409) from exc
|
|
|
|
if (
|
|
not isinstance(success, list)
|
|
or not isinstance(constraints, Mapping)
|
|
or not isinstance(capabilities, list)
|
|
or not isinstance(metadata, Mapping)
|
|
):
|
|
raise ProjectionRecoveryError("CANONICAL_TASK_SHAPE_INVALID", 409)
|
|
|
|
return {
|
|
"task_id": str(row["task_id"]),
|
|
"goal": str(row["goal"]),
|
|
"instructions": str(row["instructions"]),
|
|
"success_criteria": success,
|
|
"constraints": dict(constraints),
|
|
"required_capabilities": capabilities,
|
|
"metadata": dict(metadata),
|
|
"priority": int(row["priority"]),
|
|
"max_attempts": int(row["max_attempts"]),
|
|
"deadline": row["deadline"],
|
|
}
|
|
|
|
def _verify_existing_task(
|
|
self,
|
|
row: sqlite3.Row,
|
|
intent: CanonicalTaskProjectionIntent,
|
|
) -> int:
|
|
observed = self._task_row_payload(row)
|
|
observed_hash = sha256_json(observed)
|
|
if observed_hash != intent.task.payload_sha256:
|
|
raise ProjectionRecoveryError(
|
|
"CANONICAL_TASK_PROJECTION_IDENTITY_CONFLICT",
|
|
409,
|
|
)
|
|
return int(row["revision"])
|
|
|
|
@staticmethod
|
|
def _required_task_columns() -> set[str]:
|
|
return {
|
|
"task_id",
|
|
"goal",
|
|
"instructions",
|
|
"success_criteria_json",
|
|
"required_capabilities_json",
|
|
"constraints_json",
|
|
"metadata_json",
|
|
"priority",
|
|
"max_attempts",
|
|
"attempt",
|
|
"status",
|
|
"claimed_by",
|
|
"lease_token_hash",
|
|
"lease_expires_at",
|
|
"result_json",
|
|
"verification_json",
|
|
"revision",
|
|
"created_at",
|
|
"updated_at",
|
|
}
|
|
|
|
def _insert_task(
|
|
self,
|
|
connection: sqlite3.Connection,
|
|
intent: CanonicalTaskProjectionIntent,
|
|
) -> int:
|
|
columns = self._columns(connection, "tasks")
|
|
if not self._required_task_columns().issubset(columns):
|
|
raise ProjectionRecoveryError("TASK_SCHEMA_RECOVERY_UNSUPPORTED", 503)
|
|
|
|
now = _now()
|
|
values: dict[str, Any] = {
|
|
"task_id": intent.task.task_id,
|
|
"goal": intent.task.goal,
|
|
"instructions": intent.task.instructions,
|
|
"success_criteria_json": _canonical(
|
|
[dict(x) for x in intent.task.success_criteria]
|
|
).decode("utf-8"),
|
|
"required_capabilities_json": "[]",
|
|
"constraints_json": _canonical(dict(intent.task.constraints)).decode("utf-8"),
|
|
"metadata_json": _canonical(dict(intent.task.metadata)).decode("utf-8"),
|
|
"deadline": intent.task.deadline,
|
|
"priority": intent.task.priority,
|
|
"max_attempts": intent.task.max_attempts,
|
|
"attempt": 0,
|
|
"status": "QUEUED",
|
|
"claimed_by": None,
|
|
"lease_token_hash": None,
|
|
"lease_expires_at": None,
|
|
"result_json": None,
|
|
"verification_json": None,
|
|
"revision": 1,
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
}
|
|
for optional in (
|
|
"result_digest",
|
|
"result_idempotency_key",
|
|
):
|
|
if optional in columns:
|
|
values[optional] = None
|
|
|
|
names = [name for name in values if name in columns]
|
|
placeholders = ",".join("?" for _ in names)
|
|
connection.execute(
|
|
f'INSERT INTO tasks ({",".join(chr(34)+n+chr(34) for n in names)}) '
|
|
f"VALUES ({placeholders})",
|
|
tuple(values[name] for name in names),
|
|
)
|
|
self._insert_task_created_event(connection, intent)
|
|
return 1
|
|
|
|
def _insert_task_created_event(
|
|
self,
|
|
connection: sqlite3.Connection,
|
|
intent: CanonicalTaskProjectionIntent,
|
|
) -> None:
|
|
table, columns = self._event_table(connection)
|
|
event = {
|
|
"schema": "aios.task-projection-recovery.event.v1",
|
|
"task_id": intent.task.task_id,
|
|
"bridge_job_id": intent.binding.bridge_job_id,
|
|
"projection_id": intent.projection_id,
|
|
"projection_generation": intent.projection_generation,
|
|
"task_payload_sha256": intent.task.payload_sha256,
|
|
"source_artifact_ref": intent.source_artifact.ref,
|
|
"source_artifact_sha256": intent.source_artifact.sha256,
|
|
"recovery": True,
|
|
}
|
|
values: dict[str, Any] = {
|
|
"task_id": intent.task.task_id,
|
|
"event_type": "TASK_CREATED",
|
|
}
|
|
payload_text = _canonical(event).decode("utf-8")
|
|
if "payload_json" in columns:
|
|
values["payload_json"] = payload_text
|
|
elif "payload" in columns:
|
|
values["payload"] = payload_text
|
|
else:
|
|
raise ProjectionRecoveryError("TASK_EVENT_PAYLOAD_UNSUPPORTED", 503)
|
|
if "created_at" in columns:
|
|
values["created_at"] = _now()
|
|
if "agent_id" in columns:
|
|
values["agent_id"] = "control-plane-projection-recovery"
|
|
if "actor_id" in columns:
|
|
values["actor_id"] = "control-plane-projection-recovery"
|
|
|
|
names = list(values)
|
|
connection.execute(
|
|
f'INSERT INTO "{table}" ({",".join(chr(34)+n+chr(34) for n in names)}) '
|
|
f'VALUES ({",".join("?" for _ in names)})',
|
|
tuple(values[name] for name in names),
|
|
)
|
|
|
|
def reconcile(
|
|
self,
|
|
intent: CanonicalTaskProjectionIntent,
|
|
) -> dict[str, Any]:
|
|
# Persist first so a crash before Task creation leaves a durable PENDING
|
|
# intent for startup/supervisor reconciliation.
|
|
self.persist_intent(intent)
|
|
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
self._ensure_control_tables(connection)
|
|
existing_receipt = connection.execute(
|
|
"SELECT * FROM task_projection_receipts WHERE projection_id=?",
|
|
(intent.projection_id,),
|
|
).fetchone()
|
|
if existing_receipt is not None:
|
|
if (
|
|
str(existing_receipt["task_id"]) != intent.task.task_id
|
|
or str(existing_receipt["bridge_job_id"]) != intent.binding.bridge_job_id
|
|
or str(existing_receipt["task_payload_sha256"])
|
|
!= intent.task.payload_sha256
|
|
):
|
|
raise ProjectionRecoveryError(
|
|
"PROJECTION_RECEIPT_IDENTITY_CONFLICT",
|
|
409,
|
|
)
|
|
connection.commit()
|
|
receipt = json.loads(existing_receipt["receipt_json"])
|
|
receipt["idempotent"] = True
|
|
return receipt
|
|
|
|
row = connection.execute(
|
|
"SELECT * FROM tasks WHERE task_id=?",
|
|
(intent.task.task_id,),
|
|
).fetchone()
|
|
created_new = False
|
|
if row is None:
|
|
revision = self._insert_task(connection, intent)
|
|
created_new = True
|
|
row = connection.execute(
|
|
"SELECT * FROM tasks WHERE task_id=?",
|
|
(intent.task.task_id,),
|
|
).fetchone()
|
|
if row is None:
|
|
raise ProjectionRecoveryError(
|
|
"TASK_RECOVERY_OBSERVATION_FAILED",
|
|
503,
|
|
)
|
|
else:
|
|
revision = self._verify_existing_task(row, intent)
|
|
|
|
# Observation after mutation is mandatory.
|
|
revision = self._verify_existing_task(row, intent)
|
|
observed_at = _now()
|
|
receipt = {
|
|
"schema": RECEIPT_SCHEMA,
|
|
"projection_id": intent.projection_id,
|
|
"governed_task_id": intent.binding.governed_task_id,
|
|
"canonical_task_id": intent.task.task_id,
|
|
"bridge_job_id": intent.binding.bridge_job_id,
|
|
"projection_generation": intent.projection_generation,
|
|
"task_payload_sha256": intent.task.payload_sha256,
|
|
"canonical_revision": revision,
|
|
"state": ProjectionState.CONVERGED.value,
|
|
"created_new_row": created_new,
|
|
"source_artifact": asdict(intent.source_artifact),
|
|
"observed_at": observed_at,
|
|
"idempotent": False,
|
|
}
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO task_projection_receipts(
|
|
projection_id,task_id,bridge_job_id,projection_generation,
|
|
task_payload_sha256,canonical_revision,state,created_new_row,
|
|
observed_at,receipt_json
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
""",
|
|
(
|
|
intent.projection_id,
|
|
intent.task.task_id,
|
|
intent.binding.bridge_job_id,
|
|
intent.projection_generation,
|
|
intent.task.payload_sha256,
|
|
revision,
|
|
ProjectionState.CONVERGED.value,
|
|
1 if created_new else 0,
|
|
observed_at,
|
|
_canonical(receipt).decode("utf-8"),
|
|
),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
UPDATE task_projection_intents
|
|
SET state=?, last_error=NULL, updated_at=?
|
|
WHERE projection_id=?
|
|
""",
|
|
(
|
|
ProjectionState.CONVERGED.value,
|
|
observed_at,
|
|
intent.projection_id,
|
|
),
|
|
)
|
|
connection.commit()
|
|
return receipt
|
|
|
|
def get_projection(
|
|
self,
|
|
task_id: str,
|
|
) -> dict[str, Any] | None:
|
|
task_id = _string(task_id, "TASK_ID_INVALID", TASK_ID_RE)
|
|
with self._connect() as connection:
|
|
self._ensure_control_tables(connection)
|
|
intent = connection.execute(
|
|
"""
|
|
SELECT * FROM task_projection_intents
|
|
WHERE task_id=?
|
|
ORDER BY projection_generation DESC
|
|
LIMIT 1
|
|
""",
|
|
(task_id,),
|
|
).fetchone()
|
|
if intent is None:
|
|
return None
|
|
receipt = connection.execute(
|
|
"SELECT receipt_json FROM task_projection_receipts WHERE projection_id=?",
|
|
(intent["projection_id"],),
|
|
).fetchone()
|
|
return {
|
|
"projection_id": intent["projection_id"],
|
|
"task_id": task_id,
|
|
"bridge_job_id": intent["bridge_job_id"],
|
|
"projection_generation": int(intent["projection_generation"]),
|
|
"state": intent["state"],
|
|
"last_error": intent["last_error"],
|
|
"task_payload_sha256": intent["task_payload_sha256"],
|
|
"receipt": json.loads(receipt[0]) if receipt else None,
|
|
}
|