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/claim_extract.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""claim_extract — what (plan, phase) did an agent CLAIM it shipped? (docs/134 §2.1)
|
|
2
|
+
|
|
3
|
+
The crux of the runtime binding: a `Stop` hook is handed a *transcript*, but the
|
|
4
|
+
truth syscall (`verify`) wants `(plan, phase)`. This module is that bridge — it
|
|
5
|
+
reads what an agent *asserted* it finished, so the hook can check each assertion
|
|
6
|
+
against git. It does NOT verify anything; it only extracts the *claims* to be
|
|
7
|
+
verified (the oracle does the verifying).
|
|
8
|
+
|
|
9
|
+
Three rungs, strongest-first, mirroring the oracle's own evidence ladder:
|
|
10
|
+
|
|
11
|
+
1. **MARKER** (strongest, opt-in): the agent ended a unit with a machine line
|
|
12
|
+
``DOS-CLAIM: <plan> <phase>``. Lifted byte-exactly. This is where the
|
|
13
|
+
operator's literal "@verify-style marker" lives — the agent *declaring what
|
|
14
|
+
to check*, not a directive DOS executes.
|
|
15
|
+
2. **FRONTMATTER** (structural): a skill whose frontmatter declares
|
|
16
|
+
``dos.plan``/``dos.phase`` makes the claim known without parsing prose. That
|
|
17
|
+
rung lives at the hook boundary (the firing skill is known there), exposed
|
|
18
|
+
here as ``claim_from_frontmatter`` so both paths return the same ``Claim``.
|
|
19
|
+
3. **HEURISTIC** (weakest, ABSTAINING): absent a marker, scan a "shipped/landed
|
|
20
|
+
/done <ID>" sentence for an explicit plan/phase-shaped token. This rung's
|
|
21
|
+
ONLY failure mode is a *missed* claim (the agent then stops unverified — the
|
|
22
|
+
safe direction); it must NEVER fabricate a `(plan, phase)`, because a
|
|
23
|
+
`verify` run against a hallucinated claim would make the verifier itself the
|
|
24
|
+
unreliable narrator it exists to catch (docs/103, inward).
|
|
25
|
+
|
|
26
|
+
The load-bearing rule, stated once: **abstain, never invent.** Free prose ("I'm
|
|
27
|
+
done", "shipped the auth work") yields NO claim — there is no way to know the
|
|
28
|
+
plan/phase *identifiers* from prose without inventing them, so the heuristic only
|
|
29
|
+
fires on an explicit ID-shaped token and is marked low-confidence. A design that
|
|
30
|
+
pretends prose alone is enough is hand-waving (docs/134 §2.1).
|
|
31
|
+
|
|
32
|
+
Shape follows ``liveness.classify`` / ``git_delta``: the **pure** extractor
|
|
33
|
+
(``extract_claims(text, policy) -> list[Claim]``) operates on already-read text;
|
|
34
|
+
the **boundary reader** (``assistant_text_from_transcript``) does the file/JSON
|
|
35
|
+
I/O at the call site, never inside the pure core. The transcript-parsing
|
|
36
|
+
convention (``message.content`` list of blocks, text from ``block["text"]``)
|
|
37
|
+
mirrors ``scripts/trajectory_audit.py`` so the two readers cannot drift.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
import re
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# The explicit marker an agent (or skill) emits to declare a verifiable claim.
|
|
48
|
+
# Byte-exact, anchored at a line start (after optional list/quote markup) so a
|
|
49
|
+
# mention *inside* prose ("emit a DOS-CLAIM: line") is not mistaken for a real
|
|
50
|
+
# one — only a line that IS the marker counts.
|
|
51
|
+
_MARKER_RE = re.compile(
|
|
52
|
+
r"""^[ \t>*\-]* # optional leading markup (blockquote, list bullet)
|
|
53
|
+
DOS-CLAIM:[ \t]+ # the literal marker
|
|
54
|
+
(?P<plan>[^\s]+) # plan token (no whitespace)
|
|
55
|
+
[ \t]+
|
|
56
|
+
(?P<phase>[^\s]+) # phase token (no whitespace)
|
|
57
|
+
[ \t]*$ # nothing else on the line
|
|
58
|
+
""",
|
|
59
|
+
re.VERBOSE | re.MULTILINE,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# A plan/phase-shaped identifier for the HEURISTIC rung: an uppercase-led token
|
|
63
|
+
# of LETTERS then DIGITS (AUTH2, FQ390, DLA3) — the shape this codebase's phases
|
|
64
|
+
# actually take. Deliberately narrow: it will MISS a lowercased or prose-only
|
|
65
|
+
# claim (safe — abstain) rather than guess. It never matches a bare English word
|
|
66
|
+
# (no digits) so "done" / "shipped" alone yield nothing.
|
|
67
|
+
_PHASE_TOKEN_RE = re.compile(r"\b([A-Z][A-Z_]*[A-Z])(\d+)\b")
|
|
68
|
+
|
|
69
|
+
# The completion verbs that gate the heuristic rung — the phase-shaped token must
|
|
70
|
+
# sit in a sentence that actually CLAIMS completion, not merely mentions the id.
|
|
71
|
+
_COMPLETION_HINT_RE = re.compile(
|
|
72
|
+
r"\b(shipped|landed|completed|finished|done|merged)\b", re.IGNORECASE
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class Claim:
|
|
78
|
+
"""One (plan, phase) an agent claimed shipped, plus how we know.
|
|
79
|
+
|
|
80
|
+
``rung`` is ``marker`` › ``frontmatter`` › ``heuristic`` (strongest-first),
|
|
81
|
+
the same provenance discipline as the oracle's ``source`` tag. ``raw`` is the
|
|
82
|
+
text the claim was lifted from (the marker line, or the sentence), for an
|
|
83
|
+
auditable trail. ``confident`` is False only for the heuristic rung — a
|
|
84
|
+
caller may choose to treat a low-confidence claim as advisory.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
plan: str
|
|
88
|
+
phase: str
|
|
89
|
+
rung: str
|
|
90
|
+
raw: str = ""
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def confident(self) -> bool:
|
|
94
|
+
return self.rung in ("marker", "frontmatter")
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict:
|
|
97
|
+
return {
|
|
98
|
+
"plan": self.plan,
|
|
99
|
+
"phase": self.phase,
|
|
100
|
+
"rung": self.rung,
|
|
101
|
+
"raw": self.raw,
|
|
102
|
+
"confident": self.confident,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def claim_from_frontmatter(plan: str | None, phase: str | None) -> list[Claim]:
|
|
107
|
+
"""The FRONTMATTER rung: a skill declared (dos.plan, dos.phase).
|
|
108
|
+
|
|
109
|
+
Returns a single-element list when both are present, else empty. Pure — the
|
|
110
|
+
hook reads the frontmatter at the boundary and passes the two strings in.
|
|
111
|
+
"""
|
|
112
|
+
plan = (plan or "").strip()
|
|
113
|
+
phase = (phase or "").strip()
|
|
114
|
+
if plan and phase:
|
|
115
|
+
return [Claim(plan=plan, phase=phase, rung="frontmatter",
|
|
116
|
+
raw=f"dos.plan={plan} dos.phase={phase}")]
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def extract_claims(text: str, *, allow_heuristic: bool = True) -> list[Claim]:
|
|
121
|
+
"""The PURE extractor: claims an agent asserted, strongest rung first.
|
|
122
|
+
|
|
123
|
+
Operates on already-read assistant text (the boundary reader hands it over).
|
|
124
|
+
No I/O. Deterministic. Deduplicates on (plan, phase), keeping the strongest
|
|
125
|
+
rung. ``allow_heuristic=False`` restricts to the byte-exact MARKER rung — the
|
|
126
|
+
fail-closed posture a strict caller wants (only act on what the agent
|
|
127
|
+
explicitly declared).
|
|
128
|
+
|
|
129
|
+
Returns ``[]`` when nothing is confidently extractable — the abstain floor.
|
|
130
|
+
"""
|
|
131
|
+
if not text:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
out: dict[tuple[str, str], Claim] = {}
|
|
135
|
+
|
|
136
|
+
# Rung 1 — the byte-exact marker. Strongest; always honored.
|
|
137
|
+
for m in _MARKER_RE.finditer(text):
|
|
138
|
+
plan, phase = m.group("plan"), m.group("phase")
|
|
139
|
+
out[(plan, phase)] = Claim(plan=plan, phase=phase, rung="marker",
|
|
140
|
+
raw=m.group(0).strip())
|
|
141
|
+
|
|
142
|
+
if not allow_heuristic:
|
|
143
|
+
return list(out.values())
|
|
144
|
+
|
|
145
|
+
# Rung 3 — the abstaining heuristic. Only fires when a phase-SHAPED token
|
|
146
|
+
# (AUTH2) sits in a sentence that also carries a completion verb. This never
|
|
147
|
+
# invents an id from prose: no ID-shaped token ⇒ no claim. A token already
|
|
148
|
+
# captured by the marker rung is not downgraded.
|
|
149
|
+
for line in text.splitlines():
|
|
150
|
+
if not _COMPLETION_HINT_RE.search(line):
|
|
151
|
+
continue
|
|
152
|
+
for tok in _PHASE_TOKEN_RE.finditer(line):
|
|
153
|
+
phase = tok.group(0) # e.g. "AUTH2"
|
|
154
|
+
plan = tok.group(1) # the letter stem, e.g. "AUTH"
|
|
155
|
+
key = (plan, phase)
|
|
156
|
+
if key in out:
|
|
157
|
+
continue # don't shadow a stronger rung
|
|
158
|
+
out[key] = Claim(plan=plan, phase=phase, rung="heuristic",
|
|
159
|
+
raw=line.strip())
|
|
160
|
+
|
|
161
|
+
return list(out.values())
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
# Boundary I/O — the transcript reader. NOT pure (reads a file); kept here so the
|
|
166
|
+
# extractor's caller has one home for the read, the git_delta discipline.
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
def _text_blocks(content: object) -> list[str]:
|
|
169
|
+
"""Pull text from a message `content` (a str, or a list of typed blocks).
|
|
170
|
+
|
|
171
|
+
Mirrors scripts/trajectory_audit.py so the two transcript readers can't drift.
|
|
172
|
+
"""
|
|
173
|
+
if isinstance(content, str):
|
|
174
|
+
return [content]
|
|
175
|
+
if isinstance(content, list):
|
|
176
|
+
texts: list[str] = []
|
|
177
|
+
for b in content:
|
|
178
|
+
if isinstance(b, dict) and b.get("type") == "text":
|
|
179
|
+
t = b.get("text", "")
|
|
180
|
+
if isinstance(t, str) and t:
|
|
181
|
+
texts.append(t)
|
|
182
|
+
return texts
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def assistant_text_from_transcript(path: str, *, last_turns: int = 1) -> str:
|
|
187
|
+
"""Read the text of the last N assistant turn(s) from a transcript JSONL.
|
|
188
|
+
|
|
189
|
+
The Stop hook is told to verify "what the agent just claimed," so we read the
|
|
190
|
+
*tail* — the final assistant turn(s) — not the whole session (an earlier,
|
|
191
|
+
superseded claim must not re-trigger). Returns the concatenated text, or ``""``
|
|
192
|
+
on any read/parse failure (the no-crash floor: a missing/garbled transcript
|
|
193
|
+
yields no claims, the agent stops unverified — the safe direction).
|
|
194
|
+
"""
|
|
195
|
+
if last_turns < 1:
|
|
196
|
+
last_turns = 1
|
|
197
|
+
try:
|
|
198
|
+
lines = _read_lines(path)
|
|
199
|
+
except OSError:
|
|
200
|
+
return ""
|
|
201
|
+
|
|
202
|
+
# Collect assistant-turn texts in order, then keep the last N.
|
|
203
|
+
turns: list[str] = []
|
|
204
|
+
for raw in lines:
|
|
205
|
+
raw = raw.strip()
|
|
206
|
+
if not raw:
|
|
207
|
+
continue
|
|
208
|
+
try:
|
|
209
|
+
obj = json.loads(raw)
|
|
210
|
+
except (ValueError, TypeError):
|
|
211
|
+
continue
|
|
212
|
+
if not isinstance(obj, dict):
|
|
213
|
+
continue
|
|
214
|
+
msg = obj.get("message")
|
|
215
|
+
if not isinstance(msg, dict) or msg.get("role") != "assistant":
|
|
216
|
+
continue
|
|
217
|
+
blocks = _text_blocks(msg.get("content"))
|
|
218
|
+
if blocks:
|
|
219
|
+
turns.append("\n".join(blocks))
|
|
220
|
+
|
|
221
|
+
if not turns:
|
|
222
|
+
return ""
|
|
223
|
+
return "\n".join(turns[-last_turns:])
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _read_lines(path: str) -> list[str]:
|
|
227
|
+
"""Read a transcript file's lines (split out so a test can monkeypatch I/O)."""
|
|
228
|
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
229
|
+
return fh.readlines()
|
dos/claim_ttl.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Claim TTL / kind / status math — pure branch logic over scalars.
|
|
2
|
+
|
|
3
|
+
Lifted from the job userland's ``scripts/fanout_state.py`` (MQ3X P1, docs/62).
|
|
4
|
+
The kernel half of the OS7 per-status TTL model: given a claim's
|
|
5
|
+
``(status, kind, expected_wallclock)`` scalars (and a frozen ``TtlPolicy`` of the
|
|
6
|
+
host's tuning), compute how long the claim lives. Plus the legacy-row inference
|
|
7
|
+
(``infer_kind`` / ``infer_status``) the ``stats`` / ``disambiguate`` verbs use to
|
|
8
|
+
label un-migrated rows consistently.
|
|
9
|
+
|
|
10
|
+
``dos`` carries **mechanism, not policy** (the package-wide invariant): the OS7
|
|
11
|
+
minute constants are job *tuning*, so they live on a ``TtlPolicy`` dataclass the
|
|
12
|
+
CALLER supplies — exactly the ``LivenessPolicy`` / ``loop_decide`` thresholds
|
|
13
|
+
split. The defaults below match the job's historical values so a caller that
|
|
14
|
+
passes ``DEFAULT_POLICY`` (or nothing) is byte-identical to the pre-lift code.
|
|
15
|
+
|
|
16
|
+
Purity boundary (docs/62 §0): every function here takes scalars and returns
|
|
17
|
+
scalars — zero ``datetime.now`` / ``os`` / ``Path`` / ``open`` / ``yaml`` /
|
|
18
|
+
``importlib``. The three job-side functions that read the CLOCK
|
|
19
|
+
(``_compute_expires_at`` / ``_claim_heartbeat_expired`` / ``_is_working_claim_fresh``)
|
|
20
|
+
keep their clock-reading WRAPPER in ``agents/leases/`` and delegate their decision
|
|
21
|
+
to the ``now``-injected pure cores here (``expires_at_from`` is the first; the
|
|
22
|
+
others land with the P3 io-layer move).
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import datetime as _dt
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
|
|
29
|
+
# Claim taxonomy — the closed value sets. Carried with the inference functions
|
|
30
|
+
# because they decide which inferred label is "valid" (explicit field wins).
|
|
31
|
+
VALID_CLAIM_KINDS = ("soft", "hard", "agent_in_session")
|
|
32
|
+
VALID_CLAIM_STATUSES = ("working", "awaiting_decision", "awaiting_commit", "stale", "done")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class TtlPolicy:
|
|
37
|
+
"""OS7 per-status TTL tuning — policy, not mechanism (job-side data).
|
|
38
|
+
|
|
39
|
+
The single 90-min TTL was wrong for all three claim kinds; TTL now matches the
|
|
40
|
+
kind's lifecycle. ``None`` resolved minutes = infinity (no ``claim_expires_at``
|
|
41
|
+
written). The defaults reproduce the job's historical constants exactly:
|
|
42
|
+
|
|
43
|
+
awaiting_commit_minutes — 24h, drives the OS2 auto-archive cadence.
|
|
44
|
+
agent_in_session_minutes — 6h, one operator session (overrides status).
|
|
45
|
+
default_working_wallclock_minutes — used when ``expected_wallclock`` is absent
|
|
46
|
+
(matches pre-OS7 ``--ttl-minutes 90`` callsites).
|
|
47
|
+
working_ttl_multiplier — working TTL = ``expected_wallclock × multiplier``.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
awaiting_commit_minutes: int = 24 * 60
|
|
51
|
+
agent_in_session_minutes: int = 6 * 60
|
|
52
|
+
default_working_wallclock_minutes: int = 30
|
|
53
|
+
working_ttl_multiplier: int = 3
|
|
54
|
+
|
|
55
|
+
def __post_init__(self) -> None:
|
|
56
|
+
for name in (
|
|
57
|
+
"awaiting_commit_minutes", "agent_in_session_minutes",
|
|
58
|
+
"default_working_wallclock_minutes", "working_ttl_multiplier",
|
|
59
|
+
):
|
|
60
|
+
if getattr(self, name) < 0:
|
|
61
|
+
raise ValueError(f"TtlPolicy.{name} must be non-negative")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
DEFAULT_POLICY = TtlPolicy()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def infer_kind(
|
|
68
|
+
entry: dict,
|
|
69
|
+
dispatcher_kinds: tuple[tuple[str, str], ...] = (),
|
|
70
|
+
) -> str:
|
|
71
|
+
"""Infer claim_kind for legacy rows that pre-date the 2026-05-13 schema
|
|
72
|
+
addition. Used by ``disambiguate`` and ``stats`` so the operator sees a
|
|
73
|
+
consistent kind label even on un-migrated rows. Never written back to disk;
|
|
74
|
+
the explicit field on new rows takes precedence.
|
|
75
|
+
|
|
76
|
+
``dispatcher_kinds`` is the host's ``(dispatched_by-prefix, claim_kind)`` map for
|
|
77
|
+
the legacy fallback — the prefixes name a host's dispatcher SKILLS (e.g. the
|
|
78
|
+
reference app's ``fanout-`` → ``hard`` / ``next-up-`` → ``soft``), so the kernel
|
|
79
|
+
hardcodes NONE of them (userland-coupling audit 2026-06-08): the host passes its
|
|
80
|
+
own. Pairs are tried in order; the first matching prefix wins. Empty (the kernel
|
|
81
|
+
default) means a row with no explicit kind / TTL is simply ``unknown``."""
|
|
82
|
+
if entry.get("claim_kind") in VALID_CLAIM_KINDS:
|
|
83
|
+
return entry["claim_kind"]
|
|
84
|
+
if entry.get("claim_expires_at"):
|
|
85
|
+
return "soft"
|
|
86
|
+
by = (entry.get("dispatched_by") or "").lower()
|
|
87
|
+
for prefix, kind in dispatcher_kinds:
|
|
88
|
+
if by.startswith(prefix.lower()):
|
|
89
|
+
return kind
|
|
90
|
+
return "unknown"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def infer_status(entry: dict) -> str:
|
|
94
|
+
"""Infer claim_status. Returns 'working' for in_progress rows lacking explicit
|
|
95
|
+
claim_status; the precise working/stale split needs heartbeat substrate."""
|
|
96
|
+
if entry.get("claim_status") in VALID_CLAIM_STATUSES:
|
|
97
|
+
return entry["claim_status"]
|
|
98
|
+
if entry.get("status") == "in_progress":
|
|
99
|
+
return "working"
|
|
100
|
+
return entry.get("status", "unknown")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def expected_wallclock(claim_kind: str, ttl_minutes: int | None) -> int:
|
|
104
|
+
"""Default expected wallclock per claim_kind. Used by the stale detector when
|
|
105
|
+
an explicit ``expected_wallclock_minutes`` isn't set on the row."""
|
|
106
|
+
if ttl_minutes:
|
|
107
|
+
return ttl_minutes
|
|
108
|
+
if claim_kind == "soft":
|
|
109
|
+
return 90
|
|
110
|
+
if claim_kind == "agent_in_session":
|
|
111
|
+
return 60 # in-conversation agents tend to be quicker
|
|
112
|
+
return 360 # hard fanout default — agents can take up to a few hours
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def resolve_ttl_minutes(
|
|
116
|
+
claim_status: str, claim_kind: str,
|
|
117
|
+
expected_wallclock_minutes: int | None,
|
|
118
|
+
policy: TtlPolicy = DEFAULT_POLICY,
|
|
119
|
+
) -> int | None:
|
|
120
|
+
"""OS7 — per-status TTL formula. Returns minutes until claim expiry, or
|
|
121
|
+
``None`` for infinity (no TTL field written).
|
|
122
|
+
|
|
123
|
+
Resolution order:
|
|
124
|
+
1. claim_kind=agent_in_session → 6h (in-conversation agents shouldn't linger
|
|
125
|
+
past one operator session; overrides status formula).
|
|
126
|
+
2. claim_status=working → expected_wallclock × multiplier (defaults to
|
|
127
|
+
``policy.default_working_wallclock_minutes`` when expected_wallclock is
|
|
128
|
+
absent — matches pre-OS7 behaviour for legacy ``--ttl-minutes 90``).
|
|
129
|
+
3. claim_status=awaiting_commit → 24h (drives OS2 auto-archive).
|
|
130
|
+
4. claim_status=awaiting_decision / stale → None (operator drives resolution).
|
|
131
|
+
"""
|
|
132
|
+
if claim_kind == "agent_in_session":
|
|
133
|
+
return policy.agent_in_session_minutes
|
|
134
|
+
if claim_status == "working":
|
|
135
|
+
ew = expected_wallclock_minutes or policy.default_working_wallclock_minutes
|
|
136
|
+
return ew * policy.working_ttl_multiplier
|
|
137
|
+
if claim_status == "awaiting_commit":
|
|
138
|
+
return policy.awaiting_commit_minutes
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def expires_at_from(now: _dt.datetime, ttl_minutes: int | None) -> str | None:
|
|
143
|
+
"""Pure core of ``_compute_expires_at``: convert a TTL in minutes (or
|
|
144
|
+
None=infinity) to an ISO ``%Y-%m-%dT%H:%MZ`` timestamp, given an injected
|
|
145
|
+
``now``. The clock-reading wrapper (``agents/leases/ttl.py``) supplies
|
|
146
|
+
``dt.datetime.now(dt.timezone.utc)``; this stays pure + replay-testable."""
|
|
147
|
+
if ttl_minutes is None:
|
|
148
|
+
return None
|
|
149
|
+
expiry = now + _dt.timedelta(minutes=ttl_minutes)
|
|
150
|
+
return expiry.strftime("%Y-%m-%dT%H:%MZ")
|