worktree-controller 1.30.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- worktree_controller/__init__.py +3 -0
- worktree_controller/activation_layout.py +483 -0
- worktree_controller/branch_retirement.py +1865 -0
- worktree_controller/canonical_refresh.py +150 -0
- worktree_controller/canonical_transition.py +324 -0
- worktree_controller/cleanup_attempts.py +442 -0
- worktree_controller/cleanup_evidence.py +75 -0
- worktree_controller/cleanup_recovery.py +912 -0
- worktree_controller/cli.py +1907 -0
- worktree_controller/controller.py +1974 -0
- worktree_controller/convergence.py +893 -0
- worktree_controller/delivered_closeout.py +928 -0
- worktree_controller/disposable.py +677 -0
- worktree_controller/environment.py +1038 -0
- worktree_controller/frozen_recovery.py +291 -0
- worktree_controller/git_backend.py +682 -0
- worktree_controller/governance.py +365 -0
- worktree_controller/hardlinks.py +294 -0
- worktree_controller/integration_cleanup.py +92 -0
- worktree_controller/integration_conflict.py +117 -0
- worktree_controller/integration_retry.py +526 -0
- worktree_controller/keyed_document_scope.py +162 -0
- worktree_controller/lifecycle.py +8992 -0
- worktree_controller/lifecycle_history.py +358 -0
- worktree_controller/lifecycle_observation.py +234 -0
- worktree_controller/metadata_codec.py +1694 -0
- worktree_controller/metadata_relocation.py +131 -0
- worktree_controller/models.py +546 -0
- worktree_controller/mount_diagnostics.py +324 -0
- worktree_controller/multiple_cleanup_recovery.py +730 -0
- worktree_controller/namespace_migration.py +1412 -0
- worktree_controller/native_session_recovery.py +171 -0
- worktree_controller/owner_actions.py +34 -0
- worktree_controller/policy.py +333 -0
- worktree_controller/preservation.py +983 -0
- worktree_controller/preserved_cleanup.py +849 -0
- worktree_controller/publication_execution.py +361 -0
- worktree_controller/reconciler.py +430 -0
- worktree_controller/recovery_guidance.py +205 -0
- worktree_controller/registration_backfill.py +166 -0
- worktree_controller/registration_prune.py +391 -0
- worktree_controller/rendering.py +179 -0
- worktree_controller/resource_admission.py +1736 -0
- worktree_controller/retirement_adapters.py +130 -0
- worktree_controller/retirement_execution.py +234 -0
- worktree_controller/semantics.py +459 -0
- worktree_controller/session.py +505 -0
- worktree_controller/session_admission.py +265 -0
- worktree_controller/source_cleanup.py +515 -0
- worktree_controller/source_preservation.py +692 -0
- worktree_controller/source_recovery.py +592 -0
- worktree_controller/store.py +1228 -0
- worktree_controller/tool_cli_contract.json +1791 -0
- worktree_controller/transaction_scope.py +876 -0
- worktree_controller/writer_lease.py +662 -0
- worktree_controller/writer_session.py +887 -0
- worktree_controller-1.30.4.dist-info/METADATA +3149 -0
- worktree_controller-1.30.4.dist-info/RECORD +61 -0
- worktree_controller-1.30.4.dist-info/WHEEL +4 -0
- worktree_controller-1.30.4.dist-info/entry_points.txt +5 -0
- worktree_controller-1.30.4.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""One-process admission of an executor session (``session_admission_version`` 1).
|
|
2
|
+
|
|
3
|
+
A host that launches a governed executor would otherwise run every step below as
|
|
4
|
+
its own Controller command: each one a new process that loads the repository
|
|
5
|
+
metadata again and attaches a full repository status to its answer. These two
|
|
6
|
+
commands run the same step functions, in the same order, with the same guards,
|
|
7
|
+
journals, receipts and CAS checks. They only share one process and skip the
|
|
8
|
+
status each intermediate step would otherwise attach.
|
|
9
|
+
|
|
10
|
+
``session-admit`` covers the common start only: a new allocation acquired from
|
|
11
|
+
the canonical checkout, then execution admission. Every other start (an existing
|
|
12
|
+
allocation, a non-canonical workspace, a resumed checkout, a capacity decision
|
|
13
|
+
other than ``CREATE_ALLOWED``) is answered with ``SESSION_ADMISSION_FALLBACK`` and
|
|
14
|
+
nothing is changed, so the host keeps using its step-by-step path for it.
|
|
15
|
+
|
|
16
|
+
``session-bind`` binds write entitlement to the live executor process: the lease
|
|
17
|
+
for the exact holder, the holder-aware write guard, then the runtime occupancy
|
|
18
|
+
binding. A failure after the lease was granted releases that same lease before
|
|
19
|
+
the error is returned, so a refused bind leaves no entitlement behind.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from .controller import _plan_from_snapshot, envelope, guard_write
|
|
29
|
+
from .git_backend import inventory
|
|
30
|
+
from .lifecycle import acquire, update_external_binding
|
|
31
|
+
from .models import ControllerError, WriterHolder
|
|
32
|
+
from .policy import load_policy
|
|
33
|
+
from .semantics import managed_record as _managed_record
|
|
34
|
+
from .semantics import record_for as _record_for
|
|
35
|
+
from .store import MetadataStore
|
|
36
|
+
from .writer_lease import RUNTIME_BINDING_PREFIX, acquire_writer_lease, release_writer_lease
|
|
37
|
+
|
|
38
|
+
SESSION_ADMISSION_VERSION = 1
|
|
39
|
+
RUNTIME_KINDS = frozenset({"agent", "command", "test"})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _fallback(command: str, reason: str, **extra: Any) -> dict[str, Any]:
|
|
43
|
+
payload = envelope(command)
|
|
44
|
+
payload["decision"] = {
|
|
45
|
+
"code": "SESSION_ADMISSION_FALLBACK",
|
|
46
|
+
"reason": reason,
|
|
47
|
+
"changed": False,
|
|
48
|
+
**extra,
|
|
49
|
+
}
|
|
50
|
+
return payload
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def admit_session(
|
|
54
|
+
repository_path: Path,
|
|
55
|
+
*,
|
|
56
|
+
task_key: str,
|
|
57
|
+
authority_key: str,
|
|
58
|
+
module: str,
|
|
59
|
+
owner: str,
|
|
60
|
+
branch: str,
|
|
61
|
+
base_ref: str,
|
|
62
|
+
client_id: str,
|
|
63
|
+
session_id: str,
|
|
64
|
+
run_id: str | None = None,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
"""Acquire a new feature allocation and admit its execution, or change nothing."""
|
|
67
|
+
|
|
68
|
+
command = "session-admit"
|
|
69
|
+
repository = inventory(repository_path)
|
|
70
|
+
requested = repository_path.expanduser().resolve(strict=True)
|
|
71
|
+
if requested != repository.canonical_path:
|
|
72
|
+
return _fallback(command, "workspace is not the canonical checkout")
|
|
73
|
+
snapshot = MetadataStore(repository.common_dir).load()
|
|
74
|
+
for item in repository.worktrees:
|
|
75
|
+
record = _record_for(item, snapshot)
|
|
76
|
+
if record is not None and record.task_key == task_key:
|
|
77
|
+
return _fallback(
|
|
78
|
+
command,
|
|
79
|
+
"an allocation for this task is already registered",
|
|
80
|
+
allocation_id=record.allocation_id,
|
|
81
|
+
)
|
|
82
|
+
planned = _plan_from_snapshot(
|
|
83
|
+
repository,
|
|
84
|
+
load_policy(repository.canonical_path),
|
|
85
|
+
snapshot,
|
|
86
|
+
task_key=task_key,
|
|
87
|
+
authority_key=authority_key,
|
|
88
|
+
module=module,
|
|
89
|
+
role="feature",
|
|
90
|
+
)
|
|
91
|
+
planned_decision = planned.get("decision")
|
|
92
|
+
if not isinstance(planned_decision, dict) or planned_decision.get("code") != "CREATE_ALLOWED":
|
|
93
|
+
return _fallback(command, "plan did not allow an immediate checkout", plan=planned_decision)
|
|
94
|
+
acquired = acquire(
|
|
95
|
+
repository_path,
|
|
96
|
+
task_key=task_key,
|
|
97
|
+
authority_key=authority_key,
|
|
98
|
+
module=module,
|
|
99
|
+
role="feature",
|
|
100
|
+
owner=owner,
|
|
101
|
+
branch=branch,
|
|
102
|
+
base_ref=base_ref,
|
|
103
|
+
observe=False,
|
|
104
|
+
)
|
|
105
|
+
decision = acquired["decision"]
|
|
106
|
+
if decision.get("code") != "ACQUIRED":
|
|
107
|
+
# RESOURCE_WAIT and similar outcomes keep their own durable request; the
|
|
108
|
+
# host handles them exactly as it handles a step-by-step acquire.
|
|
109
|
+
acquired["command"] = command
|
|
110
|
+
return acquired
|
|
111
|
+
allocation = {
|
|
112
|
+
"canonical_path": str(repository.canonical_path),
|
|
113
|
+
"path": decision["path"],
|
|
114
|
+
"allocation_id": decision["allocation_id"],
|
|
115
|
+
"task_key": task_key,
|
|
116
|
+
"authority_key": authority_key,
|
|
117
|
+
"owner": owner,
|
|
118
|
+
"role": "feature",
|
|
119
|
+
"lifecycle": "ACTIVE",
|
|
120
|
+
"branch": decision["branch"],
|
|
121
|
+
"head": decision["head"],
|
|
122
|
+
}
|
|
123
|
+
execution: dict[str, Any] | None = None
|
|
124
|
+
policy = load_policy(repository.canonical_path)
|
|
125
|
+
if policy.resource_admission:
|
|
126
|
+
from .resource_admission import admit_execution
|
|
127
|
+
|
|
128
|
+
execution = admit_execution(
|
|
129
|
+
repository_path,
|
|
130
|
+
target=Path(decision["path"]),
|
|
131
|
+
owner=owner,
|
|
132
|
+
client_id=client_id,
|
|
133
|
+
session_id=session_id,
|
|
134
|
+
run_id=run_id,
|
|
135
|
+
)["decision"]
|
|
136
|
+
payload = envelope(command)
|
|
137
|
+
payload["decision"] = {
|
|
138
|
+
"code": "SESSION_ADMITTED",
|
|
139
|
+
"reason": "new allocation acquired and its execution admitted"
|
|
140
|
+
if execution is not None
|
|
141
|
+
else "new allocation acquired",
|
|
142
|
+
"allocation": allocation,
|
|
143
|
+
"admission_strategy": "resource-budget-v1" if policy.resource_admission else None,
|
|
144
|
+
"execution": execution,
|
|
145
|
+
"session_admission_version": SESSION_ADMISSION_VERSION,
|
|
146
|
+
}
|
|
147
|
+
return payload
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def runtime_binding_value(holder: dict[str, Any], lease_id: str, kind: str) -> str:
|
|
151
|
+
"""The typed occupancy identity, in the exact form the registry decodes."""
|
|
152
|
+
identity = {
|
|
153
|
+
"holder": {
|
|
154
|
+
"pid": holder["pid"],
|
|
155
|
+
"boot_id": holder["boot_id"],
|
|
156
|
+
"start_ticks": holder["start_ticks"],
|
|
157
|
+
"client_id": holder["client_id"],
|
|
158
|
+
"session_id": holder["session_id"],
|
|
159
|
+
"run_id": holder["run_id"],
|
|
160
|
+
},
|
|
161
|
+
"lease_id": lease_id,
|
|
162
|
+
"kind": kind,
|
|
163
|
+
}
|
|
164
|
+
return RUNTIME_BINDING_PREFIX + json.dumps(identity, sort_keys=True, separators=(",", ":"))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def bind_session(
|
|
168
|
+
repository_path: Path,
|
|
169
|
+
*,
|
|
170
|
+
target: Path,
|
|
171
|
+
task_key: str,
|
|
172
|
+
authority_key: str,
|
|
173
|
+
owner: str,
|
|
174
|
+
client_id: str,
|
|
175
|
+
holder_pid: int,
|
|
176
|
+
session_id: str,
|
|
177
|
+
run_id: str | None = None,
|
|
178
|
+
kind: str = "agent",
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
"""Grant, check and register one executor's write entitlement, or leave none."""
|
|
181
|
+
|
|
182
|
+
if kind not in RUNTIME_KINDS:
|
|
183
|
+
raise ControllerError("INVALID_BINDING", f"unsupported runtime kind: {kind}")
|
|
184
|
+
repository = inventory(repository_path)
|
|
185
|
+
snapshot = MetadataStore(repository.common_dir).load()
|
|
186
|
+
_item, record = _managed_record(repository, snapshot, target)
|
|
187
|
+
if (
|
|
188
|
+
record.task_key != task_key
|
|
189
|
+
or record.authority_key != authority_key
|
|
190
|
+
or record.owner != owner
|
|
191
|
+
or record.lifecycle != "ACTIVE"
|
|
192
|
+
):
|
|
193
|
+
raise ControllerError(
|
|
194
|
+
"SESSION_ALLOCATION_DRIFT", "allocation no longer matches the admitted session"
|
|
195
|
+
)
|
|
196
|
+
leased = acquire_writer_lease(
|
|
197
|
+
repository_path,
|
|
198
|
+
target=target,
|
|
199
|
+
owner=owner,
|
|
200
|
+
client_id=client_id,
|
|
201
|
+
holder_pid=holder_pid,
|
|
202
|
+
session_id=session_id,
|
|
203
|
+
run_id=run_id,
|
|
204
|
+
observe=False,
|
|
205
|
+
)
|
|
206
|
+
lease = leased["decision"]
|
|
207
|
+
holder = lease["holder"]
|
|
208
|
+
try:
|
|
209
|
+
guarded = guard_write(
|
|
210
|
+
repository_path,
|
|
211
|
+
target=target,
|
|
212
|
+
task_key=task_key,
|
|
213
|
+
authority_key=authority_key,
|
|
214
|
+
owner=owner,
|
|
215
|
+
operation="feature-write",
|
|
216
|
+
holder=WriterHolder(**holder),
|
|
217
|
+
observe=False,
|
|
218
|
+
)["decision"]
|
|
219
|
+
if guarded.get("code") != "WRITE_ALLOWED":
|
|
220
|
+
raise ControllerError(
|
|
221
|
+
str(guarded.get("code") or "WRITE_GUARD_REFUSED"),
|
|
222
|
+
str(guarded.get("reason") or "write guard refused the holder"),
|
|
223
|
+
)
|
|
224
|
+
binding = runtime_binding_value(holder, lease["lease_id"], kind)
|
|
225
|
+
bound = update_external_binding(
|
|
226
|
+
repository_path,
|
|
227
|
+
target=target,
|
|
228
|
+
expected_record_version=record.record_version,
|
|
229
|
+
owner=owner,
|
|
230
|
+
binding=binding,
|
|
231
|
+
action="add",
|
|
232
|
+
observe=False,
|
|
233
|
+
)["decision"]
|
|
234
|
+
except Exception as failure:
|
|
235
|
+
outcome: dict[str, Any] = {"lease_id": lease["lease_id"], "lease_released": True}
|
|
236
|
+
try:
|
|
237
|
+
release_writer_lease(
|
|
238
|
+
repository_path,
|
|
239
|
+
target=target,
|
|
240
|
+
holder_pid=holder["pid"],
|
|
241
|
+
lease_id=lease["lease_id"],
|
|
242
|
+
holder=WriterHolder(**holder),
|
|
243
|
+
)
|
|
244
|
+
except Exception as release_failure:
|
|
245
|
+
# The host still knows this exact lease and releases it with its own cleanup.
|
|
246
|
+
outcome = {
|
|
247
|
+
"lease_id": lease["lease_id"],
|
|
248
|
+
"lease_released": False,
|
|
249
|
+
"lease": lease,
|
|
250
|
+
"release_error": getattr(release_failure, "code", type(release_failure).__name__),
|
|
251
|
+
}
|
|
252
|
+
if isinstance(failure, (ControllerError, OSError)):
|
|
253
|
+
failure.details = outcome # type: ignore[attr-defined]
|
|
254
|
+
raise
|
|
255
|
+
payload = envelope("session-bind")
|
|
256
|
+
payload["decision"] = {
|
|
257
|
+
"code": "SESSION_BOUND",
|
|
258
|
+
"reason": "write entitlement granted, checked and registered for the live executor",
|
|
259
|
+
"lease": lease,
|
|
260
|
+
"write_guard": guarded["code"],
|
|
261
|
+
"runtime_binding": binding,
|
|
262
|
+
"record_version": bound["record_version"],
|
|
263
|
+
"session_admission_version": SESSION_ADMISSION_VERSION,
|
|
264
|
+
}
|
|
265
|
+
return payload
|