Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 883088c921 | |||
| 0af892c573 |
@@ -40,6 +40,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
execution and confirms proper cleanup behavior.
|
||||
|
||||
### Fixed
|
||||
- **`SqliteChangeSetStore.get_for_plan` per-changeset granularity** (#7502): Fixed `get_for_plan()` merging all changeset entries into a single `SpecChangeSet`, losing per-changeset granularity. Now groups entries by `changeset_id` and returns one `SpecChangeSet` per `start()` invocation, preserving version history and rollback granularity for plans with multiple changesets.
|
||||
- **`SqliteChangeSetStore.get_for_plan` per-changeset granularity** (#7502): Fixed `get_for_plan()` merging all changeset entries into a single `SpecChangeSet`, losing per-changeset granularity. Now groups entries by `changeset_id` and returns one `SpecChangeSet` per `start()` invocation, preserving version history and rollback granularity for plans with multiple changesets.
|
||||
|
||||
- **Atomic `server_connect` config writes** (#993): Fixed `server_connect` in
|
||||
`cli/commands/server.py` to write all three config values (`server.url`,
|
||||
@@ -295,6 +297,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
`AUTO-TIME`, `AUTO-PROJ-OWN`, and `AUTO-PROD-BLDR`.
|
||||
|
||||
### Fixed
|
||||
- **`SqliteChangeSetStore.get_for_plan` per-changeset granularity** (#7502): Fixed `get_for_plan()` merging all changeset entries into a single `SpecChangeSet`, losing per-changeset granularity. Now groups entries by `changeset_id` and returns one `SpecChangeSet` per `start()` invocation, preserving version history and rollback granularity for plans with multiple changesets.
|
||||
- **`SqliteChangeSetStore.get_for_plan` per-changeset granularity** (#7502): Fixed `get_for_plan()` merging all changeset entries into a single `SpecChangeSet`, losing per-changeset granularity. Now groups entries by `changeset_id` and returns one `SpecChangeSet` per `start()` invocation, preserving version history and rollback granularity for plans with multiple changesets.
|
||||
|
||||
- **Plan Concurrency Race Condition** (#7989): Fixed critical race condition in `execute_plan()` and
|
||||
`apply_plan()` where concurrent CLI/worker sessions could simultaneously modify the same plan,
|
||||
|
||||
+6
-1
@@ -14,14 +14,19 @@ Below are some of the specific details of various contributions.
|
||||
|
||||
* Jeffrey Phillips Freeman has acted as Lead Developer, daily contributor, and Project Owner.
|
||||
* Brent E. Edwards has contributed quality assurance, test coverage, and CI pipeline improvements.
|
||||
* HAL 9000 has contributed automated implementation, bug fixes, and feature development as part of the CleverAgents automation pool.
|
||||
* HAL 9000 has contributed concurrency safety improvements, including thread-safe context tier management (issue #7547) for parallel plan execution.
|
||||
* HAL 9000 has contributed the trusted automation profile description correction (#9156): updated the `trusted` built-in profile description to match the specification.
|
||||
* HAL 9000 has contributed automated implementation, bug fixes, and feature development as part of the CleverAgents automation pool.
|
||||
* HAL 9000 has contributed the plan concurrency race-condition fix (#7989): wired `LockService` into the plan lifecycle, guarding `execute_plan()` and `apply_plan()` with plan-level advisory locks and unique per-invocation owner identities to prevent silent concurrent state corruption.
|
||||
* HAL 9000 has contributed the bug-hunt-pool-supervisor non-blocking tracking fix: updated step 5 to be best-effort and added rule 9 to prevent the automation-tracking-manager call from blocking the main supervisor loop.
|
||||
* HAL 9000 has contributed the plugin entry point security hardening fix (#7476): enforced entry point allowlist validation before importing plugin modules to prevent malicious plugin loading.
|
||||
* HAL 9000 has contributed the benchmark workflow separation (#9040): moved the benchmark-regression job out of the default PR workflow into a dedicated scheduled workflow, reducing median PR CI turnaround time from 99-132 minutes to under 30 minutes.
|
||||
* HAL 9000 has contributed automated bug fixes, security improvements, and migration safety enhancements including the migration prompt safe-default fix (#7503).
|
||||
* HAL 9000 has contributed CONTRIBUTING.md compliance improvements to the agent-evolution-worker (#8370): replaced hardcoded milestone references with dynamic Forgejo API queries and standardised label usage to `Type/Task`.
|
||||
* This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc.
|
||||
* HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system.
|
||||
* HAL 9000 has contributed the file edit encoding parameter fix (PR #8258 / issue #7559).
|
||||
* HAL 9000 has contributed the architecture-pool-supervisor milestone assignment feature (PR #8188 / issue #7521): added `forgejo_update_pull_request` permission and documented the PR workflow for major spec changes, enabling automatic milestone assignment for specification PRs.
|
||||
* HAL 9000 has contributed the git worktree TOCTOU race condition fix (PR #8178 / issue #7507): replaced the unsafe mkdtemp() + rmdir() pattern with a parent-directory approach to eliminate the race window in concurrent git worktree operations.
|
||||
* HAL 9000 has contributed the resource_dag.robot SQLite pool and cycle detection fix (PR #1228 / issue #1226): updated all three test cases to use StaticPool-based SQLite connections and improved cycle detection test coverage with distinct resource types.
|
||||
* HAL 9000 has contributed the atomic `server_connect` config write fix (PR #1203 / issue #993): resolved merge conflict in `config_service.py`, added `emit_config_changed()` helper for decoupled audit event emission, introduced typed `_AutoDiscover` sentinel to eliminate `# type: ignore[assignment]`, added `ReactiveEventBus.close()` for proper test teardown, and fixed hardcoded config path in `server_connect` rollback path.
|
||||
|
||||
@@ -49,6 +49,7 @@ class Operation(BaseModel):
|
||||
validate_assignment=True,
|
||||
use_enum_values=True,
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
|
||||
class Change(BaseModel):
|
||||
@@ -75,6 +76,7 @@ class Change(BaseModel):
|
||||
validate_assignment=True,
|
||||
use_enum_values=True,
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
|
||||
class LegacyChangeSet(BaseModel):
|
||||
@@ -84,6 +86,7 @@ class LegacyChangeSet(BaseModel):
|
||||
changes: list[Change] = Field(
|
||||
default_factory=lambda: [],
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
created_at: datetime = Field(default_factory=datetime.now)
|
||||
|
||||
@property
|
||||
@@ -117,6 +120,7 @@ class LegacyChangeSet(BaseModel):
|
||||
str_strip_whitespace=True,
|
||||
validate_assignment=True,
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
|
||||
# Backward-compatible alias — existing code imports ``ChangeSet``
|
||||
@@ -167,6 +171,9 @@ def normalize_change_path(path: str, repo_root: str = "") -> str:
|
||||
|
||||
|
||||
class ChangeEntry(BaseModel):
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
"""A single recorded change from a tool execution.
|
||||
|
||||
Each entry captures exactly one mutation made by a tool during
|
||||
@@ -175,57 +182,70 @@ class ChangeEntry(BaseModel):
|
||||
"""
|
||||
|
||||
entry_id: str = Field(
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
default_factory=_new_ulid,
|
||||
description="ULID uniquely identifying this entry",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
plan_id: str = Field(
|
||||
...,
|
||||
description="ULID of the plan that owns this change",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
resource_id: str = Field(
|
||||
...,
|
||||
description="ULID of the resource affected",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
tool_name: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description="Namespaced tool name that caused the change",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
operation: ChangeOperation = Field(
|
||||
...,
|
||||
description="Type of change operation",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
path: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description="Repo-relative file path",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
before_hash: str | None = Field(
|
||||
default=None,
|
||||
description="SHA-256 of file content before change",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
after_hash: str | None = Field(
|
||||
default=None,
|
||||
description="SHA-256 of file content after change",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
before_mode: int | None = Field(
|
||||
default=None,
|
||||
description="File mode before change",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
after_mode: int | None = Field(
|
||||
default=None,
|
||||
description="File mode after change",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
timestamp: datetime = Field(
|
||||
default_factory=lambda: datetime.now(UTC),
|
||||
description="UTC timestamp of the change",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
model_config = ConfigDict(
|
||||
str_strip_whitespace=True,
|
||||
validate_assignment=True,
|
||||
use_enum_values=True,
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_hash_fields(self) -> "ChangeEntry":
|
||||
@@ -282,23 +302,28 @@ class SpecChangeSet(BaseModel):
|
||||
default_factory=_new_ulid,
|
||||
description="ULID uniquely identifying this changeset",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
plan_id: str = Field(
|
||||
...,
|
||||
description="ULID of the plan",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
entries: list[ChangeEntry] = Field(
|
||||
default_factory=list,
|
||||
description="Ordered list of recorded change entries",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
created_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(UTC),
|
||||
description="UTC timestamp when the changeset was created",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
model_config = ConfigDict(
|
||||
str_strip_whitespace=True,
|
||||
validate_assignment=True,
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
# -- computed summary properties ---------------------------------
|
||||
|
||||
@@ -368,6 +393,7 @@ class SpecChangeSet(BaseModel):
|
||||
self.entries,
|
||||
key=lambda e: (e.resource_id, e.path, e.timestamp),
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
def grouped_by_resource(self) -> dict[str, list[ChangeEntry]]:
|
||||
"""Group entries by resource_id for plan diff output.
|
||||
@@ -390,6 +416,7 @@ class SpecChangeSet(BaseModel):
|
||||
f"ChangeEntry plan_id '{entry.plan_id}' does not match "
|
||||
f"SpecChangeSet plan_id '{self.plan_id}'"
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
self.entries.append(entry)
|
||||
|
||||
|
||||
@@ -409,69 +436,85 @@ class ToolInvocation(BaseModel):
|
||||
default_factory=_new_ulid,
|
||||
description="ULID uniquely identifying this invocation",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
plan_id: str = Field(
|
||||
...,
|
||||
description="ULID of the plan that triggered this call",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
tool_name: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description="Namespaced tool name (namespace/short_name)",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
skill_name: str | None = Field(
|
||||
default=None,
|
||||
description="Skill that provided the tool, if any",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
arguments: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Input parameters passed to the tool",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
result: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description="Output payload (None when failed before return)",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
error: str | None = Field(
|
||||
default=None,
|
||||
description="Error message when the tool execution failed",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
success: bool = Field(
|
||||
default=True,
|
||||
description="Whether execution completed without error",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
duration_ms: float = Field(
|
||||
default=0.0,
|
||||
ge=0.0,
|
||||
description="Wall-clock execution time in milliseconds",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
started_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(UTC),
|
||||
description="UTC timestamp when execution started",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
completed_at: datetime | None = Field(
|
||||
default=None,
|
||||
description="UTC timestamp when execution finished",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
change_ids: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="ChangeEntry IDs produced by this invocation",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
sequence_number: int = Field(
|
||||
default=0,
|
||||
ge=0,
|
||||
description="Deterministic ordering within a plan",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
sandbox_path: str | None = Field(
|
||||
default=None,
|
||||
description="Sandbox root used during this invocation",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
resource_refs: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Resource IDs involved in this invocation",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
provider_metadata: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description="Provider info (model name, latency, etc.)",
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
container_metadata: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
@@ -479,12 +522,15 @@ class ToolInvocation(BaseModel):
|
||||
"Includes container_id, image, workspace_folder, exec_time_ms, "
|
||||
"exit_code, and timed_out."
|
||||
),
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
model_config = ConfigDict(
|
||||
str_strip_whitespace=True,
|
||||
validate_assignment=True,
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
|
||||
class InvocationTracker(Protocol):
|
||||
@@ -503,6 +549,7 @@ class InvocationTracker(Protocol):
|
||||
plan_id: str,
|
||||
skill_name: str,
|
||||
) -> list[ToolInvocation]:
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
"""Return invocations for a specific skill within a plan."""
|
||||
...
|
||||
|
||||
@@ -532,17 +579,20 @@ class InMemoryInvocationTracker:
|
||||
self._by_plan[plan_id],
|
||||
key=lambda i: i.sequence_number,
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
def get_invocations_for_skill(
|
||||
self,
|
||||
plan_id: str,
|
||||
skill_name: str,
|
||||
) -> list[ToolInvocation]:
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
"""Return invocations for a specific skill within a plan."""
|
||||
return sorted(
|
||||
[i for i in self._by_plan[plan_id] if i.skill_name == skill_name],
|
||||
key=lambda i: i.sequence_number,
|
||||
)
|
||||
changeset_id: str = Field(default="", description="ULID of the changeset this entry belongs to")
|
||||
|
||||
def get_changes(self, plan_id: str) -> list[str]:
|
||||
"""Return all change IDs produced by invocations for a plan."""
|
||||
|
||||
@@ -452,7 +452,7 @@ class SqliteChangeSetStore:
|
||||
self,
|
||||
plan_id: str,
|
||||
) -> list[SpecChangeSet]:
|
||||
"""Return all ChangeSets associated with *plan_id*."""
|
||||
"""Return all ChangeSets for *plan_id* with per-changeset granularity."""
|
||||
if not plan_id:
|
||||
return []
|
||||
|
||||
@@ -460,8 +460,21 @@ class SqliteChangeSetStore:
|
||||
if not entries:
|
||||
return []
|
||||
|
||||
# Group entries by changeset_id
|
||||
changeset_groups = {}
|
||||
for entry in entries:
|
||||
changeset_id = entry.changeset_id
|
||||
if changeset_id not in changeset_groups:
|
||||
changeset_groups[changeset_id] = []
|
||||
changeset_groups[changeset_id].append(entry)
|
||||
|
||||
return [
|
||||
SpecChangeSet(plan_id=plan_id, entries=entries),
|
||||
SpecChangeSet(
|
||||
changeset_id=cs_id,
|
||||
plan_id=plan_id,
|
||||
entries=group_entries
|
||||
)
|
||||
for cs_id, group_entries in changeset_groups.items()
|
||||
]
|
||||
|
||||
def summarize(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user