dos-kernel 0.22.0__py3-none-win_amd64.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.
- dos/__init__.py +261 -0
- dos/_bin/dos-hook.exe +0 -0
- dos/_filelock.py +255 -0
- dos/_job_policy.py +97 -0
- dos/_tree.py +145 -0
- dos/admission.py +433 -0
- dos/answer_shape.py +299 -0
- dos/arbiter.py +859 -0
- dos/archive_lock.py +266 -0
- dos/arg_provenance.py +814 -0
- dos/attest.py +472 -0
- dos/breaker.py +311 -0
- dos/churn.py +226 -0
- dos/claim_extract.py +229 -0
- dos/claim_ttl.py +150 -0
- dos/cli.py +8721 -0
- dos/commit_audit.py +666 -0
- dos/completion.py +466 -0
- dos/concurrency_class.py +154 -0
- dos/config.py +1380 -0
- dos/config_lint.py +464 -0
- dos/cooldown.py +390 -0
- dos/coverage.py +387 -0
- dos/dangling_intent.py +287 -0
- dos/data_class.py +397 -0
- dos/decisions.py +1274 -0
- dos/decisions_tui.py +251 -0
- dos/dispatch_top.py +740 -0
- dos/dispatch_top_tui.py +116 -0
- dos/drivers/__init__.py +40 -0
- dos/drivers/ci_status.py +630 -0
- dos/drivers/citation_resolve.py +703 -0
- dos/drivers/decision_stop.py +98 -0
- dos/drivers/export_file.py +173 -0
- dos/drivers/export_otlp.py +275 -0
- dos/drivers/export_statsd.py +242 -0
- dos/drivers/hook_dialects.py +391 -0
- dos/drivers/job.py +47 -0
- dos/drivers/llm_judge.py +360 -0
- dos/drivers/memory_recall.py +1231 -0
- dos/drivers/notify_slack.py +373 -0
- dos/drivers/notify_webhook.py +251 -0
- dos/drivers/operator_judge.py +114 -0
- dos/drivers/os_acceptance.py +228 -0
- dos/drivers/paste_log.py +132 -0
- dos/drivers/plan_scope.py +133 -0
- dos/drivers/self_improve.py +375 -0
- dos/drivers/similarity_judge.py +249 -0
- dos/drivers/state_diff.py +274 -0
- dos/drivers/supervisor.py +347 -0
- dos/drivers/watchdog.py +363 -0
- dos/drivers/workshop.py +160 -0
- dos/durable_schema.py +344 -0
- dos/effect_witness.py +393 -0
- dos/efficiency.py +318 -0
- dos/enforce.py +414 -0
- dos/enumerate.py +776 -0
- dos/env_print.py +378 -0
- dos/event_severity.py +258 -0
- dos/evidence.py +692 -0
- dos/exec_capability.py +256 -0
- dos/export_cursor.py +143 -0
- dos/exporter.py +320 -0
- dos/firing_label.py +353 -0
- dos/fleet_roll.py +226 -0
- dos/gate_classify.py +827 -0
- dos/gh4_coverage.py +179 -0
- dos/git_delta.py +122 -0
- dos/guard.py +215 -0
- dos/health.py +552 -0
- dos/help_summary.py +519 -0
- dos/home.py +934 -0
- dos/hook_binary.py +194 -0
- dos/hook_dialect.py +271 -0
- dos/hook_exit.py +191 -0
- dos/hook_install.py +437 -0
- dos/id_alloc.py +304 -0
- dos/improve.py +499 -0
- dos/intent_ledger.py +635 -0
- dos/interpret.py +176 -0
- dos/intervention.py +769 -0
- dos/intervention_eval.py +371 -0
- dos/journal_delta.py +308 -0
- dos/judge_eval.py +328 -0
- dos/judges.py +366 -0
- dos/lane_infer.py +127 -0
- dos/lane_journal.py +1001 -0
- dos/lane_lease.py +952 -0
- dos/lane_overlap.py +228 -0
- dos/lease_health.py +282 -0
- dos/lifecycle.py +211 -0
- dos/liveness.py +352 -0
- dos/lock_modes.py +185 -0
- dos/log_source.py +395 -0
- dos/loop_decide.py +1746 -0
- dos/marker_gate.py +254 -0
- dos/marker_sensor.py +396 -0
- dos/noop_streak.py +280 -0
- dos/notify.py +479 -0
- dos/observe.py +175 -0
- dos/oracle.py +1661 -0
- dos/overlap_eval.py +214 -0
- dos/overlap_policy.py +342 -0
- dos/packet_sidecar.py +267 -0
- dos/phase_shipped.py +1985 -0
- dos/pick_priority.py +225 -0
- dos/pickable.py +369 -0
- dos/picker_oracle.py +1037 -0
- dos/plan_board.py +513 -0
- dos/plan_board_tui.py +113 -0
- dos/plan_source.py +455 -0
- dos/posttool_sensor.py +528 -0
- dos/precursor_gate.py +499 -0
- dos/precursor_gate_eval.py +239 -0
- dos/preflight.py +825 -0
- dos/pretool_sensor.py +490 -0
- dos/proc_delta.py +181 -0
- dos/productivity.py +296 -0
- dos/provider_limit.py +242 -0
- dos/py.typed +4 -0
- dos/reason_morphology.py +299 -0
- dos/reasons.py +449 -0
- dos/reconcile.py +173 -0
- dos/recurring_wedge.py +206 -0
- dos/render.py +393 -0
- dos/result_state.py +468 -0
- dos/resume.py +578 -0
- dos/resume_evidence.py +293 -0
- dos/retention.py +344 -0
- dos/reward.py +372 -0
- dos/rewind.py +587 -0
- dos/rewind_evidence.py +168 -0
- dos/rewind_tokens.py +252 -0
- dos/run_id.py +342 -0
- dos/scope.py +520 -0
- dos/scope_source.py +382 -0
- dos/scout.py +982 -0
- dos/self_modify.py +209 -0
- dos/sibling_scan.py +569 -0
- dos/skills/EXAMPLES.md +584 -0
- dos/skills/dos-class-cycle/SKILL.md +107 -0
- dos/skills/dos-dispatch/SKILL.md +177 -0
- dos/skills/dos-dispatch-loop/SKILL.md +254 -0
- dos/skills/dos-goal-gate/SKILL.md +269 -0
- dos/skills/dos-next-up/SKILL.md +231 -0
- dos/skills/dos-promote/SKILL.md +114 -0
- dos/skills/dos-replan/SKILL.md +159 -0
- dos/skills/dos-replan-loop/SKILL.md +114 -0
- dos/skills/dos-self-improve/SKILL.md +213 -0
- dos/skills/dos-supervise-loop/SKILL.md +180 -0
- dos/skills/dos-unstick/SKILL.md +108 -0
- dos/skills/dos-witness-claim/SKILL.md +251 -0
- dos/stamp.py +1002 -0
- dos/state_health.py +387 -0
- dos/status.py +114 -0
- dos/stop_policy.py +334 -0
- dos/supervise.py +1014 -0
- dos/testwitness.py +392 -0
- dos/timeline.py +1027 -0
- dos/tokens.py +485 -0
- dos/tool_stream.py +393 -0
- dos/tool_stream_eval.py +226 -0
- dos/trace.py +524 -0
- dos/verdict.py +140 -0
- dos/verdict_cli.py +189 -0
- dos/verdict_journal.py +497 -0
- dos/verdict_rollup.py +217 -0
- dos/verdicts.py +181 -0
- dos/wedge_reason.py +282 -0
- dos_kernel-0.22.0.dist-info/METADATA +859 -0
- dos_kernel-0.22.0.dist-info/RECORD +178 -0
- dos_kernel-0.22.0.dist-info/WHEEL +5 -0
- dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
- dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
- dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
- dos_mcp/__init__.py +52 -0
- dos_mcp/py.typed +2 -0
- dos_mcp/server.py +779 -0
dos/intent_ledger.py
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"""The intent ledger — a third durable surface for *declared intent + adjudicated progress* (docs/107 §3).
|
|
2
|
+
|
|
3
|
+
> **The WAL answers "what was decided about leases." The intent ledger answers
|
|
4
|
+
> "what was the run trying to accomplish, and how far did the *evidence* say it
|
|
5
|
+
> got." The first is the kernel believing only effects; the second is the kernel
|
|
6
|
+
> storing a self-report so it can later distrust it against the fossils.**
|
|
7
|
+
|
|
8
|
+
DOS already records what a run **decided** (`lane_journal`: leases taken, dropped,
|
|
9
|
+
evicted) and what it **committed** (git ancestry). It records nothing about what a
|
|
10
|
+
run was **trying to do** and **how far it got** on the part that isn't a commit
|
|
11
|
+
yet — so when a run crashes mid-flight, a successor cannot *continue* it (only
|
|
12
|
+
`SCAVENGE` its lane). This module is the missing log: an append-only, `fsync`'d,
|
|
13
|
+
replay-foldable record of **declared intent** and **progress beats against it**,
|
|
14
|
+
one per run, keyed by `run_id` from birth.
|
|
15
|
+
|
|
16
|
+
It is `lane_journal`'s sibling, deliberately byte-mirroring its ARIES discipline
|
|
17
|
+
(`docs/107 §3`):
|
|
18
|
+
|
|
19
|
+
* **Same shape, different subject.** `append`/`read_all`/`replay`/`compact`,
|
|
20
|
+
`fsync` on write, torn-tail tolerant (`read_all` skips ONLY the torn final
|
|
21
|
+
line, keeps a mid-file `_CORRUPT` sentinel). Where `lane_journal` folds to a
|
|
22
|
+
*live-lease set*, this folds to a *declared-intent + verified-step set* (the
|
|
23
|
+
`LedgerState`) the pure `resume.resume_plan` consumes.
|
|
24
|
+
* **Different key, which closes a gap as a side effect.** Keyed by `run_id`
|
|
25
|
+
(the ledger lives at ``.dos/runs/<run_id>/intent.jsonl``, next to `run.json`),
|
|
26
|
+
NOT by `(loop_ts, lane)`. So the `(loop_ts,lane)→run_id` join `94 §7` /
|
|
27
|
+
`journal_delta`'s "HARD PROBLEM" flagged is *sidestepped* — the resumption
|
|
28
|
+
data is in a log keyed by `run_id` from the start. The WAL stays exactly what
|
|
29
|
+
it is (lease correctness); this carries the run-scoped progress it was never
|
|
30
|
+
meant to hold.
|
|
31
|
+
|
|
32
|
+
**The epistemic spine** (`docs/107 §3.2`, the `103`/`102` move): the asymmetry
|
|
33
|
+
between `STEP_CLAIMED` (the agent's say-so — *content*, distrusted) and
|
|
34
|
+
`STEP_VERIFIED` (a minted belief over git ancestry — *structure*, trusted). Replay
|
|
35
|
+
reads `STEP_VERIFIED`s as done and treats every `STEP_CLAIMED` without a matching
|
|
36
|
+
`STEP_VERIFIED` as **not done** — fail-closed. This module holds the *vocabulary,
|
|
37
|
+
the writers, and the pure replay fold*; the `STEP_VERIFIED` MINT (re-verifying a
|
|
38
|
+
claimed SHA against ancestry on the non-forgeable rung) is a CLI-boundary helper
|
|
39
|
+
(it does git I/O), exactly as `liveness`'s evidence-gather is a boundary, not the
|
|
40
|
+
pure verdict. The pure verdict over a `LedgerState` lives in `dos.resume`.
|
|
41
|
+
|
|
42
|
+
Write is library-only and happens UNDER the run's own writer (the dispatch loop /
|
|
43
|
+
a driver), each run owning its own file — there is no cross-run contention, so
|
|
44
|
+
unlike the WAL there is no shared mutex to hold (one writer per `run_id`). `O_APPEND`
|
|
45
|
+
+ `fsync` is the durability floor.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import datetime as dt
|
|
51
|
+
import json
|
|
52
|
+
import os
|
|
53
|
+
import sys
|
|
54
|
+
from dataclasses import dataclass, field
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import Iterable, Mapping
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
60
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
from dos import config as _config
|
|
65
|
+
from dos import durable_schema as _schema
|
|
66
|
+
|
|
67
|
+
# The durable-schema family + version every intent-ledger record carries (§6).
|
|
68
|
+
# Bumped ONLY on a NON-additive shape change; a new op or a new optional field is
|
|
69
|
+
# additive and does NOT bump it (the `durable_schema` contract). This kernel
|
|
70
|
+
# UNDERSTANDS up to `INTENT_LEDGER_SCHEMA` — a record tagged higher is REFUSED at
|
|
71
|
+
# read time (`read_all`'s schema gate), never guessed.
|
|
72
|
+
SCHEMA_FAMILY = "intent-ledger"
|
|
73
|
+
INTENT_LEDGER_SCHEMA = 1
|
|
74
|
+
|
|
75
|
+
INTENT_JSONL_NAME = "intent.jsonl"
|
|
76
|
+
|
|
77
|
+
# The closed op vocabulary (§3.2). Additive: a future op a newer writer emits is
|
|
78
|
+
# SKIPPED by an older `replay` (it acts only on the ops it knows), the same
|
|
79
|
+
# forward-compat the lane-journal `_STATE_MUTATING_OPS` gate gives — so adding an
|
|
80
|
+
# op never bumps the schema version.
|
|
81
|
+
OP_INTENT = "INTENT" # a run declares its goal (at spawn / first dispatch)
|
|
82
|
+
OP_STEP_CLAIMED = "STEP_CLAIMED" # the agent SAYS it finished a unit of work (forgeable)
|
|
83
|
+
OP_STEP_VERIFIED = "STEP_VERIFIED" # the kernel CONFIRMED a claimed step against ancestry
|
|
84
|
+
OP_SUSPEND = "SUSPEND" # a run voluntarily yields (pause; §4)
|
|
85
|
+
OP_RESUME_PROPOSED = "RESUME_PROPOSED" # a successor minted a resume point + proposed continuation
|
|
86
|
+
OP_CORRUPT = "_CORRUPT" # replay hit an unparseable non-trailing line (sentinel)
|
|
87
|
+
|
|
88
|
+
# The ops `replay` folds into the LedgerState. `_CORRUPT` and any unknown op are
|
|
89
|
+
# recorded-but-not-folded (the lane-journal `_STATE_MUTATING_OPS` posture): a
|
|
90
|
+
# torn/foreign line must never silently mutate the reconstructed intent.
|
|
91
|
+
_FOLDED_OPS = frozenset(
|
|
92
|
+
{OP_INTENT, OP_STEP_CLAIMED, OP_STEP_VERIFIED, OP_SUSPEND, OP_RESUME_PROPOSED}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def ledger_now_iso() -> str:
|
|
97
|
+
"""Second-resolution UTC stamp for ledger entries (the `lane_journal` idiom)."""
|
|
98
|
+
return dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --------------------------------------------------------------------------
|
|
102
|
+
# Where it lives — the run-dir the spine already creates, keyed by run_id.
|
|
103
|
+
# --------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run_dir_for(run_id: str, *, cfg: "_config.SubstrateConfig | None" = None) -> Path:
|
|
107
|
+
"""The run-dir ``<runs>/<run_id>/`` for ``run_id`` under the active workspace.
|
|
108
|
+
|
|
109
|
+
Rides the layout's run-dir tree (`paths.fanout_runs`, which is `.dos/runs`
|
|
110
|
+
under the generic layout — the same tree the spine stamps `run.json` into).
|
|
111
|
+
Keyed by the run-id token itself, NOT a UTC-timestamp dir name: the ledger is
|
|
112
|
+
correlated-by-construction with the spine (`docs/107 §3.1`). Pure path
|
|
113
|
+
arithmetic — never creates the dir (a read-only caller must be able to ASK for
|
|
114
|
+
the path without a write; `append` is the only creator).
|
|
115
|
+
"""
|
|
116
|
+
cfg = _config.ensure(cfg)
|
|
117
|
+
return cfg.paths.fanout_runs / run_id
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def ledger_path_for(run_id: str, *, cfg: "_config.SubstrateConfig | None" = None) -> Path:
|
|
121
|
+
"""The ``intent.jsonl`` path for ``run_id`` (next to its ``run.json``)."""
|
|
122
|
+
return run_dir_for(run_id, cfg=cfg) / INTENT_JSONL_NAME
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# --------------------------------------------------------------------------
|
|
126
|
+
# I/O — append (fsync, library-only) + read_all (torn-tail + schema gate).
|
|
127
|
+
# --------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def append(run_id: str, entry: dict, *, path: Path | None = None,
|
|
131
|
+
cfg: "_config.SubstrateConfig | None" = None) -> dict:
|
|
132
|
+
"""Append one entry to ``run_id``'s intent ledger and `fsync` it. Returns the stamped entry.
|
|
133
|
+
|
|
134
|
+
Stamps `run_id` (the key — always THIS run's), `ts` (if absent), and the §6
|
|
135
|
+
`schema` tag (if the builder didn't already), then writes one canonical-JSON
|
|
136
|
+
line + newline, `flush` + `os.fsync` so the record is durable before the
|
|
137
|
+
function returns (log-before-act, the WAL invariant). `O_APPEND` makes the
|
|
138
|
+
write atomic w.r.t. any other appender at the OS level; one writer per `run_id`
|
|
139
|
+
means there is no cross-run mutex to hold (unlike the shared WAL).
|
|
140
|
+
|
|
141
|
+
The entry shape is the caller's decision payload (use the `*_entry` builders);
|
|
142
|
+
this only fills the universal fields. `path` overrides the resolved run-dir
|
|
143
|
+
location (tests / a driver writing elsewhere).
|
|
144
|
+
"""
|
|
145
|
+
p = path or ledger_path_for(run_id, cfg=cfg)
|
|
146
|
+
e = dict(entry)
|
|
147
|
+
e["run_id"] = run_id # the key is authoritative — always this run's
|
|
148
|
+
e.setdefault("ts", ledger_now_iso())
|
|
149
|
+
if _schema.SCHEMA_KEY not in e:
|
|
150
|
+
e.update(_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA))
|
|
151
|
+
line = json.dumps(e, sort_keys=True, default=str, ensure_ascii=False) + "\n"
|
|
152
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
fd = os.open(str(p), os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o644)
|
|
154
|
+
try:
|
|
155
|
+
os.write(fd, line.encode("utf-8"))
|
|
156
|
+
os.fsync(fd)
|
|
157
|
+
finally:
|
|
158
|
+
os.close(fd)
|
|
159
|
+
return e
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def read_all(run_id: str | None = None, *, path: Path | None = None,
|
|
163
|
+
cfg: "_config.SubstrateConfig | None" = None,
|
|
164
|
+
understands: int = INTENT_LEDGER_SCHEMA) -> list[dict]:
|
|
165
|
+
"""Return every ledger entry for ``run_id`` in append order, schema-gated.
|
|
166
|
+
|
|
167
|
+
Two distrust postures layered (the §6 floor on top of the ARIES floor):
|
|
168
|
+
|
|
169
|
+
* **Torn-tail tolerance** (the `lane_journal.read_all` contract): an
|
|
170
|
+
unparseable TRAILING line (a crash mid-`append`) is skipped — a
|
|
171
|
+
half-written record is "didn't happen", the safe WAL read. A non-trailing
|
|
172
|
+
unparseable line is a real integrity breach, kept as an `_CORRUPT`
|
|
173
|
+
sentinel so an audit/replay still flags it.
|
|
174
|
+
* **Schema gate** (§6, the refuse-don't-guess floor): a parseable record
|
|
175
|
+
whose `schema` tag is a NON-additively-newer version than `understands` is
|
|
176
|
+
NOT returned as data — it is replaced by an `_CORRUPT`-style
|
|
177
|
+
`_UNREADABLE` sentinel carrying the readability verdict, so `replay`/the
|
|
178
|
+
fold treat it as un-foldable rather than best-effort-parsing a shape this
|
|
179
|
+
kernel does not know. An UNTAGGED (legacy/pre-tag) record is treated
|
|
180
|
+
permissively as readable (the family's implicit v1) — the tolerant-fold
|
|
181
|
+
side of the `durable_schema.UNTAGGED` contract. A WRONG_FAMILY record (a
|
|
182
|
+
foreign line in the file) is likewise kept as an `_UNREADABLE` sentinel.
|
|
183
|
+
|
|
184
|
+
Pass `run_id` to resolve the run-dir path, or `path` to read a specific file
|
|
185
|
+
(tests). `understands` is the reader's schema ceiling (defaults to this
|
|
186
|
+
kernel's) — injectable so a test can simulate an OLD reader meeting a NEW
|
|
187
|
+
record.
|
|
188
|
+
"""
|
|
189
|
+
p = path or (ledger_path_for(run_id, cfg=cfg) if run_id else None)
|
|
190
|
+
if p is None:
|
|
191
|
+
raise ValueError("read_all needs a run_id or an explicit path")
|
|
192
|
+
if not p.exists():
|
|
193
|
+
return []
|
|
194
|
+
try:
|
|
195
|
+
raw = p.read_text(encoding="utf-8", errors="replace")
|
|
196
|
+
except OSError:
|
|
197
|
+
return []
|
|
198
|
+
lines = raw.splitlines()
|
|
199
|
+
out: list[dict] = []
|
|
200
|
+
for i, line in enumerate(lines):
|
|
201
|
+
s = line.strip()
|
|
202
|
+
if not s:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
obj = json.loads(s)
|
|
206
|
+
except json.JSONDecodeError:
|
|
207
|
+
# Torn final line → "didn't happen"; a mid-file corrupt line → sentinel.
|
|
208
|
+
if i == len(lines) - 1:
|
|
209
|
+
break
|
|
210
|
+
out.append({"op": OP_CORRUPT, "_raw": s, "_line": i})
|
|
211
|
+
continue
|
|
212
|
+
if not isinstance(obj, dict):
|
|
213
|
+
continue
|
|
214
|
+
# The §6 schema gate. UNTAGGED/READABLE proceed; UNREADABLE_NEWER and
|
|
215
|
+
# WRONG_FAMILY become an un-foldable sentinel that records WHY (so a
|
|
216
|
+
# surfaced resume verdict can say "v3 record, kernel reads ≤ v1 — migrate").
|
|
217
|
+
v = _schema.classify(obj, family=SCHEMA_FAMILY, understands=understands)
|
|
218
|
+
if v.readability in (_schema.Readability.READABLE, _schema.Readability.UNTAGGED):
|
|
219
|
+
out.append(obj)
|
|
220
|
+
else:
|
|
221
|
+
out.append({
|
|
222
|
+
"op": OP_CORRUPT,
|
|
223
|
+
"_unreadable": v.to_dict(),
|
|
224
|
+
"_raw": s,
|
|
225
|
+
"_line": i,
|
|
226
|
+
})
|
|
227
|
+
return out
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# --------------------------------------------------------------------------
|
|
231
|
+
# The replay fold → LedgerState (pure; the resume verdict's evidence shape).
|
|
232
|
+
# --------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True)
|
|
236
|
+
class SuspendCheckpoint:
|
|
237
|
+
"""The CONVERSATION rewind anchor a SUSPEND record may carry (docs/164 F1.5).
|
|
238
|
+
|
|
239
|
+
Defined HERE — beside the SUSPEND record it serializes onto — because it is a
|
|
240
|
+
durable-record shape, the sibling of the git-axis `suspend_resume_sha`. The SAME
|
|
241
|
+
SUSPEND record carries both: `resume_sha` (the git re-entry point, read by
|
|
242
|
+
`resume.resume_plan`) and this `(turn_index, transcript_digest)` (the
|
|
243
|
+
conversation re-entry checkpoint, read by `rewind.rewind_plan`). `rewind` imports
|
|
244
|
+
this type from here — the one-way layering arrow (`rewind` → `intent_ledger`),
|
|
245
|
+
never the reverse.
|
|
246
|
+
|
|
247
|
+
`transcript_digest` is the kernel's hash of the transcript-up-to-`turn_index` at
|
|
248
|
+
suspend time — the NON-FORGEABLE anchor (its byte-author is the kernel's hash, not
|
|
249
|
+
the judged agent). The conversation rewind is valid ONLY if the live turn at
|
|
250
|
+
`turn_index` still digests to it (`rewind` enforces this — else UNANCHORED).
|
|
251
|
+
|
|
252
|
+
`present=False` is the honest zero: a SUSPEND from a kernel too old to stamp a
|
|
253
|
+
checkpoint (the additive-evolution case) folds back to an absent checkpoint, never
|
|
254
|
+
a guessed one. `from_record` builds it from a folded SUSPEND dict, tolerating the
|
|
255
|
+
missing fields.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
turn_index: int = -1
|
|
259
|
+
transcript_digest: str = ""
|
|
260
|
+
present: bool = False
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def absent(cls) -> "SuspendCheckpoint":
|
|
264
|
+
"""The honest zero — no minted checkpoint (an older kernel's SUSPEND)."""
|
|
265
|
+
return cls(turn_index=-1, transcript_digest="", present=False)
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def from_record(cls, entry: "Mapping | dict") -> "SuspendCheckpoint":
|
|
269
|
+
"""Build from a SUSPEND record's additive fields, tolerating their absence.
|
|
270
|
+
|
|
271
|
+
A SUSPEND with no `transcript_digest` (an older kernel, or a git-only suspend)
|
|
272
|
+
folds to `absent()` — the skip-unknown tolerant-read rule. Present iff a
|
|
273
|
+
non-empty `transcript_digest` was recorded (the digest is the load-bearing
|
|
274
|
+
field; a `checkpoint_turn` with no digest is not a usable anchor).
|
|
275
|
+
"""
|
|
276
|
+
digest = str(entry.get("transcript_digest") or "")
|
|
277
|
+
if not digest:
|
|
278
|
+
return cls.absent()
|
|
279
|
+
turn_raw = entry.get("checkpoint_turn")
|
|
280
|
+
try:
|
|
281
|
+
turn = int(turn_raw)
|
|
282
|
+
except (TypeError, ValueError):
|
|
283
|
+
turn = -1
|
|
284
|
+
return cls(turn_index=turn, transcript_digest=digest, present=True)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@dataclass(frozen=True)
|
|
288
|
+
class LedgerState:
|
|
289
|
+
"""The reconstructed intent of one run — `replay`'s output, `resume_plan`'s input.
|
|
290
|
+
|
|
291
|
+
The intent-ledger analogue of `lane_journal.replay`'s live-lease list: a pure
|
|
292
|
+
fold of the entry sequence into "what did this run DECLARE, and which steps did
|
|
293
|
+
the kernel VERIFY." Deliberately carries CLAIMED and VERIFIED separately — the
|
|
294
|
+
whole epistemic point (§3.2) is that they are not the same, and the resume fold
|
|
295
|
+
treats only VERIFIED as done.
|
|
296
|
+
|
|
297
|
+
run_id — the run this state describes (the ledger's key).
|
|
298
|
+
goal — the declared free-form goal string (the latest INTENT's).
|
|
299
|
+
plan / phase — the declared (plan, phase) if the run named one (else "").
|
|
300
|
+
start_sha — the run's declared start commit (the resume floor anchor).
|
|
301
|
+
declared_steps — the ordered step ids the INTENT declared (may be empty: a
|
|
302
|
+
run with a free-form goal and no enumerated steps).
|
|
303
|
+
step_regions — {step_id: (glob, …)} — each step's declared FILE REGION
|
|
304
|
+
(repo-relative globs). OPTIONAL: a step with no region falls
|
|
305
|
+
back to the non-empty-footprint check. When present, the
|
|
306
|
+
resume re-adjudication (`resume_evidence`) requires the
|
|
307
|
+
step's commit footprint to INTERSECT this region, closing the
|
|
308
|
+
§5 hole where a forged record points at a real-but-unrelated
|
|
309
|
+
commit (a commit outside the step's region isn't its work).
|
|
310
|
+
claimed — {step_id: claimed_sha} — the agent's self-reports
|
|
311
|
+
(DISTRUSTED; a pointer to a commit to check, not proof).
|
|
312
|
+
verified — {step_id: VerifiedStep} — steps the kernel CONFIRMED
|
|
313
|
+
against ancestry on the non-forgeable rung (TRUSTED).
|
|
314
|
+
suspended — True iff the run's last lifecycle record is a SUSPEND
|
|
315
|
+
(it parked voluntarily; §4) and no later INTENT re-opened it.
|
|
316
|
+
suspend_resume_sha— the resume-point SHA the SUSPEND recorded (a cheaper,
|
|
317
|
+
still-re-verified hint; "" if not suspended / not given).
|
|
318
|
+
suspend_checkpoint— the CONVERSATION rewind anchor the SUSPEND recorded (docs/164
|
|
319
|
+
F1.5): a `(turn_index, transcript_digest)` the kernel stamped,
|
|
320
|
+
read by `rewind.rewind_plan`. The sibling of `suspend_resume_sha`
|
|
321
|
+
on the SAME SUSPEND record (git axis reads the SHA, conversation
|
|
322
|
+
axis reads this). `absent()` if not suspended / an older kernel's
|
|
323
|
+
SUSPEND that stamped no checkpoint (the additive-evolution zero).
|
|
324
|
+
resume_proposed — predecessor run_ids a RESUME_PROPOSED was already minted
|
|
325
|
+
for (idempotence; §5 req 4): a second resume sees these and
|
|
326
|
+
does not double-propose.
|
|
327
|
+
corrupt_lines — count of `_CORRUPT`/`_UNREADABLE` sentinels seen (a
|
|
328
|
+
non-zero count is an integrity signal the resume verdict
|
|
329
|
+
degrades on — UNRESUMABLE when the fold isn't sound).
|
|
330
|
+
unreadable_newer — True iff ≥1 sentinel was an UNREADABLE_NEWER schema (a
|
|
331
|
+
record this kernel is too OLD to read): the §6 floor —
|
|
332
|
+
resume must refuse, not guess.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
run_id: str
|
|
336
|
+
goal: str = ""
|
|
337
|
+
plan: str = ""
|
|
338
|
+
phase: str = ""
|
|
339
|
+
start_sha: str = ""
|
|
340
|
+
declared_steps: tuple[str, ...] = ()
|
|
341
|
+
step_regions: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
342
|
+
claimed: dict[str, str] = field(default_factory=dict)
|
|
343
|
+
verified: dict[str, "VerifiedStep"] = field(default_factory=dict)
|
|
344
|
+
suspended: bool = False
|
|
345
|
+
suspend_resume_sha: str = ""
|
|
346
|
+
suspend_checkpoint: SuspendCheckpoint = field(default_factory=SuspendCheckpoint.absent)
|
|
347
|
+
resume_proposed: tuple[str, ...] = ()
|
|
348
|
+
corrupt_lines: int = 0
|
|
349
|
+
unreadable_newer: bool = False
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def has_intent(self) -> bool:
|
|
353
|
+
"""True iff at least one INTENT record was folded (a goal/plan/steps exist).
|
|
354
|
+
|
|
355
|
+
UNRESUMABLE's floor: with no INTENT there is no declared work to compute a
|
|
356
|
+
residual from — `resume_plan` returns UNRESUMABLE rather than guessing one.
|
|
357
|
+
"""
|
|
358
|
+
return bool(self.goal or self.plan or self.declared_steps)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@dataclass(frozen=True)
|
|
362
|
+
class VerifiedStep:
|
|
363
|
+
"""A step the kernel confirmed against ancestry (`STEP_VERIFIED`'s payload).
|
|
364
|
+
|
|
365
|
+
`sha` is the ancestry-backed commit; `via` names the verify RUNG that backed it
|
|
366
|
+
(`file-path`/`registry`/… — NEVER the forgeable subject-grep, §5 req 2);
|
|
367
|
+
`rungs`/`verdicts` echo the backing detail for forensics. This is the minted
|
|
368
|
+
belief — the only thing resume reads as "done."
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
step_id: str
|
|
372
|
+
sha: str
|
|
373
|
+
via: str = ""
|
|
374
|
+
verdicts: tuple[str, ...] = ()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def replay(entries: Iterable[dict]) -> LedgerState:
|
|
378
|
+
"""Fold the ledger sequence into a `LedgerState`. PURE — entries in, state out.
|
|
379
|
+
|
|
380
|
+
The intent-ledger redo fold (the third ARIES phase's input). Folding rules
|
|
381
|
+
(later records win for scalar fields; sets accumulate):
|
|
382
|
+
|
|
383
|
+
* INTENT → set goal/plan/phase/start_sha/declared_steps; a later
|
|
384
|
+
INTENT (a re-declared/re-opened run) overrides and clears
|
|
385
|
+
`suspended` (the run is live again).
|
|
386
|
+
* STEP_CLAIMED → record claimed[step_id] = claimed_sha (the distrusted
|
|
387
|
+
self-report — a pointer to a commit to check).
|
|
388
|
+
* STEP_VERIFIED → record verified[step_id] = VerifiedStep (the minted
|
|
389
|
+
belief; the ONLY "done" signal).
|
|
390
|
+
* SUSPEND → mark suspended + carry its recorded resume-point SHA.
|
|
391
|
+
* RESUME_PROPOSED → record the predecessor run_id (idempotence).
|
|
392
|
+
* _CORRUPT / _UNREADABLE / unknown → counted, never folded into intent (a
|
|
393
|
+
torn/foreign/too-new line must not mutate the
|
|
394
|
+
reconstructed goal — the lane-journal skip-unknown rule).
|
|
395
|
+
|
|
396
|
+
Returns a frozen `LedgerState`; `replay([])` is an empty state with
|
|
397
|
+
`has_intent == False` (the UNRESUMABLE floor for a run that declared nothing).
|
|
398
|
+
"""
|
|
399
|
+
run_id = ""
|
|
400
|
+
goal = ""
|
|
401
|
+
plan = ""
|
|
402
|
+
phase = ""
|
|
403
|
+
start_sha = ""
|
|
404
|
+
declared_steps: tuple[str, ...] = ()
|
|
405
|
+
step_regions: dict[str, tuple[str, ...]] = {}
|
|
406
|
+
claimed: dict[str, str] = {}
|
|
407
|
+
verified: dict[str, VerifiedStep] = {}
|
|
408
|
+
suspended = False
|
|
409
|
+
suspend_resume_sha = ""
|
|
410
|
+
suspend_checkpoint = SuspendCheckpoint.absent()
|
|
411
|
+
resume_proposed: list[str] = []
|
|
412
|
+
corrupt = 0
|
|
413
|
+
unreadable_newer = False
|
|
414
|
+
|
|
415
|
+
for e in entries:
|
|
416
|
+
op = str(e.get("op") or "")
|
|
417
|
+
rid = str(e.get("run_id") or "")
|
|
418
|
+
if rid:
|
|
419
|
+
run_id = rid
|
|
420
|
+
if op not in _FOLDED_OPS:
|
|
421
|
+
# _CORRUPT / _UNREADABLE / unknown — recorded, not folded.
|
|
422
|
+
if op == OP_CORRUPT:
|
|
423
|
+
corrupt += 1
|
|
424
|
+
un = e.get("_unreadable")
|
|
425
|
+
if isinstance(un, dict) and un.get("readability") == "UNREADABLE_NEWER":
|
|
426
|
+
unreadable_newer = True
|
|
427
|
+
continue
|
|
428
|
+
if op == OP_INTENT:
|
|
429
|
+
goal = str(e.get("goal") or goal)
|
|
430
|
+
plan = str(e.get("plan") or plan)
|
|
431
|
+
phase = str(e.get("phase") or phase)
|
|
432
|
+
start_sha = str(e.get("start_sha") or start_sha)
|
|
433
|
+
steps = e.get("declared_steps")
|
|
434
|
+
if isinstance(steps, (list, tuple)):
|
|
435
|
+
declared_steps = tuple(str(s) for s in steps)
|
|
436
|
+
regions = e.get("step_regions")
|
|
437
|
+
if isinstance(regions, dict):
|
|
438
|
+
step_regions = {
|
|
439
|
+
str(k): tuple(str(g) for g in v)
|
|
440
|
+
for k, v in regions.items()
|
|
441
|
+
if isinstance(v, (list, tuple))
|
|
442
|
+
}
|
|
443
|
+
# A fresh INTENT re-opens a parked run (it is live again).
|
|
444
|
+
suspended = False
|
|
445
|
+
suspend_resume_sha = ""
|
|
446
|
+
suspend_checkpoint = SuspendCheckpoint.absent()
|
|
447
|
+
elif op == OP_STEP_CLAIMED:
|
|
448
|
+
sid = str(e.get("step_id") or "")
|
|
449
|
+
if sid:
|
|
450
|
+
claimed[sid] = str(e.get("sha") or "")
|
|
451
|
+
elif op == OP_STEP_VERIFIED:
|
|
452
|
+
sid = str(e.get("step_id") or "")
|
|
453
|
+
if sid:
|
|
454
|
+
vds = e.get("verdicts")
|
|
455
|
+
verified[sid] = VerifiedStep(
|
|
456
|
+
step_id=sid,
|
|
457
|
+
sha=str(e.get("sha") or ""),
|
|
458
|
+
via=str(e.get("via") or ""),
|
|
459
|
+
verdicts=tuple(str(v) for v in vds) if isinstance(vds, (list, tuple)) else (),
|
|
460
|
+
)
|
|
461
|
+
elif op == OP_SUSPEND:
|
|
462
|
+
suspended = True
|
|
463
|
+
suspend_resume_sha = str(e.get("resume_sha") or e.get("sha") or "")
|
|
464
|
+
# The conversation-rewind anchor (docs/164 F1.5) — additive, tolerant of
|
|
465
|
+
# absence (an older kernel's SUSPEND folds to an absent checkpoint).
|
|
466
|
+
suspend_checkpoint = SuspendCheckpoint.from_record(e)
|
|
467
|
+
elif op == OP_RESUME_PROPOSED:
|
|
468
|
+
pred = str(e.get("predecessor_run_id") or e.get("predecessor") or "")
|
|
469
|
+
if pred and pred not in resume_proposed:
|
|
470
|
+
resume_proposed.append(pred)
|
|
471
|
+
|
|
472
|
+
return LedgerState(
|
|
473
|
+
run_id=run_id,
|
|
474
|
+
goal=goal,
|
|
475
|
+
plan=plan,
|
|
476
|
+
phase=phase,
|
|
477
|
+
start_sha=start_sha,
|
|
478
|
+
declared_steps=declared_steps,
|
|
479
|
+
step_regions=dict(step_regions),
|
|
480
|
+
claimed=dict(claimed),
|
|
481
|
+
verified=dict(verified),
|
|
482
|
+
suspended=suspended,
|
|
483
|
+
suspend_resume_sha=suspend_resume_sha,
|
|
484
|
+
suspend_checkpoint=suspend_checkpoint,
|
|
485
|
+
resume_proposed=tuple(resume_proposed),
|
|
486
|
+
corrupt_lines=corrupt,
|
|
487
|
+
unreadable_newer=unreadable_newer,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# --------------------------------------------------------------------------
|
|
492
|
+
# Entry builders — the writer's vocabulary, defined HERE (one home), pure.
|
|
493
|
+
# Each carries the §6 schema tag so even a record written directly (not via
|
|
494
|
+
# `append`) is self-declaring.
|
|
495
|
+
# --------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def intent_entry(
|
|
499
|
+
*,
|
|
500
|
+
goal: str = "",
|
|
501
|
+
plan: str = "",
|
|
502
|
+
phase: str = "",
|
|
503
|
+
start_sha: str = "",
|
|
504
|
+
declared_steps: Iterable[str] | None = None,
|
|
505
|
+
step_regions: "dict[str, Iterable[str]] | None" = None,
|
|
506
|
+
env: "Mapping | None" = None,
|
|
507
|
+
) -> dict:
|
|
508
|
+
"""Build an INTENT entry — a run declaring its goal (at spawn / first dispatch).
|
|
509
|
+
|
|
510
|
+
`goal` is a free-form intent string; `plan`/`phase` the structured target if one
|
|
511
|
+
exists; `start_sha` the run's start commit (the resume floor anchor); `declared_steps`
|
|
512
|
+
the ordered step ids the run means to complete (may be omitted — a free-form goal).
|
|
513
|
+
`step_regions` (OPTIONAL) maps a step id → its file region (repo-relative globs): at
|
|
514
|
+
resume, a step's verifying commit footprint must INTERSECT this region (§5, the
|
|
515
|
+
real-but-unrelated-commit defense). A step with no region falls back to the
|
|
516
|
+
non-empty-footprint check. Believed AS A CLAIM at resume (§3.2): the residual is
|
|
517
|
+
computed from it, but every "done" is re-verified.
|
|
518
|
+
|
|
519
|
+
`env` (OPTIONAL) is the run's environment print — ``cfg.env.to_dict()`` (an
|
|
520
|
+
`env_print.EnvPrint`), recorded at birth so the fossil says *under what* the run
|
|
521
|
+
declared its intent (``docs/115`` primitive 1: kernel version + SHA + Python +
|
|
522
|
+
OS + declared tools). Purely ADDITIVE — an INTENT with no `env` is a run from a
|
|
523
|
+
kernel that did not stamp prints, read back unchanged (the additive-evolution
|
|
524
|
+
contract: a new optional field never bumps `INTENT_LEDGER_SCHEMA`). The kernel
|
|
525
|
+
RECORDS it; it does not yet adjudicate on it (a later phase reads env-divergence
|
|
526
|
+
as a resume signal). The print is data, not a decision input — the docs/76 line.
|
|
527
|
+
"""
|
|
528
|
+
e = {
|
|
529
|
+
**_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
|
|
530
|
+
"op": OP_INTENT,
|
|
531
|
+
"goal": goal,
|
|
532
|
+
"plan": plan,
|
|
533
|
+
"phase": phase,
|
|
534
|
+
"start_sha": start_sha,
|
|
535
|
+
}
|
|
536
|
+
if declared_steps is not None:
|
|
537
|
+
e["declared_steps"] = [str(s) for s in declared_steps]
|
|
538
|
+
if step_regions is not None:
|
|
539
|
+
e["step_regions"] = {str(k): [str(g) for g in v] for k, v in step_regions.items()}
|
|
540
|
+
if env is not None:
|
|
541
|
+
e["env"] = dict(env)
|
|
542
|
+
return e
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def step_claimed_entry(step_id: str, sha: str) -> dict:
|
|
546
|
+
"""Build a STEP_CLAIMED entry — the agent SAYS it finished a step (forgeable).
|
|
547
|
+
|
|
548
|
+
`sha` is the commit the agent CLAIMS landed the step. Never believed on its own
|
|
549
|
+
(§3.2): a pointer to a commit to check, not proof. The `STEP_VERIFIED` mint is
|
|
550
|
+
what turns a claim into a belief.
|
|
551
|
+
"""
|
|
552
|
+
return {
|
|
553
|
+
**_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
|
|
554
|
+
"op": OP_STEP_CLAIMED,
|
|
555
|
+
"step_id": str(step_id),
|
|
556
|
+
"sha": str(sha or ""),
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def step_verified_entry(step_id: str, sha: str, *, via: str = "",
|
|
561
|
+
verdicts: Iterable[str] | None = None) -> dict:
|
|
562
|
+
"""Build a STEP_VERIFIED entry — the kernel CONFIRMED a claimed step (§5).
|
|
563
|
+
|
|
564
|
+
Written ONLY by the CLI-boundary mint (`dos.resume.verify_step` / the dispatch
|
|
565
|
+
loop) after re-checking the claimed SHA against ancestry on the NON-FORGEABLE
|
|
566
|
+
rung (§5 req 2: `via` is `file-path`/`registry`, never the forgeable
|
|
567
|
+
subject-grep). The minted belief — the only "done" resume reads.
|
|
568
|
+
"""
|
|
569
|
+
return {
|
|
570
|
+
**_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
|
|
571
|
+
"op": OP_STEP_VERIFIED,
|
|
572
|
+
"step_id": str(step_id),
|
|
573
|
+
"sha": str(sha or ""),
|
|
574
|
+
"via": str(via or ""),
|
|
575
|
+
"verdicts": [str(v) for v in verdicts] if verdicts is not None else [],
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def suspend_entry(*, reason: str = "", resume_sha: str = "",
|
|
580
|
+
residual: Iterable[str] | None = None,
|
|
581
|
+
checkpoint: "SuspendCheckpoint | None" = None) -> dict:
|
|
582
|
+
"""Build a SUSPEND entry — a run voluntarily yields (pause; §4).
|
|
583
|
+
|
|
584
|
+
`resume_sha` is the recorded resume point at suspend time (a cheaper hint than a
|
|
585
|
+
full re-derivation — but still re-verified at resume, since a suspend an hour ago
|
|
586
|
+
may be stale). `residual` is the remaining step ids at suspend time (forensic).
|
|
587
|
+
Believed as a recorded DECISION (not a progress claim) — but the resume still
|
|
588
|
+
re-checks ancestry (§4).
|
|
589
|
+
|
|
590
|
+
`checkpoint` (OPTIONAL — docs/164 F1.5) is the CONVERSATION rewind anchor: a
|
|
591
|
+
`(turn_index, transcript_digest)` the kernel stamped at suspend time, the
|
|
592
|
+
sibling of the git-axis `resume_sha`. The SAME SUSPEND record carries both —
|
|
593
|
+
the git-rewind axis (`resume.resume_plan`) reads `resume_sha`, the
|
|
594
|
+
conversation-rewind axis (`rewind.rewind_plan`) reads these two fields. Written
|
|
595
|
+
only when present, as two additive fields `"checkpoint_turn"` + `"transcript_digest"`.
|
|
596
|
+
PURELY ADDITIVE: a SUSPEND from an older kernel that wrote no checkpoint reads
|
|
597
|
+
back unchanged (the additive-evolution contract above — a new optional field
|
|
598
|
+
never bumps `INTENT_LEDGER_SCHEMA`), and a kernel too OLD to know the fields
|
|
599
|
+
simply ignores them (the skip-unknown tolerant-read rule). The digest is the
|
|
600
|
+
NON-FORGEABLE rewind anchor: the kernel rewinds to a turn IT stamped here, never
|
|
601
|
+
to a turn the agent claims (the §6 conversation-axis litmus).
|
|
602
|
+
"""
|
|
603
|
+
e = {
|
|
604
|
+
**_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
|
|
605
|
+
"op": OP_SUSPEND,
|
|
606
|
+
"reason": str(reason or ""),
|
|
607
|
+
"resume_sha": str(resume_sha or ""),
|
|
608
|
+
}
|
|
609
|
+
if residual is not None:
|
|
610
|
+
e["residual"] = [str(s) for s in residual]
|
|
611
|
+
if checkpoint is not None:
|
|
612
|
+
# Two additive fields, written only when a checkpoint was minted. The
|
|
613
|
+
# conversation axis reads these; the git axis reads `resume_sha` above.
|
|
614
|
+
e["checkpoint_turn"] = int(checkpoint.turn_index)
|
|
615
|
+
e["transcript_digest"] = str(checkpoint.transcript_digest or "")
|
|
616
|
+
return e
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def resume_proposed_entry(*, predecessor_run_id: str, resume_sha: str = "",
|
|
620
|
+
residual: Iterable[str] | None = None) -> dict:
|
|
621
|
+
"""Build a RESUME_PROPOSED entry — a successor minted a resume point + proposed continuation (§5).
|
|
622
|
+
|
|
623
|
+
Recorded on the SUCCESSOR's ledger for forensics + idempotence (§5 req 4): a
|
|
624
|
+
second resume attempt sees this predecessor already proposed-for and does not
|
|
625
|
+
double-propose. `predecessor_run_id` is the dead/parked run being resumed.
|
|
626
|
+
"""
|
|
627
|
+
e = {
|
|
628
|
+
**_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
|
|
629
|
+
"op": OP_RESUME_PROPOSED,
|
|
630
|
+
"predecessor_run_id": str(predecessor_run_id),
|
|
631
|
+
"resume_sha": str(resume_sha or ""),
|
|
632
|
+
}
|
|
633
|
+
if residual is not None:
|
|
634
|
+
e["residual"] = [str(s) for s in residual]
|
|
635
|
+
return e
|