codex-flow 2.1.13__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.
- codex_flow/__init__.py +28 -0
- codex_flow/__main__.py +9 -0
- codex_flow/cli.py +242 -0
- codex_flow/data/LICENSE +21 -0
- codex_flow/data/README.en.md +303 -0
- codex_flow/data/README.md +305 -0
- codex_flow/data/VERSION +1 -0
- codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
- codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
- codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
- codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
- codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
- codex_flow/data/apps/macos-overlay/README.en.md +121 -0
- codex_flow/data/apps/macos-overlay/README.md +123 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
- codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
- codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
- codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
- codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
- codex_flow/data/apps/macos-overlay/build.sh +75 -0
- codex_flow/data/benchmark/corpus.json +103 -0
- codex_flow/data/benchmark/manifest.example.json +41 -0
- codex_flow/data/benchmark/manifest.schema.json +137 -0
- codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
- codex_flow/data/benchmark/profiles.json +90 -0
- codex_flow/data/benchmark/schema.json +77 -0
- codex_flow/data/benchmark/tasks.json +50 -0
- codex_flow/data/completions/codex-flow.bash +34 -0
- codex_flow/data/completions/codex-flow.zsh +52 -0
- codex_flow/data/glama.json +6 -0
- codex_flow/data/install-release.ps1 +126 -0
- codex_flow/data/install-release.sh +155 -0
- codex_flow/data/install.ps1 +349 -0
- codex_flow/data/install.sh +362 -0
- codex_flow/data/policy/benchmark.toml +49 -0
- codex_flow/data/policy/defaults.toml +70 -0
- codex_flow/data/scripts/analyze-benchmark.py +510 -0
- codex_flow/data/scripts/benchmark-local.py +171 -0
- codex_flow/data/scripts/check-recommendation.py +277 -0
- codex_flow/data/scripts/doctor.py +449 -0
- codex_flow/data/scripts/generate-release-manifest.py +74 -0
- codex_flow/data/scripts/localization.py +192 -0
- codex_flow/data/scripts/manage-hooks.py +448 -0
- codex_flow/data/scripts/manage-instructions.py +389 -0
- codex_flow/data/scripts/manage-shell.py +151 -0
- codex_flow/data/scripts/materialize-corpus.py +193 -0
- codex_flow/data/scripts/menu.py +646 -0
- codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
- codex_flow/data/scripts/package-release.py +132 -0
- codex_flow/data/scripts/render-benchmark-report.py +292 -0
- codex_flow/data/scripts/run-benchmark.py +829 -0
- codex_flow/data/scripts/strategies/__init__.py +28 -0
- codex_flow/data/scripts/strategies/balanced.py +115 -0
- codex_flow/data/scripts/strategies/base.py +363 -0
- codex_flow/data/scripts/strategies/efficient.py +158 -0
- codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
- codex_flow/data/scripts/strategies/quality.py +209 -0
- codex_flow/data/scripts/strategies/speed.py +108 -0
- codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
- codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
- codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
- codex_flow/data/scripts/strategy_runtime.py +1091 -0
- codex_flow/data/scripts/telemetry.py +400 -0
- codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
- codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
- codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
- codex_flow/data/scripts/telemetry_core/common.py +421 -0
- codex_flow/data/scripts/telemetry_core/latency.py +593 -0
- codex_flow/data/scripts/telemetry_core/query.py +427 -0
- codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
- codex_flow/data/scripts/telemetry_core/render.py +460 -0
- codex_flow/data/scripts/telemetry_core/repair.py +223 -0
- codex_flow/data/scripts/ui.py +266 -0
- codex_flow/data/scripts/update-homebrew-formula.py +146 -0
- codex_flow/data/scripts/update_runtime_config.py +134 -0
- codex_flow/data/scripts/updater.py +1718 -0
- codex_flow/data/smithery.yaml +18 -0
- codex_flow/data/templates/agents/worker-explorer.toml +24 -0
- codex_flow/data/templates/agents/worker-implementer.toml +49 -0
- codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
- codex_flow/data/templates/flow-pilot-instructions.md +35 -0
- codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
- codex_flow/mcp.py +35 -0
- codex_flow-2.1.13.dist-info/METADATA +342 -0
- codex_flow-2.1.13.dist-info/RECORD +113 -0
- codex_flow-2.1.13.dist-info/WHEEL +5 -0
- codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
- codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
- codex_flow-2.1.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Deterministic Worker lifecycle evaluator for FlowPilot."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from typing import Any, Iterable
|
|
10
|
+
|
|
11
|
+
FALLBACK_ACTIONS = {
|
|
12
|
+
"continue_partial": "continue_partial",
|
|
13
|
+
"parent_delta": "parent_delta",
|
|
14
|
+
"replan": "replan",
|
|
15
|
+
"retry_review": "retry_review",
|
|
16
|
+
"fail": "fail",
|
|
17
|
+
}
|
|
18
|
+
JOIN_POLICIES = {"opportunistic", "quorum", "required"}
|
|
19
|
+
WRITER_FALLBACKS = {"parent_delta", "replan"}
|
|
20
|
+
EPOCH_MILLISECONDS_THRESHOLD = 100_000_000_000
|
|
21
|
+
CHECKPOINT_STATUSES = {"not_requested", "requested", "received", "harvested"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _strict_timeout(value: Any, label: str) -> float:
|
|
25
|
+
if type(value) not in (int, float):
|
|
26
|
+
raise ValueError(f"{label} must be a number")
|
|
27
|
+
result = float(value)
|
|
28
|
+
if not math.isfinite(result):
|
|
29
|
+
raise ValueError(f"{label} must be finite")
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class LifecyclePolicy:
|
|
35
|
+
join_policy: str
|
|
36
|
+
min_successful_workers: int
|
|
37
|
+
idle_timeout_seconds: float
|
|
38
|
+
hard_timeout_seconds: float
|
|
39
|
+
cancel_if_superseded: bool
|
|
40
|
+
cancel_stragglers_after_quorum: bool
|
|
41
|
+
fallback_policy: str
|
|
42
|
+
soft_timeout_seconds: float | None = None
|
|
43
|
+
checkpoint_rearm_seconds: float | None = None
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_dict(cls, value: dict[str, Any]) -> "LifecyclePolicy":
|
|
47
|
+
if type(value) is not dict:
|
|
48
|
+
raise ValueError("stage policy must be an object")
|
|
49
|
+
required = {
|
|
50
|
+
"join_policy",
|
|
51
|
+
"min_successful_workers",
|
|
52
|
+
"idle_timeout_seconds",
|
|
53
|
+
"hard_timeout_seconds",
|
|
54
|
+
"cancel_if_superseded",
|
|
55
|
+
"cancel_stragglers_after_quorum",
|
|
56
|
+
"fallback_policy",
|
|
57
|
+
}
|
|
58
|
+
missing = required.difference(value)
|
|
59
|
+
if missing:
|
|
60
|
+
raise ValueError(f"stage policy missing fields: {sorted(missing)}")
|
|
61
|
+
for key in ("cancel_if_superseded", "cancel_stragglers_after_quorum"):
|
|
62
|
+
if type(value[key]) is not bool:
|
|
63
|
+
raise ValueError(f"{key} must be boolean")
|
|
64
|
+
if type(value["join_policy"]) is not str or type(value["fallback_policy"]) is not str:
|
|
65
|
+
raise ValueError("join_policy and fallback_policy must be strings")
|
|
66
|
+
if type(value["min_successful_workers"]) is not int:
|
|
67
|
+
raise ValueError("min_successful_workers must be an integer")
|
|
68
|
+
raw_soft = value.get("soft_timeout_seconds")
|
|
69
|
+
raw_rearm = value.get("checkpoint_rearm_seconds")
|
|
70
|
+
policy = cls(
|
|
71
|
+
join_policy=value["join_policy"],
|
|
72
|
+
min_successful_workers=value["min_successful_workers"],
|
|
73
|
+
idle_timeout_seconds=_strict_timeout(value["idle_timeout_seconds"], "idle_timeout_seconds"),
|
|
74
|
+
hard_timeout_seconds=_strict_timeout(value["hard_timeout_seconds"], "hard_timeout_seconds"),
|
|
75
|
+
cancel_if_superseded=value["cancel_if_superseded"],
|
|
76
|
+
cancel_stragglers_after_quorum=value["cancel_stragglers_after_quorum"],
|
|
77
|
+
fallback_policy=value["fallback_policy"],
|
|
78
|
+
soft_timeout_seconds=None if raw_soft is None else _strict_timeout(raw_soft, "soft_timeout_seconds"),
|
|
79
|
+
checkpoint_rearm_seconds=None if raw_rearm is None else _strict_timeout(raw_rearm, "checkpoint_rearm_seconds"),
|
|
80
|
+
)
|
|
81
|
+
policy.validate()
|
|
82
|
+
return policy
|
|
83
|
+
|
|
84
|
+
def validate(self) -> None:
|
|
85
|
+
if self.join_policy not in JOIN_POLICIES:
|
|
86
|
+
raise ValueError(f"invalid join policy: {self.join_policy}")
|
|
87
|
+
if self.fallback_policy not in FALLBACK_ACTIONS:
|
|
88
|
+
raise ValueError(f"invalid fallback policy: {self.fallback_policy}")
|
|
89
|
+
if type(self.min_successful_workers) is not int or self.min_successful_workers < 0:
|
|
90
|
+
raise ValueError("min_successful_workers must be a non-negative integer")
|
|
91
|
+
if self.join_policy == "opportunistic" and self.min_successful_workers != 0:
|
|
92
|
+
raise ValueError("opportunistic stage must use min_successful_workers=0")
|
|
93
|
+
if self.join_policy != "opportunistic" and self.min_successful_workers < 1:
|
|
94
|
+
raise ValueError(f"{self.join_policy} stage requires at least one successful worker")
|
|
95
|
+
for label, value in (
|
|
96
|
+
("idle_timeout_seconds", self.idle_timeout_seconds),
|
|
97
|
+
("hard_timeout_seconds", self.hard_timeout_seconds),
|
|
98
|
+
):
|
|
99
|
+
if type(value) not in (int, float) or not math.isfinite(float(value)) or value <= 0:
|
|
100
|
+
raise ValueError(f"{label} must be a positive finite number")
|
|
101
|
+
if self.hard_timeout_seconds < self.idle_timeout_seconds:
|
|
102
|
+
raise ValueError("hard_timeout_seconds must be >= idle_timeout_seconds")
|
|
103
|
+
if self.soft_timeout_seconds is not None:
|
|
104
|
+
if type(self.soft_timeout_seconds) not in (int, float) or not math.isfinite(float(self.soft_timeout_seconds)):
|
|
105
|
+
raise ValueError("soft_timeout_seconds must be finite")
|
|
106
|
+
if self.soft_timeout_seconds <= 0 or self.soft_timeout_seconds >= self.hard_timeout_seconds:
|
|
107
|
+
raise ValueError("soft_timeout_seconds must be positive and lower than hard_timeout_seconds")
|
|
108
|
+
if self.checkpoint_rearm_seconds is not None:
|
|
109
|
+
if type(self.checkpoint_rearm_seconds) not in (int, float) or not math.isfinite(float(self.checkpoint_rearm_seconds)):
|
|
110
|
+
raise ValueError("checkpoint_rearm_seconds must be finite")
|
|
111
|
+
if self.checkpoint_rearm_seconds <= 0 or self.checkpoint_rearm_seconds >= self.hard_timeout_seconds:
|
|
112
|
+
raise ValueError("checkpoint_rearm_seconds must be positive and lower than hard_timeout_seconds")
|
|
113
|
+
if self.soft_timeout_seconds is None:
|
|
114
|
+
raise ValueError("checkpoint_rearm_seconds requires soft_timeout_seconds")
|
|
115
|
+
if self.soft_timeout_seconds + self.checkpoint_rearm_seconds >= self.hard_timeout_seconds:
|
|
116
|
+
raise ValueError("checkpoint_rearm_seconds must leave time for a second checkpoint before hard_timeout_seconds")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class CheckpointRecord:
|
|
121
|
+
sequence: int
|
|
122
|
+
generation: int
|
|
123
|
+
requested_at: float
|
|
124
|
+
received_at: float | None = None
|
|
125
|
+
harvested_at: float | None = None
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_dict(cls, value: dict[str, Any]) -> "CheckpointRecord":
|
|
129
|
+
if type(value) is not dict:
|
|
130
|
+
raise ValueError("checkpoint record must be an object")
|
|
131
|
+
allowed = {"sequence", "generation", "requested_at", "received_at", "harvested_at"}
|
|
132
|
+
unknown = sorted(set(value).difference(allowed))
|
|
133
|
+
if unknown:
|
|
134
|
+
raise ValueError(f"checkpoint record has unknown fields: {unknown}")
|
|
135
|
+
for key in ("sequence", "generation"):
|
|
136
|
+
if type(value.get(key)) is not int:
|
|
137
|
+
raise ValueError(f"checkpoint {key} must be an integer")
|
|
138
|
+
if "requested_at" not in value:
|
|
139
|
+
raise ValueError("checkpoint record missing requested_at")
|
|
140
|
+
return cls(
|
|
141
|
+
sequence=value["sequence"],
|
|
142
|
+
generation=value["generation"],
|
|
143
|
+
requested_at=_strict_timeout(value["requested_at"], "checkpoint requested_at"),
|
|
144
|
+
received_at=None if value.get("received_at") is None else _strict_timeout(value["received_at"], "checkpoint received_at"),
|
|
145
|
+
harvested_at=None if value.get("harvested_at") is None else _strict_timeout(value["harvested_at"], "checkpoint harvested_at"),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def validate(self, *, started_at: float, now: float) -> None:
|
|
149
|
+
if self.sequence < 1 or self.generation < 0:
|
|
150
|
+
raise ValueError("checkpoint sequence must be positive and generation non-negative")
|
|
151
|
+
for label, value in (
|
|
152
|
+
("requested_at", self.requested_at),
|
|
153
|
+
("received_at", self.received_at),
|
|
154
|
+
("harvested_at", self.harvested_at),
|
|
155
|
+
):
|
|
156
|
+
if value is None:
|
|
157
|
+
continue
|
|
158
|
+
if type(value) not in (int, float) or not math.isfinite(float(value)):
|
|
159
|
+
raise ValueError(f"checkpoint {label} must be finite")
|
|
160
|
+
if value > EPOCH_MILLISECONDS_THRESHOLD:
|
|
161
|
+
raise ValueError(f"checkpoint {label} must use Unix seconds")
|
|
162
|
+
if value < started_at or value > now:
|
|
163
|
+
raise ValueError(f"checkpoint {label} must be between started_at and now")
|
|
164
|
+
if self.received_at is not None and self.received_at < self.requested_at:
|
|
165
|
+
raise ValueError("checkpoint received_at cannot precede requested_at")
|
|
166
|
+
if self.harvested_at is not None:
|
|
167
|
+
if self.received_at is None:
|
|
168
|
+
raise ValueError("checkpoint harvested_at requires received_at")
|
|
169
|
+
if self.harvested_at < self.received_at:
|
|
170
|
+
raise ValueError("checkpoint harvested_at cannot precede received_at")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class WorkerObservation:
|
|
175
|
+
scope_id: str
|
|
176
|
+
stage: str
|
|
177
|
+
started_at: float
|
|
178
|
+
last_progress_at: float
|
|
179
|
+
now: float
|
|
180
|
+
last_meaningful_progress_at: float | None = None
|
|
181
|
+
writable: bool = False
|
|
182
|
+
in_flight: bool = False
|
|
183
|
+
terminal_success: bool = False
|
|
184
|
+
terminal_failure: bool = False
|
|
185
|
+
scope_superseded: bool = False
|
|
186
|
+
cancel_confirmed: bool = False
|
|
187
|
+
replacement_isolated: bool = False
|
|
188
|
+
generation: int = 0
|
|
189
|
+
checkpoint_sequence: tuple[CheckpointRecord, ...] = ()
|
|
190
|
+
|
|
191
|
+
def meaningful_progress_at(self) -> float:
|
|
192
|
+
return self.last_progress_at if self.last_meaningful_progress_at is None else self.last_meaningful_progress_at
|
|
193
|
+
|
|
194
|
+
def checkpoint_records(self) -> tuple[CheckpointRecord, ...]:
|
|
195
|
+
return self.checkpoint_sequence
|
|
196
|
+
|
|
197
|
+
def latest_checkpoint(self) -> CheckpointRecord | None:
|
|
198
|
+
return self.checkpoint_sequence[-1] if self.checkpoint_sequence else None
|
|
199
|
+
|
|
200
|
+
def latest_harvested_checkpoint(self) -> CheckpointRecord | None:
|
|
201
|
+
harvested = [record for record in self.checkpoint_sequence if record.harvested_at is not None]
|
|
202
|
+
return harvested[-1] if harvested else None
|
|
203
|
+
|
|
204
|
+
def checkpoint_status(self) -> str:
|
|
205
|
+
latest = self.latest_checkpoint()
|
|
206
|
+
if latest is None:
|
|
207
|
+
return "not_requested"
|
|
208
|
+
if latest.harvested_at is not None:
|
|
209
|
+
return "harvested"
|
|
210
|
+
if latest.received_at is not None:
|
|
211
|
+
return "received"
|
|
212
|
+
return "requested"
|
|
213
|
+
|
|
214
|
+
def validate(self) -> None:
|
|
215
|
+
if not self.scope_id:
|
|
216
|
+
raise ValueError("scope_id is required")
|
|
217
|
+
if self.stage not in {"exploration", "implementation", "review"}:
|
|
218
|
+
raise ValueError(f"invalid stage: {self.stage}")
|
|
219
|
+
if type(self.generation) is not int or self.generation < 0:
|
|
220
|
+
raise ValueError("generation must be a non-negative integer")
|
|
221
|
+
if type(self.checkpoint_sequence) is not tuple:
|
|
222
|
+
raise ValueError("checkpoint_sequence must be a tuple")
|
|
223
|
+
if self.stage != "implementation" and self.checkpoint_sequence:
|
|
224
|
+
raise ValueError("checkpoint_sequence is only valid for implementation stage")
|
|
225
|
+
|
|
226
|
+
timestamps = [self.started_at, self.last_progress_at, self.now]
|
|
227
|
+
if self.last_meaningful_progress_at is not None:
|
|
228
|
+
timestamps.append(self.last_meaningful_progress_at)
|
|
229
|
+
if any(type(v) not in (int, float) or not math.isfinite(float(v)) or v < 0 for v in timestamps):
|
|
230
|
+
raise ValueError("timestamps must be finite non-negative seconds")
|
|
231
|
+
if any(v > EPOCH_MILLISECONDS_THRESHOLD for v in timestamps):
|
|
232
|
+
raise ValueError("timestamps must use Unix seconds")
|
|
233
|
+
if self.now < self.started_at:
|
|
234
|
+
raise ValueError("now cannot precede started_at")
|
|
235
|
+
if self.last_progress_at < self.started_at or self.last_progress_at > self.now:
|
|
236
|
+
raise ValueError("last_progress_at must be between started_at and now")
|
|
237
|
+
meaningful = self.meaningful_progress_at()
|
|
238
|
+
if meaningful < self.started_at or meaningful > self.last_progress_at:
|
|
239
|
+
raise ValueError("last_meaningful_progress_at must be between started_at and last_progress_at")
|
|
240
|
+
|
|
241
|
+
for index, record in enumerate(self.checkpoint_sequence, start=1):
|
|
242
|
+
if record.sequence != index:
|
|
243
|
+
raise ValueError("checkpoint sequences must be contiguous starting at 1")
|
|
244
|
+
if record.generation != self.generation:
|
|
245
|
+
raise ValueError("checkpoint generation must equal observation generation")
|
|
246
|
+
record.validate(started_at=self.started_at, now=self.now)
|
|
247
|
+
if index < len(self.checkpoint_sequence) and record.harvested_at is None:
|
|
248
|
+
raise ValueError("every checkpoint except the latest must be harvested")
|
|
249
|
+
if index > 1:
|
|
250
|
+
previous = self.checkpoint_sequence[index - 2]
|
|
251
|
+
if previous.harvested_at is None:
|
|
252
|
+
raise ValueError("a new checkpoint cannot start before the previous one is harvested")
|
|
253
|
+
if record.requested_at < previous.harvested_at:
|
|
254
|
+
raise ValueError("checkpoint requested_at cannot precede previous harvest")
|
|
255
|
+
if self.checkpoint_status() not in CHECKPOINT_STATUSES:
|
|
256
|
+
raise AssertionError("invalid checkpoint status")
|
|
257
|
+
if self.terminal_success and self.terminal_failure:
|
|
258
|
+
raise ValueError("worker cannot be both terminal-success and terminal-failure")
|
|
259
|
+
if self.cancel_confirmed and (self.terminal_success or self.terminal_failure):
|
|
260
|
+
raise ValueError("cancel_confirmed cannot be combined with another terminal state")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass(frozen=True)
|
|
264
|
+
class LifecycleDecision:
|
|
265
|
+
state: str
|
|
266
|
+
action: str
|
|
267
|
+
reason: str
|
|
268
|
+
cancel_required: bool
|
|
269
|
+
replacement_allowed: bool
|
|
270
|
+
fence_required: bool
|
|
271
|
+
idle_seconds: float
|
|
272
|
+
meaningful_idle_seconds: float
|
|
273
|
+
progress_quality: str
|
|
274
|
+
checkpoint_status: str
|
|
275
|
+
replan_scope: str | None
|
|
276
|
+
checkpoint_reuse_mode: str | None
|
|
277
|
+
wall_seconds: float
|
|
278
|
+
fallback_policy: str | None
|
|
279
|
+
checkpoint_generation: int
|
|
280
|
+
checkpoint_sequence: int
|
|
281
|
+
next_checkpoint_sequence: int | None
|
|
282
|
+
harvested_checkpoint_sequence: int
|
|
283
|
+
checkpoint_rearm_at: float | None
|
|
284
|
+
checkpoint_rearm_remaining_seconds: float | None
|
|
285
|
+
|
|
286
|
+
def to_dict(self) -> dict[str, Any]:
|
|
287
|
+
return asdict(self)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _progress_metrics(policy: LifecyclePolicy, observation: WorkerObservation) -> tuple[float, float, float, str]:
|
|
291
|
+
idle = max(0.0, observation.now - observation.last_progress_at)
|
|
292
|
+
wall = max(0.0, observation.now - observation.started_at)
|
|
293
|
+
meaningful_at = observation.meaningful_progress_at()
|
|
294
|
+
meaningful_idle = max(0.0, observation.now - meaningful_at)
|
|
295
|
+
if meaningful_at > observation.started_at and meaningful_idle < policy.idle_timeout_seconds:
|
|
296
|
+
quality = "meaningful"
|
|
297
|
+
elif observation.in_flight or idle < policy.idle_timeout_seconds:
|
|
298
|
+
quality = "activity_only"
|
|
299
|
+
else:
|
|
300
|
+
quality = "none"
|
|
301
|
+
return idle, meaningful_idle, wall, quality
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _replan_contract(observation: WorkerObservation, fallback_policy: str | None) -> tuple[str | None, str | None]:
|
|
305
|
+
if fallback_policy != "replan":
|
|
306
|
+
return None, None
|
|
307
|
+
if observation.latest_harvested_checkpoint() is None:
|
|
308
|
+
return "uncovered_scope", None
|
|
309
|
+
return (
|
|
310
|
+
"checkpoint_remaining_delta",
|
|
311
|
+
"harvested_snapshot_only" if observation.replacement_isolated else "retained_workspace",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _decision(
|
|
316
|
+
policy: LifecyclePolicy,
|
|
317
|
+
observation: WorkerObservation,
|
|
318
|
+
*,
|
|
319
|
+
state: str,
|
|
320
|
+
action: str,
|
|
321
|
+
reason: str,
|
|
322
|
+
cancel_required: bool,
|
|
323
|
+
replacement_allowed: bool,
|
|
324
|
+
fence_required: bool,
|
|
325
|
+
fallback_policy: str | None,
|
|
326
|
+
) -> LifecycleDecision:
|
|
327
|
+
idle, meaningful_idle, wall, quality = _progress_metrics(policy, observation)
|
|
328
|
+
replan_scope, reuse = _replan_contract(observation, fallback_policy)
|
|
329
|
+
latest = observation.latest_checkpoint()
|
|
330
|
+
latest_harvested = observation.latest_harvested_checkpoint()
|
|
331
|
+
latest_sequence = latest.sequence if latest is not None else 0
|
|
332
|
+
if latest_harvested is not None and policy.checkpoint_rearm_seconds is not None:
|
|
333
|
+
rearm_at = latest_harvested.harvested_at + policy.checkpoint_rearm_seconds
|
|
334
|
+
rearm_remaining = max(0.0, rearm_at - observation.now)
|
|
335
|
+
else:
|
|
336
|
+
rearm_at = None
|
|
337
|
+
rearm_remaining = None
|
|
338
|
+
return LifecycleDecision(
|
|
339
|
+
state=state,
|
|
340
|
+
action=action,
|
|
341
|
+
reason=reason,
|
|
342
|
+
cancel_required=cancel_required,
|
|
343
|
+
replacement_allowed=replacement_allowed,
|
|
344
|
+
fence_required=fence_required,
|
|
345
|
+
idle_seconds=idle,
|
|
346
|
+
meaningful_idle_seconds=meaningful_idle,
|
|
347
|
+
progress_quality=quality,
|
|
348
|
+
checkpoint_status=observation.checkpoint_status(),
|
|
349
|
+
replan_scope=replan_scope,
|
|
350
|
+
checkpoint_reuse_mode=reuse,
|
|
351
|
+
wall_seconds=wall,
|
|
352
|
+
fallback_policy=fallback_policy,
|
|
353
|
+
checkpoint_generation=observation.generation,
|
|
354
|
+
checkpoint_sequence=latest_sequence,
|
|
355
|
+
next_checkpoint_sequence=latest_sequence + 1 if action == "request_checkpoint" else None,
|
|
356
|
+
harvested_checkpoint_sequence=latest_harvested.sequence if latest_harvested is not None else 0,
|
|
357
|
+
checkpoint_rearm_at=rearm_at,
|
|
358
|
+
checkpoint_rearm_remaining_seconds=rearm_remaining,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _checkpoint_harvest_decision(policy: LifecyclePolicy, observation: WorkerObservation) -> LifecycleDecision:
|
|
363
|
+
return _decision(
|
|
364
|
+
policy,
|
|
365
|
+
observation,
|
|
366
|
+
state="progressing" if observation.last_progress_at > observation.started_at or observation.in_flight else "running",
|
|
367
|
+
action="harvest_checkpoint",
|
|
368
|
+
reason="worker checkpoint is available; harvest durable work before any fallback",
|
|
369
|
+
cancel_required=False,
|
|
370
|
+
replacement_allowed=False,
|
|
371
|
+
fence_required=False,
|
|
372
|
+
fallback_policy=None,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _fallback_decision(
|
|
377
|
+
policy: LifecyclePolicy,
|
|
378
|
+
observation: WorkerObservation,
|
|
379
|
+
*,
|
|
380
|
+
state: str,
|
|
381
|
+
reason: str,
|
|
382
|
+
terminal: bool,
|
|
383
|
+
) -> LifecycleDecision:
|
|
384
|
+
if policy.fallback_policy == "retry_review" and observation.stage != "review":
|
|
385
|
+
raise ValueError("retry_review fallback is only valid for review stage")
|
|
386
|
+
fallback = FALLBACK_ACTIONS[policy.fallback_policy]
|
|
387
|
+
cancel_required = not terminal
|
|
388
|
+
fallback_creates_writer = policy.fallback_policy in WRITER_FALLBACKS
|
|
389
|
+
fence_required = observation.stage == "implementation" and observation.writable and fallback_creates_writer
|
|
390
|
+
|
|
391
|
+
if policy.fallback_policy == "retry_review":
|
|
392
|
+
action = "retry_review"
|
|
393
|
+
replacement_allowed = True
|
|
394
|
+
reason += "; replacement is read-only and must consume a review_attempt reservation"
|
|
395
|
+
elif fence_required and not terminal and not observation.replacement_isolated:
|
|
396
|
+
action = "request_cancel"
|
|
397
|
+
replacement_allowed = False
|
|
398
|
+
else:
|
|
399
|
+
action = fallback
|
|
400
|
+
replacement_allowed = fallback == "replan" and (not fence_required or terminal or observation.replacement_isolated)
|
|
401
|
+
if fence_required and observation.replacement_isolated and not terminal:
|
|
402
|
+
reason += "; downstream writer is isolated and old output is fenced"
|
|
403
|
+
|
|
404
|
+
if policy.fallback_policy == "replan" and observation.latest_harvested_checkpoint() is not None:
|
|
405
|
+
reason += "; replan is restricted to harvested remaining_delta"
|
|
406
|
+
if cancel_required:
|
|
407
|
+
reason += "; non-terminal Worker cancellation is required"
|
|
408
|
+
return _decision(
|
|
409
|
+
policy,
|
|
410
|
+
observation,
|
|
411
|
+
state=state,
|
|
412
|
+
action=action,
|
|
413
|
+
reason=reason,
|
|
414
|
+
cancel_required=cancel_required,
|
|
415
|
+
replacement_allowed=replacement_allowed,
|
|
416
|
+
fence_required=fence_required,
|
|
417
|
+
fallback_policy=policy.fallback_policy,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _superseded_decision(policy: LifecyclePolicy, observation: WorkerObservation) -> LifecycleDecision:
|
|
422
|
+
terminal = observation.terminal_failure or observation.cancel_confirmed
|
|
423
|
+
return _decision(
|
|
424
|
+
policy,
|
|
425
|
+
observation,
|
|
426
|
+
state="superseded",
|
|
427
|
+
action="continue" if terminal else "request_cancel",
|
|
428
|
+
reason="scope is already covered; no fallback work is required",
|
|
429
|
+
cancel_required=not terminal,
|
|
430
|
+
replacement_allowed=False,
|
|
431
|
+
fence_required=False,
|
|
432
|
+
fallback_policy=None,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _soft_budget_decision(policy: LifecyclePolicy, observation: WorkerObservation) -> LifecycleDecision:
|
|
437
|
+
_idle, _meaningful_idle, _wall, quality = _progress_metrics(policy, observation)
|
|
438
|
+
state = "progressing" if observation.last_progress_at > observation.started_at or observation.in_flight else "running"
|
|
439
|
+
checkpoint_status = observation.checkpoint_status()
|
|
440
|
+
if checkpoint_status == "not_requested":
|
|
441
|
+
action, suffix = "request_checkpoint", "request a non-terminal checkpoint"
|
|
442
|
+
elif checkpoint_status == "requested":
|
|
443
|
+
action, suffix = "await_checkpoint", "checkpoint is already requested"
|
|
444
|
+
elif checkpoint_status == "received":
|
|
445
|
+
return _checkpoint_harvest_decision(policy, observation)
|
|
446
|
+
else:
|
|
447
|
+
latest = observation.latest_harvested_checkpoint()
|
|
448
|
+
assert latest is not None
|
|
449
|
+
rearm_at = latest.harvested_at + policy.checkpoint_rearm_seconds
|
|
450
|
+
explicit = observation.last_meaningful_progress_at
|
|
451
|
+
has_delta = explicit is not None and explicit > latest.harvested_at
|
|
452
|
+
cooldown = observation.now >= rearm_at
|
|
453
|
+
if has_delta and cooldown:
|
|
454
|
+
action, suffix = "request_checkpoint", "explicit meaningful progress and cooldown re-armed checkpointing"
|
|
455
|
+
elif explicit is None:
|
|
456
|
+
action, suffix = "continue", "last_progress_at cannot re-arm checkpointing; explicit meaningful progress is required"
|
|
457
|
+
elif not has_delta:
|
|
458
|
+
action, suffix = "continue", "harvested checkpoint remains current; no new meaningful delta"
|
|
459
|
+
else:
|
|
460
|
+
action, suffix = "continue", "harvested checkpoint remains current; checkpoint rearm cooldown has not elapsed"
|
|
461
|
+
return _decision(
|
|
462
|
+
policy,
|
|
463
|
+
observation,
|
|
464
|
+
state=state,
|
|
465
|
+
action=action,
|
|
466
|
+
reason=f"soft worker execution budget reached; progress={quality}; {suffix}",
|
|
467
|
+
cancel_required=False,
|
|
468
|
+
replacement_allowed=False,
|
|
469
|
+
fence_required=False,
|
|
470
|
+
fallback_policy=None,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def evaluate_worker(policy: LifecyclePolicy, observation: WorkerObservation) -> LifecycleDecision:
|
|
475
|
+
policy.validate()
|
|
476
|
+
observation.validate()
|
|
477
|
+
if observation.stage == "implementation":
|
|
478
|
+
if policy.soft_timeout_seconds is None:
|
|
479
|
+
raise ValueError("implementation lifecycle requires soft_timeout_seconds")
|
|
480
|
+
if policy.checkpoint_rearm_seconds is None:
|
|
481
|
+
raise ValueError("implementation lifecycle requires checkpoint_rearm_seconds")
|
|
482
|
+
idle, _meaningful_idle, wall, _quality = _progress_metrics(policy, observation)
|
|
483
|
+
|
|
484
|
+
if observation.checkpoint_status() == "received":
|
|
485
|
+
return _checkpoint_harvest_decision(policy, observation)
|
|
486
|
+
if observation.terminal_success:
|
|
487
|
+
return _decision(
|
|
488
|
+
policy,
|
|
489
|
+
observation,
|
|
490
|
+
state="completed",
|
|
491
|
+
action="consume_result",
|
|
492
|
+
reason="worker reported terminal success",
|
|
493
|
+
cancel_required=False,
|
|
494
|
+
replacement_allowed=False,
|
|
495
|
+
fence_required=False,
|
|
496
|
+
fallback_policy=None,
|
|
497
|
+
)
|
|
498
|
+
if observation.scope_superseded and policy.cancel_if_superseded:
|
|
499
|
+
return _superseded_decision(policy, observation)
|
|
500
|
+
if observation.terminal_failure:
|
|
501
|
+
return _fallback_decision(policy, observation, state="failed", reason="worker reported terminal failure", terminal=True)
|
|
502
|
+
if observation.cancel_confirmed:
|
|
503
|
+
return _fallback_decision(policy, observation, state="cancelled", reason="worker cancellation/termination confirmed", terminal=True)
|
|
504
|
+
if wall >= policy.hard_timeout_seconds:
|
|
505
|
+
return _fallback_decision(policy, observation, state="stalled", reason="hard worker wall-clock ceiling reached", terminal=False)
|
|
506
|
+
if not observation.in_flight and idle >= policy.idle_timeout_seconds:
|
|
507
|
+
return _fallback_decision(policy, observation, state="stalled", reason="idle progress lease expired with no visible in-flight work", terminal=False)
|
|
508
|
+
if policy.soft_timeout_seconds is not None and wall >= policy.soft_timeout_seconds:
|
|
509
|
+
return _soft_budget_decision(policy, observation)
|
|
510
|
+
return _decision(
|
|
511
|
+
policy,
|
|
512
|
+
observation,
|
|
513
|
+
state="progressing" if observation.last_progress_at > observation.started_at or observation.in_flight else "running",
|
|
514
|
+
action="continue",
|
|
515
|
+
reason="worker remains non-terminal with an active liveness lease",
|
|
516
|
+
cancel_required=False,
|
|
517
|
+
replacement_allowed=False,
|
|
518
|
+
fence_required=False,
|
|
519
|
+
fallback_policy=None,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _load_json_object(raw: str, label: str) -> dict[str, Any]:
|
|
524
|
+
try:
|
|
525
|
+
value = json.loads(raw)
|
|
526
|
+
except json.JSONDecodeError as exc:
|
|
527
|
+
raise ValueError(f"invalid {label} JSON: {exc}") from exc
|
|
528
|
+
if not isinstance(value, dict):
|
|
529
|
+
raise ValueError(f"{label} must be a JSON object")
|
|
530
|
+
return value
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _load_checkpoint_sequence(raw: str) -> tuple[CheckpointRecord, ...]:
|
|
534
|
+
try:
|
|
535
|
+
value = json.loads(raw)
|
|
536
|
+
except json.JSONDecodeError as exc:
|
|
537
|
+
raise ValueError(f"invalid checkpoint sequence JSON: {exc}") from exc
|
|
538
|
+
if type(value) is not list:
|
|
539
|
+
raise ValueError("checkpoint sequence must be a JSON array")
|
|
540
|
+
return tuple(CheckpointRecord.from_dict(item) for item in value)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
544
|
+
parser = argparse.ArgumentParser(prog="worker-lifecycle")
|
|
545
|
+
parser.add_argument("--policy-json", required=True)
|
|
546
|
+
parser.add_argument("--scope-id", required=True)
|
|
547
|
+
parser.add_argument("--stage", choices=("exploration", "implementation", "review"), required=True)
|
|
548
|
+
parser.add_argument("--started-at", type=float, required=True)
|
|
549
|
+
parser.add_argument("--last-progress-at", type=float, required=True)
|
|
550
|
+
parser.add_argument("--last-meaningful-progress-at", type=float)
|
|
551
|
+
parser.add_argument("--now", type=float, required=True)
|
|
552
|
+
parser.add_argument("--writable", action="store_true")
|
|
553
|
+
parser.add_argument("--in-flight", action="store_true")
|
|
554
|
+
parser.add_argument("--terminal-success", action="store_true")
|
|
555
|
+
parser.add_argument("--terminal-failure", action="store_true")
|
|
556
|
+
parser.add_argument("--scope-superseded", action="store_true")
|
|
557
|
+
parser.add_argument("--cancel-confirmed", action="store_true")
|
|
558
|
+
parser.add_argument("--replacement-isolated", action="store_true")
|
|
559
|
+
parser.add_argument("--generation", type=int, default=0)
|
|
560
|
+
parser.add_argument("--checkpoint-sequence-json")
|
|
561
|
+
return parser
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def main(argv: Iterable[str] | None = None) -> int:
|
|
565
|
+
ns = build_parser().parse_args(list(argv) if argv is not None else None)
|
|
566
|
+
policy = LifecyclePolicy.from_dict(_load_json_object(ns.policy_json, "policy"))
|
|
567
|
+
sequence = () if ns.checkpoint_sequence_json is None else _load_checkpoint_sequence(ns.checkpoint_sequence_json)
|
|
568
|
+
observation = WorkerObservation(
|
|
569
|
+
scope_id=ns.scope_id,
|
|
570
|
+
stage=ns.stage,
|
|
571
|
+
started_at=ns.started_at,
|
|
572
|
+
last_progress_at=ns.last_progress_at,
|
|
573
|
+
now=ns.now,
|
|
574
|
+
last_meaningful_progress_at=ns.last_meaningful_progress_at,
|
|
575
|
+
writable=ns.writable,
|
|
576
|
+
in_flight=ns.in_flight,
|
|
577
|
+
terminal_success=ns.terminal_success,
|
|
578
|
+
terminal_failure=ns.terminal_failure,
|
|
579
|
+
scope_superseded=ns.scope_superseded,
|
|
580
|
+
cancel_confirmed=ns.cancel_confirmed,
|
|
581
|
+
replacement_isolated=ns.replacement_isolated,
|
|
582
|
+
generation=ns.generation,
|
|
583
|
+
checkpoint_sequence=sequence,
|
|
584
|
+
)
|
|
585
|
+
print(json.dumps(evaluate_worker(policy, observation).to_dict(), ensure_ascii=False, sort_keys=True))
|
|
586
|
+
return 0
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
if __name__ == "__main__":
|
|
590
|
+
raise SystemExit(main())
|